feat(web): A4 数据字典与系统配置页

背景:用户选定方案 A 主从分栏,A4 需要落地字典与配置页。\n改动:新增 /system/dicts、/system/dicts/:type、/system/settings;主从类型列表 + 三层合并 + 树/词表切换 + 条目/类型弹层;6 组配置 + 命名预览 + 阈值二次确认 + 变更记录;客户字段上限改读 effective config;路由/样式/OpenAPI 类型同步。\n影响面:管理中心新增两个菜单页;客户页上限显示改为配置读取(fallback 共享常量)。\n回滚:回滚本提交并恢复 AppLayout / routes / main 样式 import。\n验证:web typecheck/lint/build;单测 154;浏览器自检 1440/1024 无溢出。
This commit is contained in:
Stev_Wang committed 2026-11-04 11:49:32 +08:00
1 parent e4330a43e4
commit 3b0a99d91f
36 files changed
+7691 -15

No files matched your search

@@ -0,0 +1,154 @@
/* eslint-disable max-lines-per-function, complexity -- 页面组件集中编排筛选、表单与弹层,拆散会割裂数据流;纯逻辑已抽到 *-utils.ts */
import { Button, Input, Switch } from '@douyinfe/semi-ui-19';
import type { SystemConfig } from '@lingxi/shared';
import { IconAlert, IconShare } from '@lingxi/ui/icons';
import type { ReactNode } from 'react';
import { configValueText, formatConfigTime, namingPreview, rangeTextOf } from './config-utils';
const SHARED_FIELD_KEYS: ReadonlySet<string> = new Set([
'limit.customer.base_field',
'limit.customer.gen_field',
]);
function LabelBlock({ config, dirty }: { config: SystemConfig; dirty: boolean }): ReactNode {
return (
<div className="lx-settings-config__label">
<div className="lx-settings-config__title">
<b>{config.label}</b>
{dirty ? <span className="lx-settings-dirty-tag">未保存</span> : null}
<code>{config.key}</code>
</div>
<p className="lx-settings-config__desc">{config.description}</p>
<p className="lx-settings-config__impact">
<IconAlert size={13} />
<span>{config.impact}</span>
</p>
{SHARED_FIELD_KEYS.has(config.key) ? (
<p className="lx-settings-config__shared">
<IconShare size={12} />
<span>A3 客户字段、表单校验与任务提交共用(R-26)</span>
</p>
) : null}
</div>
);
}
function ValueMeta({ config, error }: { config: SystemConfig; error: string | null }): ReactNode {
if (error !== null) return <span className="lx-settings-config__err">{error}</span>;
const range = rangeTextOf(config);
return (
<span className="lx-settings-config__meta">
{range === '' ? null : `${range} · `}v{config.version} · {config.updatedBy ?? '—'}{' '}
{formatConfigTime(config.updatedAt)}
</span>
);
}
export interface ConfigRowProps {
config: SystemConfig;
raw: string;
dirty: boolean;
readOnly: boolean;
error: string | null;
editingPattern: boolean;
namingMaxLength: number;
onRawChange: (raw: string) => void;
onEditPattern: () => void;
onCollapsePattern: () => void;
}
/** 单个配置项:文本 / 数字 / 开关;命名模板(pattern)展开行内编辑 + 校验提示 */
export function ConfigRow({
config,
raw,
dirty,
readOnly,
error,
editingPattern,
namingMaxLength,
onRawChange,
onEditPattern,
onCollapsePattern,
}: ConfigRowProps): ReactNode {
if (config.type === 'pattern') {
const preview = namingPreview(raw, namingMaxLength);
const patternError = error ?? preview.error;
return (
<div className={`lx-settings-config is-pattern${dirty ? ' is-dirty' : ''}`}>
<LabelBlock config={config} dirty={dirty} />
{editingPattern && !readOnly ? (
<div className="lx-settings-pattern">
<Input value={raw} maxLength={200} aria-label="命名规则" onChange={onRawChange} />
<div className="lx-settings-pattern__hint">
<span className={patternError === null ? 'is-ok' : 'is-bad'}>
{patternError === null ? '校验通过' : `校验未通过:${patternError}`}
</span>
<span>
{' '}
· {raw.length} / {namingMaxLength} 字符 · 点击右侧占位符插入
</span>
</div>
<Button size="small" theme="borderless" onClick={onCollapsePattern}>
收起编辑
</Button>
</div>
) : (
<div className="lx-settings-pattern__value">
<code>{configValueText(config, raw)}</code>
{readOnly ? null : (
<Button
size="small"
theme="borderless"
onClick={() => {
onEditPattern();
}}
>
编辑
</Button>
)}
</div>
)}
</div>
);
}
const control =
config.type === 'boolean' ? (
<span className="lx-settings-config__switch">
<span>{raw === 'true' ? '开启' : '关闭'}</span>
<Switch
checked={raw === 'true'}
disabled={readOnly || !config.editable}
aria-label={config.label}
onChange={(checked) => {
onRawChange(checked ? 'true' : 'false');
}}
/>
</span>
) : (
<span className={`lx-settings-config__control${error === null ? '' : ' is-error'}`}>
<Input
type={config.type === 'number' ? 'number' : 'text'}
value={raw}
maxLength={config.type === 'text' ? (config.maxLength ?? 120) : undefined}
disabled={readOnly || !config.editable}
aria-label={config.label}
onChange={onRawChange}
/>
{config.unit === null || config.unit === '' ? null : (
<span className="lx-settings-config__unit">{config.unit}</span>
)}
</span>
);
return (
<div className={`lx-settings-config${dirty ? ' is-dirty' : ''}`}>
<LabelBlock config={config} dirty={dirty} />
<div className="lx-settings-config__value">
{control}
<ValueMeta config={config} error={error} />
</div>
</div>
);
}