Commit dedde183 authored by Andy-bubu's avatar Andy-bubu

feat: finalize Xinyuan UI alignment

parent 2078e013
- generic:
- generic [ref=f10e1]:
- complementary [ref=f10e3]:
- generic [ref=f10e5]:
- img "鑫元" [ref=f10e7]
- generic [ref=f10e8]: 警示案例库
- button "切换应用" [ref=f10e11] [cursor=pointer]
- treegrid "案例库工作台" [ref=f10e15]:
- row "驾驶舱" [level=1] [ref=f10e16] [cursor=pointer]:
- gridcell "驾驶舱" [ref=f10e17]
- row "案例管理" [level=1] [ref=f10e23] [cursor=pointer]:
- gridcell "案例管理" [ref=f10e24]
- button "打开用户菜单" [ref=f10e31] [cursor=pointer]:
- generic [ref=f10e32]: A
- generic [ref=f10e34]:
- generic [ref=f10e35]: Andy
- generic [ref=f10e36]: andy
- main [ref=f10e37]:
- navigation [ref=f10e38]:
- generic [ref=f10e39]:
- button "收起侧栏" [ref=f10e40] [cursor=pointer]
- heading "驾驶舱" [level=1] [ref=f10e41]
- button "通知消息" [ref=f10e44] [cursor=pointer]
- generic [ref=f10e47]:
- heading "驾驶舱" [level=1] [ref=f10e48]
- generic [ref=f10e49]:
- generic [ref=f10e51]:
- generic [ref=f10e52]:
- term [ref=f10e54]: 案例总数
- generic [ref=f10e55]:
- definition [ref=f10e56]: "15"
- generic "案例总数" [ref=f10e57]
- generic [ref=f10e61]:
- term [ref=f10e63]: 分类数量
- generic [ref=f10e64]:
- definition [ref=f10e65]: "10"
- generic "分类数量" [ref=f10e66]
- generic [ref=f10e70]:
- term [ref=f10e72]: 标签数量
- generic [ref=f10e73]:
- definition [ref=f10e74]: "15"
- generic "标签数量" [ref=f10e75]
- generic [ref=f10e79]:
- term [ref=f10e81]: 案例类型
- generic [ref=f10e82]:
- definition [ref=f10e83]: "15"
- generic "案例类型" [ref=f10e84]
- generic [ref=f10e88]:
- term [ref=f10e90]: 待整理
- generic [ref=f10e91]:
- definition [ref=f10e92]: "0"
- generic "待整理" [ref=f10e93]
- generic [ref=f10e97]:
- generic [ref=f10e98]:
- generic [ref=f10e99]:
- heading "分类结构" [level=3] [ref=f10e101]
- generic [ref=f10e105]:
- list [ref=f10e107]:
- listitem [ref=f10e108]:
- img "信息安全风险 legend icon" [ref=f10e109]
- text: 信息安全风险
- listitem [ref=f10e111]:
- img "信息技术风险 legend icon" [ref=f10e112]
- text: 信息技术风险
- listitem [ref=f10e114]:
- img "反洗钱风险 legend icon" [ref=f10e115]
- text: 反洗钱风险
- listitem [ref=f10e117]:
- img "服务风险 legend icon" [ref=f10e118]
- text: 服务风险
- listitem [ref=f10e120]:
- img "未分类 legend icon" [ref=f10e121]
- text: 未分类
- listitem [ref=f10e123]:
- img "治理风险 legend icon" [ref=f10e124]
- text: 治理风险
- listitem [ref=f10e126]:
- img "知识管理风险 legend icon" [ref=f10e127]
- text: 知识管理风险
- listitem [ref=f10e129]:
- img "财务风险 legend icon" [ref=f10e130]
- text: 财务风险
- application [ref=f10e132]:
- generic [ref=f10e133]: 13案例记录
- generic [ref=f10e161]:
- heading "案例类型排行" [level=3] [ref=f10e163]
- application [ref=f10e168]:
- generic [ref=f10e207]:
- generic [ref=f10e208]:
- generic [ref=f10e209]: "0"
- generic [ref=f10e211]: "1"
- generic [ref=f10e213]: "2"
- generic [ref=f10e215]: "3"
- generic [ref=f10e217]: "4"
- generic [ref=f10e219]:
- generic [ref=f10e220]: 形式审查疏漏
- generic [ref=f10e222]: 评价机制失效
- generic [ref=f10e224]: 主数据同步异常
- generic [ref=f10e226]: 提醒失效
- generic [ref=f10e228]: 经验未复用
- generic [ref=f10e230]: 幂等控制缺失
- generic [ref=f10e232]: 权限超期
- generic [ref=f10e234]: 标签误分类
- generic [ref=f10e236]:
- generic [ref=f10e237]:
- heading "案例来源分布" [level=3] [ref=f10e239]
- application [ref=f10e244]:
- generic [ref=f10e283]:
- generic [ref=f10e284]:
- generic [ref=f10e285]: 招标复核
- generic [ref=f10e287]: 采购复盘
- generic [ref=f10e289]: 运营巡检
- generic [ref=f10e291]: 运营复盘
- generic [ref=f10e293]: 技术事故复盘
- generic [ref=f10e295]: 消保月度复盘
- generic [ref=f10e297]:
- generic [ref=f10e298]: "0"
- generic [ref=f10e300]: "1"
- generic [ref=f10e302]: "2"
- generic [ref=f10e304]: "3"
- generic [ref=f10e306]: "4"
- generic [ref=f10e308]:
- heading "关键字频次" [level=3] [ref=f10e310]
- application [ref=f10e315]:
- generic [ref=f10e354]:
- generic [ref=f10e355]:
- generic [ref=f10e356]: "0"
- generic [ref=f10e358]: "1"
- generic [ref=f10e360]: "2"
- generic [ref=f10e362]: "3"
- generic [ref=f10e364]: "4"
- generic [ref=f10e366]:
- generic [ref=f10e367]: 投标文件
- generic [ref=f10e369]: 盖章页
- generic [ref=f10e371]: 形式审查
- generic [ref=f10e373]: 供应商评价
- generic [ref=f10e375]: 履约管理
- generic [ref=f10e377]: 续签决策
- generic [ref=f10e379]: 印鉴卡
- generic [ref=f10e381]: 主数据同步
- generic [ref=f10e383]:
- heading "最近更新" [level=2] [ref=f10e384]
- grid "最近更新案例" [ref=f10e390]:
- rowgroup [ref=f10e391]:
- row [ref=f10e392]:
- columnheader [ref=f10e393]:
- text: 案例标题
- slider "尺寸调整器 案例标题" [ref=f10e394]: "460"
- columnheader [ref=f10e395]:
- text: 分类
- slider "尺寸调整器 分类" [ref=f10e396]: "160"
- columnheader [ref=f10e397]:
- text: 标签
- slider "尺寸调整器 标签" [ref=f10e398]: "260"
- columnheader [ref=f10e399]:
- text: 更新时间
- slider "尺寸调整器 更新时间" [ref=f10e400]: "180"
- rowgroup [ref=f10e401]:
- row [ref=f10e402]:
- rowheader [ref=f10e403]:
- button "2026测试-投标文件盖章页缺页未识别案例 某投标文件扫描件在上传过程中遗漏了一页盖章页,但评审小组仅核对目录和总页数,未逐项检查关键签章位置,导致带缺页材料进入评审流程。虽然最终未中标,但事件反映出形式审查清单执行不严。" [ref=f10e404]:
- generic [ref=f10e405]: 2026测试-投标文件盖章页缺页未识别案例
- generic [ref=f10e406]: 某投标文件扫描件在上传过程中遗漏了一页盖章页,但评审小组仅核对目录和总页数,未逐项检查关键签章位置,导致带缺页材料进入评审流程。虽然最终未中标,但事件反映出形式审查清单执行不严。
- gridcell "未分类" [ref=f10e407]
- gridcell "招投标,AI批量造数-20260521-续批" [ref=f10e410]
- gridcell "2026-08-13 15:52" [ref=f10e413]
- row [ref=f10e415]:
- rowheader [ref=f10e416]:
- button "2026测试-供应商履约评价长期未更新案例 部分长期合作供应商的履约评价停留在上一年度,项目交付中的延期、返工和服务投诉未及时沉淀到评价结果,导致续签和复购决策缺少真实依据。问题说明履约反馈回路未真正嵌入采购管理。" [ref=f10e417]:
- generic [ref=f10e418]: 2026测试-供应商履约评价长期未更新案例
- generic [ref=f10e419]: 部分长期合作供应商的履约评价停留在上一年度,项目交付中的延期、返工和服务投诉未及时沉淀到评价结果,导致续签和复购决策缺少真实依据。问题说明履约反馈回路未真正嵌入采购管理。
- gridcell "治理风险" [ref=f10e420]
- gridcell "供应商评价,AI批量造数-20260521-续批" [ref=f10e423]
- gridcell "2026-06-15 12:19" [ref=f10e426]
- row [ref=f10e428]:
- rowheader [ref=f10e429]:
- button "2026测试-印鉴卡更新后柜面未同步案例 客户完成印鉴变更后,核心主数据已更新,但部分柜面终端缓存未及时刷新,导致经办人员按旧印鉴卡进行人工复核并误判异常。事件虽经复核纠正,但暴露出关键主数据变更后的前端同步机制不足。" [ref=f10e430]:
- generic [ref=f10e431]: 2026测试-印鉴卡更新后柜面未同步案例
- generic [ref=f10e432]: 客户完成印鉴变更后,核心主数据已更新,但部分柜面终端缓存未及时刷新,导致经办人员按旧印鉴卡进行人工复核并误判异常。事件虽经复核纠正,但暴露出关键主数据变更后的前端同步机制不足。
- gridcell "治理风险" [ref=f10e433]
- gridcell "印鉴管理,AI批量造数-20260521-续批" [ref=f10e436]
- gridcell "2026-06-15 12:19" [ref=f10e439]
- row [ref=f10e441]:
- rowheader [ref=f10e442]:
- button "2026测试-客户证照到期提醒触达失败案例 系统已配置客户证照到期前提醒,但由于短信渠道签名异常和邮件地址长期未维护,多个客户未收到续补资料通知。后续业务办理时集中出现证照过期问题,影响交易时效。该问题暴露出提醒机制仅关注“发出”,未验证“送达”和“处理”。" [ref=f10e443]:
- generic [ref=f10e444]: 2026测试-客户证照到期提醒触达失败案例
- generic [ref=f10e445]: 系统已配置客户证照到期前提醒,但由于短信渠道签名异常和邮件地址长期未维护,多个客户未收到续补资料通知。后续业务办理时集中出现证照过期问题,影响交易时效。该问题暴露出提醒机制仅关注“发出”,未验证“送达”和“处理”。
- gridcell "治理风险" [ref=f10e446]
- gridcell "证照管理,AI批量造数-20260521-续批" [ref=f10e449]
- gridcell "2026-06-15 12:19" [ref=f10e452]
- row [ref=f10e454]:
- rowheader [ref=f10e455]:
- button "2026测试-项目复盘结论未进入知识库案例 某重点项目在结束后形成了较完整的问题复盘和改进建议,但材料仅保存在项目群和个人网盘中,未录入统一知识库。后续相似项目重复踩坑,团队才发现既有经验未被有效复用。说明复盘机制缺乏沉淀动作和责任要求。" [ref=f10e456]:
- generic [ref=f10e457]: 2026测试-项目复盘结论未进入知识库案例
- generic [ref=f10e458]: 某重点项目在结束后形成了较完整的问题复盘和改进建议,但材料仅保存在项目群和个人网盘中,未录入统一知识库。后续相似项目重复踩坑,团队才发现既有经验未被有效复用。说明复盘机制缺乏沉淀动作和责任要求。
- gridcell "知识管理风险" [ref=f10e459]
- gridcell "知识沉淀,AI批量造数-20260521-续批" [ref=f10e462]
- gridcell "2026-05-21 16:58" [ref=f10e465]
- row [ref=f10e467]:
- rowheader [ref=f10e468]:
- button "2026测试-系统批量导入失败重试无幂等案例 某业务系统批量导入任务在超时后被人工重复重试,因缺少批次幂等键,导致部分记录重复写入。后续清理耗费大量人工,也影响了下游报表准确性。问题说明批量任务失败处理设计不足。" [ref=f10e469]:
- generic [ref=f10e470]: 2026测试-系统批量导入失败重试无幂等案例
- generic [ref=f10e471]: 某业务系统批量导入任务在超时后被人工重复重试,因缺少批次幂等键,导致部分记录重复写入。后续清理耗费大量人工,也影响了下游报表准确性。问题说明批量任务失败处理设计不足。
- gridcell "信息技术风险" [ref=f10e472]
- gridcell "批量导入,AI批量造数-20260521-续批" [ref=f10e475]
- gridcell "2026-05-21 16:58" [ref=f10e478]
- button [ref=f10e480] [cursor=pointer]
- generic [ref=f10e481]: 投标文件
- generic [ref=f10e482]:
- generic:
- generic:
- button "取消"
- dialog "应用切换" [active] [ref=f10e483]:
- button "关闭应用切换" [ref=f10e484] [cursor=pointer]
- generic [ref=f10e486]:
- heading "应用切换" [level=2] [ref=f10e487]
- group [ref=f10e489]:
- searchbox "搜索应用" [ref=f10e490]
- button "清空应用搜索"
- generic [ref=f10e493]:
- group [ref=f10e494]:
- generic [ref=f10e495]:
- heading "演示" [level=3] [ref=f10e496]
- paragraph [ref=f10e497]: 12 个应用
- link "监察稽核管理【新】" [ref=f10e498] [cursor=pointer]:
- /url: /audit_manager_new
- generic "监察稽核管理【新】" [ref=f10e503]
- link "廉洁从业管理【新】" [ref=f10e504] [cursor=pointer]:
- /url: /integrity_practice_management_new
- generic "廉洁从业管理【新】" [ref=f10e509]
- link "廉洁风险防控【新】" [ref=f10e510] [cursor=pointer]:
- /url: /integrity_risk_prevention_new
- generic "廉洁风险防控【新】" [ref=f10e515]
- link "oa流程监控【新】" [ref=f10e516] [cursor=pointer]:
- /url: /oa_process_monitor_new
- generic "oa流程监控【新】" [ref=f10e521]
- link "模型分析" [ref=f10e522] [cursor=pointer]:
- /url: /model_analysis
- generic "模型分析" [ref=f10e527]
- link "AI应用" [ref=f10e528] [cursor=pointer]:
- /url: /ai_chat
- generic "AI应用" [ref=f10e533]
- link "问题整改" [ref=f10e534] [cursor=pointer]:
- /url: /problem_rectification_new
- generic "问题整改" [ref=f10e539]
- link "审计项目管理[新]" [ref=f10e540] [cursor=pointer]:
- /url: /audit_project_manager_new
- generic "审计项目管理[新]" [ref=f10e545]
- link "党风廉政建设" [ref=f10e546] [cursor=pointer]:
- /url: /incorrupt_party_build
- generic "党风廉政建设" [ref=f10e551]
- link "部门任务管理" [ref=f10e552] [cursor=pointer]:
- /url: /project_manager_xy
- generic "部门任务管理" [ref=f10e557]
- link "审计材料中心" [ref=f10e558] [cursor=pointer]:
- /url: /audit_material_manager
- generic "审计材料中心" [ref=f10e563]
- link "警示案例库 案例库工作台 当前" [ref=f10e564] [cursor=pointer]:
- /url: /risk_case_library
- generic [ref=f10e568]:
- generic "警示案例库" [ref=f10e569]
- generic "案例库工作台" [ref=f10e570]
- generic [ref=f10e571]: 当前
- group [ref=f10e574]:
- generic [ref=f10e575]:
- heading "审计应用" [level=3] [ref=f10e576]
- paragraph [ref=f10e577]: 6 个应用
- link "审计项目管理" [ref=f10e578] [cursor=pointer]:
- /url: /audit_project_manager
- generic "审计项目管理" [ref=f10e583]
- link "问题整改" [ref=f10e584] [cursor=pointer]:
- /url: /problem_rectification
- generic "问题整改" [ref=f10e589]
- link "流程管理" [ref=f10e590] [cursor=pointer]:
- /url: /process_management
- generic "流程管理" [ref=f10e595]
- link "公告管理" [ref=f10e596] [cursor=pointer]:
- /url: /disclosure_management
- generic "公告管理" [ref=f10e601]
- link "OA流程监控" [ref=f10e602] [cursor=pointer]:
- /url: /oa_process_monitor
- generic "OA流程监控" [ref=f10e607]
- link "量化指标 量化指标的描述信息" [ref=f10e608] [cursor=pointer]:
- /url: /indicator_manager
- generic [ref=f10e612]:
- generic "量化指标" [ref=f10e613]
- generic "量化指标的描述信息" [ref=f10e614]
- group [ref=f10e615]:
- generic [ref=f10e616]:
- heading "法规制度管理" [level=3] [ref=f10e617]
- paragraph [ref=f10e618]: 3 个应用
- link "内部制度" [ref=f10e619] [cursor=pointer]:
- /url: /internal_regulation
- generic "内部制度" [ref=f10e624]
- link "内部制度管理" [ref=f10e625] [cursor=pointer]:
- /url: /internal_regulation_manager
- generic "内部制度管理" [ref=f10e630]
- link "外部法规" [ref=f10e631] [cursor=pointer]:
- /url: /law_reference
- generic "外部法规" [ref=f10e636]
- group [ref=f10e637]:
- generic [ref=f10e638]:
- heading "廉洁风险管理" [level=3] [ref=f10e639]
- paragraph [ref=f10e640]: 2 个应用
- link "廉洁风险防控" [ref=f10e641] [cursor=pointer]:
- /url: /integrity_risk_prevention
- generic "廉洁风险防控" [ref=f10e646]
- link "廉洁从业管理" [ref=f10e647] [cursor=pointer]:
- /url: /integrity_practice_management
- generic "廉洁从业管理" [ref=f10e652]
- group [ref=f10e653]:
- generic [ref=f10e654]:
- heading "其他相关应用" [level=3] [ref=f10e655]
- paragraph [ref=f10e656]: 3 个应用
- link "监察稽核管理" [ref=f10e657] [cursor=pointer]:
- /url: /audit_manager
- generic "监察稽核管理" [ref=f10e662]
- link "规章制度管理" [ref=f10e663] [cursor=pointer]:
- /url: /policy_manager
- generic "规章制度管理" [ref=f10e668]
- link "风险管理应用" [ref=f10e669] [cursor=pointer]:
- /url: /risk_manager
- generic "风险管理应用" [ref=f10e674]
- group [ref=f10e675]:
- generic [ref=f10e676]:
- heading "其他应用" [level=3] [ref=f10e677]
- paragraph [ref=f10e678]: 3 个应用
- link "测试量化" [ref=f10e679] [cursor=pointer]:
- /url: /test
- generic "测试量化" [ref=f10e684]
- link "廉洁风险管理" [ref=f10e685] [cursor=pointer]:
- /url: /integrity_risk_manager
- generic "廉洁风险管理" [ref=f10e690]
- link "手机监测" [ref=f10e691] [cursor=pointer]:
- /url: /phone_detect
- generic "手机监测" [ref=f10e696]
\ No newline at end of file
- generic [active]:
- generic [ref=f8e1]:
- complementary [ref=f8e3]:
- generic [ref=f8e5]:
- img "鑫元" [ref=f8e7]
- generic [ref=f8e8]: 警示案例库
- button "切换应用" [ref=f8e11] [cursor=pointer]
- treegrid "案例库工作台" [ref=f8e15]:
- row "驾驶舱" [level=1] [ref=f8e16] [cursor=pointer]:
- gridcell "驾驶舱" [ref=f8e17]
- row "案例管理" [level=1] [ref=f8e23] [cursor=pointer]:
- gridcell "案例管理" [ref=f8e24]
- button "打开用户菜单" [ref=f8e31] [cursor=pointer]:
- generic [ref=f8e32]: A
- generic [ref=f8e34]:
- generic [ref=f8e35]: Andy
- generic [ref=f8e36]: andy
- main [ref=f8e37]:
- navigation [ref=f8e38]:
- generic [ref=f8e39]:
- button "收起侧栏" [ref=f8e40] [cursor=pointer]
- heading "驾驶舱" [level=1] [ref=f8e41]
- button "通知消息" [ref=f8e44] [cursor=pointer]
- generic [ref=f8e47]:
- heading "驾驶舱" [level=1] [ref=f8e48]
- generic [ref=f8e49]:
- generic [ref=f8e51]:
- generic [ref=f8e52]:
- term [ref=f8e54]: 案例总数
- generic [ref=f8e55]:
- definition [ref=f8e56]: "15"
- generic "案例总数" [ref=f8e57]
- generic [ref=f8e61]:
- term [ref=f8e63]: 分类数量
- generic [ref=f8e64]:
- definition [ref=f8e65]: "10"
- generic "分类数量" [ref=f8e66]
- generic [ref=f8e70]:
- term [ref=f8e72]: 标签数量
- generic [ref=f8e73]:
- definition [ref=f8e74]: "15"
- generic "标签数量" [ref=f8e75]
- generic [ref=f8e79]:
- term [ref=f8e81]: 案例类型
- generic [ref=f8e82]:
- definition [ref=f8e83]: "15"
- generic "案例类型" [ref=f8e84]
- generic [ref=f8e88]:
- term [ref=f8e90]: 待整理
- generic [ref=f8e91]:
- definition [ref=f8e92]: "0"
- generic "待整理" [ref=f8e93]
- generic [ref=f8e97]:
- generic [ref=f8e98]:
- generic [ref=f8e99]:
- heading "分类结构" [level=3] [ref=f8e101]
- generic [ref=f8e105]:
- list [ref=f8e107]:
- listitem [ref=f8e108]:
- img "信息安全风险 legend icon" [ref=f8e109]
- text: 信息安全风险
- listitem [ref=f8e111]:
- img "信息技术风险 legend icon" [ref=f8e112]
- text: 信息技术风险
- listitem [ref=f8e114]:
- img "反洗钱风险 legend icon" [ref=f8e115]
- text: 反洗钱风险
- listitem [ref=f8e117]:
- img "服务风险 legend icon" [ref=f8e118]
- text: 服务风险
- listitem [ref=f8e120]:
- img "未分类 legend icon" [ref=f8e121]
- text: 未分类
- listitem [ref=f8e123]:
- img "治理风险 legend icon" [ref=f8e124]
- text: 治理风险
- listitem [ref=f8e126]:
- img "知识管理风险 legend icon" [ref=f8e127]
- text: 知识管理风险
- listitem [ref=f8e129]:
- img "财务风险 legend icon" [ref=f8e130]
- text: 财务风险
- application [ref=f8e132]:
- generic [ref=f8e133]: 13案例记录
- generic [ref=f8e161]:
- heading "案例类型排行" [level=3] [ref=f8e163]
- application [ref=f8e168]:
- generic [ref=f8e207]:
- generic [ref=f8e208]:
- generic [ref=f8e209]: "0"
- generic [ref=f8e211]: "1"
- generic [ref=f8e213]: "2"
- generic [ref=f8e215]: "3"
- generic [ref=f8e217]: "4"
- generic [ref=f8e219]:
- generic [ref=f8e220]: 形式审查疏漏
- generic [ref=f8e222]: 评价机制失效
- generic [ref=f8e224]: 主数据同步异常
- generic [ref=f8e226]: 提醒失效
- generic [ref=f8e228]: 经验未复用
- generic [ref=f8e230]: 幂等控制缺失
- generic [ref=f8e232]: 权限超期
- generic [ref=f8e234]: 标签误分类
- generic [ref=f8e236]:
- generic [ref=f8e237]:
- heading "案例来源分布" [level=3] [ref=f8e239]
- application [ref=f8e244]:
- generic [ref=f8e283]:
- generic [ref=f8e284]:
- generic [ref=f8e285]: 招标复核
- generic [ref=f8e287]: 采购复盘
- generic [ref=f8e289]: 运营巡检
- generic [ref=f8e291]: 运营复盘
- generic [ref=f8e293]: 技术事故复盘
- generic [ref=f8e295]: 消保月度复盘
- generic [ref=f8e297]:
- generic [ref=f8e298]: "0"
- generic [ref=f8e300]: "1"
- generic [ref=f8e302]: "2"
- generic [ref=f8e304]: "3"
- generic [ref=f8e306]: "4"
- generic [ref=f8e308]:
- heading "关键字频次" [level=3] [ref=f8e310]
- application [ref=f8e315]:
- generic [ref=f8e354]:
- generic [ref=f8e355]:
- generic [ref=f8e356]: "0"
- generic [ref=f8e358]: "1"
- generic [ref=f8e360]: "2"
- generic [ref=f8e362]: "3"
- generic [ref=f8e364]: "4"
- generic [ref=f8e366]:
- generic [ref=f8e367]: 投标文件
- generic [ref=f8e369]: 盖章页
- generic [ref=f8e371]: 形式审查
- generic [ref=f8e373]: 供应商评价
- generic [ref=f8e375]: 履约管理
- generic [ref=f8e377]: 续签决策
- generic [ref=f8e379]: 印鉴卡
- generic [ref=f8e381]: 主数据同步
- generic [ref=f8e383]:
- heading "最近更新" [level=2] [ref=f8e384]
- grid "最近更新案例" [ref=f8e390]:
- rowgroup [ref=f8e391]:
- row [ref=f8e392]:
- columnheader [ref=f8e393]:
- text: 案例标题
- slider "尺寸调整器 案例标题" [ref=f8e394]: "460"
- columnheader [ref=f8e395]:
- text: 分类
- slider "尺寸调整器 分类" [ref=f8e396]: "160"
- columnheader [ref=f8e397]:
- text: 标签
- slider "尺寸调整器 标签" [ref=f8e398]: "260"
- columnheader [ref=f8e399]:
- text: 更新时间
- slider "尺寸调整器 更新时间" [ref=f8e400]: "180"
- rowgroup [ref=f8e401]:
- row [ref=f8e402]:
- rowheader [ref=f8e403]:
- button "2026测试-投标文件盖章页缺页未识别案例 某投标文件扫描件在上传过程中遗漏了一页盖章页,但评审小组仅核对目录和总页数,未逐项检查关键签章位置,导致带缺页材料进入评审流程。虽然最终未中标,但事件反映出形式审查清单执行不严。" [ref=f8e404]:
- generic [ref=f8e405]: 2026测试-投标文件盖章页缺页未识别案例
- generic [ref=f8e406]: 某投标文件扫描件在上传过程中遗漏了一页盖章页,但评审小组仅核对目录和总页数,未逐项检查关键签章位置,导致带缺页材料进入评审流程。虽然最终未中标,但事件反映出形式审查清单执行不严。
- gridcell "未分类" [ref=f8e407]
- gridcell "招投标,AI批量造数-20260521-续批" [ref=f8e410]
- gridcell "2026-08-13 15:52" [ref=f8e413]
- row [ref=f8e415]:
- rowheader [ref=f8e416]:
- button "2026测试-供应商履约评价长期未更新案例 部分长期合作供应商的履约评价停留在上一年度,项目交付中的延期、返工和服务投诉未及时沉淀到评价结果,导致续签和复购决策缺少真实依据。问题说明履约反馈回路未真正嵌入采购管理。" [ref=f8e417]:
- generic [ref=f8e418]: 2026测试-供应商履约评价长期未更新案例
- generic [ref=f8e419]: 部分长期合作供应商的履约评价停留在上一年度,项目交付中的延期、返工和服务投诉未及时沉淀到评价结果,导致续签和复购决策缺少真实依据。问题说明履约反馈回路未真正嵌入采购管理。
- gridcell "治理风险" [ref=f8e420]
- gridcell "供应商评价,AI批量造数-20260521-续批" [ref=f8e423]
- gridcell "2026-06-15 12:19" [ref=f8e426]
- row [ref=f8e428]:
- rowheader [ref=f8e429]:
- button "2026测试-印鉴卡更新后柜面未同步案例 客户完成印鉴变更后,核心主数据已更新,但部分柜面终端缓存未及时刷新,导致经办人员按旧印鉴卡进行人工复核并误判异常。事件虽经复核纠正,但暴露出关键主数据变更后的前端同步机制不足。" [ref=f8e430]:
- generic [ref=f8e431]: 2026测试-印鉴卡更新后柜面未同步案例
- generic [ref=f8e432]: 客户完成印鉴变更后,核心主数据已更新,但部分柜面终端缓存未及时刷新,导致经办人员按旧印鉴卡进行人工复核并误判异常。事件虽经复核纠正,但暴露出关键主数据变更后的前端同步机制不足。
- gridcell "治理风险" [ref=f8e433]
- gridcell "印鉴管理,AI批量造数-20260521-续批" [ref=f8e436]
- gridcell "2026-06-15 12:19" [ref=f8e439]
- row [ref=f8e441]:
- rowheader [ref=f8e442]:
- button "2026测试-客户证照到期提醒触达失败案例 系统已配置客户证照到期前提醒,但由于短信渠道签名异常和邮件地址长期未维护,多个客户未收到续补资料通知。后续业务办理时集中出现证照过期问题,影响交易时效。该问题暴露出提醒机制仅关注“发出”,未验证“送达”和“处理”。" [ref=f8e443]:
- generic [ref=f8e444]: 2026测试-客户证照到期提醒触达失败案例
- generic [ref=f8e445]: 系统已配置客户证照到期前提醒,但由于短信渠道签名异常和邮件地址长期未维护,多个客户未收到续补资料通知。后续业务办理时集中出现证照过期问题,影响交易时效。该问题暴露出提醒机制仅关注“发出”,未验证“送达”和“处理”。
- gridcell "治理风险" [ref=f8e446]
- gridcell "证照管理,AI批量造数-20260521-续批" [ref=f8e449]
- gridcell "2026-06-15 12:19" [ref=f8e452]
- row [ref=f8e454]:
- rowheader [ref=f8e455]:
- button "2026测试-项目复盘结论未进入知识库案例 某重点项目在结束后形成了较完整的问题复盘和改进建议,但材料仅保存在项目群和个人网盘中,未录入统一知识库。后续相似项目重复踩坑,团队才发现既有经验未被有效复用。说明复盘机制缺乏沉淀动作和责任要求。" [ref=f8e456]:
- generic [ref=f8e457]: 2026测试-项目复盘结论未进入知识库案例
- generic [ref=f8e458]: 某重点项目在结束后形成了较完整的问题复盘和改进建议,但材料仅保存在项目群和个人网盘中,未录入统一知识库。后续相似项目重复踩坑,团队才发现既有经验未被有效复用。说明复盘机制缺乏沉淀动作和责任要求。
- gridcell "知识管理风险" [ref=f8e459]
- gridcell "知识沉淀,AI批量造数-20260521-续批" [ref=f8e462]
- gridcell "2026-05-21 16:58" [ref=f8e465]
- row [ref=f8e467]:
- rowheader [ref=f8e468]:
- button "2026测试-系统批量导入失败重试无幂等案例 某业务系统批量导入任务在超时后被人工重复重试,因缺少批次幂等键,导致部分记录重复写入。后续清理耗费大量人工,也影响了下游报表准确性。问题说明批量任务失败处理设计不足。" [ref=f8e469]:
- generic [ref=f8e470]: 2026测试-系统批量导入失败重试无幂等案例
- generic [ref=f8e471]: 某业务系统批量导入任务在超时后被人工重复重试,因缺少批次幂等键,导致部分记录重复写入。后续清理耗费大量人工,也影响了下游报表准确性。问题说明批量任务失败处理设计不足。
- gridcell "信息技术风险" [ref=f8e472]
- gridcell "批量导入,AI批量造数-20260521-续批" [ref=f8e475]
- gridcell "2026-05-21 16:58" [ref=f8e478]
- button [ref=f8e480] [cursor=pointer]
- generic [ref=f8e481]: 投标文件
\ No newline at end of file
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
- UI 原语与 compound component 在页面或业务中立共享组合中直接导入 HeroUI/HeroUI Pro。 - UI 原语与 compound component 在页面或业务中立共享组合中直接导入 HeroUI/HeroUI Pro。
- 共享组合只可封装重复布局、可访问性与当前 HeroUI value/selection/event 模型,不得复刻 Catalyst props、DOM event 或旧 CSS hook。 - 共享组合只可封装重复布局、可访问性与当前 HeroUI value/selection/event 模型,不得复刻 Catalyst props、DOM event 或旧 CSS hook。
- DataGrid 组合映射列定义并转发原生 selection/fixed-column/ActionBar/Pagination;案例列表分页容量固定为 20,不展示容量选择器。 - DataGrid 组合映射列定义并转发原生 selection/fixed-column/ActionBar/Pagination;宽表以共享 `.data-table-scroll` 为唯一滚动容器并固定首业务列和操作列,案例列表分页默认 20 条并提供 20/50/100 条数选择。
- `DataTablePagination` 的页容量选择只将下一个数值传给页面既有 `onPageSizeChange`;列表页面负责将页码置零并使用原 `loadCaseList` 请求,组件不持有业务查询状态。
- 文件上传采用 HeroUI Pro DropZone 的 `File[]` 协议;页面边界继续只取既有单文件值。 - 文件上传采用 HeroUI Pro DropZone 的 `File[]` 协议;页面边界继续只取既有单文件值。
- 应用切换直接组合 HeroUI Modal/Card/Surface/Avatar 与 HeroUI Pro EmptyState;分组只读取现有 `SiteContext` 的 `/info -> site.group`,应用仍合并 `/application/info` 与当前用户权限数据,不引入硬编码分组或应用。 - 应用切换直接组合 HeroUI Modal/Card/Surface/Avatar 与 HeroUI Pro EmptyState;分组只读取现有 `SiteContext` 的 `/info -> site.group`,应用仍合并 `/application/info` 与当前用户权限数据,不引入硬编码分组或应用。
...@@ -18,7 +19,7 @@ ...@@ -18,7 +19,7 @@
## 样式所有权 ## 样式所有权
`src/design-system/theme.css` 是不可变主题权威;`tokens.css`、`shell.css`、`tailwind.css` 与 `global.less` 使用“问题整改”实际副本。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。 `src/design-system/theme.css` 与 `tokens.css` 直接同步审计项目管理当前版本;`shell.css`、`tailwind.css` 与 `global.less` 使用同一共享结构规则,并保留当前项目的 Sidebar 状态、固定列和 Autocomplete 受控值扩展。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。
本地浏览器验收产物目录 `.playwright-cli/` 与 `test-results/` 必须保持在 `.gitignore` 中。Tailwind CSS v4 会自动探测未忽略的项目文件;若验收日志进入扫描范围,CSS HMR 日志写入会反向触发持续编译,造成样式表和字体反复重载。 本地浏览器验收产物目录 `.playwright-cli/` 与 `test-results/` 必须保持在 `.gitignore` 中。Tailwind CSS v4 会自动探测未忽略的项目文件;若验收日志进入扫描范围,CSS HMR 日志写入会反向触发持续编译,造成样式表和字体反复重载。
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
## 页面迁移 ## 页面迁移
- [x] Login:问题整改几何、字段节奏、验证码、filled/autofill、浅/深色 - [x] Login:审计项目管理几何、字段节奏、验证码和 filled/autofill
- [x] Shell:Sidebar/Navbar/退出/应用切换/主题/offcanvas(代码结构) - [x] Shell:Sidebar/Navbar/退出/应用切换/offcanvas(审计项目管理结构)
- [x] 应用切换:同步 `model_analysis` Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构) - [x] 应用切换:同步审计项目管理 Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构)
- [x] 驾驶舱:Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltip(代码结构) - [x] 驾驶舱:Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltip;环形扇区间隙/10px 扇区圆角、柱图 8px 末端圆角与 24px 分析模块圆角(代码结构)
- [x] 案例管理:Tabs、筛选工具栏、DataGrid、Pagination、ActionBar、EmptyState(代码结构) - [x] 案例管理:Tabs、筛选工具栏、DataGrid、Pagination、ActionBar、EmptyState(代码结构)
- [x] 弹窗:新建、查看/编辑、批量修改、导入、删除确认(代码结构) - [x] 弹窗:新建、查看/编辑、批量修改、导入、删除确认(代码结构)
- [x] 清除 Catalyst/Headless/ECharts/原生可见控件与页面私有视觉 CSS - [x] 清除 Catalyst/Headless/ECharts/原生可见控件与页面私有视觉 CSS
...@@ -34,16 +34,17 @@ ...@@ -34,16 +34,17 @@
- [x] Login 768x1024 light/dark - [x] Login 768x1024 light/dark
- [x] Login 390x844 light/dark - [x] Login 390x844 light/dark
- [x] 应用切换 1440x900、1280x720、768x1024、390x844 light/dark:尺寸、两列/单列、动态分组、全部应用回退、唯一当前态、Body 滚动、Escape 与焦点回归 - [x] 应用切换 1440x900、1280x720、768x1024、390x844 light/dark:尺寸、两列/单列、动态分组、全部应用回退、唯一当前态、Body 滚动、Escape 与焦点回归
- [ ] 打开 Sidebar、用户退出菜单、应用切换真实动态分组、Dropdown/ListBox/DatePicker - [x] 打开 Sidebar、用户退出菜单、应用切换真实动态分组、分页 ListBox
- [ ] 验证 KPI 动态状态与图表 Tooltip - [ ] 验证 KPI 动态状态与图表 Tooltip
- [x] 目标与问题整改在 1440x900、1280x720、768x1024、390x844 完成浅/深色成对截图,页面无 viewport 级横向溢出 - [x] 历史目标验收在 1440x900、1280x720、768x1024、390x844 完成截图,页面无 viewport 级横向溢出
- [x] 真实验证 Tabs、三项折叠筛选、Checkbox、DataGrid、Pagination、ActionBar、详情查看/编辑/取消和移动端通知空态 - [x] 真实验证 Tabs、三项折叠筛选、Checkbox、DataGrid、Pagination、ActionBar、详情查看/编辑/取消和移动端通知空态
- [x] 目标与基准最终控制台 0 error / 0 warning - [x] 宽表外层滚动、首业务列/操作列固定、分页 20/50/100 条数选择、详情画布同色与导入默认 Surface
- [x] 目标最终控制台 0 error / 0 warning
- [ ] 验证 shortTitle true/false、单/多值附件、材料选择、本地上传、取消、失败与重试 - [ ] 验证 shortTitle true/false、单/多值附件、材料选择、本地上传、取消、失败与重试
- [ ] 验证通知未读 0/1/99/100、单条/全部已读、动态操作与站内外跳转 - [ ] 验证通知未读 0/1/99/100、单条/全部已读、动态操作与站内外跳转
未勾选原因:真实 View 接口返回空数组,Subject 没有 file 字段,通知接口返回 0 条;未伪造后台配置、通知或业务数据。 未勾选原因:真实 View 接口返回空数组,Subject 没有 file 字段,通知接口返回 0 条;未伪造后台配置、通知或业务数据。
- [x] Login 控制台 0 runtime/slot/provider/accessibility 错误或警告 - [ ] 本次主题同步后重新验证受鉴权页面:当前被服务端验证码拒绝,未伪造登录会话
## 最终门槛 ## 最终门槛
...@@ -56,4 +57,4 @@ ...@@ -56,4 +57,4 @@
- [x] HeroUI/React Aria 依赖树唯一 - [x] HeroUI/React Aria 依赖树唯一
- [x] HeroUI Pro 必需子路径通过 ESM 实际导入 - [x] HeroUI Pro 必需子路径通过 ESM 实际导入
应用切换目标页打开态已验收;`model_analysis` 隔离会话未登录,无法对其打开态做同屏像素测量。其余未勾选项仍需使用真实业务状态验收,不得使用伪造 token、网络数据或测试数字替代。 应用切换与数据表目标页打开态已在历史真实会话中验收;本次主题同步后的受鉴权复核仍需有效验证码,不得使用伪造 token、网络数据或测试数字替代。
# 鑫元设计系统 # 鑫元设计系统
本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“问题整改”应用是结构、尺寸、间距、组件层级与交互的唯一实现基准。 本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“审计项目管理”应用是当前 Shell、列表、弹窗与应用切换的结构、尺寸、间距、组件层级与交互基准。
## 依赖与样式 ## 依赖与样式
- 基础控件直接从 `@heroui/react` 导入。 - 基础控件直接从 `@heroui/react` 导入。
- Sidebar、Navbar、DataGrid、ActionBar、EmptyState、DropZone、KPI 与图表从 `@heroui-pro/react` 对应子路径直接导入。 - Sidebar、Navbar、DataGrid、ActionBar、EmptyState、DropZone、KPI 与图表从 `@heroui-pro/react` 对应子路径直接导入。
- 样式顺序固定为 Tailwind、HeroUI、HeroUI Pro、`theme.css`、`tokens.css`、批准的共享布局层。 - 样式顺序固定为 Tailwind、HeroUI、HeroUI Pro、`theme.css`、`tokens.css`、批准的共享布局层。
- `theme.css` 必须与 Skill 权威副本逐字节一致;页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。 - 本轮 `theme.css` 与 `tokens.css` 必须与审计项目管理当前版本逐字节一致;页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。
- 页面 `className` 只承担 flex、grid、宽度、间距、对齐和响应式布局。 - 页面 `className` 只承担 flex、grid、宽度、间距、对齐和响应式布局。
## 页面规则 ## 页面规则
- Shell 使用 HeroUI Pro offcanvas Sidebar 与 Navbar;页面和 Navbar 使用 48/32/16px 对称 gutter。 - Shell 使用 HeroUI Pro offcanvas Sidebar 与 Navbar;页面和 Navbar 使用 48/32/16px 对称 gutter。
- 应用切换严格使用 `model_analysis` 的 HeroUI Modal 基准:46rem 宽、42rem 最大高度、28px inset、Body 内滚动,小屏单列且 `sm` 及以上固定两列;分组读取 `/info -> site.group`,按真实配置顺序展示分组与数量,未分组应用进入“其他应用”,无分组配置时使用“全部应用”。 - 应用切换严格使用审计项目管理的 HeroUI Modal 基准:46rem 宽、42rem 最大高度、28px inset、Body 内滚动,小屏单列且 `sm` 及以上固定两列;分组读取 `/info -> site.group`,按真实配置顺序展示分组与数量,未分组应用进入“其他应用”,无分组配置时使用“全部应用”。
- 结构化筛选使用 HeroUI Surface,内部控件使用 `secondary`;关键词搜索位于列表工具栏左侧。 - 结构化筛选使用 HeroUI Surface,内部控件使用 `secondary`;关键词搜索位于列表工具栏左侧。
- 列表使用 HeroUI Pro DataGrid 原生选择、排序、固定列、ActionBar、EmptyState 与 compound Pagination。 - 列表使用 HeroUI Pro DataGrid 原生选择、排序、固定列、ActionBar、EmptyState 与 Pagination;宽表由共享 `.data-table-scroll` 承担横纵向滚动,首业务列及操作列固定,分页保留真实 20/50/100 条数选择。
- 详情和内容型表单使用鑫元 4xl(1088px)Modal:HeroUI `Modal.Container size="lg"` 保留官方交互与响应式结构,`Modal.Dialog` 通过共享布局类限制为 `sm:max-w-[68rem]`;不得把 HeroUI 不支持的 `4xl` 直接传给 Container。查看态只有一个 `Surface secondary` 字段组,编辑态直接放置 `secondary` 控件。 - 页容量选择使用现有 HeroUI Autocomplete 的受控 value 合同;切换容量只重置列表页码并复用既有查询回调,不改变筛选、排序或服务请求协议。
- 详情和内容型表单使用鑫元 4xl(1088px)Modal:HeroUI `Modal.Container size="lg"` 保留官方交互与响应式结构,`Modal.Dialog` 通过共享布局类限制为 `bg-background p-7 sm:max-w-[68rem]`,使详情背景与页面画布一致;查看态不添加灰色嵌套 Surface,编辑态直接放置 `secondary` 控件。
- 指标必须使用 HeroUI Pro KPI compound API;图表必须使用 Pro PieChart、BarChart、ComposedChart 与 ChartTooltipContent。 - 指标必须使用 HeroUI Pro KPI compound API;图表必须使用 Pro PieChart、BarChart、ComposedChart 与 ChartTooltipContent。
- 图表业务序列只使用 `--rc-chart-*` Token,不使用 UI 状态色或页面硬编码颜色。 - 图表业务序列只使用 `--rc-chart-*` Token,不使用 UI 状态色或页面硬编码颜色。
- 环形图通过共享 `--xinyuan-chart-sector-gap`、`--xinyuan-chart-sector-radius` 呈现扇区间隙与 10px 扇区圆角;柱图通过共享图表几何统一使用 8px 末端圆角。分析卡片沿用 `.rc-dashboard-card` 的 24px 外圆角。
## 跨应用能力 ## 跨应用能力
......
# 鑫元 UI 审计 # 鑫元 UI 审计
审计日期:2026-08-27。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。整体实现基准为“问题整改”,应用切换专项基准为 `model_analysis` 当前 HeroUI 实现。 审计日期:2026-09-01。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。本轮 Shell、列表、弹窗和应用切换的唯一实现基准为“审计项目管理”。
## 安装与运行基线 ## 安装与运行基线
- 官方安装命令已在项目根目录执行成功,`@heroui-pro/react` 解析为本项目 `node_modules` 内的 `1.0.0-beta.8`,不是符号链接或跨项目 `file:` 依赖。 - 官方安装命令已在项目根目录执行成功,`@heroui-pro/react` 解析为本项目 `node_modules` 内的 `1.0.0-beta.8`,不是符号链接或跨项目 `file:` 依赖。
- 项目使用 npm,保留 `package-lock.json`;旧 `pnpm-lock.yaml` 已删除。 - 项目使用 npm,保留 `package-lock.json`;旧 `pnpm-lock.yaml` 已删除。
- 当前迁移实例运行于 `http://localhost:8000/risk_case_library/`。 - 当前迁移实例运行于 `http://localhost:8003/risk_case_library/`。
- “问题整改”基准运行于 `http://localhost:8001/problem_rectification_new/`。 - “审计项目管理”源代码基准位于 `/Users/liuchenlong/Desktop/job/audit_project_manager`。
- 应用切换专项基准 `model_analysis` 运行于 `http://localhost:8002/model_analysis/`;该隔离浏览器会话停留在登录页,因此没有把未登录状态冒充为打开态对照。
## 业务契约 ## 业务契约
...@@ -22,14 +21,14 @@ ...@@ -22,14 +21,14 @@
| 范围 | 当前结果 | 迁移要求 | | 范围 | 当前结果 | 迁移要求 |
| --- | --- | --- | | --- | --- | --- |
| Login | HeroUI;结构、尺寸、字段节奏与问题整改一致;可访问性告警已消除 | 四视口浅/深色对照通过 | | Login | HeroUI;结构、尺寸、字段节奏与审计项目管理一致;可访问性告警已消除 | 固定浅色 Shell;本次复核受服务端验证码阻塞 |
| Shell | HeroUI Pro Sidebar/Navbar,offcanvas、主题、用户退出与应用切换保留既有业务回调 | 代码与共享基准一致;登录后浏览器状态待真实凭证 | | Shell | HeroUI Pro Sidebar/Navbar,offcanvas、主题、用户退出与应用切换保留既有业务回调 | 代码与共享基准一致;登录后浏览器状态待真实凭证 |
| 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 目标应用已在四视口浅/深色真实打开验收;动态分组和无配置回退均由真实会话覆盖,专项基准打开态因未登录仍不可复测 | | 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 与审计项目管理的两列/移动端单列结构一致 |
| 驾驶舱 | HeroUI Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltipContent | 构建通过;真实图表 Tooltip 待登录验收 | | 驾驶舱 | HeroUI Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltipContent | 环形图使用共享扇区间隙与 10px 圆角;两类柱图使用 8px 末端圆角;外层分析 Card 为 24px。真实图表 Tooltip 待登录验收 |
| 案例管理 | HeroUI Surface/Tabs/TextField/Autocomplete、Pro DataGrid/ActionBar/EmptyState | 默认 20、20/50/100 分页口径已保护;交互待登录验收 | | 案例管理 | HeroUI Surface/Tabs/TextField/Autocomplete、Pro DataGrid/ActionBar/EmptyState | 默认 20、20/50/100 分页口径已保护;交互待登录验收 |
| 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,查看态使用单一 secondary Surface | 新建弹窗宽度已按问题整改基准修正;其他打开态待登录验收 | | 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,详情 Dialog 使用 `bg-background` 并取消查看态灰色嵌套 Surface | 真实打开态已确认 Dialog 与页面画布同色 |
| 旧组件 | Catalyst、Headless、ECharts、旧 Select/Date/Table 兼容层已移除 | 活动源码扫描为零 | | 旧组件 | Catalyst、Headless、旧 Select/Date/Table 兼容层已移除 | 活动源码无旧 UI 导入;图表沿用审计项目管理的 HeroUI Pro + Recharts primitive 组合 |
| 样式 | `theme.css` 匹配 Skill;`tokens.css`、`shell.css`、`tailwind.css`、`global.less` 匹配问题整改 | `portable.css` 与页面私有视觉 CSS 已移除 | | 样式 | `theme.css`、`tokens.css` 已与审计项目管理逐字节一致;`shell.css` 保留当前项目所需的 Sidebar 状态、固定列和 Autocomplete 交互扩展 | `portable.css` 与页面私有视觉 CSS 已移除 |
## 四项功能审计 ## 四项功能审计
...@@ -48,13 +47,18 @@ View 配置审计确认本次没有实现权限功能:活动代码不读取 `v ...@@ -48,13 +47,18 @@ View 配置审计确认本次没有实现权限功能:活动代码不读取 `v
- 不在源码或浏览器中加入测试记录、伪造 token 或统计数字。 - 不在源码或浏览器中加入测试记录、伪造 token 或统计数字。
- 应用切换已使用现有真实登录会话完成浅/深色四视口验收;未注入 token、应用、分组或业务数据。其他模块的既有未验收项仍按迁移清单保留,不能据此标记全量应用迁移完成。 - 应用切换已使用现有真实登录会话完成浅/深色四视口验收;未注入 token、应用、分组或业务数据。其他模块的既有未验收项仍按迁移清单保留,不能据此标记全量应用迁移完成。
## 2026-08-27 浏览器验收 ## 2026-09-01 浏览器验收
- 使用账号 Andy 经真实验证码登录目标应用和“问题整改”,未注入 token、通知、View、附件或测试记录。 - 历史验收使用账号 Andy 经真实验证码登录目标应用,未注入 token、通知、View、附件或测试记录。
- 在 1440x900、1280x720、768x1024、390x844 分别采集两应用浅色和深色截图。各视口目标页面 `body.scrollWidth === innerWidth`,宽表滚动限制在 DataGrid 内;桌面 Sidebar/Navbar 为 240px/64px,移动端 Main 与 Navbar 均占满视口。 - 在 1440x900、1280x720、768x1024、390x844 分别采集两应用浅色和深色截图。各视口目标页面 `body.scrollWidth === innerWidth`,宽表滚动限制在 DataGrid 内;桌面 Sidebar/Navbar 为 240px/64px,移动端 Main 与 Navbar 均占满视口。
- 对照确认两应用在相同视口下的 Sidebar、Navbar、Trigger、Bell、主题按钮、页面内容边界、Tabs、筛选 Surface 与工具栏层级一致。 - 对照确认目标与当时的基准在相同视口下的 Sidebar、Navbar、Trigger、Bell、页面内容边界、Tabs、筛选 Surface 与工具栏层级一致。
- 目标真实验证:筛选默认三项与 Disclosure、卡片/表格 Tabs、DataGrid 固定操作列、Checkbox、ActionBar、固定 20 条 Pagination、详情 4xl Modal、查看态单一 secondary Surface、编辑 dirty、取消恢复且无 PUT、移动端通知空态、移动端 Sidebar、应用切换分组/计数/当前态。 - 目标真实验证:筛选默认三项与 Disclosure、卡片/表格 Tabs、DataGrid 固定操作列、Checkbox、ActionBar、固定 20 条 Pagination、详情 4xl Modal、查看态单一 secondary Surface、编辑 dirty、取消恢复且无 PUT、移动端通知空态、移动端 Sidebar、应用切换分组/计数/当前态。
- 目标与基准最终控制台均为 0 error、0 warning(忽略开发模式 React DevTools info)。 - 目标与基准最终控制台均为 0 error、0 warning(忽略开发模式 React DevTools info)。
- 当前真实接口 `GET /subject/risk_case_library/view` 返回空数组,因此没有 shortTitle true/false 配置可触发;代码路径与配置失败完整字段回退已审计,但未把 Mock 当作浏览器通过。 - 当前真实接口 `GET /subject/risk_case_library/view` 返回空数组,因此没有 shortTitle true/false 配置可触发;代码路径与配置失败完整字段回退已审计,但未把 Mock 当作浏览器通过。
- 当前 Subject 的 11 个字段均非 `file`,无法在真实页面触发单值/多值、材料选择、本地上传、取消、失败与重试。 - 当前 Subject 的 11 个字段均非 `file`,无法在真实页面触发单值/多值、材料选择、本地上传、取消、失败与重试。
- 当前通知列表和未读数均为 0,可验证空态与 Badge 隐藏;1/99/100、单条/全部已读、动态 actions 和站内外目标没有真实通知数据可操作。 - 当前通知列表和未读数均为 0,可验证空态与 Badge 隐藏;1/99/100、单条/全部已读、动态 actions 和站内外目标没有真实通知数据可操作。
- 宽表在 1440x900 下通过真实 `scrollLeft=700` 验证:外层 `.data-table-scroll` 为唯一横向滚动容器(1096px viewport / 2056px content),首业务列和右侧操作列保持固定且固定单元格背景不透明。
- 详情 Modal 与页面主画布曾计算为同一背景;Excel 导入初始上传区为单一 `Surface default`,未出现灰色嵌套预览 Surface。
- 审计项目管理基准的应用切换在 1440x900 实测为 736px Dialog、两列 332px 卡片;目标同一会话得到相同 46rem 两列结构。目标移动端 768x1024/390x844 的 Sidebar 是 500px/312px HeroUI offcanvas Dialog,均无 viewport 级横向溢出。
- 页容量选择器在真实列表中从 20 条/页切换到 50 条/页,页面回到第一页并保持原查询数据;1280x720、768x1024 视口均满足 `body.scrollWidth === innerWidth`。本轮控制台检查为 0 error、0 warning。
- 本次以审计项目管理为唯一基准同步主题后,`--background`、`--field-background` 和 `--accent` 已在目标登录页与基准登录页计算为相同值。目标服务端连续返回“验证码错误”,故没有伪造会话来重新打开受鉴权页面;登录失败产生的 3 条请求错误不计入 UI 控制台验收。
- generic [ref=f11e1]:
- complementary [ref=f11e3]:
- generic [ref=f11e5]:
- img "鑫元" [ref=f11e7]
- generic [ref=f11e8]: 警示案例库
- button "切换应用" [ref=f11e11] [cursor=pointer]
- treegrid "案例库工作台" [ref=f11e15]:
- row "驾驶舱" [level=1] [ref=f11e16] [cursor=pointer]:
- gridcell "驾驶舱" [ref=f11e17]
- row "案例管理" [level=1] [ref=f11e23] [cursor=pointer]:
- gridcell "案例管理" [ref=f11e24]
- button "打开用户菜单" [ref=f11e31] [cursor=pointer]:
- generic [ref=f11e32]: A
- generic [ref=f11e34]:
- generic [ref=f11e35]: Andy
- generic [ref=f11e36]: andy
- main [ref=f11e37]:
- navigation [ref=f11e38]:
- generic [ref=f11e39]:
- button "收起侧栏" [ref=f11e40] [cursor=pointer]
- heading "案例管理" [level=1] [ref=f11e41]
- button "通知消息" [ref=f11e44] [cursor=pointer]
- generic [ref=f11e47]:
- heading "案例管理" [level=1] [ref=f11e48]
- generic [ref=f11e49]:
- tablist "案例风险分类" [ref=f11e54]:
- tab "全部案例" [selected] [ref=f11e55] [cursor=pointer]
- tab "治理风险" [ref=f11e58] [cursor=pointer]
- tab "信息安全风险" [ref=f11e60] [cursor=pointer]
- tab "服务风险" [ref=f11e62] [cursor=pointer]
- tab "财务风险" [ref=f11e64] [cursor=pointer]
- tab "未分类" [ref=f11e66] [cursor=pointer]
- tab "知识管理风险" [ref=f11e68] [cursor=pointer]
- tab "信息技术风险" [ref=f11e70] [cursor=pointer]
- tab "反洗钱风险" [ref=f11e72] [cursor=pointer]
- tab "合同风险" [ref=f11e74] [cursor=pointer]
- tab "合规风险" [ref=f11e76] [cursor=pointer]
- generic [ref=f11e80]:
- generic [ref=f11e81]:
- generic [ref=f11e82]:
- generic [ref=f11e83]: 案例来源
- textbox "案例来源" [ref=f11e84]:
- /placeholder: 例如:监管通报
- generic [ref=f11e85]:
- generic [ref=f11e86]: 案例类型
- textbox "案例类型" [ref=f11e87]:
- /placeholder: 例如:审批绕行
- generic [ref=f11e88]:
- generic [ref=f11e89]: 问题属性
- textbox "问题属性" [ref=f11e90]:
- /placeholder: 例如:流程绕行
- button "展开" [ref=f11e92] [cursor=pointer]
- generic [ref=f11e95]:
- generic [ref=f11e96]:
- group [ref=f11e99]:
- searchbox "关键词搜索" [ref=f11e100]
- button "清空关键词"
- tablist "案例展示方式" [ref=f11e104]:
- tab "卡片视图" [ref=f11e105] [cursor=pointer]
- tab "表格视图" [active] [selected] [ref=f11e107] [cursor=pointer]
- generic [ref=f11e108]:
- button "Excel 批量导出" [ref=f11e109] [cursor=pointer]
- button "Excel 批量导入" [ref=f11e110] [cursor=pointer]
- button "手动新建案例" [ref=f11e111] [cursor=pointer]
- generic [ref=f11e112]:
- grid "全部案例" [ref=f11e386]:
- rowgroup [ref=f11e387]:
- row [ref=f11e388]:
- columnheader [ref=f11e389]:
- checkbox "Select all" [ref=f11e393]
- columnheader [ref=f11e398]:
- text: 标题
- slider "尺寸调整器 标题" [ref=f11e399]: "380"
- columnheader [ref=f11e400]:
- text: 分类
- slider "尺寸调整器 分类" [ref=f11e401]: "180"
- columnheader [ref=f11e402]:
- text: 标签
- slider "尺寸调整器 标签" [ref=f11e403]: "240"
- columnheader [ref=f11e404]:
- text: 案例类型
- slider "尺寸调整器 案例类型" [ref=f11e405]: "220"
- columnheader [ref=f11e406]:
- text: 关键字
- slider "尺寸调整器 关键字" [ref=f11e407]: "280"
- columnheader [ref=f11e408]:
- text: 案例来源
- slider "尺寸调整器 案例来源" [ref=f11e409]: "180"
- columnheader [ref=f11e410]:
- text: 案例等级
- slider "尺寸调整器 案例等级" [ref=f11e411]: "140"
- columnheader [ref=f11e412]:
- text: 案例年份
- slider "尺寸调整器 案例年份" [ref=f11e413]: "140"
- columnheader [ref=f11e414]:
- text: 更新时间
- slider "尺寸调整器 更新时间" [ref=f11e415]: "160"
- columnheader [ref=f11e416]:
- text: 操作
- slider "尺寸调整器 操作" [ref=f11e417]: "96"
- rowgroup [ref=f11e418]:
- row [ref=f11e419]:
- gridcell [ref=f11e420]:
- checkbox "Select row 2026测试-投标文件盖章页缺页未识别案例" [ref=f11e424]
- rowheader [ref=f11e429]:
- button "2026测试-投标文件盖章页缺页未识别案例" [ref=f11e430]
- gridcell [ref=f11e431]:
- button "-" [ref=f11e432]
- gridcell [ref=f11e433]:
- button "招投标,AI批量造数-20260521-续批" [ref=f11e434]
- gridcell [ref=f11e435]:
- button "形式审查疏漏" [ref=f11e436]
- gridcell [ref=f11e437]:
- button "投标文件、盖章页、形式审查" [ref=f11e438]
- gridcell [ref=f11e439]:
- button "招标复核" [ref=f11e440]
- gridcell [ref=f11e441]:
- button "中" [ref=f11e442]
- gridcell [ref=f11e443]:
- button "2025" [ref=f11e444]
- gridcell [ref=f11e445]:
- button "2026-08-13 15:52" [ref=f11e446]
- gridcell [ref=f11e447]:
- button "查看详情" [ref=f11e449] [cursor=pointer]
- row [ref=f11e451]:
- gridcell [ref=f11e452]:
- checkbox "Select row 2026测试-供应商履约评价长期未更新案例" [ref=f11e456]
- rowheader [ref=f11e461]:
- button "2026测试-供应商履约评价长期未更新案例" [ref=f11e462]
- gridcell [ref=f11e463]:
- button "治理风险" [ref=f11e464]
- gridcell [ref=f11e465]:
- button "供应商评价,AI批量造数-20260521-续批" [ref=f11e466]
- gridcell [ref=f11e467]:
- button "评价机制失效" [ref=f11e468]
- gridcell [ref=f11e469]:
- button "供应商评价、履约管理、续签决策" [ref=f11e470]
- gridcell [ref=f11e471]:
- button "采购复盘" [ref=f11e472]
- gridcell [ref=f11e473]:
- button "中" [ref=f11e474]
- gridcell [ref=f11e475]:
- button "2025" [ref=f11e476]
- gridcell [ref=f11e477]:
- button "2026-06-15 12:19" [ref=f11e478]
- gridcell [ref=f11e479]:
- button "查看详情" [ref=f11e481] [cursor=pointer]
- row [ref=f11e483]:
- gridcell [ref=f11e484]:
- checkbox "Select row 2026测试-印鉴卡更新后柜面未同步案例" [ref=f11e488]
- rowheader [ref=f11e493]:
- button "2026测试-印鉴卡更新后柜面未同步案例" [ref=f11e494]
- gridcell [ref=f11e495]:
- button "治理风险" [ref=f11e496]
- gridcell [ref=f11e497]:
- button "印鉴管理,AI批量造数-20260521-续批" [ref=f11e498]
- gridcell [ref=f11e499]:
- button "主数据同步异常" [ref=f11e500]
- gridcell [ref=f11e501]:
- button "印鉴卡、主数据同步、柜面核验" [ref=f11e502]
- gridcell [ref=f11e503]:
- button "运营巡检" [ref=f11e504]
- gridcell [ref=f11e505]:
- button "中" [ref=f11e506]
- gridcell [ref=f11e507]:
- button "2026" [ref=f11e508]
- gridcell [ref=f11e509]:
- button "2026-06-15 12:19" [ref=f11e510]
- gridcell [ref=f11e511]:
- button "查看详情" [ref=f11e513] [cursor=pointer]
- row [ref=f11e515]:
- gridcell [ref=f11e516]:
- checkbox "Select row 2026测试-客户证照到期提醒触达失败案例" [ref=f11e520]
- rowheader [ref=f11e525]:
- button "2026测试-客户证照到期提醒触达失败案例" [ref=f11e526]
- gridcell [ref=f11e527]:
- button "治理风险" [ref=f11e528]
- gridcell [ref=f11e529]:
- button "证照管理,AI批量造数-20260521-续批" [ref=f11e530]
- gridcell [ref=f11e531]:
- button "提醒失效" [ref=f11e532]
- gridcell [ref=f11e533]:
- button "证照到期、提醒触达、送达率" [ref=f11e534]
- gridcell [ref=f11e535]:
- button "运营复盘" [ref=f11e536]
- gridcell [ref=f11e537]:
- button "中" [ref=f11e538]
- gridcell [ref=f11e539]:
- button "2025" [ref=f11e540]
- gridcell [ref=f11e541]:
- button "2026-06-15 12:19" [ref=f11e542]
- gridcell [ref=f11e543]:
- button "查看详情" [ref=f11e545] [cursor=pointer]
- row [ref=f11e547]:
- gridcell [ref=f11e548]:
- checkbox "Select row 2026测试-项目复盘结论未进入知识库案例" [ref=f11e552]
- rowheader [ref=f11e557]:
- button "2026测试-项目复盘结论未进入知识库案例" [ref=f11e558]
- gridcell [ref=f11e559]:
- button "知识管理风险" [ref=f11e560]
- gridcell [ref=f11e561]:
- button "知识沉淀,AI批量造数-20260521-续批" [ref=f11e562]
- gridcell [ref=f11e563]:
- button "经验未复用" [ref=f11e564]
- gridcell [ref=f11e565]:
- button "项目复盘,知识库,经验复用" [ref=f11e566]
- gridcell [ref=f11e567]:
- button "项目后评估" [ref=f11e568]
- gridcell [ref=f11e569]:
- button "中" [ref=f11e570]
- gridcell [ref=f11e571]:
- button "2024" [ref=f11e572]
- gridcell [ref=f11e573]:
- button "2026-05-21 16:58" [ref=f11e574]
- gridcell [ref=f11e575]:
- button "查看详情" [ref=f11e577] [cursor=pointer]
- row [ref=f11e579]:
- gridcell [ref=f11e580]:
- checkbox "Select row 2026测试-系统批量导入失败重试无幂等案例" [ref=f11e584]
- rowheader [ref=f11e589]:
- button "2026测试-系统批量导入失败重试无幂等案例" [ref=f11e590]
- gridcell [ref=f11e591]:
- button "信息技术风险" [ref=f11e592]
- gridcell [ref=f11e593]:
- button "批量导入,AI批量造数-20260521-续批" [ref=f11e594]
- gridcell [ref=f11e595]:
- button "幂等控制缺失" [ref=f11e596]
- gridcell [ref=f11e597]:
- button "批量导入,幂等控制,重复写入" [ref=f11e598]
- gridcell [ref=f11e599]:
- button "技术事故复盘" [ref=f11e600]
- gridcell [ref=f11e601]:
- button "高" [ref=f11e602]
- gridcell [ref=f11e603]:
- button "2026" [ref=f11e604]
- gridcell [ref=f11e605]:
- button "2026-05-21 16:58" [ref=f11e606]
- gridcell [ref=f11e607]:
- button "查看详情" [ref=f11e609] [cursor=pointer]
- row [ref=f11e611]:
- gridcell [ref=f11e612]:
- checkbox "Select row 2026测试-访客门禁权限超过申请时长案例" [ref=f11e616]
- rowheader [ref=f11e621]:
- button "2026测试-访客门禁权限超过申请时长案例" [ref=f11e622]
- gridcell [ref=f11e623]:
- button "信息安全风险" [ref=f11e624]
- gridcell [ref=f11e625]:
- button "门禁管理,AI批量造数-20260521-续批" [ref=f11e626]
- gridcell [ref=f11e627]:
- button "权限超期" [ref=f11e628]
- gridcell [ref=f11e629]:
- button "访客门禁,权限超期,物理安全" [ref=f11e630]
- gridcell [ref=f11e631]:
- button "安保检查" [ref=f11e632]
- gridcell [ref=f11e633]:
- button "中" [ref=f11e634]
- gridcell [ref=f11e635]:
- button "2026" [ref=f11e636]
- gridcell [ref=f11e637]:
- button "2026-05-21 16:58" [ref=f11e638]
- gridcell [ref=f11e639]:
- button "查看详情" [ref=f11e641] [cursor=pointer]
- row [ref=f11e643]:
- gridcell [ref=f11e644]:
- checkbox "Select row 2026测试-客户投诉分类标签误用案例" [ref=f11e648]
- rowheader [ref=f11e653]:
- button "2026测试-客户投诉分类标签误用案例" [ref=f11e654]
- gridcell [ref=f11e655]:
- button "服务风险" [ref=f11e656]
- gridcell [ref=f11e657]:
- button "客户投诉,AI批量造数-20260521-续批" [ref=f11e658]
- gridcell [ref=f11e659]:
- button "标签误分类" [ref=f11e660]
- gridcell [ref=f11e661]:
- button "客户投诉,标签分类,统计失真" [ref=f11e662]
- gridcell [ref=f11e663]:
- button "消保月度复盘" [ref=f11e664]
- gridcell [ref=f11e665]:
- button "中" [ref=f11e666]
- gridcell [ref=f11e667]:
- button "2024" [ref=f11e668]
- gridcell [ref=f11e669]:
- button "2026-05-21 16:57" [ref=f11e670]
- gridcell [ref=f11e671]:
- button "查看详情" [ref=f11e673] [cursor=pointer]
- row [ref=f11e675]:
- gridcell [ref=f11e676]:
- checkbox "Select row 2026测试-异常交易预警处置备注模板化案例" [ref=f11e680]
- rowheader [ref=f11e685]:
- button "2026测试-异常交易预警处置备注模板化案例" [ref=f11e686]
- gridcell [ref=f11e687]:
- button "反洗钱风险" [ref=f11e688]
- gridcell [ref=f11e689]:
- button "异常交易,AI批量造数-20260521-续批" [ref=f11e690]
- gridcell [ref=f11e691]:
- button "处置质量不足" [ref=f11e692]
- gridcell [ref=f11e693]:
- button "异常交易,模板备注,复核质量" [ref=f11e694]
- gridcell [ref=f11e695]:
- button "反洗钱抽样检查" [ref=f11e696]
- gridcell [ref=f11e697]:
- button "高" [ref=f11e698]
- gridcell [ref=f11e699]:
- button "2026" [ref=f11e700]
- gridcell [ref=f11e701]:
- button "2026-05-21 16:57" [ref=f11e702]
- gridcell [ref=f11e703]:
- button "查看详情" [ref=f11e705] [cursor=pointer]
- row [ref=f11e707]:
- gridcell [ref=f11e708]:
- checkbox "Select row 2026测试-质保金释放条件核验不充分案例" [ref=f11e712]
- rowheader [ref=f11e717]:
- button "2026测试-质保金释放条件核验不充分案例" [ref=f11e718]
- gridcell [ref=f11e719]:
- button "合同风险" [ref=f11e720]
- gridcell [ref=f11e721]:
- button "质保金管理,AI批量造数-20260521-续批" [ref=f11e722]
- gridcell [ref=f11e723]:
- button "付款条件核验不足" [ref=f11e724]
- gridcell [ref=f11e725]:
- button "质保金,尾款释放,验收状态" [ref=f11e726]
- gridcell [ref=f11e727]:
- button "合同履约抽查" [ref=f11e728]
- gridcell [ref=f11e729]:
- button "高" [ref=f11e730]
- gridcell [ref=f11e731]:
- button "2025" [ref=f11e732]
- gridcell [ref=f11e733]:
- button "2026-05-21 16:57" [ref=f11e734]
- gridcell [ref=f11e735]:
- button "查看详情" [ref=f11e737] [cursor=pointer]
- row [ref=f11e739]:
- gridcell [ref=f11e740]:
- checkbox "Select row 2026测试-客服录音抽检覆盖率不足案例" [ref=f11e744]
- rowheader [ref=f11e749]:
- button "2026测试-客服录音抽检覆盖率不足案例" [ref=f11e750]
- gridcell [ref=f11e751]:
- button "服务风险" [ref=f11e752]
- gridcell [ref=f11e753]:
- button "客服质检,AI批量造数-20260521-续批" [ref=f11e754]
- gridcell [ref=f11e755]:
- button "抽检不足" [ref=f11e756]
- gridcell [ref=f11e757]:
- button "客服录音,抽检覆盖率,不规范承诺" [ref=f11e758]
- gridcell [ref=f11e759]:
- button "服务管理复盘" [ref=f11e760]
- gridcell [ref=f11e761]:
- button "中" [ref=f11e762]
- gridcell [ref=f11e763]:
- button "2024" [ref=f11e764]
- gridcell [ref=f11e765]:
- button "2026-05-21 16:57" [ref=f11e766]
- gridcell [ref=f11e767]:
- button "查看详情" [ref=f11e769] [cursor=pointer]
- row [ref=f11e771]:
- gridcell [ref=f11e772]:
- checkbox "Select row 2026测试-预算追加审批事后补录案例" [ref=f11e776]
- rowheader [ref=f11e781]:
- button "2026测试-预算追加审批事后补录案例" [ref=f11e782]
- gridcell [ref=f11e783]:
- button "财务风险" [ref=f11e784]
- gridcell [ref=f11e785]:
- button "预算控制,AI批量造数-20260521-续批" [ref=f11e786]
- gridcell [ref=f11e787]:
- button "事后审批" [ref=f11e788]
- gridcell [ref=f11e789]:
- button "预算追加,事后审批,支出承诺" [ref=f11e790]
- gridcell [ref=f11e791]:
- button "预算执行检查" [ref=f11e792]
- gridcell [ref=f11e793]:
- button "中" [ref=f11e794]
- gridcell [ref=f11e795]:
- button "2025" [ref=f11e796]
- gridcell [ref=f11e797]:
- button "2026-05-21 16:57" [ref=f11e798]
- gridcell [ref=f11e799]:
- button "查看详情" [ref=f11e801] [cursor=pointer]
- row [ref=f11e803]:
- gridcell [ref=f11e804]:
- checkbox "Select row 2026测试-供应商黑名单同步延迟案例" [ref=f11e808]
- rowheader [ref=f11e813]:
- button "2026测试-供应商黑名单同步延迟案例" [ref=f11e814]
- gridcell [ref=f11e815]:
- button "合规风险" [ref=f11e816]
- gridcell [ref=f11e817]:
- button "供应商管理,AI批量造数-20260521-续批" [ref=f11e818]
- gridcell [ref=f11e819]:
- button "名单同步缺失" [ref=f11e820]
- gridcell [ref=f11e821]:
- button "供应商黑名单,系统同步,下单校验" [ref=f11e822]
- gridcell [ref=f11e823]:
- button "采购合规检查" [ref=f11e824]
- gridcell [ref=f11e825]:
- button "高" [ref=f11e826]
- gridcell [ref=f11e827]:
- button "2026" [ref=f11e828]
- gridcell [ref=f11e829]:
- button "2026-05-21 16:57" [ref=f11e830]
- gridcell [ref=f11e831]:
- button "查看详情" [ref=f11e833] [cursor=pointer]
- row [ref=f11e835]:
- gridcell [ref=f11e836]:
- checkbox "Select row 2026测试-外包人员离场账号停用滞后案例" [ref=f11e840]
- rowheader [ref=f11e845]:
- button "2026测试-外包人员离场账号停用滞后案例" [ref=f11e846]
- gridcell [ref=f11e847]:
- button "信息安全风险" [ref=f11e848]
- gridcell [ref=f11e849]:
- button "账号停用,AI批量造数-20260521" [ref=f11e850]
- gridcell [ref=f11e851]:
- button "权限残留" [ref=f11e852]
- gridcell [ref=f11e853]:
- button "外包离场,账号停用,权限回收" [ref=f11e854]
- gridcell [ref=f11e855]:
- button "IT审计" [ref=f11e856]
- gridcell [ref=f11e857]:
- button "高" [ref=f11e858]
- gridcell [ref=f11e859]:
- button "2026" [ref=f11e860]
- gridcell [ref=f11e861]:
- button "2026-05-21 16:38" [ref=f11e862]
- gridcell [ref=f11e863]:
- button "查看详情" [ref=f11e865] [cursor=pointer]
- row [ref=f11e867]:
- gridcell [ref=f11e868]:
- checkbox "Select row 2026测试-营销活动费用分摊依据不清案例" [ref=f11e872]
- rowheader [ref=f11e877]:
- button "2026测试-营销活动费用分摊依据不清案例" [ref=f11e878]
- gridcell [ref=f11e879]:
- button "财务风险" [ref=f11e880]
- gridcell [ref=f11e881]:
- button "费用分摊,AI批量造数-20260521" [ref=f11e882]
- gridcell [ref=f11e883]:
- button "核算口径偏差" [ref=f11e884]
- gridcell [ref=f11e885]:
- button "费用分摊,管理会计,利润真实性" [ref=f11e886]
- gridcell [ref=f11e887]:
- button "经营分析复核" [ref=f11e888]
- gridcell [ref=f11e889]:
- button "中" [ref=f11e890]
- gridcell [ref=f11e891]:
- button "2024" [ref=f11e892]
- gridcell [ref=f11e893]:
- button "2026-05-21 16:38" [ref=f11e894]
- gridcell [ref=f11e895]:
- button "查看详情" [ref=f11e897] [cursor=pointer]
- generic [ref=f11e354]:
- generic [ref=f11e355]:
- generic [ref=f11e356]: 共 15 条,第 1 / 1 页
- generic [ref=f11e357]:
- group [ref=f11e358] [cursor=pointer]:
- generic "20 条/页" [ref=f11e360]
- button "20 条/页 每页条数"
- combobox [ref=f11e366]
- navigation "pagination" [ref=f11e367]:
- list [ref=f11e368]:
- listitem [ref=f11e369]:
- button "上一页" [disabled]
- listitem [ref=f11e370]:
- button "1" [ref=f11e371] [cursor=pointer]
- listitem [ref=f11e372]:
- button "下一页" [disabled]
- button [ref=f11e373] [cursor=pointer]
- button [ref=f11e374] [cursor=pointer]
- button [ref=f11e375] [cursor=pointer]
- button [ref=f11e376] [cursor=pointer]
- button [ref=f11e377] [cursor=pointer]
- button [ref=f11e378] [cursor=pointer]
- button [ref=f11e379] [cursor=pointer]
\ No newline at end of file
- generic [ref=f11e1]:
- complementary [ref=f11e3]:
- generic [ref=f11e5]:
- img "鑫元" [ref=f11e7]
- generic [ref=f11e8]: 警示案例库
- button "切换应用" [ref=f11e11] [cursor=pointer]
- treegrid "案例库工作台" [ref=f11e15]:
- row "驾驶舱" [level=1] [ref=f11e16] [cursor=pointer]:
- gridcell "驾驶舱" [ref=f11e17]
- row "案例管理" [level=1] [ref=f11e23] [cursor=pointer]:
- gridcell "案例管理" [ref=f11e24]
- button "打开用户菜单" [ref=f11e31] [cursor=pointer]:
- generic [ref=f11e32]: A
- generic [ref=f11e34]:
- generic [ref=f11e35]: Andy
- generic [ref=f11e36]: andy
- main [ref=f11e37]:
- navigation [ref=f11e38]:
- generic [ref=f11e39]:
- button "收起侧栏" [ref=f11e40] [cursor=pointer]
- heading "案例管理" [level=1] [ref=f11e41]
- button "通知消息" [ref=f11e44] [cursor=pointer]
- generic [ref=f11e47]:
- heading "案例管理" [level=1] [ref=f11e48]
- generic [ref=f11e49]:
- tablist "案例风险分类" [ref=f11e54]:
- tab "全部案例" [selected] [ref=f11e55] [cursor=pointer]
- tab "治理风险" [ref=f11e58] [cursor=pointer]
- tab "信息安全风险" [ref=f11e60] [cursor=pointer]
- tab "服务风险" [ref=f11e62] [cursor=pointer]
- tab "财务风险" [ref=f11e64] [cursor=pointer]
- tab "未分类" [ref=f11e66] [cursor=pointer]
- tab "知识管理风险" [ref=f11e68] [cursor=pointer]
- tab "信息技术风险" [ref=f11e70] [cursor=pointer]
- tab "反洗钱风险" [ref=f11e72] [cursor=pointer]
- tab "合同风险" [ref=f11e74] [cursor=pointer]
- tab "合规风险" [ref=f11e76] [cursor=pointer]
- generic [ref=f11e80]:
- generic [ref=f11e81]:
- generic [ref=f11e82]:
- generic [ref=f11e83]: 案例来源
- textbox "案例来源" [ref=f11e84]:
- /placeholder: 例如:监管通报
- generic [ref=f11e85]:
- generic [ref=f11e86]: 案例类型
- textbox "案例类型" [ref=f11e87]:
- /placeholder: 例如:审批绕行
- generic [ref=f11e88]:
- generic [ref=f11e89]: 问题属性
- textbox "问题属性" [ref=f11e90]:
- /placeholder: 例如:流程绕行
- button "展开" [ref=f11e92] [cursor=pointer]
- generic [ref=f11e95]:
- generic [ref=f11e96]:
- group [ref=f11e99]:
- searchbox "关键词搜索" [ref=f11e100]
- button "清空关键词"
- tablist "案例展示方式" [ref=f11e104]:
- tab "卡片视图" [ref=f11e105] [cursor=pointer]
- tab "表格视图" [active] [selected] [ref=f11e107] [cursor=pointer]
- generic [ref=f11e108]:
- button "Excel 批量导出" [ref=f11e109] [cursor=pointer]
- button "Excel 批量导入" [ref=f11e110] [cursor=pointer]
- button "手动新建案例" [ref=f11e111] [cursor=pointer]
- generic [ref=f11e112]:
- grid "全部案例" [ref=f11e386]:
- rowgroup [ref=f11e387]:
- row [ref=f11e388]:
- columnheader [ref=f11e389]:
- checkbox "Select all" [ref=f11e393]
- columnheader [ref=f11e398]:
- text: 标题
- slider "尺寸调整器 标题" [ref=f11e399]: "380"
- columnheader [ref=f11e400]:
- text: 分类
- slider "尺寸调整器 分类" [ref=f11e401]: "180"
- columnheader [ref=f11e402]:
- text: 标签
- slider "尺寸调整器 标签" [ref=f11e403]: "240"
- columnheader [ref=f11e404]:
- text: 案例类型
- slider "尺寸调整器 案例类型" [ref=f11e405]: "220"
- columnheader [ref=f11e406]:
- text: 关键字
- slider "尺寸调整器 关键字" [ref=f11e407]: "280"
- columnheader [ref=f11e408]:
- text: 案例来源
- slider "尺寸调整器 案例来源" [ref=f11e409]: "180"
- columnheader [ref=f11e410]:
- text: 案例等级
- slider "尺寸调整器 案例等级" [ref=f11e411]: "140"
- columnheader [ref=f11e412]:
- text: 案例年份
- slider "尺寸调整器 案例年份" [ref=f11e413]: "140"
- columnheader [ref=f11e414]:
- text: 更新时间
- slider "尺寸调整器 更新时间" [ref=f11e415]: "160"
- columnheader [ref=f11e416]:
- text: 操作
- slider "尺寸调整器 操作" [ref=f11e417]: "96"
- rowgroup [ref=f11e418]:
- row [ref=f11e419]:
- gridcell [ref=f11e420]:
- checkbox "Select row 2026测试-投标文件盖章页缺页未识别案例" [ref=f11e424]
- rowheader [ref=f11e429]:
- button "2026测试-投标文件盖章页缺页未识别案例" [ref=f11e430]
- gridcell [ref=f11e431]:
- button "-" [ref=f11e432]
- gridcell [ref=f11e433]:
- button "招投标,AI批量造数-20260521-续批" [ref=f11e434]
- gridcell [ref=f11e435]:
- button "形式审查疏漏" [ref=f11e436]
- gridcell [ref=f11e437]:
- button "投标文件、盖章页、形式审查" [ref=f11e438]
- gridcell [ref=f11e439]:
- button "招标复核" [ref=f11e440]
- gridcell [ref=f11e441]:
- button "中" [ref=f11e442]
- gridcell [ref=f11e443]:
- button "2025" [ref=f11e444]
- gridcell [ref=f11e445]:
- button "2026-08-13 15:52" [ref=f11e446]
- gridcell [ref=f11e447]:
- button "查看详情" [ref=f11e449] [cursor=pointer]
- row [ref=f11e451]:
- gridcell [ref=f11e452]:
- checkbox "Select row 2026测试-供应商履约评价长期未更新案例" [ref=f11e456]
- rowheader [ref=f11e461]:
- button "2026测试-供应商履约评价长期未更新案例" [ref=f11e462]
- gridcell [ref=f11e463]:
- button "治理风险" [ref=f11e464]
- gridcell [ref=f11e465]:
- button "供应商评价,AI批量造数-20260521-续批" [ref=f11e466]
- gridcell [ref=f11e467]:
- button "评价机制失效" [ref=f11e468]
- gridcell [ref=f11e469]:
- button "供应商评价、履约管理、续签决策" [ref=f11e470]
- gridcell [ref=f11e471]:
- button "采购复盘" [ref=f11e472]
- gridcell [ref=f11e473]:
- button "中" [ref=f11e474]
- gridcell [ref=f11e475]:
- button "2025" [ref=f11e476]
- gridcell [ref=f11e477]:
- button "2026-06-15 12:19" [ref=f11e478]
- gridcell [ref=f11e479]:
- button "查看详情" [ref=f11e481] [cursor=pointer]
- row [ref=f11e483]:
- gridcell [ref=f11e484]:
- checkbox "Select row 2026测试-印鉴卡更新后柜面未同步案例" [ref=f11e488]
- rowheader [ref=f11e493]:
- button "2026测试-印鉴卡更新后柜面未同步案例" [ref=f11e494]
- gridcell [ref=f11e495]:
- button "治理风险" [ref=f11e496]
- gridcell [ref=f11e497]:
- button "印鉴管理,AI批量造数-20260521-续批" [ref=f11e498]
- gridcell [ref=f11e499]:
- button "主数据同步异常" [ref=f11e500]
- gridcell [ref=f11e501]:
- button "印鉴卡、主数据同步、柜面核验" [ref=f11e502]
- gridcell [ref=f11e503]:
- button "运营巡检" [ref=f11e504]
- gridcell [ref=f11e505]:
- button "中" [ref=f11e506]
- gridcell [ref=f11e507]:
- button "2026" [ref=f11e508]
- gridcell [ref=f11e509]:
- button "2026-06-15 12:19" [ref=f11e510]
- gridcell [ref=f11e511]:
- button "查看详情" [ref=f11e513] [cursor=pointer]
- row [ref=f11e515]:
- gridcell [ref=f11e516]:
- checkbox "Select row 2026测试-客户证照到期提醒触达失败案例" [ref=f11e520]
- rowheader [ref=f11e525]:
- button "2026测试-客户证照到期提醒触达失败案例" [ref=f11e526]
- gridcell [ref=f11e527]:
- button "治理风险" [ref=f11e528]
- gridcell [ref=f11e529]:
- button "证照管理,AI批量造数-20260521-续批" [ref=f11e530]
- gridcell [ref=f11e531]:
- button "提醒失效" [ref=f11e532]
- gridcell [ref=f11e533]:
- button "证照到期、提醒触达、送达率" [ref=f11e534]
- gridcell [ref=f11e535]:
- button "运营复盘" [ref=f11e536]
- gridcell [ref=f11e537]:
- button "中" [ref=f11e538]
- gridcell [ref=f11e539]:
- button "2025" [ref=f11e540]
- gridcell [ref=f11e541]:
- button "2026-06-15 12:19" [ref=f11e542]
- gridcell [ref=f11e543]:
- button "查看详情" [ref=f11e545] [cursor=pointer]
- row [ref=f11e547]:
- gridcell [ref=f11e548]:
- checkbox "Select row 2026测试-项目复盘结论未进入知识库案例" [ref=f11e552]
- rowheader [ref=f11e557]:
- button "2026测试-项目复盘结论未进入知识库案例" [ref=f11e558]
- gridcell [ref=f11e559]:
- button "知识管理风险" [ref=f11e560]
- gridcell [ref=f11e561]:
- button "知识沉淀,AI批量造数-20260521-续批" [ref=f11e562]
- gridcell [ref=f11e563]:
- button "经验未复用" [ref=f11e564]
- gridcell [ref=f11e565]:
- button "项目复盘,知识库,经验复用" [ref=f11e566]
- gridcell [ref=f11e567]:
- button "项目后评估" [ref=f11e568]
- gridcell [ref=f11e569]:
- button "中" [ref=f11e570]
- gridcell [ref=f11e571]:
- button "2024" [ref=f11e572]
- gridcell [ref=f11e573]:
- button "2026-05-21 16:58" [ref=f11e574]
- gridcell [ref=f11e575]:
- button "查看详情" [ref=f11e577] [cursor=pointer]
- row [ref=f11e579]:
- gridcell [ref=f11e580]:
- checkbox "Select row 2026测试-系统批量导入失败重试无幂等案例" [ref=f11e584]
- rowheader [ref=f11e589]:
- button "2026测试-系统批量导入失败重试无幂等案例" [ref=f11e590]
- gridcell [ref=f11e591]:
- button "信息技术风险" [ref=f11e592]
- gridcell [ref=f11e593]:
- button "批量导入,AI批量造数-20260521-续批" [ref=f11e594]
- gridcell [ref=f11e595]:
- button "幂等控制缺失" [ref=f11e596]
- gridcell [ref=f11e597]:
- button "批量导入,幂等控制,重复写入" [ref=f11e598]
- gridcell [ref=f11e599]:
- button "技术事故复盘" [ref=f11e600]
- gridcell [ref=f11e601]:
- button "高" [ref=f11e602]
- gridcell [ref=f11e603]:
- button "2026" [ref=f11e604]
- gridcell [ref=f11e605]:
- button "2026-05-21 16:58" [ref=f11e606]
- gridcell [ref=f11e607]:
- button "查看详情" [ref=f11e609] [cursor=pointer]
- row [ref=f11e611]:
- gridcell [ref=f11e612]:
- checkbox "Select row 2026测试-访客门禁权限超过申请时长案例" [ref=f11e616]
- rowheader [ref=f11e621]:
- button "2026测试-访客门禁权限超过申请时长案例" [ref=f11e622]
- gridcell [ref=f11e623]:
- button "信息安全风险" [ref=f11e624]
- gridcell [ref=f11e625]:
- button "门禁管理,AI批量造数-20260521-续批" [ref=f11e626]
- gridcell [ref=f11e627]:
- button "权限超期" [ref=f11e628]
- gridcell [ref=f11e629]:
- button "访客门禁,权限超期,物理安全" [ref=f11e630]
- gridcell [ref=f11e631]:
- button "安保检查" [ref=f11e632]
- gridcell [ref=f11e633]:
- button "中" [ref=f11e634]
- gridcell [ref=f11e635]:
- button "2026" [ref=f11e636]
- gridcell [ref=f11e637]:
- button "2026-05-21 16:58" [ref=f11e638]
- gridcell [ref=f11e639]:
- button "查看详情" [ref=f11e641] [cursor=pointer]
- row [ref=f11e643]:
- gridcell [ref=f11e644]:
- checkbox "Select row 2026测试-客户投诉分类标签误用案例" [ref=f11e648]
- rowheader [ref=f11e653]:
- button "2026测试-客户投诉分类标签误用案例" [ref=f11e654]
- gridcell [ref=f11e655]:
- button "服务风险" [ref=f11e656]
- gridcell [ref=f11e657]:
- button "客户投诉,AI批量造数-20260521-续批" [ref=f11e658]
- gridcell [ref=f11e659]:
- button "标签误分类" [ref=f11e660]
- gridcell [ref=f11e661]:
- button "客户投诉,标签分类,统计失真" [ref=f11e662]
- gridcell [ref=f11e663]:
- button "消保月度复盘" [ref=f11e664]
- gridcell [ref=f11e665]:
- button "中" [ref=f11e666]
- gridcell [ref=f11e667]:
- button "2024" [ref=f11e668]
- gridcell [ref=f11e669]:
- button "2026-05-21 16:57" [ref=f11e670]
- gridcell [ref=f11e671]:
- button "查看详情" [ref=f11e673] [cursor=pointer]
- row [ref=f11e675]:
- gridcell [ref=f11e676]:
- checkbox "Select row 2026测试-异常交易预警处置备注模板化案例" [ref=f11e680]
- rowheader [ref=f11e685]:
- button "2026测试-异常交易预警处置备注模板化案例" [ref=f11e686]
- gridcell [ref=f11e687]:
- button "反洗钱风险" [ref=f11e688]
- gridcell [ref=f11e689]:
- button "异常交易,AI批量造数-20260521-续批" [ref=f11e690]
- gridcell [ref=f11e691]:
- button "处置质量不足" [ref=f11e692]
- gridcell [ref=f11e693]:
- button "异常交易,模板备注,复核质量" [ref=f11e694]
- gridcell [ref=f11e695]:
- button "反洗钱抽样检查" [ref=f11e696]
- gridcell [ref=f11e697]:
- button "高" [ref=f11e698]
- gridcell [ref=f11e699]:
- button "2026" [ref=f11e700]
- gridcell [ref=f11e701]:
- button "2026-05-21 16:57" [ref=f11e702]
- gridcell [ref=f11e703]:
- button "查看详情" [ref=f11e705] [cursor=pointer]
- row [ref=f11e707]:
- gridcell [ref=f11e708]:
- checkbox "Select row 2026测试-质保金释放条件核验不充分案例" [ref=f11e712]
- rowheader [ref=f11e717]:
- button "2026测试-质保金释放条件核验不充分案例" [ref=f11e718]
- gridcell [ref=f11e719]:
- button "合同风险" [ref=f11e720]
- gridcell [ref=f11e721]:
- button "质保金管理,AI批量造数-20260521-续批" [ref=f11e722]
- gridcell [ref=f11e723]:
- button "付款条件核验不足" [ref=f11e724]
- gridcell [ref=f11e725]:
- button "质保金,尾款释放,验收状态" [ref=f11e726]
- gridcell [ref=f11e727]:
- button "合同履约抽查" [ref=f11e728]
- gridcell [ref=f11e729]:
- button "高" [ref=f11e730]
- gridcell [ref=f11e731]:
- button "2025" [ref=f11e732]
- gridcell [ref=f11e733]:
- button "2026-05-21 16:57" [ref=f11e734]
- gridcell [ref=f11e735]:
- button "查看详情" [ref=f11e737] [cursor=pointer]
- row [ref=f11e739]:
- gridcell [ref=f11e740]:
- checkbox "Select row 2026测试-客服录音抽检覆盖率不足案例" [ref=f11e744]
- rowheader [ref=f11e749]:
- button "2026测试-客服录音抽检覆盖率不足案例" [ref=f11e750]
- gridcell [ref=f11e751]:
- button "服务风险" [ref=f11e752]
- gridcell [ref=f11e753]:
- button "客服质检,AI批量造数-20260521-续批" [ref=f11e754]
- gridcell [ref=f11e755]:
- button "抽检不足" [ref=f11e756]
- gridcell [ref=f11e757]:
- button "客服录音,抽检覆盖率,不规范承诺" [ref=f11e758]
- gridcell [ref=f11e759]:
- button "服务管理复盘" [ref=f11e760]
- gridcell [ref=f11e761]:
- button "中" [ref=f11e762]
- gridcell [ref=f11e763]:
- button "2024" [ref=f11e764]
- gridcell [ref=f11e765]:
- button "2026-05-21 16:57" [ref=f11e766]
- gridcell [ref=f11e767]:
- button "查看详情" [ref=f11e769] [cursor=pointer]
- row [ref=f11e771]:
- gridcell [ref=f11e772]:
- checkbox "Select row 2026测试-预算追加审批事后补录案例" [ref=f11e776]
- rowheader [ref=f11e781]:
- button "2026测试-预算追加审批事后补录案例" [ref=f11e782]
- gridcell [ref=f11e783]:
- button "财务风险" [ref=f11e784]
- gridcell [ref=f11e785]:
- button "预算控制,AI批量造数-20260521-续批" [ref=f11e786]
- gridcell [ref=f11e787]:
- button "事后审批" [ref=f11e788]
- gridcell [ref=f11e789]:
- button "预算追加,事后审批,支出承诺" [ref=f11e790]
- gridcell [ref=f11e791]:
- button "预算执行检查" [ref=f11e792]
- gridcell [ref=f11e793]:
- button "中" [ref=f11e794]
- gridcell [ref=f11e795]:
- button "2025" [ref=f11e796]
- gridcell [ref=f11e797]:
- button "2026-05-21 16:57" [ref=f11e798]
- gridcell [ref=f11e799]:
- button "查看详情" [ref=f11e801] [cursor=pointer]
- row [ref=f11e803]:
- gridcell [ref=f11e804]:
- checkbox "Select row 2026测试-供应商黑名单同步延迟案例" [ref=f11e808]
- rowheader [ref=f11e813]:
- button "2026测试-供应商黑名单同步延迟案例" [ref=f11e814]
- gridcell [ref=f11e815]:
- button "合规风险" [ref=f11e816]
- gridcell [ref=f11e817]:
- button "供应商管理,AI批量造数-20260521-续批" [ref=f11e818]
- gridcell [ref=f11e819]:
- button "名单同步缺失" [ref=f11e820]
- gridcell [ref=f11e821]:
- button "供应商黑名单,系统同步,下单校验" [ref=f11e822]
- gridcell [ref=f11e823]:
- button "采购合规检查" [ref=f11e824]
- gridcell [ref=f11e825]:
- button "高" [ref=f11e826]
- gridcell [ref=f11e827]:
- button "2026" [ref=f11e828]
- gridcell [ref=f11e829]:
- button "2026-05-21 16:57" [ref=f11e830]
- gridcell [ref=f11e831]:
- button "查看详情" [ref=f11e833] [cursor=pointer]
- row [ref=f11e835]:
- gridcell [ref=f11e836]:
- checkbox "Select row 2026测试-外包人员离场账号停用滞后案例" [ref=f11e840]
- rowheader [ref=f11e845]:
- button "2026测试-外包人员离场账号停用滞后案例" [ref=f11e846]
- gridcell [ref=f11e847]:
- button "信息安全风险" [ref=f11e848]
- gridcell [ref=f11e849]:
- button "账号停用,AI批量造数-20260521" [ref=f11e850]
- gridcell [ref=f11e851]:
- button "权限残留" [ref=f11e852]
- gridcell [ref=f11e853]:
- button "外包离场,账号停用,权限回收" [ref=f11e854]
- gridcell [ref=f11e855]:
- button "IT审计" [ref=f11e856]
- gridcell [ref=f11e857]:
- button "高" [ref=f11e858]
- gridcell [ref=f11e859]:
- button "2026" [ref=f11e860]
- gridcell [ref=f11e861]:
- button "2026-05-21 16:38" [ref=f11e862]
- gridcell [ref=f11e863]:
- button "查看详情" [ref=f11e865] [cursor=pointer]
- row [ref=f11e867]:
- gridcell [ref=f11e868]:
- checkbox "Select row 2026测试-营销活动费用分摊依据不清案例" [ref=f11e872]
- rowheader [ref=f11e877]:
- button "2026测试-营销活动费用分摊依据不清案例" [ref=f11e878]
- gridcell [ref=f11e879]:
- button "财务风险" [ref=f11e880]
- gridcell [ref=f11e881]:
- button "费用分摊,AI批量造数-20260521" [ref=f11e882]
- gridcell [ref=f11e883]:
- button "核算口径偏差" [ref=f11e884]
- gridcell [ref=f11e885]:
- button "费用分摊,管理会计,利润真实性" [ref=f11e886]
- gridcell [ref=f11e887]:
- button "经营分析复核" [ref=f11e888]
- gridcell [ref=f11e889]:
- button "中" [ref=f11e890]
- gridcell [ref=f11e891]:
- button "2024" [ref=f11e892]
- gridcell [ref=f11e893]:
- button "2026-05-21 16:38" [ref=f11e894]
- gridcell [ref=f11e895]:
- button "查看详情" [ref=f11e897] [cursor=pointer]
- generic [ref=f11e354]:
- generic [ref=f11e355]:
- generic [ref=f11e356]: 共 15 条,第 1 / 1 页
- generic [ref=f11e357]:
- group [ref=f11e358] [cursor=pointer]:
- generic "20 条/页" [ref=f11e360]
- button "20 条/页 每页条数"
- combobox [ref=f11e366]
- navigation "pagination" [ref=f11e367]:
- list [ref=f11e368]:
- listitem [ref=f11e369]:
- button "上一页" [disabled]
- listitem [ref=f11e370]:
- button "1" [ref=f11e371] [cursor=pointer]
- listitem [ref=f11e372]:
- button "下一页" [disabled]
- button [ref=f11e373] [cursor=pointer]
- button [ref=f11e374] [cursor=pointer]
- button [ref=f11e375] [cursor=pointer]
- button [ref=f11e376] [cursor=pointer]
- button [ref=f11e377] [cursor=pointer]
- button [ref=f11e378] [cursor=pointer]
- button [ref=f11e379] [cursor=pointer]
\ No newline at end of file
- generic [ref=f11e1]:
- complementary [ref=f11e3]:
- generic [ref=f11e5]:
- img "鑫元" [ref=f11e7]
- generic [ref=f11e8]: 警示案例库
- button "切换应用" [ref=f11e11] [cursor=pointer]
- treegrid "案例库工作台" [ref=f11e15]:
- row "驾驶舱" [level=1] [ref=f11e16] [cursor=pointer]:
- gridcell "驾驶舱" [ref=f11e17]
- row "案例管理" [level=1] [ref=f11e23] [cursor=pointer]:
- gridcell "案例管理" [ref=f11e24]
- button "打开用户菜单" [ref=f11e31] [cursor=pointer]:
- generic [ref=f11e32]: A
- generic [ref=f11e34]:
- generic [ref=f11e35]: Andy
- generic [ref=f11e36]: andy
- main [ref=f11e37]:
- navigation [ref=f11e38]:
- generic [ref=f11e39]:
- button "收起侧栏" [ref=f11e40] [cursor=pointer]
- heading "案例管理" [level=1] [ref=f11e41]
- button "通知消息" [ref=f11e44] [cursor=pointer]
- generic [ref=f11e47]:
- heading "案例管理" [level=1] [ref=f11e48]
- generic [ref=f11e49]:
- tablist "案例风险分类" [ref=f11e54]:
- tab "全部案例" [selected] [ref=f11e55] [cursor=pointer]
- tab "治理风险" [ref=f11e58] [cursor=pointer]
- tab "信息安全风险" [ref=f11e60] [cursor=pointer]
- tab "服务风险" [ref=f11e62] [cursor=pointer]
- tab "财务风险" [ref=f11e64] [cursor=pointer]
- tab "未分类" [ref=f11e66] [cursor=pointer]
- tab "知识管理风险" [ref=f11e68] [cursor=pointer]
- tab "信息技术风险" [ref=f11e70] [cursor=pointer]
- tab "反洗钱风险" [ref=f11e72] [cursor=pointer]
- tab "合同风险" [ref=f11e74] [cursor=pointer]
- tab "合规风险" [ref=f11e76] [cursor=pointer]
- generic [ref=f11e80]:
- generic [ref=f11e81]:
- generic [ref=f11e82]:
- generic [ref=f11e83]: 案例来源
- textbox "案例来源" [ref=f11e84]:
- /placeholder: 例如:监管通报
- generic [ref=f11e85]:
- generic [ref=f11e86]: 案例类型
- textbox "案例类型" [ref=f11e87]:
- /placeholder: 例如:审批绕行
- generic [ref=f11e88]:
- generic [ref=f11e89]: 问题属性
- textbox "问题属性" [ref=f11e90]:
- /placeholder: 例如:流程绕行
- button "展开" [ref=f11e92] [cursor=pointer]
- generic [ref=f11e95]:
- generic [ref=f11e96]:
- group [ref=f11e99]:
- searchbox "关键词搜索" [ref=f11e100]
- button "清空关键词"
- tablist "案例展示方式" [ref=f11e104]:
- tab "卡片视图" [selected] [ref=f11e105] [cursor=pointer]
- tab "表格视图" [ref=f11e107] [cursor=pointer]
- generic [ref=f11e108]:
- button "Excel 批量导出" [ref=f11e109] [cursor=pointer]
- button "Excel 批量导入" [ref=f11e110] [cursor=pointer]
- button "手动新建案例" [ref=f11e111] [cursor=pointer]
- generic [ref=f11e112]:
- generic [ref=f11e113]:
- button "查看案例:2026测试-投标文件盖章页缺页未识别案例" [ref=f11e114] [cursor=pointer]:
- generic [ref=f11e115]:
- generic [ref=f11e116]:
- generic [ref=f11e117]:
- generic [ref=f11e118]: 未分类
- generic [ref=f11e120]: 中
- generic [ref=f11e122]: 招投标,AI批量造数-20260521-续批
- generic [ref=f11e124]: 2026测试-投标文件盖章页缺页未识别案例
- generic [ref=f11e125]: 形式审查疏漏
- generic [ref=f11e126]: 某投标文件扫描件在上传过程中遗漏了一页盖章页,但评审小组仅核对目录和总页数,未逐项检查关键签章位置,导致带缺页材料进入评审流程。虽然最终未中标,但事件反映出形式审查清单执行不严。
- generic [ref=f11e127]:
- generic [ref=f11e128]: 2025 年
- generic [ref=f11e129]: 更新于 2026-08-13 15:52
- button "查看案例:2026测试-供应商履约评价长期未更新案例" [ref=f11e130] [cursor=pointer]:
- generic [ref=f11e131]:
- generic [ref=f11e132]:
- generic [ref=f11e133]:
- generic [ref=f11e134]: 治理风险
- generic [ref=f11e136]: 中
- generic [ref=f11e138]: 供应商评价,AI批量造数-20260521-续批
- generic [ref=f11e140]: 2026测试-供应商履约评价长期未更新案例
- generic [ref=f11e141]: 评价机制失效
- generic [ref=f11e142]: 部分长期合作供应商的履约评价停留在上一年度,项目交付中的延期、返工和服务投诉未及时沉淀到评价结果,导致续签和复购决策缺少真实依据。问题说明履约反馈回路未真正嵌入采购管理。
- generic [ref=f11e143]:
- generic [ref=f11e144]: 2025 年
- generic [ref=f11e145]: 更新于 2026-06-15 12:19
- button "查看案例:2026测试-印鉴卡更新后柜面未同步案例" [ref=f11e146] [cursor=pointer]:
- generic [ref=f11e147]:
- generic [ref=f11e148]:
- generic [ref=f11e149]:
- generic [ref=f11e150]: 治理风险
- generic [ref=f11e152]: 中
- generic [ref=f11e154]: 印鉴管理,AI批量造数-20260521-续批
- generic [ref=f11e156]: 2026测试-印鉴卡更新后柜面未同步案例
- generic [ref=f11e157]: 主数据同步异常
- generic [ref=f11e158]: 客户完成印鉴变更后,核心主数据已更新,但部分柜面终端缓存未及时刷新,导致经办人员按旧印鉴卡进行人工复核并误判异常。事件虽经复核纠正,但暴露出关键主数据变更后的前端同步机制不足。
- generic [ref=f11e159]:
- generic [ref=f11e160]: 2026 年
- generic [ref=f11e161]: 更新于 2026-06-15 12:19
- button "查看案例:2026测试-客户证照到期提醒触达失败案例" [ref=f11e162] [cursor=pointer]:
- generic [ref=f11e163]:
- generic [ref=f11e164]:
- generic [ref=f11e165]:
- generic [ref=f11e166]: 治理风险
- generic [ref=f11e168]: 中
- generic [ref=f11e170]: 证照管理,AI批量造数-20260521-续批
- generic [ref=f11e172]: 2026测试-客户证照到期提醒触达失败案例
- generic [ref=f11e173]: 提醒失效
- generic [ref=f11e174]: 系统已配置客户证照到期前提醒,但由于短信渠道签名异常和邮件地址长期未维护,多个客户未收到续补资料通知。后续业务办理时集中出现证照过期问题,影响交易时效。该问题暴露出提醒机制仅关注“发出”,未验证“送达”和“处理”。
- generic [ref=f11e175]:
- generic [ref=f11e176]: 2025 年
- generic [ref=f11e177]: 更新于 2026-06-15 12:19
- button "查看案例:2026测试-项目复盘结论未进入知识库案例" [ref=f11e178] [cursor=pointer]:
- generic [ref=f11e179]:
- generic [ref=f11e180]:
- generic [ref=f11e181]:
- generic [ref=f11e182]: 知识管理风险
- generic [ref=f11e184]: 中
- generic [ref=f11e186]: 知识沉淀,AI批量造数-20260521-续批
- generic [ref=f11e188]: 2026测试-项目复盘结论未进入知识库案例
- generic [ref=f11e189]: 经验未复用
- generic [ref=f11e190]: 某重点项目在结束后形成了较完整的问题复盘和改进建议,但材料仅保存在项目群和个人网盘中,未录入统一知识库。后续相似项目重复踩坑,团队才发现既有经验未被有效复用。说明复盘机制缺乏沉淀动作和责任要求。
- generic [ref=f11e191]:
- generic [ref=f11e192]: 2024 年
- generic [ref=f11e193]: 更新于 2026-05-21 16:58
- button "查看案例:2026测试-系统批量导入失败重试无幂等案例" [ref=f11e194] [cursor=pointer]:
- generic [ref=f11e195]:
- generic [ref=f11e196]:
- generic [ref=f11e197]:
- generic [ref=f11e198]: 信息技术风险
- generic [ref=f11e200]: 高
- generic [ref=f11e202]: 批量导入,AI批量造数-20260521-续批
- generic [ref=f11e204]: 2026测试-系统批量导入失败重试无幂等案例
- generic [ref=f11e205]: 幂等控制缺失
- generic [ref=f11e206]: 某业务系统批量导入任务在超时后被人工重复重试,因缺少批次幂等键,导致部分记录重复写入。后续清理耗费大量人工,也影响了下游报表准确性。问题说明批量任务失败处理设计不足。
- generic [ref=f11e207]:
- generic [ref=f11e208]: 2026 年
- generic [ref=f11e209]: 更新于 2026-05-21 16:58
- button "查看案例:2026测试-访客门禁权限超过申请时长案例" [ref=f11e210] [cursor=pointer]:
- generic [ref=f11e211]:
- generic [ref=f11e212]:
- generic [ref=f11e213]:
- generic [ref=f11e214]: 信息安全风险
- generic [ref=f11e216]: 中
- generic [ref=f11e218]: 门禁管理,AI批量造数-20260521-续批
- generic [ref=f11e220]: 2026测试-访客门禁权限超过申请时长案例
- generic [ref=f11e221]: 权限超期
- generic [ref=f11e222]: 某外部访客因现场培训申请了两小时门禁权限,但系统默认授权至当日24点,且未要求访客离场后回收。虽然未发生异常出入,但该设置与最小必要原则不符,增加了物理安全暴露面。
- generic [ref=f11e223]:
- generic [ref=f11e224]: 2026 年
- generic [ref=f11e225]: 更新于 2026-05-21 16:58
- button "查看案例:2026测试-客户投诉分类标签误用案例" [ref=f11e226] [cursor=pointer]:
- generic [ref=f11e227]:
- generic [ref=f11e228]:
- generic [ref=f11e229]:
- generic [ref=f11e230]: 服务风险
- generic [ref=f11e232]: 中
- generic [ref=f11e234]: 客户投诉,AI批量造数-20260521-续批
- generic [ref=f11e236]: 2026测试-客户投诉分类标签误用案例
- generic [ref=f11e237]: 标签误分类
- generic [ref=f11e238]: 某批客户投诉被统一标记为“咨询建议”,未按实际情形区分收费争议、服务态度和系统故障等类型,导致后续统计分析失真,也影响整改责任部门定位。说明一线录入标准不清、复核机制不足。
- generic [ref=f11e239]:
- generic [ref=f11e240]: 2024 年
- generic [ref=f11e241]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-异常交易预警处置备注模板化案例" [ref=f11e242] [cursor=pointer]:
- generic [ref=f11e243]:
- generic [ref=f11e244]:
- generic [ref=f11e245]:
- generic [ref=f11e246]: 反洗钱风险
- generic [ref=f11e248]: 高
- generic [ref=f11e250]: 异常交易,AI批量造数-20260521-续批
- generic [ref=f11e252]: 2026测试-异常交易预警处置备注模板化案例
- generic [ref=f11e253]: 处置质量不足
- generic [ref=f11e254]: 部分异常交易预警在人工复核后直接套用“客户交易正常、风险可接受”等模板备注,缺少对交易背景、资金去向和客户行业特征的具体分析。抽查时难以判断复核结论是否充分。问题反映处置过程重关闭数量、轻分析质量。
- generic [ref=f11e255]:
- generic [ref=f11e256]: 2026 年
- generic [ref=f11e257]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-质保金释放条件核验不充分案例" [ref=f11e258] [cursor=pointer]:
- generic [ref=f11e259]:
- generic [ref=f11e260]:
- generic [ref=f11e261]:
- generic [ref=f11e262]: 合同风险
- generic [ref=f11e264]: 高
- generic [ref=f11e266]: 质保金管理,AI批量造数-20260521-续批
- generic [ref=f11e268]: 2026测试-质保金释放条件核验不充分案例
- generic [ref=f11e269]: 付款条件核验不足
- generic [ref=f11e270]: 某项目在质保期未完全届满的情况下,根据供应商邮件说明提前释放质保金,但未完整核验缺陷整改记录、维保工单和质保期限。后续设备再次出现故障时,追偿空间明显受限。说明尾款和质保金支付控制较弱。
- generic [ref=f11e271]:
- generic [ref=f11e272]: 2025 年
- generic [ref=f11e273]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-客服录音抽检覆盖率不足案例" [ref=f11e274] [cursor=pointer]:
- generic [ref=f11e275]:
- generic [ref=f11e276]:
- generic [ref=f11e277]:
- generic [ref=f11e278]: 服务风险
- generic [ref=f11e280]: 中
- generic [ref=f11e282]: 客服质检,AI批量造数-20260521-续批
- generic [ref=f11e284]: 2026测试-客服录音抽检覆盖率不足案例
- generic [ref=f11e285]: 抽检不足
- generic [ref=f11e286]: 某客服中心仅对投诉工单关联通话进行录音抽检,普通咨询类通话长期未纳入质量检查范围。后续在外部回访中发现,部分坐席存在不规范承诺和用语不当问题。说明现有抽检模型偏重事后投诉,未能覆盖高频风险场景。
- generic [ref=f11e287]:
- generic [ref=f11e288]: 2024 年
- generic [ref=f11e289]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-预算追加审批事后补录案例" [ref=f11e290] [cursor=pointer]:
- generic [ref=f11e291]:
- generic [ref=f11e292]:
- generic [ref=f11e293]:
- generic [ref=f11e294]: 财务风险
- generic [ref=f11e296]: 中
- generic [ref=f11e298]: 预算控制,AI批量造数-20260521-续批
- generic [ref=f11e300]: 2026测试-预算追加审批事后补录案例
- generic [ref=f11e301]: 事后审批
- generic [ref=f11e302]: 某项目因营销节点临近,先行发生推广支出后再补提预算追加流程。系统中虽然最终形成审批记录,但审批时间晚于合同签订和付款申请。该做法削弱了预算前置控制作用,容易使审批流沦为形式性补录。
- generic [ref=f11e303]:
- generic [ref=f11e304]: 2025 年
- generic [ref=f11e305]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-供应商黑名单同步延迟案例" [ref=f11e306] [cursor=pointer]:
- generic [ref=f11e307]:
- generic [ref=f11e308]:
- generic [ref=f11e309]:
- generic [ref=f11e310]: 合规风险
- generic [ref=f11e312]: 高
- generic [ref=f11e314]: 供应商管理,AI批量造数-20260521-续批
- generic [ref=f11e316]: 2026测试-供应商黑名单同步延迟案例
- generic [ref=f11e317]: 名单同步缺失
- generic [ref=f11e318]: 某采购平台中的供应商黑名单由法务部门按月维护,但业务下单系统每季度才同步一次,导致已被限制合作的供应商仍可在短期内继续参与询价。后续抽查发现一笔订单正是在同步窗口期发起。问题说明黑名单治理缺少系统间联动和实时校验。
- generic [ref=f11e319]:
- generic [ref=f11e320]: 2026 年
- generic [ref=f11e321]: 更新于 2026-05-21 16:57
- button "查看案例:2026测试-外包人员离场账号停用滞后案例" [ref=f11e322] [cursor=pointer]:
- generic [ref=f11e323]:
- generic [ref=f11e324]:
- generic [ref=f11e325]:
- generic [ref=f11e326]: 信息安全风险
- generic [ref=f11e328]: 高
- generic [ref=f11e330]: 账号停用,AI批量造数-20260521
- generic [ref=f11e332]: 2026测试-外包人员离场账号停用滞后案例
- generic [ref=f11e333]: 权限残留
- generic [ref=f11e334]: 某外包开发人员项目结束后已办理门禁注销,但其测试环境和日志平台账号仍保留一周以上。例行审计发现,该账号期间仍存在登录记录,虽未检测到异常操作,但说明离场流程未实现资产、权限和物理通行的统一回收。该问题一旦叠加高权限配置,可能形成明显安全暴露面。
- generic [ref=f11e335]:
- generic [ref=f11e336]: 2026 年
- generic [ref=f11e337]: 更新于 2026-05-21 16:38
- button "查看案例:2026测试-营销活动费用分摊依据不清案例" [ref=f11e338] [cursor=pointer]:
- generic [ref=f11e339]:
- generic [ref=f11e340]:
- generic [ref=f11e341]:
- generic [ref=f11e342]: 财务风险
- generic [ref=f11e344]: 中
- generic [ref=f11e346]: 费用分摊,AI批量造数-20260521
- generic [ref=f11e348]: 2026测试-营销活动费用分摊依据不清案例
- generic [ref=f11e349]: 核算口径偏差
- generic [ref=f11e350]: 某跨区域营销活动结束后,相关场地、物料和外包费用按经验比例分摊至多个项目,但缺少事先约定的分摊规则和可追溯依据。月底复盘时,不同部门对费用承担范围产生争议,影响项目利润真实性。问题在于活动立项时未同步明确费用归集与分摊口径。
- generic [ref=f11e351]:
- generic [ref=f11e352]: 2024 年
- generic [ref=f11e353]: 更新于 2026-05-21 16:38
- generic [ref=f11e354]:
- generic [ref=f11e355]:
- generic [ref=f11e356]: 共 15 条,第 1 / 1 页
- generic [ref=f11e357]:
- group [ref=f11e358] [cursor=pointer]:
- generic "20 条/页" [ref=f11e360]
- button "20 条/页 每页条数"
- combobox [ref=f11e366]
- navigation "pagination" [ref=f11e367]:
- list [ref=f11e368]:
- listitem [ref=f11e369]:
- button "上一页" [disabled]
- listitem [ref=f11e370]:
- button "1" [ref=f11e371] [cursor=pointer]
- listitem [ref=f11e372]:
- button "下一页" [disabled]
- button [ref=f11e373] [cursor=pointer]
- button [ref=f11e374] [cursor=pointer]
- button [ref=f11e375] [cursor=pointer]
- button [ref=f11e376] [cursor=pointer]
- button [ref=f11e377] [cursor=pointer]
- button [ref=f11e378] [cursor=pointer]
- button [ref=f11e379] [cursor=pointer]
\ No newline at end of file
import { Close } from '@/components/AppIcons'; import { Close } from '@/components/AppIcons';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
import { ActionBar } from '@heroui-pro/react/action-bar'; import { ActionBar } from '@heroui-pro/react/action-bar';
import { DataGrid } from '@heroui-pro/react/data-grid'; import { DataGrid } from '@heroui-pro/react/data-grid';
import { EmptyState } from '@heroui-pro/react/empty-state'; import { EmptyState } from '@heroui-pro/react/empty-state';
...@@ -8,8 +9,28 @@ import { ...@@ -8,8 +9,28 @@ import {
Pagination, Pagination,
Separator, Separator,
Spinner, Spinner,
Surface,
} from '@heroui/react'; } from '@heroui/react';
import { Tooltip } from '@heroui/react'; import { Tooltip } from '@heroui/react';
import { useLayoutEffect, useRef, useState } from 'react';
const DATA_TABLE_LONG_TEXT_TYPES = new Set([
'long_text',
'rich_text',
'object',
'form',
'subject',
]);
const DATA_TABLE_LONG_TEXT_LABEL_PATTERN =
/需求明细|需求内容|材料清单|工作内容|检查内容|审计范围|整改情况|问题简述|整改措施|问题描述|具体问题|整改要求|描述|措施|原因|意见|备注|说明/;
export function isDataTableLongTextField(fieldDef, label = '') {
return DATA_TABLE_LONG_TEXT_TYPES.has(
String(fieldDef?.type || '').toLowerCase(),
) || DATA_TABLE_LONG_TEXT_LABEL_PATTERN.test(
String(label || fieldDef?.label || ''),
);
}
function normalizeDataTableEmptyTitle(title) { function normalizeDataTableEmptyTitle(title) {
const text = String(title || '').trim(); const text = String(title || '').trim();
...@@ -24,23 +45,38 @@ function normalizeDataTableEmptyTitle(title) { ...@@ -24,23 +45,38 @@ function normalizeDataTableEmptyTitle(title) {
return title; return title;
} }
export function DataTableEmptyState({ title = '暂无数据' }) { export function EmptyStateSurface({ children, className = '' }) {
return (
<Surface
className={['w-full min-w-0 rounded-2xl p-4', className]
.filter(Boolean)
.join(' ')}
variant="secondary"
>
{children}
</Surface>
);
}
export function DataTableEmptyState({ title = '暂无数据', size = 'md' }) {
const resolvedTitle = normalizeDataTableEmptyTitle(title); const resolvedTitle = normalizeDataTableEmptyTitle(title);
return ( return (
<EmptyState size="md"> <EmptyStateSurface>
<EmptyState size={size}>
<EmptyState.Header> <EmptyState.Header>
<EmptyState.Title>{resolvedTitle}</EmptyState.Title> <EmptyState.Description className="rc-empty-state-message">
{resolvedTitle}
</EmptyState.Description>
</EmptyState.Header> </EmptyState.Header>
</EmptyState> </EmptyState>
</EmptyStateSurface>
); );
} }
function DataTableLoadingState() { function DataTableLoadingState() {
return ( return (
<div className="flex min-h-48 items-center justify-center" aria-label="数据加载中"> <div className="min-h-48 w-full" aria-hidden="true" />
<Spinner size="sm" />
</div>
); );
} }
...@@ -87,6 +123,7 @@ export function DataTable({ ...@@ -87,6 +123,7 @@ export function DataTable({
selectedRowKeys, selectedRowKeys,
onSelectedRowKeysChange, onSelectedRowKeysChange,
emptyTitle = '暂无数据', emptyTitle = '暂无数据',
emptyStateSize = 'md',
className = '', className = '',
isWide, isWide,
contentMinWidth, contentMinWidth,
...@@ -95,18 +132,53 @@ export function DataTable({ ...@@ -95,18 +132,53 @@ export function DataTable({
sortDescriptor, sortDescriptor,
onSortChange, onSortChange,
renderSelectionActions, renderSelectionActions,
allowsColumnResize = true,
}) { }) {
const selectionEnabled = Boolean(selectedRowKeys && onSelectedRowKeysChange); const selectionEnabled = Boolean(selectedRowKeys && onSelectedRowKeysChange);
const selectedKeys = selectionEnabled ? new Set(selectedRowKeys) : undefined; const selectedKeys = selectionEnabled ? new Set(selectedRowKeys) : undefined;
const wide = isWide ?? columns.length > 6; const gridViewportRef = useRef(null);
const [hasHorizontalOverflow, setHasHorizontalOverflow] = useState(false);
const resolvedContentMinWidth = const resolvedContentMinWidth =
wide && Number(contentMinWidth) > 0 ? Number(contentMinWidth) : 0; Number(contentMinWidth) > 0 ? Number(contentMinWidth) : 0;
const firstBusinessColumnIndex = columns.findIndex( const firstBusinessColumnIndex = columns.findIndex(
(column) => (column) =>
!column.isSelection && !column.isSelection &&
!column.isActions && !column.isActions &&
column.key !== 'actions', column.key !== 'actions',
); );
useLayoutEffect(() => {
if (typeof isWide === 'boolean') {
setHasHorizontalOverflow(isWide);
return undefined;
}
const viewport = gridViewportRef.current;
if (!viewport) return undefined;
const updateOverflow = () => {
const scrollContainer =
viewport.querySelector('.table__resizable-container') ||
viewport.querySelector('.data-table-scroll');
if (!scrollContainer) return;
setHasHorizontalOverflow(
scrollContainer.scrollWidth > scrollContainer.clientWidth + 1,
);
};
updateOverflow();
if (typeof ResizeObserver === 'undefined') return undefined;
const resizeObserver = new ResizeObserver(updateOverflow);
resizeObserver.observe(viewport);
const scrollContainer =
viewport.querySelector('.table__resizable-container') ||
viewport.querySelector('.data-table-scroll');
if (scrollContainer) resizeObserver.observe(scrollContainer);
return () => resizeObserver.disconnect();
}, [columns, isWide, rows]);
const wide = typeof isWide === 'boolean' ? isWide : hasHorizontalOverflow;
const gridColumns = columns.map((column, index) => { const gridColumns = columns.map((column, index) => {
const isActions = column.isActions || column.key === 'actions'; const isActions = column.isActions || column.key === 'actions';
const isPrimary = const isPrimary =
...@@ -115,7 +187,7 @@ export function DataTable({ ...@@ -115,7 +187,7 @@ export function DataTable({
const numericWidth = const numericWidth =
typeof configuredWidth === 'number' ? configuredWidth : undefined; typeof configuredWidth === 'number' ? configuredWidth : undefined;
const pinned = wide const pinned = wide
? column.pin || (isActions ? 'end' : undefined) ? column.pin || (isActions ? 'end' : isPrimary ? 'start' : undefined)
: undefined; : undefined;
const minWidth = const minWidth =
column.minWidth || column.minWidth ||
...@@ -148,13 +220,20 @@ export function DataTable({ ...@@ -148,13 +220,20 @@ export function DataTable({
/> />
); );
} }
return value; return isActions ? value : (
<div className="min-w-0 max-w-full">
{value}
</div>
);
}, },
isRowHeader: index === 0, isRowHeader: index === firstBusinessColumnIndex,
allowsSorting: column.sortable === true, allowsSorting: column.sortable === true,
headerClassName: column.headerClassName, headerClassName: column.headerClassName,
cellClassName: column.cellClassName, cellClassName: [
allowsResizing: true, 'min-w-0',
column.cellClassName,
].filter(Boolean).join(' '),
allowsResizing: allowsColumnResize,
minWidth, minWidth,
width, width,
pinned, pinned,
...@@ -182,15 +261,21 @@ export function DataTable({ ...@@ -182,15 +261,21 @@ export function DataTable({
return ( return (
<div <div
className={['relative min-w-0 w-full', className].filter(Boolean).join(' ')} className={[
'relative min-w-0 w-full',
wide && 'rc-data-table-sticky',
className,
]
.filter(Boolean)
.join(' ')}
style={ style={
resolvedContentMinWidth resolvedContentMinWidth
? { '--data-table-content-min-width': `${resolvedContentMinWidth}px` } ? { '--data-table-content-min-width': `${resolvedContentMinWidth}px` }
: undefined : undefined
} }
> >
<div className="relative min-w-0 w-full"> <div ref={gridViewportRef} className="relative min-w-0 w-full">
{isLoading && rows.length > 0 ? ( {isLoading ? (
<div <div
className="absolute inset-0 z-10 grid place-items-center" className="absolute inset-0 z-10 grid place-items-center"
aria-label="数据加载中" aria-label="数据加载中"
...@@ -200,11 +285,11 @@ export function DataTable({ ...@@ -200,11 +285,11 @@ export function DataTable({
) : null} ) : null}
<DataGrid <DataGrid
showSelectionCheckboxes={selectionEnabled} showSelectionCheckboxes={selectionEnabled}
allowsColumnResize allowsColumnResize={allowsColumnResize}
aria-label={ariaLabel} aria-label={ariaLabel}
columns={gridColumns} columns={gridColumns}
contentClassName={ contentClassName={
resolvedContentMinWidth resolvedContentMinWidth && wide
? 'min-w-full [min-width:var(--data-table-content-min-width)]' ? 'min-w-full [min-width:var(--data-table-content-min-width)]'
: wide : wide
? 'min-w-max' ? 'min-w-max'
...@@ -214,7 +299,11 @@ export function DataTable({ ...@@ -214,7 +299,11 @@ export function DataTable({
disabledKeys={disabledRowKeys} disabledKeys={disabledRowKeys}
getRowId={(row) => resolveRowKey(row, rows.indexOf(row), getRowKey)} getRowId={(row) => resolveRowKey(row, rows.indexOf(row), getRowKey)}
renderEmptyState={() => renderEmptyState={() =>
isLoading ? <DataTableLoadingState /> : <DataTableEmptyState title={emptyTitle} /> isLoading ? (
<DataTableLoadingState />
) : (
<DataTableEmptyState title={emptyTitle} size={emptyStateSize} />
)
} }
scrollContainerClassName="data-table-scroll" scrollContainerClassName="data-table-scroll"
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
...@@ -289,8 +378,10 @@ export function DataTableText({ ...@@ -289,8 +378,10 @@ export function DataTableText({
}) { }) {
const fullText = String(text ?? children ?? '').trim(); const fullText = String(text ?? children ?? '').trim();
const multilineClassName = const multilineClassName =
maxLines === 2 maxLines > 1
? 'overflow-hidden whitespace-normal break-words [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [overflow-wrap:anywhere]' ? `overflow-hidden whitespace-normal break-words [display:-webkit-box] [-webkit-box-orient:vertical] ${
maxLines === 3 ? '[-webkit-line-clamp:3]' : '[-webkit-line-clamp:2]'
} [overflow-wrap:anywhere]`
: ''; : '';
const trigger = showTooltip ? ( const trigger = showTooltip ? (
<DataTableTextTooltipTrigger <DataTableTextTooltipTrigger
...@@ -301,16 +392,16 @@ export function DataTableText({ ...@@ -301,16 +392,16 @@ export function DataTableText({
{children ?? (fullText || '-')} {children ?? (fullText || '-')}
</DataTableTextTooltipTrigger> </DataTableTextTooltipTrigger>
) : ( ) : (
<span className={['block min-w-0 truncate', className].filter(Boolean).join(' ')}> <span className={[
maxLines > 1 ? multilineClassName : 'truncate',
'block min-w-0 max-w-full overflow-hidden',
className,
].filter(Boolean).join(' ')}>
{children ?? (fullText || '-')} {children ?? (fullText || '-')}
</span> </span>
); );
if ( if (!showTooltip || !fullText || fullText === '-') {
!fullText ||
fullText === '-' ||
(!showTooltip && fullText.length <= 18)
) {
return trigger; return trigger;
} }
...@@ -335,14 +426,18 @@ function DataTableTextTooltipTrigger({ ...@@ -335,14 +426,18 @@ function DataTableTextTooltipTrigger({
<Tooltip> <Tooltip>
<Tooltip.Trigger <Tooltip.Trigger
className={[ className={[
'block w-full max-w-full overflow-hidden', 'block min-w-0 max-w-full overflow-hidden',
truncate ? 'min-w-0 truncate' : '', truncate ? 'min-w-0 truncate' : '',
className, className,
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
{children} {children}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content>{text}</Tooltip.Content> <Tooltip.Content>
<span className="block max-w-[min(32rem,calc(100vw-2rem))] whitespace-pre-wrap break-words">
{text}
</span>
</Tooltip.Content>
</Tooltip> </Tooltip>
); );
} }
...@@ -444,22 +539,9 @@ export function DataTableChip({ ...@@ -444,22 +539,9 @@ export function DataTableChip({
children, children,
text, text,
className = '', className = '',
color,
variant,
}) { }) {
const fullText = String(text ?? children ?? '').trim(); const fullText = String(text ?? children ?? '').trim();
const trigger = ( const trigger = <Chip className={className} size="sm">{children ?? (fullText || '-')}</Chip>;
<Chip
className={['max-w-full overflow-hidden', className]
.filter(Boolean)
.join(' ')}
color={color}
size="sm"
variant={variant}
>
{children ?? (fullText || '-')}
</Chip>
);
if (!fullText || fullText === '-' || fullText.length <= 18) { if (!fullText || fullText === '-' || fullText.length <= 18) {
return trigger; return trigger;
...@@ -476,8 +558,10 @@ export function DataTableChip({ ...@@ -476,8 +558,10 @@ export function DataTableChip({
export function DataTablePagination({ export function DataTablePagination({
page, page,
pageSize, pageSize,
pageSizeOptions = [],
total, total,
onPageChange, onPageChange,
onPageSizeChange,
isDisabled = false, isDisabled = false,
itemLabel = '条', itemLabel = '条',
}) { }) {
...@@ -486,13 +570,46 @@ export function DataTablePagination({ ...@@ -486,13 +570,46 @@ export function DataTablePagination({
const pageWindowEnd = total const pageWindowEnd = total
? Math.min(totalPages, pageWindowStart + 5) ? Math.min(totalPages, pageWindowStart + 5)
: pageWindowStart; : pageWindowStart;
const normalizedPageSizeOptions = pageSizeOptions
.map((value) => Number(value))
.filter((value, index, values) => (
Number.isFinite(value) && value > 0 && values.indexOf(value) === index
));
const pageSizeOptionsForPicker = normalizedPageSizeOptions.map((value) => ({
id: String(value),
label: `${value} 条/页`,
}));
const hasPageSizePicker =
pageSizeOptionsForPicker.length > 0 && typeof onPageSizeChange === 'function';
return ( return (
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex min-w-0 flex-wrap items-center justify-between gap-3">
<Pagination size="sm"> <div className="flex min-w-0 shrink-0 items-center gap-3">
<Pagination.Summary> <span className="min-w-0 text-sm text-muted">
共 {total} {itemLabel},第{' '} 共 {total} {itemLabel},第{' '}
{total ? page + 1 : 0} / {total ? totalPages : 0} 页 {total ? page + 1 : 0} / {total ? totalPages : 0} 页
</Pagination.Summary> </span>
{hasPageSizePicker ? (
<XinyuanAutocomplete
aria-label="每页条数"
className="w-24 shrink-0"
isDisabled={isDisabled}
options={pageSizeOptionsForPicker}
placeholder="每页条数"
selectedKey={String(pageSize)}
showSearch={false}
valueText={`${pageSize} 条/页`}
variant="secondary"
onSelectionChange={(key) => {
const nextPageSize = Number(key);
if (normalizedPageSizeOptions.includes(nextPageSize)) {
onPageSizeChange(nextPageSize);
}
}}
/>
) : null}
</div>
<Pagination className="ml-auto w-auto shrink-0" size={hasPageSizePicker ? 'lg' : 'sm'}>
<Pagination.Content> <Pagination.Content>
<Pagination.Item> <Pagination.Item>
<Pagination.Previous <Pagination.Previous
......
...@@ -30,6 +30,7 @@ const toSelectionKey = (value) => String(value ?? ''); ...@@ -30,6 +30,7 @@ const toSelectionKey = (value) => String(value ?? '');
export default function XinyuanAutocomplete({ export default function XinyuanAutocomplete({
'aria-label': ariaLabel, 'aria-label': ariaLabel,
allowsEmptyCollection = false,
className, className,
emptyText = '暂无可选项', emptyText = '暂无可选项',
errorText = '', errorText = '',
...@@ -37,23 +38,28 @@ export default function XinyuanAutocomplete({ ...@@ -37,23 +38,28 @@ export default function XinyuanAutocomplete({
fullWidth = true, fullWidth = true,
inputValue, inputValue,
isDisabled = false, isDisabled = false,
isClearable = false,
isLoading = false, isLoading = false,
isOpen, isOpen,
isRequired = false, isRequired = false,
name, name,
onInputChange, onInputChange,
onClear,
onOpenChange, onOpenChange,
onSelectionChange, onSelectionChange,
options = [], options = [],
placeholder = '请选择', placeholder = '请选择',
placement = 'bottom start',
popoverHeader,
renderOption, renderOption,
searchPlaceholder = '输入关键词搜索', searchPlaceholder = '输入关键词搜索',
showSearch = true,
selectedKey, selectedKey,
selectedKeys, selectedKeys,
selectionMode = 'single', selectionMode = 'single',
shouldFilter = true, shouldFilter = true,
valueText, valueText,
variant = 'secondary', variant,
}) { }) {
const selectedKeySet = getSelectedKeySet(selectionMode, selectedKey, selectedKeys); const selectedKeySet = getSelectedKeySet(selectionMode, selectedKey, selectedKeys);
const selectedKeyStringSet = new Set( const selectedKeyStringSet = new Set(
...@@ -87,12 +93,14 @@ export default function XinyuanAutocomplete({ ...@@ -87,12 +93,14 @@ export default function XinyuanAutocomplete({
<Autocomplete <Autocomplete
{...rootSelectionProps} {...rootSelectionProps}
aria-label={ariaLabel || placeholder} aria-label={ariaLabel || placeholder}
allowsEmptyCollection={allowsEmptyCollection}
className={className} className={className}
fullWidth={fullWidth} fullWidth={fullWidth}
isDisabled={isDisabled} isDisabled={isDisabled}
isOpen={isOpen} isOpen={isOpen}
isRequired={isRequired} isRequired={isRequired}
name={name} name={name}
onClear={onClear}
selectionMode={selectionMode} selectionMode={selectionMode}
variant={variant} variant={variant}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
...@@ -104,23 +112,29 @@ export default function XinyuanAutocomplete({ ...@@ -104,23 +112,29 @@ export default function XinyuanAutocomplete({
{resolvedValue} {resolvedValue}
</span> </span>
</Autocomplete.Value> </Autocomplete.Value>
{isClearable ? (
<Autocomplete.ClearButton aria-label={`清除${ariaLabel || placeholder}`} />
) : null}
<Autocomplete.Indicator> <Autocomplete.Indicator>
<ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} /> <ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} />
</Autocomplete.Indicator> </Autocomplete.Indicator>
</Autocomplete.Trigger> </Autocomplete.Trigger>
<Autocomplete.Popover placement="bottom start"> <Autocomplete.Popover placement={placement}>
<Autocomplete.Filter <Autocomplete.Filter
filter={shouldFilter ? filter || defaultFilter : () => true} filter={shouldFilter ? filter || defaultFilter : () => true}
inputValue={inputValue} inputValue={inputValue}
onInputChange={onInputChange} onInputChange={onInputChange}
> >
<SearchField aria-label={searchPlaceholder} fullWidth variant="secondary"> {popoverHeader}
{showSearch ? (
<SearchField aria-label={searchPlaceholder} fullWidth variant={variant}>
<SearchField.Group> <SearchField.Group>
<SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon> <SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={searchPlaceholder} /> <SearchField.Input autoFocus placeholder={searchPlaceholder} />
<SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton> <SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton>
</SearchField.Group> </SearchField.Group>
</SearchField> </SearchField>
) : null}
<ListBox <ListBox
aria-label={ariaLabel || placeholder} aria-label={ariaLabel || placeholder}
items={visibleOptions} items={visibleOptions}
......
...@@ -18,6 +18,21 @@ export const XINYUAN_CHART_SEMANTIC = Object.freeze({ ...@@ -18,6 +18,21 @@ export const XINYUAN_CHART_SEMANTIC = Object.freeze({
neutral: 'var(--rc-chart-neutral)', neutral: 'var(--rc-chart-neutral)',
}); });
export function getXinyuanChartGeometry() {
const rootStyles = typeof document === 'undefined'
? null
: window.getComputedStyle(document.documentElement);
const readToken = (token, fallback) => rootStyles?.getPropertyValue(token).trim() || fallback;
return {
barRadius: 8,
sectorGap: readToken('--xinyuan-chart-sector-gap', '#ffffff'),
sectorRadius: Number.parseFloat(
readToken('--xinyuan-chart-sector-radius', '10'),
) || 10,
};
}
function normalizeChartKey(key) { function normalizeChartKey(key) {
return String(key ?? '').trim(); return String(key ?? '').trim();
} }
......
...@@ -381,18 +381,20 @@ ...@@ -381,18 +381,20 @@
gap: var(--rc-space-2); gap: var(--rc-space-2);
} }
.xy-app-switcher-dialog { .rc-application-identity-well {
width: min(46rem, calc(100vw - 2rem)); display: grid;
max-width: min(46rem, calc(100vw - 2rem)); width: 2.25rem;
max-height: min(42rem, calc(100dvh - 2rem)); height: 2.25rem;
} flex: none;
place-items: center;
.xy-app-identity-well, overflow: hidden;
.xy-app-identity-avatar {
border-radius: var(--rc-radius-control); border-radius: var(--rc-radius-control);
} }
.xy-app-identity-avatar { .rc-application-identity-avatar {
width: 100%;
height: 100%;
border-radius: var(--rc-radius-control);
background: transparent; background: transparent;
} }
...@@ -670,6 +672,85 @@ ...@@ -670,6 +672,85 @@
gap: var(--rc-list-stack-gap); gap: var(--rc-list-stack-gap);
} }
/* Keep wide DataGrid headings and pinned cells aligned to one shared scroll
container, matching the audit-project list behavior. */
.rc-data-table-sticky .data-table-scroll {
position: relative;
max-height: min(60dvh, 42rem);
overflow-x: auto;
overflow-y: auto;
overscroll-behavior: contain;
}
.rc-data-table-sticky .table__resizable-container {
overflow: visible;
}
.rc-data-table-sticky [data-slot='table-header'] .table__column {
position: sticky;
top: 0;
inset-block-start: 0;
z-index: 4;
display: table-cell;
color: var(--muted);
background: var(--surface-secondary);
box-shadow: inset 0 -1px 0 var(--separator);
}
.rc-data-table-sticky [data-slot='table-header'] .table__column.bg-accent-soft {
color: var(--accent-soft-foreground);
background: var(--accent-soft);
}
.rc-data-table-sticky [data-pinned='start'] {
position: sticky;
z-index: 6;
background: var(--surface);
}
.rc-data-table-sticky [data-slot='table-header'] [data-pinned='start'] {
z-index: 8;
background: var(--surface-secondary);
}
@media (hover: hover) {
.rc-data-table-sticky [data-slot='data-grid'] .table__row:hover [data-pinned='start'].table__cell,
.rc-data-table-sticky [data-slot='data-grid'] .table__row[data-hovered='true'] [data-pinned='start'].table__cell {
background-color: var(--color-surface-secondary);
}
@supports (color: color-mix(in oklab, white, black)) {
.rc-data-table-sticky [data-slot='data-grid'] .table__row:hover [data-pinned='start'].table__cell,
.rc-data-table-sticky [data-slot='data-grid'] .table__row[data-hovered='true'] [data-pinned='start'].table__cell {
background-color: color-mix(
in oklab,
var(--color-surface) 40%,
var(--color-surface-secondary)
);
}
}
}
.rc-data-table-sticky [data-slot='data-grid'] .table__row[data-selected='true'] [data-pinned='start'].table__cell {
background-color: var(--color-surface-secondary);
}
@supports (color: color-mix(in oklab, white, black)) {
.rc-data-table-sticky [data-slot='data-grid'] .table__row[data-selected='true'] [data-pinned='start'].table__cell {
background-color: color-mix(
in oklab,
var(--color-surface) 10%,
var(--color-surface-secondary)
);
}
}
@media (max-width: 48rem) {
.rc-data-table-sticky .data-table-scroll {
max-height: min(54dvh, 32rem);
}
}
@media (min-width: 80rem) { @media (min-width: 80rem) {
.rc-chart-grid { .rc-chart-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
...@@ -820,22 +901,23 @@ ...@@ -820,22 +901,23 @@
color: var(--foreground); color: var(--foreground);
background: transparent; background: transparent;
line-height: 1; line-height: 1;
--button-bg-hover: var(--accent-soft); --button-bg-hover: var(--rc-sidebar-item-selected);
--button-bg-pressed: var(--accent-soft); --button-bg-pressed: var(--rc-sidebar-item-selected);
--button-fg: var(--rc-sidebar-item-selected-foreground);
} }
@media (hover: hover) { @media (hover: hover) {
.rc-sidebar-app-trigger.button:hover, .rc-sidebar-app-trigger.button:hover,
.rc-sidebar-app-trigger.button[data-hovered='true'] { .rc-sidebar-app-trigger.button[data-hovered='true'] {
color: var(--foreground); color: var(--rc-sidebar-item-selected-foreground);
background: var(--accent-soft); background: var(--rc-sidebar-item-selected);
} }
} }
.rc-sidebar-app-trigger.button[data-pressed='true'], .rc-sidebar-app-trigger.button[data-pressed='true'],
.rc-sidebar-app-trigger.button[aria-expanded='true'] { .rc-sidebar-app-trigger.button[aria-expanded='true'] {
color: var(--foreground); color: var(--rc-sidebar-item-selected-foreground);
background: var(--accent-soft); background: var(--rc-sidebar-item-selected);
} }
.rc-sidebar-app-popover { .rc-sidebar-app-popover {
...@@ -1121,16 +1203,22 @@ ...@@ -1121,16 +1203,22 @@
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
border: 0; border: 0;
border-right: 1px solid var(--rc-border-default); border-right: 1px solid var(--rc-sidebar-border);
border-radius: 0; border-radius: 0;
color: var(--foreground); color: var(--rc-sidebar-foreground);
background: var(--rc-bg-canvas); background: var(--rc-sidebar-background);
box-shadow: none; box-shadow: none;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
/* Match the Pro Sidebar root selector so its canvas default cannot win the cascade. */
.template-dashboard-shell .app-sidebar-glass.sidebar {
border-right-color: var(--rc-sidebar-border);
background-color: var(--rc-sidebar-background);
}
.template-dashboard-shell .sidebar__header, .template-dashboard-shell .sidebar__header,
.rc-mobile-sidebar .sidebar__header { .rc-mobile-sidebar .sidebar__header {
padding: 6px 1rem 0.5rem; padding: 6px 1rem 0.5rem;
...@@ -1174,7 +1262,7 @@ ...@@ -1174,7 +1262,7 @@
.app-sidebar-title { .app-sidebar-title {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
color: var(--foreground); color: var(--rc-sidebar-foreground);
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 650; font-weight: 650;
line-height: 1.2rem; line-height: 1.2rem;
...@@ -1203,9 +1291,9 @@ ...@@ -1203,9 +1291,9 @@
color: var(--foreground); color: var(--foreground);
background-color: transparent; background-color: transparent;
line-height: 1; line-height: 1;
--button-bg-hover: var(--accent-soft); --button-bg-hover: var(--rc-sidebar-item-selected);
--button-bg-pressed: var(--accent-soft); --button-bg-pressed: var(--rc-sidebar-item-selected);
--button-fg: var(--foreground); --button-fg: var(--rc-sidebar-item-selected-foreground);
} }
.app-sidebar-app-trigger .anticon { .app-sidebar-app-trigger .anticon {
...@@ -1225,16 +1313,16 @@ ...@@ -1225,16 +1313,16 @@
@media (hover: hover) { @media (hover: hover) {
.app-sidebar-app-trigger.button:hover, .app-sidebar-app-trigger.button:hover,
.app-sidebar-app-trigger.button[data-hovered='true'] { .app-sidebar-app-trigger.button[data-hovered='true'] {
color: var(--foreground); color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--accent-soft); background-color: var(--rc-sidebar-item-selected);
} }
} }
.app-sidebar-app-trigger.button:active, .app-sidebar-app-trigger.button:active,
.app-sidebar-app-trigger.button[data-pressed='true'], .app-sidebar-app-trigger.button[data-pressed='true'],
.app-sidebar-app-trigger.button[aria-expanded='true'] { .app-sidebar-app-trigger.button[aria-expanded='true'] {
color: var(--foreground); color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--accent-soft); background-color: var(--rc-sidebar-item-selected);
} }
.app-sidebar-app-dialog { .app-sidebar-app-dialog {
...@@ -1267,7 +1355,7 @@ ...@@ -1267,7 +1355,7 @@
.template-dashboard-shell .sidebar__group-label, .template-dashboard-shell .sidebar__group-label,
.rc-mobile-sidebar .sidebar__group-label { .rc-mobile-sidebar .sidebar__group-label {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
color: var(--rc-text-muted); color: var(--rc-sidebar-muted);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
line-height: 1rem; line-height: 1rem;
...@@ -1280,7 +1368,7 @@ ...@@ -1280,7 +1368,7 @@
gap: 0.75rem; gap: 0.75rem;
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: calc(var(--radius) * 2); border-radius: calc(var(--radius) * 2);
color: var(--foreground); color: var(--rc-sidebar-muted);
font-weight: 400; font-weight: 400;
} }
...@@ -1288,7 +1376,7 @@ ...@@ -1288,7 +1376,7 @@
.rc-mobile-sidebar .sidebar__menu-icon { .rc-mobile-sidebar .sidebar__menu-icon {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
color: var(--muted); color: var(--rc-sidebar-muted);
} }
.template-dashboard-shell .sidebar__menu-icon svg, .template-dashboard-shell .sidebar__menu-icon svg,
...@@ -1299,7 +1387,7 @@ ...@@ -1299,7 +1387,7 @@
.template-dashboard-shell .sidebar__menu-label, .template-dashboard-shell .sidebar__menu-label,
.rc-mobile-sidebar .sidebar__menu-label { .rc-mobile-sidebar .sidebar__menu-label {
color: var(--foreground); color: inherit;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: inherit; font-weight: inherit;
line-height: 1.25rem; line-height: 1.25rem;
...@@ -1308,19 +1396,23 @@ ...@@ -1308,19 +1396,23 @@
.template-dashboard-shell .template-dashboard-shell
.sidebar__menu-item[data-current='true'] .sidebar__menu-item[data-current='true']
.sidebar__menu-item-content, .sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content {
color: var(--rc-sidebar-item-selected-foreground);
background: var(--rc-sidebar-item-selected);
}
.template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content, .template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content,
.template-dashboard-shell .template-dashboard-shell
.sidebar__menu-item[data-hovered='true'] .sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content, .sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content, .rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content,
.rc-mobile-sidebar .rc-mobile-sidebar
.sidebar__menu-item[data-hovered='true'] .sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content { .sidebar__menu-item-content {
color: var(--foreground); color: var(--rc-sidebar-foreground);
background: var(--default); background: var(--rc-sidebar-item-hover);
} }
.template-dashboard-shell .template-dashboard-shell
...@@ -1338,7 +1430,7 @@ ...@@ -1338,7 +1430,7 @@
.rc-mobile-sidebar .rc-mobile-sidebar
.sidebar__menu-item[data-current='true'] .sidebar__menu-item[data-current='true']
:is(.sidebar__menu-icon, .sidebar__menu-label) { :is(.sidebar__menu-icon, .sidebar__menu-label) {
color: var(--foreground); color: var(--rc-sidebar-item-selected-foreground);
} }
.template-dashboard-shell .sidebar__footer, .template-dashboard-shell .sidebar__footer,
...@@ -1356,20 +1448,20 @@ ...@@ -1356,20 +1448,20 @@
gap: 0.625rem; gap: 0.625rem;
padding: 0.375rem 0.5rem !important; padding: 0.375rem 0.5rem !important;
border-radius: calc(var(--radius) * 2) !important; border-radius: calc(var(--radius) * 2) !important;
color: var(--foreground); color: var(--rc-sidebar-foreground);
background: transparent; background: transparent;
text-align: left; text-align: left;
} }
@media (hover: hover) { @media (hover: hover) {
.app-sidebar-user-trigger.dropdown__trigger:hover, .app-sidebar-user-trigger.dropdown__trigger:hover,
.app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'] { .app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'] {
background: var(--default); background: var(--rc-sidebar-item-hover);
} }
} }
.app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'] { .app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'] {
background: var(--default); background: var(--rc-sidebar-item-selected);
} }
.app-sidebar-user-avatar { .app-sidebar-user-avatar {
...@@ -1377,7 +1469,7 @@ ...@@ -1377,7 +1469,7 @@
height: 2rem; height: 2rem;
min-width: 2rem; min-width: 2rem;
flex: 0 0 2rem; flex: 0 0 2rem;
background: var(--default); background: var(--rc-sidebar-item-selected);
} }
.app-sidebar-user-copy { .app-sidebar-user-copy {
...@@ -1399,14 +1491,14 @@ ...@@ -1399,14 +1491,14 @@
} }
.app-sidebar-user-name { .app-sidebar-user-name {
color: var(--foreground); color: var(--rc-sidebar-foreground);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
line-height: 1.125rem; line-height: 1.125rem;
} }
.app-sidebar-user-description { .app-sidebar-user-description {
color: var(--muted); color: var(--rc-sidebar-muted);
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
line-height: 1rem; line-height: 1rem;
...@@ -1418,3 +1510,133 @@ ...@@ -1418,3 +1510,133 @@
border-right: 0; border-right: 0;
} }
} }
/* Fixed split-shell theme: dark navigation, permanently light content canvas. */
.template-dashboard-shell .app-sidebar-glass,
.rc-mobile-sidebar.app-sidebar-glass {
color: var(--rc-sidebar-foreground);
border-color: var(--rc-sidebar-border) !important;
background-color: var(--rc-sidebar-background) !important;
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content {
background: var(--rc-sidebar-item-selected);
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content *,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content * {
color: var(--rc-sidebar-item-selected-foreground) !important;
}
.template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content,
.template-dashboard-shell
.sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-hovered='true']
.sidebar__menu-item-content,
.app-sidebar-user-trigger.dropdown__trigger:hover,
.app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'] {
background: var(--rc-sidebar-item-hover);
}
.app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'],
.app-sidebar-app-trigger.button[data-pressed='true'],
.app-sidebar-app-trigger.button[aria-expanded='true'] {
background: var(--rc-sidebar-item-selected);
}
.app-sidebar-user-avatar,
.app-sidebar-app-trigger.button,
.app-sidebar-user-trigger.dropdown__trigger {
color: var(--rc-sidebar-foreground);
background: transparent;
}
.app-sidebar-user-avatar {
background: var(--rc-sidebar-item-hover);
}
.template-dashboard-shell .sidebar__group-label,
.rc-mobile-sidebar .sidebar__group-label,
.template-dashboard-shell .sidebar__menu-icon,
.rc-mobile-sidebar .sidebar__menu-icon,
.app-sidebar-user-description {
color: var(--rc-sidebar-muted);
}
.template-dashboard-shell .sidebar__menu-label,
.rc-mobile-sidebar .sidebar__menu-label,
.app-sidebar-title,
.app-sidebar-user-name,
.app-sidebar-app-trigger.button {
color: var(--rc-sidebar-foreground);
}
.app-sidebar-user-trigger.dropdown__trigger:focus-visible,
.app-sidebar-app-trigger.button:focus-visible,
.template-dashboard-shell .sidebar__menu-item:focus-visible,
.rc-mobile-sidebar .sidebar__menu-item:focus-visible {
outline: 2px solid var(--rc-sidebar-focus);
outline-offset: 2px;
}
/* Keep the app switcher icon in reverse contrast whenever its state surface is active. */
@media (hover: hover) {
.app-sidebar-app-trigger.button:hover,
.app-sidebar-app-trigger.button[data-hovered='true'],
.rc-sidebar-app-trigger.button:hover,
.rc-sidebar-app-trigger.button[data-hovered='true'] {
color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--rc-sidebar-item-selected);
}
}
.app-sidebar-app-trigger.button:active,
.app-sidebar-app-trigger.button[data-pressed='true'],
.app-sidebar-app-trigger.button[aria-expanded='true'],
.rc-sidebar-app-trigger.button:active,
.rc-sidebar-app-trigger.button[data-pressed='true'],
.rc-sidebar-app-trigger.button[aria-expanded='true'] {
color: var(--rc-sidebar-item-selected-foreground);
background-color: var(--rc-sidebar-item-selected);
}
.app-sidebar-app-trigger.button:is(:hover, :active) .anticon,
.app-sidebar-app-trigger.button[data-hovered='true'] .anticon,
.app-sidebar-app-trigger.button[data-pressed='true'] .anticon,
.app-sidebar-app-trigger.button[aria-expanded='true'] .anticon,
.rc-sidebar-app-trigger.button:is(:hover, :active) .anticon,
.rc-sidebar-app-trigger.button[data-hovered='true'] .anticon,
.rc-sidebar-app-trigger.button[data-pressed='true'] .anticon,
.rc-sidebar-app-trigger.button[aria-expanded='true'] .anticon {
color: inherit;
}
/* HeroUI renders the Autocomplete indicator inside a bare React Aria button.
Reserve its control slot so the icon has a real pointer and keyboard target. */
.autocomplete__trigger > .react-aria-Button {
display: inline-flex;
width: 1.5rem;
min-width: 1.5rem;
min-height: 100%;
flex: 0 0 1.5rem;
align-items: center;
justify-content: center;
}
...@@ -11,38 +11,37 @@ ...@@ -11,38 +11,37 @@
[data-theme="light"], [data-theme="light"],
[data-theme="default"] { [data-theme="default"] {
/* Theme Colors (Light Mode) */ /* Theme Colors (Light Mode) */
--accent: oklch(45.32% 0.1661 27.03); --accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0); --accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03); --background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0045 27.03); --border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03); --danger: oklch(45.32% 0.1661 27.03);
--danger-foreground: oklch(99.11% 0 0); --danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */ --default: oklch(94.00% 0.0000 29.20);
--default: var(--surface-secondary); --default-foreground: oklch(21.03% 0.0059 29.20);
--default-foreground: var(--surface-secondary-foreground); --field-background: oklch(100.00% 0.0000 29.20);
--field-background: var(--surface);
--field-border: transparent; --field-border: transparent;
--field-foreground: var(--surface-foreground); --field-foreground: oklch(21.03% 0.0000 29.20);
--field-placeholder: oklch(55.17% 0.0090 27.03); --field-placeholder: oklch(55.17% 0.0000 29.20);
--focus: oklch(45.32% 0.1661 27.03); --focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(21.03% 0.0045 27.03); --foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0090 27.03); --muted: oklch(55.17% 0.0000 29.20);
--overlay: var(--surface); --overlay: oklch(100.00% 0.0000 29.20);
--overlay-foreground: var(--surface-foreground); --overlay-foreground: oklch(21.03% 0.0000 29.20);
--scrollbar: oklch(87.10% 0.0045 27.03); --scrollbar: oklch(87.10% 0.0000 29.20);
--segment: oklch(100.00% 0.0045 27.03); --segment: oklch(100.00% 0.0000 29.20);
--segment-foreground: oklch(21.03% 0.0045 27.03); --segment-foreground: oklch(21.03% 0.0000 29.20);
--separator: oklch(92.00% 0.0045 27.03); --separator: oklch(92.00% 0.0000 29.20);
--success: oklch(73.29% 0.1952 123.59); --success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.59); --success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(100.00% 0.0022 27.03); --surface: oklch(100.00% 0.0000 29.20);
--surface-foreground: oklch(21.03% 0.0045 27.03); --surface-foreground: oklch(21.03% 0.0000 29.20);
--surface-secondary: oklch(95.24% 0.0036 27.03); --surface-secondary: oklch(95.24% 0.0000 29.20);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03); --surface-secondary-foreground: oklch(21.03% 0.0000 29.20);
--surface-tertiary: oklch(93.73% 0.0036 27.03); --surface-tertiary: oklch(93.73% 0.0000 29.20);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03); --surface-tertiary-foreground: oklch(21.03% 0.0000 29.20);
--warning: oklch(78.19% 0.1599 45.11); --warning: oklch(78.19% 0.1585 45.37);
--warning-foreground: oklch(21.03% 0.0059 45.11); --warning-foreground: oklch(21.03% 0.0059 45.37);
/* Border Radius */ /* Border Radius */
--radius: 0.5rem; --radius: 0.5rem;
...@@ -51,42 +50,90 @@ ...@@ -51,42 +50,90 @@
/* Font Family */ /* Font Family */
/* Make sure to load Inter font in your app */ /* Make sure to load Inter font in your app */
--font-sans: var(--font-inter); --font-sans: var(--font-inter);
/*
* Xinyuan fixed split-shell contract.
* The application canvas always uses this light theme. The Sidebar is the
* only persistent dark plane and must consume these semantic tokens.
*/
--xinyuan-sidebar-background: oklch(16% 0.020 255);
--xinyuan-sidebar-border: oklch(26% 0.022 255);
--xinyuan-sidebar-foreground: oklch(97% 0.004 255);
--xinyuan-sidebar-muted: oklch(76% 0.018 255);
--xinyuan-sidebar-item-hover: oklch(22% 0.026 255);
--xinyuan-sidebar-item-selected: oklch(28% 0.016 255);
--xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255);
--xinyuan-sidebar-focus: oklch(78% 0.105 29.20);
/*
* ECharts is the required chart renderer for Xinyuan applications.
* Ant Design remains the icon source. Resolve these CSS variables to
* concrete colors before assigning them to an ECharts option object.
*/
/* First pass: red -> pink -> orange -> yellow -> blue -> purple -> cyan -> neutral. */
--xinyuan-chart-1: #991b1b;
--xinyuan-chart-2: #db2777;
--xinyuan-chart-3: #ea580c;
--xinyuan-chart-4: #eab308;
--xinyuan-chart-5: #1d4ed8;
--xinyuan-chart-6: #7e22ce;
--xinyuan-chart-7: #0891b2;
--xinyuan-chart-8: #475569;
/* Second pass: stronger/lighter companions for charts with more series. */
--xinyuan-chart-9: #dc2626;
--xinyuan-chart-10: #ec4899;
--xinyuan-chart-11: #f97316;
--xinyuan-chart-12: #facc15;
--xinyuan-chart-13: #60a5fa;
--xinyuan-chart-14: #a855f7;
--xinyuan-chart-15: #06b6d4;
--xinyuan-chart-16: #1e293b;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8;
--xinyuan-chart-axis: #737373;
--xinyuan-chart-label: #737373;
--xinyuan-chart-tooltip-background: #ffffff;
--xinyuan-chart-tooltip-border: #dedede;
--xinyuan-chart-tooltip-foreground: #222222;
--xinyuan-chart-tooltip-shadow: 0 16px 40px rgb(15 23 42 / 14%);
--xinyuan-chart-legend-font-size: 0.75rem;
--xinyuan-chart-legend-line-height: 1rem;
} }
.dark, .dark,
[data-theme="dark"] { [data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
/* Theme Colors (Dark Mode) */ /* Theme Colors (Dark Mode) */
--accent: oklch(45.32% 0.1661 27.03); --accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0); --accent-foreground: oklch(99.11% 0 0);
--background: oklch(12.00% 0.0045 27.03); --background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0045 27.03); --border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62.00% 0.1600 27.03); --danger: oklch(62% 0.16 27.03);
--danger-foreground: oklch(99.11% 0 0); --danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */ --default: oklch(27.40% 0.0000 29.20);
--default: var(--surface-secondary); --default-foreground: oklch(99.11% 0 0);
--default-foreground: var(--surface-secondary-foreground); --field-background: oklch(21.03% 0.0000 29.20);
--field-background: var(--surface);
--field-border: transparent; --field-border: transparent;
--field-foreground: var(--surface-foreground); --field-foreground: oklch(99.11% 0.0000 29.20);
--field-placeholder: oklch(70.50% 0.0090 27.03); --field-placeholder: oklch(70.50% 0.0000 29.20);
--focus: oklch(45.32% 0.1661 27.03); --focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(99.11% 0.0045 27.03); --foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0090 27.03); --muted: oklch(70.50% 0.0000 29.20);
--overlay: var(--surface); --overlay: oklch(21.03% 0.0000 29.20);
--overlay-foreground: var(--surface-foreground); --overlay-foreground: oklch(99.11% 0.0000 29.20);
--scrollbar: oklch(70.50% 0.0045 27.03); --scrollbar: oklch(70.50% 0.0000 29.20);
--segment: oklch(39.64% 0.0045 27.03); --segment: oklch(39.64% 0.0000 29.20);
--segment-foreground: oklch(99.11% 0.0045 27.03); --segment-foreground: oklch(99.11% 0.0000 29.20);
--separator: oklch(25.00% 0.0045 27.03); --separator: oklch(25.00% 0.0000 29.20);
--success: oklch(73.29% 0.1952 123.59); --success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.59); --success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(21.03% 0.0090 27.03); --surface: oklch(21.03% 0.0000 29.20);
--surface-foreground: oklch(99.11% 0.0045 27.03); --surface-foreground: oklch(99.11% 0.0000 29.20);
--surface-secondary: oklch(25.70% 0.0067 27.03); --surface-secondary: oklch(25.70% 0.0000 29.20);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03); --surface-secondary-foreground: oklch(99.11% 0.0000 29.20);
--surface-tertiary: oklch(27.21% 0.0067 27.03); --surface-tertiary: oklch(27.21% 0.0000 29.20);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03); --surface-tertiary-foreground: oklch(99.11% 0.0000 29.20);
--warning: oklch(82.03% 0.1400 49.12); --warning: oklch(82.03% 0.1388 49.38);
--warning-foreground: oklch(21.03% 0.0059 49.12); --warning-foreground: oklch(21.03% 0.0059 49.38);
} }
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
.light, .light,
.default, .default,
[data-theme='light'], [data-theme='light'],
[data-theme='default'], [data-theme='default'] {
.dark,
[data-theme='dark'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */ /* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent); --rc-brand-primary: var(--accent);
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white); --rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
...@@ -27,6 +25,17 @@ ...@@ -27,6 +25,17 @@
var(--background) var(--background)
); );
--rc-text-muted: var(--muted); --rc-text-muted: var(--muted);
--rc-text-empty: color-mix(in oklab, var(--muted) 72%, var(--background));
/* Fixed dark Sidebar aliases. The content canvas remains light. */
--rc-sidebar-background: var(--xinyuan-sidebar-background);
--rc-sidebar-border: var(--xinyuan-sidebar-border);
--rc-sidebar-foreground: var(--xinyuan-sidebar-foreground);
--rc-sidebar-muted: var(--xinyuan-sidebar-muted);
--rc-sidebar-item-hover: var(--xinyuan-sidebar-item-hover);
--rc-sidebar-item-selected: var(--xinyuan-sidebar-item-selected);
--rc-sidebar-item-selected-foreground: var(--xinyuan-sidebar-item-selected-foreground);
--rc-sidebar-focus: var(--xinyuan-sidebar-focus);
--rc-font-sans: var(--font-sans); --rc-font-sans: var(--font-sans);
--rc-font-medium: 500; --rc-font-medium: 500;
...@@ -45,6 +54,7 @@ ...@@ -45,6 +54,7 @@
/* Page geometry only. Component appearance remains owned by HeroUI. */ /* Page geometry only. Component appearance remains owned by HeroUI. */
--rc-shell-inline-gutter: 3rem; --rc-shell-inline-gutter: 3rem;
--rc-sidebar-width: 15rem;
--rc-navbar-inline-padding: var(--rc-shell-inline-gutter); --rc-navbar-inline-padding: var(--rc-shell-inline-gutter);
--rc-page-inline-start: var(--rc-shell-inline-gutter); --rc-page-inline-start: var(--rc-shell-inline-gutter);
--rc-page-inline-end: var(--rc-shell-inline-gutter); --rc-page-inline-end: var(--rc-shell-inline-gutter);
...@@ -138,40 +148,28 @@ ...@@ -138,40 +148,28 @@
); );
/* Data visualization only. Do not reuse UI status colors as a series palette. */ /* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1: oklch(45.32% 0.1661 27.03); --rc-chart-1: var(--xinyuan-chart-1);
--rc-chart-2: oklch(56% 0.135 255); --rc-chart-2: var(--xinyuan-chart-2);
--rc-chart-3: oklch(64% 0.11 220); --rc-chart-3: var(--xinyuan-chart-3);
--rc-chart-4: oklch(56% 0.115 280); --rc-chart-4: var(--xinyuan-chart-4);
--rc-chart-5: oklch(60% 0.095 200); --rc-chart-5: var(--xinyuan-chart-5);
--rc-chart-6: oklch(70% 0.12 80); --rc-chart-6: var(--xinyuan-chart-6);
--rc-chart-7: oklch(60% 0.1 295); --rc-chart-7: var(--xinyuan-chart-7);
--rc-chart-8: oklch(57% 0.035 250); --rc-chart-8: var(--xinyuan-chart-8);
--rc-chart-9: var(--xinyuan-chart-9);
--rc-chart-10: var(--xinyuan-chart-10);
--rc-chart-11: var(--xinyuan-chart-11);
--rc-chart-12: var(--xinyuan-chart-12);
--rc-chart-13: var(--xinyuan-chart-13);
--rc-chart-14: var(--xinyuan-chart-14);
--rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16);
--rc-chart-primary: var(--rc-chart-1); --rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03); --rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-6); --rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-2); --rc-chart-positive: var(--rc-chart-7);
--rc-chart-comparison: var(--rc-chart-4); --rc-chart-comparison: var(--rc-chart-5);
--rc-chart-neutral: var(--rc-chart-8); --rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: 0.75rem; --rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: 1rem; --rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 76%,
var(--background)
);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
--rc-shadow-raised: 0 1px 2px rgb(0 0 0 / 24%),
0 8px 24px rgb(0 0 0 / 24%);
--rc-shadow-overlay: 0 16px 40px rgb(0 0 0 / 38%);
--rc-shadow-card: 0 1px 2px rgb(0 0 0 / 20%),
0 4px 12px rgb(0 0 0 / 18%);
} }
import { getApplications } from '@/services/DataService';
import xinyuanLogo from '@/assets/identity/xy-logo-red.png'; import xinyuanLogo from '@/assets/identity/xy-logo-red.png';
import { EmptyStateSurface } from '@/components/DataTable';
import { ExclamationTriangleIcon, MagnifyingGlassIcon } from '@/components/AppIcons';
import { import {
APP_DISPLAY_NAME, APP_DISPLAY_NAME,
APP_LOGIN_SUBTITLE, APP_LOGIN_SUBTITLE,
APP_ROUTE_NAME, APP_ROUTE_NAME,
} from '@/constants/app'; } from '@/constants/app';
import WarningOutlined from '@ant-design/icons/WarningOutlined'; import { getApplications } from '@/services/DataService';
import { EmptyState } from '@heroui-pro/react/empty-state'; import { EmptyState } from '@heroui-pro/react/empty-state';
import { Alert, Avatar, Card, Chip, Description, Modal, Skeleton, Surface } from '@heroui/react'; import { ItemCard } from '@heroui-pro/react/item-card';
import { ItemCardGroup } from '@heroui-pro/react/item-card-group';
import {
Alert,
Avatar,
Chip,
Modal,
ScrollShadow,
SearchField,
Skeleton,
Surface,
} from '@heroui/react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
const CURRENT_APPLICATION_NAME = APP_ROUTE_NAME;
function normalizeLabelList(items) { function normalizeLabelList(items) {
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
return []; return [];
...@@ -36,28 +50,26 @@ function resolveApplicationUrl(application) { ...@@ -36,28 +50,26 @@ function resolveApplicationUrl(application) {
function normalizeApplication(application, fallback = {}) { function normalizeApplication(application, fallback = {}) {
const name = application?.name || fallback?.name || ''; const name = application?.name || fallback?.name || '';
const isCurrentSystem = name === APP_ROUTE_NAME; const current = name === CURRENT_APPLICATION_NAME;
return { return {
name, name,
title: isCurrentSystem title: current
? APP_DISPLAY_NAME ? APP_DISPLAY_NAME
: application?.title || : application?.title ||
fallback?.title || fallback?.title ||
application?.name || application?.name ||
fallback?.name || fallback?.name ||
'未命名应用', '未命名应用',
description: isCurrentSystem description: current
? APP_LOGIN_SUBTITLE ? APP_LOGIN_SUBTITLE
: application?.description || : application?.description ||
fallback?.description || fallback?.description ||
fallback?.title || fallback?.title ||
application?.title || application?.title ||
'进入对应应用继续处理业务', '进入对应应用继续处理业务',
logo: application?.logo || fallback?.logo || null, logo: xinyuanLogo,
url: resolveApplicationUrl({ url: resolveApplicationUrl({ name }),
name,
}),
access: application?.access || fallback?.access || '', access: application?.access || fallback?.access || '',
status: application?.status || fallback?.status || '', status: application?.status || fallback?.status || '',
type: application?.type || fallback?.type || '', type: application?.type || fallback?.type || '',
...@@ -75,13 +87,11 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -75,13 +87,11 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
const fallbackMap = new Map( const fallbackMap = new Map(
fallbackApplications.map((application) => [application.name, application]), fallbackApplications.map((application) => [application.name, application]),
); );
const normalizedFetched = fetchedApplications const normalizedFetched = fetchedApplications
.map((application) => .map((application) =>
normalizeApplication(application, fallbackMap.get(application?.name)), normalizeApplication(application, fallbackMap.get(application?.name)),
) )
.filter((application) => application.name); .filter((application) => application.name);
const mergedMap = new Map( const mergedMap = new Map(
normalizedFetched.map((application) => [application.name, application]), normalizedFetched.map((application) => [application.name, application]),
); );
...@@ -91,23 +101,20 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -91,23 +101,20 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
return; return;
} }
mergedMap.set( mergedMap.set(application.name, normalizeApplication(application, application));
application.name,
normalizeApplication(application, application),
);
}); });
if (!mergedMap.has(APP_ROUTE_NAME)) { const currentApplication = mergedMap.get(CURRENT_APPLICATION_NAME);
mergedMap.set( mergedMap.set(
APP_ROUTE_NAME, CURRENT_APPLICATION_NAME,
normalizeApplication({ normalizeApplication(
name: APP_ROUTE_NAME, { name: CURRENT_APPLICATION_NAME },
currentApplication || {
title: APP_DISPLAY_NAME, title: APP_DISPLAY_NAME,
description: APP_LOGIN_SUBTITLE, description: APP_LOGIN_SUBTITLE,
logo: xinyuanLogo, },
}), ),
); );
}
return Array.from(mergedMap.values()).sort((left, right) => { return Array.from(mergedMap.values()).sort((left, right) => {
const orderDifference = Number(left.order || 0) - Number(right.order || 0); const orderDifference = Number(left.order || 0) - Number(right.order || 0);
...@@ -120,23 +127,18 @@ function mergeApplications(fetchedApplications, fallbackApplications) { ...@@ -120,23 +127,18 @@ function mergeApplications(fetchedApplications, fallbackApplications) {
} }
function unwrapApplications(response) { function unwrapApplications(response) {
if (Array.isArray(response)) { const candidates = [
return response; response,
} response?.data,
response?.applications,
if (Array.isArray(response?.data)) { response?.apps,
return response.data; response?.records,
} response?.rows,
response?.list,
if (Array.isArray(response?.applications)) { response?.result,
return response.applications; ];
}
return candidates.find(Array.isArray) || [];
if (Array.isArray(response?.result)) {
return response.result;
}
return [];
} }
function buildApplicationSections(applications, applicationGroups) { function buildApplicationSections(applications, applicationGroups) {
...@@ -191,6 +193,10 @@ function buildApplicationSections(applications, applicationGroups) { ...@@ -191,6 +193,10 @@ function buildApplicationSections(applications, applicationGroups) {
} }
function isCurrentApplication(application) { function isCurrentApplication(application) {
if (application?.name === CURRENT_APPLICATION_NAME) {
return true;
}
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return false; return false;
} }
...@@ -200,9 +206,8 @@ function isCurrentApplication(application) { ...@@ -200,9 +206,8 @@ function isCurrentApplication(application) {
if (application?.url?.startsWith('http')) { if (application?.url?.startsWith('http')) {
try { try {
const url = new URL(application.url); return new URL(application.url).pathname === currentPath;
return url.pathname === currentPath; } catch {
} catch (error) {
return false; return false;
} }
} }
...@@ -215,82 +220,78 @@ function isCurrentApplication(application) { ...@@ -215,82 +220,78 @@ function isCurrentApplication(application) {
); );
} }
function markCurrentApplication(applications) { function ApplicationCard({ application, current }) {
if (!Array.isArray(applications) || applications.length === 0) {
return [];
}
const currentApplication = applications.find(isCurrentApplication);
return applications.map((application) => ({
...application,
current: currentApplication?.name === application.name,
}));
}
function ApplicationCard({ application }) {
const current = application.current === true;
const target = application?.url?.startsWith('http') ? '_blank' : undefined; const target = application?.url?.startsWith('http') ? '_blank' : undefined;
const description = application.description?.trim();
const hasDistinctDescription =
description && description !== application.title.trim();
return ( return (
<Card className="h-full" variant="secondary"> <ItemCard
<Card.Content>
<a
aria-current={current ? 'page' : undefined} aria-current={current ? 'page' : undefined}
className="flex min-w-0 items-center gap-3 no-underline" className="h-full min-w-0 no-underline"
href={application.url} href={application.url}
rel={target ? 'noreferrer' : undefined} rel={target ? 'noreferrer' : undefined}
render={(props) => <a {...props} />}
target={target} target={target}
variant={current ? 'outline' : 'default'}
> >
<Surface <ItemCard.Icon aria-hidden="true">
aria-hidden="true" <Surface className="rc-application-identity-well" variant="default">
className="xy-app-identity-well grid size-9 shrink-0 place-items-center overflow-hidden"
variant="tertiary"
>
<Avatar <Avatar
className="xy-app-identity-avatar size-full" className="rc-application-identity-avatar"
color="accent" color="accent"
variant="soft" variant="soft"
> >
{application.logo ? ( <Avatar.Image
<Avatar.Image alt="" className="object-contain p-1.5" src={application.logo} /> alt=""
) : null} className="object-contain p-1.5"
<Avatar.Fallback>{application.title.trim().slice(0, 1) || '应'}</Avatar.Fallback> src={application.logo || xinyuanLogo}
/>
<Avatar.Fallback>
<img alt="" className="object-contain p-1.5" src={xinyuanLogo} />
</Avatar.Fallback>
</Avatar> </Avatar>
</Surface> </Surface>
<span className="min-w-0 flex-1"> </ItemCard.Icon>
<span className="block truncate text-sm font-medium leading-5" title={application.title}> <ItemCard.Content>
<ItemCard.Title title={application.title}>
{application.title} {application.title}
</span> </ItemCard.Title>
<Description {hasDistinctDescription ? (
className="mt-0.5 block truncate leading-4" <ItemCard.Description title={description}>
title={application.description} {description}
> </ItemCard.Description>
{application.description || application.name || '应用'} ) : null}
</Description> </ItemCard.Content>
</span> {current ? (
{current ? <Chip color="accent" size="sm">当前</Chip> : null} <ItemCard.Action>
</a> <Chip color="accent" size="sm">当前</Chip>
</Card.Content> </ItemCard.Action>
</Card> ) : null}
</ItemCard>
); );
} }
function LoadingSkeleton() { function LoadingSkeleton() {
return ( return (
<div className="grid gap-3 sm:grid-cols-2" aria-label="正在加载应用"> <ItemCardGroup
aria-label="正在加载应用"
className="grid-cols-1 sm:grid-cols-2"
columns={2}
layout="grid"
variant="transparent"
>
{Array.from({ length: 6 }).map((_, index) => ( {Array.from({ length: 6 }).map((_, index) => (
<Card key={index} variant="secondary"> <ItemCard key={index} variant="default">
<Card.Content className="flex items-center gap-3"> <ItemCard.Icon><Skeleton className="size-9" /></ItemCard.Icon>
<Skeleton className="size-9 shrink-0" /> <ItemCard.Content className="gap-2">
<div className="min-w-0 flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" /> <Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-full" /> <Skeleton className="h-3 w-full" />
</div> </ItemCard.Content>
</Card.Content> </ItemCard>
</Card>
))} ))}
</div> </ItemCardGroup>
); );
} }
...@@ -314,72 +315,84 @@ export default function ApplicationSwitcherDialog({ ...@@ -314,72 +315,84 @@ export default function ApplicationSwitcherDialog({
) )
.filter((application) => application.name); .filter((application) => application.name);
}, [userInfo]); }, [userInfo]);
const [applications, setApplications] = useState(() => const [applications, setApplications] = useState(() =>
markCurrentApplication(fallbackApplications), mergeApplications([], fallbackApplications),
); );
const [status, setStatus] = useState( const [status, setStatus] = useState(
fallbackApplications.length > 0 ? 'success' : 'idle', fallbackApplications.length > 0 ? 'success' : 'idle',
); );
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [applicationSearch, setApplicationSearch] = useState('');
const currentApplicationName = useMemo(
() =>
applications.find(isCurrentApplication)?.name || CURRENT_APPLICATION_NAME,
[applications],
);
const applicationSections = useMemo( const applicationSections = useMemo(
() => buildApplicationSections(applications, siteInfo?.group), () => buildApplicationSections(applications, siteInfo?.group),
[applications, siteInfo?.group], [applications, siteInfo?.group],
); );
const filteredApplicationSections = useMemo(() => {
const keyword = applicationSearch.trim().toLocaleLowerCase();
if (!keyword) {
return applicationSections;
}
return applicationSections
.map((section) => ({
...section,
applications: section.applications.filter((application) => [
application.title,
application.description,
application.name,
].some((value) => String(value || '').toLocaleLowerCase().includes(keyword))),
}))
.filter((section) => section.applications.length > 0);
}, [applicationSearch, applicationSections]);
useEffect(() => { useEffect(() => {
if (fallbackApplications.length === 0) { if (fallbackApplications.length === 0) {
return; return;
} }
setApplications((previous) => { setApplications((previous) =>
if (previous.length > 0) { mergeApplications(previous, fallbackApplications),
return previous; );
}
return markCurrentApplication(fallbackApplications);
});
}, [fallbackApplications]); }, [fallbackApplications]);
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setApplicationSearch('');
return; return;
} }
let cancelled = false; let cancelled = false;
const loadApplications = async () => { const loadApplications = async () => {
setStatus((previous) => (previous === 'success' ? previous : 'loading')); setStatus((previous) => (previous === 'success' ? previous : 'loading'));
setErrorMessage(''); setErrorMessage('');
try { try {
const response = await getApplications(); const response = await getApplications();
const fetchedApplications = unwrapApplications(response);
if (cancelled) { if (cancelled) {
return; return;
} }
const mergedApplications = mergeApplications( setApplications(
fetchedApplications, mergeApplications(unwrapApplications(response), fallbackApplications),
fallbackApplications,
); );
setApplications(markCurrentApplication(mergedApplications));
setStatus('success'); setStatus('success');
} catch (error) { } catch {
if (cancelled) { if (cancelled) {
return; return;
} }
setApplications(markCurrentApplication(fallbackApplications)); setApplications(mergeApplications([], fallbackApplications));
setStatus('error'); setStatus('error');
setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。'); setErrorMessage('应用列表加载失败,已回退展示当前账号上的应用信息。');
} }
}; };
loadApplications(); loadApplications();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
...@@ -395,70 +408,93 @@ export default function ApplicationSwitcherDialog({ ...@@ -395,70 +408,93 @@ export default function ApplicationSwitcherDialog({
if (!isOpen) onClose?.(); if (!isOpen) onClose?.();
}} }}
> >
<Modal.Container <Modal.Container placement="center" scroll="inside" size="lg">
className="w-full p-4 sm:w-full sm:p-4"
placement="center"
scroll="inside"
size="lg"
>
<Modal.Dialog <Modal.Dialog
aria-label="应用切换" aria-label="应用切换"
className="xy-app-switcher-dialog p-7" className="max-h-[min(42rem,calc(100dvh-2rem))] bg-background p-7"
style={{
maxWidth: 'min(46rem, calc(100vw - 2rem))',
width: 'min(46rem, calc(100vw - 2rem))',
}}
> >
<Modal.CloseTrigger <Modal.CloseTrigger
aria-label="关闭应用切换" aria-label="关闭应用切换"
style={{ insetInlineEnd: '1.75rem', top: '1.75rem' }} style={{ insetInlineEnd: '1.75rem', top: '1.75rem' }}
/> />
<Modal.Header> <Modal.Header>
<Modal.Heading>应用切换</Modal.Heading> <SearchField
<Description>选择你可访问的应用</Description> aria-label="搜索应用"
className="w-full min-w-0"
fullWidth
value={applicationSearch}
variant="secondary"
onChange={setApplicationSearch}
>
<SearchField.Group>
<SearchField.SearchIcon>
<MagnifyingGlassIcon size={16} />
</SearchField.SearchIcon>
<SearchField.Input placeholder="搜索应用" />
<SearchField.ClearButton aria-label="清空应用搜索" />
</SearchField.Group>
</SearchField>
</Modal.Header> </Modal.Header>
<Modal.Body className="flex min-h-0 overflow-hidden" style={{ marginTop: '0.75rem' }}>
<Modal.Body style={{ marginTop: '0.75rem' }}> <ScrollShadow
className="h-full min-h-0 flex-1 overflow-y-auto pe-1 pb-1"
hideScrollBar={false}
offset={12}
size={24}
>
{status === 'error' && errorMessage ? ( {status === 'error' && errorMessage ? (
<Alert status="warning"> <Alert className="mb-4" status="warning">
<Alert.Indicator> <Alert.Indicator><ExclamationTriangleIcon size={20} /></Alert.Indicator>
<WarningOutlined className="size-5" /> <Alert.Content><Alert.Description>{errorMessage}</Alert.Description></Alert.Content>
</Alert.Indicator>
<Alert.Content>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Content>
</Alert> </Alert>
) : null} ) : null}
{status === 'loading' && applications.length === 0 ? ( {status === 'loading' && applications.length === 0 ? (
<LoadingSkeleton /> <LoadingSkeleton />
) : applications.length > 0 ? ( ) : filteredApplicationSections.length > 0 ? (
<div className="space-y-6"> <div className="space-y-6">
{applicationSections.map((section) => ( {filteredApplicationSections.map((section) => (
<section key={section.key}> <ItemCardGroup
<div className="mb-3 flex items-center justify-between gap-3"> key={section.key}
<h3 className="text-sm font-medium leading-5"> className="grid-cols-1 sm:grid-cols-2"
{section.label} columns={2}
</h3> layout="grid"
<Description> variant="transparent"
{section.applications.length} 个应用 >
</Description> <ItemCardGroup.Header>
</div> <ItemCardGroup.Title>{section.label}</ItemCardGroup.Title>
<div className="grid gap-3 sm:grid-cols-2"> </ItemCardGroup.Header>
{section.applications.map((application) => ( {section.applications.map((application) => (
<ApplicationCard <ApplicationCard
key={application.name} key={application.name}
application={application} application={application}
current={application.name === currentApplicationName}
/> />
))} ))}
</div> </ItemCardGroup>
</section>
))} ))}
</div> </div>
) : ( ) : (
<EmptyStateSurface>
<EmptyState size="md"> <EmptyState size="md">
<EmptyState.Header> <EmptyState.Header>
<EmptyState.Title>暂无可切换应用</EmptyState.Title> <EmptyState.Title>
<EmptyState.Description>当前账号还没有分配其它应用权限。</EmptyState.Description> {applicationSearch.trim() ? '没有找到匹配的应用' : '暂无可切换应用'}
</EmptyState.Title>
<EmptyState.Description>
{applicationSearch.trim()
? '请尝试其他关键词。'
: '当前账号还没有分配其它应用权限。'}
</EmptyState.Description>
</EmptyState.Header> </EmptyState.Header>
</EmptyState> </EmptyState>
</EmptyStateSurface>
)} )}
</ScrollShadow>
</Modal.Body> </Modal.Body>
</Modal.Dialog> </Modal.Dialog>
</Modal.Container> </Modal.Container>
......
...@@ -14,10 +14,9 @@ import { Avatar, Badge, Button, Dropdown, Label, Toast, Tooltip } from '@heroui/ ...@@ -14,10 +14,9 @@ import { Avatar, Badge, Button, Dropdown, Label, Toast, Tooltip } from '@heroui/
import { useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { Heading } from 'react-aria-components/Dialog'; import { Heading } from 'react-aria-components/Dialog';
import { history, Outlet, useLocation } from 'umi'; import { history, Outlet, useLocation } from 'umi';
import { AppGrid, Archive, BarChart, Bell, LogOut, Moon, Sun } from './AppIcons'; import { AppGrid, Archive, BarChart, Bell, LogOut } from './AppIcons';
const APP_NAME = APP_DISPLAY_NAME; const APP_NAME = APP_DISPLAY_NAME;
const THEME_STORAGE_KEY = 'xinyuan_color_theme';
const menuGroups = [ const menuGroups = [
{ {
...@@ -29,18 +28,6 @@ const menuGroups = [ ...@@ -29,18 +28,6 @@ const menuGroups = [
}, },
]; ];
function getInitialTheme() {
if (typeof window === 'undefined') return 'light';
try {
return window.localStorage.getItem(THEME_STORAGE_KEY) === 'dark'
? 'dark'
: 'light';
} catch {
return 'light';
}
}
function getRouteTitle(pathname) { function getRouteTitle(pathname) {
if (pathname === '/notifications') return '通知消息'; if (pathname === '/notifications') return '通知消息';
for (const group of menuGroups) { for (const group of menuGroups) {
...@@ -223,13 +210,9 @@ function SidebarContents({ onOpenApplicationSwitcher, pathname, userInfo }) { ...@@ -223,13 +210,9 @@ function SidebarContents({ onOpenApplicationSwitcher, pathname, userInfo }) {
function DashboardNavbar({ function DashboardNavbar({
notificationCount, notificationCount,
onOpenNotifications, onOpenNotifications,
onThemeChange,
theme,
title, title,
}) { }) {
const { isMobile, isOpen } = useSidebar(); const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
const unreadCount = Math.max(0, Number(notificationCount) || 0); const unreadCount = Math.max(0, Number(notificationCount) || 0);
return ( return (
...@@ -262,20 +245,6 @@ function DashboardNavbar({ ...@@ -262,20 +245,6 @@ function DashboardNavbar({
</Badge.Anchor> </Badge.Anchor>
<Tooltip.Content>通知消息</Tooltip.Content> <Tooltip.Content>通知消息</Tooltip.Content>
</Tooltip> </Tooltip>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="app-navbar-icon-button rc-navbar-icon-button"
size="sm"
variant="ghost"
onPress={() => onThemeChange(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Tooltip.Content>{themeLabel}</Tooltip.Content>
</Tooltip>
</div> </div>
</Navbar.Header> </Navbar.Header>
</Navbar> </Navbar>
...@@ -286,25 +255,10 @@ export default function ProjectManagerLayout() { ...@@ -286,25 +255,10 @@ export default function ProjectManagerLayout() {
const location = useLocation(); const location = useLocation();
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false); const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [theme, setTheme] = useState(getInitialTheme);
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0); const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
const userInfo = useContext(UserContext); const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext); const siteInfo = useContext(SiteContext);
useLayoutEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.classList.toggle('light', theme === 'light');
root.dataset.theme = theme;
root.style.colorScheme = theme;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// Theme still applies for this session when storage is unavailable.
}
}, [theme]);
useLayoutEffect(() => { useLayoutEffect(() => {
document.title = APP_NAME; document.title = APP_NAME;
}, []); }, []);
...@@ -336,7 +290,7 @@ export default function ProjectManagerLayout() { ...@@ -336,7 +290,7 @@ export default function ProjectManagerLayout() {
return ( return (
<Sidebar.Provider <Sidebar.Provider
className={`template-dashboard-shell rc-app-shell ${theme}`} className="template-dashboard-shell rc-app-shell light"
collapsible="offcanvas" collapsible="offcanvas"
navigate={(href) => history.push(href)} navigate={(href) => history.push(href)}
open={sidebarOpen} open={sidebarOpen}
...@@ -351,13 +305,10 @@ export default function ProjectManagerLayout() { ...@@ -351,13 +305,10 @@ export default function ProjectManagerLayout() {
<DashboardNavbar <DashboardNavbar
notificationCount={unreadNotificationCount} notificationCount={unreadNotificationCount}
onOpenNotifications={() => history.push('/notifications')} onOpenNotifications={() => history.push('/notifications')}
theme={theme}
title={routeTitle} title={routeTitle}
onThemeChange={setTheme}
/> />
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto"> <div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<div className="rc-app-page"> <div className="rc-app-page">
<h1 className="sr-only">{routeTitle}</h1>
<Outlet /> <Outlet />
</div> </div>
</div> </div>
......
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
9. 页面顶部提供 `Segment` 视图切换,可在“卡片视图 / 表格视图”之间切换 9. 页面顶部提供 `Segment` 视图切换,可在“卡片视图 / 表格视图”之间切换
10. 卡片视图用于浏览案例摘要;每条案例使用独立一级业务卡片,分类/等级/标签、标题、类型、摘要和时间信息按层级展示,点击卡片后以弹框方式查看并编辑详情 10. 卡片视图用于浏览案例摘要;每条案例使用独立一级业务卡片,分类/等级/标签、标题、类型、摘要和时间信息按层级展示,点击卡片后以弹框方式查看并编辑详情
11. 表格视图展示标题、分类、标签、案例类型、关键字、来源、等级、年份、更新时间等字段;宽表在 DataGrid 内部横向滚动,各字段只在所属单元格内单行截断,完整值通过 HeroUI Tooltip 查看,不允许跨列覆盖;首列标题仅用于展示,不承担点击操作,查看详情统一从右侧操作列进入 11. 表格视图展示标题、分类、标签、案例类型、关键字、来源、等级、年份、更新时间等字段;宽表在 DataGrid 内部横向滚动,各字段只在所属单元格内单行截断,完整值通过 HeroUI Tooltip 查看,不允许跨列覆盖;首列标题仅用于展示,不承担点击操作,查看详情统一从右侧操作列进入
12. 表格视图按页加载案例,固定每页 20 条,并显示页码、当前页和匹配总数;分页栏不提供每页条数选择器 12. 表格视图按页加载案例,默认每页 20 条,并显示页码、当前页和匹配总数;分页栏提供 20、50、100 条/页选择器,切换后回到第一页并按原查询条件重新加载
13. 表格视图支持单选、全选当前页案例,并显示当前勾选数量 13. 表格视图支持单选、全选当前页案例,并显示当前勾选数量
### 4.5 编辑案例 ### 4.5 编辑案例
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
Button, Button,
Checkbox, Checkbox,
Chip, Chip,
CloseButton,
Disclosure, Disclosure,
Input, Input,
Label, Label,
...@@ -77,7 +78,7 @@ import { ...@@ -77,7 +78,7 @@ import {
const ALL_CATEGORY_KEY = '__all__'; const ALL_CATEGORY_KEY = '__all__';
const CARD_VIEW_MODE = 'card'; const CARD_VIEW_MODE = 'card';
const TABLE_VIEW_MODE = 'table'; const TABLE_VIEW_MODE = 'table';
const XINYUAN_4XL_DIALOG_CLASS = 'sm:max-w-[68rem] p-7'; const XINYUAN_4XL_DIALOG_CLASS = 'bg-background p-7 sm:max-w-[68rem]';
const FALLBACK_CASE_FIELDS = [ const FALLBACK_CASE_FIELDS = [
{ name: 'label', label: '标题', type: 'text' }, { name: 'label', label: '标题', type: 'text' },
{ name: 'category', label: '分类', type: 'text' }, { name: 'category', label: '分类', type: 'text' },
...@@ -273,17 +274,14 @@ function KeywordEditor({ ...@@ -273,17 +274,14 @@ function KeywordEditor({
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{keywords.map((keyword) => ( {keywords.map((keyword) => (
<Chip key={keyword} color="accent" size="sm" variant="soft"> <Chip key={keyword} color="accent" size="sm" variant="soft">
<span className="truncate">{keyword}</span> <Chip.Label className="max-w-48 truncate">{keyword}</Chip.Label>
<Tooltip delay={0}> <Tooltip delay={0}>
<Button <CloseButton
isIconOnly
aria-label={`删除关键字 ${keyword}`} aria-label={`删除关键字 ${keyword}`}
size="sm"
variant="tertiary"
onPress={() => removeKeyword(keyword)} onPress={() => removeKeyword(keyword)}
> >
<XMarkIcon className="size-3" /> <XMarkIcon size={14} />
</Button> </CloseButton>
<Tooltip.Content>删除关键字</Tooltip.Content> <Tooltip.Content>删除关键字</Tooltip.Content>
</Tooltip> </Tooltip>
</Chip> </Chip>
...@@ -525,7 +523,7 @@ export default function ResourceBookingPage() { ...@@ -525,7 +523,7 @@ export default function ResourceBookingPage() {
const [activeCaseId, setActiveCaseId] = useState(''); const [activeCaseId, setActiveCaseId] = useState('');
const [selectedCaseIds, setSelectedCaseIds] = useState([]); const [selectedCaseIds, setSelectedCaseIds] = useState([]);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const pageSize = DEFAULT_PAGE_SIZE; const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
...@@ -837,6 +835,15 @@ export default function ResourceBookingPage() { ...@@ -837,6 +835,15 @@ export default function ResourceBookingPage() {
loadCaseList(nextPage, pageSize); loadCaseList(nextPage, pageSize);
} }
function handlePageSizeChange(nextPageSize) {
if (nextPageSize === pageSize) {
return;
}
setPage(0);
setPageSize(nextPageSize);
}
function resetCreateForm() { function resetCreateForm() {
setCreateForm(buildEmptyCaseForm()); setCreateForm(buildEmptyCaseForm());
} }
...@@ -1348,7 +1355,6 @@ export default function ResourceBookingPage() { ...@@ -1348,7 +1355,6 @@ export default function ResourceBookingPage() {
</div> </div>
) : null} ) : null}
<h2>案例列表</h2>
<div className="rc-page-toolbar"> <div className="rc-page-toolbar">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2"> <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<div className="w-full min-w-0 flex-none sm:w-80 lg:w-96"> <div className="w-full min-w-0 flex-none sm:w-80 lg:w-96">
...@@ -1493,10 +1499,12 @@ export default function ResourceBookingPage() { ...@@ -1493,10 +1499,12 @@ export default function ResourceBookingPage() {
)} )}
<DataTablePagination <DataTablePagination
pageSizeOptions={[20, 50, 100]}
page={page} page={page}
pageSize={pageSize} pageSize={pageSize}
total={total} total={total}
onPageChange={handlePageChange} onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
isDisabled={loading} isDisabled={loading}
/> />
</div> </div>
...@@ -1719,15 +1727,20 @@ export default function ResourceBookingPage() { ...@@ -1719,15 +1727,20 @@ export default function ResourceBookingPage() {
<Modal.Dialog className={XINYUAN_4XL_DIALOG_CLASS}> <Modal.Dialog className={XINYUAN_4XL_DIALOG_CLASS}>
<Modal.CloseTrigger aria-label="关闭案例详情" /> <Modal.CloseTrigger aria-label="关闭案例详情" />
<Modal.Header> <Modal.Header>
<Modal.Heading> <div className="flex w-full min-w-0 items-start gap-3 pr-16">
<span className="inline-flex min-w-0 items-center gap-2"> <div className="min-w-0 flex-1">
<span>案例详情</span> <div className="flex min-w-0 items-center gap-2">
<Modal.Heading className="min-w-0">
{!hideDetailTitle && activeCase
? activeCase.title || activeCase.label || '案例详情'
: '案例详情'}
</Modal.Heading>
{activeCase ? ( {activeCase ? (
<Tooltip delay={0}> <Tooltip delay={0}>
<Button <Button
isIconOnly isIconOnly
aria-label="复制详情" aria-label="复制详情"
className="size-6" className="size-6 shrink-0"
isDisabled={saving} isDisabled={saving}
size="sm" size="sm"
variant="tertiary" variant="tertiary"
...@@ -1738,20 +1751,19 @@ export default function ResourceBookingPage() { ...@@ -1738,20 +1751,19 @@ export default function ResourceBookingPage() {
<Tooltip.Content>复制详情</Tooltip.Content> <Tooltip.Content>复制详情</Tooltip.Content>
</Tooltip> </Tooltip>
) : null} ) : null}
</span> </div>
</Modal.Heading> {activeCase && !hideDetailTitle ? (
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
<span>创建于 {activeCase.createdAtLabel}</span>
<span>最近更新 {activeCase.updatedAtLabel}</span>
</div>
) : null}
</div>
</div>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{activeCase ? ( {activeCase ? (
<div className="space-y-6"> <div className="space-y-6">
{!hideDetailTitle ? (
<h2 className="break-words text-lg font-semibold">{activeCase.title || activeCase.label}</h2>
) : null}
<div className="flex flex-wrap gap-x-4 gap-y-2">
<span>创建于 {activeCase.createdAtLabel}</span>
<span>最近更新 {activeCase.updatedAtLabel}</span>
</div>
{visibleDetailViews.length > 1 ? ( {visibleDetailViews.length > 1 ? (
<Tabs selectedKey={activeDetailView?.name} onSelectionChange={(key) => setActiveDetailViewName(String(key))}> <Tabs selectedKey={activeDetailView?.name} onSelectionChange={(key) => setActiveDetailViewName(String(key))}>
<Tabs.ListContainer className="w-fit max-w-full min-w-0"> <Tabs.ListContainer className="w-fit max-w-full min-w-0">
...@@ -1767,10 +1779,7 @@ export default function ResourceBookingPage() { ...@@ -1767,10 +1779,7 @@ export default function ResourceBookingPage() {
) : null} ) : null}
{!isDetailEditing ? ( {!isDetailEditing ? (
<Surface <div className="px-1 py-2 sm:px-2 sm:py-3">
className="px-5 py-2 sm:px-6 sm:py-3"
variant="secondary"
>
<div className="grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"> <div className="grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2">
{activeDetailColumns.map((column) => { {activeDetailColumns.map((column) => {
const value = getFieldValue(activeCase.rawRecord, column.field); const value = getFieldValue(activeCase.rawRecord, column.field);
...@@ -1783,7 +1792,7 @@ export default function ResourceBookingPage() { ...@@ -1783,7 +1792,7 @@ export default function ResourceBookingPage() {
); );
})} })}
</div> </div>
</Surface> </div>
) : ( ) : (
<div className="grid min-w-0 gap-4 md:grid-cols-2"> <div className="grid min-w-0 gap-4 md:grid-cols-2">
{activeDetailColumns.map((column) => { {activeDetailColumns.map((column) => {
...@@ -2051,7 +2060,7 @@ export default function ResourceBookingPage() { ...@@ -2051,7 +2060,7 @@ export default function ResourceBookingPage() {
<Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} /> <Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} />
<Modal.Backdrop> <Modal.Backdrop>
<Modal.Container placement="center" size="md"> <Modal.Container placement="center" size="md">
<Modal.Dialog> <Modal.Dialog className="bg-background">
<Modal.CloseTrigger aria-label="关闭批量删除确认" /> <Modal.CloseTrigger aria-label="关闭批量删除确认" />
<Modal.Header> <Modal.Header>
<Modal.Heading>确认批量删除案例</Modal.Heading> <Modal.Heading>确认批量删除案例</Modal.Heading>
...@@ -2092,7 +2101,7 @@ export default function ResourceBookingPage() { ...@@ -2092,7 +2101,7 @@ export default function ResourceBookingPage() {
<Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} /> <Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} />
<Modal.Backdrop> <Modal.Backdrop>
<Modal.Container placement="center" size="md"> <Modal.Container placement="center" size="md">
<Modal.Dialog> <Modal.Dialog className="bg-background">
<Modal.CloseTrigger aria-label="关闭删除确认" /> <Modal.CloseTrigger aria-label="关闭删除确认" />
<Modal.Header> <Modal.Header>
<Modal.Heading>确认删除案例</Modal.Heading> <Modal.Heading>确认删除案例</Modal.Heading>
...@@ -2140,7 +2149,7 @@ export default function ResourceBookingPage() { ...@@ -2140,7 +2149,7 @@ export default function ResourceBookingPage() {
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<div className="space-y-6"> <div className="space-y-6">
<Surface className="p-4" variant="secondary"> <Surface className="p-4" variant="default">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<span>{importFile ? importFile.name : '还没有选择 Excel 文件'}</span> <span>{importFile ? importFile.name : '还没有选择 Excel 文件'}</span>
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
import { import {
colorizeXinyuanChartTooltipPayload, colorizeXinyuanChartTooltipPayload,
createXinyuanChartColorMap, createXinyuanChartColorMap,
getXinyuanChartGeometry,
XINYUAN_CHART_SEMANTIC, XINYUAN_CHART_SEMANTIC,
} from '@/design-system/chartPalette'; } from '@/design-system/chartPalette';
import { Alert, Card, Spinner } from '@heroui/react'; import { Alert, Card, Spinner } from '@heroui/react';
...@@ -111,6 +112,7 @@ function DonutChart({ groups }) { ...@@ -111,6 +112,7 @@ function DonutChart({ groups }) {
const data = normalizeChartData(groups); const data = normalizeChartData(groups);
const total = data.reduce((sum, item) => sum + item.value, 0); const total = data.reduce((sum, item) => sum + item.value, 0);
const colorMap = createXinyuanChartColorMap(data.map((item) => item.name)); const colorMap = createXinyuanChartColorMap(data.map((item) => item.name));
const geometry = getXinyuanChartGeometry();
return ( return (
<PieChart className="rc-chart-interaction" height={320}> <PieChart className="rc-chart-interaction" height={320}>
...@@ -121,6 +123,9 @@ function DonutChart({ groups }) { ...@@ -121,6 +123,9 @@ function DonutChart({ groups }) {
nameKey="name" nameKey="name"
outerRadius="72%" outerRadius="72%"
paddingAngle={2} paddingAngle={2}
cornerRadius={geometry.sectorRadius}
stroke={geometry.sectorGap}
strokeWidth={3}
> >
{data.map((item) => ( {data.map((item) => (
<PieChart.Cell <PieChart.Cell
...@@ -163,6 +168,7 @@ function DonutChart({ groups }) { ...@@ -163,6 +168,7 @@ function DonutChart({ groups }) {
function HorizontalBarChart({ groups }) { function HorizontalBarChart({ groups }) {
const data = normalizeChartData(groups); const data = normalizeChartData(groups);
const geometry = getXinyuanChartGeometry();
return ( return (
<BarChart <BarChart
...@@ -187,7 +193,7 @@ function HorizontalBarChart({ groups }) { ...@@ -187,7 +193,7 @@ function HorizontalBarChart({ groups }) {
fill={XINYUAN_CHART_SEMANTIC.primary} fill={XINYUAN_CHART_SEMANTIC.primary}
maxBarSize={28} maxBarSize={28}
name="数量" name="数量"
radius={[0, 4, 4, 0]} radius={[0, geometry.barRadius, geometry.barRadius, 0]}
/> />
</BarChart> </BarChart>
); );
...@@ -196,6 +202,7 @@ function HorizontalBarChart({ groups }) { ...@@ -196,6 +202,7 @@ function HorizontalBarChart({ groups }) {
function SourceComposedChart({ groups }) { function SourceComposedChart({ groups }) {
const data = normalizeChartData(groups); const data = normalizeChartData(groups);
const colorMap = createXinyuanChartColorMap(data.map((item) => item.name)); const colorMap = createXinyuanChartColorMap(data.map((item) => item.name));
const geometry = getXinyuanChartGeometry();
return ( return (
<ComposedChart <ComposedChart
...@@ -219,6 +226,7 @@ function SourceComposedChart({ groups }) { ...@@ -219,6 +226,7 @@ function SourceComposedChart({ groups }) {
fill={XINYUAN_CHART_SEMANTIC.comparison} fill={XINYUAN_CHART_SEMANTIC.comparison}
maxBarSize={28} maxBarSize={28}
name="数量" name="数量"
radius={[geometry.barRadius, geometry.barRadius, 0, 0]}
> >
{data.map((item) => ( {data.map((item) => (
<Cell key={item.key || item.name} fill={colorMap.get(item.name)} /> <Cell key={item.key || item.name} fill={colorMap.get(item.name)} />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment