Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
196 lines
8.4 KiB
TypeScript
196 lines
8.4 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { Download, Plus, RefreshCcw, Trash2, Upload } from 'lucide-react';
|
||
import { RouteGuard } from '@/components/auth/Guard';
|
||
import { api } from '@/lib/api';
|
||
import { useAuthStore } from '@/stores/useAuthStore';
|
||
import { useMemberStore } from '@/stores/useMemberStore';
|
||
|
||
type GovernanceKind = 'task_category' | 'requirement_type' | 'requirement_platform' | 'requirement_source';
|
||
|
||
interface GovernanceItem {
|
||
id: string;
|
||
kind?: string;
|
||
name: string;
|
||
code?: string | null;
|
||
group?: string | null;
|
||
isSystem?: boolean;
|
||
}
|
||
|
||
const KIND_LABEL: Record<GovernanceKind, string> = {
|
||
task_category: '任务类型',
|
||
requirement_type: '需求类型',
|
||
requirement_platform: '支持端',
|
||
requirement_source: '需求来源',
|
||
};
|
||
|
||
const TASK_CATEGORY_GROUPS = [
|
||
{ value: 'development', label: '开发' },
|
||
{ value: 'testing', label: '测试' },
|
||
{ value: 'implementation', label: '实施' },
|
||
{ value: 'other', label: '其他' },
|
||
];
|
||
|
||
const SOURCE_TYPE_GROUPS = [
|
||
{ value: 'customer', label: '客户' },
|
||
{ value: 'internal', label: '内部' },
|
||
{ value: 'operation', label: '运营' },
|
||
{ value: 'aftersale', label: '售后' },
|
||
{ value: 'market', label: '市场' },
|
||
{ value: 'competitor', label: '竞品' },
|
||
{ value: 'management', label: '管理层' },
|
||
];
|
||
|
||
function GovernancePageInner() {
|
||
const user = useAuthStore((s) => s.user);
|
||
const role = useMemberStore((s) => s.roles.find((item) => item.id === user?.roleId));
|
||
const [kind, setKind] = useState<GovernanceKind>('task_category');
|
||
const [items, setItems] = useState<GovernanceItem[]>([]);
|
||
const [name, setName] = useState('');
|
||
const [group, setGroup] = useState(defaultGroupForKind('task_category'));
|
||
const [exportText, setExportText] = useState('');
|
||
const actorId = user?.id ?? '';
|
||
const permissions = role?.permissions ?? [];
|
||
|
||
const reload = async () => {
|
||
const rows = await api.get<GovernanceItem[]>(`/governance/dictionaries?kind=${kind}`);
|
||
setItems(rows);
|
||
};
|
||
|
||
useEffect(() => {
|
||
void reload().catch(() => setItems([]));
|
||
}, [kind]);
|
||
|
||
useEffect(() => {
|
||
setGroup(defaultGroupForKind(kind));
|
||
}, [kind]);
|
||
|
||
const create = async () => {
|
||
if (!actorId || !name.trim()) return;
|
||
await api.post('/governance/dictionaries', { actorId, permissions, kind, name: name.trim(), group: groupForPayload(kind, group) });
|
||
setName('');
|
||
await reload();
|
||
};
|
||
|
||
const remove = async (item: GovernanceItem) => {
|
||
if (!actorId) return;
|
||
await api.deleteWithBody(`/governance/dictionaries/${kind}/${item.id}`, { actorId, permissions });
|
||
await reload();
|
||
};
|
||
|
||
const exportAll = async () => {
|
||
const data = await api.get('/governance/export');
|
||
setExportText(JSON.stringify(data, null, 2));
|
||
};
|
||
|
||
const importAll = async () => {
|
||
if (!actorId || !exportText.trim()) return;
|
||
const parsed = JSON.parse(exportText);
|
||
const sourceItems = Array.isArray(parsed.items)
|
||
? parsed.items
|
||
: [...(parsed.dictionaries ?? []), ...(parsed.taskCategories ?? []).map((item: GovernanceItem) => ({ ...item, kind: 'task_category' }))];
|
||
await api.post('/governance/import', { actorId, permissions, items: sourceItems });
|
||
await reload();
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-full bg-[var(--bg)] p-6">
|
||
<div className="mx-auto max-w-5xl space-y-5">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-[18px] font-semibold text-[var(--ink)]">治理设置</h1>
|
||
<p className="mt-1 text-[12px] text-[var(--ink-muted)]">统一维护任务类型、需求类型、支持端与来源字典。</p>
|
||
</div>
|
||
<button onClick={reload} className="inline-flex h-8 items-center gap-1 rounded-md border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[12px] text-[var(--ink-soft)]">
|
||
<RefreshCcw className="h-3.5 w-3.5" /> 刷新
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
{Object.entries(KIND_LABEL).map(([key, label]) => (
|
||
<button
|
||
key={key}
|
||
onClick={() => setKind(key as GovernanceKind)}
|
||
className={`h-8 rounded-md px-3 text-[12px] font-medium ${kind === key ? 'bg-[var(--accent)] text-white' : 'border border-[var(--line)] bg-[var(--bg-card)] text-[var(--ink-soft)]'}`}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<section className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)]">
|
||
<div className={`grid gap-2 border-b border-[var(--line)] p-3 ${groupOptionsForKind(kind) ? 'grid-cols-[1fr_140px_auto]' : 'grid-cols-[1fr_auto]'}`}>
|
||
<input value={name} onChange={(event) => setName(event.target.value)} placeholder={`新增${KIND_LABEL[kind]}`} className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
|
||
{groupOptionsForKind(kind) && (
|
||
<select value={group} onChange={(event) => setGroup(event.target.value)} className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none">
|
||
{groupOptionsForKind(kind)?.map((option) => (
|
||
<option key={option.value} value={option.value}>{option.label}</option>
|
||
))}
|
||
</select>
|
||
)}
|
||
<button onClick={create} disabled={!name.trim()} className="inline-flex h-8 items-center gap-1 rounded-md bg-[var(--accent)] px-3 text-[12px] font-medium text-white disabled:opacity-50">
|
||
<Plus className="h-3.5 w-3.5" /> 添加
|
||
</button>
|
||
</div>
|
||
<div className="divide-y divide-[var(--line)]">
|
||
{items.map((item) => (
|
||
<div key={item.id} className="grid grid-cols-[1fr_160px_80px_32px] items-center gap-3 px-4 py-2.5">
|
||
<span className="truncate text-[13px] text-[var(--ink)]">{item.name}</span>
|
||
<span className="truncate text-[11px] text-[var(--ink-muted)]">{item.code ?? '-'}</span>
|
||
<span className="truncate text-[11px] text-[var(--ink-muted)]">{item.group ?? '-'}</span>
|
||
<button onClick={() => remove(item).catch(() => {})} disabled={item.isSystem} className="rounded p-1.5 text-[var(--ink-muted)] hover:bg-red-50 hover:text-red-600 disabled:opacity-30" title="删除">
|
||
<Trash2 className="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
{items.length === 0 && <div className="px-4 py-8 text-center text-[12px] text-[var(--ink-muted)]">暂无字典项</div>}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||
<div className="mb-3 flex items-center gap-2">
|
||
<button onClick={exportAll} className="inline-flex h-8 items-center gap-1 rounded-md border border-[var(--line)] px-3 text-[12px] text-[var(--ink-soft)]">
|
||
<Download className="h-3.5 w-3.5" /> 导出
|
||
</button>
|
||
<button onClick={importAll} disabled={!exportText.trim()} className="inline-flex h-8 items-center gap-1 rounded-md border border-[var(--line)] px-3 text-[12px] text-[var(--ink-soft)] disabled:opacity-50">
|
||
<Upload className="h-3.5 w-3.5" /> 导入
|
||
</button>
|
||
</div>
|
||
<textarea
|
||
value={exportText}
|
||
onChange={(event) => setExportText(event.target.value)}
|
||
rows={10}
|
||
className="w-full rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 py-2 font-mono text-[11px] leading-5 focus:border-[var(--accent)] focus:outline-none"
|
||
placeholder="点击导出生成 JSON,也可以粘贴 JSON 后导入"
|
||
/>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function groupOptionsForKind(kind: GovernanceKind) {
|
||
if (kind === 'task_category') return TASK_CATEGORY_GROUPS;
|
||
if (kind === 'requirement_source') return SOURCE_TYPE_GROUPS;
|
||
return null;
|
||
}
|
||
|
||
function defaultGroupForKind(kind: GovernanceKind) {
|
||
if (kind === 'task_category') return 'development';
|
||
if (kind === 'requirement_source') return 'customer';
|
||
return '';
|
||
}
|
||
|
||
function groupForPayload(kind: GovernanceKind, group: string) {
|
||
return groupOptionsForKind(kind) ? group : undefined;
|
||
}
|
||
|
||
export default function GovernancePage() {
|
||
return (
|
||
<RouteGuard permission="governance:manage">
|
||
<GovernancePageInner />
|
||
</RouteGuard>
|
||
);
|
||
}
|