diff --git a/public/vite.svg b/public/vite.svg index ee9fada..e7b8dfb 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/scripts/sql/20260316_missing_page_menu_check.sql b/scripts/sql/20260316_missing_page_menu_check.sql new file mode 100644 index 0000000..30d7b53 --- /dev/null +++ b/scripts/sql/20260316_missing_page_menu_check.sql @@ -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; diff --git a/scripts/sql/20260316_permission_button_reconcile.sql b/scripts/sql/20260316_permission_button_reconcile.sql new file mode 100644 index 0000000..9cc066d --- /dev/null +++ b/scripts/sql/20260316_permission_button_reconcile.sql @@ -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; diff --git a/scripts/sql/20260316_worklog_menu_init.sql b/scripts/sql/20260316_worklog_menu_init.sql new file mode 100644 index 0000000..3e9c809 --- /dev/null +++ b/scripts/sql/20260316_worklog_menu_init.sql @@ -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; diff --git a/scripts/sql/20260317_page_menu_init.sql b/scripts/sql/20260317_page_menu_init.sql new file mode 100644 index 0000000..7a9377c --- /dev/null +++ b/scripts/sql/20260317_page_menu_init.sql @@ -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; diff --git a/src/App.css b/src/App.css index 4a335ae..8e37785 100644 --- a/src/App.css +++ b/src/App.css @@ -1 +1 @@ -/* Remove default styles */ +/* Remove default styles */ \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 791ac7f..5e11781 100644 --- a/src/App.tsx +++ b/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 = () => ( +
+ +
+); + const PrivateRoute = () => { const location = useLocation(); const token = getToken(); - const { ready, loading, canAccessPath } = usePermission(); + const { ready, loading, canAccessPath, defaultRoutePath } = usePermission(); if (!token) { return ; @@ -53,8 +61,26 @@ const PrivateRoute = () => { ); } + if (location.pathname === '/') { + if (defaultRoutePath && defaultRoutePath !== '/') { + return ; + } + return ( +
+ 暂无可访问页面 +
+ ); + } + if (!canAccessPath(location.pathname)) { - return ; + if (defaultRoutePath && defaultRoutePath !== location.pathname) { + return ; + } + return ( +
+ 暂无权限访问当前页面 +
+ ); } return ; @@ -64,48 +90,53 @@ const PrivateRoute = () => { function App() { return ( - - } /> - }> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + }> + + } /> + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + - - + + ); } diff --git a/src/api/user.ts b/src/api/user.ts index 6c6b23a..4942b92 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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, + }); +} diff --git a/src/assets/error/404.png b/src/assets/error/404.png index fb8199f..a98e772 100644 --- a/src/assets/error/404.png +++ b/src/assets/error/404.png @@ -1 +1 @@ -iVBORw0KGgoAAAANSUhEUgAAAioAAAHbCAYAAACw33E7AAAgAElEQVR4nOy9d5xcV3nvf7t+Vfd+rU8vvdIDCRAQCBAEBERExV0URVEREVFEFAFFxQXsuBfEioiCIgiCe+/tPb3eU++p6j33/f6xZz2z1j27u2d2d9/l+Xk+eM0zZ87MvrP2Xmvf2hEREBERAREQAREQAREQAREQgRkIjBkhgIiIiIiIiIiIiIiIiAgyhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQAREQgRCFCkBERAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkberEIAAAACBjQAEj/YC/9k= +iVBORw0KGgoAAAANSUhEUgAAAioAAAHbCAYAAACw33E7AAAgAElEQVR4nOy9d5xcV3nvf7t+Vfd+rU8vvdIDCRAQCBAEBERExV0URVEREVFEFAFFxQXsuBfEioiCIgiCe+/tPb3eU++p6j33/f6xZz2z1j27u2d2d9/l+Xk+eM0zZ87MvrP2Xmvf2hEREBERAREQAREQAREQAREQgRkIjBkhgIiIiIiIiIiIiIiIiAgyhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQAREQgRCFCkBERAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkBERAREQAREQAREQEQghUKIiIiIiIiIiIiIiIiIgAhChEQEREBERAREQAREQAREQEIQQqREQEREBERAREQAREQAREQgRCFCkberEIAAAACBjQAEj/YC/9k= \ No newline at end of file diff --git a/src/assets/login-background.jpg b/src/assets/login-background.jpg index 9e31d16..9b72ae3 100644 --- a/src/assets/login-background.jpg +++ b/src/assets/login-background.jpg @@ -1 +1 @@ -g/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEAAQBIAAAAAQABOEJJTQQGAAAAAAAEAAAAAjhCSU0EAgAAAAAABAAAAAD/4Q/gaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MCA3OS4xNjA0NTEsIDIwMTcvMDUvMDYtMDE6MDg6MjEgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIiBwZGY6UHJvZHVjZXI9IkNvbnZlcnQgZnJvbSBBcHBsZSBCb29rIHRvIFBERiAoQXBwbGUgSW5jLikgLSBRdWFydHogMi4wIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZBNjczQjlCQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZBNjczQjlBQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzIyQjE1NTM4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzIyQjE1NTQ4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAOGAAEABgAGgAfABdhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGZ3dHB0AAABwAAAABRia3B0AAAB1AAAABRyWFlaAAAB7AAAABRnWFlaAAACAAAAABRiWFlaAAACFAAAABRyVFJDAAACGAAQAMBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRsdW1pAAACjAAAAA埬 +g/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEAAQBIAAAAAQABOEJJTQQGAAAAAAAEAAAAAjhCSU0EAgAAAAAABAAAAAD/4Q/gaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MCA3OS4xNjA0NTEsIDIwMTcvMDUvMDYtMDE6MDg6MjEgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIiBwZGY6UHJvZHVjZXI9IkNvbnZlcnQgZnJvbSBBcHBsZSBCb29rIHRvIFBERiAoQXBwbGUgSW5jLikgLSBRdWFydHogMi4wIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZBNjczQjlCQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZBNjczQjlBQjNGNDExRTk4NjY0QzZERjhDMzYxNTMyIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzIyQjE1NTM4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzIyQjE1NTQ4RjM4MTFFOUExM0Q4RjhBRjE3RTBDRDQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAOGAAEABgAGgAfABdhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGZ3dHB0AAABwAAAABRia3B0AAAB1AAAABRyWFlaAAAB7AAAABRnWFlaAAACAAAAABRiWFlaAAACFAAAABRyVFJDAAACGAAQAMBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRsdW1pAAACjAAAAA埬 \ No newline at end of file diff --git a/src/assets/pay.png b/src/assets/pay.png index b20caec..73bf42a 100644 --- a/src/assets/pay.png +++ b/src/assets/pay.png @@ -1 +1 @@ -iVBORw0KGgoAAAANSUhEUgAAAoAAAAFoCAYAAABq2s2dAAAKpmlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjQhSYkYFEVFFRWLgqLgBFExgSJoQBBQYMmCIoKiouIFAVFFQREREREFFeHn/9/f7O/953vO+V7u+d/33n3nN4CAE/oA4CExzYskABiYmJjZ2Tn5+T8/I8AUEhYSDgoPCQ8REhEVFRkXFxkfHR8lJyYpLS4vNTZATE5MU11eX19fYGFiY2RlZmdoaWprbW9pam1vb3BxcXN0dXd4eXp7fH1+f4CBg4SFh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dYXA2FvUP+9p4+fn5/f4+v3/As6/7E/R2fj4+L8/Lv7Rfr5/L/8D/zP/iAAdM0QBhAIAhDAEEgYlgAIAiDAZgAsLChEMMAh2AhYBEYCLoCFgEmgGlgFngGfga+BqYCFYEKwRjBGcEywQvBDsEOARHBEwEUwRjBGcEJwQrBC8EOwQ9BFkEXQRzBHMEVwR3BIIElQSuBLcEuQTDhGEEnYSNBJlEm4SBhJvEkASVBKGErASvhLGEt4S7hMMEwMTBhMGEyYTRhNOE4YTrhPWE/4TNhO6E84T5hP+FCYUXhROCx4UThT+VB4WLhY2FG4UrhVuFq4W2hfeFz4VnhdeF94Z3hzeGj4dPhneHj4ZfhSeGn4avh2+Gr4fXh/eHz4d/h9+Kj4nfiR+L/4k/it+Jn4tfih+LL4s/i9+L34ufiB+Mv4r/jb+PP49/jD+P/5B/l3+R/6A/p3+V/4w/r3+T/4f/sAAVAhmBGsEIAQfBEAEJwQPBFIEVgR8BGQEYQRtBOgEfwR/BGUEcAR+BJUEiQSzBKMEqgS+BNMEwwTTBNME4wTDBOWE8wT1hPiE9QT9BQ8FEwUzBTcFSQLdBQ0FTwUrBYIFhQWqBYUFgQXFBYYFvQXABcQF0wXTBe8FyAXaBdoF+wX7Bg4GKwZLBmMGRwYtBm8GdgaZBq0GswaTBqcGvgbWBtIG3wbVBv8HAwcxBzsHLwc9B1EHSgdlB28HcAdcB34Hggd/B4cHoQeLh5AHoQedB64HrwfNB8EH0wfjB94H1wffB+8H9Af0B/YH/gf9B/4H/wf/BwEHgQdBBkEGQQ/BEAEZwYyBL0FwwTDBPME4wTjBN0E7QT/BQ0FKwWDBYUFqgWFBZcFyAXTBdMF5wX+BgcGIwYzBkMGRwa1BnsGsgbSBtMG5wbnBwsHIwc7By8HQAdIB2UHbweAB24HfQeGB4kHkgeVB5sHrwfBB8cH2wfvB/QH9Qf3B/4H/wf/B/8HAQeDBEEGQwYhBjkGQwY9Bl0GcwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGCBAgECBAoGFAIBBBIECAQIBggQDBAkGDAYKDA0OFBcaGCg2PDRAQ0pHVFVdX2BjaG1wc3R3ent/g4eLj5OXm5+jp6uvs7fDx8vP19/v8AAEBBAQGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHx8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQIEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqc3V5f4KMl56wtr6/xMjP0tff4uXq7/P3/v8AAQIECg4UGBwgLDBAUFhgZGiAiJCYoKiwuMDQ4PEBESExQVFhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqb3F0eXp9f4OFj5SanqKvsr/Gy9HT2t7h5u3w9fn+//8BAgMICAwOEBQWGhweICEiJygtNT5ESktPUVZXWFpcXV9fY2RmaWpqbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v//wAABAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQEEAgUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hxanN1eX+CjJeer7GzwM/T2N3i5uvw9fj+//8CAwQEBQUGBwgICQoLDAwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqc3V5f4KMl56wsbO9wsnO0dXa3eHn7fD2+f7/AgQFBggIDg8TFhscHyEmKCkwNjg8QUpOUldeYGVqdHx9goeLj5OYm6Glq7W3u8bJz9XY2+Dp6/L09/v9AAICAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v//AQIBAQICAwMEBAUFBgYHBw== +iVBORw0KGgoAAAANSUhEUgAAAoAAAAFoCAYAAABq2s2dAAAKpmlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjQhSYkYFEVFFRWLgqLgBFExgSJoQBBQYMmCIoKiouIFAVFFQREREREFFeHn/9/f7O/953vO+V7u+d/33n3nN4CAE/oA4CExzYskABiYmJjZ2Tn5+T8/I8AUEhYSDgoPCQ8REhEVFRkXFxkfHR8lJyYpLS4vNTZATE5MU11eX19fYGFiY2RlZmdoaWprbW9pam1vb3BxcXN0dXd4eXp7fH1+f4CBg4SFh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dYXA2FvUP+9p4+fn5/f4+v3/As6/7E/R2fj4+L8/Lv7Rfr5/L/8D/zP/iAAdM0QBhAIAhDAEEgYlgAIAiDAZgAsLChEMMAh2AhYBEYCLoCFgEmgGlgFngGfga+BqYCFYEKwRjBGcEywQvBDsEOARHBEwEUwRjBGcEJwQrBC8EOwQ9BFkEXQRzBHMEVwR3BIIElQSuBLcEuQTDhGEEnYSNBJlEm4SBhJvEkASVBKGErASvhLGEt4S7hMMEwMTBhMGEyYTRhNOE4YTrhPWE/4TNhO6E84T5hP+FCYUXhROCx4UThT+VB4WLhY2FG4UrhVuFq4W2hfeFz4VnhdeF94Z3hzeGj4dPhneHj4ZfhSeGn4avh2+Gr4fXh/eHz4d/h9+Kj4nfiR+L/4k/it+Jn4tfih+LL4s/i9+L34ufiB+Mv4r/jb+PP49/jD+P/5B/l3+R/6A/p3+V/4w/r3+T/4f/sAAVAhmBGsEIAQfBEAEJwQPBFIEVgR8BGQEYQRtBOgEfwR/BGUEcAR+BJUEiQSzBKMEqgS+BNMEwwTTBNME4wTDBOWE8wT1hPiE9QT9BQ8FEwUzBTcFSQLdBQ0FTwUrBYIFhQWqBYUFgQXFBYYFvQXABcQF0wXTBe8FyAXaBdoF+wX7Bg4GKwZLBmMGRwYtBm8GdgaZBq0GswaTBqcGvgbWBtIG3wbVBv8HAwcxBzsHLwc9B1EHSgdlB28HcAdcB34Hggd/B4cHoQeLh5AHoQedB64HrwfNB8EH0wfjB94H1wffB+8H9Af0B/YH/gf9B/4H/wf/BwEHgQdBBkEGQQ/BEAEZwYyBL0FwwTDBPME4wTjBN0E7QT/BQ0FKwWDBYUFqgWFBZcFyAXTBdMF5wX+BgcGIwYzBkMGRwa1BnsGsgbSBtMG5wbnBwsHIwc7By8HQAdIB2UHbweAB24HfQeGB4kHkgeVB5sHrwfBB8cH2wfvB/QH9Qf3B/4H/wf/B/8HAQeDBEEGQwYhBjkGQwY9Bl0GcwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGQwZDBkMGCBAgECBAoGFAIBBBIECAQIBggQDBAkGDAYKDA0OFBcaGCg2PDRAQ0pHVFVdX2BjaG1wc3R3ent/g4eLj5OXm5+jp6uvs7fDx8vP19/v8AAEBBAQGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHx8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQIEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqc3V5f4KMl56wtr6/xMjP0tff4uXq7/P3/v8AAQIECg4UGBwgLDBAUFhgZGiAiJCYoKiwuMDQ4PEBESExQVFhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqb3F0eXp9f4OFj5SanqKvsr/Gy9HT2t7h5u3w9fn+//8BAgMICAwOEBQWGhweICEiJygtNT5ESktPUVZXWFpcXV9fY2RmaWpqbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v//wAABAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQEEAgUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hxanN1eX+CjJeer7GzwM/T2N3i5uvw9fj+//8CAwQEBQUGBwgICQoLDAwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaHFqc3V5f4KMl56wsbO9wsnO0dXa3eHn7fD2+f7/AgQFBggIDg8TFhscHyEmKCkwNjg8QUpOUldeYGVqdHx9goeLj5OYm6Glq7W3u8bJz9XY2+Dp6/L09/v9AAICAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdocWpua29zdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v//AQIBAQICAwMEBAUFBgYHBw== \ No newline at end of file diff --git a/src/assets/react.svg b/src/assets/react.svg index 8e0e0f1..6c87de9 100644 --- a/src/assets/react.svg +++ b/src/assets/react.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index b2c7f1c..45e5d4d 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -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 = ({ collapsed, onToggle }) => { navigate('/login'); }; + const menuItems: MenuProps['items'] = [ + { + key: 'profile', + icon: , + label: '个人中心', + }, + { + type: 'divider' as const, + }, + { + key: 'logout', + icon: , + label: '退出登录', + }, + ]; + + const handleMenuClick: MenuProps['onClick'] = ({ key }) => { + if (key === 'profile') { + navigate('/user/profile'); + return; + } + if (key === 'logout') { + handleLogout(); + } + }; + return ( -
+
- - +
+ + + +
); }; diff --git a/src/components/Navbar/navbar.css b/src/components/Navbar/navbar.css new file mode 100644 index 0000000..fd99cf6 --- /dev/null +++ b/src/components/Navbar/navbar.css @@ -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; + } +} diff --git a/src/components/Permission/ReadonlyAction.tsx b/src/components/Permission/ReadonlyAction.tsx new file mode 100644 index 0000000..37af1ed --- /dev/null +++ b/src/components/Permission/ReadonlyAction.tsx @@ -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 = ({ + icon, + children, + danger = false, + className = '', +}) => { + const classes = ['permission-link-disabled', danger ? 'is-danger' : '', className] + .filter(Boolean) + .join(' '); + + return ( + + {icon} + {children} + + ); +}; + +export default ReadonlyAction; diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 7a17abb..da45ebf 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -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['items'][number]; +type MenuItemWithChildren = MenuItem & { children?: MenuItem[] }; const BACKEND_PATH_TO_APP_PATH: Record = { '/project': '/project/list', '/projectBank/userScore': '/workAppraisal/myPerformance', + '/system/logininfor': '/monitor/logininfor', + '/system/operlog': '/monitor/operlog', }; const MENU_ROUTE_ALIASES: Record = { '/projectBank/projectProgress': ['/dashboard/project-execution'], '/workAppraisal/myPerformance': ['/projectBank/userScore'], + '/monitor/logininfor': ['/system/logininfor'], + '/monitor/operlog': ['/system/operlog'], }; const ICON_MAP: Record = { @@ -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 ( .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; +} diff --git a/src/contexts/PermissionContext.tsx b/src/contexts/PermissionContext.tsx index 21edabc..86fe792 100644 --- a/src/contexts/PermissionContext.tsx +++ b/src/contexts/PermissionContext.tsx @@ -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; hasRole: (roles: string | string[]) => boolean; hasPermi: (permissions: string | string[]) => boolean; @@ -19,12 +21,16 @@ interface PermissionContextValue { const PermissionContext = createContext(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 = { + '/project': '/project/list', + '/projectBank/userScore': '/workAppraisal/myPerformance', + '/system/logininfor': '/monitor/logininfor', + '/system/operlog': '/monitor/operlog', +}; const ROUTE_ALIASES: Record = { + '/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 = { '/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) => { + 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(); 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([]); const [permissions, setPermissions] = useState([]); const [routers, setRouters] = useState([]); + const [defaultRoutePath, setDefaultRoutePath] = useState(''); const [allowedPaths, setAllowedPaths] = useState>(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(); - let nextRouteGuardEnabled = false; + let nextDefaultRoutePath = '/index'; + let nextAllowedPaths = new Set(['/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 | 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).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).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 {children}; diff --git a/src/index.css b/src/index.css index a49f610..911ce08 100644 --- a/src/index.css +++ b/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); + } +} diff --git a/src/layout/MainLayout.tsx b/src/layout/MainLayout.tsx index 6a628b4..f8dcb65 100644 --- a/src/layout/MainLayout.tsx +++ b/src/layout/MainLayout.tsx @@ -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 ( - - -
+ + +
+
pms
+ {!collapsed ? ( +
+ UNISSENSE PMS + 新光线平台 +
+ ) : null} +
- -
+ +
setCollapsed(!collapsed)} />
- - + +
+ +
diff --git a/src/layout/layout.css b/src/layout/layout.css index 1ad21ac..0fbc74b 100644 --- a/src/layout/layout.css +++ b/src/layout/layout.css @@ -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; + } } diff --git a/src/main.tsx b/src/main.tsx index b87dea3..727a279 100644 --- a/src/main.tsx +++ b/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( - + + + + + + , ) diff --git a/src/pages/Home/home.css b/src/pages/Home/home.css index 81abac9..2520f61 100644 --- a/src/pages/Home/home.css +++ b/src/pages/Home/home.css @@ -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; + } } diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 418643a..857f724 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -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: , + title: '统一工作入口', + description: '围绕项目、日志、绩效和系统管理组织工作流,减少页面跳转和重复确认。', + }, + { + icon: , + title: '权限清晰可控', + description: '页面、按钮、角色数据分层管理,既适合企业管控,也兼顾一线使用效率。', + }, + { + icon: , + title: '稳定交付视角', + description: '保留后台业务效率,同时升级成更专业、可长期演进的企业产品界面语言。', + }, + ]; + return (
-
- 领取阿里云通用云产品1888优惠券
- - https://www.aliyun.com/minisite/goods?userCode=brki8iof - -
- 领取腾讯云通用云产品2860优惠券
- - https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console - -
- 阿里云服务器折扣区 - >☛☛点我进入☚☚ -     腾讯云服务器秒杀区 - >☛☛点我进入☚☚ -
- - 云产品通用红包,可叠加官网常规优惠使用。(仅限新用户) - -
+
+
+ UNISSENSE PRODUCT CONSOLE + 现代化企业协作平台 + + 保留原有业务结构和效率,同时升级整体视觉语言,让系统更像正式商业化的 AI SaaS 产品。 + + + + + +
+
+ {highlights.map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+
- + - - 若依后台管理框架 -

- 一直想做一款后台管理系统,看了很多优秀的开源项目但是发现没有合适自己的。于是利用空闲休息时间开始自己写一套后台系统。如此有了若依管理系统... -

-

当前版本: v{version}

-

¥免费开源

-

- - -

+ + +
+ {productValues.map((item) => ( +
+
{item.icon}
+
+

{item.title}

+

{item.description}

+
+
+ ))} +
+
- - 技术选型 - - -

后端技术

-
  • SpringBoot
  • Spring Security
  • JWT
  • MyBatis
  • ...
- - -

前端技术

-
  • Vue
  • Vuex
  • Element-ui
  • Axios
  • ...
- -
+ + +
+
+
+ + 后端技术 +
+
    +
  • Spring Boot
  • +
  • Spring Security
  • +
  • JWT
  • +
  • MyBatis
  • +
+
+
+
+ + 前端技术 +
+
    +
  • React
  • +
  • Ant Design
  • +
  • Vite
  • +
  • Axios
  • +
+
+
+
- + - -

官网:http://www.ruoyi.vip

-

QQ群:151450850

+ +
+

阿里云通用云产品优惠券

+ + aliyun.com/minisite/goods?userCode=brki8iof + +

腾讯云通用云产品优惠券

+ + cloud.tencent.com/redirect.php?redirect=1025 + + 仅限新用户,可叠加官网常规优惠使用 +
- + { - - donate - 你可以请作者喝杯咖啡表示鼓励 + +
+
+

官网:ruoyi.vip

+

QQ群:151450850

+ 你可以通过捐赠支持持续迭代。 +
+ donate +
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index f666923..2f5c5f3 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -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(); 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 (
-
-

新光线平台

- - - } placeholder="账号" /> - - - - } placeholder="密码" /> - - - {captchaEnabled && ( - - - } placeholder="验证码" /> - -
- {codeUrl ? void fetchCode()} alt="Captcha" /> : null} +
+
+
UNISSENSE PMS
+

新光线管理平台

+

+ 统一承载项目、日志、绩效和系统管理业务。保持原有流程稳定,同时提供更专业、克制、清晰的产品体验。 +

+
+
+ AI-ready + 现代化企业工作台
- - )} +
+ Role-based + 页面与按钮权限体系 +
+
+ Responsive + 兼顾桌面与移动访问 +
+
+
+ +
+ Sign in +

