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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user