外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
/**
* Dialog — 令牌驱动的模态对话框(task 18.8Req 19.1 / 19.5)。
*
* 受控显隐(`open` + `onClose`)。可访问性(便于 task 22.x):
* - 容器 `role="dialog"` 且 `aria-modal="true"`
* - `aria-labelledby` 关联标题;
* - 打开时自动聚焦关闭按钮;按 `Escape` 触发关闭;
* - 关闭按钮图标取自单一来源图标集(Req 19.5)。
*
* 完整的焦点陷阱(focus trap)留待 task 22.x;此处提供合理的基础行为。
*/
import { useEffect, useRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { colorVar, FONT_FAMILY, RADIUS, space, typographyStyle } from './styles.js';
import { Icon } from './Icon.js';
/** `Dialog` 组件属性。 */
export interface DialogProps {
/** 是否显示。 */
readonly open: boolean;
/** 请求关闭的回调(点击遮罩、关闭按钮或按 Escape 时触发)。 */
readonly onClose: () => void;
/** 对话框标题。 */
readonly title: string;
/** 对话框主体内容。 */
readonly children: ReactNode;
/** 可选底部操作区。 */
readonly footer?: ReactNode;
/** 关闭按钮的无障碍标签,默认「关闭」。 */
readonly closeLabel?: string;
}
/**
* 模态对话框。显隐受控,具备基础键盘与焦点行为(Req 19.1)。
*/
export function Dialog({
open,
onClose,
title,
children,
footer,
closeLabel = '关闭',
}: DialogProps): JSX.Element | null {
const closeButtonRef = useRef<HTMLButtonElement>(null);
const titleId = useRef(`dialog-title-${Math.random().toString(36).slice(2)}`).current;
// 打开时聚焦关闭按钮,提供合理的初始焦点。
useEffect(() => {
if (open) {
closeButtonRef.current?.focus();
}
}, [open]);
// Escape 关闭。
useEffect(() => {
if (!open) {
return;
}
const handler = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [open, onClose]);
if (!open) {
return null;
}
const overlayStyle: CSSProperties = {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: `${space(4)}px`,
zIndex: 1000,
};
const dialogStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
width: '100%',
maxWidth: '480px',
maxHeight: '100%',
backgroundColor: colorVar('color.bg.elevated'),
border: `1px solid ${colorVar('color.border.default')}`,
borderRadius: `${RADIUS.md}px`,
fontFamily: FONT_FAMILY,
color: colorVar('color.text.primary'),
overflow: 'hidden',
};
const headerStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: `${space(3)}px`,
padding: `${space(3)}px ${space(4)}px`,
borderBottom: `1px solid ${colorVar('color.border.default')}`,
...typographyStyle('title'),
fontWeight: 700,
};
const closeButtonStyle: CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: `${space(1)}px`,
background: 'transparent',
border: 'none',
borderRadius: `${RADIUS.sm}px`,
cursor: 'pointer',
color: colorVar('color.text.secondary'),
};
return (
<div
style={overlayStyle}
onClick={(event) => {
if (event.target === event.currentTarget) {
onClose();
}
}}
>
<div role="dialog" aria-modal="true" aria-labelledby={titleId} style={dialogStyle}>
<header style={headerStyle}>
<h2 id={titleId} style={{ margin: 0, ...typographyStyle('title') }}>
{title}
</h2>
<button
ref={closeButtonRef}
type="button"
aria-label={closeLabel}
onClick={onClose}
style={closeButtonStyle}
>
<Icon name="close" size={20} />
</button>
</header>
<div style={{ padding: `${space(4)}px`, overflowY: 'auto', ...typographyStyle('body') }}>
{children}
</div>
{footer !== undefined ? (
<footer
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: `${space(2)}px`,
padding: `${space(3)}px ${space(4)}px`,
borderTop: `1px solid ${colorVar('color.border.default')}`,
}}
>
{footer}
</footer>
) : null}
</div>
</div>
);
}