feat: 完成TikTok短剧管理后台基础功能开发
本次提交搭建了完整的后台管理系统,包含: 1. 登录认证与权限校验体系 2. 工作台、短剧、用户、角色、人员、日志等全业务模块 3. 响应式布局与通用样式组件 4. 菜单动态过滤与路由权限控制 5. 移除了README中冗余的权限管理菜单项
This commit is contained in:
163
src/Role/PermissionTreePicker.tsx
Normal file
163
src/Role/PermissionTreePicker.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Tree } from '@arco-design/web-react';
|
||||
import type { Permission } from '../api/types';
|
||||
|
||||
type PermissionTreeNode = {
|
||||
key: string;
|
||||
title: string;
|
||||
children?: PermissionTreeNode[];
|
||||
};
|
||||
|
||||
type PermissionModuleConfig = {
|
||||
prefix: string;
|
||||
title: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
const systemPermissionModules: PermissionModuleConfig[] = [
|
||||
{ prefix: 'permission', title: '权限管理', subject: '权限' },
|
||||
{ prefix: 'role', title: '角色管理', subject: '角色' },
|
||||
{ prefix: 'admin-user', title: '人员管理', subject: '人员' },
|
||||
{ prefix: 'user', title: '用户管理', subject: '用户' },
|
||||
{ prefix: 'log', title: '日志管理', subject: '日志' },
|
||||
];
|
||||
|
||||
const businessPermissionModules: PermissionModuleConfig[] = [
|
||||
{ prefix: 'drama', title: '短剧管理', subject: '短剧' },
|
||||
{ prefix: 'episode', title: '剧集管理', subject: '剧集' },
|
||||
{ prefix: 'media', title: '媒体管理', subject: '媒体' },
|
||||
{ prefix: 'payment', title: '支付管理', subject: '支付' },
|
||||
];
|
||||
|
||||
const actionNames: Record<string, string> = {
|
||||
read: '查看',
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
manage: '管理',
|
||||
};
|
||||
|
||||
function permissionKey(id: number) {
|
||||
return `permission:${id}`;
|
||||
}
|
||||
|
||||
function permissionIdFromKey(key: string) {
|
||||
const id = Number(key.replace('permission:', ''));
|
||||
return Number.isInteger(id) ? id : null;
|
||||
}
|
||||
|
||||
function getPermissionPrefix(code: string) {
|
||||
return code.split(':')[0];
|
||||
}
|
||||
|
||||
function getPermissionAction(code: string) {
|
||||
return code.split(':')[1] || code;
|
||||
}
|
||||
|
||||
function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) {
|
||||
return permission.name || (module ? `${actionNames[getPermissionAction(permission.code)]}${module.subject}` : permission.code);
|
||||
}
|
||||
|
||||
function buildPermissionModuleNode(
|
||||
permissions: Permission[],
|
||||
module: PermissionModuleConfig,
|
||||
): PermissionTreeNode | null {
|
||||
const children = permissions
|
||||
.filter((permission) => getPermissionPrefix(permission.code) === module.prefix)
|
||||
.map((permission) => ({
|
||||
key: permissionKey(permission.id),
|
||||
title: getPermissionTitle(permission, module),
|
||||
}));
|
||||
|
||||
if (!children.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: `module:${module.prefix}`,
|
||||
title: module.title,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPermissionTree(permissions: Permission[]): PermissionTreeNode[] {
|
||||
const knownPrefixes = new Set([
|
||||
...systemPermissionModules.map((module) => module.prefix),
|
||||
...businessPermissionModules.map((module) => module.prefix),
|
||||
]);
|
||||
const systemChildren = systemPermissionModules
|
||||
.map((module) => buildPermissionModuleNode(permissions, module))
|
||||
.filter((node): node is PermissionTreeNode => Boolean(node));
|
||||
const businessChildren = businessPermissionModules
|
||||
.map((module) => buildPermissionModuleNode(permissions, module))
|
||||
.filter((node): node is PermissionTreeNode => Boolean(node));
|
||||
const otherPermissions = permissions
|
||||
.filter((permission) => !knownPrefixes.has(getPermissionPrefix(permission.code)))
|
||||
.map((permission) => ({
|
||||
key: permissionKey(permission.id),
|
||||
title: permission.name,
|
||||
}));
|
||||
|
||||
if (otherPermissions.length) {
|
||||
businessChildren.push({
|
||||
key: 'module:other',
|
||||
title: '其他权限',
|
||||
children: otherPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
{ key: 'group:system', title: '系统权限', children: systemChildren },
|
||||
{ key: 'group:business', title: '业务权限', children: businessChildren },
|
||||
];
|
||||
}
|
||||
|
||||
function collectExpandedKeys(nodes: PermissionTreeNode[]) {
|
||||
const keys: string[] = [];
|
||||
const visit = (items: PermissionTreeNode[]) => {
|
||||
items.forEach((item) => {
|
||||
if (item.children?.length) {
|
||||
keys.push(item.key);
|
||||
visit(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
visit(nodes);
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function PermissionTreePicker({
|
||||
permissions,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
permissions: Permission[];
|
||||
value: number[];
|
||||
onChange: (permissionIds: number[]) => void;
|
||||
}) {
|
||||
const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]);
|
||||
const expandedKeys = useMemo(() => collectExpandedKeys(treeData), [treeData]);
|
||||
const checkedKeys = value.map((id) => permissionKey(id));
|
||||
|
||||
return (
|
||||
<div className="permission-tree-panel">
|
||||
<Tree
|
||||
blockNode
|
||||
checkable
|
||||
checkedStrategy="child"
|
||||
checkedKeys={checkedKeys}
|
||||
defaultExpandedKeys={expandedKeys}
|
||||
selectable={false}
|
||||
showLine
|
||||
treeData={treeData}
|
||||
onCheck={(keys) => {
|
||||
onChange(
|
||||
keys
|
||||
.map((key) => permissionIdFromKey(key))
|
||||
.filter((id): id is number => id !== null),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/Role/index.css
Normal file
22
src/Role/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.form-label {
|
||||
margin: 0 0 8px;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
margin: -12px 0 18px;
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.permission-tree-panel {
|
||||
height: 360px;
|
||||
overflow: auto;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 6px;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
117
src/Role/index.tsx
Normal file
117
src/Role/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Card, Form, Input, Message, Modal, Space, Table, Tag } from '@arco-design/web-react';
|
||||
import { IconPlus } from '@arco-design/web-react/icon';
|
||||
import type { Permission, Role } from '../api/types';
|
||||
import { api } from '../api/client';
|
||||
import { usePageData } from '../hooks/usePageData';
|
||||
import { PermissionTreePicker } from './PermissionTreePicker';
|
||||
import './index.css';
|
||||
|
||||
function RolesPage() {
|
||||
const { data, loading, reload } = usePageData<Role>('/api/admin/v1/roles');
|
||||
const permissions = usePageData<Permission>('/api/admin/v1/permissions');
|
||||
const [createForm] = Form.useForm();
|
||||
const [createVisible, setCreateVisible] = useState(false);
|
||||
const [creatingRole, setCreatingRole] = useState(false);
|
||||
const [createPermissionIds, setCreatePermissionIds] = useState<number[]>([]);
|
||||
const [assignmentVisible, setAssignmentVisible] = useState(false);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
|
||||
const [savingPermissions, setSavingPermissions] = useState(false);
|
||||
|
||||
const openCreateRole = () => {
|
||||
createForm.resetFields();
|
||||
setCreatePermissionIds([]);
|
||||
setCreateVisible(true);
|
||||
};
|
||||
|
||||
const createRole = async (values: { code: string; name: string; description?: string }) => {
|
||||
setCreatingRole(true);
|
||||
try {
|
||||
await api.post('/api/admin/v1/roles', {
|
||||
...values,
|
||||
permissionIds: createPermissionIds,
|
||||
});
|
||||
Message.success('角色创建成功');
|
||||
setCreateVisible(false);
|
||||
await reload();
|
||||
} finally {
|
||||
setCreatingRole(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAssignPermissions = (role: Role) => {
|
||||
setSelectedRole(role);
|
||||
setSelectedPermissionIds(role.permissions.map((permission) => permission.id));
|
||||
setAssignmentVisible(true);
|
||||
};
|
||||
|
||||
const saveRolePermissions = async () => {
|
||||
if (!selectedRole) {
|
||||
return;
|
||||
}
|
||||
setSavingPermissions(true);
|
||||
try {
|
||||
await api.patch(`/api/admin/v1/roles/${selectedRole.id}/permissions`, {
|
||||
permissionIds: selectedPermissionIds,
|
||||
});
|
||||
Message.success('角色权限已更新');
|
||||
setAssignmentVisible(false);
|
||||
await reload();
|
||||
} finally {
|
||||
setSavingPermissions(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="section-card" title="角色管理" extra={<Button type="primary" icon={<IconPlus />} onClick={openCreateRole}>新增角色</Button>}>
|
||||
<Table rowKey="id" loading={loading} data={data.list} pagination={{ total: data.pagination.total, pageSize: data.pagination.pageSize }} columns={[
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '编码', dataIndex: 'code' },
|
||||
{ title: '名称', dataIndex: 'name' },
|
||||
{ title: '权限列表', render: (_, record: Role) => <Space wrap>{record.permissions.map((item) => <Tag key={item.id}>{item.name}</Tag>)}</Space> },
|
||||
{ title: '操作', width: 140, render: (_, record: Role) => <Button size="small" onClick={() => openAssignPermissions(record)}>分配权限</Button> },
|
||||
]} />
|
||||
<Modal title="新增角色" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null}>
|
||||
<Form form={createForm} layout="vertical" onSubmit={createRole}>
|
||||
<Form.Item label="角色编码" field="code" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入角色编码,例如 drama_operator" />
|
||||
</Form.Item>
|
||||
<div className="form-help">角色的唯一标识,创建后不可修改</div>
|
||||
<Form.Item label="角色名称" field="name" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="角色描述" field="description">
|
||||
<Input.TextArea placeholder="请输入角色描述" autoSize={{ minRows: 2, maxRows: 4 }} />
|
||||
</Form.Item>
|
||||
<div className="form-label">权限设置</div>
|
||||
<PermissionTreePicker
|
||||
permissions={permissions.data.list}
|
||||
value={createPermissionIds}
|
||||
onChange={setCreatePermissionIds}
|
||||
/>
|
||||
<div className="form-help">为角色分配系统权限,决定拥有此角色的用户可以进行的操作</div>
|
||||
<div className="modal-actions">
|
||||
<Button onClick={() => setCreateVisible(false)}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={creatingRole}>创建</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`分配权限${selectedRole ? ` - ${selectedRole.name}` : ''}`}
|
||||
visible={assignmentVisible}
|
||||
confirmLoading={savingPermissions}
|
||||
onOk={saveRolePermissions}
|
||||
onCancel={() => setAssignmentVisible(false)}
|
||||
>
|
||||
<PermissionTreePicker
|
||||
permissions={permissions.data.list}
|
||||
value={selectedPermissionIds}
|
||||
onChange={setSelectedPermissionIds}
|
||||
/>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default RolesPage;
|
||||
Reference in New Issue
Block a user