main
parent
b75150dca0
commit
f718ee4834
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,70 @@
|
|||
-- 页面菜单缺失清单 SQL
|
||||
-- 用途:
|
||||
-- 1. 检查前端当前已接入的页面路由,哪些还没有对应的 sys_menu 页面菜单
|
||||
-- 2. 只查询,不做插入
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. 这里按前端当前实际路由整理了一份页面菜单基线
|
||||
-- 2. 仅检查 menu_type in ('M','C') 的目录/页面菜单
|
||||
-- 3. 管理员无需在角色管理中额外勾选页面;前端当前已按 admin 角色默认放行
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_page_menu_specs;
|
||||
CREATE TEMPORARY TABLE tmp_page_menu_specs (
|
||||
path VARCHAR(128) NOT NULL,
|
||||
component_like VARCHAR(255) NULL,
|
||||
menu_name VARCHAR(128) NOT NULL,
|
||||
remark VARCHAR(255) NULL
|
||||
);
|
||||
|
||||
INSERT INTO tmp_page_menu_specs (path, component_like, menu_name, remark) VALUES
|
||||
('index', '%worklog%', '工作日志', 'WorkLogPage 首页'),
|
||||
('dashboard/project-execution', '%dashboard/project-execution%', '项目进度', 'ProjectExecutionPage'),
|
||||
('projectBank/projectProgress', '%dashboard/project-execution%', '项目进度(兼容)', 'ProjectExecutionPage 别名路由'),
|
||||
('projectBank/projectUser', '%projectBank/projectUser%', '项目成员', 'ProjectUserPage'),
|
||||
('projectBank/userProject', '%projectBank/userProject%', '人员项目', 'UserProjectPage'),
|
||||
('projectBank/userScore', '%projectBank/userScore%', '我的绩效', 'UserScorePage'),
|
||||
('projectBank/userScoreDetail', '%projectBank/userScoreDetail%', '绩效详情', 'UserScoreDetailPage'),
|
||||
('user/profile', '%Profile%', '个人中心', 'ProfilePage'),
|
||||
('monitor/cache', '%monitor/cache%', '缓存监控', 'CacheMonitorPage'),
|
||||
('monitor/job', '%monitor/job%', '定时任务', 'JobMonitorPage'),
|
||||
('monitor/logininfor', '%monitor/login%', '登录日志', 'LoginLogPage'),
|
||||
('monitor/online', '%monitor/online%', '在线用户', 'OnlineUserPage'),
|
||||
('monitor/operlog', '%monitor/operation%', '操作日志', 'OperationLogPage'),
|
||||
('monitor/server', '%monitor/server%', '服务监控', 'ServerMonitorPage'),
|
||||
('monitor/cacheList', '%monitor/cache-list%', '缓存列表', 'CacheListPage'),
|
||||
('system/user', '%system/user%', '用户管理', 'UserPage'),
|
||||
('system/role', '%system/role%', '角色管理', 'RolePage'),
|
||||
('system/menu', '%system/menu%', '菜单管理', 'MenuPage'),
|
||||
('system/dept', '%system/dept%', '部门管理', 'DeptPage'),
|
||||
('system/dict', '%system/dict%', '字典管理', 'DictPage'),
|
||||
('system/config', '%system/config%', '参数配置', 'ConfigPage'),
|
||||
('project', '%project/list%', '项目管理', 'ProjectPage 后端标准路径'),
|
||||
('project/list', '%project/list%', '项目管理', 'ProjectPage 前端兼容路径'),
|
||||
('project/detail', '%project/detail%', '项目详情', 'ProjectDetailPage'),
|
||||
('project/demandManage', '%project/%DemandManage%', '需求管理', 'DemandManagePage 后端路径'),
|
||||
('demandManage', '%project/%DemandManage%', '需求管理', 'DemandManagePage 前端兼容路径'),
|
||||
('workAppraisal/manager', '%workAppraisal/ManagerPage%', '经理评分', 'ManagerPage'),
|
||||
('workAppraisal/normalWorker', '%workAppraisal/NormalWorkerPage%', '员工评分', 'NormalWorkerPage'),
|
||||
('workAppraisal/managerUser', '%workAppraisal/ManagerUserPage%', '经理评分用户', 'ManagerUserPage'),
|
||||
('workAppraisal/taskSet', '%workAppraisal/TaskSetPage%', '任务设置', 'TaskSetPage'),
|
||||
('workAppraisal/detail', '%workAppraisal/AppraisalDetailPage%', '评分详情', 'AppraisalDetailPage'),
|
||||
('workAppraisal/taskModule', '%workAppraisal/AppraisalDashboardPage%', '绩效看板', 'AppraisalDashboardPage'),
|
||||
('workAppraisal/dashboard', '%workAppraisal/AppraisalDashboardPage%', '绩效看板(兼容)', 'AppraisalDashboardPage 别名路由'),
|
||||
('workAppraisal/moduleDetail', '%workAppraisal/AppraisalModuleDetailPage%', '模块详情', 'AppraisalModuleDetailPage'),
|
||||
('workAppraisal/myPerformance', '%projectBank/userScore%', '我的绩效(兼容)', 'UserScorePage 前端映射');
|
||||
|
||||
SELECT
|
||||
s.path,
|
||||
s.menu_name,
|
||||
s.remark
|
||||
FROM tmp_page_menu_specs s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND (
|
||||
m.path = s.path
|
||||
OR (s.component_like IS NOT NULL AND s.component_like <> '' AND m.component LIKE s.component_like)
|
||||
)
|
||||
)
|
||||
ORDER BY s.path;
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
-- 权限按钮对账与补齐脚本
|
||||
-- 用途:
|
||||
-- 1. 根据前端当前实际使用到的权限码,对 sys_menu 中的按钮菜单做补齐
|
||||
-- 2. 仅插入缺失按钮,不修改已有按钮,不重复插入
|
||||
-- 3. 执行前会先输出“未找到父页面菜单”的按钮清单,便于先补页面再补按钮
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. 当前环境无法直接连库校验,这份脚本基于前端代码中的权限使用点生成
|
||||
-- 2. 父页面使用 sys_menu.path 定位,个别页面补充 component 模糊匹配兜底
|
||||
-- 3. 同一权限码如果在多个页面中复用,会按页面分别补按钮节点
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_permission_button_specs;
|
||||
CREATE TEMPORARY TABLE tmp_permission_button_specs (
|
||||
parent_path VARCHAR(128) NOT NULL,
|
||||
parent_component_like VARCHAR(255) NULL,
|
||||
menu_name VARCHAR(128) NOT NULL,
|
||||
perms VARCHAR(128) NOT NULL,
|
||||
order_num INT NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NULL
|
||||
);
|
||||
|
||||
INSERT INTO tmp_permission_button_specs (parent_path, parent_component_like, menu_name, perms, order_num, remark) VALUES
|
||||
-- 工作日志
|
||||
('index', '%worklog%', '新增日志', 'business:work:hour:add', 1, 'WorkLogPage 添加日志'),
|
||||
('index', '%worklog%', '编辑日志', 'business:work:hour:edit', 2, 'WorkLogPage 编辑/确认'),
|
||||
('index', '%worklog%', '更新日志', 'business:work:hour:update', 3, 'WorkLogPage 兼容旧权限码'),
|
||||
('index', '%worklog%', '删除日志', 'business:work:hour:remove', 4, 'WorkLogPage 删除/取消'),
|
||||
|
||||
-- 项目管理
|
||||
('project', '%project/list%', '新增项目', 'business:project:add', 1, 'ProjectPage'),
|
||||
('project', '%project/list%', '编辑项目', 'business:project:edit', 2, 'ProjectPage/ProjectDetailPage'),
|
||||
('project', '%project/list%', '删除项目', 'business:project:remove', 3, 'ProjectPage'),
|
||||
('project', '%project/list%', '查看需求', 'project:demand:list', 4, 'ProjectPage'),
|
||||
|
||||
-- 需求管理
|
||||
('demandManage', NULL, '新增需求', 'project:demand:add', 1, 'DemandManagePage'),
|
||||
('demandManage', NULL, '编辑需求', 'project:demand:edit', 2, 'DemandManagePage'),
|
||||
('demandManage', NULL, '删除需求', 'project:demand:remove', 3, 'DemandManagePage'),
|
||||
('demandManage', NULL, '新增版本', 'project:version:add', 4, 'DemandManagePage'),
|
||||
('demandManage', NULL, '编辑版本', 'project:version:edit', 5, 'DemandManagePage'),
|
||||
('demandManage', NULL, '删除版本', 'project:version:remove', 6, 'DemandManagePage'),
|
||||
|
||||
-- 任务监控
|
||||
('job', NULL, '新增任务', 'monitor:job:add', 1, 'JobMonitorPage'),
|
||||
('job', NULL, '编辑任务', 'monitor:job:edit', 2, 'JobMonitorPage'),
|
||||
('job', NULL, '删除任务', 'monitor:job:remove', 3, 'JobMonitorPage'),
|
||||
('job', NULL, '导出任务', 'monitor:job:export', 4, 'JobMonitorPage'),
|
||||
('job', NULL, '查询任务', 'monitor:job:query', 5, 'JobMonitorPage'),
|
||||
('job', NULL, '状态修改', 'monitor:job:changeStatus', 6, 'JobMonitorPage'),
|
||||
|
||||
-- 缓存列表
|
||||
('cacheList', NULL, '清理缓存', 'monitor:cache:clear', 1, 'CacheListPage'),
|
||||
|
||||
-- 在线用户
|
||||
('online', NULL, '强退用户', 'monitor:online:forceLogout', 1, 'OnlineUserPage'),
|
||||
|
||||
-- 登录日志
|
||||
('logininfor', NULL, '删除日志', 'monitor:logininfor:remove', 1, 'LoginLogPage'),
|
||||
('logininfor', NULL, '清空日志', 'monitor:logininfor:clean', 2, 'LoginLogPage'),
|
||||
('logininfor', NULL, '账户解锁', 'monitor:logininfor:unlock', 3, 'LoginLogPage'),
|
||||
('logininfor', NULL, '导出日志', 'monitor:logininfor:export', 4, 'LoginLogPage'),
|
||||
|
||||
-- 操作日志
|
||||
('operlog', NULL, '删除日志', 'monitor:operlog:remove', 1, 'OperationLogPage'),
|
||||
('operlog', NULL, '清空日志', 'monitor:operlog:clean', 2, 'OperationLogPage'),
|
||||
('operlog', NULL, '导出日志', 'monitor:operlog:export', 3, 'OperationLogPage'),
|
||||
|
||||
-- 用户管理
|
||||
('user', NULL, '新增用户', 'system:user:add', 1, 'UserPage'),
|
||||
('user', NULL, '修改用户', 'system:user:edit', 2, 'UserPage'),
|
||||
('user', NULL, '删除用户', 'system:user:remove', 3, 'UserPage'),
|
||||
('user', NULL, '重置密码', 'system:user:resetPwd', 4, 'UserPage'),
|
||||
|
||||
-- 角色管理
|
||||
('role', NULL, '新增角色', 'system:role:add', 1, 'RolePage'),
|
||||
('role', NULL, '修改角色', 'system:role:edit', 2, 'RolePage'),
|
||||
('role', NULL, '删除角色', 'system:role:remove', 3, 'RolePage'),
|
||||
('role', NULL, '导出角色', 'system:role:export', 4, 'RolePage'),
|
||||
('role', NULL, '数据权限', 'system:role:dataScope', 5, 'RolePage'),
|
||||
('role', NULL, '分配用户', 'system:role:authUser', 6, 'RolePage'),
|
||||
|
||||
-- 菜单管理
|
||||
('menu', NULL, '新增菜单', 'system:menu:add', 1, 'MenuPage'),
|
||||
('menu', NULL, '修改菜单', 'system:menu:edit', 2, 'MenuPage'),
|
||||
('menu', NULL, '删除菜单', 'system:menu:remove', 3, 'MenuPage'),
|
||||
|
||||
-- 部门管理
|
||||
('dept', NULL, '新增部门', 'system:dept:add', 1, 'DeptPage'),
|
||||
('dept', NULL, '修改部门', 'system:dept:edit', 2, 'DeptPage'),
|
||||
('dept', NULL, '删除部门', 'system:dept:remove', 3, 'DeptPage'),
|
||||
|
||||
-- 字典管理
|
||||
('dict', NULL, '新增字典', 'system:dict:add', 1, 'DictPage'),
|
||||
('dict', NULL, '修改字典', 'system:dict:edit', 2, 'DictPage'),
|
||||
('dict', NULL, '删除字典', 'system:dict:remove', 3, 'DictPage'),
|
||||
('dict', NULL, '导出字典', 'system:dict:export', 4, 'DictPage'),
|
||||
('dict', NULL, '刷新缓存', 'system:dict:refreshCache', 5, 'DictPage'),
|
||||
|
||||
-- 参数配置
|
||||
('config', NULL, '新增参数', 'system:config:add', 1, 'ConfigPage'),
|
||||
('config', NULL, '修改参数', 'system:config:edit', 2, 'ConfigPage'),
|
||||
('config', NULL, '删除参数', 'system:config:remove', 3, 'ConfigPage'),
|
||||
('config', NULL, '导出参数', 'system:config:export', 4, 'ConfigPage'),
|
||||
('config', NULL, '刷新缓存', 'system:config:refreshCache', 5, 'ConfigPage'),
|
||||
|
||||
-- 绩效任务设置
|
||||
('taskSet', NULL, '新增任务', 'task:add', 1, 'TaskSetPage'),
|
||||
('taskSet', NULL, '创建任务', 'task:create', 2, 'TaskSetPage 兼容旧权限码'),
|
||||
('taskSet', NULL, '编辑任务', 'task:edit', 3, 'TaskSetPage'),
|
||||
('taskSet', NULL, '更新任务', 'task:update', 4, 'TaskSetPage 兼容旧权限码'),
|
||||
('taskSet', NULL, '删除任务', 'task:remove', 5, 'TaskSetPage'),
|
||||
('taskSet', NULL, '删除任务兼容', 'task:delete', 6, 'TaskSetPage 兼容旧权限码'),
|
||||
('taskSet', NULL, '指标配置', 'task:config', 7, 'TaskSetPage'),
|
||||
('taskSet', NULL, '设置指标', 'task:set', 8, 'TaskSetPage 兼容旧权限码'),
|
||||
|
||||
-- 绩效看板
|
||||
('taskModule', NULL, '删除模板', 'examine:template:remove', 1, 'AppraisalDashboardPage'),
|
||||
('taskModule', NULL, '删除模板兼容', 'examine:template:delete', 2, 'AppraisalDashboardPage 兼容旧权限码'),
|
||||
|
||||
-- 绩效打分相关页面
|
||||
('manager', NULL, '新增评分', 'examine:detail:add', 1, 'ManagerPage'),
|
||||
('manager', NULL, '编辑评分', 'examine:detail:edit', 2, 'ManagerPage'),
|
||||
('manager', NULL, '提交评分', 'examine:detail:submit', 3, 'ManagerPage'),
|
||||
|
||||
('managerUser', NULL, '新增评分', 'examine:detail:add', 1, 'ManagerUserPage'),
|
||||
('managerUser', NULL, '编辑评分', 'examine:detail:edit', 2, 'ManagerUserPage'),
|
||||
('managerUser', NULL, '提交评分', 'examine:detail:submit', 3, 'ManagerUserPage'),
|
||||
|
||||
('normalWorker', NULL, '新增评分', 'examine:detail:add', 1, 'NormalWorkerPage'),
|
||||
('normalWorker', NULL, '编辑评分', 'examine:detail:edit', 2, 'NormalWorkerPage'),
|
||||
('normalWorker', NULL, '提交评分', 'examine:detail:submit', 3, 'NormalWorkerPage'),
|
||||
|
||||
('detail', NULL, '新增评分', 'examine:detail:add', 1, 'AppraisalDetailPage'),
|
||||
('detail', NULL, '编辑评分', 'examine:detail:edit', 2, 'AppraisalDetailPage'),
|
||||
('detail', NULL, '提交评分', 'examine:detail:submit', 3, 'AppraisalDetailPage');
|
||||
|
||||
-- 先看哪些按钮找不到父页面
|
||||
SELECT
|
||||
s.parent_path,
|
||||
s.menu_name,
|
||||
s.perms,
|
||||
s.remark
|
||||
FROM tmp_permission_button_specs s
|
||||
WHERE (
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND (
|
||||
m.path = s.parent_path
|
||||
OR (s.parent_component_like IS NOT NULL AND s.parent_component_like <> '' AND m.component LIKE s.parent_component_like)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN m.path = s.parent_path THEN 0 ELSE 1 END,
|
||||
m.menu_id
|
||||
LIMIT 1
|
||||
) IS NULL
|
||||
ORDER BY s.parent_path, s.order_num;
|
||||
|
||||
-- 补齐缺失按钮
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
s.menu_name,
|
||||
(
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND (
|
||||
m.path = s.parent_path
|
||||
OR (s.parent_component_like IS NOT NULL AND s.parent_component_like <> '' AND m.component LIKE s.parent_component_like)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN m.path = s.parent_path THEN 0 ELSE 1 END,
|
||||
m.menu_id
|
||||
LIMIT 1
|
||||
) AS parent_id,
|
||||
s.order_num,
|
||||
'#',
|
||||
'',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'F',
|
||||
'0',
|
||||
'0',
|
||||
s.perms,
|
||||
'#',
|
||||
'codex',
|
||||
NOW(),
|
||||
s.remark
|
||||
FROM tmp_permission_button_specs s
|
||||
WHERE (
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND (
|
||||
m.path = s.parent_path
|
||||
OR (s.parent_component_like IS NOT NULL AND s.parent_component_like <> '' AND m.component LIKE s.parent_component_like)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN m.path = s.parent_path THEN 0 ELSE 1 END,
|
||||
m.menu_id
|
||||
LIMIT 1
|
||||
) IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu f
|
||||
WHERE f.menu_type = 'F'
|
||||
AND f.perms = s.perms
|
||||
AND f.parent_id = (
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND (
|
||||
m.path = s.parent_path
|
||||
OR (s.parent_component_like IS NOT NULL AND s.parent_component_like <> '' AND m.component LIKE s.parent_component_like)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN m.path = s.parent_path THEN 0 ELSE 1 END,
|
||||
m.menu_id
|
||||
LIMIT 1
|
||||
)
|
||||
);
|
||||
|
||||
-- 补齐后可查看这一批按钮
|
||||
SELECT
|
||||
p.menu_name AS parent_menu_name,
|
||||
p.path AS parent_path,
|
||||
c.menu_name,
|
||||
c.perms,
|
||||
c.order_num
|
||||
FROM sys_menu c
|
||||
INNER JOIN sys_menu p ON p.menu_id = c.parent_id
|
||||
WHERE c.menu_type = 'F'
|
||||
AND c.create_by = 'codex'
|
||||
ORDER BY p.path, c.order_num, c.menu_id;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
-- 工作日志菜单与按钮补录
|
||||
-- 目标:
|
||||
-- 1. 确保 sys_menu 中存在工作日志页面菜单(/index)
|
||||
-- 2. 为工作日志页面补齐按钮权限项
|
||||
-- 3. 所有插入都做不存在判断,避免重复
|
||||
--
|
||||
-- 对应前端逻辑:
|
||||
-- - 页面路由:/index
|
||||
-- - 页面组件:component 包含 worklog 即可被前端识别为工作日志页
|
||||
-- - 按钮权限:
|
||||
-- business:work:hour:add
|
||||
-- business:work:hour:edit
|
||||
-- business:work:hour:remove
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- 1. 如果工作日志页面菜单不存在,则补一条页面菜单
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
'工作日志',
|
||||
0,
|
||||
1,
|
||||
'index',
|
||||
'worklog/index',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'C',
|
||||
'0',
|
||||
'0',
|
||||
'business:work:hour:list',
|
||||
'dashboard',
|
||||
'codex',
|
||||
NOW(),
|
||||
'工作日志首页菜单'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE menu_type IN ('M', 'C')
|
||||
AND (
|
||||
path IN ('index', '/index')
|
||||
OR component LIKE '%worklog%'
|
||||
)
|
||||
);
|
||||
|
||||
-- 2. 定位工作日志页面菜单
|
||||
SET @worklog_menu_id := (
|
||||
SELECT menu_id
|
||||
FROM sys_menu
|
||||
WHERE menu_type IN ('M', 'C')
|
||||
AND (
|
||||
path IN ('index', '/index')
|
||||
OR component LIKE '%worklog%'
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN path IN ('index', '/index') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
menu_id
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- 3. 补录按钮:新增日志
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
'新增日志',
|
||||
@worklog_menu_id,
|
||||
1,
|
||||
'#',
|
||||
'',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'F',
|
||||
'0',
|
||||
'0',
|
||||
'business:work:hour:add',
|
||||
'#',
|
||||
'codex',
|
||||
NOW(),
|
||||
'工作日志页按钮:添加日志'
|
||||
FROM DUAL
|
||||
WHERE @worklog_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @worklog_menu_id
|
||||
AND menu_type = 'F'
|
||||
AND perms = 'business:work:hour:add'
|
||||
);
|
||||
|
||||
-- 4. 补录按钮:编辑日志
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
'编辑日志',
|
||||
@worklog_menu_id,
|
||||
2,
|
||||
'#',
|
||||
'',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'F',
|
||||
'0',
|
||||
'0',
|
||||
'business:work:hour:edit',
|
||||
'#',
|
||||
'codex',
|
||||
NOW(),
|
||||
'工作日志页按钮:编辑/确认'
|
||||
FROM DUAL
|
||||
WHERE @worklog_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @worklog_menu_id
|
||||
AND menu_type = 'F'
|
||||
AND perms = 'business:work:hour:edit'
|
||||
);
|
||||
|
||||
-- 5. 补录按钮:删除日志
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
'删除日志',
|
||||
@worklog_menu_id,
|
||||
3,
|
||||
'#',
|
||||
'',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'F',
|
||||
'0',
|
||||
'0',
|
||||
'business:work:hour:remove',
|
||||
'#',
|
||||
'codex',
|
||||
NOW(),
|
||||
'工作日志页按钮:删除/取消'
|
||||
FROM DUAL
|
||||
WHERE @worklog_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @worklog_menu_id
|
||||
AND menu_type = 'F'
|
||||
AND perms = 'business:work:hour:remove'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
-- 页面菜单自动补齐 SQL
|
||||
-- 目标:
|
||||
-- 1. 按若依常见模型补齐目录(M)与页面(C)菜单
|
||||
-- 2. 管理员无需依赖 sys_role_menu 勾选;前端已按 admin 角色默认放行
|
||||
-- 3. 仅插入缺失菜单,不重复插入
|
||||
--
|
||||
-- 说明:
|
||||
-- 1. 本脚本不删除已有数据,只补齐缺失目录/页面
|
||||
-- 2. 页面 path 采用“目录 + 子页面”的常见结构,贴近若依模型
|
||||
-- 3. 一些详情类页面设为隐藏 visible = '1',保留访问能力但不在侧边栏展示
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_menu_dir_specs;
|
||||
CREATE TEMPORARY TABLE tmp_menu_dir_specs (
|
||||
path VARCHAR(128) NOT NULL,
|
||||
menu_name VARCHAR(128) NOT NULL,
|
||||
icon VARCHAR(64) NULL,
|
||||
order_num INT NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NULL
|
||||
);
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_menu_page_specs;
|
||||
CREATE TEMPORARY TABLE tmp_menu_page_specs (
|
||||
parent_path VARCHAR(128) NULL,
|
||||
menu_name VARCHAR(128) NOT NULL,
|
||||
path VARCHAR(128) NOT NULL,
|
||||
component VARCHAR(255) NOT NULL,
|
||||
perms VARCHAR(128) NULL,
|
||||
icon VARCHAR(64) NULL,
|
||||
order_num INT NOT NULL DEFAULT 1,
|
||||
visible CHAR(1) NOT NULL DEFAULT '0',
|
||||
is_cache CHAR(1) NOT NULL DEFAULT '0',
|
||||
remark VARCHAR(255) NULL
|
||||
);
|
||||
|
||||
INSERT INTO tmp_menu_dir_specs (path, menu_name, icon, order_num, remark) VALUES
|
||||
('dashboard', '看板统计', 'dashboard', 1, '首页看板目录'),
|
||||
('projectBank', '项目库', 'table', 2, '项目库目录'),
|
||||
('monitor', '系统监控', 'monitor', 3, '监控目录'),
|
||||
('system', '系统管理', 'system', 4, '系统管理目录'),
|
||||
('project', '项目管理', 'table', 5, '项目目录'),
|
||||
('workAppraisal', '绩效管理', 'chart', 6, '绩效目录'),
|
||||
('user', '用户中心', 'user', 7, '个人中心目录');
|
||||
|
||||
INSERT INTO tmp_menu_page_specs (parent_path, menu_name, path, component, perms, icon, order_num, visible, is_cache, remark) VALUES
|
||||
-- 根页面
|
||||
(NULL, '工作日志', 'index', 'worklog/index', 'business:work:hour:list', 'dashboard', 1, '0', '0', '工作日志首页'),
|
||||
|
||||
-- 看板统计
|
||||
('dashboard', '项目进度', 'project-execution', 'dashboard/project-execution', NULL, 'table', 1, '0', '0', '项目进度页面'),
|
||||
|
||||
-- 项目库
|
||||
('projectBank', '项目进度(兼容)', 'projectProgress', 'dashboard/project-execution', NULL, 'table', 1, '1', '0', '兼容旧路径'),
|
||||
('projectBank', '项目成员', 'projectUser', 'projectBank/projectUser', NULL, 'peoples', 2, '0', '0', '项目成员页面'),
|
||||
('projectBank', '人员项目', 'userProject', 'projectBank/userProject', NULL, 'table', 3, '0', '0', '人员项目页面'),
|
||||
('projectBank', '我的绩效', 'userScore', 'projectBank/userScore', NULL, 'chart', 4, '0', '0', '我的绩效页面'),
|
||||
('projectBank', '绩效详情', 'userScoreDetail', 'projectBank/userScoreDetail', NULL, 'form', 5, '1', '0', '绩效详情页'),
|
||||
|
||||
-- 用户中心
|
||||
('user', '个人中心', 'profile', 'profile/index', NULL, 'user', 1, '1', '0', '个人中心页面'),
|
||||
|
||||
-- 系统监控
|
||||
('monitor', '缓存监控', 'cache', 'monitor/cache/index', NULL, 'cache', 1, '0', '0', '缓存监控'),
|
||||
('monitor', '定时任务', 'job', 'monitor/job/index', NULL, 'job', 2, '0', '0', '定时任务'),
|
||||
('monitor', '登录日志', 'logininfor', 'monitor/logininfor/index', NULL, 'user', 3, '0', '0', '登录日志'),
|
||||
('monitor', '在线用户', 'online', 'monitor/online/index', NULL, 'online', 4, '0', '0', '在线用户'),
|
||||
('monitor', '操作日志', 'operlog', 'monitor/operlog/index', NULL, 'form', 5, '0', '0', '操作日志'),
|
||||
('monitor', '服务监控', 'server', 'monitor/server/index', NULL, 'server', 6, '0', '0', '服务监控'),
|
||||
('monitor', '缓存列表', 'cacheList', 'monitor/cache-list/index', NULL, 'cache', 7, '0', '0', '缓存列表'),
|
||||
|
||||
-- 系统管理
|
||||
('system', '用户管理', 'user', 'system/user/index', NULL, 'user', 1, '0', '0', '用户管理'),
|
||||
('system', '角色管理', 'role', 'system/role/index', NULL, 'peoples', 2, '0', '0', '角色管理'),
|
||||
('system', '菜单管理', 'menu', 'system/menu/index', NULL, 'tree-table', 3, '0', '0', '菜单管理'),
|
||||
('system', '部门管理', 'dept', 'system/dept/index', NULL, 'tree', 4, '0', '0', '部门管理'),
|
||||
('system', '字典管理', 'dict', 'system/dict/index', NULL, 'dict', 5, '0', '0', '字典管理'),
|
||||
('system', '参数配置', 'config', 'system/config/index', NULL, 'edit', 6, '0', '0', '参数配置'),
|
||||
|
||||
-- 项目管理
|
||||
('project', '项目列表', 'list', 'project/list', NULL, 'table', 1, '0', '0', '项目列表'),
|
||||
('project', '项目详情', 'detail', 'project/detail', NULL, 'form', 2, '1', '0', '项目详情页'),
|
||||
('project', '需求管理', 'demandManage', 'project/DemandManagePage', NULL, 'edit', 3, '0', '0', '需求管理'),
|
||||
|
||||
-- 绩效管理
|
||||
('workAppraisal', '经理评分', 'manager', 'workAppraisal/manager', NULL, 'build', 1, '0', '0', '经理评分页'),
|
||||
('workAppraisal', '员工评分', 'normalWorker', 'workAppraisal/normalWorker', NULL, 'chart', 2, '0', '0', '员工评分页'),
|
||||
('workAppraisal', '经理评分用户', 'managerUser', 'workAppraisal/managerUser', NULL, 'peoples', 3, '1', '0', '经理评分用户页'),
|
||||
('workAppraisal', '任务设置', 'taskSet', 'workAppraisal/taskSet', NULL, 'tool', 4, '0', '0', '任务设置页'),
|
||||
('workAppraisal', '评分详情', 'detail', 'workAppraisal/detail', NULL, 'form', 5, '1', '0', '评分详情页'),
|
||||
('workAppraisal', '绩效看板', 'taskModule', 'workAppraisal/taskModule', NULL, 'chart', 6, '0', '0', '绩效看板页'),
|
||||
('workAppraisal', '绩效看板(兼容)', 'dashboard', 'workAppraisal/taskModule', NULL, 'chart', 7, '1', '0', '兼容旧路径'),
|
||||
('workAppraisal', '模块详情', 'moduleDetail', 'workAppraisal/moduleDetail', NULL, 'form', 8, '1', '0', '模块详情页'),
|
||||
('workAppraisal', '我的绩效(兼容)', 'myPerformance', 'projectBank/userScore', NULL, 'chart', 9, '1', '0', '兼容旧路径');
|
||||
|
||||
-- 1. 先补目录
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
d.menu_name,
|
||||
0,
|
||||
d.order_num,
|
||||
d.path,
|
||||
'',
|
||||
'',
|
||||
'1',
|
||||
'0',
|
||||
'M',
|
||||
'0',
|
||||
'0',
|
||||
'',
|
||||
d.icon,
|
||||
'codex',
|
||||
NOW(),
|
||||
d.remark
|
||||
FROM tmp_menu_dir_specs d
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type IN ('M', 'C')
|
||||
AND m.parent_id = 0
|
||||
AND m.path = d.path
|
||||
);
|
||||
|
||||
-- 2. 再补页面
|
||||
INSERT INTO sys_menu (
|
||||
menu_name,
|
||||
parent_id,
|
||||
order_num,
|
||||
path,
|
||||
component,
|
||||
query,
|
||||
is_frame,
|
||||
is_cache,
|
||||
menu_type,
|
||||
visible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
SELECT
|
||||
p.menu_name,
|
||||
CASE
|
||||
WHEN p.parent_path IS NULL OR p.parent_path = '' THEN 0
|
||||
ELSE (
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type = 'M'
|
||||
AND m.path = p.parent_path
|
||||
ORDER BY m.menu_id
|
||||
LIMIT 1
|
||||
)
|
||||
END AS parent_id,
|
||||
p.order_num,
|
||||
p.path,
|
||||
p.component,
|
||||
'',
|
||||
'1',
|
||||
p.is_cache,
|
||||
'C',
|
||||
p.visible,
|
||||
'0',
|
||||
COALESCE(p.perms, ''),
|
||||
p.icon,
|
||||
'codex',
|
||||
NOW(),
|
||||
p.remark
|
||||
FROM tmp_menu_page_specs p
|
||||
WHERE (
|
||||
p.parent_path IS NULL
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type = 'M'
|
||||
AND m.path = p.parent_path
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu c
|
||||
WHERE c.menu_type IN ('M', 'C')
|
||||
AND c.path = p.path
|
||||
AND (
|
||||
c.component = p.component
|
||||
OR c.menu_name = p.menu_name
|
||||
)
|
||||
AND c.parent_id = CASE
|
||||
WHEN p.parent_path IS NULL OR p.parent_path = '' THEN 0
|
||||
ELSE (
|
||||
SELECT m.menu_id
|
||||
FROM sys_menu m
|
||||
WHERE m.menu_type = 'M'
|
||||
AND m.path = p.parent_path
|
||||
ORDER BY m.menu_id
|
||||
LIMIT 1
|
||||
)
|
||||
END
|
||||
);
|
||||
|
||||
-- 3. 查看本次补齐结果
|
||||
SELECT
|
||||
parent.menu_name AS parent_menu_name,
|
||||
child.menu_name,
|
||||
child.path,
|
||||
child.component,
|
||||
child.menu_type,
|
||||
child.visible
|
||||
FROM sys_menu child
|
||||
LEFT JOIN sys_menu parent ON parent.menu_id = child.parent_id
|
||||
WHERE child.create_by = 'codex'
|
||||
AND child.menu_type IN ('M', 'C')
|
||||
ORDER BY child.parent_id, child.order_num, child.menu_id;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1 +1 @@
|
|||
/* Remove default styles */
|
||||
/* Remove default styles */
|
||||
181
src/App.tsx
181
src/App.tsx
|
|
@ -1,45 +1,53 @@
|
|||
import { Suspense, lazy } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { Spin } from 'antd';
|
||||
import LoginPage from './pages/Login';
|
||||
import JobMonitorPage from './pages/monitor/JobMonitorPage';
|
||||
import CacheMonitorPage from './pages/monitor/CacheMonitorPage';
|
||||
import LoginLogPage from './pages/monitor/LoginLogPage'; // Import LoginLogPage
|
||||
import OnlineUserPage from './pages/monitor/OnlineUserPage'; // Import OnlineUserPage
|
||||
import OperationLogPage from './pages/monitor/OperationLogPage'; // Import OperationLogPage
|
||||
import ServerMonitorPage from './pages/monitor/ServerMonitorPage'; // Import ServerMonitorPage
|
||||
import CacheListPage from './pages/monitor/CacheListPage'; // Import CacheListPage
|
||||
import UserPage from './pages/system/UserPage'; // Import UserPage
|
||||
import RolePage from './pages/system/RolePage'; // Import RolePage
|
||||
import MenuPage from './pages/system/MenuPage'; // Import MenuPage
|
||||
import DeptPage from './pages/system/DeptPage'; // Import DeptPage
|
||||
import DictPage from './pages/system/DictPage'; // Import DictPage
|
||||
import ConfigPage from './pages/system/ConfigPage'; // Import ConfigPage
|
||||
import ProjectPage from './pages/project/ProjectPage'; // Import ProjectPage
|
||||
import ProjectDetailPage from './pages/project/ProjectDetailPage'; // Import ProjectDetailPage
|
||||
import DemandManagePage from './pages/project/DemandManagePage'; // Import DemandManagePage
|
||||
import TaskSetPage from './pages/workAppraisal/TaskSetPage'; // Import TaskSetPage
|
||||
import ManagerPage from './pages/workAppraisal/ManagerPage';
|
||||
import ManagerUserPage from './pages/workAppraisal/ManagerUserPage';
|
||||
import AppraisalDetailPage from './pages/workAppraisal/AppraisalDetailPage'; // Import AppraisalDetailPage
|
||||
import NormalWorkerPage from './pages/workAppraisal/NormalWorkerPage';
|
||||
import AppraisalDashboardPage from './pages/workAppraisal/AppraisalDashboardPage';
|
||||
import AppraisalModuleDetailPage from './pages/workAppraisal/AppraisalModuleDetailPage';
|
||||
import WorkLogPage from './pages/worklog/WorkLogPage'; // Import WorkLogPage
|
||||
import ProjectExecutionPage from './pages/dashboard/ProjectExecutionPage';
|
||||
import ProjectUserPage from './pages/projectBank/ProjectUserPage';
|
||||
import UserProjectPage from './pages/projectBank/UserProjectPage';
|
||||
import UserScorePage from './pages/projectBank/UserScorePage';
|
||||
import UserScoreDetailPage from './pages/projectBank/UserScoreDetailPage';
|
||||
import ProfilePage from './pages/Profile';
|
||||
import MainLayout from './layout/MainLayout';
|
||||
import './App.css';
|
||||
import { getToken } from './utils/auth';
|
||||
import { PermissionProvider, usePermission } from './contexts/PermissionContext';
|
||||
|
||||
const LoginPage = lazy(() => import('./pages/Login'));
|
||||
const MainLayout = lazy(() => import('./layout/MainLayout'));
|
||||
const JobMonitorPage = lazy(() => import('./pages/monitor/JobMonitorPage'));
|
||||
const CacheMonitorPage = lazy(() => import('./pages/monitor/CacheMonitorPage'));
|
||||
const LoginLogPage = lazy(() => import('./pages/monitor/LoginLogPage'));
|
||||
const OnlineUserPage = lazy(() => import('./pages/monitor/OnlineUserPage'));
|
||||
const OperationLogPage = lazy(() => import('./pages/monitor/OperationLogPage'));
|
||||
const ServerMonitorPage = lazy(() => import('./pages/monitor/ServerMonitorPage'));
|
||||
const CacheListPage = lazy(() => import('./pages/monitor/CacheListPage'));
|
||||
const UserPage = lazy(() => import('./pages/system/UserPage'));
|
||||
const RolePage = lazy(() => import('./pages/system/RolePage'));
|
||||
const MenuPage = lazy(() => import('./pages/system/MenuPage'));
|
||||
const DeptPage = lazy(() => import('./pages/system/DeptPage'));
|
||||
const DictPage = lazy(() => import('./pages/system/DictPage'));
|
||||
const ConfigPage = lazy(() => import('./pages/system/ConfigPage'));
|
||||
const ProjectPage = lazy(() => import('./pages/project/ProjectPage'));
|
||||
const ProjectDetailPage = lazy(() => import('./pages/project/ProjectDetailPage'));
|
||||
const DemandManagePage = lazy(() => import('./pages/project/DemandManagePage'));
|
||||
const TaskSetPage = lazy(() => import('./pages/workAppraisal/TaskSetPage'));
|
||||
const ManagerPage = lazy(() => import('./pages/workAppraisal/ManagerPage'));
|
||||
const ManagerUserPage = lazy(() => import('./pages/workAppraisal/ManagerUserPage'));
|
||||
const AppraisalDetailPage = lazy(() => import('./pages/workAppraisal/AppraisalDetailPage'));
|
||||
const NormalWorkerPage = lazy(() => import('./pages/workAppraisal/NormalWorkerPage'));
|
||||
const AppraisalDashboardPage = lazy(() => import('./pages/workAppraisal/AppraisalDashboardPage'));
|
||||
const AppraisalModuleDetailPage = lazy(() => import('./pages/workAppraisal/AppraisalModuleDetailPage'));
|
||||
const WorkLogPage = lazy(() => import('./pages/worklog/WorkLogPage'));
|
||||
const ProjectExecutionPage = lazy(() => import('./pages/dashboard/ProjectExecutionPage'));
|
||||
const ProjectUserPage = lazy(() => import('./pages/projectBank/ProjectUserPage'));
|
||||
const UserProjectPage = lazy(() => import('./pages/projectBank/UserProjectPage'));
|
||||
const UserScorePage = lazy(() => import('./pages/projectBank/UserScorePage'));
|
||||
const UserScoreDetailPage = lazy(() => import('./pages/projectBank/UserScoreDetailPage'));
|
||||
const ProfilePage = lazy(() => import('./pages/Profile'));
|
||||
|
||||
const RouteLoading = () => (
|
||||
<div style={{ width: '100%', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const location = useLocation();
|
||||
const token = getToken();
|
||||
const { ready, loading, canAccessPath } = usePermission();
|
||||
const { ready, loading, canAccessPath, defaultRoutePath } = usePermission();
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
|
|
@ -53,8 +61,26 @@ const PrivateRoute = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (location.pathname === '/') {
|
||||
if (defaultRoutePath && defaultRoutePath !== '/') {
|
||||
return <Navigate to={defaultRoutePath} replace />;
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
暂无可访问页面
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canAccessPath(location.pathname)) {
|
||||
return <Navigate to="/index" replace />;
|
||||
if (defaultRoutePath && defaultRoutePath !== location.pathname) {
|
||||
return <Navigate to={defaultRoutePath} replace />;
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
暂无权限访问当前页面
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
|
|
@ -64,48 +90,53 @@ const PrivateRoute = () => {
|
|||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<PermissionProvider><PrivateRoute /></PermissionProvider>}>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<WorkLogPage />} />
|
||||
<Route path="/index" element={<WorkLogPage />} />
|
||||
<Route path="/dashboard/project-execution" element={<ProjectExecutionPage />} />
|
||||
<Route path="/projectBank/projectProgress" element={<ProjectExecutionPage />} />
|
||||
<Route path="/projectBank/projectUser" element={<ProjectUserPage />} />
|
||||
<Route path="/projectBank/userProject" element={<UserProjectPage />} />
|
||||
<Route path="/projectBank/userScore" element={<UserScorePage />} />
|
||||
<Route path="/projectBank/userScoreDetail" element={<UserScoreDetailPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/monitor/cache" element={<CacheMonitorPage />} />
|
||||
<Route path="/monitor/job" element={<JobMonitorPage />} />
|
||||
<Route path="/monitor/logininfor" element={<LoginLogPage />} />
|
||||
<Route path="/monitor/online" element={<OnlineUserPage />} />
|
||||
<Route path="/monitor/operlog" element={<OperationLogPage />} />
|
||||
<Route path="/monitor/server" element={<ServerMonitorPage />} />
|
||||
<Route path="/monitor/cacheList" element={<CacheListPage />} />
|
||||
<Route path="/system/user" element={<UserPage />} />
|
||||
<Route path="/system/role" element={<RolePage />} />
|
||||
<Route path="/system/menu" element={<MenuPage />} />
|
||||
<Route path="/system/dept" element={<DeptPage />} />
|
||||
<Route path="/system/dict" element={<DictPage />} />
|
||||
<Route path="/system/config" element={<ConfigPage />} />
|
||||
<Route path="/project/list" element={<ProjectPage />} />
|
||||
<Route path="/project/detail" element={<ProjectDetailPage />} />
|
||||
<Route path="/project/demandManage" element={<DemandManagePage />} />
|
||||
<Route path="/demandManage" element={<DemandManagePage />} />
|
||||
<Route path="/workAppraisal/manager" element={<ManagerPage />} />
|
||||
<Route path="/workAppraisal/normalWorker" element={<NormalWorkerPage />} />
|
||||
<Route path="/workAppraisal/managerUser" element={<ManagerUserPage />} />
|
||||
<Route path="/workAppraisal/taskSet" element={<TaskSetPage />} />
|
||||
<Route path="/workAppraisal/detail" element={<AppraisalDetailPage />} />
|
||||
<Route path="/workAppraisal/taskModule" element={<AppraisalDashboardPage />} />
|
||||
<Route path="/workAppraisal/dashboard" element={<AppraisalDashboardPage />} />
|
||||
<Route path="/workAppraisal/moduleDetail" element={<AppraisalModuleDetailPage />} />
|
||||
<Route path="/workAppraisal/myPerformance" element={<UserScorePage />} />
|
||||
<Suspense fallback={<RouteLoading />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<PermissionProvider><PrivateRoute /></PermissionProvider>}>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<WorkLogPage />} />
|
||||
<Route path="/index" element={<WorkLogPage />} />
|
||||
<Route path="/dashboard/project-execution" element={<ProjectExecutionPage />} />
|
||||
<Route path="/projectBank/projectProgress" element={<ProjectExecutionPage />} />
|
||||
<Route path="/projectBank/projectUser" element={<ProjectUserPage />} />
|
||||
<Route path="/projectBank/userProject" element={<UserProjectPage />} />
|
||||
<Route path="/projectBank/userScore" element={<UserScorePage />} />
|
||||
<Route path="/projectBank/userScoreDetail" element={<UserScoreDetailPage />} />
|
||||
<Route path="/user/profile" element={<ProfilePage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/monitor/cache" element={<CacheMonitorPage />} />
|
||||
<Route path="/monitor/job" element={<JobMonitorPage />} />
|
||||
<Route path="/monitor/logininfor" element={<LoginLogPage />} />
|
||||
<Route path="/system/logininfor" element={<LoginLogPage />} />
|
||||
<Route path="/monitor/online" element={<OnlineUserPage />} />
|
||||
<Route path="/monitor/operlog" element={<OperationLogPage />} />
|
||||
<Route path="/system/operlog" element={<OperationLogPage />} />
|
||||
<Route path="/monitor/server" element={<ServerMonitorPage />} />
|
||||
<Route path="/monitor/cacheList" element={<CacheListPage />} />
|
||||
<Route path="/system/user" element={<UserPage />} />
|
||||
<Route path="/system/role" element={<RolePage />} />
|
||||
<Route path="/system/menu" element={<MenuPage />} />
|
||||
<Route path="/system/dept" element={<DeptPage />} />
|
||||
<Route path="/system/dict" element={<DictPage />} />
|
||||
<Route path="/system/config" element={<ConfigPage />} />
|
||||
<Route path="/project/list" element={<ProjectPage />} />
|
||||
<Route path="/project/detail" element={<ProjectDetailPage />} />
|
||||
<Route path="/project/demandManage" element={<DemandManagePage />} />
|
||||
<Route path="/demandManage" element={<DemandManagePage />} />
|
||||
<Route path="/workAppraisal/manager" element={<ManagerPage />} />
|
||||
<Route path="/workAppraisal/normalWorker" element={<NormalWorkerPage />} />
|
||||
<Route path="/workAppraisal/managerUser" element={<ManagerUserPage />} />
|
||||
<Route path="/workAppraisal/taskSet" element={<TaskSetPage />} />
|
||||
<Route path="/workAppraisal/detail" element={<AppraisalDetailPage />} />
|
||||
<Route path="/workAppraisal/taskModule" element={<AppraisalDashboardPage />} />
|
||||
<Route path="/workAppraisal/dashboard" element={<AppraisalDashboardPage />} />
|
||||
<Route path="/workAppraisal/moduleDetail" element={<AppraisalModuleDetailPage />} />
|
||||
<Route path="/workAppraisal/myPerformance" element={<UserScorePage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,3 +30,11 @@ export function updateUserPwd(oldPassword: string, newPassword: string) {
|
|||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadUserAvatar(data: FormData) {
|
||||
return request<{ imgUrl?: string }, FormData>({
|
||||
url: '/system/user/profile/avatar',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
<base64_decode_as_bytes>g/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEAAQBIAAAAAQABOEJJTQQGAAAAAAAEAAAAAjhCSU0EAgAAAAAABAAAAAD/4Q/gaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MCA3OS4xNjA0NTEsIDIwMTcvMDUvMDYtMDE6MDg6MjEgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIiBwZGY6UHJvZHVjZXI9IkNvbnZlcnQgZnJvbSBBcHBsZSBCb29rIHRvIFBERiAoQXBwbGUgSW5jLikgLSBRdWFydHogMi4wIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZBNjczQjlCQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZBNjczQjlBQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzIyQjE1NTM4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzIyQjE1NTQ4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAOGAAEABgAGgAfABdhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGZ3dHB0AAABwAAAABRia3B0AAAB1AAAABRyWFlaAAAB7AAAABRnWFlaAAACAAAAABRiWFlaAAACFAAAABRyVFJDAAACGAAQAMBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRsdW1pAAACjAAAAA埬</base64_decode_as_bytes>
|
||||
<base64_decode_as_bytes>g/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEAAQBIAAAAAQABOEJJTQQGAAAAAAAEAAAAAjhCSU0EAgAAAAAABAAAAAD/4Q/gaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MCA3OS4xNjA0NTEsIDIwMTcvMDUvMDYtMDE6MDg6MjEgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIiBwZGY6UHJvZHVjZXI9IkNvbnZlcnQgZnJvbSBBcHBsZSBCb29rIHRvIFBERiAoQXBwbGUgSW5jLikgLSBRdWFydHogMi4wIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZBNjczQjlCQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZBNjczQjlBQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzIyQjE1NTM4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzIyQjE1NTQ4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAOGAAEABgAGgAfABdhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGZ3dHB0AAABwAAAABRia3B0AAAB1AAAABRyWFlaAAAB7AAAABRnWFlaAAACAAAAABRiWFlaAAACFAAAABRyVFJDAAACGAAQAMBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRsdW1pAAACjAAAAA埬</base64_decode_as_bytes>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
|
@ -1,9 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Button, Space } from 'antd';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Button, Dropdown, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { DownOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { removeToken } from '../../utils/auth';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import './navbar.css';
|
||||
|
||||
interface AppNavbarProps {
|
||||
collapsed: boolean;
|
||||
|
|
@ -18,26 +20,51 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
|||
navigate('/login');
|
||||
};
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '个人中心',
|
||||
},
|
||||
{
|
||||
type: 'divider' as const,
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
if (key === 'profile') {
|
||||
navigate('/user/profile');
|
||||
return;
|
||||
}
|
||||
if (key === 'logout') {
|
||||
handleLogout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingRight: '20px' }}>
|
||||
<div className="app-navbar">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
className="app-navbar-toggle"
|
||||
/>
|
||||
<Space style={{ marginLeft: 'auto' }}>
|
||||
<Button type="text" icon={<UserOutlined />} onClick={() => navigate('/profile')}>
|
||||
{userName || '用户'}
|
||||
</Button>
|
||||
<Button type="link" onClick={handleLogout}>
|
||||
退出登录
|
||||
</Button>
|
||||
</Space>
|
||||
<div className="app-navbar-meta">
|
||||
<Dropdown menu={{ items: menuItems, onClick: handleMenuClick }} trigger={['click']}>
|
||||
<Button type="text" className="app-navbar-user">
|
||||
<Space size={10}>
|
||||
<Avatar size={36} className="app-navbar-avatar" icon={<UserOutlined />} />
|
||||
<span className="app-navbar-user-name">{userName || '用户'}</span>
|
||||
<DownOutlined className="app-navbar-chevron" />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
.app-navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 74px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 14px 36px rgba(22, 32, 51, 0.05);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.app-navbar-toggle.ant-btn {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 46px;
|
||||
min-width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
color: #384866;
|
||||
background: linear-gradient(180deg, rgba(247, 249, 255, 0.96), rgba(255, 255, 255, 0.96));
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
}
|
||||
|
||||
.app-navbar-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.app-navbar-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.app-navbar-kicker {
|
||||
color: #7d8cab;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-navbar-title {
|
||||
color: #1a2439;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-navbar-user.ant-btn {
|
||||
height: 48px;
|
||||
padding: 6px 10px 6px 8px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(246, 249, 255, 0.96));
|
||||
}
|
||||
|
||||
.app-navbar-avatar {
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.18), rgba(25, 180, 184, 0.18));
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.app-navbar-user-name {
|
||||
color: #162033;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-navbar-chevron {
|
||||
font-size: 12px;
|
||||
color: #7d8cab;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-navbar {
|
||||
min-height: 64px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.app-navbar-copy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-navbar-user-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import '@/styles/permission-link.css';
|
||||
|
||||
interface ReadonlyActionProps {
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
danger?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ReadonlyAction: React.FC<ReadonlyActionProps> = ({
|
||||
icon,
|
||||
children,
|
||||
danger = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const classes = ['permission-link-disabled', danger ? 'is-danger' : '', className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<span className={classes}>
|
||||
{icon}
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReadonlyAction;
|
||||
|
|
@ -30,17 +30,23 @@ import {
|
|||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import type { RouterNode } from '@/api/permission';
|
||||
import './sidebar.css';
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
type MenuItemWithChildren = MenuItem & { children?: MenuItem[] };
|
||||
|
||||
const BACKEND_PATH_TO_APP_PATH: Record<string, string> = {
|
||||
'/project': '/project/list',
|
||||
'/projectBank/userScore': '/workAppraisal/myPerformance',
|
||||
'/system/logininfor': '/monitor/logininfor',
|
||||
'/system/operlog': '/monitor/operlog',
|
||||
};
|
||||
|
||||
const MENU_ROUTE_ALIASES: Record<string, string[]> = {
|
||||
'/projectBank/projectProgress': ['/dashboard/project-execution'],
|
||||
'/workAppraisal/myPerformance': ['/projectBank/userScore'],
|
||||
'/monitor/logininfor': ['/system/logininfor'],
|
||||
'/monitor/operlog': ['/system/operlog'],
|
||||
};
|
||||
|
||||
const ICON_MAP: Record<string, React.ReactNode> = {
|
||||
|
|
@ -185,6 +191,8 @@ const buildMenuItems = (
|
|||
};
|
||||
|
||||
const isRouteKey = (key: React.Key): key is string => typeof key === 'string' && key.startsWith('/');
|
||||
const getMenuItemChildren = (item: MenuItem): MenuItem[] | undefined =>
|
||||
(item as MenuItemWithChildren).children;
|
||||
|
||||
const findMatchedPath = (menuItems: MenuItem[], pathname: string): string | null => {
|
||||
for (const item of menuItems) {
|
||||
|
|
@ -192,7 +200,7 @@ const findMatchedPath = (menuItems: MenuItem[], pathname: string): string | null
|
|||
continue;
|
||||
}
|
||||
|
||||
const children = item.children as MenuItem[] | undefined;
|
||||
const children = getMenuItemChildren(item);
|
||||
if (Array.isArray(children) && children.length > 0) {
|
||||
const childHit = findMatchedPath(children, pathname);
|
||||
if (childHit) {
|
||||
|
|
@ -200,14 +208,15 @@ const findMatchedPath = (menuItems: MenuItem[], pathname: string): string | null
|
|||
}
|
||||
}
|
||||
|
||||
if (isRouteKey(item.key)) {
|
||||
const aliases = MENU_ROUTE_ALIASES[item.key] ?? [];
|
||||
const itemKey = item.key;
|
||||
if (itemKey !== undefined && isRouteKey(itemKey)) {
|
||||
const aliases = MENU_ROUTE_ALIASES[itemKey] ?? [];
|
||||
if (
|
||||
item.key === pathname ||
|
||||
pathname.startsWith(`${item.key}/`) ||
|
||||
itemKey === pathname ||
|
||||
pathname.startsWith(`${itemKey}/`) ||
|
||||
aliases.some((alias) => alias === pathname || pathname.startsWith(`${alias}/`))
|
||||
) {
|
||||
return item.key;
|
||||
return itemKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -229,7 +238,7 @@ const findParentKeys = (
|
|||
return parentKeys;
|
||||
}
|
||||
|
||||
const children = item.children as MenuItem[] | undefined;
|
||||
const children = getMenuItemChildren(item);
|
||||
if (Array.isArray(children) && children.length > 0) {
|
||||
const found = findParentKeys(children, targetKey, [...parentKeys, key]);
|
||||
if (found) {
|
||||
|
|
@ -269,7 +278,7 @@ const AppSidebar: React.FC = () => {
|
|||
|
||||
return (
|
||||
<Menu
|
||||
theme="dark"
|
||||
className="app-sidebar-menu"
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
openKeys={manualOpenKeys}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
.app-sidebar-menu.ant-menu {
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border-inline-end: none;
|
||||
color: #5f6f92;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item,
|
||||
.app-sidebar-menu .ant-menu-submenu-title {
|
||||
height: 46px;
|
||||
margin: 4px 0;
|
||||
line-height: 46px;
|
||||
border-radius: 14px;
|
||||
font-weight: 600;
|
||||
transition: background-color 180ms ease, color 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item .ant-menu-title-content,
|
||||
.app-sidebar-menu .ant-menu-submenu-title .ant-menu-title-content {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item .anticon,
|
||||
.app-sidebar-menu .ant-menu-submenu-title .anticon {
|
||||
color: #7d8cab;
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item:hover,
|
||||
.app-sidebar-menu .ant-menu-submenu-title:hover {
|
||||
background: rgba(79, 124, 255, 0.08);
|
||||
color: #172138;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item:hover .anticon,
|
||||
.app-sidebar-menu .ant-menu-submenu-title:hover .anticon {
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item-selected {
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.14), rgba(79, 124, 255, 0.08)) !important;
|
||||
color: #4f46e5 !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(99, 91, 255, 0.12);
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-item-selected .anticon,
|
||||
.app-sidebar-menu .ant-menu-submenu-selected > .ant-menu-submenu-title .anticon {
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-sub.ant-menu-inline {
|
||||
margin: 4px 0 8px;
|
||||
padding: 6px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.72);
|
||||
border-radius: 18px;
|
||||
background: rgba(246, 249, 255, 0.7);
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-sub.ant-menu-inline > .ant-menu-item {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.app-sidebar-menu .ant-menu-submenu-arrow {
|
||||
color: #98a6c2;
|
||||
}
|
||||
|
||||
.app-sidebar-menu.ant-menu-inline-collapsed {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-sidebar-menu.ant-menu-inline-collapsed .ant-menu-item,
|
||||
.app-sidebar-menu.ant-menu-inline-collapsed .ant-menu-submenu-title {
|
||||
padding-inline: calc(50% - 16px) !important;
|
||||
}
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { getInfo, getRouters, type RouterNode } from '@/api/permission';
|
||||
import { getUserProfile } from '@/api/user';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { notify } from '@/utils/notify';
|
||||
|
||||
interface PermissionContextValue {
|
||||
loading: boolean;
|
||||
ready: boolean;
|
||||
isAdmin: boolean;
|
||||
userName: string;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
routers: RouterNode[];
|
||||
defaultRoutePath: string;
|
||||
refreshPermissions: () => Promise<void>;
|
||||
hasRole: (roles: string | string[]) => boolean;
|
||||
hasPermi: (permissions: string | string[]) => boolean;
|
||||
|
|
@ -19,12 +21,16 @@ interface PermissionContextValue {
|
|||
|
||||
const PermissionContext = createContext<PermissionContextValue | null>(null);
|
||||
|
||||
const ALWAYS_ALLOW_PATHS = new Set([
|
||||
'/',
|
||||
'/index',
|
||||
'/profile',
|
||||
]);
|
||||
const ALWAYS_ALLOW_PATHS = new Set(['/', '/profile', '/user/profile']);
|
||||
const BACKEND_PATH_TO_APP_PATH: Record<string, string> = {
|
||||
'/project': '/project/list',
|
||||
'/projectBank/userScore': '/workAppraisal/myPerformance',
|
||||
'/system/logininfor': '/monitor/logininfor',
|
||||
'/system/operlog': '/monitor/operlog',
|
||||
};
|
||||
const ROUTE_ALIASES: Record<string, string[]> = {
|
||||
'/profile': ['/user/profile'],
|
||||
'/user/profile': ['/profile'],
|
||||
'/demandManage': ['/project/demandManage'],
|
||||
'/workAppraisal/dashboard': ['/workAppraisal/taskModule'],
|
||||
'/dashboard/project-execution': ['/projectBank/projectProgress'],
|
||||
|
|
@ -33,10 +39,11 @@ const ROUTE_ALIASES: Record<string, string[]> = {
|
|||
'/workAppraisal/myPerformance': ['/projectBank/userScore'],
|
||||
'/projectBank/userScore': ['/workAppraisal/myPerformance'],
|
||||
'/projectBank/userScoreDetail': ['/workAppraisal/myPerformance'],
|
||||
'/monitor/logininfor': ['/system/logininfor'],
|
||||
'/monitor/operlog': ['/system/operlog'],
|
||||
};
|
||||
const SUPER_PERMI = '*:*:*';
|
||||
const ADMIN_ROLE = 'admin';
|
||||
const ADMIN_ROLE_ALIASES = ['admin', '超级管理员', 'superadmin'];
|
||||
|
||||
const normalizePath = (rawPath: string) => {
|
||||
const path = rawPath.split('?')[0]?.split('#')[0] ?? '';
|
||||
|
|
@ -65,6 +72,17 @@ const joinPath = (base: string, child: string) => {
|
|||
return normalizePath(`${normalizedBase}/${child}`);
|
||||
};
|
||||
|
||||
const resolveAppPath = (fullBackendPath: string, node?: Partial<RouterNode>) => {
|
||||
const normalized = normalizePath(fullBackendPath);
|
||||
if (normalized === '/index' || String(node?.component ?? '').includes('worklog')) {
|
||||
return '/index';
|
||||
}
|
||||
if (normalized === '/project' || String(node?.component ?? '') === 'project/list') {
|
||||
return '/project/list';
|
||||
}
|
||||
return BACKEND_PATH_TO_APP_PATH[normalized] ?? normalized;
|
||||
};
|
||||
|
||||
const parseStringList = (input: unknown) => {
|
||||
if (!Array.isArray(input)) {
|
||||
return [] as string[];
|
||||
|
|
@ -109,22 +127,21 @@ const extractRouteNodes = (payload: unknown): RouterNode[] => {
|
|||
return [];
|
||||
};
|
||||
|
||||
const isAdminRoleName = (role: string) => {
|
||||
const roleText = role.trim().toLowerCase();
|
||||
return ADMIN_ROLE_ALIASES.some((alias) => roleText === alias.toLowerCase());
|
||||
};
|
||||
|
||||
const flattenRouterPaths = (routes: RouterNode[]) => {
|
||||
const pathSet = new Set<string>();
|
||||
|
||||
const walk = (nodes: RouterNode[], parentPath: string) => {
|
||||
nodes.forEach((node) => {
|
||||
const currentPath = joinPath(parentPath, String(node.path ?? ''));
|
||||
if (currentPath && currentPath !== '/') {
|
||||
pathSet.add(currentPath);
|
||||
const backendPath = joinPath(parentPath, String(node.path ?? ''));
|
||||
const appPath = resolveAppPath(backendPath, node);
|
||||
if (backendPath && backendPath !== '/') {
|
||||
pathSet.add(backendPath);
|
||||
}
|
||||
if (appPath && appPath !== '/') {
|
||||
pathSet.add(appPath);
|
||||
}
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
walk(node.children, currentPath);
|
||||
walk(node.children, backendPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -133,6 +150,33 @@ const flattenRouterPaths = (routes: RouterNode[]) => {
|
|||
return pathSet;
|
||||
};
|
||||
|
||||
const findFirstVisibleRoutePath = (routes: RouterNode[], parentPath = '/'): string => {
|
||||
for (const node of routes) {
|
||||
if (node.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const backendPath = joinPath(parentPath, String(node.path ?? ''));
|
||||
const children = Array.isArray(node.children) ? node.children : [];
|
||||
if (children.length > 0) {
|
||||
const childPath = findFirstVisibleRoutePath(children, backendPath);
|
||||
if (childPath) {
|
||||
return childPath;
|
||||
}
|
||||
}
|
||||
|
||||
const title = String(node.meta?.title ?? node.name ?? '').trim();
|
||||
if (title) {
|
||||
const appPath = resolveAppPath(backendPath, node);
|
||||
if (appPath && appPath !== '/') {
|
||||
return appPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const matchPathPattern = (allowedPath: string, actualPath: string) => {
|
||||
if (allowedPath === actualPath) {
|
||||
return true;
|
||||
|
|
@ -157,6 +201,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
const [roles, setRoles] = useState<string[]>([]);
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const [routers, setRouters] = useState<RouterNode[]>([]);
|
||||
const [defaultRoutePath, setDefaultRoutePath] = useState('');
|
||||
const [allowedPaths, setAllowedPaths] = useState<Set<string>>(new Set());
|
||||
const [routeGuardEnabled, setRouteGuardEnabled] = useState(false);
|
||||
|
||||
|
|
@ -165,6 +210,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
setRoles([]);
|
||||
setPermissions([]);
|
||||
setRouters([]);
|
||||
setDefaultRoutePath('');
|
||||
setAllowedPaths(new Set());
|
||||
setRouteGuardEnabled(false);
|
||||
}, []);
|
||||
|
|
@ -185,8 +231,10 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
let nextRoles: string[] = [];
|
||||
let nextPermissions: string[] = [];
|
||||
let nextRouters: RouterNode[] = [];
|
||||
let nextAllowedPaths = new Set<string>();
|
||||
let nextRouteGuardEnabled = false;
|
||||
let nextDefaultRoutePath = '/index';
|
||||
let nextAllowedPaths = new Set<string>(['/index', '/profile', '/user/profile']);
|
||||
let nextRouteGuardEnabled = true;
|
||||
let bootstrapFailed = false;
|
||||
|
||||
try {
|
||||
try {
|
||||
|
|
@ -195,37 +243,48 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
nextPermissions = parseStringList(info.permissions);
|
||||
nextUserName = String((info.user as Record<string, unknown> | undefined)?.userName ?? '');
|
||||
} catch (error) {
|
||||
// Fallback for environments where /getInfo is not available yet.
|
||||
const profile = await getUserProfile();
|
||||
nextUserName = String(profile.user?.userName ?? '');
|
||||
const roleGroup = String((profile as Record<string, unknown>).roleGroup ?? '');
|
||||
nextRoles = roleGroup
|
||||
.split(',')
|
||||
.map((role) => role.trim())
|
||||
.filter(Boolean);
|
||||
console.error('Failed to load /getInfo, fallback to profile:', error);
|
||||
try {
|
||||
const profile = await getUserProfile();
|
||||
nextUserName = String(profile.user?.userName ?? '');
|
||||
const roleGroup = String((profile as Record<string, unknown>).roleGroup ?? '');
|
||||
nextRoles = roleGroup
|
||||
.split(',')
|
||||
.map((role) => role.trim())
|
||||
.filter(Boolean);
|
||||
} catch (profileError) {
|
||||
console.error('Failed to load profile fallback:', profileError);
|
||||
bootstrapFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const routersRaw = await getRouters();
|
||||
const routes = extractRouteNodes(routersRaw);
|
||||
nextRouters = routes;
|
||||
nextAllowedPaths = flattenRouterPaths(routes);
|
||||
nextRouteGuardEnabled = true;
|
||||
if (routes.length > 0) {
|
||||
nextRouters = routes;
|
||||
nextAllowedPaths = flattenRouterPaths(routes);
|
||||
nextAllowedPaths.add('/profile');
|
||||
nextAllowedPaths.add('/user/profile');
|
||||
nextDefaultRoutePath = findFirstVisibleRoutePath(routes, '/') || '/index';
|
||||
nextRouteGuardEnabled = true;
|
||||
}
|
||||
} catch (routerError) {
|
||||
console.error('Failed to load router permission data:', routerError);
|
||||
const isAdminUser = nextRoles.some((role) => isAdminRoleName(role));
|
||||
nextAllowedPaths = new Set();
|
||||
nextRouteGuardEnabled = !isAdminUser;
|
||||
bootstrapFailed = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load permission data:', error);
|
||||
message.error('加载权限信息失败');
|
||||
clearPermissionState();
|
||||
bootstrapFailed = true;
|
||||
} finally {
|
||||
if (bootstrapFailed && nextUserName === '' && nextRoles.length === 0 && nextRouters.length === 0) {
|
||||
notify.warning('权限信息加载超时,已使用基础访问模式');
|
||||
}
|
||||
setUserName(nextUserName);
|
||||
setRoles(nextRoles);
|
||||
setPermissions(nextPermissions);
|
||||
setRouters(nextRouters);
|
||||
setDefaultRoutePath(nextDefaultRoutePath);
|
||||
setAllowedPaths(nextAllowedPaths);
|
||||
setRouteGuardEnabled(nextRouteGuardEnabled);
|
||||
setLoading(false);
|
||||
|
|
@ -241,7 +300,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
const permissionSet = useMemo(() => new Set(permissions), [permissions]);
|
||||
|
||||
const isAdmin = useCallback(
|
||||
() => roleSet.has(ADMIN_ROLE) || Array.from(roleSet).some((role) => isAdminRoleName(role)),
|
||||
() => roleSet.has(ADMIN_ROLE),
|
||||
[roleSet],
|
||||
);
|
||||
|
||||
|
|
@ -258,11 +317,11 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
|
||||
const hasPermi = useCallback(
|
||||
(required: string | string[]) => {
|
||||
if (isAdmin() || permissionSet.has(SUPER_PERMI)) {
|
||||
if (isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
const targets = Array.isArray(required) ? required : [required];
|
||||
return targets.some((item) => permissionSet.has(item));
|
||||
return targets.some((item) => permissionSet.has(item) || permissionSet.has(SUPER_PERMI));
|
||||
},
|
||||
[isAdmin, permissionSet],
|
||||
);
|
||||
|
|
@ -310,16 +369,18 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
() => ({
|
||||
loading,
|
||||
ready,
|
||||
isAdmin: isAdmin(),
|
||||
userName,
|
||||
roles,
|
||||
permissions,
|
||||
routers,
|
||||
defaultRoutePath,
|
||||
refreshPermissions,
|
||||
hasRole,
|
||||
hasPermi,
|
||||
canAccessPath,
|
||||
}),
|
||||
[loading, ready, userName, roles, permissions, routers, refreshPermissions, hasRole, hasPermi, canAccessPath],
|
||||
[loading, ready, isAdmin, userName, roles, permissions, routers, defaultRoutePath, refreshPermissions, hasRole, hasPermi, canAccessPath],
|
||||
);
|
||||
|
||||
return <PermissionContext.Provider value={value}>{children}</PermissionContext.Provider>;
|
||||
|
|
|
|||
459
src/index.css
459
src/index.css
|
|
@ -1,9 +1,462 @@
|
|||
:root {
|
||||
--font-sans: "Plus Jakarta Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--color-bg: #f6f8fc;
|
||||
--color-bg-soft: #f8faff;
|
||||
--color-surface: rgba(255, 255, 255, 0.92);
|
||||
--color-surface-strong: #ffffff;
|
||||
--color-panel: #f4f7ff;
|
||||
--color-text: #162033;
|
||||
--color-text-soft: #5f6f92;
|
||||
--color-text-mute: #7d8cab;
|
||||
--color-border: #dbe4f3;
|
||||
--color-border-soft: #e9eef7;
|
||||
--color-primary: #635bff;
|
||||
--color-primary-strong: #4f46e5;
|
||||
--color-primary-soft: rgba(99, 91, 255, 0.1);
|
||||
--color-accent: #19b4b8;
|
||||
--color-success-soft: rgba(16, 185, 129, 0.12);
|
||||
--color-warning-soft: rgba(245, 158, 11, 0.14);
|
||||
--color-danger-soft: rgba(239, 68, 68, 0.12);
|
||||
--shadow-sm: 0 8px 18px rgba(22, 32, 51, 0.06);
|
||||
--shadow-md: 0 16px 40px rgba(22, 32, 51, 0.08);
|
||||
--shadow-lg: 0 28px 60px rgba(72, 84, 159, 0.12);
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 22px;
|
||||
--radius-pill: 999px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--motion-base: 180ms ease;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(99, 91, 255, 0.12), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.08), transparent 24%),
|
||||
linear-gradient(180deg, #f8faff 0%, #f4f7fc 100%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7)),
|
||||
linear-gradient(90deg, rgba(219, 228, 243, 0.18) 1px, transparent 1px),
|
||||
linear-gradient(rgba(219, 228, 243, 0.12) 1px, transparent 1px);
|
||||
background-size: auto, 36px 36px, 36px 36px;
|
||||
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.34), transparent 80%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(99, 91, 255, 0.18);
|
||||
}
|
||||
|
||||
.app-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.app-container > .ant-card,
|
||||
.app-container > .ant-table-wrapper,
|
||||
.app-container > .ant-tabs,
|
||||
.app-container > .ant-spin-nested-loading,
|
||||
.app-container > .ant-row {
|
||||
animation: fade-up var(--motion-base);
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 14px;
|
||||
align-items: flex-start;
|
||||
padding: 18px 20px 4px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(246, 249, 255, 0.96));
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.search-form .ant-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.search-form .ant-form-item:last-child .ant-form-item-control-input-content,
|
||||
.dashboard-search .ant-form-item:last-child .ant-form-item-control-input-content,
|
||||
.task-set-search .ant-form-item:last-child .ant-form-item-control-input-content,
|
||||
.demand-search-form .ant-form-item:last-child .ant-form-item-control-input-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-form .ant-form-item:last-child .ant-btn,
|
||||
.dashboard-search .ant-form-item:last-child .ant-btn,
|
||||
.task-set-search .ant-form-item:last-child .ant-btn,
|
||||
.demand-search-form .ant-form-item:last-child .ant-btn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.search-form .ant-form-item-label > label,
|
||||
.search-form .ant-form-item .ant-form-item-label > label {
|
||||
color: var(--color-text-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.mb8 .ant-btn,
|
||||
.search-form .ant-btn {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th,
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
|
||||
.ant-table-pagination-right {
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border: 1px solid rgba(219, 228, 243, 0.86);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 251, 255, 0.98));
|
||||
transition: transform var(--motion-base), box-shadow var(--motion-base), border-color var(--motion-base);
|
||||
}
|
||||
|
||||
.ant-card:hover {
|
||||
border-color: rgba(99, 91, 255, 0.18);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.ant-card .ant-card-head {
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
}
|
||||
|
||||
.ant-card .ant-card-head-title {
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
font-weight: 600;
|
||||
transition: transform var(--motion-base), box-shadow var(--motion-base), border-color var(--motion-base), background-color var(--motion-base);
|
||||
}
|
||||
|
||||
.ant-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ant-btn:disabled,
|
||||
.ant-btn[disabled],
|
||||
.ant-btn.ant-btn-disabled,
|
||||
.ant-btn-primary:disabled,
|
||||
.ant-btn-primary[disabled],
|
||||
.ant-btn-primary.ant-btn-disabled {
|
||||
color: #7b88a6 !important;
|
||||
background: #eef3fb !important;
|
||||
border-color: #d7e1f0 !important;
|
||||
box-shadow: none !important;
|
||||
opacity: 1 !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.ant-btn:disabled:hover,
|
||||
.ant-btn[disabled]:hover,
|
||||
.ant-btn.ant-btn-disabled:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
background: linear-gradient(135deg, #635bff 0%, #4f46e5 100%);
|
||||
border-color: transparent;
|
||||
color: #ffffff !important;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.ant-btn-primary .anticon,
|
||||
.ant-btn-primary > span {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ant-btn-default,
|
||||
.ant-btn-color-default {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.ant-btn-dangerous.ant-btn-variant-solid,
|
||||
.ant-btn-color-dangerous.ant-btn-variant-solid {
|
||||
box-shadow: 0 10px 24px rgba(239, 68, 68, 0.18);
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.ant-btn-dangerous.ant-btn-variant-solid .anticon,
|
||||
.ant-btn-dangerous.ant-btn-variant-solid > span,
|
||||
.ant-btn-color-dangerous.ant-btn-variant-solid .anticon,
|
||||
.ant-btn-color-dangerous.ant-btn-variant-solid > span {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-picker,
|
||||
.ant-select-selector,
|
||||
.ant-input-number,
|
||||
.ant-input-number-affix-wrapper {
|
||||
border-color: var(--color-border) !important;
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
box-shadow: none !important;
|
||||
transition: border-color var(--motion-base), box-shadow var(--motion-base), background-color var(--motion-base);
|
||||
}
|
||||
|
||||
.ant-input:hover,
|
||||
.ant-input-affix-wrapper:hover,
|
||||
.ant-picker:hover,
|
||||
.ant-select-selector:hover,
|
||||
.ant-input-number:hover,
|
||||
.ant-input-number-affix-wrapper:hover {
|
||||
border-color: rgba(99, 91, 255, 0.38) !important;
|
||||
}
|
||||
|
||||
.ant-input:focus,
|
||||
.ant-input-focused,
|
||||
.ant-input-affix-wrapper-focused,
|
||||
.ant-picker-focused,
|
||||
.ant-select-focused .ant-select-selector,
|
||||
.ant-input-number-focused,
|
||||
.ant-input-number-affix-wrapper-focused {
|
||||
border-color: rgba(99, 91, 255, 0.55) !important;
|
||||
box-shadow: 0 0 0 4px rgba(99, 91, 255, 0.12) !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.ant-form-item-explain,
|
||||
.ant-form-item-extra {
|
||||
color: var(--color-text-mute);
|
||||
}
|
||||
|
||||
.ant-table-wrapper {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(219, 228, 243, 0.86);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 700;
|
||||
color: #34425f;
|
||||
background: #f7f9fe !important;
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #eef2f9;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||
background: #f8faff !important;
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
margin: 18px 0 4px;
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item,
|
||||
.ant-pagination .ant-pagination-prev,
|
||||
.ant-pagination .ant-pagination-next {
|
||||
border-color: var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.ant-pagination .ant-pagination-item-active {
|
||||
border-color: rgba(99, 91, 255, 0.2);
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
padding-inline: 10px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-content,
|
||||
.ant-drawer .ant-drawer-content {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
|
||||
border: 1px solid rgba(219, 228, 243, 0.92);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-header,
|
||||
.ant-drawer .ant-drawer-header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-title,
|
||||
.ant-drawer .ant-drawer-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item-label > label,
|
||||
.ant-drawer .ant-form-horizontal .ant-form-item-label > label {
|
||||
color: var(--color-text-soft);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ant-dropdown .ant-dropdown-menu {
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ant-dropdown .ant-dropdown-menu-item,
|
||||
.ant-dropdown .ant-dropdown-menu-submenu-title {
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ant-tabs .ant-tabs-tab {
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.ant-tabs .ant-tabs-tab-active {
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.ant-alert {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-form {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
.search-form .ant-form-item {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-form .ant-form-item-control,
|
||||
.search-form .ant-form-item-control-input,
|
||||
.search-form .ant-form-item-control-input-content,
|
||||
.search-form .ant-select,
|
||||
.search-form .ant-picker,
|
||||
.search-form .ant-input-affix-wrapper,
|
||||
.search-form .ant-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
max-width: calc(100vw - 24px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-content {
|
||||
padding: 20px 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item .ant-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item-label,
|
||||
.ant-modal .ant-form-horizontal .ant-form-item-control,
|
||||
.ant-modal .ant-form-horizontal .ant-col {
|
||||
max-width: 100%;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item-label {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.ant-modal .ant-form-horizontal .ant-form-item-control {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.ant-card .ant-card-head {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Layout, theme } from 'antd';
|
||||
import { Layout } from 'antd';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import AppSidebar from '../components/Sidebar/index';
|
||||
import AppNavbar from '../components/Navbar';
|
||||
|
|
@ -9,30 +9,29 @@ const { Header, Sider, Content } = Layout;
|
|||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider trigger={null} collapsible collapsed={collapsed}>
|
||||
<div className="logo" />
|
||||
<Layout className="main-layout">
|
||||
<Sider className="main-sider" trigger={null} collapsible collapsed={collapsed} width={260}>
|
||||
<div className="logo">
|
||||
<div className="logo-mark">pms</div>
|
||||
{!collapsed ? (
|
||||
<div className="logo-copy">
|
||||
<span className="logo-kicker">UNISSENSE PMS</span>
|
||||
<strong className="logo-title">新光线平台</strong>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<AppSidebar />
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ padding: 0, background: colorBgContainer }}>
|
||||
<Layout className="main-layout-body">
|
||||
<Header className="main-header">
|
||||
<AppNavbar collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
margin: '24px 16px',
|
||||
padding: 24,
|
||||
minHeight: 280,
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
<Content className="main-content">
|
||||
<div className="main-content-panel">
|
||||
<Outlet />
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,121 @@
|
|||
.logo {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
font-weight: bold;
|
||||
.main-layout {
|
||||
min-height: 100vh;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-sider.ant-layout-sider {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
padding: 18px 14px 18px 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 249, 255, 0.98)) !important;
|
||||
border-right: 1px solid rgba(219, 228, 243, 0.82);
|
||||
box-shadow: 18px 0 44px rgba(22, 32, 51, 0.04);
|
||||
}
|
||||
|
||||
.main-layout-body {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-header.ant-layout-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
padding: 18px 20px 0;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.main-content.ant-layout-content {
|
||||
margin: 0;
|
||||
padding: 18px 20px 20px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.main-content-panel {
|
||||
min-height: calc(100vh - 104px);
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(248, 250, 255, 0.96));
|
||||
box-shadow: 0 20px 48px rgba(22, 32, 51, 0.06);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0 0 18px;
|
||||
padding: 14px 12px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.7);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.08), rgba(25, 180, 184, 0.08));
|
||||
box-shadow: 0 12px 28px rgba(22, 32, 51, 0.05);
|
||||
}
|
||||
|
||||
.logo-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #635bff, #4f46e5);
|
||||
box-shadow: 0 14px 28px rgba(99, 91, 255, 0.28);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.logo-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logo-kicker {
|
||||
color: #6f7ea0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
color: #182238;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.main-sider.ant-layout-sider {
|
||||
position: fixed;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
.main-header.ant-layout-header {
|
||||
padding: 14px 14px 0;
|
||||
}
|
||||
|
||||
.main-content.ant-layout-content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.main-content-panel {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
113
src/main.tsx
113
src/main.tsx
|
|
@ -1,11 +1,120 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { StrictMode, useEffect } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { App as AntdApp, ConfigProvider, theme } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'antd/dist/reset.css';
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { bindMessageApi } from './utils/notify'
|
||||
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const MessageBinder = () => {
|
||||
const { message } = AntdApp.useApp();
|
||||
|
||||
useEffect(() => {
|
||||
bindMessageApi(message);
|
||||
}, [message]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#635bff',
|
||||
colorSuccess: '#10b981',
|
||||
colorWarning: '#f59e0b',
|
||||
colorError: '#ef4444',
|
||||
colorInfo: '#4f7cff',
|
||||
colorBgBase: '#f6f8fc',
|
||||
colorBgLayout: '#f6f8fc',
|
||||
colorBgContainer: '#ffffff',
|
||||
colorText: '#162033',
|
||||
colorTextSecondary: '#5f6f92',
|
||||
colorTextTertiary: '#7d8cab',
|
||||
colorBorder: '#dbe4f3',
|
||||
colorBorderSecondary: '#e9eef7',
|
||||
borderRadius: 14,
|
||||
borderRadiusLG: 20,
|
||||
borderRadiusSM: 10,
|
||||
fontFamily: '"Plus Jakarta Sans", "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
fontSize: 14,
|
||||
fontSizeLG: 16,
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 46,
|
||||
lineWidth: 1,
|
||||
boxShadow: '0 18px 45px rgba(22, 32, 51, 0.08)',
|
||||
boxShadowSecondary: '0 10px 24px rgba(99, 91, 255, 0.10)',
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: 'rgba(255,255,255,0.86)',
|
||||
siderBg: '#ffffff',
|
||||
bodyBg: '#f6f8fc',
|
||||
triggerBg: '#ffffff',
|
||||
triggerColor: '#162033',
|
||||
},
|
||||
Menu: {
|
||||
itemBg: 'transparent',
|
||||
itemColor: '#5f6f92',
|
||||
itemHoverColor: '#162033',
|
||||
itemSelectedColor: '#4f46e5',
|
||||
itemSelectedBg: 'rgba(99, 91, 255, 0.12)',
|
||||
subMenuItemBg: 'transparent',
|
||||
iconSize: 16,
|
||||
collapsedIconSize: 16,
|
||||
itemBorderRadius: 12,
|
||||
},
|
||||
Button: {
|
||||
borderRadius: 12,
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 44,
|
||||
fontWeight: 600,
|
||||
primaryShadow: '0 12px 24px rgba(99, 91, 255, 0.24)',
|
||||
},
|
||||
Card: {
|
||||
borderRadiusLG: 20,
|
||||
boxShadowTertiary: '0 12px 32px rgba(22, 32, 51, 0.06)',
|
||||
},
|
||||
Input: {
|
||||
borderRadius: 12,
|
||||
controlHeight: 40,
|
||||
activeShadow: '0 0 0 4px rgba(99, 91, 255, 0.12)',
|
||||
},
|
||||
Select: {
|
||||
borderRadius: 12,
|
||||
controlHeight: 40,
|
||||
optionSelectedBg: 'rgba(99, 91, 255, 0.1)',
|
||||
},
|
||||
Table: {
|
||||
borderColor: '#e9eef7',
|
||||
headerBg: '#f7f9fe',
|
||||
headerColor: '#34425f',
|
||||
rowHoverBg: '#f8faff',
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 22,
|
||||
},
|
||||
Drawer: {
|
||||
borderRadiusLG: 22,
|
||||
},
|
||||
Tag: {
|
||||
borderRadiusSM: 999,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AntdApp>
|
||||
<MessageBinder />
|
||||
<App />
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,37 +1,213 @@
|
|||
.home-container {
|
||||
font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #676a6c;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.home-container .notification {
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
border-left: 5px solid #eee;
|
||||
.home-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr);
|
||||
gap: 22px;
|
||||
padding: 30px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.16), transparent 28%),
|
||||
radial-gradient(circle at left center, rgba(99, 91, 255, 0.18), transparent 32%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(245, 248, 255, 0.96));
|
||||
box-shadow: 0 18px 44px rgba(22, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.home-container ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
.home-kicker {
|
||||
display: inline-flex;
|
||||
margin-bottom: 16px;
|
||||
color: #5f57f5;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.home-container h4 {
|
||||
margin-top: 0px;
|
||||
.home-hero .ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.home-container h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 100;
|
||||
.home-hero h1.ant-typography {
|
||||
margin-bottom: 12px;
|
||||
color: #162033;
|
||||
font-size: clamp(30px, 4vw, 44px);
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.home-container p {
|
||||
margin-top: 10px;
|
||||
.home-hero-text {
|
||||
display: inline-block;
|
||||
max-width: 680px;
|
||||
color: #5f6f92;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.home-container .ant-card {
|
||||
margin-bottom: 20px;
|
||||
.home-hero-actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.home-hero-panels {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.home-highlight {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 18px 18px 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 12px 28px rgba(22, 32, 51, 0.05);
|
||||
}
|
||||
|
||||
.home-highlight span {
|
||||
color: #7d8cab;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.home-highlight strong {
|
||||
color: #162033;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.home-highlight-purple {
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.12), rgba(255, 255, 255, 0.88));
|
||||
}
|
||||
|
||||
.home-highlight-cyan {
|
||||
background: linear-gradient(135deg, rgba(25, 180, 184, 0.12), rgba(255, 255, 255, 0.88));
|
||||
}
|
||||
|
||||
.home-highlight-slate {
|
||||
background: linear-gradient(135deg, rgba(79, 124, 255, 0.1), rgba(255, 255, 255, 0.88));
|
||||
}
|
||||
|
||||
.home-section-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.home-feature-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-feature {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.76);
|
||||
border-radius: 18px;
|
||||
background: rgba(247, 249, 255, 0.7);
|
||||
}
|
||||
|
||||
.home-feature-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.16), rgba(79, 124, 255, 0.16));
|
||||
color: #4f46e5;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.home-feature h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.home-feature p,
|
||||
.home-notification p,
|
||||
.home-support p {
|
||||
margin: 0;
|
||||
color: #5f6f92;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.home-stack-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-stack-group {
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.76);
|
||||
border-radius: 18px;
|
||||
background: rgba(247, 249, 255, 0.7);
|
||||
}
|
||||
|
||||
.home-stack-head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
color: #162033;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.home-stack-group ul {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.home-stack-group li {
|
||||
color: #5f6f92;
|
||||
}
|
||||
|
||||
.home-notification {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.home-notification .ant-typography,
|
||||
.home-notification .ant-tag {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.home-support {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.home-support img {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.home-hero {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home-hero {
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.home-highlight strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +1,146 @@
|
|||
import { Row, Col, Typography, Button, Tag, Divider, Card, Collapse } from 'antd';
|
||||
import { Row, Col, Typography, Button, Tag, Card, Collapse, Space } from 'antd';
|
||||
import {
|
||||
ArrowRightOutlined,
|
||||
CloudServerOutlined,
|
||||
CodeOutlined,
|
||||
RocketOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './home.css';
|
||||
import payImg from '../../assets/pay.png';
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
const HomePage = () => {
|
||||
const version = "3.8.8"; // From original file
|
||||
const version = "3.8.8";
|
||||
|
||||
const goTarget = (href: string) => {
|
||||
window.open(href, "_blank");
|
||||
};
|
||||
|
||||
const highlights = [
|
||||
{ label: '当前版本', value: `v${version}`, tone: 'purple' },
|
||||
{ label: '产品定位', value: '企业协作平台', tone: 'cyan' },
|
||||
{ label: '部署形态', value: '内网 / 云端', tone: 'slate' },
|
||||
];
|
||||
|
||||
const productValues = [
|
||||
{
|
||||
icon: <RocketOutlined />,
|
||||
title: '统一工作入口',
|
||||
description: '围绕项目、日志、绩效和系统管理组织工作流,减少页面跳转和重复确认。',
|
||||
},
|
||||
{
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
title: '权限清晰可控',
|
||||
description: '页面、按钮、角色数据分层管理,既适合企业管控,也兼顾一线使用效率。',
|
||||
},
|
||||
{
|
||||
icon: <CloudServerOutlined />,
|
||||
title: '稳定交付视角',
|
||||
description: '保留后台业务效率,同时升级成更专业、可长期演进的企业产品界面语言。',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col span={24}>
|
||||
<div className="notification">
|
||||
领取阿里云通用云产品1888优惠券<br />
|
||||
<Link href="https://www.aliyun.com/minisite/goods?userCode=brki8iof" target="_blank">
|
||||
https://www.aliyun.com/minisite/goods?userCode=brki8iof
|
||||
</Link>
|
||||
<br />
|
||||
领取腾讯云通用云产品2860优惠券<br />
|
||||
<Link href="https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console" target="_blank">
|
||||
https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console
|
||||
</Link>
|
||||
<br />
|
||||
阿里云服务器折扣区
|
||||
<Link href="http://aly.ruoyi.vip" target="_blank"> >☛☛点我进入☚☚</Link>
|
||||
腾讯云服务器秒杀区
|
||||
<Link href="http://txy.ruoyi.vip" target="_blank"> >☛☛点我进入☚☚</Link>
|
||||
<br />
|
||||
<Text type="danger" strong>
|
||||
云产品通用红包,可叠加官网常规优惠使用。(仅限新用户)
|
||||
</Text>
|
||||
</div>
|
||||
<section className="home-hero">
|
||||
<div className="home-hero-copy">
|
||||
<span className="home-kicker">UNISSENSE PRODUCT CONSOLE</span>
|
||||
<Title level={1}>现代化企业协作平台</Title>
|
||||
<Text className="home-hero-text">
|
||||
保留原有业务结构和效率,同时升级整体视觉语言,让系统更像正式商业化的 AI SaaS 产品。
|
||||
</Text>
|
||||
<Space wrap className="home-hero-actions">
|
||||
<Button type="primary" size="large" onClick={() => goTarget('https://gitee.com/y_project/RuoYi-Vue')}>
|
||||
查看开源基座
|
||||
</Button>
|
||||
<Button size="large" onClick={() => goTarget('http://ruoyi.vip')}>
|
||||
官方主页
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="home-hero-panels">
|
||||
{highlights.map((item) => (
|
||||
<div key={item.label} className={`home-highlight home-highlight-${item.tone}`}>
|
||||
<span>{item.label}</span>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col xs={24} sm={24} md={12}>
|
||||
<Title level={2}>若依后台管理框架</Title>
|
||||
<p>
|
||||
一直想做一款后台管理系统,看了很多优秀的开源项目但是发现没有合适自己的。于是利用空闲休息时间开始自己写一套后台系统。如此有了若依管理系统...
|
||||
</p>
|
||||
<p><Text strong>当前版本:</Text> <span>v{version}</span></p>
|
||||
<p><Tag color="red">¥免费开源</Tag></p>
|
||||
<p>
|
||||
<Button onClick={() => goTarget('https://gitee.com/y_project/RuoYi-Vue')}>访问码云</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={() => goTarget('http://ruoyi.vip')}>访问主页</Button>
|
||||
</p>
|
||||
<Col xs={24} lg={16}>
|
||||
<Card className="home-section-card" title="产品特性">
|
||||
<div className="home-feature-grid">
|
||||
{productValues.map((item) => (
|
||||
<div key={item.title} className="home-feature">
|
||||
<div className="home-feature-icon">{item.icon}</div>
|
||||
<div>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12}>
|
||||
<Title level={2}>技术选型</Title>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<h4>后端技术</h4>
|
||||
<ul><li>SpringBoot</li><li>Spring Security</li><li>JWT</li><li>MyBatis</li><li>...</li></ul>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<h4>前端技术</h4>
|
||||
<ul><li>Vue</li><li>Vuex</li><li>Element-ui</li><li>Axios</li><li>...</li></ul>
|
||||
</Col>
|
||||
</Row>
|
||||
<Col xs={24} lg={8}>
|
||||
<Card className="home-section-card" title="技术选型">
|
||||
<div className="home-stack-grid">
|
||||
<div className="home-stack-group">
|
||||
<div className="home-stack-head">
|
||||
<CodeOutlined />
|
||||
<span>后端技术</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Spring Boot</li>
|
||||
<li>Spring Security</li>
|
||||
<li>JWT</li>
|
||||
<li>MyBatis</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="home-stack-group">
|
||||
<div className="home-stack-head">
|
||||
<CodeOutlined />
|
||||
<span>前端技术</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>React</li>
|
||||
<li>Ant Design</li>
|
||||
<li>Vite</li>
|
||||
<li>Axios</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card title="联系信息">
|
||||
<p><Link href="http://www.ruoyi.vip" target="_blank">官网:http://www.ruoyi.vip</Link></p>
|
||||
<p>QQ群:<Link href="#" target="_blank">151450850</Link></p>
|
||||
<Card className="home-section-card" title="云资源提示">
|
||||
<div className="home-notification">
|
||||
<p>阿里云通用云产品优惠券</p>
|
||||
<Link href="https://www.aliyun.com/minisite/goods?userCode=brki8iof" target="_blank">
|
||||
aliyun.com/minisite/goods?userCode=brki8iof
|
||||
</Link>
|
||||
<p>腾讯云通用云产品优惠券</p>
|
||||
<Link href="https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console" target="_blank">
|
||||
cloud.tencent.com/redirect.php?redirect=1025
|
||||
</Link>
|
||||
<Tag color="error">仅限新用户,可叠加官网常规优惠使用</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card title="更新日志">
|
||||
<Card className="home-section-card" title="更新日志">
|
||||
<Collapse accordion items={[
|
||||
{
|
||||
key: '1',
|
||||
|
|
@ -90,9 +156,15 @@ const HomePage = () => {
|
|||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card title="捐赠支持">
|
||||
<img src={payImg} alt="donate" style={{ width: '100%' }}/>
|
||||
<Text>你可以请作者喝杯咖啡表示鼓励</Text>
|
||||
<Card className="home-section-card" title="联系与支持">
|
||||
<div className="home-support">
|
||||
<div className="home-support-copy">
|
||||
<p><Link href="http://www.ruoyi.vip" target="_blank">官网:ruoyi.vip</Link></p>
|
||||
<p>QQ群:151450850</p>
|
||||
<Text>你可以通过捐赠支持持续迭代。</Text>
|
||||
</div>
|
||||
<img src={payImg} alt="donate" />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Form, Input, Button, Checkbox, message } from 'antd';
|
||||
import { Form, Input, Button, Checkbox } from 'antd';
|
||||
import { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Cookies from 'js-cookie';
|
||||
import { login, getCodeImg } from '../../api/login';
|
||||
import { TokenKey } from '../../utils/auth';
|
||||
import type { LoginRequest } from '@/types/api';
|
||||
import { notify } from '@/utils/notify';
|
||||
import './login.css';
|
||||
|
||||
interface LoginFormValues {
|
||||
|
|
@ -15,6 +16,22 @@ interface LoginFormValues {
|
|||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
const REMEMBER_USERNAME_KEY = 'remember_username';
|
||||
const REMEMBER_PASSWORD_KEY = 'remember_password';
|
||||
const REMEMBER_ME_KEY = 'remember_me';
|
||||
|
||||
const encodeRememberValue = (value: string) => window.btoa(encodeURIComponent(value));
|
||||
const decodeRememberValue = (value?: string) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(window.atob(value));
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const LoginPage = () => {
|
||||
const [form] = Form.useForm<LoginFormValues>();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -39,7 +56,7 @@ const LoginPage = () => {
|
|||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error('Failed to load captcha image:', error);
|
||||
message.error('Failed to load captcha image, please refresh.');
|
||||
notify.error('Failed to load captcha image, please refresh.');
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -47,19 +64,40 @@ const LoginPage = () => {
|
|||
void fetchCode();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const remembered = Cookies.get(REMEMBER_ME_KEY) === 'true';
|
||||
const username = decodeRememberValue(Cookies.get(REMEMBER_USERNAME_KEY));
|
||||
const password = decodeRememberValue(Cookies.get(REMEMBER_PASSWORD_KEY));
|
||||
|
||||
form.setFieldsValue({
|
||||
rememberMe: remembered,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
const handleLogin = (values: LoginFormValues) => {
|
||||
setLoading(true);
|
||||
const data: LoginRequest = { ...values, uuid };
|
||||
login(data)
|
||||
.then((res) => {
|
||||
message.success('Login successful!');
|
||||
notify.success('Login successful!');
|
||||
const tokenToSet = res.token ?? 'mock_token';
|
||||
Cookies.set(TokenKey, tokenToSet);
|
||||
if (values.rememberMe) {
|
||||
Cookies.set(REMEMBER_ME_KEY, 'true', { expires: 30 });
|
||||
Cookies.set(REMEMBER_USERNAME_KEY, encodeRememberValue(values.username), { expires: 30 });
|
||||
Cookies.set(REMEMBER_PASSWORD_KEY, encodeRememberValue(values.password), { expires: 30 });
|
||||
} else {
|
||||
Cookies.remove(REMEMBER_ME_KEY);
|
||||
Cookies.remove(REMEMBER_USERNAME_KEY);
|
||||
Cookies.remove(REMEMBER_PASSWORD_KEY);
|
||||
}
|
||||
navigate('/');
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Login failed.';
|
||||
message.error(errorMessage);
|
||||
notify.error(errorMessage);
|
||||
if (captchaEnabled) {
|
||||
void fetchCode();
|
||||
}
|
||||
|
|
@ -71,56 +109,83 @@ const LoginPage = () => {
|
|||
|
||||
return (
|
||||
<div className="login">
|
||||
<Form
|
||||
form={form}
|
||||
name="loginForm"
|
||||
className="login-form"
|
||||
initialValues={{ rememberMe: true }}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
<h3 className="title">新光线平台</h3>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入您的账号!' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="账号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入您的密码!' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
|
||||
{captchaEnabled && (
|
||||
<Form.Item>
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{ required: true, message: '请输入验证码!' }]}
|
||||
style={{ display: 'inline-block', width: 'calc(67% - 8px)', marginRight: '8px' }}
|
||||
>
|
||||
<Input prefix={<SafetyOutlined />} placeholder="验证码" />
|
||||
</Form.Item>
|
||||
<div className="login-code">
|
||||
{codeUrl ? <img src={codeUrl} onClick={() => void fetchCode()} alt="Captcha" /> : null}
|
||||
<div className="login-shell">
|
||||
<div className="login-brand-panel">
|
||||
<div className="login-brand-badge">UNISSENSE PMS</div>
|
||||
<h1>新光线管理平台</h1>
|
||||
<p>
|
||||
统一承载项目、日志、绩效和系统管理业务。保持原有流程稳定,同时提供更专业、克制、清晰的产品体验。
|
||||
</p>
|
||||
<div className="login-brand-metrics">
|
||||
<div className="login-metric">
|
||||
<strong>AI-ready</strong>
|
||||
<span>现代化企业工作台</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
<div className="login-metric">
|
||||
<strong>Role-based</strong>
|
||||
<span>页面与按钮权限体系</span>
|
||||
</div>
|
||||
<div className="login-metric">
|
||||
<strong>Responsive</strong>
|
||||
<span>兼顾桌面与移动访问</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
name="loginForm"
|
||||
className="login-form"
|
||||
initialValues={{ rememberMe: true }}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
<div className="login-form-head">
|
||||
<span className="login-kicker">Sign in</span>
|
||||
<h3 className="title">欢迎回来</h3>
|
||||
<p className="login-subtitle">使用你的企业账户进入工作台。</p>
|
||||
</div>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item name="rememberMe" valuePropName="checked" noStyle>
|
||||
<Checkbox>记住密码</Checkbox>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入您的账号!' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="账号" />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="login-form-button" loading={loading} style={{ width: '100%' }}>
|
||||
{loading ? '登 录 中...' : '登 录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入您的密码!' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
|
||||
{captchaEnabled && (
|
||||
<div className="login-code-row">
|
||||
<Form.Item
|
||||
name="code"
|
||||
rules={[{ required: true, message: '请输入验证码!' }]}
|
||||
className="login-code-input"
|
||||
>
|
||||
<Input prefix={<SafetyOutlined />} placeholder="验证码" />
|
||||
</Form.Item>
|
||||
<div className="login-code">
|
||||
{codeUrl ? <img src={codeUrl} onClick={() => void fetchCode()} alt="Captcha" /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item className="login-remember">
|
||||
<Form.Item name="rememberMe" valuePropName="checked" noStyle>
|
||||
<Checkbox>记住密码</Checkbox>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="login-form-button" loading={loading}>
|
||||
{loading ? '登 录 中...' : '登 录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="el-login-footer">
|
||||
<span>unissense.tech</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,63 +1,269 @@
|
|||
.login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-image: url("../../assets/login-background.jpg");
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 28px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(244, 247, 255, 0.84), rgba(246, 250, 255, 0.92)),
|
||||
url("../../assets/login-background.jpg") center/cover no-repeat;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
.login::before,
|
||||
.login::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(8px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.login::before {
|
||||
top: 10%;
|
||||
left: 8%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: radial-gradient(circle, rgba(99, 91, 255, 0.24), transparent 66%);
|
||||
}
|
||||
|
||||
.login::after {
|
||||
right: 8%;
|
||||
bottom: 12%;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
background: radial-gradient(circle, rgba(25, 180, 184, 0.18), transparent 66%);
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1.05fr) minmax(360px, 420px);
|
||||
gap: 22px;
|
||||
width: min(1120px, 100%);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.login-brand-panel,
|
||||
.login-form {
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 28px;
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.login-brand-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 620px;
|
||||
padding: 34px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.14), transparent 28%),
|
||||
radial-gradient(circle at left center, rgba(99, 91, 255, 0.16), transparent 36%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.82), rgba(244, 247, 255, 0.78));
|
||||
box-shadow: 0 24px 60px rgba(22, 32, 51, 0.12);
|
||||
}
|
||||
|
||||
.login-brand-badge {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(99, 91, 255, 0.1);
|
||||
color: #564fee;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.login-brand-panel h1 {
|
||||
max-width: 520px;
|
||||
margin: 22px 0 14px;
|
||||
color: #162033;
|
||||
font-size: clamp(34px, 4vw, 50px);
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.login-brand-panel p {
|
||||
max-width: 560px;
|
||||
margin: 0;
|
||||
color: #5f6f92;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.login-brand-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.login-metric {
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.72);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.login-metric strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #162033;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-metric span {
|
||||
color: #6a7898;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
padding: 32px 30px 22px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 20px 50px rgba(22, 32, 51, 0.1);
|
||||
}
|
||||
|
||||
.login-form-head {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-kicker {
|
||||
color: #635bff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0 8px;
|
||||
color: #162033;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 0;
|
||||
color: #6a7898;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.login-form .ant-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-form .ant-input-affix-wrapper,
|
||||
.login-form .ant-input,
|
||||
.login-form .ant-btn {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.login-form .ant-input-affix-wrapper {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.login-form .ant-input-affix-wrapper > input.ant-input,
|
||||
.login-form .ant-input {
|
||||
height: 100%;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.login-form .ant-input-prefix {
|
||||
margin-right: 8px;
|
||||
margin-right: 10px;
|
||||
color: #7d8cab;
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
.login-code-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 132px;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.login-code-input.ant-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-code-input .ant-form-item-control,
|
||||
.login-code-input .ant-form-item-control-input,
|
||||
.login-code-input .ant-form-item-control-input-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 33%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 46px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
border-radius: 14px;
|
||||
background: rgba(247, 249, 255, 0.84);
|
||||
}
|
||||
|
||||
.login-code img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.login-remember .ant-checkbox-wrapper {
|
||||
color: #6a7898;
|
||||
}
|
||||
|
||||
.login-form-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: 18px;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #74839f;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.login-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.login-brand-panel {
|
||||
min-height: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-brand-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.login {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-form,
|
||||
.login-brand-panel {
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 24px 18px 16px;
|
||||
}
|
||||
|
||||
.login-code-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { CameraOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Upload, message } from 'antd';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { uploadUserAvatar } from '@/api/user';
|
||||
|
||||
interface AvatarUploaderProps {
|
||||
avatarUrl?: string;
|
||||
displayName?: string;
|
||||
onUploaded: (nextAvatar: string) => void;
|
||||
}
|
||||
|
||||
const buildAvatarUrl = (value?: string) => {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
if (raw.startsWith('http://') || raw.startsWith('https://') || raw.startsWith('data:')) {
|
||||
return raw;
|
||||
}
|
||||
return raw.startsWith('/api/') ? raw : `/api${raw.startsWith('/') ? raw : `/${raw}`}`;
|
||||
};
|
||||
|
||||
const AvatarUploader = ({ avatarUrl, displayName, onUploaded }: AvatarUploaderProps) => {
|
||||
const uploadProps: UploadProps = {
|
||||
accept: 'image/*',
|
||||
showUploadList: false,
|
||||
customRequest: async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('avatarfile', file as Blob);
|
||||
const response = await uploadUserAvatar(formData);
|
||||
const nextAvatar = String(response.imgUrl ?? '');
|
||||
onUploaded(nextAvatar);
|
||||
message.success('修改成功');
|
||||
onSuccess?.(response);
|
||||
} catch (error) {
|
||||
message.error('头像上传失败');
|
||||
onError?.(error as Error);
|
||||
}
|
||||
},
|
||||
beforeUpload: (file) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('请上传图片文件');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
message.error('头像大小不能超过 5MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile-avatar-block">
|
||||
<Upload {...uploadProps}>
|
||||
<button type="button" className="profile-avatar-trigger">
|
||||
<Avatar
|
||||
size={108}
|
||||
src={buildAvatarUrl(avatarUrl) || undefined}
|
||||
icon={avatarUrl ? undefined : <UserOutlined />}
|
||||
className="profile-avatar-image"
|
||||
/>
|
||||
<span className="profile-avatar-mask">
|
||||
<CameraOutlined />
|
||||
更换头像
|
||||
</span>
|
||||
</button>
|
||||
</Upload>
|
||||
<div className="profile-avatar-name">{displayName || '未命名用户'}</div>
|
||||
<div className="profile-avatar-tip">支持 JPG、PNG,点击头像直接上传</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarUploader;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { Form, Input, Button, message } from 'antd';
|
||||
import { updateUserPwd } from '../../api/user';
|
||||
import { Button, Form, Input, Space, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { updateUserPwd } from '@/api/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
|
||||
interface ResetPasswordValues {
|
||||
oldPassword: string;
|
||||
|
|
@ -8,62 +10,78 @@ interface ResetPasswordValues {
|
|||
}
|
||||
|
||||
const ResetPassword = () => {
|
||||
const [form] = Form.useForm<ResetPasswordValues>();
|
||||
const [form] = Form.useForm<ResetPasswordValues>();
|
||||
const navigate = useNavigate();
|
||||
const { defaultRoutePath } = usePermission();
|
||||
|
||||
const onFinish = (values: ResetPasswordValues) => {
|
||||
updateUserPwd(values.oldPassword, values.newPassword).then(() => {
|
||||
message.success('修改成功,请重新登录');
|
||||
form.resetFields();
|
||||
}).catch(() => {
|
||||
message.error('修改失败');
|
||||
});
|
||||
};
|
||||
const handleClose = () => {
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
navigate(defaultRoutePath || '/index');
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={onFinish} labelCol={{ span: 4 }} wrapperCol={{ span: 16 }}>
|
||||
<Form.Item
|
||||
label="旧密码"
|
||||
name="oldPassword"
|
||||
rules={[{ required: true, message: '旧密码不能为空' }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
rules={[
|
||||
{ required: true, message: '新密码不能为空' },
|
||||
{ min: 6, message: '长度不能小于6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="确认密码"
|
||||
name="confirmPassword"
|
||||
dependencies={['newPassword']}
|
||||
hasFeedback
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致!'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 4, span: 16 }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
const onFinish = async (values: ResetPasswordValues) => {
|
||||
try {
|
||||
await updateUserPwd(values.oldPassword, values.newPassword);
|
||||
message.success('修改成功');
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
console.error('Failed to update password:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{ flex: '80px' }}
|
||||
wrapperCol={{ flex: 'auto' }}
|
||||
className="profile-form"
|
||||
>
|
||||
<Form.Item label="旧密码" name="oldPassword" rules={[{ required: true, message: '旧密码不能为空' }]}>
|
||||
<Input.Password placeholder="请输入旧密码" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
rules={[
|
||||
{ required: true, message: '新密码不能为空' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符' },
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > \" \' \\ |' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入新密码" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="确认密码"
|
||||
name="confirmPassword"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '确认密码不能为空' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请确认新密码" />
|
||||
</Form.Item>
|
||||
<Form.Item className="profile-form-actions">
|
||||
<Space size={12}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
<Button onClick={handleClose}>关闭</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
|
|
|
|||
|
|
@ -1,42 +1,56 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Form, Input, Button, Radio, message } from 'antd';
|
||||
import { updateUserProfile } from '../../api/user';
|
||||
import { Button, Form, Input, Radio, Space, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { updateUserProfile } from '@/api/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import type { UpdateUserProfilePayload, UserProfileUser } from '@/types/api';
|
||||
|
||||
interface UserInfoProps {
|
||||
user: UserProfileUser;
|
||||
onUpdated: (values: Partial<UserProfileUser>) => void;
|
||||
}
|
||||
|
||||
const UserInfo = ({ user }: UserInfoProps) => {
|
||||
const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
|
||||
const [form] = Form.useForm<UpdateUserProfilePayload>();
|
||||
const navigate = useNavigate();
|
||||
const { defaultRoutePath } = usePermission();
|
||||
|
||||
useEffect(() => {
|
||||
// Set form fields when user data is available
|
||||
form.setFieldsValue({
|
||||
nickName: user.nickName,
|
||||
phonenumber: user.phonenumber,
|
||||
email: user.email,
|
||||
sex: user.sex,
|
||||
nickName: user.nickName ?? '',
|
||||
phonenumber: user.phonenumber ?? '',
|
||||
email: user.email ?? '',
|
||||
sex: user.sex ?? '0',
|
||||
});
|
||||
}, [user, form]);
|
||||
}, [form, user]);
|
||||
|
||||
const onFinish = (values: UpdateUserProfilePayload) => {
|
||||
updateUserProfile(values)
|
||||
.then(() => {
|
||||
message.success('修改成功');
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('修改失败');
|
||||
});
|
||||
const handleClose = () => {
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
return;
|
||||
}
|
||||
navigate(defaultRoutePath || '/index');
|
||||
};
|
||||
|
||||
const onFinish = async (values: UpdateUserProfilePayload) => {
|
||||
try {
|
||||
await updateUserProfile(values);
|
||||
message.success('修改成功');
|
||||
onUpdated(values);
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={onFinish} labelCol={{ span: 4 }} wrapperCol={{ span: 16 }}>
|
||||
<Form.Item
|
||||
label="用户昵称"
|
||||
name="nickName"
|
||||
rules={[{ required: true, message: '用户昵称不能为空' }]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
labelCol={{ flex: '80px' }}
|
||||
wrapperCol={{ flex: 'auto' }}
|
||||
className="profile-form"
|
||||
>
|
||||
<Form.Item label="用户昵称" name="nickName" rules={[{ required: true, message: '用户昵称不能为空' }]}>
|
||||
<Input maxLength={30} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
|
@ -65,10 +79,13 @@ const UserInfo = ({ user }: UserInfoProps) => {
|
|||
<Radio value="1">女</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 4, span: 16 }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
<Form.Item className="profile-form-actions">
|
||||
<Space size={12}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
<Button onClick={handleClose}>关闭</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,84 +1,172 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Row, Col, Card, Tabs, Spin, List, message } from 'antd';
|
||||
import { UserOutlined, PhoneOutlined, MailOutlined, HomeOutlined, TeamOutlined, CalendarOutlined } from '@ant-design/icons';
|
||||
import { getUserProfile } from '../../api/user';
|
||||
import UserInfo from './UserInfo';
|
||||
import ResetPassword from './ResetPassword';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Card, Spin, Tabs, message } from 'antd';
|
||||
import type { TabsProps } from 'antd';
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
CalendarOutlined,
|
||||
MailOutlined,
|
||||
PhoneOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { getUserProfile } from '@/api/user';
|
||||
import type { UserProfileUser } from '@/types/api';
|
||||
import AvatarUploader from './AvatarUploader';
|
||||
import ResetPassword from './ResetPassword';
|
||||
import UserInfo from './UserInfo';
|
||||
import './profile.css';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'userinfo',
|
||||
label: '基本资料',
|
||||
},
|
||||
{
|
||||
key: 'resetPwd',
|
||||
label: '修改密码',
|
||||
},
|
||||
];
|
||||
|
||||
const ProfilePage = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [user, setUser] = useState<UserProfileUser | null>(null);
|
||||
const [roleGroup, setRoleGroup] = useState('');
|
||||
const [postGroup, setPostGroup] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const activeTab = searchParams.get('tab') === 'resetPwd' ? 'resetPwd' : 'userinfo';
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
getUserProfile()
|
||||
.then((response) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setUser(response.user);
|
||||
setRoleGroup(response.roleGroup);
|
||||
setPostGroup(response.postGroup);
|
||||
setLoading(false);
|
||||
setRoleGroup(response.roleGroup ?? '');
|
||||
setPostGroup(response.postGroup ?? '');
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error('Failed to load user profile:', error);
|
||||
message.error('Failed to load user profile.');
|
||||
setLoading(false);
|
||||
message.error('获取个人中心信息失败');
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading || !user) {
|
||||
return <Spin tip="Loading..." style={{ display: 'block', marginTop: '50px' }} />;
|
||||
const profileItems = useMemo(() => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'userName',
|
||||
label: '用户名称',
|
||||
icon: <UserOutlined />,
|
||||
value: user.userName ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
label: '手机号码',
|
||||
icon: <PhoneOutlined />,
|
||||
value: user.phonenumber ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: '用户邮箱',
|
||||
icon: <MailOutlined />,
|
||||
value: user.email ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'dept',
|
||||
label: '所属部门',
|
||||
icon: <ApartmentOutlined />,
|
||||
value: user.dept?.deptName ? `${user.dept.deptName} / ${postGroup || '-'}` : '-',
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: '所属角色',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
value: roleGroup || '-',
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
label: '创建日期',
|
||||
icon: <CalendarOutlined />,
|
||||
value: user.createTime ?? '-',
|
||||
},
|
||||
];
|
||||
}, [postGroup, roleGroup, user]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="profile-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const profileItems = [
|
||||
{ icon: <UserOutlined />, label: '用户名称', value: user.userName ?? '' },
|
||||
{ icon: <PhoneOutlined />, label: '手机号码', value: user.phonenumber ?? '' },
|
||||
{ icon: <MailOutlined />, label: '用户邮箱', value: user.email ?? '' },
|
||||
{ icon: <HomeOutlined />, label: '所属部门', value: user.dept ? `${user.dept.deptName ?? ''} / ${postGroup}` : '' },
|
||||
{ icon: <TeamOutlined />, label: '所属角色', value: roleGroup },
|
||||
{ icon: <CalendarOutlined />, label: '创建日期', value: user.createTime ?? '' },
|
||||
];
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="profile-app-container">
|
||||
<Row gutter={20}>
|
||||
<Col span={6} xs={24}>
|
||||
<Card title="个人信息">
|
||||
<div style={{ textAlign: 'center', marginBottom: '20px' }}>
|
||||
<img src={user.avatar || ''} alt="avatar" style={{width: 120, height: 120, borderRadius: '50%'}}/>
|
||||
</div>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={profileItems}
|
||||
renderItem={item => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={item.icon}
|
||||
title={item.label}
|
||||
description={<div className="pull-right">{item.value}</div>}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
<div className="profile-page app-container">
|
||||
<div className="profile-grid">
|
||||
<div className="profile-grid__aside">
|
||||
<Card className="profile-card profile-card--summary" title="个人信息" variant="borderless">
|
||||
<AvatarUploader
|
||||
avatarUrl={user.avatar as string | undefined}
|
||||
displayName={user.nickName || user.userName || '未命名用户'}
|
||||
onUploaded={(nextAvatar) => setUser((prev) => (prev ? { ...prev, avatar: nextAvatar } : prev))}
|
||||
/>
|
||||
|
||||
<ul className="profile-info-list">
|
||||
{profileItems.map((item) => (
|
||||
<li key={item.key} className="profile-info-item">
|
||||
<span className="profile-info-label">
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="profile-info-value">{item.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={18} xs={24}>
|
||||
<Card>
|
||||
<Tabs defaultActiveKey="userinfo">
|
||||
<TabPane tab="基本资料" key="userinfo">
|
||||
<UserInfo user={user} />
|
||||
</TabPane>
|
||||
<TabPane tab="修改密码" key="resetPwd">
|
||||
</div>
|
||||
|
||||
<div className="profile-grid__main">
|
||||
<Card className="profile-card" title="基本资料" variant="borderless">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={tabItems}
|
||||
onChange={(key) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('tab', key);
|
||||
setSearchParams(params);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="profile-tab-panel">
|
||||
{activeTab === 'userinfo' ? (
|
||||
<UserInfo user={user} onUpdated={(values) => setUser((prev) => (prev ? { ...prev, ...values } : prev))} />
|
||||
) : (
|
||||
<ResetPassword />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,202 @@
|
|||
.profile-app-container .pull-right {
|
||||
float: right;
|
||||
color: #999;
|
||||
.profile-loading {
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-app-container .ant-list-item-meta-description {
|
||||
width: 100%;
|
||||
.profile-page {
|
||||
min-height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 340px minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.profile-grid__aside,
|
||||
.profile-grid__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
height: 100%;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
box-shadow: 0 14px 36px rgba(22, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.profile-card .ant-card-head {
|
||||
min-height: 56px;
|
||||
border-bottom: 1px solid #edf2fa;
|
||||
}
|
||||
|
||||
.profile-card .ant-card-head-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1a2940;
|
||||
}
|
||||
|
||||
.profile-card .ant-card-body {
|
||||
padding: 20px 22px 22px;
|
||||
}
|
||||
|
||||
.profile-avatar-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid #edf2fa;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.profile-avatar-trigger {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-avatar-image {
|
||||
border: 4px solid #f2f5ff;
|
||||
box-shadow: 0 12px 26px rgba(22, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.profile-avatar-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(16, 24, 40, 0.48);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.profile-avatar-trigger:hover .profile-avatar-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile-avatar-name {
|
||||
margin-top: 14px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #18263d;
|
||||
}
|
||||
|
||||
.profile-avatar-tip {
|
||||
margin-top: 6px;
|
||||
color: #6f819a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.profile-info-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 13px 0;
|
||||
border-bottom: 1px solid #f1f4fa;
|
||||
}
|
||||
|
||||
.profile-info-item:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.profile-info-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #60748f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-info-value {
|
||||
color: #1f314b;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.profile-tab-panel {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.profile-tab-panel .ant-tabs-nav {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-form .ant-form-item-label > label {
|
||||
color: #4c5f7b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-form .ant-input,
|
||||
.profile-form .ant-input-password,
|
||||
.profile-form .ant-radio-group {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.profile-form .ant-input,
|
||||
.profile-form .ant-input-password {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.profile-form-actions {
|
||||
margin-bottom: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.profile-form .ant-btn-primary {
|
||||
min-width: 108px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.profile-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.profile-grid__aside {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-form .ant-form-item .ant-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-form .ant-form-item-label,
|
||||
.profile-form .ant-form-item-control {
|
||||
max-width: 100%;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile-form .ant-form-item-control {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.profile-info-item {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.profile-info-value {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { parseTime } from '@/utils/ruoyi';
|
|||
import { listProject } from '@/api/project';
|
||||
import { listProjectExecution } from '@/api/projectExecution';
|
||||
import { getDicts } from '@/api/system/dict';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './project-execution.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
|
@ -309,6 +311,7 @@ const formatPercent = (value: number) => `${clampPercent(value)}%`;
|
|||
|
||||
const ProjectExecutionPage = () => {
|
||||
const [queryForm] = Form.useForm();
|
||||
const { canAccessPath } = usePermission();
|
||||
const navigate = useNavigate();
|
||||
const defaultRange = useMemo(() => getDefaultRange(), []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -335,6 +338,7 @@ const ProjectExecutionPage = () => {
|
|||
[defaultRange, queryParams.endTime],
|
||||
);
|
||||
const periods = useMemo(() => buildPeriods(rangeStart, rangeEnd), [rangeEnd, rangeStart]);
|
||||
const canViewProjectUser = canAccessPath('/projectBank/projectUser');
|
||||
const statusMap = useMemo(
|
||||
() => new Map(statusOptions.map((item) => [String(item.dictValue), item.dictLabel])),
|
||||
[statusOptions],
|
||||
|
|
@ -415,6 +419,9 @@ const ProjectExecutionPage = () => {
|
|||
|
||||
const openProject = useCallback(
|
||||
(row: BoardRow) => {
|
||||
if (!canViewProjectUser) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('projectId', String(row.projectId ?? ''));
|
||||
params.set('projectName', row.projectName ?? '');
|
||||
|
|
@ -430,7 +437,7 @@ const ProjectExecutionPage = () => {
|
|||
|
||||
navigate(`/projectBank/projectUser?${params.toString()}`);
|
||||
},
|
||||
[navigate],
|
||||
[canViewProjectUser, navigate],
|
||||
);
|
||||
|
||||
const handleQuery = () => {
|
||||
|
|
@ -472,9 +479,13 @@ const ProjectExecutionPage = () => {
|
|||
fixed: 'left',
|
||||
render: (value: unknown, row) => (
|
||||
<div className="project-name-cell">
|
||||
<Button type="link" className="project-name-link" onClick={() => openProject(row)}>
|
||||
{String(value ?? '-') || '-'}
|
||||
</Button>
|
||||
{canViewProjectUser ? (
|
||||
<Button type="link" className="project-name-link" onClick={() => openProject(row)}>
|
||||
{String(value ?? '-') || '-'}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="project-name-link is-disabled permission-link-disabled">{String(value ?? '-') || '-'}</span>
|
||||
)}
|
||||
<div className="project-name-meta">
|
||||
<span>{row.projectCode || '未配置项目编号'}</span>
|
||||
<span>
|
||||
|
|
@ -581,7 +592,7 @@ const ProjectExecutionPage = () => {
|
|||
}));
|
||||
|
||||
return [...baseColumns, ...periodColumns];
|
||||
}, [openProject, periods, statusMap]);
|
||||
}, [canViewProjectUser, openProject, periods, statusMap]);
|
||||
|
||||
const stats = useMemo(
|
||||
() => [
|
||||
|
|
|
|||
|
|
@ -283,6 +283,19 @@
|
|||
color: #0958d9;
|
||||
}
|
||||
|
||||
.project-name-link.is-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
color: #98a2b3;
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.project-name-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import type {
|
|||
CacheNamePayload,
|
||||
CacheNameRecord,
|
||||
} from '@/types/api';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import './cache-list.css';
|
||||
|
||||
interface CacheForm {
|
||||
|
|
@ -91,6 +94,7 @@ const extractCacheKeys = (payload: CacheKeyPayload): string[] => {
|
|||
};
|
||||
|
||||
const CacheListPage: React.FC = () => {
|
||||
const { hasPermi } = usePermission();
|
||||
const [cacheNames, setCacheNames] = useState<CacheNameRecord[]>([]);
|
||||
const [cacheKeys, setCacheKeys] = useState<string[]>([]);
|
||||
const [cacheForm, setCacheForm] = useState<CacheForm>(defaultForm);
|
||||
|
|
@ -98,6 +102,7 @@ const CacheListPage: React.FC = () => {
|
|||
const [subLoading, setSubLoading] = useState(false);
|
||||
const [nowCacheName, setNowCacheName] = useState('');
|
||||
const [tableHeight, setTableHeight] = useState(window.innerHeight - 200);
|
||||
const canClearCache = hasPermi('monitor:cache:list');
|
||||
|
||||
const getCacheKeys = useCallback(
|
||||
async (cacheName?: string) => {
|
||||
|
|
@ -192,6 +197,9 @@ const CacheListPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleClearCacheName = async (row: CacheNameRecord) => {
|
||||
if (!canClearCache) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await clearCacheName(row.cacheName);
|
||||
message.success(`清理缓存名称[${row.cacheName}]成功`);
|
||||
|
|
@ -207,6 +215,9 @@ const CacheListPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleClearCacheKey = async (fullCacheKey: string) => {
|
||||
if (!canClearCache) {
|
||||
return;
|
||||
}
|
||||
if (!nowCacheName) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -222,6 +233,9 @@ const CacheListPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleClearCacheAll = async () => {
|
||||
if (!canClearCache) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await clearCacheAll();
|
||||
message.success('清理全部缓存成功');
|
||||
|
|
@ -259,7 +273,12 @@ const CacheListPage: React.FC = () => {
|
|||
key: 'operation',
|
||||
width: 60,
|
||||
render: (_text, record) => (
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => void handleClearCacheName(record)} />
|
||||
<Permission
|
||||
permissions="monitor:cache:list"
|
||||
fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>清理</ReadonlyAction>}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => void handleClearCacheName(record)} />
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
@ -282,12 +301,17 @@ const CacheListPage: React.FC = () => {
|
|||
key: 'operation',
|
||||
width: 60,
|
||||
render: (_text, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => void handleClearCacheKey(record.cacheKey)}
|
||||
/>
|
||||
<Permission
|
||||
permissions="monitor:cache:list"
|
||||
fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>清理</ReadonlyAction>}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => void handleClearCacheKey(record.cacheKey)}
|
||||
/>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
@ -347,7 +371,11 @@ const CacheListPage: React.FC = () => {
|
|||
<Col span={8}>
|
||||
<Card
|
||||
title={<Space><FileTextOutlined /> 缓存内容</Space>}
|
||||
extra={<Button type="text" danger onClick={() => void handleClearCacheAll()}>清理全部</Button>}
|
||||
extra={
|
||||
<Permission permissions="monitor:cache:list">
|
||||
<Button type="text" danger onClick={() => void handleClearCacheAll()}>清理全部</Button>
|
||||
</Permission>
|
||||
}
|
||||
style={{ height: tableHeight + 50, overflow: 'auto' }}
|
||||
>
|
||||
<Form layout="vertical" initialValues={cacheForm}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Row, Col, Card, Descriptions, Spin, message, Typography } from 'antd';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import * as echarts from 'echarts/core';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import { echarts } from '@/utils/echarts';
|
||||
import { macarons } from '../../themes/macarons';
|
||||
import { getCache } from '../../api/monitor/cache';
|
||||
import type { CacheMonitorResponse } from '@/types/api';
|
||||
|
|
@ -95,7 +95,7 @@ const CacheMonitorPage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="cache-monitor-container">
|
||||
<div className="app-container cache-monitor-container">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24} className="card-box">
|
||||
<Card title="基本信息">
|
||||
|
|
@ -129,12 +129,12 @@ const CacheMonitorPage = () => {
|
|||
</Col>
|
||||
<Col span={12} className="card-box">
|
||||
<Card title="命令统计">
|
||||
<ReactECharts theme="macarons" option={commandStatsOptions} style={{ height: '420px' }} />
|
||||
<ReactEChartsCore echarts={echarts} theme="macarons" option={commandStatsOptions} style={{ height: '420px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12} className="card-box">
|
||||
<Card title="内存信息">
|
||||
<ReactECharts theme="macarons" option={usedMemoryOptions} style={{ height: '420px' }} />
|
||||
<ReactEChartsCore echarts={echarts} theme="macarons" option={usedMemoryOptions} style={{ height: '420px' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import { saveAs } from 'file-saver';
|
|||
import dayjs from 'dayjs';
|
||||
import type { JobQueryParams, JobRecord } from '@/types/api';
|
||||
import './job-monitor.css';
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
const sysJobGroupDict = [
|
||||
{ value: 'DEFAULT', label: '默认' },
|
||||
|
|
@ -309,12 +311,24 @@ const JobMonitorPage = () => {
|
|||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
render: (_value, record) => (
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="暂停"
|
||||
onChange={() => void handleStatusChange(record)}
|
||||
/>
|
||||
<Permission
|
||||
permissions="monitor:job:changeStatus"
|
||||
fallback={
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="暂停"
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="暂停"
|
||||
onChange={() => void handleStatusChange(record)}
|
||||
/>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -345,17 +359,27 @@ const JobMonitorPage = () => {
|
|||
|
||||
return (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => void handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
<Button type="link" icon={<DeleteOutlined />} danger onClick={() => void handleDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
<Dropdown menu={{ items: moreItems, onClick: handleMoreMenuClick }} trigger={['click']}>
|
||||
<Button type="link">
|
||||
更多 <DownOutlined />
|
||||
<Permission permissions="monitor:job:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => void handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:job:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Button type="link" icon={<DeleteOutlined />} danger onClick={() => void handleDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission
|
||||
permissions={['monitor:job:changeStatus', 'monitor:job:query']}
|
||||
mode="or"
|
||||
fallback={<ReadonlyAction>更多</ReadonlyAction>}
|
||||
>
|
||||
<Dropdown menu={{ items: moreItems, onClick: handleMoreMenuClick }} trigger={['click']}>
|
||||
<Button type="link">
|
||||
更多 <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Permission>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
|
|
@ -397,21 +421,31 @@ const JobMonitorPage = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => void handleUpdate()}>
|
||||
修改
|
||||
</Button>
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => void handleDelete()}>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
<Button type="default" ghost icon={<SyncOutlined />} onClick={() => handleJobLog()}>
|
||||
日志
|
||||
</Button>
|
||||
<Permission permissions="monitor:job:add">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:job:edit">
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => void handleUpdate()}>
|
||||
修改
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:job:remove">
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => void handleDelete()}>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:job:export">
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:job:query">
|
||||
<Button type="default" ghost icon={<SyncOutlined />} onClick={() => handleJobLog()}>
|
||||
日志
|
||||
</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
App,
|
||||
Table,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Space,
|
||||
Tag,
|
||||
DatePicker,
|
||||
|
|
@ -29,6 +29,8 @@ import {
|
|||
import { saveAs } from 'file-saver';
|
||||
import dayjs from 'dayjs';
|
||||
import type { LogininforQueryParams, LogininforRecord } from '@/types/api';
|
||||
import Permission from '@/components/Permission';
|
||||
import './login-log.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
|
|
@ -61,6 +63,7 @@ const escapeCsvCell = (value: unknown): string => {
|
|||
};
|
||||
|
||||
const LoginLogPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm<LogininforQueryParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState<LogininforRecord[]>([]);
|
||||
|
|
@ -120,7 +123,7 @@ const LoginLogPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除访问编号为"${infoIds}"的数据项?`,
|
||||
onOk: async () => {
|
||||
|
|
@ -138,7 +141,7 @@ const LoginLogPage = () => {
|
|||
};
|
||||
|
||||
const handleClean = async () => {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认清空',
|
||||
content: '是否确认清空所有登录日志数据项?',
|
||||
onOk: async () => {
|
||||
|
|
@ -166,7 +169,7 @@ const LoginLogPage = () => {
|
|||
}
|
||||
|
||||
const targetUserName = userNames[0];
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认解锁',
|
||||
content: `是否确认解锁用户"${targetUserName}"的登录状态?`,
|
||||
onOk: async () => {
|
||||
|
|
@ -277,7 +280,7 @@ const LoginLogPage = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container login-log-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="登录地址" name="ipaddr">
|
||||
<Input placeholder="请输入登录地址" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
|
|
@ -316,31 +319,39 @@ const LoginLogPage = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
ghost
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => void handleDelete()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} onClick={() => void handleClean()}>
|
||||
清空
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<UnlockOutlined />}
|
||||
disabled={selectedRowKeys.length !== 1}
|
||||
onClick={() => void handleUnlock()}
|
||||
>
|
||||
解锁
|
||||
</Button>
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
<Permission permissions="monitor:logininfor:remove">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
ghost
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => void handleDelete()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:logininfor:remove">
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} onClick={() => void handleClean()}>
|
||||
清空
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:logininfor:unlock">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<UnlockOutlined />}
|
||||
disabled={selectedRowKeys.length !== 1}
|
||||
onClick={() => void handleUnlock()}
|
||||
>
|
||||
解锁
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:logininfor:export">
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import { SearchOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icon
|
|||
import { listOnline, forceLogout } from '../../api/monitor/online';
|
||||
import dayjs from 'dayjs';
|
||||
import type { OnlineQueryParams, OnlineRecord } from '@/types/api';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import './online-user.css';
|
||||
|
||||
const defaultQueryParams: OnlineQueryParams = {
|
||||
pageNum: 1,
|
||||
|
|
@ -28,12 +32,14 @@ const normalizeDateValue = (value: unknown): string | number | Date | undefined
|
|||
};
|
||||
|
||||
const OnlineUserPage = () => {
|
||||
const { hasPermi } = usePermission();
|
||||
const [queryForm] = Form.useForm<OnlineQueryParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState<OnlineRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const [queryParams, setQueryParams] = useState<OnlineQueryParams>(defaultQueryParams);
|
||||
const canForceLogout = hasPermi('monitor:online:forceLogout');
|
||||
|
||||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -64,6 +70,9 @@ const OnlineUserPage = () => {
|
|||
};
|
||||
|
||||
const handleForceLogout = (row: OnlineRecord) => {
|
||||
if (!canForceLogout) {
|
||||
return;
|
||||
}
|
||||
if (!row.tokenId) {
|
||||
message.warning('当前会话缺少 tokenId,无法强退');
|
||||
return;
|
||||
|
|
@ -117,15 +126,20 @@ const OnlineUserPage = () => {
|
|||
align: 'center',
|
||||
width: 80,
|
||||
render: (_value, record) => (
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleForceLogout(record)}>
|
||||
强退
|
||||
</Button>
|
||||
<Permission
|
||||
permissions="monitor:online:forceLogout"
|
||||
fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>强退</ReadonlyAction>}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleForceLogout(record)}>
|
||||
强退
|
||||
</Button>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container online-user-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="登录地址" name="ipaddr">
|
||||
<Input placeholder="请输入登录地址" allowClear onPressEnter={handleQuery} />
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import { listOperlog, delOperlog, cleanOperlog } from '../../api/monitor/operlog
|
|||
import { saveAs } from 'file-saver';
|
||||
import dayjs from 'dayjs';
|
||||
import type { OperlogQueryParams, OperlogRecord } from '@/types/api';
|
||||
import Permission from '@/components/Permission';
|
||||
import './operation-log.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
|
|
@ -316,7 +318,7 @@ const OperationLogPage = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container operation-log-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="操作地址" name="operIp">
|
||||
<Input placeholder="请输入操作地址" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
|
|
@ -367,22 +369,28 @@ const OperationLogPage = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
ghost
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => void handleDelete()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} onClick={() => void handleClean()}>
|
||||
清空
|
||||
</Button>
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
<Permission permissions="monitor:operlog:remove">
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
ghost
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => void handleDelete()}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:operlog:remove">
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} onClick={() => void handleClean()}>
|
||||
清空
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="monitor:operlog:export">
|
||||
<Button type="default" ghost icon={<DownloadOutlined />} onClick={() => void handleExport()}>
|
||||
导出
|
||||
</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Card, Col, Row, message, Typography, Descriptions, Progress, Table, Space } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { App, Card, Col, Row, Typography, Descriptions, Progress, Table, Space } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
SettingOutlined,
|
||||
|
|
@ -50,10 +50,17 @@ const renderUsageProgress = (usage: unknown) => {
|
|||
};
|
||||
|
||||
const ServerMonitorPage = () => {
|
||||
const { message } = App.useApp();
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfoResponse>(defaultServerInfo);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const requestedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestedRef.current) {
|
||||
return;
|
||||
}
|
||||
requestedRef.current = true;
|
||||
|
||||
const getList = async () => {
|
||||
setLoading(true);
|
||||
const hide = message.loading('正在加载服务监控数据,请稍候!', 0);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.cache-list-container .ant-card-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.card-box {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.job-monitor-container .ant-form-item {
|
||||
margin-bottom: 0px; /* Reduce vertical space in search form */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.login-log-container .ant-form-item {
|
||||
margin-bottom: 0px; /* Reduce vertical space in search form */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
.login-log-container,
|
||||
.online-user-container,
|
||||
.operation-log-container,
|
||||
.job-monitor-container,
|
||||
.cache-list-container,
|
||||
.server-monitor-container,
|
||||
.cache-monitor-container {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.login-log-container .search-form .ant-form-item,
|
||||
.online-user-container .search-form .ant-form-item,
|
||||
.operation-log-container .search-form .ant-form-item,
|
||||
.job-monitor-container .search-form .ant-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.login-log-container .mb8,
|
||||
.operation-log-container .mb8,
|
||||
.job-monitor-container .mb8 {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.login-log-container .ant-table-wrapper .ant-btn-link,
|
||||
.online-user-container .ant-table-wrapper .ant-btn-link,
|
||||
.operation-log-container .ant-table-wrapper .ant-btn-link,
|
||||
.job-monitor-container .ant-table-wrapper .ant-btn-link,
|
||||
.cache-list-container .ant-table-wrapper .ant-btn-link {
|
||||
padding-inline: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.server-monitor-container .card-box,
|
||||
.cache-monitor-container .card-box {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.server-monitor-container .ant-card,
|
||||
.cache-monitor-container .ant-card,
|
||||
.cache-list-container .ant-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.server-monitor-container .ant-card-head,
|
||||
.cache-monitor-container .ant-card-head,
|
||||
.cache-list-container .ant-card-head {
|
||||
background: linear-gradient(180deg, rgba(248, 250, 255, 0.92), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
|
||||
.server-monitor-container .ant-descriptions,
|
||||
.cache-monitor-container .ant-descriptions {
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.server-monitor-container .ant-descriptions-bordered .ant-descriptions-item-label,
|
||||
.server-monitor-container .ant-descriptions-bordered .ant-descriptions-item-content,
|
||||
.cache-monitor-container .ant-descriptions-bordered .ant-descriptions-item-label,
|
||||
.cache-monitor-container .ant-descriptions-bordered .ant-descriptions-item-content {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.server-monitor-container .ant-progress .ant-progress-bg,
|
||||
.cache-monitor-container .ant-progress .ant-progress-bg {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.cache-list-container .ant-card-body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.cache-list-container .ant-table-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.online-user-container .ant-form-item {
|
||||
margin-bottom: 0px; /* Reduce vertical space in search form */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.operation-log-container .ant-form-item {
|
||||
margin-bottom: 0px; /* Reduce vertical space in search form */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import "./monitor-shared.css";
|
||||
|
||||
.server-monitor-container .card-box {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ import {
|
|||
} from '@/api/project';
|
||||
import { getDicts } from '@/api/system/dict';
|
||||
import { listUser } from '@/api/system/user';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import './demand-manage.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -266,6 +270,7 @@ const DemandManagePage: React.FC = () => {
|
|||
const [queryForm] = Form.useForm();
|
||||
const [demandForm] = Form.useForm<DemandFormValues>();
|
||||
const [versionForm] = Form.useForm<VersionFormValues>();
|
||||
const { hasPermi } = usePermission();
|
||||
|
||||
const projectId = searchParams.get('id') ?? '';
|
||||
const projectName = searchParams.get('projectName') ?? '未命名项目';
|
||||
|
|
@ -336,6 +341,9 @@ const DemandManagePage: React.FC = () => {
|
|||
);
|
||||
|
||||
const versionRows = useMemo(() => versionTree.filter((item) => item.type === 0), [versionTree]);
|
||||
const canCreateDemand = hasPermi('project:list:demand');
|
||||
const canEditVersion = hasPermi('project:list:demand');
|
||||
const canDeleteVersion = hasPermi('project:list:demand');
|
||||
const versionChildNodeIds = useMemo(() => {
|
||||
const idSet = new Set<string>();
|
||||
versionRows.forEach((item) => {
|
||||
|
|
@ -904,17 +912,22 @@ const DemandManagePage: React.FC = () => {
|
|||
render: (_value, record) => (
|
||||
<Space size={6}>
|
||||
<Badge color={STATUS_COLOR_MAP[record.demandStatus] ?? '#8c8c8c'} />
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 110 }}
|
||||
value={record.demandStatus}
|
||||
options={statusSelectOptions}
|
||||
onChange={(value) => {
|
||||
if (value !== record.demandStatus) {
|
||||
handleQuickUpdate(record, { demandStatus: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Permission
|
||||
permissions="project:list:demand"
|
||||
fallback={<Text>{record.demandStatusLabel}</Text>}
|
||||
>
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 110 }}
|
||||
value={record.demandStatus}
|
||||
options={statusSelectOptions}
|
||||
onChange={(value) => {
|
||||
if (value !== record.demandStatus) {
|
||||
handleQuickUpdate(record, { demandStatus: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
@ -928,17 +941,22 @@ const DemandManagePage: React.FC = () => {
|
|||
key: 'priority',
|
||||
width: 130,
|
||||
render: (_value, record) => (
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 90 }}
|
||||
value={record.priority}
|
||||
options={priorityOptions}
|
||||
onChange={(value) => {
|
||||
if (value !== record.priority) {
|
||||
handleQuickUpdate(record, { priority: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Permission
|
||||
permissions="project:list:demand"
|
||||
fallback={<Text>{record.priority}</Text>}
|
||||
>
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 90 }}
|
||||
value={record.priority}
|
||||
options={priorityOptions}
|
||||
onChange={(value) => {
|
||||
if (value !== record.priority) {
|
||||
handleQuickUpdate(record, { priority: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -947,19 +965,23 @@ const DemandManagePage: React.FC = () => {
|
|||
width: 150,
|
||||
render: (_value, record) => (
|
||||
<Space size={4}>
|
||||
<Button type="link" onClick={() => openEditDemand(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="此操作将永久删除该需求,是否继续?"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteOne(record)}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
<Permission permissions="project:list:demand" fallback={<ReadonlyAction>编辑</ReadonlyAction>}>
|
||||
<Button type="link" onClick={() => openEditDemand(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
<Permission permissions="project:list:demand" fallback={<ReadonlyAction danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="此操作将永久删除该需求,是否继续?"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteOne(record)}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
@ -980,7 +1002,24 @@ const DemandManagePage: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container demand-manage-page">
|
||||
<div className="demand-page-hero">
|
||||
<div>
|
||||
<div className="demand-page-kicker">DEMAND WORKSPACE</div>
|
||||
<div className="demand-page-title">需求管理</div>
|
||||
<div className="demand-page-subtitle">围绕版本结构、排期状态和负责人维护项目交付节奏。</div>
|
||||
</div>
|
||||
<div className="demand-page-hero-meta">
|
||||
<div className="demand-page-meta-card">
|
||||
<span>所属项目</span>
|
||||
<strong>{projectName}</strong>
|
||||
</div>
|
||||
<div className="demand-page-meta-card">
|
||||
<span>需求总数</span>
|
||||
<strong>{total}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={16} align="top">
|
||||
<Col xs={24} lg={versionSidebarCollapsed ? 1 : 7} xl={versionSidebarCollapsed ? 1 : 6}>
|
||||
{versionSidebarCollapsed ? (
|
||||
|
|
@ -994,6 +1033,7 @@ const DemandManagePage: React.FC = () => {
|
|||
</div>
|
||||
) : (
|
||||
<Card
|
||||
className="demand-version-card"
|
||||
size="small"
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
|
|
@ -1039,7 +1079,9 @@ const DemandManagePage: React.FC = () => {
|
|||
</Text>
|
||||
</Space>
|
||||
<Space size={4}>
|
||||
<Button type="text" icon={<PlusOutlined />} onClick={openCreateVersion} />
|
||||
<Permission permissions="project:list:demand">
|
||||
<Button type="text" icon={<PlusOutlined />} onClick={openCreateVersion} />
|
||||
</Permission>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
|
@ -1113,27 +1155,39 @@ const DemandManagePage: React.FC = () => {
|
|||
<Space size={6}>
|
||||
<Text style={{ color: selected ? '#1677ff' : '#8c8c8c' }}>{count}</Text>
|
||||
{hoverVersionNodeId === item.nodeId ? (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'add', label: '新建需求' },
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'delete', label: <span style={{ color: '#ff4d4f' }}>删除</span> },
|
||||
],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
handleVersionMenuAction(String(key), item);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MoreOutlined />}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
(() => {
|
||||
const versionMenuItems = [
|
||||
canCreateDemand ? { key: 'add', label: '新建需求' } : null,
|
||||
canEditVersion ? { key: 'edit', label: '编辑' } : null,
|
||||
canDeleteVersion
|
||||
? { key: 'delete', label: <span style={{ color: '#ff4d4f' }}>删除</span> }
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
if (versionMenuItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: versionMenuItems,
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
handleVersionMenuAction(String(key), item);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MoreOutlined />}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
})()
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
|
|
@ -1233,6 +1287,7 @@ const DemandManagePage: React.FC = () => {
|
|||
|
||||
<Col xs={24} lg={versionSidebarCollapsed ? 23 : 17} xl={versionSidebarCollapsed ? 23 : 18}>
|
||||
<Card
|
||||
className="demand-list-card"
|
||||
size="small"
|
||||
title="需求列表"
|
||||
extra={
|
||||
|
|
@ -1247,7 +1302,7 @@ const DemandManagePage: React.FC = () => {
|
|||
<Text type="secondary">项目周期:{projectStartText} ~ {projectEndText}</Text>
|
||||
</Space>
|
||||
|
||||
<Form form={queryForm} layout="inline" onFinish={handleSearch}>
|
||||
<Form form={queryForm} layout="inline" className="demand-search-form" onFinish={handleSearch}>
|
||||
<Form.Item name="title">
|
||||
<Input placeholder="标题名称" allowClear style={{ width: 220 }} />
|
||||
</Form.Item>
|
||||
|
|
@ -1291,18 +1346,22 @@ const DemandManagePage: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space style={{ margin: '12px 0' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateDemand}>
|
||||
新建需求
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="此操作将永久删除选中需求,是否继续?"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={handleDeleteBatch}
|
||||
>
|
||||
<Button disabled={selectedRowKeys.length === 0}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Space className="demand-toolbar">
|
||||
<Permission permissions="project:list:demand">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateDemand}>
|
||||
新建需求
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="project:list:demand">
|
||||
<Popconfirm
|
||||
title="此操作将永久删除选中需求,是否继续?"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={handleDeleteBatch}
|
||||
>
|
||||
<Button disabled={selectedRowKeys.length === 0}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<div ref={tableContainerRef}>
|
||||
|
|
@ -1320,7 +1379,7 @@ const DemandManagePage: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div className="demand-pagination">
|
||||
<Text type="secondary">共 {total} 条</Text>
|
||||
<Pagination
|
||||
size="small"
|
||||
|
|
@ -1337,6 +1396,7 @@ const DemandManagePage: React.FC = () => {
|
|||
</Row>
|
||||
|
||||
<Modal
|
||||
className="demand-edit-modal"
|
||||
title={editingDemand ? '修改需求' : '新建需求'}
|
||||
open={demandModalOpen}
|
||||
onOk={submitDemand}
|
||||
|
|
@ -1448,6 +1508,7 @@ const DemandManagePage: React.FC = () => {
|
|||
</Modal>
|
||||
|
||||
<Modal
|
||||
className="demand-version-modal"
|
||||
title={editingVersion ? '编辑版本号' : '新增版本号'}
|
||||
open={versionModalOpen}
|
||||
onOk={submitVersion}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,22 @@ import { getProjectDetail, addProject, updateProject, getProjectCode } from '../
|
|||
import dayjs from 'dayjs';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import './project-detail.css';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const ProjectDetailPage: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const projectCode = Form.useWatch('projectCode', form);
|
||||
const { hasPermi } = usePermission();
|
||||
const { id: pathId } = useParams<{ id: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const id = pathId ?? searchParams.get('id') ?? undefined;
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isEdit = !!id;
|
||||
const canSubmitProject = hasPermi('project:detail:save');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjectData = async () => {
|
||||
|
|
@ -50,6 +55,9 @@ const ProjectDetailPage: React.FC = () => {
|
|||
}, [id, isEdit, form]);
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
if (!canSubmitProject) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
|
|
@ -73,11 +81,22 @@ const ProjectDetailPage: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="app-container project-detail-page">
|
||||
<div className="project-detail-back">
|
||||
<PageBackButton text="返回项目列表" fallbackPath="/project/list" />
|
||||
</div>
|
||||
<Card title={isEdit ? '编辑项目' : '新建项目'}>
|
||||
<div className="project-detail-hero">
|
||||
<div>
|
||||
<div className="project-detail-kicker">PROJECT DETAIL</div>
|
||||
<div className="project-detail-title">{isEdit ? '编辑项目' : '新建项目'}</div>
|
||||
<div className="project-detail-subtitle">维护项目基本信息、负责人、周期与描述,保持项目数据结构统一。</div>
|
||||
</div>
|
||||
<div className="project-detail-summary">
|
||||
<span>{isEdit ? '当前模式:编辑' : '当前模式:新建'}</span>
|
||||
<strong>{projectCode || '待生成编号'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="project-detail-card" title={isEdit ? '编辑项目' : '新建项目'}>
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
|
|
@ -119,7 +138,7 @@ const ProjectDetailPage: React.FC = () => {
|
|||
<TextArea rows={4} placeholder="请输入项目描述" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} disabled={!canSubmitProject}>
|
||||
{isEdit ? '保 存' : '创 建'}
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={() => navigate('/project/list')}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import {
|
|||
import { getDicts } from '../../api/system/dict'; // To get dict data for project status
|
||||
import { parseTime } from '../../utils/ruoyi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import './project.css';
|
||||
|
||||
const ProjectPage: React.FC = () => {
|
||||
const [queryForm] = Form.useForm();
|
||||
|
|
@ -20,6 +23,7 @@ const ProjectPage: React.FC = () => {
|
|||
const [total, setTotal] = useState(0);
|
||||
const [statusList, setStatusList] = useState<any[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const { hasPermi } = usePermission();
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
pageNum: 1,
|
||||
|
|
@ -169,23 +173,35 @@ const ProjectPage: React.FC = () => {
|
|||
title: '操作', key: 'operation', width: 250, fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<UnorderedListOutlined />} onClick={() => handleDemand(record)}>需求管理</Button>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm
|
||||
title="是否确认删除该项目?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
{hasPermi('project:list:demand') ? (
|
||||
<Button type="link" icon={<UnorderedListOutlined />} onClick={() => handleDemand(record)}>需求管理</Button>
|
||||
) : (
|
||||
<ReadonlyAction icon={<UnorderedListOutlined />}>需求管理</ReadonlyAction>
|
||||
)}
|
||||
{hasPermi('project:list:eidt') ? (
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
) : (
|
||||
<ReadonlyAction icon={<EditOutlined />}>编辑</ReadonlyAction>
|
||||
)}
|
||||
{hasPermi('project:list:delete') ? (
|
||||
<Popconfirm
|
||||
title="是否确认删除该项目?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container project-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery}>
|
||||
<Form.Item label="项目名称" name="projectName">
|
||||
<Input placeholder="项目名称" allowClear />
|
||||
|
|
@ -207,7 +223,9 @@ const ProjectPage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8" style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddProject}>新建项目</Button>
|
||||
{hasPermi('project:list:add') ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddProject}>新建项目</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
.demand-manage-page {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.demand-page-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.12), transparent 26%),
|
||||
radial-gradient(circle at left center, rgba(99, 91, 255, 0.12), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.96));
|
||||
box-shadow: 0 18px 42px rgba(22, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.demand-page-kicker {
|
||||
margin-bottom: 10px;
|
||||
color: #6258f6;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.demand-page-title {
|
||||
color: #162033;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.demand-page-subtitle {
|
||||
margin-top: 8px;
|
||||
color: #6c7b98;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.demand-page-hero-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demand-page-meta-card {
|
||||
min-width: 140px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.demand-page-meta-card span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #7d8cab;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.demand-page-meta-card strong {
|
||||
color: #162033;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.demand-version-card,
|
||||
.demand-list-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demand-version-card .ant-card-head,
|
||||
.demand-list-card .ant-card-head {
|
||||
background: linear-gradient(180deg, rgba(248, 250, 255, 0.92), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
|
||||
.demand-list-card .ant-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.demand-search-form {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.demand-toolbar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.demand-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.demand-edit-modal .ant-modal-body,
|
||||
.demand-version-modal .ant-modal-body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.demand-page-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.demand-page-hero-meta {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
.project-detail-page {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.project-detail-back {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.project-detail-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.12), transparent 26%),
|
||||
radial-gradient(circle at left center, rgba(99, 91, 255, 0.12), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.96));
|
||||
box-shadow: 0 18px 42px rgba(22, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.project-detail-kicker {
|
||||
margin-bottom: 10px;
|
||||
color: #6258f6;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.project-detail-title {
|
||||
color: #162033;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.project-detail-subtitle {
|
||||
margin-top: 8px;
|
||||
color: #6c7b98;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.project-detail-summary {
|
||||
min-width: 180px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.project-detail-summary span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #7d8cab;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.project-detail-summary strong {
|
||||
color: #162033;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.project-detail-card .ant-form-item:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.project-detail-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,27 @@
|
|||
.project-page-container {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.project-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.project-page-container .search-form .ant-input,
|
||||
.project-page-container .search-form .ant-select {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.project-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.project-page-container .ant-table-wrapper .ant-tag {
|
||||
min-width: 74px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.project-page-container .ant-table-wrapper .ant-btn-link {
|
||||
padding-inline: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import zhCN from 'antd/es/date-picker/locale/zh_CN';
|
|||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { getProjectDetail, listProject } from '@/api/project';
|
||||
import { getProjectExecutionInfo, getProjectWorkInfo } from '@/api/projectExecution';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import './project-user.css';
|
||||
|
||||
|
|
@ -115,6 +117,7 @@ const normalizeDateParam = (value: string | null) => {
|
|||
|
||||
const ProjectUserPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId') ?? searchParams.get('id') ?? '';
|
||||
const queryStartDate = normalizeDateParam(searchParams.get('startDate'));
|
||||
|
|
@ -132,8 +135,12 @@ const ProjectUserPage = () => {
|
|||
dayjs(defaultEndDate, 'YYYY-MM-DD'),
|
||||
]);
|
||||
const [detailList, setDetailList] = useState<ProjectUserItem[][]>([]);
|
||||
const canViewWorkLog = canAccessPath('/index');
|
||||
|
||||
const openUserLog = (row: ProjectUserItem, queryDate?: string) => {
|
||||
if (!canViewWorkLog) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('projectId', String(selectedProjectId ?? ''));
|
||||
params.set('userId', String(row.userId ?? ''));
|
||||
|
|
@ -298,15 +305,24 @@ const ProjectUserPage = () => {
|
|||
return (
|
||||
<div className="project-user-button-group">
|
||||
{users.map((item, itemIndex) => (
|
||||
<Button
|
||||
key={`${String(item.userId ?? item.userName ?? itemIndex)}-${columnIndex}`}
|
||||
type="link"
|
||||
size="small"
|
||||
className="project-user-log-button"
|
||||
onClick={() => openUserLog(item, queryDate)}
|
||||
>
|
||||
{`${String(item.userName ?? '-') }(${toNumber(item.workTime, 0)}天)`}
|
||||
</Button>
|
||||
canViewWorkLog ? (
|
||||
<Button
|
||||
key={`${String(item.userId ?? item.userName ?? itemIndex)}-${columnIndex}`}
|
||||
type="link"
|
||||
size="small"
|
||||
className="project-user-log-button"
|
||||
onClick={() => openUserLog(item, queryDate)}
|
||||
>
|
||||
{`${String(item.userName ?? '-') }(${toNumber(item.workTime, 0)}天)`}
|
||||
</Button>
|
||||
) : (
|
||||
<span
|
||||
key={`${String(item.userId ?? item.userName ?? itemIndex)}-${columnIndex}`}
|
||||
className="project-user-log-button is-disabled permission-link-disabled"
|
||||
>
|
||||
{`${String(item.userName ?? '-') }(${toNumber(item.workTime, 0)}天)`}
|
||||
</span>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -317,7 +333,7 @@ const ProjectUserPage = () => {
|
|||
}
|
||||
return list;
|
||||
})();
|
||||
}, [dateRange, detailList]);
|
||||
}, [canViewWorkLog, dateRange, detailList]);
|
||||
|
||||
const selectedProjectName = useMemo(() => {
|
||||
const hit = projectList.find((item) => String(item.projectId) === String(selectedProjectId ?? ''));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { useNavigate } from 'react-router-dom';
|
|||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { getProjectExecutionInfo } from '@/api/projectExecution';
|
||||
import { deptTreeSelect, getUserProfile, listUser } from '@/api/system/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './user-project.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
|
@ -129,6 +131,7 @@ const matchesUserKeyword = (user: UserRow, keyword: string) => {
|
|||
|
||||
const UserProjectPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
const [deptLoading, setDeptLoading] = useState(false);
|
||||
|
|
@ -148,6 +151,7 @@ const UserProjectPage = () => {
|
|||
const [userPageSize, setUserPageSize] = useState(10);
|
||||
const [userTotal, setUserTotal] = useState(0);
|
||||
const deferredUserKeyword = useDeferredValue(userKeyword);
|
||||
const canViewProjectDetail = canAccessPath('/project/detail');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentUser = async () => {
|
||||
|
|
@ -280,13 +284,17 @@ const UserProjectPage = () => {
|
|||
fixed: 'left',
|
||||
width: 180,
|
||||
render: (value: unknown, row) => (
|
||||
<Button
|
||||
type="link"
|
||||
className="user-project-link"
|
||||
onClick={() => navigate(`/project/detail?id=${String(row.projectId ?? '')}`)}
|
||||
>
|
||||
{String(value ?? '-')}
|
||||
</Button>
|
||||
canViewProjectDetail ? (
|
||||
<Button
|
||||
type="link"
|
||||
className="user-project-link"
|
||||
onClick={() => navigate(`/project/detail?id=${String(row.projectId ?? '')}`)}
|
||||
>
|
||||
{String(value ?? '-')}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="user-project-link is-disabled permission-link-disabled">{String(value ?? '-')}</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -320,7 +328,7 @@ const UserProjectPage = () => {
|
|||
}
|
||||
|
||||
return columns;
|
||||
}, [dateRange, navigate]);
|
||||
}, [canViewProjectDetail, dateRange, navigate]);
|
||||
|
||||
const summaryValues = useMemo(() => {
|
||||
const dayCount = dateRange[1].startOf('day').diff(dateRange[0].startOf('day'), 'day') + 1;
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ const UserScoreDetailPage = () => {
|
|||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 360,
|
||||
render: (value: unknown, row) => (
|
||||
render: (value: unknown, row: DetailItem) => (
|
||||
row.reviewCategory === '发展与协作'
|
||||
? <span className="user-score-detail-empty">详见自评总结</span>
|
||||
: <ScoreBar value={toNumber(value, 0)} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
|||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { getTaskList, getTaskUserList } from '@/api/appraisal';
|
||||
import { deptTreeSelect, listUser } from '@/api/system/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './user-score.css';
|
||||
|
||||
interface DeptTreeNode {
|
||||
|
|
@ -142,6 +144,7 @@ const getScoreValue = (row: UserScoreRow) => row.score ?? row.manageScore ?? row
|
|||
|
||||
const UserScorePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [searchParams] = useSearchParams();
|
||||
const routeTaskId = searchParams.get('taskId') ?? '';
|
||||
const routeDeptId = searchParams.get('deptId') ?? '';
|
||||
|
|
@ -160,6 +163,7 @@ const UserScorePage = () => {
|
|||
const [deptUserIds, setDeptUserIds] = useState<Set<string>>(new Set());
|
||||
const [pageNum, setPageNum] = useState(routePageNum);
|
||||
const [pageSize, setPageSize] = useState(routePageSize);
|
||||
const canViewScoreDetail = canAccessPath('/projectBank/userScoreDetail');
|
||||
|
||||
useEffect(() => {
|
||||
const loadBaseData = async () => {
|
||||
|
|
@ -274,6 +278,9 @@ const UserScorePage = () => {
|
|||
};
|
||||
|
||||
const openDetail = (row: UserScoreRow) => {
|
||||
if (!canViewScoreDetail) {
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('userId', String(row.userId ?? ''));
|
||||
params.set('userName', String(row.userName ?? ''));
|
||||
|
|
@ -323,9 +330,13 @@ const UserScorePage = () => {
|
|||
key: 'action',
|
||||
width: 160,
|
||||
render: (_value, row) => (
|
||||
<Button type="link" onClick={() => openDetail(row)}>
|
||||
查看详情
|
||||
</Button>
|
||||
canViewScoreDetail ? (
|
||||
<Button type="link" onClick={() => openDetail(row)}>
|
||||
查看详情
|
||||
</Button>
|
||||
) : (
|
||||
<span className="user-score-link is-disabled permission-link-disabled">查看详情</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -129,6 +129,20 @@
|
|||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.project-user-log-button.is-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
color: #98a2b3;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.project-user-empty-cell {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,17 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-project-link.is-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: #98a2b3;
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.user-select-modal {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -193,6 +193,15 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-score-link.is-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #98a2b3;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-score-total-value {
|
||||
color: #f97316;
|
||||
font-weight: 700;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import {
|
|||
import { saveAs } from 'file-saver';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseTime } from '../../utils/ruoyi';
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
|
|
@ -21,20 +24,43 @@ const sysYesNoDict = [
|
|||
{ value: 'N', label: '否' },
|
||||
];
|
||||
|
||||
type QueryParams = {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
configName?: string;
|
||||
configKey?: string;
|
||||
configType?: string;
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
type ConfigRecord = {
|
||||
configId?: string | number;
|
||||
configName?: string;
|
||||
configKey?: string;
|
||||
configValue?: string;
|
||||
configType?: string;
|
||||
remark?: string;
|
||||
createTime?: string | number | Date;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const ConfigPage: React.FC = () => {
|
||||
const { canAccessPath } = usePermission();
|
||||
const canOperateConfig = canAccessPath('/system/config');
|
||||
const [queryForm] = Form.useForm();
|
||||
const [configForm] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [configList, setConfigList] = useState<any[]>([]);
|
||||
const [configList, setConfigList] = useState<ConfigRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [currentConfig, setCurrentConfig] = useState<any>({});
|
||||
const [currentConfig, setCurrentConfig] = useState<ConfigRecord>({});
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
const [queryParams, setQueryParams] = useState<QueryParams>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
configName: undefined,
|
||||
|
|
@ -45,17 +71,17 @@ const ConfigPage: React.FC = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formattedQueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange.length === 2) {
|
||||
formattedQueryParams['beginTime'] = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams['endTime'] = dateRange[1].format('YYYY-MM-DD');
|
||||
const formattedQueryParams: QueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange[0] && dateRange[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD');
|
||||
} else {
|
||||
formattedQueryParams['beginTime'] = undefined;
|
||||
formattedQueryParams['endTime'] = undefined;
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
const response = await listConfig(formattedQueryParams);
|
||||
setConfigList(response.rows);
|
||||
setTotal(response.total);
|
||||
const response = await listConfig(formattedQueryParams) as { rows?: ConfigRecord[]; total?: number };
|
||||
setConfigList(response.rows ?? []);
|
||||
setTotal(response.total ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch config list:', error);
|
||||
message.error('获取参数列表失败');
|
||||
|
|
@ -96,14 +122,19 @@ const ConfigPage: React.FC = () => {
|
|||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleUpdate = async (row: any) => {
|
||||
const handleUpdate = async (row: Partial<ConfigRecord> | React.Key[]) => {
|
||||
resetConfigForm();
|
||||
try {
|
||||
const configId = row.configId || selectedRowKeys[0];
|
||||
const response = await getConfig(configId);
|
||||
const configId = Array.isArray(row) ? row[0] : row.configId;
|
||||
if (configId === undefined || configId === null) {
|
||||
message.warning('请选择要修改的参数');
|
||||
return;
|
||||
}
|
||||
const normalizedConfigId = typeof configId === 'bigint' ? Number(configId) : configId;
|
||||
const response = await getConfig(normalizedConfigId) as unknown;
|
||||
const detail = (response && typeof response === 'object' && 'data' in response)
|
||||
? response.data ?? {}
|
||||
: response ?? {};
|
||||
? (response.data as ConfigRecord | undefined) ?? {}
|
||||
: (response as ConfigRecord | undefined) ?? {};
|
||||
setCurrentConfig(detail);
|
||||
configForm.setFieldsValue({
|
||||
...detail,
|
||||
|
|
@ -141,16 +172,16 @@ const ConfigPage: React.FC = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listConfig({ ...queryParams, pageNum: undefined, pageSize: undefined });
|
||||
const response = await listConfig({ ...queryParams, pageNum: undefined, pageSize: undefined }) as { rows?: ConfigRecord[] };
|
||||
const header = ['参数主键', '参数名称', '参数键名', '参数键值', '系统内置', '备注', '创建时间'];
|
||||
const data = response.rows.map((config: any) => [
|
||||
const data = (response.rows ?? []).map((config) => [
|
||||
config.configId, config.configName, config.configKey, config.configValue,
|
||||
sysYesNoDict.find((d) => d.value === config.configType)?.label ?? '',
|
||||
config.remark, parseTime(config.createTime),
|
||||
]);
|
||||
|
||||
const csvContent = [header, ...data]
|
||||
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.map((row) => row.map((cell: unknown) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
|
@ -205,22 +236,26 @@ const ConfigPage: React.FC = () => {
|
|||
title: '操作', key: 'operation', align: 'center', width: 180,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
<Popconfirm
|
||||
title="是否确认删除该参数?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'} fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'} fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="是否确认删除该参数?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container config-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="参数名称" name="configName">
|
||||
<Input placeholder="请输入参数名称" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
|
|
@ -250,11 +285,21 @@ const ConfigPage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate(selectedRowKeys)}>修改</Button>
|
||||
<Button type="danger" ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
<Button type="warning" ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
<Button type="danger" ghost icon={<SyncOutlined />} onClick={handleRefreshCache}>刷新缓存</Button>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'}>
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'}>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate(selectedRowKeys)}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'}>
|
||||
<Button danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'}>
|
||||
<Button ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canOperateConfig ? undefined : '__forbidden__'}>
|
||||
<Button danger ghost icon={<SyncOutlined />} onClick={handleRefreshCache}>刷新缓存</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
@ -281,6 +326,7 @@ const ConfigPage: React.FC = () => {
|
|||
|
||||
{/* Add/Edit Config Modal */}
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={configForm.submit}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
listDept, getDept, addDept, updateDept, delDept, listDeptExcludeChild
|
||||
} from '../../api/system/dept';
|
||||
import { parseTime, handleTree } from '../../utils/ruoyi'; // Using handleTree utility
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
// Mock Dictionaries
|
||||
const sysNormalDisableDict = [
|
||||
|
|
@ -226,17 +228,23 @@ const DeptPage: React.FC = () => {
|
|||
title: '操作', key: 'operation', align: 'center', width: 200,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
<Button type="link" icon={<PlusOutlined />} onClick={() => handleAdd(record)}>新增</Button>
|
||||
<Permission permissions="system:dept:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:dept:add" fallback={<ReadonlyAction icon={<PlusOutlined />}>新增</ReadonlyAction>}>
|
||||
<Button type="link" icon={<PlusOutlined />} onClick={() => handleAdd(record)}>新增</Button>
|
||||
</Permission>
|
||||
{record.parentId !== 0 && (
|
||||
<Popconfirm
|
||||
title="是否确认删除该部门?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Permission permissions="system:dept:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="是否确认删除该部门?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
|
|
@ -244,7 +252,7 @@ const DeptPage: React.FC = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container dept-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="部门名称" name="deptName">
|
||||
<Input placeholder="请输入部门名称" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
|
|
@ -263,8 +271,10 @@ const DeptPage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={() => handleAdd()}>新增</Button>
|
||||
<Button type="info" ghost icon={<SortAscendingOutlined />} onClick={toggleExpandAll}>展开/折叠</Button>
|
||||
<Permission permissions="system:dept:add">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={() => handleAdd()}>新增</Button>
|
||||
</Permission>
|
||||
<Button ghost icon={<SortAscendingOutlined />} onClick={toggleExpandAll}>展开/折叠</Button>
|
||||
</Space>
|
||||
|
||||
{refreshTable && (
|
||||
|
|
@ -284,6 +294,7 @@ const DeptPage: React.FC = () => {
|
|||
|
||||
{/* Add/Edit Department Modal */}
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={deptForm.submit}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import 'dayjs/locale/zh-cn';
|
|||
import zhCN from 'antd/es/date-picker/locale/zh_CN';
|
||||
import { parseTime } from '../../utils/ruoyi';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
dayjs.locale('zh-cn');
|
||||
|
|
@ -25,20 +28,41 @@ const sysNormalDisableDict = [
|
|||
{ value: '1', label: '停用' },
|
||||
];
|
||||
|
||||
type DictQueryParams = {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
dictName?: string;
|
||||
dictType?: string;
|
||||
status?: string;
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
type DictTypeRecord = {
|
||||
dictId?: string | number;
|
||||
dictName?: string;
|
||||
dictType?: string;
|
||||
status?: string | number;
|
||||
remark?: string;
|
||||
createTime?: string | number | Date;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const DictPage: React.FC = () => {
|
||||
const { canAccessPath } = usePermission();
|
||||
const [queryForm] = Form.useForm();
|
||||
const [dictForm] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [typeList, setTypeList] = useState<any[]>([]);
|
||||
const [typeList, setTypeList] = useState<DictTypeRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
const [currentDict, setCurrentDict] = useState<any>({});
|
||||
const [currentDict, setCurrentDict] = useState<DictTypeRecord>({});
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
const [queryParams, setQueryParams] = useState<DictQueryParams>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
dictName: undefined,
|
||||
|
|
@ -49,17 +73,17 @@ const DictPage: React.FC = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formattedQueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange.length === 2) {
|
||||
formattedQueryParams['beginTime'] = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams['endTime'] = dateRange[1].format('YYYY-MM-DD');
|
||||
const formattedQueryParams: DictQueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange[0] && dateRange[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD');
|
||||
} else {
|
||||
formattedQueryParams['beginTime'] = undefined;
|
||||
formattedQueryParams['endTime'] = undefined;
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
const response = await listDictType(formattedQueryParams);
|
||||
setTypeList(response.rows);
|
||||
setTotal(response.total);
|
||||
const response = await listDictType(formattedQueryParams) as { rows?: DictTypeRecord[]; total?: number };
|
||||
setTypeList(response.rows ?? []);
|
||||
setTotal(response.total ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dictionary type list:', error);
|
||||
message.error('获取字典类型列表失败');
|
||||
|
|
@ -100,14 +124,19 @@ const DictPage: React.FC = () => {
|
|||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleUpdate = async (row: any) => {
|
||||
const handleUpdate = async (row: Partial<DictTypeRecord> | React.Key[]) => {
|
||||
resetDictForm();
|
||||
try {
|
||||
const dictId = row.dictId || selectedRowKeys[0];
|
||||
const response = await getDictType(dictId);
|
||||
const dictId = Array.isArray(row) ? row[0] : row.dictId;
|
||||
if (dictId === undefined || dictId === null) {
|
||||
message.warning('请选择要修改的字典类型');
|
||||
return;
|
||||
}
|
||||
const normalizedDictId = typeof dictId === 'bigint' ? Number(dictId) : dictId;
|
||||
const response = await getDictType(normalizedDictId) as unknown;
|
||||
const detail = (response && typeof response === 'object' && 'data' in response)
|
||||
? response.data ?? {}
|
||||
: response ?? {};
|
||||
? (response.data as DictTypeRecord | undefined) ?? {}
|
||||
: (response as DictTypeRecord | undefined) ?? {};
|
||||
setCurrentDict(detail);
|
||||
dictForm.setFieldsValue({
|
||||
...detail,
|
||||
|
|
@ -145,16 +174,16 @@ const DictPage: React.FC = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listDictType({ ...queryParams, pageNum: undefined, pageSize: undefined });
|
||||
const response = await listDictType({ ...queryParams, pageNum: undefined, pageSize: undefined }) as { rows?: DictTypeRecord[] };
|
||||
const header = ['字典编号', '字典名称', '字典类型', '状态', '备注', '创建时间'];
|
||||
const data = response.rows.map((dict: any) => [
|
||||
const data = (response.rows ?? []).map((dict) => [
|
||||
dict.dictId, dict.dictName, dict.dictType,
|
||||
sysNormalDisableDict.find((d) => d.value === String(dict.status ?? ''))?.label ?? '',
|
||||
dict.remark, parseTime(dict.createTime),
|
||||
]);
|
||||
|
||||
const csvContent = [header, ...data]
|
||||
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.map((row) => row.map((cell: unknown) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
|
@ -213,22 +242,26 @@ const DictPage: React.FC = () => {
|
|||
title: '操作', key: 'operation', align: 'center', width: 180,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
<Popconfirm
|
||||
title="是否确认删除该字典类型?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Permission permissions="system:dict:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:dict:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="是否确认删除该字典类型?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container dict-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="字典名称" name="dictName">
|
||||
<Input placeholder="请输入字典名称" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
|
|
@ -260,11 +293,21 @@ const DictPage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate(selectedRowKeys)}>修改</Button>
|
||||
<Button type="danger" ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
<Button type="warning" ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
<Button type="danger" ghost icon={<SyncOutlined />} onClick={handleRefreshCache}>刷新缓存</Button>
|
||||
<Permission permissions="system:dict:add">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:dict:edit">
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate(selectedRowKeys)}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:dict:remove">
|
||||
<Button danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:dict:export">
|
||||
<Button ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
</Permission>
|
||||
<Permission permissions={canAccessPath('/system/dict') ? undefined : '__forbidden__'}>
|
||||
<Button danger ghost icon={<SyncOutlined />} onClick={handleRefreshCache}>刷新缓存</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
@ -291,6 +334,7 @@ const DictPage: React.FC = () => {
|
|||
|
||||
{/* Add/Edit Dictionary Type Modal */}
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={dictForm.submit}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Col,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
message,
|
||||
} from 'antd';
|
||||
|
|
@ -35,7 +38,6 @@ import {
|
|||
ClockCircleOutlined,
|
||||
CodeSandboxOutlined,
|
||||
ControlOutlined,
|
||||
DashboardOutlined,
|
||||
DesktopOutlined,
|
||||
EditOutlined as EditMenuIcon,
|
||||
FileTextOutlined,
|
||||
|
|
@ -61,8 +63,10 @@ import {
|
|||
treeselect,
|
||||
updateMenu,
|
||||
} from '../../api/system/menu';
|
||||
import { handleTree, parseTime } from '../../utils/ruoyi';
|
||||
import { handleTree } from '../../utils/ruoyi';
|
||||
import './menu.css';
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
interface MenuRecord {
|
||||
menuId?: number;
|
||||
|
|
@ -90,6 +94,17 @@ interface MenuTreeOption {
|
|||
children?: MenuTreeOption[];
|
||||
}
|
||||
|
||||
interface PageTreeRecord extends MenuRecord {
|
||||
children?: PageTreeRecord[];
|
||||
buttonChildren?: MenuRecord[];
|
||||
}
|
||||
|
||||
interface PageTreeNode {
|
||||
key: string;
|
||||
title: React.ReactNode;
|
||||
children?: PageTreeNode[];
|
||||
}
|
||||
|
||||
const sysNormalDisableDict = [
|
||||
{ value: '0', label: '正常', color: 'success' },
|
||||
{ value: '1', label: '停用', color: 'error' },
|
||||
|
|
@ -185,26 +200,131 @@ const extractDetailData = (response: unknown): MenuRecord => {
|
|||
};
|
||||
|
||||
const normalizeTreeSelectNodes = (nodes: MenuTreeOption[]): MenuTreeOption[] =>
|
||||
(nodes ?? [])
|
||||
.map((node) => {
|
||||
const id = node?.id;
|
||||
if (id === undefined || id === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
label: String(node.label ?? id),
|
||||
children: normalizeTreeSelectNodes(node.children ?? []),
|
||||
};
|
||||
})
|
||||
.filter((node): node is MenuTreeOption => Boolean(node));
|
||||
(nodes ?? []).reduce<MenuTreeOption[]>((acc, node) => {
|
||||
const id = node?.id;
|
||||
if (id === undefined || id === null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
id,
|
||||
label: String(node.label ?? id),
|
||||
children: normalizeTreeSelectNodes(node.children ?? []),
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const collectExpandedKeys = (nodes: MenuRecord[]): React.Key[] =>
|
||||
nodes.flatMap((node) => [
|
||||
node.menuId ?? '',
|
||||
String(node.menuId ?? ''),
|
||||
...(Array.isArray(node.children) ? collectExpandedKeys(node.children) : []),
|
||||
]).filter(Boolean);
|
||||
|
||||
const countMenuNodes = (nodes: MenuRecord[]): number =>
|
||||
nodes.reduce((total, node) => total + 1 + countMenuNodes(Array.isArray(node.children) ? node.children : []), 0);
|
||||
|
||||
const countByMenuType = (nodes: MenuRecord[], menuType: string): number =>
|
||||
nodes.reduce((total, node) => {
|
||||
const self = String(node.menuType ?? '') === menuType ? 1 : 0;
|
||||
const children = countByMenuType(Array.isArray(node.children) ? node.children : [], menuType);
|
||||
return total + self + children;
|
||||
}, 0);
|
||||
|
||||
const sortMenusByOrder = <T extends MenuRecord>(nodes: T[]): T[] =>
|
||||
[...nodes].sort((a, b) => {
|
||||
const orderDiff = Number(a.orderNum ?? 0) - Number(b.orderNum ?? 0);
|
||||
if (orderDiff !== 0) {
|
||||
return orderDiff;
|
||||
}
|
||||
return Number(a.menuId ?? 0) - Number(b.menuId ?? 0);
|
||||
});
|
||||
|
||||
const buildPageTreeRecords = (nodes: MenuRecord[]): PageTreeRecord[] =>
|
||||
sortMenusByOrder(nodes).reduce<PageTreeRecord[]>((acc, node) => {
|
||||
if (String(node.menuType ?? '') === 'F') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const children = sortMenusByOrder(Array.isArray(node.children) ? node.children : []);
|
||||
const buttonChildren = children
|
||||
.filter((child) => String(child.menuType ?? '') === 'F')
|
||||
.sort((a, b) => {
|
||||
const orderDiff = Number(a.orderNum ?? 0) - Number(b.orderNum ?? 0);
|
||||
if (orderDiff !== 0) {
|
||||
return orderDiff;
|
||||
}
|
||||
return Number(a.menuId ?? 0) - Number(b.menuId ?? 0);
|
||||
});
|
||||
const pageChildren = buildPageTreeRecords(children);
|
||||
|
||||
acc.push({
|
||||
...node,
|
||||
children: pageChildren,
|
||||
buttonChildren,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const flattenPageTree = (nodes: PageTreeRecord[]): PageTreeRecord[] =>
|
||||
nodes.flatMap((node) => [node, ...flattenPageTree(Array.isArray(node.children) ? node.children : [])]);
|
||||
|
||||
const findPageNodeById = (nodes: PageTreeRecord[], menuId?: React.Key): PageTreeRecord | undefined => {
|
||||
for (const node of nodes) {
|
||||
if (String(node.menuId ?? '') === String(menuId ?? '')) {
|
||||
return node;
|
||||
}
|
||||
const childHit = findPageNodeById(node.children ?? [], menuId);
|
||||
if (childHit) {
|
||||
return childHit;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildTreeNodeData = (
|
||||
nodes: PageTreeRecord[],
|
||||
expandedKeys: React.Key[] = [],
|
||||
selectedKey?: React.Key,
|
||||
onNodeClick?: (node: PageTreeRecord) => void,
|
||||
): PageTreeNode[] =>
|
||||
nodes.map((node) => ({
|
||||
key: String(node.menuId ?? ''),
|
||||
title: (
|
||||
<div
|
||||
className={`menu-tree-node${String(selectedKey ?? '') === String(node.menuId ?? '') ? ' menu-tree-node--active' : ''}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onNodeClick?.(node);
|
||||
}}
|
||||
>
|
||||
<div className="menu-tree-node-main">
|
||||
{node.children?.length ? (
|
||||
<span className="menu-tree-node-toggle">
|
||||
{expandedKeys.some((item) => String(item) === String(node.menuId ?? '')) ? (
|
||||
<CaretDownFilled />
|
||||
) : (
|
||||
<CaretRightFilled />
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="menu-tree-node-title">{String(node.menuName ?? '-')}</span>
|
||||
</div>
|
||||
<Space size={6} wrap>
|
||||
<Tag color={findDict(menuTypeDict, String(node.menuType ?? ''))?.color}>
|
||||
{findDict(menuTypeDict, String(node.menuType ?? ''))?.label ?? '页面'}
|
||||
</Tag>
|
||||
{node.buttonChildren?.length ? (
|
||||
<Tag variant="filled" className="menu-tree-node-badge">
|
||||
{node.buttonChildren.length} 个按钮
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
children: buildTreeNodeData(node.children ?? [], expandedKeys, selectedKey, onNodeClick),
|
||||
}));
|
||||
|
||||
const findDict = (dict: Array<{ value: string; label: string; color?: string }>, value?: string) =>
|
||||
dict.find((item) => item.value === String(value ?? ''));
|
||||
|
||||
|
|
@ -223,8 +343,8 @@ const MenuPage: React.FC = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [menuList, setMenuList] = useState<MenuRecord[]>([]);
|
||||
const [menuOptions, setMenuOptions] = useState<MenuTreeOption[]>([]);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
|
||||
const [isExpandAll, setIsExpandAll] = useState(true);
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||
const [selectedPageId, setSelectedPageId] = useState<React.Key>();
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
|
|
@ -241,14 +361,13 @@ const MenuPage: React.FC = () => {
|
|||
const response = await listMenu(queryParams);
|
||||
const list = handleTree(extractListData(response), 'menuId') as MenuRecord[];
|
||||
setMenuList(list);
|
||||
setExpandedRowKeys(isExpandAll ? collectExpandedKeys(list) : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch menu list:', error);
|
||||
message.error('获取菜单列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isExpandAll, queryParams]);
|
||||
}, [queryParams]);
|
||||
|
||||
const getTreeselect = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -293,14 +412,15 @@ const MenuPage: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleAdd = async (row?: MenuRecord) => {
|
||||
const handleAdd = async (row?: MenuRecord, preset?: Partial<MenuRecord>, title?: string) => {
|
||||
resetMenuForm();
|
||||
await getTreeselect();
|
||||
menuForm.setFieldsValue({
|
||||
...defaultMenuValues,
|
||||
...preset,
|
||||
parentId: row?.menuId ?? 0,
|
||||
});
|
||||
setModalTitle(row?.menuId ? '新增下级菜单' : '新增菜单');
|
||||
setModalTitle(title ?? (row?.menuId ? '新增下级菜单' : '新增菜单'));
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -335,17 +455,6 @@ const MenuPage: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
if (isExpandAll) {
|
||||
setExpandedRowKeys([]);
|
||||
setIsExpandAll(false);
|
||||
return;
|
||||
}
|
||||
const keys = collectExpandedKeys(menuList);
|
||||
setExpandedRowKeys(keys);
|
||||
setIsExpandAll(true);
|
||||
};
|
||||
|
||||
const submitMenuForm = async (values: MenuRecord) => {
|
||||
try {
|
||||
const payload = { ...currentMenu, ...values };
|
||||
|
|
@ -365,27 +474,77 @@ const MenuPage: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const columns: TableColumnsType<MenuRecord> = useMemo(() => [
|
||||
const pageTreeData = useMemo(() => buildPageTreeRecords(menuList), [menuList]);
|
||||
const allExpandableKeys = useMemo(() => collectExpandedKeys(pageTreeData), [pageTreeData]);
|
||||
const isAllExpanded = useMemo(
|
||||
() => allExpandableKeys.length > 0 && allExpandableKeys.every((key) => expandedKeys.some((item) => String(item) === String(key))),
|
||||
[allExpandableKeys, expandedKeys],
|
||||
);
|
||||
|
||||
const selectedPage = useMemo(
|
||||
() => findPageNodeById(pageTreeData, selectedPageId),
|
||||
[pageTreeData, selectedPageId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageTreeData.length) {
|
||||
setSelectedPageId(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPageId !== undefined && findPageNodeById(pageTreeData, selectedPageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPageId(pageTreeData[0]?.menuId !== undefined ? String(pageTreeData[0].menuId) : undefined);
|
||||
}, [pageTreeData, selectedPageId]);
|
||||
|
||||
const pageNodes = useMemo(() => flattenPageTree(pageTreeData), [pageTreeData]);
|
||||
const handlePageNodeClick = useCallback((node: PageTreeRecord) => {
|
||||
const nextSelectedKey = String(node.menuId ?? '');
|
||||
setSelectedPageId(nextSelectedKey);
|
||||
|
||||
if (!node.children?.length || !nextSelectedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedKeys((prev) => {
|
||||
const exists = prev.some((item) => String(item) === nextSelectedKey);
|
||||
return exists
|
||||
? prev.filter((item) => String(item) !== nextSelectedKey)
|
||||
: [...prev, nextSelectedKey];
|
||||
});
|
||||
}, []);
|
||||
const toggleExpandAll = useCallback(() => {
|
||||
setExpandedKeys(isAllExpanded ? [] : allExpandableKeys);
|
||||
}, [allExpandableKeys, isAllExpanded]);
|
||||
const pageTreeNodes = useMemo(
|
||||
() => buildTreeNodeData(pageTreeData, expandedKeys, selectedPageId, handlePageNodeClick),
|
||||
[expandedKeys, handlePageNodeClick, pageTreeData, selectedPageId],
|
||||
);
|
||||
|
||||
const buttonColumns: TableColumnsType<MenuRecord> = useMemo(() => [
|
||||
{
|
||||
title: '菜单名称',
|
||||
title: '按钮名称',
|
||||
dataIndex: 'menuName',
|
||||
key: 'menuName',
|
||||
width: 240,
|
||||
ellipsis: true,
|
||||
width: 280,
|
||||
render: (value: unknown, record) => (
|
||||
<div className="menu-name-cell">
|
||||
<span className="menu-name-text">{String(value ?? '-')}</span>
|
||||
{record.icon ? <Tag className="menu-icon-tag">{String(record.icon)}</Tag> : null}
|
||||
<span className="menu-icon-chip">{renderIconPreview(String(record.icon ?? ''))}</span>
|
||||
<div className="menu-name-main">
|
||||
<div className="menu-name-topline">
|
||||
<span className="menu-name-text">{String(value ?? '-')}</span>
|
||||
<Tag color={findDict(menuTypeDict, 'F')?.color}>按钮</Tag>
|
||||
</div>
|
||||
<div className="menu-meta-line">
|
||||
<span>权限:{String(record.perms ?? '-')}</span>
|
||||
{record.icon ? <span>图标:{String(record.icon)}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
key: 'icon',
|
||||
width: 180,
|
||||
render: (value: string) => renderIconPreview(value),
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'orderNum',
|
||||
|
|
@ -393,66 +552,51 @@ const MenuPage: React.FC = () => {
|
|||
align: 'center',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
dataIndex: 'perms',
|
||||
key: 'perms',
|
||||
ellipsis: true,
|
||||
render: (value: unknown) => String(value ?? '-') || '-',
|
||||
},
|
||||
{
|
||||
title: '组件路径',
|
||||
dataIndex: 'component',
|
||||
key: 'component',
|
||||
ellipsis: true,
|
||||
render: (value: unknown) => String(value ?? '-') || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
render: (value: string) => {
|
||||
const dict = findDict(sysNormalDisableDict, value);
|
||||
return <Tag color={dict?.color}>{dict?.label ?? '-'}</Tag>;
|
||||
width: 150,
|
||||
render: (value: string, record) => {
|
||||
const statusDict = findDict(sysNormalDisableDict, value);
|
||||
const visibleDict = findDict(sysShowHideDict, String(record.visible ?? '0'));
|
||||
return (
|
||||
<Space size={6} wrap>
|
||||
<Tag color={statusDict?.color}>{statusDict?.label ?? '-'}</Tag>
|
||||
<Tag variant="filled">{visibleDict?.label ?? '-'}</Tag>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: (value: string) => parseTime(value) || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
align: 'center',
|
||||
width: 220,
|
||||
width: 180,
|
||||
render: (_value, record) => (
|
||||
<Space size={4} wrap>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
<Button type="link" icon={<PlusOutlined />} onClick={() => handleAdd(record)}>
|
||||
新增
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确认删除 "${record.menuName}" 吗?`}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => void handleDelete(record)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
<Permission permissions="system:menu:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
<Permission permissions="system:menu:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title={`确认删除 "${record.menuName}" 吗?`}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => void handleDelete(record)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
], [menuList]);
|
||||
], []);
|
||||
|
||||
return (
|
||||
<div className="app-container menu-page-container">
|
||||
|
|
@ -488,52 +632,111 @@ const MenuPage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<div className="table-toolbar">
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => void handleAdd()}>
|
||||
新增
|
||||
</Button>
|
||||
<Space align="center" wrap>
|
||||
<Permission permissions="system:menu:add">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => void handleAdd(undefined, { menuType: 'M' }, '新增页面')}>
|
||||
新增页面
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:menu:add">
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => selectedPage && void handleAdd(selectedPage, { menuType: 'C' }, '新增下级页面')}
|
||||
disabled={!selectedPage}
|
||||
>
|
||||
新增下级页面
|
||||
</Button>
|
||||
</Permission>
|
||||
<Button
|
||||
icon={isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
|
||||
icon={isAllExpanded ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
|
||||
onClick={toggleExpandAll}
|
||||
>
|
||||
{isExpandAll ? '折叠' : '展开'}
|
||||
{isAllExpanded ? '折叠' : '展开'}
|
||||
</Button>
|
||||
<span className="menu-toolbar-tip">左侧看页面树,右侧看当前页面按钮</span>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="menu-table-wrap">
|
||||
<Table<MenuRecord>
|
||||
columns={columns}
|
||||
dataSource={menuList}
|
||||
rowKey="menuId"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
expandable={{
|
||||
expandedRowKeys,
|
||||
onExpandedRowsChange: (keys) => {
|
||||
setExpandedRowKeys(keys);
|
||||
setIsExpandAll(keys.length > 0);
|
||||
},
|
||||
expandIcon: ({ expanded, onExpand, record }) => {
|
||||
const hasChildren = Array.isArray(record.children) && record.children.length > 0;
|
||||
if (!hasChildren) {
|
||||
return <span className="menu-expand-spacer" />;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="menu-expand-trigger"
|
||||
onClick={(event) => onExpand(record, event)}
|
||||
aria-label={expanded ? '收起' : '展开'}
|
||||
>
|
||||
{expanded ? <CaretDownFilled /> : <CaretRightFilled />}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}}
|
||||
scroll={{ y: 'calc(100vh - 320px)' }}
|
||||
/>
|
||||
<div className="menu-split-layout">
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="menu-tree-card"
|
||||
title={`页面树 (${pageNodes.length})`}
|
||||
extra={
|
||||
selectedPage ? (
|
||||
<Space size={4} wrap>
|
||||
<Permission permissions="system:menu:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => void handleUpdate(selectedPage)}>
|
||||
修改页面
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:menu:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title={`确认删除 "${selectedPage.menuName}" 吗?`}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => void handleDelete(selectedPage)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除页面
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<Tree
|
||||
blockNode
|
||||
showLine={{ showLeafIcon: false }}
|
||||
selectable={false}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => {
|
||||
setExpandedKeys(keys.map((key) => String(key)));
|
||||
}}
|
||||
treeData={pageTreeNodes}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
variant="borderless"
|
||||
className="menu-button-card"
|
||||
title={selectedPage ? `${selectedPage.menuName} 的按钮` : '页面按钮'}
|
||||
extra={
|
||||
<Permission permissions="system:menu:add">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => selectedPage && void handleAdd(selectedPage, { menuType: 'F' }, '新增按钮')}
|
||||
disabled={!selectedPage}
|
||||
>
|
||||
新增按钮
|
||||
</Button>
|
||||
</Permission>
|
||||
}
|
||||
>
|
||||
{selectedPage ? (
|
||||
<>
|
||||
<div className="menu-button-summary">
|
||||
<span>页面路径:{String(selectedPage.path ?? '-')}</span>
|
||||
<span>组件:{String(selectedPage.component ?? '-')}</span>
|
||||
<span>按钮数:{selectedPage.buttonChildren?.length ?? 0}</span>
|
||||
</div>
|
||||
<Table<MenuRecord>
|
||||
columns={buttonColumns}
|
||||
dataSource={selectedPage.buttonChildren ?? []}
|
||||
rowKey="menuId"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
locale={{ emptyText: <Empty description="当前页面没有按钮" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无页面数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table, Form, Input, Select, Button, Modal, message, Space, Tag, DatePicker, Switch, Dropdown, Menu, InputNumber, Tree, Tooltip, Checkbox, Popconfirm, Row, Col
|
||||
App, Table, Form, Input, Select, Button, Modal, Space, DatePicker, Switch, Dropdown, InputNumber, Tree, Tooltip, Checkbox, Popconfirm, Row, Col
|
||||
} from 'antd';
|
||||
import type { TableColumnsType, MenuProps, GetProps } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
SearchOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, DownloadOutlined,
|
||||
KeyOutlined, CheckCircleOutlined, DownOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, UserOutlined
|
||||
CheckCircleOutlined, DownOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, UserOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
listRole, getRole, addRole, updateRole, dataScope, changeRoleStatus, delRole, deptTreeSelect
|
||||
|
|
@ -14,6 +14,8 @@ import { treeselect as menuTreeselect, roleMenuTreeselect } from '../../api/syst
|
|||
import { saveAs } from 'file-saver';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseTime } from '../../utils/ruoyi'; // Custom utility
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
|
|
@ -31,16 +33,36 @@ const dataScopeOptions = [
|
|||
{ value: '5', label: '仅本人数据权限' },
|
||||
];
|
||||
|
||||
type RoleQueryParams = {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
roleName?: string;
|
||||
roleKey?: string;
|
||||
status?: string;
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
type RoleRecord = {
|
||||
roleId?: string | number;
|
||||
roleName?: string;
|
||||
roleKey?: string;
|
||||
roleSort?: string | number;
|
||||
status?: string;
|
||||
createTime?: string | number | Date;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const RolePage: React.FC = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm();
|
||||
const [roleForm] = Form.useForm();
|
||||
const [dataScopeForm] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [roleList, setRoleList] = useState<any[]>([]);
|
||||
const [roleList, setRoleList] = useState<RoleRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]); // Added to store selected rows
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||||
|
||||
const [addEditModalVisible, setAddEditModalVisible] = useState(false);
|
||||
const [addEditModalTitle, setAddEditModalTitle] = useState('');
|
||||
|
|
@ -65,9 +87,9 @@ const RolePage: React.FC = () => {
|
|||
const [menuCheckStrictly, setMenuCheckStrictly] = useState(true); // For menu tree linkage
|
||||
const [deptCheckStrictly, setDeptCheckStrictly] = useState(true); // For dept tree linkage
|
||||
|
||||
const [currentRole, setCurrentRole] = useState<any>({}); // For storing current role in modals
|
||||
const [currentRole, setCurrentRole] = useState<RoleRecord>({}); // For storing current role in modals
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
const [queryParams, setQueryParams] = useState<RoleQueryParams>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
roleName: undefined,
|
||||
|
|
@ -131,17 +153,17 @@ const RolePage: React.FC = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formattedQueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange.length === 2) {
|
||||
formattedQueryParams['beginTime'] = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams['endTime'] = dateRange[1].format('YYYY-MM-DD');
|
||||
const formattedQueryParams: RoleQueryParams = { ...queryParams };
|
||||
if (dateRange && dateRange[0] && dateRange[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD');
|
||||
} else {
|
||||
formattedQueryParams['beginTime'] = undefined;
|
||||
formattedQueryParams['endTime'] = undefined;
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
const response = await listRole(formattedQueryParams);
|
||||
setRoleList(response.rows);
|
||||
setTotal(response.total);
|
||||
const response = await listRole(formattedQueryParams) as { rows?: RoleRecord[]; total?: number };
|
||||
setRoleList(response.rows ?? []);
|
||||
setTotal(response.total ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch role list:', error);
|
||||
message.error('获取角色列表失败');
|
||||
|
|
@ -164,7 +186,7 @@ const RolePage: React.FC = () => {
|
|||
|
||||
const getDeptTreeselect = useCallback(async (roleId?: string | number) => {
|
||||
try {
|
||||
const response = await deptTreeSelect(roleId);
|
||||
const response = await deptTreeSelect(roleId ?? '');
|
||||
setDeptOptions(extractTreeNodes(response, 'depts'));
|
||||
return response;
|
||||
} catch (error) {
|
||||
|
|
@ -179,8 +201,15 @@ const RolePage: React.FC = () => {
|
|||
}, [getList]);
|
||||
|
||||
|
||||
const handleQuery = () => {
|
||||
setQueryParams(prev => ({ ...prev, pageNum: 1 }));
|
||||
const handleQuery = (values?: Partial<RoleQueryParams>) => {
|
||||
const formValues = values ?? queryForm.getFieldsValue();
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
pageNum: 1,
|
||||
roleName: formValues.roleName || undefined,
|
||||
roleKey: formValues.roleKey || undefined,
|
||||
status: formValues.status || undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const resetQuery = () => {
|
||||
|
|
@ -195,15 +224,14 @@ const RolePage: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleSelectionChange = (selectedKeys: React.Key[], selectedRows: any[]) => {
|
||||
const handleSelectionChange = (selectedKeys: React.Key[]) => {
|
||||
setSelectedRowKeys(selectedKeys);
|
||||
setSelectedRows(selectedRows);
|
||||
};
|
||||
|
||||
const handleStatusChange = async (record: any) => {
|
||||
const newStatus = record.status === '0' ? '1' : '0';
|
||||
const text = record.status === '0' ? '停用' : '启用';
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认操作',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `确认要"${text}"角色"${record.roleName}"吗?`,
|
||||
|
|
@ -268,13 +296,12 @@ const RolePage: React.FC = () => {
|
|||
try {
|
||||
const roleMenuResponse = await getMenuTreeselect(roleId); // Get menu tree with selected keys
|
||||
const roleResponse = await getRole(roleId);
|
||||
const checkedKeys = extractCheckedKeys(roleMenuResponse);
|
||||
|
||||
const roleData = extractResponseData(roleResponse);
|
||||
setCurrentRole(roleData);
|
||||
setMenuCheckStrictly(roleData.menuCheckStrictly === undefined ? true : !!roleData.menuCheckStrictly);
|
||||
setDeptCheckStrictly(roleData.deptCheckStrictly === undefined ? true : !!roleData.deptCheckStrictly);
|
||||
setMenuCheckedKeys(checkedKeys);
|
||||
setMenuCheckedKeys(extractCheckedKeys(roleMenuResponse));
|
||||
setMenuHalfCheckedKeys([]);
|
||||
setAddEditModalTitle('修改角色');
|
||||
setFormType('edit');
|
||||
|
|
@ -296,7 +323,7 @@ const RolePage: React.FC = () => {
|
|||
message.warning('请选择要删除的角色');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认删除角色编号为"${roleIds.join(',')}"的数据项?`,
|
||||
|
|
@ -316,16 +343,16 @@ const RolePage: React.FC = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listRole({ ...queryParams, pageNum: undefined, pageSize: undefined });
|
||||
const response = await listRole({ ...queryParams, pageNum: undefined, pageSize: undefined }) as { rows?: RoleRecord[] };
|
||||
const header = ['角色编号', '角色名称', '权限字符', '显示顺序', '状态', '创建时间'];
|
||||
const data = response.rows.map((role: any) => [
|
||||
const data = (response.rows ?? []).map((role) => [
|
||||
role.roleId, role.roleName, role.roleKey, role.roleSort,
|
||||
sysNormalDisableDict.find((d) => d.value === String(role.status ?? ''))?.label ?? '',
|
||||
parseTime(role.createTime),
|
||||
]);
|
||||
|
||||
const csvContent = [header, ...data]
|
||||
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.map((row) => row.map((cell: unknown) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
|
@ -338,7 +365,7 @@ const RolePage: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleCommand: MenuProps['onClick'] = ({ key }, record: any) => {
|
||||
const handleCommand = (key: string, record: any) => {
|
||||
switch (key) {
|
||||
case 'handleDataScope':
|
||||
handleDataScope(record);
|
||||
|
|
@ -479,14 +506,26 @@ const RolePage: React.FC = () => {
|
|||
{ title: '显示顺序', dataIndex: 'roleSort', align: 'center', width: 80 },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', align: 'center', width: 100,
|
||||
render: (text: string, record: any) => (
|
||||
<Switch
|
||||
checked={text === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
onChange={() => handleStatusChange(record)}
|
||||
disabled={record.roleId === 1} // Disable for admin role
|
||||
/>
|
||||
render: (text: string, record: RoleRecord) => (
|
||||
<Permission
|
||||
permissions="system:role:edit"
|
||||
fallback={
|
||||
<Switch
|
||||
checked={text === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
checked={text === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
onChange={() => handleStatusChange(record)}
|
||||
disabled={record.roleId === 1}
|
||||
/>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'createTime', align: 'center', width: 180, render: (text: string) => parseTime(text) },
|
||||
|
|
@ -494,42 +533,52 @@ const RolePage: React.FC = () => {
|
|||
title: '操作', key: 'operation', align: 'center', width: 220,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)} disabled={record.roleId === 1}>修改</Button>
|
||||
<Popconfirm
|
||||
title="是否确认删除该角色?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
disabled={record.roleId === 1} // Disable delete for admin role
|
||||
<Permission permissions="system:role:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleUpdate(record)} disabled={record.roleId === 1}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:role:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="是否确认删除该角色?"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
disabled={record.roleId === 1}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} disabled={record.roleId === 1}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
<Permission
|
||||
permissions="system:role:edit"
|
||||
mode="or"
|
||||
fallback={<ReadonlyAction icon={<DownOutlined />}>更多</ReadonlyAction>}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} disabled={record.roleId === 1}>删除</Button>
|
||||
</Popconfirm>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'handleDataScope', icon: <CheckCircleOutlined />, label: '数据权限' },
|
||||
{ key: 'handleAuthUser', icon: <UserOutlined />, label: '分配用户' },
|
||||
],
|
||||
onClick: ({ key }) => handleCommand({ key }, record),
|
||||
}}
|
||||
trigger={['click']}
|
||||
disabled={record.roleId === 1} // Disable dropdown for admin role
|
||||
>
|
||||
<Button type="link" icon={<DownOutlined />} disabled={record.roleId === 1}>更多</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'handleDataScope', icon: <CheckCircleOutlined />, label: '数据权限' },
|
||||
{ key: 'handleAuthUser', icon: <UserOutlined />, label: '分配用户' },
|
||||
],
|
||||
onClick: ({ key }) => handleCommand(String(key), record),
|
||||
}}
|
||||
trigger={['click']}
|
||||
disabled={record.roleId === 1}
|
||||
>
|
||||
<Button type="link" icon={<DownOutlined />} disabled={record.roleId === 1}>更多</Button>
|
||||
</Dropdown>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container role-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="角色名称" name="roleName">
|
||||
<Input placeholder="请输入角色名称" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
<Input placeholder="请输入角色名称" allowClear onPressEnter={() => queryForm.submit()} style={{ width: 240 }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="权限字符" name="roleKey">
|
||||
<Input placeholder="请输入权限字符" allowClear onPressEnter={handleQuery} style={{ width: 240 }} />
|
||||
<Input placeholder="请输入权限字符" allowClear onPressEnter={() => queryForm.submit()} style={{ width: 240 }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select placeholder="角色状态" allowClear style={{ width: 240 }}>
|
||||
|
|
@ -553,10 +602,18 @@ const RolePage: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate()}>修改</Button>
|
||||
<Button type="danger" ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
<Button type="warning" ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
<Permission permissions="system:role:add">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:role:edit">
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => handleUpdate()}>修改</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:role:remove">
|
||||
<Button danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>删除</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:role:export">
|
||||
<Button ghost icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
@ -583,6 +640,7 @@ const RolePage: React.FC = () => {
|
|||
|
||||
{/* Add/Edit Role Modal */}
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={addEditModalTitle}
|
||||
open={addEditModalVisible}
|
||||
onOk={roleForm.submit}
|
||||
|
|
@ -631,6 +689,7 @@ const RolePage: React.FC = () => {
|
|||
</Col>
|
||||
</Row>
|
||||
<Tree
|
||||
className="system-tree-panel"
|
||||
checkable
|
||||
treeData={menuOptions}
|
||||
fieldNames={{ title: 'label', key: 'id' }}
|
||||
|
|
@ -653,6 +712,7 @@ const RolePage: React.FC = () => {
|
|||
|
||||
{/* Assign Data Scope Modal */}
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={dataScopeModalTitle}
|
||||
open={dataScopeModalVisible}
|
||||
onOk={dataScopeForm.submit}
|
||||
|
|
@ -690,6 +750,7 @@ const RolePage: React.FC = () => {
|
|||
</Col>
|
||||
</Row>
|
||||
<Tree
|
||||
className="system-tree-panel"
|
||||
checkable
|
||||
treeData={deptOptions}
|
||||
fieldNames={{ title: 'label', key: 'id' }}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ import {
|
|||
changeUserStatus,
|
||||
} from '../../api/system/user';
|
||||
import dayjs from 'dayjs';
|
||||
import Permission from '@/components/Permission';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
interface UserDept {
|
||||
deptName?: string;
|
||||
|
|
@ -316,12 +318,24 @@ const UserPage = () => {
|
|||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
render: (_value, record) => (
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
onChange={() => handleStatusChange(record)}
|
||||
/>
|
||||
<Permission
|
||||
permissions="system:user:edit"
|
||||
fallback={
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
disabled
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
checked={String(record.status ?? '') === '0'}
|
||||
checkedChildren="正常"
|
||||
unCheckedChildren="停用"
|
||||
onChange={() => handleStatusChange(record)}
|
||||
/>
|
||||
</Permission>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -338,22 +352,28 @@ const UserPage = () => {
|
|||
width: 280,
|
||||
render: (_value, record) => (
|
||||
<Space size="small">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => void handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
<Button type="link" icon={<KeyOutlined />} onClick={() => handleResetPwd(record)}>
|
||||
重置密码
|
||||
</Button>
|
||||
<Permission permissions="system:user:edit" fallback={<ReadonlyAction icon={<EditOutlined />}>修改</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => void handleUpdate(record)}>
|
||||
修改
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:user:remove" fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:user:resetPwd" fallback={<ReadonlyAction icon={<KeyOutlined />}>重置密码</ReadonlyAction>}>
|
||||
<Button type="link" icon={<KeyOutlined />} onClick={() => handleResetPwd(record)}>
|
||||
重置密码
|
||||
</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<div className="app-container user-page-container">
|
||||
<Form form={queryForm} layout="inline" className="search-form" onFinish={handleQuery} initialValues={queryParams}>
|
||||
<Form.Item label="用户名称" name="userName">
|
||||
<Input placeholder="请输入用户名称" allowClear onPressEnter={handleQuery} />
|
||||
|
|
@ -381,15 +401,21 @@ const UserPage = () => {
|
|||
</Form>
|
||||
|
||||
<Space className="mb8">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => void handleUpdate()}>
|
||||
修改
|
||||
</Button>
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>
|
||||
删除
|
||||
</Button>
|
||||
<Permission permissions="system:user:add">
|
||||
<Button type="primary" ghost icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:user:edit">
|
||||
<Button type="primary" ghost icon={<EditOutlined />} disabled={selectedRowKeys.length !== 1} onClick={() => void handleUpdate()}>
|
||||
修改
|
||||
</Button>
|
||||
</Permission>
|
||||
<Permission permissions="system:user:remove">
|
||||
<Button type="primary" danger ghost icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} onClick={() => handleDelete()}>
|
||||
删除
|
||||
</Button>
|
||||
</Permission>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
|
|
@ -415,6 +441,7 @@ const UserPage = () => {
|
|||
/>
|
||||
|
||||
<Modal
|
||||
className="system-admin-modal"
|
||||
title={modalTitle}
|
||||
open={modalVisible}
|
||||
onOk={submitForm}
|
||||
|
|
|
|||
|
|
@ -1,7 +1 @@
|
|||
.config-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.config-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@import "./system-admin.css";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
.dept-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dept-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@import "./system-admin.css";
|
||||
|
||||
.dept-page-container .ant-table-row-expand-icon {
|
||||
margin-right: 8px;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
.dict-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dict-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@import "./system-admin.css";
|
||||
|
||||
.dict-page-container .link-type {
|
||||
color: #1890ff;
|
||||
color: #4f46e5;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dict-page-container .link-type:hover {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
.menu-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
padding: 4px 2px 8px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(64, 158, 255, 0.06), transparent 22%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
radial-gradient(circle at top left, rgba(21, 128, 61, 0.08), transparent 24%),
|
||||
radial-gradient(circle at right top, rgba(14, 116, 144, 0.08), transparent 18%),
|
||||
linear-gradient(180deg, #f7faf8 0%, #f3f6f4 100%);
|
||||
}
|
||||
|
||||
.menu-page-container .search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 0;
|
||||
padding: 16px 18px 4px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid #e5eaf3;
|
||||
padding: 14px 16px 2px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid #e4ebe7;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.05);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.menu-page-container .ant-form-item {
|
||||
|
|
@ -32,7 +33,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px;
|
||||
padding: 0 4px 2px;
|
||||
}
|
||||
|
||||
.table-toolbar .ant-btn {
|
||||
|
|
@ -40,20 +41,139 @@
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.menu-toolbar-tip {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-table-wrap {
|
||||
background: #fff;
|
||||
border: 1px solid #e5eaf3;
|
||||
border: 1px solid #e4ebe7;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.menu-split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1.05fr) minmax(420px, 1.35fr);
|
||||
gap: 16px;
|
||||
min-height: calc(100vh - 330px);
|
||||
}
|
||||
|
||||
.menu-tree-card,
|
||||
.menu-button-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e4ebe7;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-card-head,
|
||||
.menu-button-card .ant-card-head {
|
||||
min-height: 58px;
|
||||
border-bottom: 1px solid #edf2ee;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-card-body,
|
||||
.menu-button-card .ant-card-body {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-list-holder-inner {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-treenode {
|
||||
width: 100%;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-switcher {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-node-content-wrapper {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-node-content-wrapper:hover {
|
||||
background: #f5faf7;
|
||||
}
|
||||
|
||||
.menu-tree-card .ant-tree-node-selected {
|
||||
background: #eef8f1 !important;
|
||||
box-shadow: inset 0 0 0 1px #d7eadc;
|
||||
}
|
||||
|
||||
.menu-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px 8px 2px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.menu-tree-node--active {
|
||||
background: #eef8f1;
|
||||
}
|
||||
|
||||
.menu-tree-node-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.menu-tree-node-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
color: #7a8699;
|
||||
font-size: 12px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.menu-tree-node-title {
|
||||
min-width: 0;
|
||||
color: #172033;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-tree-node-badge {
|
||||
color: #0f766e;
|
||||
background: #eef8f6;
|
||||
}
|
||||
|
||||
.menu-button-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
padding: 0 0 14px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-table-wrap .ant-table {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-table-wrap .ant-table-thead > tr > th {
|
||||
background: linear-gradient(180deg, #fbfcfe 0%, #f3f6fb 100%);
|
||||
.menu-button-card .ant-table {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-table-wrap .ant-table-thead > tr > th,
|
||||
.menu-button-card .ant-table-thead > tr > th {
|
||||
background: linear-gradient(180deg, #f9fcfa 0%, #f1f6f3 100%);
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
|
@ -61,21 +181,43 @@
|
|||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.menu-table-wrap .ant-table-tbody > tr > td {
|
||||
.menu-table-wrap .ant-table-tbody > tr > td,
|
||||
.menu-button-card .ant-table-tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-table-wrap .ant-table-tbody > tr:hover > td {
|
||||
background: #f8fbff !important;
|
||||
.menu-table-wrap .ant-table-tbody > tr:hover > td,
|
||||
.menu-button-card .ant-table-tbody > tr:hover > td {
|
||||
background: #f7fbf8 !important;
|
||||
}
|
||||
|
||||
.menu-name-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.menu-icon-chip {
|
||||
display: inline-flex;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.menu-name-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.menu-name-topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.menu-name-text {
|
||||
|
|
@ -87,6 +229,14 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-meta-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-icon-tag {
|
||||
margin-inline-end: 0;
|
||||
color: #64748b;
|
||||
|
|
@ -108,9 +258,9 @@
|
|||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%);
|
||||
border: 1px solid #dbe7fb;
|
||||
color: #3b82f6;
|
||||
background: linear-gradient(180deg, #f9fcfa 0%, #edf7f1 100%);
|
||||
border: 1px solid #dcebdd;
|
||||
color: #15803d;
|
||||
font-size: 14px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
|
@ -143,8 +293,8 @@
|
|||
}
|
||||
|
||||
.menu-expand-trigger:hover {
|
||||
background: #eef5ff;
|
||||
color: #409eff;
|
||||
background: #edf7f1;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.menu-expand-spacer {
|
||||
|
|
@ -213,4 +363,8 @@
|
|||
.menu-page-container .search-form {
|
||||
padding: 14px 14px 2px;
|
||||
}
|
||||
.menu-split-layout {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
.role-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.role-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@import "./system-admin.css";
|
||||
|
||||
.role-page-container .ant-tree-checkbox-inner {
|
||||
margin-right: 8px;
|
||||
|
|
@ -15,9 +9,5 @@
|
|||
}
|
||||
|
||||
.role-page-container .tree-border {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
min-height: 200px;
|
||||
overflow-y: auto;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
.user-page-container,
|
||||
.role-page-container,
|
||||
.dept-page-container,
|
||||
.config-page-container,
|
||||
.dict-page-container {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.user-page-container .search-form .ant-form-item,
|
||||
.role-page-container .search-form .ant-form-item,
|
||||
.dept-page-container .search-form .ant-form-item,
|
||||
.config-page-container .search-form .ant-form-item,
|
||||
.dict-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.user-page-container .mb8,
|
||||
.role-page-container .mb8,
|
||||
.dept-page-container .mb8,
|
||||
.config-page-container .mb8,
|
||||
.dict-page-container .mb8 {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.user-page-container .ant-card-head,
|
||||
.role-page-container .ant-card-head,
|
||||
.dept-page-container .ant-card-head,
|
||||
.config-page-container .ant-card-head,
|
||||
.dict-page-container .ant-card-head {
|
||||
padding: 0 18px;
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.user-page-container .ant-card-body,
|
||||
.role-page-container .ant-card-body,
|
||||
.dept-page-container .ant-card-body,
|
||||
.config-page-container .ant-card-body,
|
||||
.dict-page-container .ant-card-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.user-page-container .ant-table-wrapper .ant-btn-link,
|
||||
.role-page-container .ant-table-wrapper .ant-btn-link,
|
||||
.dept-page-container .ant-table-wrapper .ant-btn-link,
|
||||
.config-page-container .ant-table-wrapper .ant-btn-link,
|
||||
.dict-page-container .ant-table-wrapper .ant-btn-link {
|
||||
padding-inline: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-page-container .ant-select-tree,
|
||||
.dept-page-container .ant-select-tree {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.role-page-container .ant-tree-checkbox-inner {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.role-page-container .ant-tree-checkbox + span {
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
|
||||
.tree-border,
|
||||
.system-tree-panel {
|
||||
min-height: 220px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 18px;
|
||||
background: rgba(247, 249, 255, 0.82);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-modal-content {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-modal-header {
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-modal-title {
|
||||
color: #162033;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-modal-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-form-item-label > label {
|
||||
color: #5f6f92;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-radio-wrapper {
|
||||
margin-inline-end: 0;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 14px;
|
||||
background: rgba(247, 249, 255, 0.7);
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-tree,
|
||||
.system-admin-modal .ant-select-tree {
|
||||
color: #34425f;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-tree .ant-tree-node-content-wrapper,
|
||||
.system-admin-modal .ant-select-tree .ant-select-tree-node-content-wrapper {
|
||||
min-height: 34px;
|
||||
border-radius: 10px;
|
||||
transition: background-color 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-tree .ant-tree-node-content-wrapper:hover,
|
||||
.system-admin-modal .ant-select-tree .ant-select-tree-node-content-wrapper:hover {
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.system-admin-modal .ant-tree .ant-tree-node-selected,
|
||||
.system-admin-modal .ant-select-tree .ant-select-tree-node-selected {
|
||||
background: rgba(99, 91, 255, 0.12) !important;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
|
@ -1,21 +1,14 @@
|
|||
.user-page-container .search-form .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.user-page-container .mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@import "./system-admin.css";
|
||||
|
||||
.user-page-container .ant-card-head {
|
||||
padding: 0 16px;
|
||||
min-height: 48px;
|
||||
padding: 0 18px;
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.user-page-container .ant-card-body {
|
||||
padding: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
/* Adjust Ant Design TreeSelect for deptOptions */
|
||||
.user-page-container .ant-select-tree {
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import type { ColumnsType } from 'antd/es/table';
|
|||
import { ReloadOutlined, SearchOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { delTaskModule, getTaskModel } from '@/api/appraisal';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { listUser } from '@/api/system/user';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import './appraisal-dashboard.css';
|
||||
|
|
@ -128,6 +130,7 @@ const getUserDisplayName = (user?: UserRow | null) =>
|
|||
|
||||
const AppraisalDashboardPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [queryForm] = Form.useForm<QueryFormValues>();
|
||||
const [userQueryForm] = Form.useForm<{ userName?: string }>();
|
||||
|
||||
|
|
@ -151,6 +154,8 @@ const AppraisalDashboardPage = () => {
|
|||
const [userPageSize, setUserPageSize] = useState(10);
|
||||
const [userKeyword, setUserKeyword] = useState('');
|
||||
const [selectedCreator, setSelectedCreator] = useState<UserRow | null>(null);
|
||||
const canRemoveBoard = canAccessPath('/workAppraisal/taskModule');
|
||||
const canViewBoardDetail = canAccessPath('/workAppraisal/moduleDetail');
|
||||
|
||||
const selectedCreatorRowKeys = useMemo(
|
||||
() => (selectedCreator ? [selectedCreator._rowKey] : []),
|
||||
|
|
@ -277,6 +282,9 @@ const AppraisalDashboardPage = () => {
|
|||
};
|
||||
|
||||
const handleDeleteBoard = async (row: BoardRow) => {
|
||||
if (!canRemoveBoard) {
|
||||
return;
|
||||
}
|
||||
const id = row.id;
|
||||
if (id === undefined || id === null) {
|
||||
message.warning('看板ID缺失,无法删除');
|
||||
|
|
@ -294,6 +302,9 @@ const AppraisalDashboardPage = () => {
|
|||
};
|
||||
|
||||
const handleViewDetail = (row: BoardRow) => {
|
||||
if (!canViewBoardDetail) {
|
||||
return;
|
||||
}
|
||||
const id = row.id;
|
||||
if (id === undefined || id === null) {
|
||||
message.warning('看板ID缺失,无法查看详情');
|
||||
|
|
@ -347,19 +358,23 @@ const AppraisalDashboardPage = () => {
|
|||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Space size={4} className="dashboard-ops">
|
||||
<Button type="link" onClick={() => handleViewDetail(record)}>
|
||||
看板详情
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="是否确认删除该条看板"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteBoard(record)}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
{canViewBoardDetail && (
|
||||
<Button type="link" onClick={() => handleViewDetail(record)}>
|
||||
看板详情
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Permission permissions={canRemoveBoard ? undefined : '__forbidden__'}>
|
||||
<Popconfirm
|
||||
title="是否确认删除该条看板"
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteBoard(record)}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { UserOutlined } from '@ant-design/icons';
|
|||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { getTaskScoreDetail, saveTaskUserScore } from '@/api/appraisal';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './appraisal-detail.css';
|
||||
|
||||
interface DetailItem {
|
||||
|
|
@ -178,6 +180,7 @@ const ScoreBar = ({ value, editable, onChange }: ScoreBarProps) => {
|
|||
const AppraisalDetailPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { canAccessPath } = usePermission();
|
||||
|
||||
const isNormal = toBoolean(searchParams.get('isNormal'));
|
||||
const [isEdit, setIsEdit] = useState(toBoolean(searchParams.get('edit')));
|
||||
|
|
@ -197,6 +200,8 @@ const AppraisalDetailPage = () => {
|
|||
const [remarkModalOpen, setRemarkModalOpen] = useState(false);
|
||||
const [editingCell, setEditingCell] = useState<{ groupIndex: number; rowIndex: number } | null>(null);
|
||||
const [remarkDraft, setRemarkDraft] = useState('');
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
const effectiveIsEdit = isEdit && canEditScore;
|
||||
|
||||
const templateType = useMemo(() => String(examineTask.templateType ?? ''), [examineTask.templateType]);
|
||||
const isTemplateZero = templateType === '0';
|
||||
|
|
@ -447,6 +452,9 @@ const AppraisalDetailPage = () => {
|
|||
};
|
||||
|
||||
const submitScore = async (submitStatus: 0 | 1) => {
|
||||
if (!effectiveIsEdit) {
|
||||
return;
|
||||
}
|
||||
if (!validateSubmit(submitStatus)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -509,7 +517,7 @@ const AppraisalDetailPage = () => {
|
|||
render: (_, row, rowIndex) => (
|
||||
<ScoreBar
|
||||
value={toNumber(row.score, 0)}
|
||||
editable={isEdit}
|
||||
editable={effectiveIsEdit}
|
||||
onChange={(nextScore) => updateItemScore(groupIndex, rowIndex, nextScore)}
|
||||
/>
|
||||
),
|
||||
|
|
@ -522,9 +530,15 @@ const AppraisalDetailPage = () => {
|
|||
dataIndex: 'remark',
|
||||
width: 140,
|
||||
render: (_, row, rowIndex) => (
|
||||
<Button type="link" onClick={() => openRemarkModal(groupIndex, rowIndex)}>
|
||||
{String(row.remark ?? '').trim() ? '查看' : '暂未评价'}
|
||||
</Button>
|
||||
effectiveIsEdit ? (
|
||||
<Button type="link" onClick={() => openRemarkModal(groupIndex, rowIndex)}>
|
||||
{String(row.remark ?? '').trim() ? '查看' : '暂未评价'}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="permission-link-disabled">
|
||||
{String(row.remark ?? '').trim() ? '查看' : '暂未评价'}
|
||||
</span>
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
@ -548,7 +562,7 @@ const AppraisalDetailPage = () => {
|
|||
<span className="block" />
|
||||
<span>{examineTask.taskName ?? '绩效考核详情'}</span>
|
||||
</div>
|
||||
{isEdit && (
|
||||
{effectiveIsEdit && (
|
||||
<Space size={20}>
|
||||
<Button style={{ width: 90 }} onClick={() => submitScore(0)} loading={submitting}>
|
||||
保存
|
||||
|
|
@ -597,7 +611,7 @@ const AppraisalDetailPage = () => {
|
|||
showCount
|
||||
value={group.remarkCate}
|
||||
onChange={(event) => updateGroupRemark(groupIndex, event.target.value)}
|
||||
readOnly={!isEdit}
|
||||
readOnly={!effectiveIsEdit}
|
||||
placeholder="0/300"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -615,7 +629,7 @@ const AppraisalDetailPage = () => {
|
|||
showCount
|
||||
value={judgeContent}
|
||||
onChange={(event) => setJudgeContent(event.target.value)}
|
||||
readOnly={!isEdit}
|
||||
readOnly={!effectiveIsEdit}
|
||||
placeholder="0/300"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -627,11 +641,11 @@ const AppraisalDetailPage = () => {
|
|||
<Modal
|
||||
title="自评总结"
|
||||
open={remarkModalOpen}
|
||||
onOk={isEdit ? saveRemarkModal : () => setRemarkModalOpen(false)}
|
||||
onOk={effectiveIsEdit ? saveRemarkModal : () => setRemarkModalOpen(false)}
|
||||
onCancel={() => setRemarkModalOpen(false)}
|
||||
okButtonProps={{ style: { display: isEdit ? 'inline-flex' : 'none' } }}
|
||||
okButtonProps={{ style: { display: effectiveIsEdit ? 'inline-flex' : 'none' } }}
|
||||
okText="确定"
|
||||
cancelText={isEdit ? '取消' : '关闭'}
|
||||
cancelText={effectiveIsEdit ? '取消' : '关闭'}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 4 }}
|
||||
|
|
@ -639,7 +653,7 @@ const AppraisalDetailPage = () => {
|
|||
showCount
|
||||
value={remarkDraft}
|
||||
onChange={(event) => setRemarkDraft(event.target.value)}
|
||||
readOnly={!isEdit}
|
||||
readOnly={!effectiveIsEdit}
|
||||
placeholder="0/200"
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,22 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||
import { Card, Row, Col, message, Tabs, Tag, Empty } from 'antd';
|
||||
import { getTaskListSelf } from '../../api/appraisal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './appraisal.css'; // Assuming a shared CSS for appraisal pages
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const AppraisalManagerPage: React.FC = () => {
|
||||
const { canAccessPath } = usePermission();
|
||||
const [activeTab, setActiveTab] = useState('进行中');
|
||||
const [taskList, setTaskList] = useState<{ [key: string]: any[] }>({});
|
||||
const navigate = useNavigate();
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
|
||||
const getTasks = useCallback(async () => {
|
||||
try {
|
||||
const response = await getTaskListSelf();
|
||||
const response = await getTaskListSelf() as { data?: { [key: string]: any[] } };
|
||||
setTaskList(response.data || {});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task list:', error);
|
||||
|
|
@ -26,6 +30,9 @@ const AppraisalManagerPage: React.FC = () => {
|
|||
}, [getTasks]);
|
||||
|
||||
const viewDetails = (task: any, isEdit: number) => {
|
||||
if (isEdit === 1 && !canEditScore) {
|
||||
return;
|
||||
}
|
||||
if (!task.taskEditFlag) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
|
|
@ -50,10 +57,17 @@ const AppraisalManagerPage: React.FC = () => {
|
|||
<div className="task-deadline">截止时间:{task.endTime.split(" ")[0]}</div>
|
||||
<div className="card-footer">
|
||||
<div className="people-count">考核人数:{task.peopleNumber}</div>
|
||||
<div className={`action-button ${!task.taskEditFlag ? 'disabled' : ''}`} onClick={() => viewDetails(task, isEdit)}>
|
||||
{isEdit ? '考核评分' : '查看详情'}
|
||||
<img src="/src/assets/task/right.png" alt=">" style={{ width: 16, height: 16, marginLeft: 8 }} />
|
||||
</div>
|
||||
{task.taskEditFlag && (isEdit === 0 || canEditScore) ? (
|
||||
<div className="action-button" onClick={() => viewDetails(task, isEdit)}>
|
||||
{isEdit ? '考核评分' : '查看详情'}
|
||||
<img src="/src/assets/task/right.png" alt=">" style={{ width: 16, height: 16, marginLeft: 8 }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-button disabled permission-link-disabled">
|
||||
{isEdit ? '考核评分' : '查看详情'}
|
||||
<img src="/src/assets/task/right.png" alt=">" style={{ width: 16, height: 16, marginLeft: 8 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import type { TableColumnsType } from 'antd';
|
|||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getTaskUserList } from '../../api/appraisal';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
const AppraisalManagerUserPage: React.FC = () => {
|
||||
const [queryForm] = Form.useForm();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userList, setUserList] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
|
@ -16,6 +20,7 @@ const AppraisalManagerUserPage: React.FC = () => {
|
|||
const queryParamsFromUrl = new URLSearchParams(location.search);
|
||||
const taskId = queryParamsFromUrl.get('taskId');
|
||||
const isEdit = queryParamsFromUrl.get('isEdit');
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
pageNum: 1,
|
||||
|
|
@ -36,9 +41,9 @@ const AppraisalManagerUserPage: React.FC = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getTaskUserList(queryParams);
|
||||
setUserList(response.rows);
|
||||
setTotal(response.total);
|
||||
const response = await getTaskUserList(queryParams) as { rows?: any[]; total?: number };
|
||||
setUserList(response.rows ?? []);
|
||||
setTotal(response.total ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task user list:', error);
|
||||
message.error('获取考核用户列表失败');
|
||||
|
|
@ -68,10 +73,13 @@ const AppraisalManagerUserPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleEdit = (row: any, edit: number) => {
|
||||
if (edit === 1 && !canEditScore) {
|
||||
return;
|
||||
}
|
||||
navigate(`/workAppraisal/detail?examineTaskId=${taskId}&examineId=${row.id}&reviewType=0&edit=${edit}`);
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
const handleTableChange = (_pagination: any, _filters: any, sorter: any) => {
|
||||
if (sorter.field === 'manageScore') {
|
||||
setQueryParams(prev => ({
|
||||
...prev,
|
||||
|
|
@ -92,7 +100,12 @@ const AppraisalManagerUserPage: React.FC = () => {
|
|||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
{isEdit === '1' && record.examineStatus === '0' ? (
|
||||
<Button type="link" onClick={() => handleEdit(record, 1)}>去评分</Button>
|
||||
<Permission
|
||||
permissions={canEditScore ? 'examine:manager:list' : '__forbidden__'}
|
||||
fallback={<ReadonlyAction>去评分</ReadonlyAction>}
|
||||
>
|
||||
<Button type="link" onClick={() => handleEdit(record, 1)}>去评分</Button>
|
||||
</Permission>
|
||||
) : null}
|
||||
{record.examineStatus === '1' || isEdit === '0' ? (
|
||||
<Button type="link" onClick={() => handleEdit(record, 0)}>查看详情</Button>
|
||||
|
|
|
|||
|
|
@ -279,10 +279,22 @@ const AppraisalModuleDetailPage = () => {
|
|||
|
||||
return (
|
||||
<div className="app-container appraisal-module-detail-page">
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="appraisal-module-back">
|
||||
<PageBackButton fallbackPath="/workAppraisal/taskModule" />
|
||||
</div>
|
||||
|
||||
<div className="appraisal-module-hero">
|
||||
<div>
|
||||
<div className="appraisal-module-kicker">PERFORMANCE MODULE</div>
|
||||
<div className="appraisal-module-title">绩效模块详情</div>
|
||||
<div className="appraisal-module-subtitle">查看指标分组、分类结构和权重配置,保持考核模块结构清晰一致。</div>
|
||||
</div>
|
||||
<div className="appraisal-module-summary">
|
||||
<span>模块类型</span>
|
||||
<strong>{TEMPLATE_TYPE_OPTIONS.find((item) => item.value === moduleType)?.label ?? '-'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="module-detail-header">
|
||||
<Form layout="inline">
|
||||
<Form.Item label="看板名称">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { CarryOutOutlined, RightOutlined } from '@ant-design/icons';
|
|||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getTaskListSelf } from '@/api/appraisal';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './manager.css';
|
||||
|
||||
type TaskStatusKey = '0' | '2';
|
||||
|
|
@ -97,9 +99,11 @@ const isTaskEditable = (value: unknown) => {
|
|||
|
||||
const ManagerPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [activeTab, setActiveTab] = useState<TaskStatusKey>('0');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [taskBuckets, setTaskBuckets] = useState<TaskBuckets>(EMPTY_BUCKETS);
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
|
||||
const fetchTaskList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -128,6 +132,9 @@ const ManagerPage = () => {
|
|||
);
|
||||
|
||||
const handleView = (task: TaskItem, status: TaskStatusKey) => {
|
||||
if (status === '0' && !canEditScore) {
|
||||
return;
|
||||
}
|
||||
if (!isTaskEditable(task.taskEditFlag)) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
|
|
@ -170,14 +177,21 @@ const ManagerPage = () => {
|
|||
<div className="appraisal-task-meta">截止时间:{formatEndDate(task.endTime)}</div>
|
||||
<div className="appraisal-task-foot">
|
||||
<span className="appraisal-task-people">考核人数:{task.peopleNumber ?? 0}</span>
|
||||
<Button
|
||||
type="link"
|
||||
className={`appraisal-task-action ${editable ? '' : 'is-disabled'}`}
|
||||
onClick={() => handleView(task, status)}
|
||||
>
|
||||
{tabMap[status].actionText}
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
{editable && (status === '2' || canEditScore) ? (
|
||||
<Button
|
||||
type="link"
|
||||
className="appraisal-task-action"
|
||||
onClick={() => handleView(task, status)}
|
||||
>
|
||||
{tabMap[status].actionText}
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
) : (
|
||||
<span className="appraisal-task-action is-disabled permission-link-disabled">
|
||||
{tabMap[status].actionText}
|
||||
<RightOutlined />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import type { TableColumnsType } from 'antd';
|
|||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { getTaskUserList } from '@/api/appraisal';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
interface UserScoreRow {
|
||||
_rowKey?: string;
|
||||
|
|
@ -73,6 +76,7 @@ const attachRowKey = (rows: UserScoreRow[], taskId: string, pageNum: number) =>
|
|||
const ManagerUserPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { canAccessPath } = usePermission();
|
||||
const taskId = searchParams.get('taskId');
|
||||
const isEdit = searchParams.get('isEdit') === '1';
|
||||
|
||||
|
|
@ -81,6 +85,7 @@ const ManagerUserPage = () => {
|
|||
const [total, setTotal] = useState(0);
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
|
||||
const loadList = useCallback(async () => {
|
||||
if (!taskId) {
|
||||
|
|
@ -107,6 +112,9 @@ const ManagerUserPage = () => {
|
|||
}, [loadList]);
|
||||
|
||||
const navigateToDetail = (record: UserScoreRow, edit: 0 | 1) => {
|
||||
if (edit === 1 && !canEditScore) {
|
||||
return;
|
||||
}
|
||||
if (!taskId) {
|
||||
message.warning('缺少 taskId 参数,无法打开详情');
|
||||
return;
|
||||
|
|
@ -154,9 +162,14 @@ const ManagerUserPage = () => {
|
|||
render: (_, record) => (
|
||||
<Space size={4}>
|
||||
{isEdit && String(record.examineStatus ?? '') === '0' && (
|
||||
<Button type="link" onClick={() => navigateToDetail(record, 1)}>
|
||||
去评分
|
||||
</Button>
|
||||
<Permission
|
||||
permissions={canEditScore ? 'examine:manager:list' : '__forbidden__'}
|
||||
fallback={<ReadonlyAction>去评分</ReadonlyAction>}
|
||||
>
|
||||
<Button type="link" onClick={() => navigateToDetail(record, 1)}>
|
||||
去评分
|
||||
</Button>
|
||||
</Permission>
|
||||
)}
|
||||
{(String(record.examineStatus ?? '') === '1' || !isEdit) && (
|
||||
<Button type="link" onClick={() => navigateToDetail(record, 0)}>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { CarryOutOutlined, RightOutlined } from '@ant-design/icons';
|
|||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getTaskListSelfNormal } from '@/api/appraisal';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import './manager.css';
|
||||
|
||||
type TaskStatusKey = '0' | '2';
|
||||
|
|
@ -97,9 +99,11 @@ const isSelfSubmitted = (value: unknown) => String(value ?? '') === '1';
|
|||
|
||||
const NormalWorkerPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [activeTab, setActiveTab] = useState<TaskStatusKey>('0');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [taskBuckets, setTaskBuckets] = useState<TaskBuckets>(EMPTY_BUCKETS);
|
||||
const canEditScore = canAccessPath('/workAppraisal/detail');
|
||||
|
||||
const fetchTaskList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -128,6 +132,9 @@ const NormalWorkerPage = () => {
|
|||
);
|
||||
|
||||
const goDetail = (task: TaskItem, edit: '0' | '1') => {
|
||||
if (edit === '1' && !canEditScore) {
|
||||
return;
|
||||
}
|
||||
if (!isTaskEditable(task.taskEditFlag)) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
|
|
@ -151,29 +158,41 @@ const NormalWorkerPage = () => {
|
|||
const submitted = isSelfSubmitted(task.examineStatusSelf);
|
||||
const actionText = submitted ? '查看详情' : '考核评分';
|
||||
const editFlag: '0' | '1' = submitted ? '0' : '1';
|
||||
const canUseAction = isTaskEditable(task.taskEditFlag) && (submitted || canEditScore);
|
||||
|
||||
if (!canUseAction) {
|
||||
return (
|
||||
<span className="appraisal-task-action is-disabled permission-link-disabled">
|
||||
{actionText}
|
||||
<RightOutlined />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
className={`appraisal-task-action ${isTaskEditable(task.taskEditFlag) ? '' : 'is-disabled'}`}
|
||||
onClick={() => goDetail(task, editFlag)}
|
||||
>
|
||||
<Button type="link" className="appraisal-task-action" onClick={() => goDetail(task, editFlag)}>
|
||||
{actionText}
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderExpiredAction = (task: TaskItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
className={`appraisal-task-action ${isTaskEditable(task.taskEditFlag) ? '' : 'is-disabled'}`}
|
||||
onClick={() => goDetail(task, '0')}
|
||||
>
|
||||
查看详情
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
);
|
||||
const renderExpiredAction = (task: TaskItem) =>
|
||||
isTaskEditable(task.taskEditFlag) ? (
|
||||
<Button
|
||||
type="link"
|
||||
className="appraisal-task-action"
|
||||
onClick={() => goDetail(task, '0')}
|
||||
>
|
||||
查看详情
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
) : (
|
||||
<span className="appraisal-task-action is-disabled permission-link-disabled">
|
||||
查看详情
|
||||
<RightOutlined />
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderTaskList = (status: TaskStatusKey) => {
|
||||
const list = taskBuckets[status] ?? [];
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { addTask, delTask, getTaskList, getTaskSet, setTaskSet, updateTask } from '@/api/appraisal';
|
||||
import { deptTreeSelect, listUser } from '@/api/system/user';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import './taskSet.css';
|
||||
|
||||
|
|
@ -345,6 +348,7 @@ const WeightSlider = ({ value, onChange }: WeightSliderProps) => {
|
|||
};
|
||||
|
||||
const TaskSetPage = () => {
|
||||
const { canAccessPath } = usePermission();
|
||||
const [queryForm] = Form.useForm<QueryFormValues>();
|
||||
const [taskForm] = Form.useForm<TaskFormValues>();
|
||||
const [userQueryForm] = Form.useForm<{ userName?: string }>();
|
||||
|
|
@ -387,6 +391,10 @@ const TaskSetPage = () => {
|
|||
const [scoreList, setScoreList] = useState<ScoreGroup[]>([]);
|
||||
const [activeCollapseKey, setActiveCollapseKey] = useState('');
|
||||
const [selectedSubItemKey, setSelectedSubItemKey] = useState('');
|
||||
const canAddTask = canAccessPath('/workAppraisal/taskSet');
|
||||
const canEditTask = canAccessPath('/workAppraisal/taskSet');
|
||||
const canRemoveTask = canAccessPath('/workAppraisal/taskSet');
|
||||
const canConfigTask = canAccessPath('/workAppraisal/taskSet');
|
||||
|
||||
const disabledPastDate = useCallback((current: Dayjs) => current.endOf('day').isBefore(dayjs().startOf('day')), []);
|
||||
|
||||
|
|
@ -562,6 +570,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (!canAddTask) {
|
||||
return;
|
||||
}
|
||||
setTaskModalTitle('新增考核任务');
|
||||
setCurrentTask(null);
|
||||
setSelectedTaskUsers([]);
|
||||
|
|
@ -570,6 +581,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleEditTask = (row: TaskRow) => {
|
||||
if (!canEditTask) {
|
||||
return;
|
||||
}
|
||||
setTaskModalTitle('编辑考核任务');
|
||||
setCurrentTask(row);
|
||||
|
||||
|
|
@ -587,6 +601,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleDeleteTask = async (row: TaskRow) => {
|
||||
if (!canRemoveTask) {
|
||||
return;
|
||||
}
|
||||
if (row.id === undefined || row.id === null) {
|
||||
message.warning('任务ID缺失,无法删除');
|
||||
return;
|
||||
|
|
@ -603,6 +620,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleSaveTask = async () => {
|
||||
if ((currentTask && !canEditTask) || (!currentTask && !canAddTask)) {
|
||||
return;
|
||||
}
|
||||
let values: TaskFormValues;
|
||||
try {
|
||||
values = await taskForm.validateFields();
|
||||
|
|
@ -659,6 +679,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleConfigTask = async (row: TaskRow) => {
|
||||
if (!canConfigTask) {
|
||||
return;
|
||||
}
|
||||
if (row.id === undefined || row.id === null) {
|
||||
message.warning('任务ID缺失,无法配置指标');
|
||||
return;
|
||||
|
|
@ -737,6 +760,9 @@ const TaskSetPage = () => {
|
|||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!canConfigTask) {
|
||||
return;
|
||||
}
|
||||
if (totalWeight !== 100) {
|
||||
message.warning('累计权重值必须为100%,请调整后再试');
|
||||
return;
|
||||
|
|
@ -874,25 +900,31 @@ const TaskSetPage = () => {
|
|||
width: 250,
|
||||
render: (_, record) => (
|
||||
<Space size={4} className="task-set-ops">
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEditTask(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Permission permissions={canEditTask ? undefined : '__forbidden__'} fallback={<ReadonlyAction icon={<EditOutlined />}>编辑</ReadonlyAction>}>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEditTask(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
</Permission>
|
||||
{String(record.taskStatus ?? '') === '0' && (
|
||||
<Button type="link" icon={<SettingOutlined />} onClick={() => handleConfigTask(record)}>
|
||||
指标配置
|
||||
</Button>
|
||||
<Permission permissions={canConfigTask ? undefined : '__forbidden__'} fallback={<ReadonlyAction icon={<SettingOutlined />}>指标配置</ReadonlyAction>}>
|
||||
<Button type="link" icon={<SettingOutlined />} onClick={() => handleConfigTask(record)}>
|
||||
指标配置
|
||||
</Button>
|
||||
</Permission>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确认删除任务"
|
||||
description="删除任务及绩效数据,该操作不可逆,请谨慎操作"
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteTask(record)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Permission permissions={canRemoveTask ? undefined : '__forbidden__'} fallback={<ReadonlyAction icon={<DeleteOutlined />} danger>删除</ReadonlyAction>}>
|
||||
<Popconfirm
|
||||
title="确认删除任务"
|
||||
description="删除任务及绩效数据,该操作不可逆,请谨慎操作"
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
onConfirm={() => handleDeleteTask(record)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Permission>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
|
@ -921,9 +953,11 @@ const TaskSetPage = () => {
|
|||
</Form>
|
||||
|
||||
<div className="task-set-actions">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddTask}>
|
||||
新增任务
|
||||
</Button>
|
||||
<Permission permissions={canAddTask ? undefined : '__forbidden__'}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAddTask}>
|
||||
新增任务
|
||||
</Button>
|
||||
</Permission>
|
||||
</div>
|
||||
|
||||
<div className="task-set-table">
|
||||
|
|
@ -956,6 +990,7 @@ const TaskSetPage = () => {
|
|||
confirmLoading={taskSubmitting}
|
||||
destroyOnHidden
|
||||
forceRender
|
||||
okButtonProps={{ disabled: currentTask ? !canEditTask : !canAddTask }}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
width={460}
|
||||
|
|
@ -977,8 +1012,12 @@ const TaskSetPage = () => {
|
|||
readOnly
|
||||
placeholder="考核人员"
|
||||
className="task-set-user-input"
|
||||
onClick={openUserSelector}
|
||||
suffix={<UserOutlined onClick={openUserSelector} />}
|
||||
onClick={currentTask ? (canEditTask ? openUserSelector : undefined) : (canAddTask ? openUserSelector : undefined)}
|
||||
suffix={
|
||||
<UserOutlined
|
||||
onClick={currentTask ? (canEditTask ? openUserSelector : undefined) : (canAddTask ? openUserSelector : undefined)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -1090,6 +1129,7 @@ const TaskSetPage = () => {
|
|||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={configSaving}
|
||||
okButtonProps={{ disabled: !canConfigTask }}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="task-set-config-modal" aria-busy={configLoading}>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,24 @@
|
|||
.appraisal-dashboard-page {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 120px);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-search {
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-search .ant-form-item {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-search .ant-input,
|
||||
.appraisal-dashboard-page .dashboard-search .ant-select {
|
||||
width: 300px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-search .dashboard-user-picker {
|
||||
|
|
@ -34,16 +27,19 @@
|
|||
|
||||
.appraisal-dashboard-page .dashboard-search .ant-input-suffix .anticon {
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
color: #7d8cab;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-ops {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .dashboard-ops .ant-btn-link {
|
||||
padding: 0 6px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .ant-table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,100 +1,112 @@
|
|||
.appraisal-detail-page .conetentBox {
|
||||
padding: 40px 30px 30px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 560px;
|
||||
height: calc(100vh - 200px);
|
||||
padding: 28px 24px 24px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.08), transparent 24%),
|
||||
radial-gradient(circle at left top, rgba(99, 91, 255, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 249, 255, 0.98));
|
||||
box-shadow: 0 18px 42px rgba(22, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.appraisal-detail-page .titleBox {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22px;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .titleMain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .block {
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background-color: #4096ff;
|
||||
margin-right: 10px;
|
||||
width: 6px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, #635bff, #19b4b8);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .headerBox {
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 22px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.76);
|
||||
border-radius: 20px;
|
||||
background: rgba(247, 249, 255, 0.82);
|
||||
}
|
||||
|
||||
.appraisal-detail-page .userInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #333;
|
||||
gap: 10px;
|
||||
color: #34425f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .totalBox {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
color: #5f6f92;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .scoreTotal {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ff7d00;
|
||||
margin-left: 6px;
|
||||
margin-left: 8px;
|
||||
color: #4f46e5;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .tableBox {
|
||||
height: calc(100% - 110px);
|
||||
height: calc(100% - 104px);
|
||||
padding: 22px 0 0;
|
||||
overflow: auto;
|
||||
padding: 20px 0 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .tableRow {
|
||||
.appraisal-detail-page .tableRow,
|
||||
.appraisal-detail-page .detail-overall {
|
||||
margin-bottom: 18px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 10px 28px rgba(22, 32, 51, 0.05);
|
||||
}
|
||||
|
||||
.appraisal-detail-page .userBox,
|
||||
.appraisal-detail-page .detail-group-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
color: #162033;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-subtitle {
|
||||
margin-bottom: 8px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
color: #34425f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-group-remark,
|
||||
.appraisal-detail-page .detail-overall {
|
||||
margin-top: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-wrap {
|
||||
position: relative;
|
||||
width: 92%;
|
||||
min-width: 240px;
|
||||
max-width: 360px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-top {
|
||||
|
|
@ -102,17 +114,17 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
color: #8c8c8c;
|
||||
font-size: 24px;
|
||||
color: #8b97ac;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-track {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
overflow: visible;
|
||||
background: #ffd8a7;
|
||||
border-radius: 999px;
|
||||
background: #e6e8ff;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-track.is-editable {
|
||||
|
|
@ -128,11 +140,11 @@
|
|||
inset: 0 auto 0 0;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #f3a73f 0%, #f58f14 55%, #f57f00 100%);
|
||||
background: linear-gradient(90deg, #8a83ff 0%, #635bff 55%, #4f46e5 100%);
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-track::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
|
|
@ -150,27 +162,27 @@
|
|||
position: absolute;
|
||||
top: -30px;
|
||||
min-width: 28px;
|
||||
padding: 0 6px;
|
||||
height: 24px;
|
||||
border-radius: 5px;
|
||||
padding: 0 6px;
|
||||
transform: translateX(-50%);
|
||||
background-color: #f57f00;
|
||||
border-radius: 6px;
|
||||
background-color: #4f46e5;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-bubble::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -6px;
|
||||
margin-left: -5px;
|
||||
border-width: 6px 5px 0;
|
||||
border-style: solid;
|
||||
border-color: #f57f00 transparent transparent;
|
||||
border-color: #4f46e5 transparent transparent;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .detail-score-bubble.is-start {
|
||||
|
|
@ -182,8 +194,27 @@
|
|||
}
|
||||
|
||||
.appraisal-detail-page .statusText {
|
||||
color: #ff7d00;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
color: #4f46e5;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.appraisal-detail-page .conetentBox {
|
||||
height: auto;
|
||||
padding: 18px 16px 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .titleBox,
|
||||
.appraisal-detail-page .headerBox {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.appraisal-detail-page .tableBox {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,115 @@
|
|||
.appraisal-module-detail-page {
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 120px);
|
||||
box-sizing: border-box;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.appraisal-module-back {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.appraisal-module-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.84);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.12), transparent 26%),
|
||||
radial-gradient(circle at left center, rgba(99, 91, 255, 0.12), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.96));
|
||||
box-shadow: 0 18px 42px rgba(22, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.appraisal-module-kicker {
|
||||
margin-bottom: 10px;
|
||||
color: #6258f6;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.appraisal-module-title {
|
||||
color: #162033;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.appraisal-module-subtitle {
|
||||
margin-top: 8px;
|
||||
color: #6c7b98;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.appraisal-module-summary {
|
||||
min-width: 160px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.appraisal-module-summary span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #7d8cab;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-module-summary strong {
|
||||
color: #162033;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px 20px 4px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 14px 36px rgba(22, 32, 51, 0.05);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-layout {
|
||||
display: flex;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
height: calc(100vh - 250px);
|
||||
gap: 18px;
|
||||
min-height: 460px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-left,
|
||||
.appraisal-module-detail-page .module-detail-right {
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 14px 36px rgba(22, 32, 51, 0.05);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-left {
|
||||
width: 20%;
|
||||
border-right: 1px solid #d9d9d9;
|
||||
width: 22%;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-set-text,
|
||||
.appraisal-module-detail-page .module-set-title,
|
||||
.appraisal-module-detail-page .module-category-title,
|
||||
.appraisal-module-detail-page .module-header-item,
|
||||
.appraisal-module-detail-page .module-row-item.is-name {
|
||||
color: #162033;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-set-text {
|
||||
margin-bottom: 14px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-set-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-status-text {
|
||||
color: #ff5722;
|
||||
font-weight: 600;
|
||||
color: #4f46e5;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-collapse {
|
||||
|
|
@ -49,47 +118,52 @@
|
|||
}
|
||||
|
||||
.appraisal-module-detail-page .module-collapse .ant-collapse-item {
|
||||
border-bottom: none;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.76);
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
background: rgba(247, 249, 255, 0.7);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-collapse .ant-collapse-header {
|
||||
padding-left: 0 !important;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-collapse .ant-collapse-content > .ant-collapse-content-box {
|
||||
padding: 10px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-content-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-left-sub {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px 10px 10px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-left-sub:hover {
|
||||
background: #f5f8ff;
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-left-sub.is-selected {
|
||||
background-color: #4096ff;
|
||||
background: linear-gradient(135deg, #635bff, #4f46e5);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 24px rgba(99, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-left-sub.is-selected .module-status-text {
|
||||
|
|
@ -104,44 +178,40 @@
|
|||
}
|
||||
|
||||
.appraisal-module-detail-page .module-total-box {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-right {
|
||||
width: 80%;
|
||||
padding: 20px 40px;
|
||||
width: 78%;
|
||||
padding: 20px 26px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-category-box {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-category-title {
|
||||
margin: 10px;
|
||||
margin: 0;
|
||||
padding: 18px 18px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-set-header {
|
||||
background-color: #f8f8f9;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-header-item,
|
||||
.appraisal-module-detail-page .module-row-item {
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-header-item {
|
||||
font-weight: 600;
|
||||
padding: 14px 18px;
|
||||
background: #f7f9fe;
|
||||
border-top: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-bottom: 1px solid rgba(219, 228, 243, 0.78);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-header-item.is-name,
|
||||
|
|
@ -160,56 +230,55 @@
|
|||
}
|
||||
|
||||
.appraisal-module-detail-page .module-content-row {
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 96px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid rgba(233, 238, 247, 0.92);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-row-item.is-name {
|
||||
font-weight: 600;
|
||||
.appraisal-module-detail-page .module-content-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-row-item.is-remark {
|
||||
color: #4e5969;
|
||||
line-height: 1.6;
|
||||
padding-right: 16px;
|
||||
color: #5f6f92;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-scale {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #8c8c8c;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #8b97ac;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-track {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background-color: #ffd8a7;
|
||||
border-radius: 999px;
|
||||
background-color: #e6e8ff;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
inset: 0 auto 0 0;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #ffb144 0%, #ff7d00 100%);
|
||||
background: linear-gradient(90deg, #8a83ff 0%, #635bff 55%, #4f46e5 100%);
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-track::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
|
@ -226,27 +295,27 @@
|
|||
position: absolute;
|
||||
top: -30px;
|
||||
min-width: 28px;
|
||||
padding: 0 6px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
padding: 0 6px;
|
||||
transform: translateX(-50%);
|
||||
background-color: #ff7d00;
|
||||
border-radius: 6px;
|
||||
background-color: #4f46e5;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-text::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -6px;
|
||||
margin-left: -5px;
|
||||
border-width: 6px 5px 0;
|
||||
border-style: solid;
|
||||
border-color: #ff7d00 transparent transparent;
|
||||
border-color: #4f46e5 transparent transparent;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-score-text.is-start {
|
||||
|
|
@ -256,3 +325,20 @@
|
|||
.appraisal-module-detail-page .module-score-text.is-end {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.appraisal-module-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.appraisal-module-detail-page .module-detail-left,
|
||||
.appraisal-module-detail-page .module-detail-right {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,106 @@
|
|||
.appraisal-dashboard-page .assessment-container {
|
||||
max-height: 650px;
|
||||
overflow-y: auto;
|
||||
gap: 2%;
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
.appraisal-manager-page {
|
||||
gap: 18px;
|
||||
}
|
||||
.appraisal-dashboard-page .task-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.appraisal-manager-page .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.appraisal-dashboard-page .card-header {
|
||||
|
||||
.appraisal-manager-page .ant-tabs-tab {
|
||||
min-width: 104px;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .assessment-container,
|
||||
.appraisal-manager-page .assessment-container {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
padding: 18px 0 4px;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .task-card,
|
||||
.appraisal-manager-page .task-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 14px 36px rgba(22, 32, 51, 0.06);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(25, 180, 184, 0.08), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.98));
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .card-header,
|
||||
.appraisal-manager-page .card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
gap: 12px;
|
||||
padding: 24px 22px 18px;
|
||||
}
|
||||
.appraisal-dashboard-page .task-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
.appraisal-dashboard-page .card-header img,
|
||||
.appraisal-manager-page .card-header img {
|
||||
width: 40px !important;
|
||||
height: 42px !important;
|
||||
padding: 8px;
|
||||
border-radius: 14px;
|
||||
background: rgba(99, 91, 255, 0.1);
|
||||
}
|
||||
.appraisal-dashboard-page .task-deadline {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
|
||||
.appraisal-dashboard-page .task-name,
|
||||
.appraisal-manager-page .task-name {
|
||||
color: #162033;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.appraisal-dashboard-page .card-footer {
|
||||
|
||||
.appraisal-dashboard-page .task-deadline,
|
||||
.appraisal-manager-page .task-deadline {
|
||||
margin-bottom: 0;
|
||||
padding: 0 20px 18px;
|
||||
color: #7a89a8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .card-footer,
|
||||
.appraisal-manager-page .card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
background-color: #f7faff;
|
||||
min-height: 58px;
|
||||
padding: 0 20px;
|
||||
border-top: 1px solid rgba(219, 228, 243, 0.82);
|
||||
background: rgba(247, 249, 255, 0.8);
|
||||
}
|
||||
.appraisal-dashboard-page .people-count {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
.appraisal-dashboard-page .people-count,
|
||||
.appraisal-manager-page .people-count {
|
||||
color: #4d5b78;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.appraisal-dashboard-page .action-button {
|
||||
display: flex;
|
||||
|
||||
.appraisal-dashboard-page .action-button,
|
||||
.appraisal-manager-page .action-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #4096ff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
color: #4f46e5;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
.appraisal-dashboard-page .action-button.disabled {
|
||||
color: #999;
|
||||
|
||||
.appraisal-dashboard-page .action-button img,
|
||||
.appraisal-manager-page .action-button img {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.appraisal-dashboard-page .action-button.disabled,
|
||||
.appraisal-manager-page .action-button.disabled {
|
||||
color: #9aa6bc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
.task-set-page {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 120px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-search {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-search .ant-form-item {
|
||||
|
|
@ -19,23 +13,22 @@
|
|||
.task-set-page .task-set-search .ant-input,
|
||||
.task-set-page .task-set-search .ant-select,
|
||||
.task-set-page .task-set-search .ant-picker {
|
||||
width: 300px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-ops .ant-btn-link {
|
||||
padding: 0 6px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-set-page .task-set-user-input {
|
||||
|
|
@ -46,25 +39,27 @@
|
|||
display: flex;
|
||||
min-height: 430px;
|
||||
max-height: 520px;
|
||||
gap: 16px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.task-set-user-tree {
|
||||
width: 220px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
padding-right: 12px;
|
||||
width: 240px;
|
||||
padding: 16px 14px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 18px;
|
||||
background: rgba(247, 249, 255, 0.74);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.task-set-user-tree-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 10px;
|
||||
color: #162033;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-set-tree-loading {
|
||||
margin-bottom: 8px;
|
||||
color: #999;
|
||||
color: #7d8cab;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +69,7 @@
|
|||
}
|
||||
|
||||
.task-set-user-query {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-set-config-modal {
|
||||
|
|
@ -83,37 +78,46 @@
|
|||
|
||||
.task-set-config-layout {
|
||||
display: flex;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
gap: 0;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: rgba(250, 251, 255, 0.92);
|
||||
}
|
||||
|
||||
.task-set-config-left {
|
||||
width: 40%;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #d9d9d9;
|
||||
min-height: 450px;
|
||||
max-height: 450px;
|
||||
padding: 20px;
|
||||
border-right: 1px solid rgba(219, 228, 243, 0.82);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.task-set-weight-title,
|
||||
.task-set-weight-total,
|
||||
.task-set-config-head,
|
||||
.task-set-config-item-name {
|
||||
color: #162033;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-set-weight-title {
|
||||
margin-bottom: 14px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.task-set-group-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.task-set-weight-value {
|
||||
color: #ff7d00;
|
||||
font-weight: 600;
|
||||
color: #4f46e5;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-set-config-left .ant-collapse {
|
||||
|
|
@ -122,35 +126,40 @@
|
|||
}
|
||||
|
||||
.task-set-config-left .ant-collapse-item {
|
||||
border-bottom: none;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.74);
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.task-set-config-left .ant-collapse-header {
|
||||
padding-left: 0 !important;
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
|
||||
.task-set-config-left .ant-collapse-content > .ant-collapse-content-box {
|
||||
padding: 8px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-set-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.task-set-group-item:hover {
|
||||
background: #f5f8ff;
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.task-set-group-item.is-active {
|
||||
background: #4096ff;
|
||||
background: linear-gradient(135deg, #635bff, #4f46e5);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 24px rgba(99, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.task-set-group-item.is-active .task-set-weight-value {
|
||||
|
|
@ -168,7 +177,6 @@
|
|||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -177,18 +185,17 @@
|
|||
min-height: 450px;
|
||||
max-height: 450px;
|
||||
overflow: auto;
|
||||
padding: 20px 38px 20px 26px;
|
||||
padding: 22px 32px 20px 24px;
|
||||
}
|
||||
|
||||
.task-set-config-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-set-config-head span:first-child {
|
||||
.task-set-config-head span:first-child,
|
||||
.task-set-config-item-name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
|
|
@ -207,13 +214,10 @@
|
|||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.task-set-config-item-name {
|
||||
width: 50%;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
line-height: 1.5;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(219, 228, 243, 0.78);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
.task-set-config-item-slider {
|
||||
|
|
@ -229,11 +233,11 @@
|
|||
.task-weight-scale {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
padding: 0 2px;
|
||||
margin-bottom: 2px;
|
||||
color: #8b97ac;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider {
|
||||
|
|
@ -247,12 +251,12 @@
|
|||
}
|
||||
|
||||
.task-weight-slider .ant-slider-rail {
|
||||
background-color: #ffe0ba;
|
||||
background-color: #e7e8ff;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-track,
|
||||
.task-weight-slider .ant-slider:hover .ant-slider-track {
|
||||
background: linear-gradient(90deg, #ffb144 0%, #ff8f17 55%, #ff7d00 100%);
|
||||
background: linear-gradient(90deg, #8b83ff 0%, #635bff 55%, #4f46e5 100%);
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-step {
|
||||
|
|
@ -260,62 +264,23 @@
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-dot {
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
margin-left: -1px;
|
||||
margin-top: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: #fff;
|
||||
top: 0;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.task-set-user-select,
|
||||
.task-set-config-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-dot-active {
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
.task-set-user-tree,
|
||||
.task-set-user-table,
|
||||
.task-set-config-left,
|
||||
.task-set-config-right {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-handle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.task-weight-slider .ant-slider-handle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-weight-bubble {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
transform: translateX(-50%);
|
||||
min-width: 34px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
background: #ff7d00;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.task-weight-bubble::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -6px;
|
||||
margin-left: -5px;
|
||||
border-width: 6px 5px 0;
|
||||
border-style: solid;
|
||||
border-color: #ff7d00 transparent transparent;
|
||||
}
|
||||
|
||||
.task-weight-bubble.is-end {
|
||||
transform: translateX(-100%);
|
||||
.task-set-config-left,
|
||||
.task-set-config-right {
|
||||
min-height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
DatePicker,
|
||||
Input,
|
||||
message,
|
||||
|
|
@ -14,7 +13,7 @@ import {
|
|||
Table,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import type { CalendarProps, TableColumnsType, UploadProps } from 'antd';
|
||||
import type { TableColumnsType, UploadProps } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ArrowRightOutlined,
|
||||
|
|
@ -43,6 +42,7 @@ import { listProject } from '@/api/project';
|
|||
import { getUserProfile } from '@/api/user';
|
||||
import { TokenKey } from '@/utils/auth';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import './worklog.css';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -65,6 +65,7 @@ interface CalendarLogItem {
|
|||
|
||||
interface ProjectRow {
|
||||
projectId?: string | number;
|
||||
name?: string;
|
||||
projectName?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
|
|
@ -163,6 +164,38 @@ const normalizeFileList = (value: unknown): FileRecord[] =>
|
|||
}))
|
||||
: [];
|
||||
|
||||
const normalizeProjectRows = (value: unknown): ProjectRow[] =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
.filter((item) => isObject(item))
|
||||
.map((item) => ({
|
||||
...item,
|
||||
projectId: item.projectId as string | number | undefined,
|
||||
projectName: String(item.projectName ?? item.name ?? ''),
|
||||
name: String(item.name ?? item.projectName ?? ''),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const toNullableNumber = (value: unknown) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : value;
|
||||
};
|
||||
|
||||
const buildWorkLogPayload = (row: WorkLogRow): Record<string, unknown> => ({
|
||||
loggerId: toNullableNumber(row.loggerId),
|
||||
loggerDate: row.loggerDate,
|
||||
projectId: toNullableNumber(row.projectId),
|
||||
userId: toNullableNumber(row.userId),
|
||||
workTime: row.workTime === '' ? undefined : row.workTime,
|
||||
workContent: String(row.workContent ?? ''),
|
||||
state: row.state,
|
||||
demandId: toNullableNumber(row.demandId),
|
||||
fileList: normalizeFileList(row.fileList),
|
||||
});
|
||||
|
||||
const WorkLogPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryUserId = searchParams.get('userId') ?? '';
|
||||
|
|
@ -195,6 +228,7 @@ const WorkLogPage = () => {
|
|||
|
||||
const previousContentRef = useRef('');
|
||||
const seedRef = useRef(0);
|
||||
const { hasPermi } = usePermission();
|
||||
|
||||
const token = Cookies.get(TokenKey) ?? '';
|
||||
|
||||
|
|
@ -212,6 +246,9 @@ const WorkLogPage = () => {
|
|||
}, [currentUser.userId, queryUserId]);
|
||||
|
||||
const disableTable = isReadOnlyByQuery || dayReadonly;
|
||||
const canAddWorkLog = hasPermi('worklog/worklog:list');
|
||||
const canEditWorkLog = hasPermi('worklog/worklog:list');
|
||||
const canDeleteWorkLog = hasPermi('worklog/worklog:list');
|
||||
const showDetailBack = useMemo(() => Boolean(queryUserId || queryProjectId), [queryProjectId, queryUserId]);
|
||||
const displayUserName = queryNickName || currentUser.nickName || currentUser.userName || '当日日志';
|
||||
const detailFallbackPath = useMemo(() => {
|
||||
|
|
@ -327,7 +364,7 @@ const WorkLogPage = () => {
|
|||
const response = await userProject(viewUserId);
|
||||
const payload = normalizeResponseData(response);
|
||||
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
|
||||
setProjectList(rows as ProjectRow[]);
|
||||
setProjectList(normalizeProjectRows(rows));
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch user project list, fallback to full project list:', error);
|
||||
|
|
@ -337,7 +374,7 @@ const WorkLogPage = () => {
|
|||
const response = await listProject({ pageNum: 1, pageSize: 10000 });
|
||||
const payload = normalizeResponseData(response);
|
||||
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
|
||||
setProjectList(rows as ProjectRow[]);
|
||||
setProjectList(normalizeProjectRows(rows));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all projects:', error);
|
||||
message.error('获取项目列表失败');
|
||||
|
|
@ -379,7 +416,7 @@ const WorkLogPage = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
createBy: viewUserId,
|
||||
userId: viewUserId,
|
||||
loggerDate: selectedDay,
|
||||
};
|
||||
|
||||
|
|
@ -534,8 +571,43 @@ const WorkLogPage = () => {
|
|||
const hasOtherEditingRows = (rowKey: string) =>
|
||||
tableData.some((item) => item.edit && item._rowKey !== rowKey);
|
||||
|
||||
const canMutateRow = useCallback(
|
||||
(row: WorkLogRow) => {
|
||||
if (disableTable) {
|
||||
return false;
|
||||
}
|
||||
return row.loggerId ? canEditWorkLog : canAddWorkLog;
|
||||
},
|
||||
[canAddWorkLog, canEditWorkLog, disableTable],
|
||||
);
|
||||
|
||||
const canDeleteRow = useCallback(
|
||||
(row: WorkLogRow) => {
|
||||
if (disableTable) {
|
||||
return false;
|
||||
}
|
||||
if (row.loggerId) {
|
||||
return row.edit ? canEditWorkLog : canDeleteWorkLog;
|
||||
}
|
||||
return canAddWorkLog;
|
||||
},
|
||||
[canAddWorkLog, canDeleteWorkLog, canEditWorkLog, disableTable],
|
||||
);
|
||||
|
||||
const activeFileDialogRow = useMemo(() => {
|
||||
if (!fileDialogRowKey) {
|
||||
return null;
|
||||
}
|
||||
return tableData.find((item) => item._rowKey === fileDialogRowKey) ?? null;
|
||||
}, [fileDialogRowKey, tableData]);
|
||||
|
||||
const fileDialogEditable = useMemo(
|
||||
() => Boolean(activeFileDialogRow && canMutateRow(activeFileDialogRow)),
|
||||
[activeFileDialogRow, canMutateRow],
|
||||
);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (disableTable) {
|
||||
if (disableTable || !canAddWorkLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -634,7 +706,7 @@ const WorkLogPage = () => {
|
|||
};
|
||||
|
||||
const handleEdit = async (row: WorkLogRow) => {
|
||||
if (disableTable) {
|
||||
if (!canMutateRow(row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -655,7 +727,7 @@ const WorkLogPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = { ...row };
|
||||
const payload = buildWorkLogPayload(row);
|
||||
|
||||
try {
|
||||
if (row.loggerId !== undefined && row.loggerId !== null && row.loggerId !== '') {
|
||||
|
|
@ -679,7 +751,7 @@ const WorkLogPage = () => {
|
|||
};
|
||||
|
||||
const confirmDeletePersisted = async (row: WorkLogRow) => {
|
||||
if (row.loggerId === undefined || row.loggerId === null || row.loggerId === '') {
|
||||
if (!canDeleteWorkLog || row.loggerId === undefined || row.loggerId === null || row.loggerId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -695,7 +767,7 @@ const WorkLogPage = () => {
|
|||
};
|
||||
|
||||
const handleDelete = (row: WorkLogRow) => {
|
||||
if (disableTable) {
|
||||
if (!canDeleteRow(row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -785,6 +857,11 @@ const WorkLogPage = () => {
|
|||
};
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const row = getRowByKey(fileDialogRowKey);
|
||||
if (!row || !canMutateRow(row)) {
|
||||
message.warning('当前无权上传附件');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const limit = 100 * 1024 * 1024;
|
||||
if (file.size > limit) {
|
||||
message.warning('单个文件不能大于100M');
|
||||
|
|
@ -823,6 +900,11 @@ const WorkLogPage = () => {
|
|||
};
|
||||
|
||||
const removeFileInDialog = (file: FileRecord) => {
|
||||
const row = getRowByKey(fileDialogRowKey);
|
||||
if (!row || !canMutateRow(row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.id !== undefined && file.id !== null && file.id !== '') {
|
||||
setPendingDeletedFileIds((prev) => [...prev, file.id as string | number]);
|
||||
}
|
||||
|
|
@ -839,6 +921,11 @@ const WorkLogPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!canMutateRow(row)) {
|
||||
setFileDialogOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRow: WorkLogRow = {
|
||||
...row,
|
||||
fileList: fileDialogList,
|
||||
|
|
@ -849,7 +936,7 @@ const WorkLogPage = () => {
|
|||
|
||||
try {
|
||||
if (nextRow.loggerId !== undefined && nextRow.loggerId !== null && nextRow.loggerId !== '') {
|
||||
await editLog(nextRow);
|
||||
await editLog(buildWorkLogPayload(nextRow));
|
||||
}
|
||||
|
||||
if (pendingDeletedFileIds.length > 0) {
|
||||
|
|
@ -865,37 +952,30 @@ const WorkLogPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const calendarFullCellRender: CalendarProps<Dayjs>['fullCellRender'] = (current, info) => {
|
||||
if (info.type !== 'date') {
|
||||
return info.originNode;
|
||||
}
|
||||
const monthDayList = useMemo(() => {
|
||||
const start = selectedMonth.startOf('month');
|
||||
const end = selectedMonth.endOf('month');
|
||||
const totalDays = end.date();
|
||||
|
||||
const dayKey = formatDay(current);
|
||||
const record = monthCellMap.get(dayKey);
|
||||
return Array.from({ length: totalDays }, (_item, index) => {
|
||||
const current = start.add(index, 'day');
|
||||
const dayKey = formatDay(current);
|
||||
const record = monthCellMap.get(dayKey);
|
||||
const isFuture = current.isAfter(dayjs(), 'day');
|
||||
const hasLog = record !== undefined && toNumber(record.state, -1) !== -1;
|
||||
const isTimeout = record !== undefined && toNumber(record.state, -1) === -1 && current.isBefore(dayjs(), 'day');
|
||||
const isSelected = current.format('YYYY-MM-DD') === selectedDayOnly;
|
||||
|
||||
const isFuture = current.isAfter(dayjs(), 'day');
|
||||
const hasLog = record !== undefined && toNumber(record.state, -1) !== -1;
|
||||
const isTimeout = record !== undefined && toNumber(record.state, -1) === -1 && current.isBefore(dayjs(), 'day');
|
||||
const isSelected = current.format('YYYY-MM-DD') === selectedDayOnly;
|
||||
const isOutMonth = !current.isSame(selectedMonth, 'month');
|
||||
|
||||
const className = [
|
||||
'worklog-day-box',
|
||||
isFuture ? 'is-disabled' : '',
|
||||
hasLog ? 'is-has-log' : '',
|
||||
isTimeout ? 'is-timeout' : '',
|
||||
isSelected ? 'is-selected' : '',
|
||||
isOutMonth ? 'is-out-month' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<div className={className} onClick={() => handlePickDay(current)}>
|
||||
{current.date()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return {
|
||||
key: dayKey,
|
||||
current,
|
||||
isFuture,
|
||||
hasLog,
|
||||
isTimeout,
|
||||
isSelected,
|
||||
};
|
||||
});
|
||||
}, [monthCellMap, selectedDayOnly, selectedMonth]);
|
||||
|
||||
const columns: TableColumnsType<WorkLogRow> = [
|
||||
{
|
||||
|
|
@ -1062,23 +1142,26 @@ const WorkLogPage = () => {
|
|||
key: 'operation',
|
||||
width: 220,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
render: (_value, row) => {
|
||||
const rowCanMutate = canMutateRow(row);
|
||||
const rowCanDelete = canDeleteRow(row);
|
||||
const editLabel = row.loggerId && !row.edit ? '编辑' : '确认';
|
||||
const deleteLabel = row.loggerId && !row.edit ? '删除' : '取消';
|
||||
|
||||
return (
|
||||
<Space size={0}>
|
||||
<Space size={0} className="worklog-row-actions">
|
||||
<Button type="link" onClick={() => openFileDialog(row)}>
|
||||
附件详情
|
||||
</Button>
|
||||
|
||||
{!disableTable && (
|
||||
{rowCanMutate && (
|
||||
<Button type="link" onClick={() => void handleEdit(row)}>
|
||||
{editLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!disableTable && row.loggerId && !row.edit ? (
|
||||
{rowCanDelete && row.loggerId && !row.edit ? (
|
||||
<Popconfirm
|
||||
title="此操作将永久删除该日志,是否继续?"
|
||||
okText="确定"
|
||||
|
|
@ -1090,7 +1173,7 @@ const WorkLogPage = () => {
|
|||
</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
!disableTable && (
|
||||
rowCanDelete && (
|
||||
<Button type="link" onClick={() => handleDelete(row)}>
|
||||
{deleteLabel}
|
||||
</Button>
|
||||
|
|
@ -1144,15 +1227,33 @@ const WorkLogPage = () => {
|
|||
</div>
|
||||
|
||||
<Spin spinning={monthLoading}>
|
||||
<Calendar
|
||||
value={selectedMonth}
|
||||
fullscreen={false}
|
||||
fullCellRender={calendarFullCellRender}
|
||||
onSelect={(value) => handlePickDay(value)}
|
||||
onPanelChange={(value) => setSelectedMonth(value.startOf('month'))}
|
||||
headerRender={() => null}
|
||||
className="worklog-calendar"
|
||||
/>
|
||||
<div className="worklog-calendar-strip">
|
||||
{monthDayList.map((item) => {
|
||||
const className = [
|
||||
'worklog-strip-day',
|
||||
item.isSelected ? 'is-selected' : '',
|
||||
item.hasLog ? 'is-has-log' : '',
|
||||
item.isTimeout ? 'is-timeout' : '',
|
||||
item.isFuture ? 'is-disabled' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={() => handlePickDay(item.current)}
|
||||
disabled={item.isFuture}
|
||||
>
|
||||
<span className="worklog-strip-weekday">{item.current.format('dd')}</span>
|
||||
<strong className="worklog-strip-date">{item.current.format('DD')}</strong>
|
||||
<span className="worklog-strip-meta">{item.current.format('MM/DD')}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1199,7 +1300,7 @@ const WorkLogPage = () => {
|
|||
<div className="worklog-table-toolbar-subtitle">围绕当前日期快速记录项目、版本、需求与工时</div>
|
||||
</div>
|
||||
<div className="worklog-table-toolbar-actions">
|
||||
{!disableTable && (
|
||||
{!disableTable && canAddWorkLog && (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => void handleAdd()}>
|
||||
添加日志
|
||||
</Button>
|
||||
|
|
@ -1213,7 +1314,7 @@ const WorkLogPage = () => {
|
|||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1400 }}
|
||||
scroll={{ x: 1500 }}
|
||||
className="worklog-table"
|
||||
rowClassName={(record) => (record.edit ? 'is-editing' : '')}
|
||||
/>
|
||||
|
|
@ -1231,9 +1332,10 @@ const WorkLogPage = () => {
|
|||
}}
|
||||
width={780}
|
||||
destroyOnHidden
|
||||
okButtonProps={{ disabled: !fileDialogEditable }}
|
||||
>
|
||||
<Spin spinning={fileDialogLoading}>
|
||||
{!disableTable && (
|
||||
{fileDialogEditable && (
|
||||
<Upload
|
||||
action={FILE_UPLOAD_URL}
|
||||
headers={{ Authorization: `Bearer ${token}` }}
|
||||
|
|
@ -1293,7 +1395,7 @@ const WorkLogPage = () => {
|
|||
width: 90,
|
||||
align: 'center',
|
||||
render: (_value, row) =>
|
||||
!disableTable ? (
|
||||
fileDialogEditable ? (
|
||||
<Button type="link" danger onClick={() => removeFileInDialog(row)}>
|
||||
删除
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,65 @@
|
|||
.worklog-layout {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 190px);
|
||||
background:
|
||||
linear-gradient(rgba(52, 86, 142, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(52, 86, 142, 0.04) 1px, transparent 1px),
|
||||
radial-gradient(circle at top left, rgba(74, 141, 255, 0.08), transparent 24%),
|
||||
linear-gradient(180deg, #f5f8fd 0%, #edf3fb 100%);
|
||||
background-size: 24px 24px, 24px 24px, auto, auto;
|
||||
background: #ffffff;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.worklog-sidebar {
|
||||
flex: 0 0 336px;
|
||||
width: 336px;
|
||||
max-width: 336px;
|
||||
height: calc(100vh - 190px);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.worklog-main {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.worklog-sidebar-panel,
|
||||
.worklog-table-card {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 251, 255, 0.98) 100%);
|
||||
border: 1px solid rgba(205, 219, 240, 0.9);
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
border-radius: 22px;
|
||||
box-shadow:
|
||||
0 18px 42px rgba(23, 47, 89, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 14px 32px rgba(23, 47, 89, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.worklog-sidebar-panel::before,
|
||||
.worklog-table-card::before,
|
||||
.worklog-main-heading::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(77, 144, 255, 0.55), rgba(77, 144, 255, 0));
|
||||
display: none;
|
||||
}
|
||||
|
||||
.worklog-sidebar-panel {
|
||||
padding: 22px 22px 18px;
|
||||
padding: 12px 16px 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"header meta"
|
||||
"picker meta"
|
||||
"legend meta";
|
||||
column-gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.worklog-left-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
gap: 8px;
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.worklog-panel-kicker,
|
||||
|
|
@ -73,38 +69,40 @@
|
|||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: #7e95b7;
|
||||
color: #7a88a6;
|
||||
}
|
||||
|
||||
.worklog-panel-title {
|
||||
margin-top: 8px;
|
||||
font-size: 20px;
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #14213d;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.worklog-panel-subtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: #7c8ba1;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.worklog-month-picker {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 6px;
|
||||
grid-area: picker;
|
||||
}
|
||||
|
||||
.worklog-month-picker .ant-picker {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
border: 1px solid #d7e3f8;
|
||||
border-radius: 14px;
|
||||
height: 36px;
|
||||
border: 1px solid #dbe4f3;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.worklog-month-picker .ant-picker-input > input {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
color: #223554;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -112,163 +110,171 @@
|
|||
.worklog-left-arrows {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
color: #5c6f8f;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.worklog-left-arrows .anticon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: #f3f7ff;
|
||||
border: 1px solid #dce7fb;
|
||||
border-radius: 9px;
|
||||
background: #f6f8ff;
|
||||
border: 1px solid #dbe4f3;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.worklog-left-arrows .anticon:hover {
|
||||
color: #165dff;
|
||||
background: #e9f1ff;
|
||||
border-color: #b8d0ff;
|
||||
color: #4f46e5;
|
||||
background: rgba(99, 91, 255, 0.08);
|
||||
border-color: rgba(99, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.worklog-calendar-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
margin-bottom: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e7eef9;
|
||||
border-radius: 16px;
|
||||
gap: 6px 10px;
|
||||
margin-bottom: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #e9eef7;
|
||||
border-radius: 14px;
|
||||
background: rgba(244, 248, 255, 0.9);
|
||||
grid-area: legend;
|
||||
}
|
||||
|
||||
.worklog-legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: #6b7b92;
|
||||
}
|
||||
|
||||
.worklog-legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #d8e5fb;
|
||||
}
|
||||
|
||||
.worklog-legend-dot.is-selected {
|
||||
background: #1d63ff;
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.worklog-legend-dot.is-filled {
|
||||
background: #79aefe;
|
||||
background: #8c84ff;
|
||||
}
|
||||
|
||||
.worklog-legend-dot.is-timeout {
|
||||
background: #ffb74d;
|
||||
}
|
||||
|
||||
.worklog-calendar .ant-picker-calendar-header {
|
||||
display: none;
|
||||
.worklog-calendar-strip {
|
||||
grid-area: meta;
|
||||
border: 1px solid #edf2fb;
|
||||
border-radius: 18px;
|
||||
padding: 8px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(99, 91, 255, 0.24) transparent;
|
||||
}
|
||||
|
||||
.worklog-calendar {
|
||||
border-top: 1px solid #edf2fb;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.worklog-calendar .ant-picker-panel {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.worklog-calendar .ant-picker-content th {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #8b98ad;
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.worklog-calendar .ant-picker-cell {
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.worklog-calendar .ant-picker-cell-inner {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.worklog-day-box {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
margin: 0 auto;
|
||||
.worklog-strip-day {
|
||||
flex: 0 0 58px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
min-height: 62px;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
color: #24344d;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border: 1px solid #e6edf8;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
|
||||
}
|
||||
|
||||
.worklog-day-box:hover {
|
||||
.worklog-strip-day:hover:not(:disabled) {
|
||||
background: #edf4ff;
|
||||
border-color: #d6e5ff;
|
||||
}
|
||||
|
||||
.worklog-day-box.is-out-month {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.worklog-day-box.is-selected {
|
||||
background: linear-gradient(135deg, #2a74ff 0%, #0b56f0 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 20px rgba(42, 116, 255, 0.28);
|
||||
}
|
||||
|
||||
.worklog-day-box.is-has-log {
|
||||
background: linear-gradient(135deg, #8ebcff 0%, #69a3ff 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.worklog-day-box.is-timeout {
|
||||
background: linear-gradient(135deg, #ffd36c 0%, #ffb53d 100%);
|
||||
color: #7a4500;
|
||||
}
|
||||
|
||||
.worklog-day-box.is-disabled {
|
||||
.worklog-strip-day:disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
background: transparent;
|
||||
background: #f8fafc;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.worklog-strip-day.is-selected {
|
||||
background: linear-gradient(135deg, #635bff 0%, #4f46e5 100%);
|
||||
color: #fff;
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 10px 20px rgba(99, 91, 255, 0.22);
|
||||
}
|
||||
|
||||
.worklog-strip-day.is-has-log:not(.is-selected) {
|
||||
background: linear-gradient(135deg, rgba(163, 157, 255, 0.22) 0%, rgba(127, 119, 255, 0.14) 100%);
|
||||
border-color: rgba(127, 119, 255, 0.28);
|
||||
}
|
||||
|
||||
.worklog-strip-day.is-timeout:not(.is-selected) {
|
||||
background: linear-gradient(135deg, rgba(255, 211, 108, 0.34) 0%, rgba(255, 181, 61, 0.18) 100%);
|
||||
border-color: rgba(255, 181, 61, 0.35);
|
||||
color: #8b4f00;
|
||||
}
|
||||
|
||||
.worklog-strip-weekday {
|
||||
font-size: 11px;
|
||||
color: #8190a7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worklog-strip-date {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.worklog-strip-meta {
|
||||
font-size: 10px;
|
||||
color: #97a3b8;
|
||||
}
|
||||
|
||||
.worklog-strip-day.is-selected .worklog-strip-weekday,
|
||||
.worklog-strip-day.is-selected .worklog-strip-meta {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.worklog-main-top {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.worklog-main-heading {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding: 24px 26px;
|
||||
padding: 18px 22px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(205, 219, 240, 0.92);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(43, 118, 255, 0.08) 0%, rgba(43, 118, 255, 0.02) 48%, rgba(255, 255, 255, 0.97) 100%),
|
||||
rgba(255, 255, 255, 0.96);
|
||||
box-shadow:
|
||||
0 18px 42px rgba(23, 47, 89, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(219, 228, 243, 0.92);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 14px 32px rgba(23, 47, 89, 0.05);
|
||||
}
|
||||
|
||||
.worklog-back-wrap {
|
||||
|
|
@ -280,7 +286,7 @@
|
|||
}
|
||||
|
||||
.worklog-main-title {
|
||||
font-size: 28px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #18263d;
|
||||
|
|
@ -298,17 +304,17 @@
|
|||
.worklog-status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worklog-date-chip {
|
||||
color: #1d4f91;
|
||||
background: linear-gradient(180deg, #f3f8ff 0%, #ebf3ff 100%);
|
||||
border: 1px solid #d6e5ff;
|
||||
color: #4a5680;
|
||||
background: linear-gradient(180deg, #f7f8ff 0%, #f1f3ff 100%);
|
||||
border: 1px solid #dfe4ff;
|
||||
}
|
||||
|
||||
.worklog-status-chip.is-editable {
|
||||
|
|
@ -318,9 +324,9 @@
|
|||
}
|
||||
|
||||
.worklog-status-chip.is-readonly {
|
||||
color: #4d50c8;
|
||||
background: linear-gradient(180deg, #f4f1ff 0%, #eeebff 100%);
|
||||
border: 1px solid #e1d7ff;
|
||||
color: #4f46e5;
|
||||
background: linear-gradient(180deg, #f5f3ff 0%, #efedff 100%);
|
||||
border: 1px solid #e4deff;
|
||||
}
|
||||
|
||||
.worklog-status-chip.is-disabled {
|
||||
|
|
@ -330,14 +336,14 @@
|
|||
}
|
||||
|
||||
.worklog-summary-strip {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.worklog-summary-card {
|
||||
min-width: 132px;
|
||||
padding: 18px 18px 16px;
|
||||
min-width: 0;
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(210, 223, 243, 0.92);
|
||||
background:
|
||||
|
|
@ -349,8 +355,8 @@
|
|||
|
||||
.worklog-summary-card.is-primary {
|
||||
background:
|
||||
linear-gradient(135deg, #1d67ff 0%, #094cdc 100%);
|
||||
border-color: #0a54eb;
|
||||
linear-gradient(135deg, #635bff 0%, #4f46e5 100%);
|
||||
border-color: #4f46e5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
|
@ -363,7 +369,7 @@
|
|||
}
|
||||
|
||||
.worklog-summary-card strong {
|
||||
font-size: 26px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: #1e2c42;
|
||||
letter-spacing: -0.03em;
|
||||
|
|
@ -375,7 +381,7 @@
|
|||
}
|
||||
|
||||
.worklog-table-card {
|
||||
padding: 18px 18px 12px;
|
||||
padding: 18px 18px 14px;
|
||||
}
|
||||
|
||||
.worklog-table-toolbar {
|
||||
|
|
@ -383,7 +389,7 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.worklog-table-toolbar-kicker {
|
||||
|
|
@ -406,9 +412,33 @@
|
|||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #226cff 0%, #0b56f0 100%);
|
||||
border-color: #145cf2;
|
||||
box-shadow: 0 10px 18px rgba(34, 108, 255, 0.18);
|
||||
background: linear-gradient(135deg, #635bff 0%, #4f46e5 100%);
|
||||
border-color: #4f46e5;
|
||||
box-shadow: 0 10px 18px rgba(99, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.worklog-entry-guide {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e9eef7;
|
||||
border-radius: 16px;
|
||||
background: rgba(247, 249, 255, 0.82);
|
||||
}
|
||||
|
||||
.worklog-entry-step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
color: #5f6f92;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
border: 1px solid rgba(219, 228, 243, 0.82);
|
||||
}
|
||||
|
||||
.worklog-table .ant-table {
|
||||
|
|
@ -432,7 +462,7 @@
|
|||
|
||||
.worklog-table .ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #eef3fb;
|
||||
vertical-align: top;
|
||||
vertical-align: middle;
|
||||
color: #203149;
|
||||
}
|
||||
|
||||
|
|
@ -450,12 +480,32 @@
|
|||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.worklog-table .ant-select-selector,
|
||||
.worklog-table .ant-input-affix-wrapper,
|
||||
.worklog-table .ant-input,
|
||||
.worklog-table .ant-input-number,
|
||||
.worklog-table .ant-input-number-affix-wrapper {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.worklog-table .ant-btn-link {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.worklog-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.worklog-row-actions .ant-btn {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.worklog-content-text {
|
||||
min-height: 38px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
|
|
@ -469,8 +519,8 @@
|
|||
}
|
||||
|
||||
.worklog-content-text:hover {
|
||||
background: #f7faff;
|
||||
border-color: #d6e5ff;
|
||||
background: #f7f7ff;
|
||||
border-color: #ddd8ff;
|
||||
}
|
||||
|
||||
.worklog-content-text.is-empty {
|
||||
|
|
@ -478,21 +528,19 @@
|
|||
}
|
||||
|
||||
.worklog-content-popover {
|
||||
width: 420px;
|
||||
width: min(520px, 70vw);
|
||||
}
|
||||
|
||||
.worklog-content-popover .ant-input {
|
||||
border: 1px solid #dce6f5;
|
||||
border: 1px solid #dbe4f3;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.worklog-content-actions {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.worklog-content-actions .ant-btn + .ant-btn {
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.worklog-file-tip {
|
||||
|
|
@ -506,12 +554,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.worklog-layout {
|
||||
flex-direction: column;
|
||||
min-height: auto;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.worklog-sidebar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
|
@ -519,12 +561,22 @@
|
|||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.worklog-sidebar-panel {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"picker"
|
||||
"legend"
|
||||
"meta";
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
.worklog-main-top {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.worklog-summary-strip {
|
||||
flex-wrap: wrap;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -535,6 +587,16 @@
|
|||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.worklog-calendar-strip {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.worklog-strip-day {
|
||||
flex-basis: 56px;
|
||||
min-height: 58px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.worklog-main-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
|
@ -556,4 +618,13 @@
|
|||
.worklog-table-toolbar-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.worklog-entry-guide {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.worklog-entry-step {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const routeMapper = {
|
|||
'/projectBank/userProject': UserProjectPage,
|
||||
'/projectBank/userScore': UserScorePage,
|
||||
'/projectBank/userScoreDetail': UserScoreDetailPage,
|
||||
'/user/profile': ProfilePage,
|
||||
'/profile': ProfilePage,
|
||||
'/monitor/cache': CacheMonitorPage,
|
||||
'/monitor/job': JobMonitorPage,
|
||||
|
|
@ -119,6 +120,11 @@ export const routes = [
|
|||
component: UserScoreDetailPage,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/user/profile',
|
||||
component: ProfilePage,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: ProfilePage,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
.permission-link-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
color: #98a2b3;
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.permission-link-disabled.is-danger {
|
||||
color: #c0a2a8;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import * as echarts from 'echarts/core';
|
||||
import { PieChart, GaugeChart } from 'echarts/charts';
|
||||
import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
echarts.use([
|
||||
PieChart,
|
||||
GaugeChart,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
export { echarts };
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import type { MessageInstance } from 'antd/es/message/interface';
|
||||
|
||||
let messageApi: MessageInstance | null = null;
|
||||
|
||||
export const bindMessageApi = (api: MessageInstance) => {
|
||||
messageApi = api;
|
||||
};
|
||||
|
||||
const logFallback = (level: 'error' | 'warn' | 'info' | 'log', content: string) => {
|
||||
const logger = console[level] ?? console.log;
|
||||
logger(content);
|
||||
};
|
||||
|
||||
export const notify = {
|
||||
success(content: string) {
|
||||
if (messageApi) {
|
||||
messageApi.success(content);
|
||||
return;
|
||||
}
|
||||
logFallback('log', content);
|
||||
},
|
||||
error(content: string) {
|
||||
if (messageApi) {
|
||||
messageApi.error(content);
|
||||
return;
|
||||
}
|
||||
logFallback('error', content);
|
||||
},
|
||||
warning(content: string) {
|
||||
if (messageApi) {
|
||||
messageApi.warning(content);
|
||||
return;
|
||||
}
|
||||
logFallback('warn', content);
|
||||
},
|
||||
info(content: string) {
|
||||
if (messageApi) {
|
||||
messageApi.info(content);
|
||||
return;
|
||||
}
|
||||
logFallback('info', content);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import axios, { AxiosHeaders, type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import { message } from 'antd';
|
||||
import { TokenKey } from './auth';
|
||||
import type { ApiEnvelope } from '@/types/api';
|
||||
import { notify } from './notify';
|
||||
|
||||
type RequestConfig<D = unknown> = AxiosRequestConfig<D> & {
|
||||
headers?: Record<string, unknown>;
|
||||
|
|
@ -51,7 +51,7 @@ service.interceptors.response.use(
|
|||
} else if (errorMessage.includes('Request failed with status code')) {
|
||||
errorMessage = `系统接口${errorMessage.slice(-3)}异常`;
|
||||
}
|
||||
message.error(errorMessage);
|
||||
notify.error(errorMessage);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
|
@ -61,7 +61,7 @@ const unwrapResponse = <T>(raw: unknown): T => {
|
|||
const res = raw as ApiEnvelope<T>;
|
||||
if (res.code !== 200) {
|
||||
const errorMessage = res.msg ?? 'Error';
|
||||
message.error(errorMessage);
|
||||
notify.error(errorMessage);
|
||||
|
||||
if (res.code === 401 || res.code === 50012 || res.code === 50014) {
|
||||
Cookies.remove(TokenKey);
|
||||
|
|
@ -69,6 +69,18 @@ const unwrapResponse = <T>(raw: unknown): T => {
|
|||
}
|
||||
throw new Error(errorMessage);
|
||||
} else {
|
||||
if (
|
||||
res.data !== undefined &&
|
||||
typeof res.data === 'object' &&
|
||||
res.data !== null &&
|
||||
('roleGroup' in (res as Record<string, unknown>) || 'postGroup' in (res as Record<string, unknown>))
|
||||
) {
|
||||
return {
|
||||
user: res.data,
|
||||
roleGroup: (res as Record<string, unknown>).roleGroup,
|
||||
postGroup: (res as Record<string, unknown>).postGroup,
|
||||
} as T;
|
||||
}
|
||||
// Some APIs return business fields on the root object (e.g. captcha img/uuid),
|
||||
// so when `data` is absent we must return the envelope itself.
|
||||
return (res.data !== undefined ? res.data : res) as T;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,43 @@ import { defineConfig } from 'vite'
|
|||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path' // Import path module
|
||||
|
||||
const inferChunkName = (moduleIds: string[]) => {
|
||||
const joined = moduleIds.join('\n')
|
||||
|
||||
if (joined.includes('/node_modules/antd/es/table') || joined.includes('/node_modules/rc-table/')) {
|
||||
return 'antd-table'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/input') || joined.includes('/node_modules/rc-input/')) {
|
||||
return 'antd-input'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/button') || joined.includes('/node_modules/rc-button/')) {
|
||||
return 'antd-button'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/collapse') || joined.includes('/node_modules/rc-collapse/')) {
|
||||
return 'antd-collapse'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/skeleton')) {
|
||||
return 'antd-skeleton'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/grid') || joined.includes('/node_modules/antd/es/row')) {
|
||||
return 'antd-grid'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/locale') || joined.includes('/node_modules/antd/locale/')) {
|
||||
return 'antd-locale'
|
||||
}
|
||||
if (joined.includes('/node_modules/@ant-design/icons/')) {
|
||||
return 'antd-icons'
|
||||
}
|
||||
if (joined.includes('/src/components/')) {
|
||||
return 'app-components'
|
||||
}
|
||||
if (joined.includes('/src/utils/')) {
|
||||
return 'app-utils'
|
||||
}
|
||||
|
||||
return 'shared'
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
// base: '/pms-react-new/', // Set base URL if deploying to a sub-path
|
||||
|
|
@ -11,6 +48,35 @@ export default defineConfig({
|
|||
'@': path.resolve(__dirname, './src'), // Alias @ to ./src
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
chunkFileNames(chunkInfo) {
|
||||
const moduleIds = chunkInfo.moduleIds ?? []
|
||||
const chunkName = chunkInfo.name === 'index' ? inferChunkName(moduleIds) : chunkInfo.name
|
||||
return `assets/${chunkName}-[hash].js`
|
||||
},
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) {
|
||||
return undefined;
|
||||
}
|
||||
if (/node_modules\/(react|react-dom|scheduler)\//.test(id)) {
|
||||
return 'react-vendor';
|
||||
}
|
||||
if (/node_modules\/(echarts|echarts-for-react)\//.test(id)) {
|
||||
return 'echarts-vendor';
|
||||
}
|
||||
if (/node_modules\/(react-router|react-router-dom)\//.test(id)) {
|
||||
return 'router-vendor';
|
||||
}
|
||||
if (/node_modules\/dayjs\//.test(id)) {
|
||||
return 'dayjs-vendor';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
|
|
|||
Loading…
Reference in New Issue