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,121 @@
|
||||
import type { SystemConfig, SystemConfigChange } from '@lingxi/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
|
||||
import { setUnauthorizedHandler } from './client';
|
||||
import {
|
||||
fetchEffectiveSystemConfigs,
|
||||
fetchSystemConfigHistory,
|
||||
fetchSystemConfigs,
|
||||
updateSystemConfigs,
|
||||
} from './configs';
|
||||
|
||||
const NOW = '2026-11-04T10:00:00.000Z';
|
||||
const ID = '01HZZZZZZZZZZZZZZZZZZZZZZ1';
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200): Response =>
|
||||
new Response(status === 204 ? null : JSON.stringify(body), {
|
||||
status,
|
||||
headers: status === 204 ? undefined : { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const config: SystemConfig = {
|
||||
key: 'limit.customer.base_field',
|
||||
group: 'limits',
|
||||
label: '客户基础字段上限',
|
||||
description: '每客户基础字段的最大数量',
|
||||
impact: '前后端共用;修改后新申请按新上限校验',
|
||||
type: 'number',
|
||||
value: 10,
|
||||
valueType: 'number',
|
||||
unit: '个',
|
||||
version: 3,
|
||||
updatedAt: NOW,
|
||||
updatedBy: '江岚',
|
||||
editable: true,
|
||||
isPublic: true,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: null,
|
||||
maxLength: null,
|
||||
};
|
||||
|
||||
const change: SystemConfigChange = {
|
||||
key: 'limit.customer.base_field',
|
||||
label: '客户基础字段上限',
|
||||
version: 3,
|
||||
oldValue: 8,
|
||||
newValue: 10,
|
||||
reason: '在系统配置页调整',
|
||||
operator: '江岚',
|
||||
changedAt: NOW,
|
||||
result: '已生效',
|
||||
};
|
||||
|
||||
function lastCall(): { url: string; init: RequestInit | undefined } {
|
||||
const fetchMock = vi.mocked(globalThis.fetch);
|
||||
const call = fetchMock.mock.calls.at(-1);
|
||||
const input = call?.[0];
|
||||
const url =
|
||||
typeof input === 'string' ? input : input instanceof URL ? input.href : (input?.url ?? '');
|
||||
return { url, init: call?.[1] };
|
||||
}
|
||||
|
||||
function lastBody(): unknown {
|
||||
const body = lastCall().init?.body;
|
||||
return typeof body === 'string' ? (JSON.parse(body) as unknown) : undefined;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
window.localStorage.clear();
|
||||
useAuthStore.getState().clearSession();
|
||||
setUnauthorizedHandler(null);
|
||||
useAuthStore.getState().setSession({
|
||||
accessToken: 'token',
|
||||
user: { id: ID, name: '江岚', account: 'jiang.lan' },
|
||||
permissions: ['system:configure'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('系统配置接口封装', () => {
|
||||
it('全量配置 / 生效配置', async () => {
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(jsonResponse({ items: [config] }))
|
||||
.mockResolvedValueOnce(jsonResponse({ items: [{ key: config.key, value: 10 }] }));
|
||||
|
||||
const list = await fetchSystemConfigs();
|
||||
expect(list.items).toHaveLength(1);
|
||||
expect(lastCall().url).toContain('/system/configs');
|
||||
|
||||
const effective = await fetchEffectiveSystemConfigs();
|
||||
expect(effective.items[0]?.value).toBe(10);
|
||||
expect(lastCall().url).toContain('/system/configs/effective');
|
||||
});
|
||||
|
||||
it('批量保存:PUT changes 带 expectedVersion(乐观锁)', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(jsonResponse({ items: [config] }));
|
||||
await updateSystemConfigs({
|
||||
changes: [{ key: 'limit.customer.base_field', value: 12, expectedVersion: 3 }],
|
||||
});
|
||||
expect(lastCall().init?.method).toBe('PUT');
|
||||
expect(lastBody()).toEqual({
|
||||
changes: [{ key: 'limit.customer.base_field', value: 12, expectedVersion: 3 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('变更记录:query 用 snake_case(config_key / limit)', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
||||
jsonResponse({ items: [change], next_cursor: null }),
|
||||
);
|
||||
const history = await fetchSystemConfigHistory({
|
||||
config_key: 'limit.customer.base_field',
|
||||
limit: 50,
|
||||
});
|
||||
expect(history.items[0]?.version).toBe(3);
|
||||
expect(lastCall().url).toContain('/system/configs/history?');
|
||||
expect(lastCall().url).toContain('config_key=');
|
||||
expect(lastCall().url).toContain('limit=50');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* A4 系统配置接口(唯一出处)。
|
||||
* 依据:packages/shared/src/schemas/config.ts、PRD R-26、技术架构 §5.7。
|
||||
*
|
||||
* `system_configs` 是阈值 / 上限唯一来源:页面保存走 PUT changes + expectedVersion 乐观锁;
|
||||
* 前端上限读取走 /system/configs/effective(仅登录,只读)。响应 / 入参全部 shared Zod parse。
|
||||
*/
|
||||
import {
|
||||
type EffectiveSystemConfig,
|
||||
effectiveSystemConfigsResponseSchema,
|
||||
type SystemConfig,
|
||||
type SystemConfigChange,
|
||||
type SystemConfigHistoryQuery,
|
||||
systemConfigHistoryQuerySchema,
|
||||
systemConfigHistoryResponseSchema,
|
||||
systemConfigListResponseSchema,
|
||||
type UpdateSystemConfigsInput,
|
||||
updateSystemConfigsInputSchema,
|
||||
} from '@lingxi/shared';
|
||||
|
||||
import { request } from './client';
|
||||
import { ENDPOINTS } from './endpoints';
|
||||
|
||||
function searchParams(entries: Record<string, string | number | undefined>): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
if (value !== undefined && value !== '') params.set(key, String(value));
|
||||
}
|
||||
const search = params.toString();
|
||||
return search === '' ? '' : `?${search}`;
|
||||
}
|
||||
|
||||
/** 全量配置(含非公开项,需 system:configure) */
|
||||
export async function fetchSystemConfigs(): Promise<{ items: SystemConfig[] }> {
|
||||
const payload: unknown = await request(ENDPOINTS.systemConfigs);
|
||||
return systemConfigListResponseSchema.parse(payload);
|
||||
}
|
||||
|
||||
/** 生效配置(仅登录;前端上限读取,无需 system:configure) */
|
||||
export async function fetchEffectiveSystemConfigs(): Promise<{
|
||||
items: EffectiveSystemConfig[];
|
||||
}> {
|
||||
const payload: unknown = await request(ENDPOINTS.systemConfigsEffective);
|
||||
return effectiveSystemConfigsResponseSchema.parse(payload);
|
||||
}
|
||||
|
||||
/** 批量保存:changes 带 expectedVersion(不一致返回 40901) */
|
||||
export async function updateSystemConfigs(
|
||||
input: UpdateSystemConfigsInput,
|
||||
): Promise<{ items: SystemConfig[] }> {
|
||||
const body = updateSystemConfigsInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.systemConfigs, { method: 'PUT', body });
|
||||
return systemConfigListResponseSchema.parse(payload);
|
||||
}
|
||||
|
||||
/** 变更记录(含前后值、操作人、时间、原因与结果) */
|
||||
export async function fetchSystemConfigHistory(
|
||||
query: Partial<SystemConfigHistoryQuery> = {},
|
||||
): Promise<{ items: SystemConfigChange[]; next_cursor: string | null }> {
|
||||
const parsed = systemConfigHistoryQuerySchema.parse(query);
|
||||
const payload: unknown = await request(
|
||||
`${ENDPOINTS.systemConfigsHistory}${searchParams({
|
||||
config_key: parsed.config_key,
|
||||
limit: parsed.limit,
|
||||
})}`,
|
||||
);
|
||||
return systemConfigHistoryResponseSchema.parse(payload);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import type {
|
||||
DictItem,
|
||||
DictItemListResponse,
|
||||
DictTypeDetail,
|
||||
DictTypeSummary,
|
||||
} from '@lingxi/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
|
||||
import { setUnauthorizedHandler } from './client';
|
||||
import {
|
||||
createDictItem,
|
||||
createDictType,
|
||||
deleteDictType,
|
||||
fetchDictItemList,
|
||||
fetchDictTypeDetail,
|
||||
fetchDictTypeList,
|
||||
setDictTypeStatus,
|
||||
updateDictItem,
|
||||
updateDictType,
|
||||
} from './dicts';
|
||||
|
||||
const NOW = '2026-11-04T10:00:00.000Z';
|
||||
const ID = '01HZZZZZZZZZZZZZZZZZZZZZZ1';
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200): Response =>
|
||||
new Response(status === 204 ? null : JSON.stringify(body), {
|
||||
status,
|
||||
headers: status === 204 ? undefined : { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const typeSummary: DictTypeSummary = {
|
||||
code: 'script_direction',
|
||||
name: '脚本方向',
|
||||
group: 'content',
|
||||
kind: 'list',
|
||||
description: '脚本“说什么”的方向',
|
||||
industryScoped: true,
|
||||
builtin: true,
|
||||
status: 'active',
|
||||
sortOrder: 60,
|
||||
itemCount: 11,
|
||||
activeItemCount: 11,
|
||||
industryCount: 4,
|
||||
updatedAt: NOW,
|
||||
updatedBy: '江岚',
|
||||
};
|
||||
|
||||
const item: DictItem = {
|
||||
id: ID,
|
||||
typeCode: 'script_direction',
|
||||
code: 'oral',
|
||||
name: '口播',
|
||||
parentCode: null,
|
||||
parentName: null,
|
||||
industryId: null,
|
||||
industryPath: null,
|
||||
sortOrder: 10,
|
||||
status: 'active',
|
||||
remark: '',
|
||||
source: 'generic',
|
||||
overridden: false,
|
||||
refCount: 15,
|
||||
updatedAt: NOW,
|
||||
updatedBy: '江岚',
|
||||
};
|
||||
|
||||
const typeDetail: DictTypeDetail = { ...typeSummary, items: [item] };
|
||||
|
||||
const itemList: DictItemListResponse = {
|
||||
items: [item],
|
||||
next_cursor: null,
|
||||
layers: [
|
||||
{ source: 'generic', industryId: null, label: '通用', count: 5 },
|
||||
{ source: 'parent', industryId: ID, label: '父行业', count: 2 },
|
||||
{ source: 'current', industryId: ID, label: '本行业', count: 1 },
|
||||
],
|
||||
effectiveCount: 7,
|
||||
overriddenCount: 1,
|
||||
disabledCount: 0,
|
||||
};
|
||||
|
||||
function lastCall(): { url: string; init: RequestInit | undefined } {
|
||||
const fetchMock = vi.mocked(globalThis.fetch);
|
||||
const call = fetchMock.mock.calls.at(-1);
|
||||
const input = call?.[0];
|
||||
const url =
|
||||
typeof input === 'string' ? input : input instanceof URL ? input.href : (input?.url ?? '');
|
||||
return { url, init: call?.[1] };
|
||||
}
|
||||
|
||||
function lastBody(): unknown {
|
||||
const body = lastCall().init?.body;
|
||||
return typeof body === 'string' ? (JSON.parse(body) as unknown) : undefined;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
window.localStorage.clear();
|
||||
useAuthStore.getState().clearSession();
|
||||
setUnauthorizedHandler(null);
|
||||
useAuthStore.getState().setSession({
|
||||
accessToken: 'token',
|
||||
user: { id: ID, name: '江岚', account: 'jiang.lan' },
|
||||
permissions: ['dict:manage'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('数据字典接口封装', () => {
|
||||
it('类型列表:query 用 snake_case(keyword / group / status / limit / cursor)', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
||||
jsonResponse({ items: [typeSummary], next_cursor: 'cursor-2' }),
|
||||
);
|
||||
const result = await fetchDictTypeList({
|
||||
keyword: '脚本',
|
||||
group: 'content',
|
||||
status: 'active',
|
||||
limit: 100,
|
||||
cursor: 'cursor-1',
|
||||
});
|
||||
expect(result.next_cursor).toBe('cursor-2');
|
||||
expect(lastCall().url).toContain('/dicts?keyword=');
|
||||
expect(lastCall().url).toContain('group=content');
|
||||
expect(lastCall().url).toContain('status=active');
|
||||
expect(lastCall().url).toContain('limit=100');
|
||||
expect(lastCall().url).toContain('cursor=cursor-1');
|
||||
});
|
||||
|
||||
it('类型详情 / 新建 / 更新 / 状态 / 删除', async () => {
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(jsonResponse(typeDetail))
|
||||
.mockResolvedValueOnce(jsonResponse(typeDetail, 201))
|
||||
.mockResolvedValueOnce(jsonResponse(typeDetail))
|
||||
.mockResolvedValueOnce(jsonResponse(typeDetail))
|
||||
.mockResolvedValueOnce(jsonResponse(undefined, 204));
|
||||
|
||||
await fetchDictTypeDetail('script_direction');
|
||||
expect(lastCall().url).toContain('/dicts/script_direction');
|
||||
|
||||
await createDictType({
|
||||
code: 'delivery_slot',
|
||||
name: '投放位置',
|
||||
group: 'platform',
|
||||
kind: 'list',
|
||||
industryScoped: false,
|
||||
description: '',
|
||||
sortOrder: 10,
|
||||
});
|
||||
expect(lastCall().init?.method).toBe('POST');
|
||||
expect(lastBody()).toMatchObject({ code: 'delivery_slot', industryScoped: false });
|
||||
|
||||
await updateDictType('script_direction', { name: '脚本方向2' });
|
||||
expect(lastCall().init?.method).toBe('PATCH');
|
||||
expect(lastBody()).toEqual({ name: '脚本方向2' });
|
||||
|
||||
await setDictTypeStatus('script_direction', { status: 'disabled' });
|
||||
expect(lastCall().url).toContain('/dicts/script_direction/status');
|
||||
expect(lastBody()).toEqual({ status: 'disabled' });
|
||||
|
||||
await deleteDictType('script_direction');
|
||||
expect(lastCall().init?.method).toBe('DELETE');
|
||||
});
|
||||
|
||||
it('条目列表:industry_id 只在传入行业视角时出现', async () => {
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(jsonResponse(itemList))
|
||||
.mockResolvedValueOnce(jsonResponse(itemList));
|
||||
|
||||
await fetchDictItemList('script_direction', { industry_id: ID });
|
||||
expect(lastCall().url).toContain('/dicts/script_direction/items?industry_id=');
|
||||
|
||||
await fetchDictItemList('script_direction');
|
||||
expect(lastCall().url).not.toContain('industry_id');
|
||||
});
|
||||
|
||||
it('新建 / 编辑条目:body 用 camelCase,编辑不提交编码与行业', async () => {
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(jsonResponse(item, 201))
|
||||
.mockResolvedValueOnce(jsonResponse(item));
|
||||
|
||||
await createDictItem('script_direction', {
|
||||
code: 'oral',
|
||||
name: '口播',
|
||||
parentCode: null,
|
||||
industryId: null,
|
||||
sortOrder: 10,
|
||||
status: 'active',
|
||||
remark: '',
|
||||
});
|
||||
expect(lastCall().init?.method).toBe('POST');
|
||||
expect(lastBody()).toMatchObject({ code: 'oral', parentCode: null, industryId: null });
|
||||
|
||||
await updateDictItem('script_direction', ID, {
|
||||
name: '口播(新)',
|
||||
sortOrder: 20,
|
||||
status: 'disabled',
|
||||
remark: '说明',
|
||||
});
|
||||
expect(lastCall().url).toContain(`/dicts/script_direction/items/${ID}`);
|
||||
expect(lastCall().init?.method).toBe('PATCH');
|
||||
expect(lastBody()).toEqual({
|
||||
name: '口播(新)',
|
||||
sortOrder: 20,
|
||||
status: 'disabled',
|
||||
remark: '说明',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* A4 数据字典接口(唯一出处)。
|
||||
* 依据:packages/shared/src/schemas/dict.ts、PRD R-10、技术架构 §6.3.7。
|
||||
*
|
||||
* 约定:query 用 snake_case(行业视角 `industry_id`);写接口 body 用 camelCase(C-01)。
|
||||
* 所有响应 / 入参都用 shared Zod schema parse,契约漂移在请求层直接暴露。
|
||||
*/
|
||||
import {
|
||||
type CreateDictItemInput,
|
||||
createDictItemInputSchema,
|
||||
type CreateDictTypeInput,
|
||||
createDictTypeInputSchema,
|
||||
type DictItem,
|
||||
type DictItemListQuery,
|
||||
dictItemListQuerySchema,
|
||||
type DictItemListResponse,
|
||||
dictItemListResponseSchema,
|
||||
dictItemSchema,
|
||||
type DictTypeDetail,
|
||||
dictTypeDetailSchema,
|
||||
type DictTypeListQuery,
|
||||
dictTypeListQuerySchema,
|
||||
type DictTypeSummary,
|
||||
dictTypeSummarySchema,
|
||||
listResponseSchema,
|
||||
type SetDictTypeStatusInput,
|
||||
setDictTypeStatusInputSchema,
|
||||
type UpdateDictItemInput,
|
||||
updateDictItemInputSchema,
|
||||
type UpdateDictTypeInput,
|
||||
updateDictTypeInputSchema,
|
||||
} from '@lingxi/shared';
|
||||
|
||||
import { request } from './client';
|
||||
import { ENDPOINTS } from './endpoints';
|
||||
|
||||
const dictTypeListResponseSchema = listResponseSchema(dictTypeSummarySchema);
|
||||
|
||||
function searchParams(entries: Record<string, string | number | undefined>): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
if (value !== undefined && value !== '') params.set(key, String(value));
|
||||
}
|
||||
const search = params.toString();
|
||||
return search === '' ? '' : `?${search}`;
|
||||
}
|
||||
|
||||
/** 字典类型列表:keyword / group / status / limit / cursor(query snake_case) */
|
||||
export async function fetchDictTypeList(
|
||||
query: Partial<DictTypeListQuery> = {},
|
||||
): Promise<{ items: DictTypeSummary[]; next_cursor: string | null }> {
|
||||
const parsed = dictTypeListQuerySchema.parse(query);
|
||||
const payload: unknown = await request(
|
||||
`${ENDPOINTS.dicts}${searchParams({
|
||||
keyword: parsed.keyword,
|
||||
group: parsed.group,
|
||||
status: parsed.status,
|
||||
limit: parsed.limit,
|
||||
cursor: parsed.cursor,
|
||||
})}`,
|
||||
);
|
||||
return dictTypeListResponseSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function fetchDictTypeDetail(typeCode: string): Promise<DictTypeDetail> {
|
||||
const payload: unknown = await request(ENDPOINTS.dict(typeCode));
|
||||
return dictTypeDetailSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function createDictType(input: CreateDictTypeInput): Promise<DictTypeDetail> {
|
||||
const body = createDictTypeInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.dicts, { method: 'POST', body });
|
||||
return dictTypeDetailSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function updateDictType(
|
||||
typeCode: string,
|
||||
input: UpdateDictTypeInput,
|
||||
): Promise<DictTypeDetail> {
|
||||
const body = updateDictTypeInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.dict(typeCode), { method: 'PATCH', body });
|
||||
return dictTypeDetailSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function setDictTypeStatus(
|
||||
typeCode: string,
|
||||
input: SetDictTypeStatusInput,
|
||||
): Promise<DictTypeDetail> {
|
||||
const body = setDictTypeStatusInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.dictStatus(typeCode), {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
});
|
||||
return dictTypeDetailSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function deleteDictType(typeCode: string): Promise<void> {
|
||||
await request<unknown>(ENDPOINTS.dict(typeCode), { method: 'DELETE' });
|
||||
}
|
||||
|
||||
/** 生效条目列表(三层合并结果):不传 industry_id 表示仅通用层 */
|
||||
export async function fetchDictItemList(
|
||||
typeCode: string,
|
||||
query: Partial<DictItemListQuery> = {},
|
||||
): Promise<DictItemListResponse> {
|
||||
const parsed = dictItemListQuerySchema.parse(query);
|
||||
const payload: unknown = await request(
|
||||
`${ENDPOINTS.dictItems(typeCode)}${searchParams({ industry_id: parsed.industry_id })}`,
|
||||
);
|
||||
return dictItemListResponseSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function createDictItem(
|
||||
typeCode: string,
|
||||
input: CreateDictItemInput,
|
||||
): Promise<DictItem> {
|
||||
const body = createDictItemInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.dictItems(typeCode), { method: 'POST', body });
|
||||
return dictItemSchema.parse(payload);
|
||||
}
|
||||
|
||||
export async function updateDictItem(
|
||||
typeCode: string,
|
||||
itemId: string,
|
||||
input: UpdateDictItemInput,
|
||||
): Promise<DictItem> {
|
||||
const body = updateDictItemInputSchema.parse(input);
|
||||
const payload: unknown = await request(ENDPOINTS.dictItem(typeCode, itemId), {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
});
|
||||
return dictItemSchema.parse(payload);
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
* 后端若调整路径,只改这里;联调时以 `apps/web/src/api/generated/openapi.d.ts` 为准。
|
||||
*/
|
||||
const USERS = '/users';
|
||||
const DICTS = '/dicts';
|
||||
const SYSTEM_CONFIGS = '/system/configs';
|
||||
|
||||
export const ENDPOINTS = {
|
||||
login: '/auth/login',
|
||||
@@ -52,4 +54,14 @@ export const ENDPOINTS = {
|
||||
`/customers/${encodeURIComponent(customerId)}/fields`,
|
||||
customerFieldApprove: (fieldId: string): string =>
|
||||
`/customer-fields/${encodeURIComponent(fieldId)}/approve`,
|
||||
// A4 dict / system config
|
||||
dicts: DICTS,
|
||||
dict: (typeCode: string): string => `${DICTS}/${encodeURIComponent(typeCode)}`,
|
||||
dictStatus: (typeCode: string): string => `${DICTS}/${encodeURIComponent(typeCode)}/status`,
|
||||
dictItems: (typeCode: string): string => `${DICTS}/${encodeURIComponent(typeCode)}/items`,
|
||||
dictItem: (typeCode: string, itemId: string): string =>
|
||||
`${DICTS}/${encodeURIComponent(typeCode)}/items/${encodeURIComponent(itemId)}`,
|
||||
systemConfigs: SYSTEM_CONFIGS,
|
||||
systemConfigsEffective: `${SYSTEM_CONFIGS}/effective`,
|
||||
systemConfigsHistory: `${SYSTEM_CONFIGS}/history`,
|
||||
} as const;
|
||||
+362
@@ -627,6 +627,147 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/dicts': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 字典类型列表(分组顺序 + 排序;含条目 / 行业层统计) */
|
||||
get: operations['DictsController_list'];
|
||||
put?: never;
|
||||
/** 新建字典类型(编码唯一;builtin=false) */
|
||||
post: operations['DictsController_create'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/dicts/{code}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 字典类型详情(不存在 40401) */
|
||||
get: operations['DictsController_detail'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
/** 删除自定义字典类型(内置 / 有子条目时不可删) */
|
||||
delete: operations['DictsController_remove'];
|
||||
options?: never;
|
||||
head?: never;
|
||||
/** 更新字典类型(编码 / 形态 / 行业作用域不可改) */
|
||||
patch: operations['DictsController_update'];
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/dicts/{code}/status': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
/** 启用 / 停用字典类型(内置类型允许停用) */
|
||||
patch: operations['DictsController_setStatus'];
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/dicts/{code}/items': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 生效条目列表(industry_id 三层合并;非行业类型忽略该参数) */
|
||||
get: operations['DictsController_listItems'];
|
||||
put?: never;
|
||||
/** 新建字典条目(编码 / 父级 / 行业校验;编码创建后不可改) */
|
||||
post: operations['DictsController_createItem'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/dicts/{code}/items/{itemId}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
/** 更新字典条目(仅名称 / 排序 / 启停 / 备注) */
|
||||
patch: operations['DictsController_updateItem'];
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/system/configs': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 全量系统配置(定义元数据 + 当前值 / 版本) */
|
||||
get: operations['SystemConfigController_list'];
|
||||
/** 批量保存配置(乐观锁 expectedVersion;写版本历史与审计) */
|
||||
put: operations['SystemConfigController_update'];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/system/configs/effective': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 生效配置(仅登录;只返回 public 项,前端上限读取入口) */
|
||||
get: operations['SystemConfigController_effective'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/system/configs/history': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 配置变更记录(按变更时间倒序;操作人为用户姓名) */
|
||||
get: operations['SystemConfigController_history'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/health': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1559,6 +1700,227 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_create: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_detail: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_remove: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_update: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_setStatus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_listItems: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_createItem: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
DictsController_updateItem: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SystemConfigController_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SystemConfigController_update: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SystemConfigController_effective: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SystemConfigController_history: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
HealthController_check: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -26,4 +26,13 @@ export const queryKeys = {
|
||||
customerGroup: (groupId: string) => ['customer-groups', 'detail', groupId] as const,
|
||||
customerFields: (query: object) => ['customer-fields', 'list', query] as const,
|
||||
customerFieldsRoot: ['customer-fields'] as const,
|
||||
// A4 dict / system config
|
||||
dicts: ['dicts'] as const,
|
||||
dictTypeList: (query: object) => ['dicts', 'list', query] as const,
|
||||
dictType: (typeCode: string) => ['dicts', 'detail', typeCode] as const,
|
||||
dictItems: (typeCode: string, industryId: string | null) =>
|
||||
['dicts', 'items', typeCode, industryId ?? 'all'] as const,
|
||||
systemConfigs: ['system-configs'] as const,
|
||||
systemConfigsEffective: ['system-configs', 'effective'] as const,
|
||||
systemConfigHistory: (query: object) => ['system-configs', 'history', query] as const,
|
||||
};
|
||||
Reference in new issue
Block a user