背景:用户选定方案 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 无溢出。
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
/* 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>
|
||
);
|
||
}
|