chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)

为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。
下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与
api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动,
既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。

本提交只做归档,不改行为:
- 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理)
- 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口
- 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传)
- 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
Vicky committed 2026-10-15 13:28:01 +08:00
1 parent 40546bfd01
commit 44c2c46a68
59 files changed
+4082 -982

No files matched your search

@@ -1,192 +1,165 @@
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { customMenus, saveCustomMenus } from '../../../data/customMenus'
import { useRbac } from '../../../stores/rbac'
import router, { registerCustomMenu } from '../../../router'
import { createMenuApi, deleteMenuApi, getMenuTreeApi, updateMenuApi } from '@/api/admin/role'
const builtInMenus = [
{
id: 'builtin-content',
label: '内容管理',
children: [
{
id: 'builtin-dramas',
label: '短剧管理',
route: '/content/dramas',
permission: '查看短剧列表'
},
{
id: 'builtin-episodes',
label: '剧集管理',
route: '/content/episodes',
permission: '查看剧集列表'
}
]
},
{
id: 'builtin-admin',
label: '后台管理',
children: [
{
id: 'builtin-access',
label: '角色权限配置',
route: '/system/access',
permission: '角色与权限配置'
},
{
id: 'builtin-users',
label: '管理员用户列表',
route: '/system/admin-users',
permission: '管理员用户列表'
},
{ id: 'builtin-menus', label: '菜单管理', route: '/system/menus', permission: '菜单管理' },
{
id: 'builtin-categories',
label: '角色分类',
route: '/system/role-categories',
permission: '角色分类管理'
}
]
}
export const menuIconNames = [
'Document',
'DataAnalysis',
'Picture',
'User',
'Setting',
'VideoCamera',
'Menu',
'Grid'
]
function buildCustomNodes(parent) {
return customMenus
.filter((menu) => (menu.parent || '后台管理') === parent)
.map((menu) => ({
id: menu.key,
label: menu.title,
menu,
children: buildCustomNodes(menu.title)
}))
}
/** 菜单管理:直接读写后端 sys_menu,变更后重新登录生效。 */
export function useMenuManagement() {
const { createPermission, removePermission } = useRbac()
const selectedKey = ref('')
const selectedParent = ref('后台管理')
const form = reactive({
key: '',
title: '',
route: '',
permission: '',
parent: '后台管理',
type: '页面',
icon: 'Document',
method: '',
sort: 1
})
const menuTree = computed(() =>
builtInMenus.map((group) => ({
...group,
children: [...group.children, ...buildCustomNodes(group.label)]
}))
)
const parentOptions = computed(() => [
'内容管理',
'后台管理',
...customMenus.filter((menu) => menu.type !== '按钮').map((menu) => menu.title)
])
const menuTree = ref([])
const loading = ref(false)
function resetForm(parent = selectedParent.value) {
selectedKey.value = ''
const form = reactive({
id: null,
parentId: 0,
title: '',
routePath: '',
routeName: '',
component: '',
icon: '',
permissionCode: '',
type: 'MENU',
sort: 1,
visible: 1,
status: 1
})
function mapNodes(nodes) {
return (nodes || []).map((node) => ({
id: node.id,
label: node.meta?.title || node.path || String(node.id),
raw: node,
children: node.children?.length ? mapNodes(node.children) : undefined
}))
}
const treeData = computed(() => mapNodes(menuTree.value))
const parentOptions = computed(() => {
const options = [{ id: 0, label: '顶级菜单' }]
const walk = (nodes, depth) => {
;(nodes || []).forEach((node) => {
if (node.type !== 'BUTTON') {
options.push({
id: node.id,
label: `${' '.repeat(depth)}${node.meta?.title || node.path || node.id}`
})
}
if (node.children?.length) walk(node.children, depth + 1)
})
}
walk(menuTree.value, 0)
return options
})
async function load() {
loading.value = true
try {
const res = await getMenuTreeApi()
menuTree.value = res.data || []
} finally {
loading.value = false
}
}
function resetForm(parentId = 0) {
Object.assign(form, {
key: '',
id: null,
parentId,
title: '',
route: '',
permission: '',
parent,
type: '页面',
icon: 'Document',
method: '',
sort: 1
routePath: '',
routeName: '',
component: '',
icon: '',
permissionCode: '',
type: 'MENU',
sort: 1,
visible: 1,
status: 1
})
}
function selectNode(node) {
if (node.menu) {
selectedKey.value = node.menu.key
selectedParent.value = node.menu.parent || '后台管理'
Object.assign(form, {
...node.menu,
route: node.menu.route || `/custom/${node.menu.key}`,
permission: node.menu.permission || `访问菜单:${node.menu.title}`,
type: node.menu.type || '页面',
method: node.menu.method || '',
sort: node.menu.sort || 1
})
} else if (node.children) {
selectedParent.value = node.label
resetForm(node.label)
}
const raw = node.raw || {}
Object.assign(form, {
id: raw.id,
parentId: raw.parentId ?? 0,
title: raw.meta?.title || '',
routePath: raw.path || '',
routeName: raw.name || '',
component: raw.component === '#' ? '' : raw.component || '',
icon: raw.icon || '',
permissionCode: raw.permissionCode || '',
type: raw.type || 'MENU',
sort: raw.sort ?? 1,
visible: raw.visible ?? 1,
status: raw.status ?? 1
})
}
function saveMenu() {
async function saveMenu() {
if (!form.title.trim()) return ElMessage.warning('请填写菜单名称')
if (form.type !== '按钮' && !form.route.trim()) return ElMessage.warning('请填写对应路由')
const duplicate = customMenus.some(
(menu) => menu.title === form.title.trim() && menu.key !== selectedKey.value
)
if (duplicate) return ElMessage.warning('菜单名称已存在')
const isNew = !selectedKey.value
const key = selectedKey.value || `${Date.now()}`
const existingMenu = customMenus.find((menu) => menu.key === key)
const permission = form.permission.trim() || `访问菜单:${form.title.trim()}`
if (existingMenu) {
if (router.hasRoute(`custom-menu-${key}`)) router.removeRoute(`custom-menu-${key}`)
if (existingMenu.permission !== permission) {
removePermission(existingMenu.permission)
createPermission(permission)
}
Object.assign(existingMenu, {
...form,
key,
title: form.title.trim(),
permission,
route: form.route.trim(),
sort: Number(form.sort) || 1
})
} else {
customMenus.push({
...form,
key,
title: form.title.trim(),
permission,
route: form.route.trim(),
sort: Number(form.sort) || 1
})
createPermission(permission)
if (form.type !== 'BUTTON' && !form.routePath.trim()) {
return ElMessage.warning('请填写对应路由')
}
const savedMenu = customMenus.find((menu) => menu.key === key)
if (savedMenu?.type !== '按钮') registerCustomMenu(savedMenu)
saveCustomMenus()
selectedKey.value = key
ElMessage.success(isNew ? '菜单已添加' : '菜单已保存')
const payload = {
parentId: form.parentId || 0,
title: form.title.trim(),
routePath: form.routePath.trim(),
routeName: form.routeName.trim(),
component: form.type === 'CATALOG' ? '#' : form.component.trim(),
icon: form.icon,
permissionCode: form.permissionCode.trim(),
type: form.type,
sort: Number(form.sort) || 1,
visible: form.visible,
status: form.status
}
const isCreate = !form.id
if (isCreate) {
await createMenuApi(payload)
} else {
await updateMenuApi(form.id, payload)
}
await load()
resetForm()
ElMessage.success(isCreate ? '菜单已添加,重新登录后生效' : '菜单已保存,重新登录后生效')
}
async function deleteMenu() {
const menu = customMenus.find((item) => item.key === selectedKey.value)
if (!menu) return ElMessage.warning('请先在左侧选择一个自定义菜单')
if (!form.id) return ElMessage.warning('请先在左侧选择一个菜单')
try {
await ElMessageBox.confirm(`确认删除「${menu.title}」菜单吗?`, '删除确认', {
await ElMessageBox.confirm(`确认删除「${form.title}」菜单吗?`, '删除确认', {
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'el-button--danger'
})
customMenus.splice(customMenus.indexOf(menu), 1)
removePermission(menu.permission)
if (router.hasRoute(`custom-menu-${menu.key}`)) router.removeRoute(`custom-menu-${menu.key}`)
saveCustomMenus()
resetForm()
ElMessage.success('菜单已删除')
} catch {
// 用户取消时不删除菜单。
return
}
await deleteMenuApi(form.id)
ElMessage.success('菜单已删除,重新登录后生效')
resetForm()
await load()
}
onMounted(load)
return {
selectedKey,
selectedParent,
form,
menuTree,
treeData,
loading,
form,
parentOptions,
resetForm,
selectNode,