Files
lingxi/apps/web/src/pages/system/settings/ConfigRow.tsx
T
Stev_Wang 3b0a99d91f 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 无溢出。
2026-11-04 11:49:32 +08:00

155 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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>
);
}