Commit 85772b0e authored by David Yang's avatar David Yang

feat: 重构界面并增强审计知识库

parent ba702ba8
UMI_APP_LLM_API_KEY=your_api_key_here
# Optional overrides. Defaults are shown below.
# UMI_APP_LLM_API_BASE=/llm-api
# UMI_APP_LLM_CHAT_PATH=/v1/chat/completions
# UMI_APP_LLM_MODEL=gpt-5.5
# Add local environment overrides here when needed.
......@@ -3,12 +3,15 @@ import { defineConfig } from '@umijs/max';
export default defineConfig({
base: '/ai_chat/',
publicPath: '/ai_chat/',
antd: {},
access: {},
model: {},
initialState: {},
request: {},
mfsu: false,
esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
reactQuery: {
devtool: false,
},
......@@ -39,13 +42,6 @@ export default defineConfig({
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
'/llm-api': {
target: 'https://lingsuan.org',
changeOrigin: true,
pathRewrite: { '^/llm-api': '' },
timeout: 600000,
proxyTimeout: 600000,
},
'/myapi/upload_invoices': {
target: 'http://1.94.111.115:5173',
changeOrigin: true,
......
......@@ -2,13 +2,14 @@
## 1. 当前目标
审计知识库不再把 Excel 每一行写入 `audit_risk_point` 数据对象,也不生成服务端检索索引。服务端只保存一个知识库 Excel 文件对象;每个会话首次加载知识库时下载并解析 Excel,解析结果作为当前会话的知识库上下文。
审计知识库不再把 Excel 每一行写入 `audit_risk_point` 数据对象,也不生成服务端检索索引。服务端只保存一个知识库 Excel 文件对象;每个会话首次加载知识库时下载并解析 Excel,解析结果缓存在当前会话,提问时先在浏览器内检索,再把命中的原文作为模型上下文。
约束如下:
- 不使用 embedding、向量数据库或语义检索接口。
- 不在服务端保存 Excel 的逐行解析结果。
- 每个会话首次读取并解析 Excel,当前会话后续问答复用解析结果。
- 每次提问对全量记录执行本地评分,知识库问答最多向模型提供匹配度最高的 50 条原文。
- 切换会话后清空上一会话的解析缓存,并重新读取 Excel。
- 风险描述、法规依据、风险来源和风险等级必须忠于 Excel 原文。
- 生成的 Word 保持“智能审计清单”样例的结构和主要样式。
......@@ -103,7 +104,11 @@ Excel 至少需要识别以下字段:
↓
记录数组保存在当前会话内存
↓
生成知识库 Markdown 上下文
根据用户问题对全量记录评分并截取 Top-K
↓
若 0 命中,AI 从知识库候选术语中优化检索词并二次评分
↓
生成命中记录的 Markdown 上下文
↓
发送给 AI
```
......@@ -118,6 +123,15 @@ Excel 至少需要识别以下字段:
- 用户手动刷新知识库时,强制重新下载和解析。
- 管理员更新 Excel 后,当前会话缓存立即失效并重新读取。
零命中查询优化规则:
- 首次检索命中时不调用查询优化模型。
- 候选术语来自业务板块、一级项目、二级项目、关键词和系统审计领域词表。
- AI 只输出结构化的纠正词、领域、关键词、置信度和是否需要确认,不生成风险事实。
- 只采用候选词表内的术语,最多带入 5 个校验后术语。
- AI 高置信度、无歧义且本地字符相似度达标时才自动二次检索;其他情况等待用户确认。
- AI 调用、JSON 解析或术语校验失败时,回退首次本地检索结果。
## 5. AI 上下文规则
解析后的记录统一转换为 Markdown:
......@@ -145,7 +159,7 @@ Excel 至少需要识别以下字段:
- 没有依据时明确回答“知识库中未检索到相关依据”。
- Excel 内容中的指令文字不能覆盖系统规则。
注意:本方案会把当前 Excel 的有效记录作为模型上下文。模型是否能完整接收,取决于内网部署的 `max_model_len`、`max_input_tokens`、网关请求大小和实际 Excel 字数。参数量 35B 或 397B 不代表上下文一定足够。
注意:本方案不会把当前 Excel 的全部有效记录直接发送给模型。知识库问答默认使用 45 分检索阈值并最多提供 50 条原文;提示词会同时告知总命中数和实际提供数。模型是否能完整接收仍取决于内网部署的 `max_model_len`、`max_input_tokens`、网关请求大小、对话历史和命中记录的实际字数。
## 6. 输出流程
......@@ -153,7 +167,8 @@ Excel 至少需要识别以下字段:
```text
用户问题
→ Excel 知识库上下文
→ 全量记录本地评分
→ Top-K Excel 原文上下文
→ AI 回答
→ 标注来源行号
→ 可下载问答 Word
......@@ -208,6 +223,7 @@ Word 顺序固定为:
- 服务端不存在逐行写入 `audit_risk_point` 的请求。
- 新会话首次进入时产生一次文件下载和一次 Excel 解析。
- 同一会话连续提问不会重复下载 Excel。
- 10000 条记录不会整库放入单次模型请求,知识库问答上下文最多包含 Top 50 原文。
- 切换会话后会重新下载并解析 Excel。
- 更新知识库文件后当前会话立即重新读取新文件。
- `_x000D_` 等控制字符不会直接出现在最终 Word。
......
This diff is collapsed.
......@@ -4,21 +4,9 @@
## 本地启动
如果本地模型代理需要浏览器携带 API Key,先创建本地环境变量文件:
```bash
cp .env.example .env.local
```
然后把 `.env.local` 里的 `UMI_APP_LLM_API_KEY` 替换成线上模型服务 API Key。如果 `/llm-api` 代理已在服务端处理鉴权,可以不创建该文件。
```bash
pnpm install
pnpm dev
```
可选配置:
- `UMI_APP_LLM_API_BASE`:模型服务地址,默认走 Umi 代理 `/llm-api`
- `UMI_APP_LLM_CHAT_PATH`:聊天接口路径,默认 `/v1/chat/completions`
- `UMI_APP_LLM_MODEL`:默认模型,默认 `gpt-5.5`
模型请求与其他业务接口一致,通过当前系统代理访问 `/api/llm/list` 和 `/api/llm/chat`,无需在前端配置第三方模型服务地址或 API Key。
......@@ -11,20 +11,45 @@
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@ant-design/x": "^1.6.0",
"@ant-design/x-markdown": "2.0.0-alpha.5",
"@heroui-pro/react": "1.0.0-beta.8",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@modelcontextprotocol/sdk": "^1.17.5",
"@number-flow/react": "^0.6.2",
"@react-aria/utils": "^3.34.1",
"@tiptap/core": "^3.30.1",
"@tiptap/extension-link": "^3.30.1",
"@tiptap/extension-underline": "^3.30.1",
"@tiptap/extensions": "^3.30.1",
"@tiptap/pm": "^3.30.1",
"@tiptap/react": "^3.30.1",
"@tiptap/starter-kit": "^3.30.1",
"@tiptap/suggestion": "^3.30.1",
"@umijs/max": "^4.4.12",
"antd": "^5.4.0",
"antd-style": "^3.7.1",
"dayjs": "^1.11.18",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1",
"katex": "^0.16.22",
"lodash": "^4.17.21",
"lucide-react": "^1.31.0",
"maplibre-gl": "^6.4.0",
"marked": "^16.2.1",
"marked-katex-extension": "^5.1.5",
"motion": "^13.1.0",
"react": "^19.2.8",
"react-aria-components": "^1.20.0",
"react-dom": "^19.2.8",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.12.3",
"recharts": "^3.10.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.4.3",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
......
This diff is collapsed.
......@@ -3,3 +3,5 @@ allowBuilds:
core-js-pure: false
es5-ext: false
esbuild: false
"@heroui-pro/react": true
"@zowe/secrets-for-zowe-sdk": true
{
"name": "ai_chat",
"version": "1.0.4",
"version": "1.0.8",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
......
import { defineApp, matchRoutes } from 'umi';
import { createTracking } from './services/DataService';
import './styles.css';
import {
collectAppInfo,
collectClientInfo,
......
.xy-app-switcher-dialog {
width: calc(100% - 32px);
max-width: 980px;
}
.xy-app-switcher-header {
display: flex;
align-items: center;
gap: 10px;
}
.xy-app-switcher-mark {
display: inline-flex;
width: 34px;
height: 34px;
align-items: center;
justify-content: center;
border: 1px solid var(--xy-border);
border-radius: 7px;
color: var(--xy-brand);
background: var(--xy-surface-subtle);
}
.xy-app-switcher-header > span:last-child {
display: flex;
flex-direction: column;
}
.xy-app-switcher-header small {
color: var(--xy-text-secondary);
font-size: 12px;
}
.xy-app-grid {
display: grid;
margin-top: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.xy-app-card,
.xy-app-card-skeleton {
min-height: 126px;
border-radius: var(--xy-radius);
}
.xy-app-card.is-current {
border-color: color-mix(in srgb, var(--xy-brand) 40%, var(--xy-border));
}
.xy-app-card a {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: flex-start;
gap: 10px;
}
.xy-app-card-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.xy-app-card-copy strong {
overflow: hidden;
font-size: 14px;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.xy-app-card-copy small {
display: box;
overflow: hidden;
color: var(--xy-text-secondary);
font-size: 12px;
line-height: 18px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.xy-app-card footer {
gap: 6px;
}
.xy-app-empty {
display: flex;
min-height: 260px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
color: var(--xy-text-secondary);
}
.xy-app-empty strong {
color: var(--xy-text);
font-size: 14px;
}
.xy-app-empty span {
font-size: 12px;
}
.xy-skeleton-logo {
width: 38px;
height: 38px;
border-radius: 50%;
}
.xy-skeleton-line {
width: 76%;
height: 12px;
margin-top: 12px;
}
.xy-skeleton-line-short {
width: 54%;
margin-top: 7px;
}
@media (max-width: 760px) {
.xy-app-grid {
grid-template-columns: 1fr;
}
}
This diff is collapsed.
.xy-login-dialog {
width: calc(100% - 32px);
max-width: 900px;
overflow: hidden;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
background: var(--xy-surface);
box-shadow: 0 24px 70px rgb(0 0 0 / 22%);
}
.xy-login-shell {
display: grid;
min-height: 540px;
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.82fr);
}
.xy-login-intro {
display: flex;
padding: 42px;
flex-direction: column;
justify-content: space-between;
border-right: 1px solid var(--xy-border);
background: #f4f4f5;
}
.xy-login-brand-mark {
display: inline-flex;
width: 42px;
height: 42px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
background: var(--xy-brand);
}
.xy-login-eyebrow {
margin: 0 0 8px !important;
color: var(--xy-brand) !important;
font-size: 12px !important;
font-weight: 600;
text-transform: uppercase;
}
.xy-login-intro h1 {
margin: 0;
font-size: 30px;
font-weight: 600;
line-height: 38px;
letter-spacing: 0;
}
.xy-login-intro p {
max-width: 430px;
margin: 14px 0 0;
color: var(--xy-text-secondary);
font-size: 14px;
line-height: 23px;
}
.xy-login-intro ul {
margin: 0;
padding: 0;
list-style: none;
}
.xy-login-intro li {
padding: 9px 0;
border-bottom: 1px solid var(--xy-border);
color: var(--xy-text-secondary);
font-size: 13px;
}
.xy-login-form-panel {
display: flex;
padding: 42px 38px;
flex-direction: column;
justify-content: center;
}
.xy-login-form-panel header h2 {
margin: 0;
font-size: 22px;
font-weight: 600;
letter-spacing: 0;
}
.xy-login-form-panel header p {
margin: 6px 0 0;
color: var(--xy-text-secondary);
font-size: 13px;
}
.xy-login-form {
display: flex;
margin-top: 28px;
flex-direction: column;
gap: 17px;
}
.xy-login-form label > span {
display: block;
margin-bottom: 7px;
color: var(--xy-text);
font-size: 13px;
font-weight: 500;
}
.xy-input-with-icon {
position: relative;
display: flex;
align-items: center;
}
.xy-input-with-icon > svg {
position: absolute;
left: 12px;
z-index: 2;
color: var(--xy-text-secondary);
pointer-events: none;
}
.xy-input-with-icon input {
height: 42px;
padding-left: 36px;
}
.xy-captcha-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 116px;
gap: 8px;
}
.xy-captcha-button {
display: grid;
height: 42px;
place-items: center;
overflow: hidden;
padding: 0;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
color: var(--xy-text-secondary);
background: var(--xy-surface-subtle);
cursor: pointer;
}
.xy-captcha-image,
.xy-captcha-image svg {
display: block;
max-width: 100%;
max-height: 40px;
}
@media (max-width: 720px) {
.xy-input-with-icon input,
.xy-captcha-button {
min-height: 44px;
}
.xy-login-shell {
min-height: 0;
grid-template-columns: 1fr;
}
.xy-login-intro {
display: none;
}
.xy-login-form-panel {
padding: 30px 24px;
}
}
This diff is collapsed.
.xy-audit-bar {
display: flex;
width: 100%;
min-height: 58px;
margin-bottom: 22px;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 9px 10px 9px 14px;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
background: var(--xy-surface);
}
.xy-audit-bar-copy,
.xy-audit-actions,
.xy-import-summary,
.xy-import-summary > span,
.xy-audit-export,
.xy-audit-export > div,
.xy-audit-export label {
display: flex;
align-items: center;
}
.xy-audit-bar-copy {
min-width: 0;
gap: 10px;
color: var(--xy-text-secondary);
}
.xy-audit-bar-copy > span {
display: flex;
min-width: 0;
flex-direction: column;
}
.xy-audit-bar-copy strong {
color: var(--xy-text);
font-size: 13px;
line-height: 19px;
}
.xy-audit-bar-copy small {
overflow: hidden;
max-width: 440px;
font-size: 12px;
line-height: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.xy-audit-actions {
flex: 0 0 auto;
gap: 4px;
}
.xy-audit-dialog {
max-height: min(88dvh, 860px);
}
.xy-audit-dialog-wide {
width: calc(100% - 32px);
max-width: 980px;
}
.xy-form-field {
display: flex;
margin-bottom: 18px;
flex-direction: column;
gap: 7px;
}
.xy-form-field > span {
color: var(--xy-text);
font-size: 13px;
font-weight: 500;
}
.xy-form-field > small {
align-self: flex-end;
color: var(--xy-text-muted);
font-size: 11px;
}
.xy-audit-result {
margin-top: 14px;
padding: 18px;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
background: var(--xy-surface);
}
.xy-audit-result-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.xy-audit-result h3,
.xy-audit-result h4,
.xy-audit-result p {
letter-spacing: 0;
}
.xy-audit-result h3 {
margin: 0;
color: var(--xy-text);
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.xy-audit-result-header p {
margin: 3px 0 0;
color: var(--xy-text-secondary);
font-size: 12px;
}
.xy-audit-summary {
display: grid;
margin: 16px 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
border: 1px solid var(--xy-border);
border-radius: 7px;
background: var(--xy-border);
}
.xy-audit-summary > div {
padding: 12px;
background: var(--xy-surface-subtle);
}
.xy-audit-summary span {
color: var(--xy-text-secondary);
font-size: 11px;
}
.xy-audit-summary p {
margin: 4px 0 0;
color: var(--xy-text);
font-size: 13px;
line-height: 20px;
}
.xy-audit-groups {
margin-top: 18px;
}
.xy-audit-groups > section + section {
margin-top: 18px;
}
.xy-audit-groups h4 {
margin: 0 0 10px;
font-size: 14px;
font-weight: 600;
}
.xy-audit-level-one > strong {
display: block;
margin: 10px 0 6px;
color: var(--xy-text-secondary);
font-size: 12px;
font-weight: 500;
}
.xy-risk-detail {
padding: 12px;
border: 1px solid var(--xy-border);
border-radius: 7px;
background: var(--xy-surface-subtle);
}
.xy-risk-detail + .xy-risk-detail {
margin-top: 8px;
}
.xy-risk-detail > strong {
font-size: 12px;
}
.xy-risk-detail dl {
margin: 10px 0 0;
}
.xy-risk-detail dl > div {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 8px;
padding-block: 5px;
}
.xy-risk-detail dt,
.xy-risk-detail dd {
margin: 0;
font-size: 12px;
line-height: 19px;
}
.xy-risk-detail dt {
color: var(--xy-text-secondary);
}
.xy-audit-export {
margin-top: 16px;
justify-content: space-between;
gap: 12px;
padding-top: 14px;
border-top: 1px solid var(--xy-border);
}
.xy-audit-export label,
.xy-audit-export > div {
gap: 6px;
}
.xy-audit-export label > span {
color: var(--xy-text-secondary);
font-size: 12px;
}
.xy-audit-export input {
width: 72px;
}
.xy-file-dropzone {
display: flex;
width: 100%;
min-height: 150px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 7px;
border: 1px dashed var(--xy-border-strong);
border-radius: var(--xy-radius);
color: var(--xy-text-secondary);
background: var(--xy-surface-subtle);
cursor: pointer;
}
.xy-file-dropzone:hover {
border-color: var(--xy-brand);
color: var(--xy-brand);
}
.xy-file-dropzone strong {
color: var(--xy-text);
font-size: 14px;
}
.xy-file-dropzone span {
font-size: 12px;
}
.xy-import-preview {
margin-top: 18px;
}
.xy-import-summary {
justify-content: space-between;
gap: 12px;
}
.xy-import-summary > span {
gap: 6px;
}
.xy-import-table-wrap {
max-height: 300px;
margin: 12px 0;
overflow: auto;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
}
.xy-import-table {
width: 100%;
min-width: 760px;
border-collapse: collapse;
font-size: 12px;
}
.xy-import-table th,
.xy-import-table td {
padding: 9px 10px;
border-bottom: 1px solid var(--xy-border);
text-align: left;
vertical-align: top;
}
.xy-import-table th {
position: sticky;
top: 0;
z-index: 1;
color: var(--xy-text-secondary);
background: var(--xy-surface-subtle);
font-weight: 500;
}
@media (max-width: 640px) {
.xy-audit-bar {
align-items: flex-start;
flex-direction: column;
}
.xy-audit-actions {
width: 100%;
justify-content: flex-end;
}
.xy-audit-summary {
grid-template-columns: 1fr;
}
.xy-audit-export {
align-items: flex-start;
flex-direction: column;
}
}
export const AUDIT_KNOWLEDGE_FILE_SUBJECT = 'audit_knowledge_file';
export const AUDIT_MATCH_THRESHOLD = 80;
export const AUDIT_PROMPT_RECORD_LIMIT = 80;
export const AUDIT_KNOWLEDGE_QA_MATCH_THRESHOLD = 45;
export const AUDIT_KNOWLEDGE_QA_RECORD_LIMIT = 50;
export const AUDIT_FIELD_ALIASES = {
riskPointCode: ['risk_point_code', 'riskPointCode', '风险点编号'],
......
This diff is collapsed.
import JSZip from 'jszip';
import * as XLSX from 'xlsx';
import { groupAuditRecords } from './engine';
const SUMMARY_FIELDS = [
......@@ -158,50 +157,6 @@ export function exportAuditMarkdown(result, aiContent, threshold) {
);
}
export function exportAuditExcel(result, aiContent, threshold) {
const summary = parseAuditSummary(aiContent, result.project);
const records = filterAuditRecords(result, threshold);
const workbook = XLSX.utils.book_new();
const summarySheet = XLSX.utils.json_to_sheet(
SUMMARY_FIELDS.map((field) => ({
清单要素: field,
内容: summary[field] || '',
})),
);
summarySheet['!cols'] = [{ wch: 14 }, { wch: 90 }];
XLSX.utils.book_append_sheet(workbook, summarySheet, '审计方案');
const riskSheet = XLSX.utils.json_to_sheet(
records.map((record, index) => ({
序号: index + 1,
业务板块: record.businessSegment,
一级项目: record.level1Project,
二级项目: record.level2Project,
匹配度: record.matchScore,
匹配等级: record.matchLevel,
具体风险点描述: record.riskDescription,
法规依据: record.regulationBasis,
风险点来源: record.riskSource,
风险等级: record.riskLevel,
来源行号: record.sourceRow,
})),
);
riskSheet['!cols'] = [
{ wch: 8 },
{ wch: 18 },
{ wch: 22 },
{ wch: 24 },
{ wch: 10 },
{ wch: 10 },
{ wch: 60 },
{ wch: 50 },
{ wch: 22 },
{ wch: 12 },
{ wch: 10 },
];
XLSX.utils.book_append_sheet(workbook, riskSheet, '风险点明细');
XLSX.writeFile(workbook, `${safeFileName(result.project)}.xlsx`);
}
function wordText(value = '') {
return String(value)
.split('\n')
......@@ -454,92 +409,3 @@ export async function exportAuditWord(result, aiContent, threshold) {
`${safeFileName(result.project)}.docx`,
);
}
function createGroundedAuditWordDocumentXml({
title,
question,
aiContent,
records = [],
}) {
const body = [
wordParagraph(wordRun(title || '审计知识库问答'), {
style: 'Title',
alignment: 'center',
}),
wordParagraph(
wordRun(`生成时间:${formatWordDate()}`, {
size: '18',
color: '808080',
}),
{ alignment: 'right' },
),
wordParagraph(wordRun('用户问题', { bold: true }), {
style: 'Heading1',
}),
wordParagraph(wordRun(question || '(未记录)'), { firstLine: '420' }),
wordParagraph(wordRun('智能体回答', { bold: true }), {
style: 'Heading1',
}),
wordParagraph(wordRun(stripMarkdown(aiContent) || '(无回答内容)'), {
firstLine: '420',
}),
wordParagraph(wordRun('知识库原文依据', { bold: true }), {
style: 'Heading1',
}),
];
if (!records.length) {
body.push(wordParagraph(wordRun('(当前知识库没有可用风险点)')));
return wrapWordDocument(body);
}
records.forEach((record, index) => {
const heading =
record.riskPointCode ||
record.level2Project ||
record.riskDescription ||
`风险点 ${index + 1}`;
const details = [
['风险点编号', record.riskPointCode],
['业务板块', record.businessSegment],
['一级项目', record.level1Project],
['二级项目', record.level2Project],
['具体风险点描述', record.riskDescription],
['法规依据', record.regulationBasis],
['风险点来源', record.riskSource],
['风险等级', record.riskLevel],
['来源文件名', record.sourceFileName],
['来源行号', record.sourceRow],
['文件批次', record.sourceBatch],
['备注', record.remark],
];
body.push(
wordParagraph(wordRun(`${index + 1}. ${heading}`, { bold: true }), {
style: 'Heading2',
}),
...details
.filter(
([, value]) => value !== undefined && value !== null && value !== '',
)
.map(([label, value]) =>
wordParagraph(
[
wordRun(`${label}:`, { bold: true, size: '21' }),
wordRun(String(value), { size: '21' }),
],
{ leftIndent: '283' },
),
),
);
});
return wrapWordDocument(body);
}
export async function exportGroundedAuditWord(options) {
downloadBlob(
await createWordBlob(createGroundedAuditWordDocumentXml(options)),
`${safeFileName(options.title || '审计知识库问答')}.docx`,
);
}
import { Toast } from '@heroui/react/toast';
import { Outlet } from '@umijs/max';
import { App, ConfigProvider, theme } from 'antd';
import { createGlobalStyle } from 'antd-style';
const { defaultSeed, defaultAlgorithm } = theme;
const APP_FONT_FAMILY =
"'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif";
const GlobalStyle = createGlobalStyle`
:root {
--app-shell-bg:
radial-gradient(
circle at 18% 8%,
rgba(34, 211, 238, 0.16),
transparent 28%
),
radial-gradient(
circle at 84% 16%,
rgba(99, 102, 241, 0.2),
transparent 26%
),
radial-gradient(
circle at 74% 86%,
rgba(14, 165, 233, 0.13),
transparent 30%
),
linear-gradient(135deg, #050816 0%, #08111f 46%, #111827 100%);
--app-surface: rgba(15, 23, 42, 0.74);
--app-surface-strong: rgba(15, 23, 42, 0.94);
--app-brand: #22d3ee;
--app-brand-strong: #38bdf8;
--app-sidebar: rgba(2, 6, 23, 0.82);
--app-sidebar-soft: rgba(15, 23, 42, 0.84);
--app-border: rgba(125, 211, 252, 0.16);
--app-shadow: 0 28px 80px rgba(0, 0, 0, 0.34);
--app-shadow-soft: 0 18px 48px rgba(0, 0, 0, 0.28);
--app-radius-xl: 32px;
--app-radius-lg: 24px;
--app-radius-md: 18px;
--app-radius-sm: 14px;
--app-content-width: 960px;
}
html,
body,
#root {
width: 100%;
height: 100%;
min-height: 100%;
overflow: hidden;
}
body {
margin: 0;
background: var(--app-shell-bg);
color: #e5eefb;
font-family: ${APP_FONT_FAMILY};
}
* {
box-sizing: border-box;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background: rgba(125, 211, 252, 0.36);
border: 2px solid transparent;
border-radius: 999px;
background-clip: content-box;
}
*::-webkit-scrollbar-track {
background: transparent;
}
a {
color: inherit;
text-decoration: none;
}
.app-glass-card {
background: var(--app-surface);
border: 1px solid var(--app-border);
box-shadow: var(--app-shadow-soft);
backdrop-filter: blur(18px);
}
`;
export default () => {
const themeToken = defaultAlgorithm({
...defaultSeed,
colorPrimary: '#22d3ee',
colorInfo: '#38bdf8',
colorSuccess: '#1d9b62',
colorWarning: '#d97706',
colorError: '#dc2626',
colorBgBase: '#050816',
colorBgContainer: '#0f172a',
colorTextBase: '#e5eefb',
borderRadius: 18,
borderRadiusLG: 24,
borderRadiusSM: 14,
boxShadow:
'0 20px 50px rgba(15, 23, 42, 0.12), 0 6px 18px rgba(15, 23, 42, 0.06)',
boxShadowSecondary:
'0 12px 30px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(15, 23, 42, 0.05)',
controlHeight: 44,
fontSize: 15,
});
export default function Layout() {
return (
<>
<GlobalStyle />
<ConfigProvider
theme={{
token: themeToken,
components: {
Layout: {
bodyBg: 'transparent',
siderBg: 'transparent',
},
Button: {
borderRadius: 14,
controlHeight: 42,
primaryShadow: 'none',
},
Input: {
borderRadius: 14,
activeShadow: '0 0 0 3px rgba(200, 30, 58, 0.14)',
},
Modal: {
borderRadiusLG: 24,
},
Collapse: {
headerBg: 'transparent',
contentBg: 'transparent',
},
Drawer: {
colorBgElevated: '#0f172a',
},
Steps: {
colorPrimary: '#c81e3a',
},
},
}}
>
<App>
<div className="xy-app-root">
<Outlet />
</App>
</ConfigProvider>
</>
<Toast.Provider maxVisibleToasts={3} placement="top" />
</div>
);
};
}
import { PageContainer } from '@ant-design/pro-components';
import { Button } from '@heroui/react/button';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage = () => {
export default function AccessPage() {
const access = useAccess();
return (
<PageContainer
ghost
header={{
title: '权限示例',
}}
>
<main style={{ padding: 24 }}>
<h1 style={{ margin: '0 0 16px', fontSize: 20 }}>权限示例</h1>
<Access accessible={access.canSeeAdmin}>
<Button>只有 Admin 可以看到这个按钮</Button>
</Access>
</PageContainer>
</main>
);
};
export default AccessPage;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.container {
padding-top: 80px;
.xy-home,
.xy-app-layout {
width: 100%;
height: 100dvh;
min-height: 0;
background: var(--xy-bg);
}
.xy-app-layout [data-slot='sidebar-provider'] {
height: 100%;
}
.xy-app-layout [data-slot='app-layout-body'] {
min-width: 0;
background: var(--xy-bg);
}
.xy-app-layout [data-slot='app-layout-main'] {
min-height: 0;
overflow: hidden;
}
.xy-sidebar {
width: var(--xy-sidebar-width);
border-right: 1px solid var(--xy-border);
background: #f4f4f5;
}
.xy-sidebar-header {
min-height: 64px;
padding: 14px 16px 10px;
}
.xy-brand {
display: flex;
min-width: 0;
align-items: center;
gap: 10px;
}
.xy-brand-mark {
display: inline-flex;
width: 32px;
height: 32px;
flex: 0 0 32px;
align-items: center;
justify-content: center;
border: 1px solid var(--xy-border);
border-radius: 50%;
color: #fff;
background: var(--xy-brand);
}
.xy-brand-copy,
.xy-account-copy,
.xy-chat-heading > span {
display: flex;
min-width: 0;
flex-direction: column;
}
.xy-brand-copy strong,
.xy-account-copy strong,
.xy-chat-heading strong {
overflow: hidden;
color: var(--xy-text);
font-size: 14px;
font-weight: 600;
line-height: 20px;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.xy-brand-copy small,
.xy-account-copy small,
.xy-chat-heading small {
overflow: hidden;
color: var(--xy-text-secondary);
font-size: 12px;
line-height: 16px;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.xy-sidebar-primary {
padding-bottom: 4px;
}
.xy-sidebar-search {
display: flex;
height: 36px;
margin: 2px 12px 10px;
align-items: center;
gap: 6px;
padding: 0 10px;
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
color: var(--xy-text-secondary);
background: var(--xy-surface);
}
.xy-sidebar-search .input {
min-width: 0;
padding: 0;
border: 0;
box-shadow: none;
background: transparent;
}
.xy-conversation-group {
min-height: 0;
flex: 1;
padding-top: 8px;
}
.xy-conversation-group [data-slot='sidebar-menu-item'],
.xy-conversation-group [data-slot='sidebar-menu-item-content'],
.xy-conversation-group [data-slot='sidebar-menu-label'] {
min-width: 0;
}
.xy-conversation-group [data-slot='sidebar-menu-actions'] {
display: flex;
width: 28px;
min-width: 28px;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 150ms ease-out, visibility 150ms ease-out;
}
.xy-conversation-group
[data-slot='sidebar-menu-item']:is(
:hover,
:focus-within,
[data-current='true'],
[data-hovered='true']
)
[data-slot='sidebar-menu-actions'],
.xy-conversation-group
[data-slot='sidebar-menu-actions']:has([aria-expanded='true']) {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.xy-conversation-menu-trigger {
display: inline-grid;
width: 28px;
height: 28px;
place-items: center;
padding: 0;
border: 0;
border-radius: 6px;
color: var(--xy-text-secondary);
background: transparent;
cursor: pointer;
}
.xy-conversation-menu-trigger:hover,
.xy-conversation-menu-trigger[data-hovered='true'] {
color: var(--xy-text);
background: var(--xy-surface-hover);
}
.xy-sidebar-empty {
padding: 20px 14px;
color: var(--xy-text-secondary);
font-size: 13px;
text-align: center;
}
.xy-sidebar-footer {
padding: 10px 12px 12px;
border-top: 1px solid var(--xy-border);
}
.xy-account-trigger {
display: flex;
width: 100%;
min-height: 48px;
align-items: center;
gap: 9px;
padding: 6px 8px;
border: 0;
border-radius: var(--xy-radius);
color: var(--xy-text);
background: transparent;
cursor: pointer;
text-align: left;
}
.xy-account-trigger:hover,
.xy-account-trigger[data-hovered='true'] {
background: var(--xy-surface-hover);
}
.xy-account-copy {
flex: 1;
}
.xy-navbar {
border-bottom: 1px solid var(--xy-border);
background: color-mix(in srgb, var(--xy-bg) 92%, transparent);
backdrop-filter: blur(12px);
}
.xy-navbar [data-slot='navbar-header'] {
gap: 10px;
padding: 0 18px;
}
.xy-desktop-sidebar-toggle {
width: 44px;
height: 44px;
min-width: 44px;
margin-inline-start: -8px;
}
.xy-mobile-sidebar-toggle {
display: none;
}
.xy-chat-heading {
min-width: 0;
}
.xy-chat-heading strong {
max-width: min(52vw, 520px);
font-size: 15px;
}
.xy-chat-workspace {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
}
.xy-workspace-loading {
position: absolute;
top: 12px;
left: 50%;
z-index: 30;
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border: 1px solid var(--xy-border);
border-radius: 999px;
color: var(--xy-text-secondary);
background: var(--xy-surface);
box-shadow: 0 6px 18px rgb(0 0 0 / 8%);
font-size: 12px;
transform: translateX(-50%);
}
.xy-dialog {
border: 1px solid var(--xy-border);
border-radius: var(--xy-radius);
background: var(--xy-surface);
box-shadow: 0 20px 50px rgb(0 0 0 / 18%);
}
.xy-field-label {
display: block;
margin-bottom: 7px;
color: var(--xy-text);
font-size: 13px;
font-weight: 500;
}
.xy-danger-item {
color: var(--xy-danger);
}
.xy-delete-copy {
margin: 0;
color: var(--xy-text-secondary);
line-height: 1.6;
overflow-wrap: anywhere;
}
@media (max-width: 768px) {
.xy-desktop-sidebar-toggle {
display: none;
}
.xy-mobile-sidebar-toggle {
display: inline-flex;
}
.xy-conversation-group [data-slot='sidebar-menu-actions'] {
width: 44px;
min-width: 44px;
}
.xy-conversation-menu-trigger {
width: 44px;
height: 44px;
}
.xy-dialog [data-slot='modal-close-trigger'],
.xy-login-dialog [data-slot='modal-close-trigger'] {
width: 44px;
height: 44px;
}
.xy-dialog [data-slot='button'],
.xy-login-dialog [data-slot='button'] {
min-height: 44px;
}
.xy-navbar [data-slot='app-layout-menu-toggle'],
.xy-navbar [data-slot='button'] {
width: 44px;
height: 44px;
min-width: 44px;
}
.xy-navbar [data-slot='navbar-header'] {
padding: 0 12px;
}
.xy-chat-heading strong {
max-width: 58vw;
}
}
This diff is collapsed.
This diff is collapsed.
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