欢迎回来

+

使用你的企业账户进入工作台。

+
- - - 记住密码 + + } placeholder="账号" /> - - - - - + + } placeholder="密码" /> + + + {captchaEnabled && ( +
+ + } placeholder="验证码" /> + +
+ {codeUrl ? void fetchCode()} alt="Captcha" /> : null} +
+
+ )} + + + + 记住密码 + + + + + + + +
unissense.tech
diff --git a/src/pages/Login/login.css b/src/pages/Login/login.css index 1379a1e..9f51b66 100644 --- a/src/pages/Login/login.css +++ b/src/pages/Login/login.css @@ -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; + } } diff --git a/src/pages/Profile/AvatarUploader.tsx b/src/pages/Profile/AvatarUploader.tsx new file mode 100644 index 0000000..cff3c7c --- /dev/null +++ b/src/pages/Profile/AvatarUploader.tsx @@ -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 ( +
+ + + +
{displayName || '未命名用户'}
+
支持 JPG、PNG,点击头像直接上传
+
+ ); +}; + +export default AvatarUploader; diff --git a/src/pages/Profile/ResetPassword.tsx b/src/pages/Profile/ResetPassword.tsx index 1de5735..48388f4 100644 --- a/src/pages/Profile/ResetPassword.tsx +++ b/src/pages/Profile/ResetPassword.tsx @@ -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(); + const [form] = Form.useForm(); + 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 ( -
- - - - - - - ({ - validator(_, value) { - if (!value || getFieldValue('newPassword') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致!')); - }, - }), - ]} - > - - - - - -
- ); + 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 ( +
+ + + + "'|\\]+$/, message: '不能包含非法字符:< > \" \' \\ |' }, + ]} + > + + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致')); + }, + }), + ]} + > + + + + + + + + +
+ ); }; export default ResetPassword; diff --git a/src/pages/Profile/UserInfo.tsx b/src/pages/Profile/UserInfo.tsx index 35c7dc7..7d200c0 100644 --- a/src/pages/Profile/UserInfo.tsx +++ b/src/pages/Profile/UserInfo.tsx @@ -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) => void; } -const UserInfo = ({ user }: UserInfoProps) => { +const UserInfo = ({ user, onUpdated }: UserInfoProps) => { const [form] = Form.useForm(); + 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 ( -
- + + { - - + + + + + ); diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx index 2ee0e68..e0af391 100644 --- a/src/pages/Profile/index.tsx +++ b/src/pages/Profile/index.tsx @@ -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(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 ; + const profileItems = useMemo(() => { + if (!user) { + return []; + } + + return [ + { + key: 'userName', + label: '用户名称', + icon: , + value: user.userName ?? '-', + }, + { + key: 'phone', + label: '手机号码', + icon: , + value: user.phonenumber ?? '-', + }, + { + key: 'email', + label: '用户邮箱', + icon: , + value: user.email ?? '-', + }, + { + key: 'dept', + label: '所属部门', + icon: , + value: user.dept?.deptName ? `${user.dept.deptName} / ${postGroup || '-'}` : '-', + }, + { + key: 'role', + label: '所属角色', + icon: , + value: roleGroup || '-', + }, + { + key: 'createTime', + label: '创建日期', + icon: , + value: user.createTime ?? '-', + }, + ]; + }, [postGroup, roleGroup, user]); + + if (loading) { + return ( +
+ +
+ ); } - const profileItems = [ - { icon: , label: '用户名称', value: user.userName ?? '' }, - { icon: , label: '手机号码', value: user.phonenumber ?? '' }, - { icon: , label: '用户邮箱', value: user.email ?? '' }, - { icon: , label: '所属部门', value: user.dept ? `${user.dept.deptName ?? ''} / ${postGroup}` : '' }, - { icon: , label: '所属角色', value: roleGroup }, - { icon: , label: '创建日期', value: user.createTime ?? '' }, - ]; + if (!user) { + return null; + } return ( -
- - - -
- avatar -
- ( - - {item.value}
} - /> - - )} +
+
+
+ + setUser((prev) => (prev ? { ...prev, avatar: nextAvatar } : prev))} /> + +
    + {profileItems.map((item) => ( +
  • + + {item.icon} + {item.label} + + {item.value} +
  • + ))} +
- - - - - - - - +
+ +
+ + { + const params = new URLSearchParams(searchParams); + params.set('tab', key); + setSearchParams(params); + }} + /> + +
+ {activeTab === 'userinfo' ? ( + setUser((prev) => (prev ? { ...prev, ...values } : prev))} /> + ) : ( - - + )} +
- - +
+
); }; diff --git a/src/pages/Profile/profile.css b/src/pages/Profile/profile.css index 2ff1949..95e6269 100644 --- a/src/pages/Profile/profile.css +++ b/src/pages/Profile/profile.css @@ -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; + } } diff --git a/src/pages/dashboard/ProjectExecutionPage.tsx b/src/pages/dashboard/ProjectExecutionPage.tsx index f68b4e9..00cab04 100644 --- a/src/pages/dashboard/ProjectExecutionPage.tsx +++ b/src/pages/dashboard/ProjectExecutionPage.tsx @@ -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) => (
- + {canViewProjectUser ? ( + + ) : ( + {String(value ?? '-') || '-'} + )}
{row.projectCode || '未配置项目编号'} @@ -581,7 +592,7 @@ const ProjectExecutionPage = () => { })); return [...baseColumns, ...periodColumns]; - }, [openProject, periods, statusMap]); + }, [canViewProjectUser, openProject, periods, statusMap]); const stats = useMemo( () => [ diff --git a/src/pages/dashboard/project-execution.css b/src/pages/dashboard/project-execution.css index 69c2903..bfe49cd 100644 --- a/src/pages/dashboard/project-execution.css +++ b/src/pages/dashboard/project-execution.css @@ -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; diff --git a/src/pages/monitor/CacheListPage.tsx b/src/pages/monitor/CacheListPage.tsx index 9eb6d0f..6bad116 100644 --- a/src/pages/monitor/CacheListPage.tsx +++ b/src/pages/monitor/CacheListPage.tsx @@ -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([]); const [cacheKeys, setCacheKeys] = useState([]); const [cacheForm, setCacheForm] = useState(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) => ( - } + extra={ + + + + } style={{ height: tableHeight + 50, overflow: 'auto' }} >
diff --git a/src/pages/monitor/CacheMonitorPage.tsx b/src/pages/monitor/CacheMonitorPage.tsx index 7eef7a8..175aaa4 100644 --- a/src/pages/monitor/CacheMonitorPage.tsx +++ b/src/pages/monitor/CacheMonitorPage.tsx @@ -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 ( -
+
@@ -129,12 +129,12 @@ const CacheMonitorPage = () => { - + - + diff --git a/src/pages/monitor/JobMonitorPage.tsx b/src/pages/monitor/JobMonitorPage.tsx index 27722a5..70ff843 100644 --- a/src/pages/monitor/JobMonitorPage.tsx +++ b/src/pages/monitor/JobMonitorPage.tsx @@ -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) => ( - void handleStatusChange(record)} - /> + + } + > + void handleStatusChange(record)} + /> + ), }, { @@ -345,17 +359,27 @@ const JobMonitorPage = () => { return ( - - - - - + + } danger>删除}> + + + 更多} + > + + + + ); }, @@ -397,21 +421,31 @@ const JobMonitorPage = () => { - - - - - + + + + + + + + + + + + + + + { }; const LoginLogPage = () => { + const { message, modal } = App.useApp(); const [queryForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [list, setList] = useState([]); @@ -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 ( -
+
@@ -316,31 +319,39 @@ const LoginLogPage = () => { - - - - + + + + + + + + + + + +
{ + const { hasPermi } = usePermission(); const [queryForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [total, setTotal] = useState(0); const [queryParams, setQueryParams] = useState(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) => ( - + } danger>强退} + > + + ), }, ]; return ( -
+
diff --git a/src/pages/monitor/OperationLogPage.tsx b/src/pages/monitor/OperationLogPage.tsx index f6c355d..c14e8e4 100644 --- a/src/pages/monitor/OperationLogPage.tsx +++ b/src/pages/monitor/OperationLogPage.tsx @@ -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 ( -
+
@@ -367,22 +369,28 @@ const OperationLogPage = () => { - - - + + + + + + + + +
{ }; const ServerMonitorPage = () => { + const { message } = App.useApp(); const [serverInfo, setServerInfo] = useState(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); diff --git a/src/pages/monitor/cache-list.css b/src/pages/monitor/cache-list.css index 3b91560..ec84569 100644 --- a/src/pages/monitor/cache-list.css +++ b/src/pages/monitor/cache-list.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .cache-list-container .ant-card-body { padding-top: 8px; } diff --git a/src/pages/monitor/cache-monitor.css b/src/pages/monitor/cache-monitor.css index f47b80d..c70f87d 100644 --- a/src/pages/monitor/cache-monitor.css +++ b/src/pages/monitor/cache-monitor.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .card-box { padding-right: 15px; padding-left: 15px; diff --git a/src/pages/monitor/job-monitor.css b/src/pages/monitor/job-monitor.css index ed4d7e0..c2a17bc 100644 --- a/src/pages/monitor/job-monitor.css +++ b/src/pages/monitor/job-monitor.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .job-monitor-container .ant-form-item { margin-bottom: 0px; /* Reduce vertical space in search form */ } diff --git a/src/pages/monitor/login-log.css b/src/pages/monitor/login-log.css index c349486..dc8349d 100644 --- a/src/pages/monitor/login-log.css +++ b/src/pages/monitor/login-log.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .login-log-container .ant-form-item { margin-bottom: 0px; /* Reduce vertical space in search form */ } diff --git a/src/pages/monitor/monitor-shared.css b/src/pages/monitor/monitor-shared.css new file mode 100644 index 0000000..07406e6 --- /dev/null +++ b/src/pages/monitor/monitor-shared.css @@ -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; +} diff --git a/src/pages/monitor/online-user.css b/src/pages/monitor/online-user.css index 3024537..e8a5bd8 100644 --- a/src/pages/monitor/online-user.css +++ b/src/pages/monitor/online-user.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .online-user-container .ant-form-item { margin-bottom: 0px; /* Reduce vertical space in search form */ } diff --git a/src/pages/monitor/operation-log.css b/src/pages/monitor/operation-log.css index 12c89cf..f5ffc12 100644 --- a/src/pages/monitor/operation-log.css +++ b/src/pages/monitor/operation-log.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .operation-log-container .ant-form-item { margin-bottom: 0px; /* Reduce vertical space in search form */ } diff --git a/src/pages/monitor/server-monitor.css b/src/pages/monitor/server-monitor.css index 34b7c9d..3a29977 100644 --- a/src/pages/monitor/server-monitor.css +++ b/src/pages/monitor/server-monitor.css @@ -1,3 +1,5 @@ +@import "./monitor-shared.css"; + .server-monitor-container .card-box { margin-bottom: 16px; } diff --git a/src/pages/project/DemandManagePage.tsx b/src/pages/project/DemandManagePage.tsx index 7229812..9431086 100644 --- a/src/pages/project/DemandManagePage.tsx +++ b/src/pages/project/DemandManagePage.tsx @@ -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(); const [versionForm] = Form.useForm(); + 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(); versionRows.forEach((item) => { @@ -904,17 +912,22 @@ const DemandManagePage: React.FC = () => { render: (_value, record) => ( - { + if (value !== record.demandStatus) { + handleQuickUpdate(record, { demandStatus: value }); + } + }} + /> + ), }, @@ -928,17 +941,22 @@ const DemandManagePage: React.FC = () => { key: 'priority', width: 130, render: (_value, record) => ( - { + if (value !== record.priority) { + handleQuickUpdate(record, { priority: value }); + } + }} + /> + ), }, { @@ -947,19 +965,23 @@ const DemandManagePage: React.FC = () => { width: 150, render: (_value, record) => ( - - handleDeleteOne(record)} - > - - + + 删除}> + handleDeleteOne(record)} + > + + + ), }, @@ -980,7 +1002,24 @@ const DemandManagePage: React.FC = () => { }; return ( -
+
+
+
+
DEMAND WORKSPACE
+
需求管理
+
围绕版本结构、排期状态和负责人维护项目交付节奏。
+
+
+
+ 所属项目 + {projectName} +
+
+ 需求总数 + {total} +
+
+
{versionSidebarCollapsed ? ( @@ -994,6 +1033,7 @@ const DemandManagePage: React.FC = () => { ) : ( @@ -1039,7 +1079,9 @@ const DemandManagePage: React.FC = () => { - { 项目周期:{projectStartText} ~ {projectEndText} -
+ @@ -1291,18 +1346,22 @@ const DemandManagePage: React.FC = () => { - - - - - + + + + + + + + +
@@ -1320,7 +1379,7 @@ const DemandManagePage: React.FC = () => { />
-
+
共 {total} 条 { { { 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 ( -
-
+
+
- +
+
+
PROJECT DETAIL
+
{isEdit ? '编辑项目' : '新建项目'}
+
维护项目基本信息、负责人、周期与描述,保持项目数据结构统一。
+
+
+ {isEdit ? '当前模式:编辑' : '当前模式:新建'} + {projectCode || '待生成编号'} +
+
+
@@ -119,7 +138,7 @@ const ProjectDetailPage: React.FC = () => {