initial commit
This commit is contained in:
73
mock/agent.ts
Normal file
73
mock/agent.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const agents = {
|
||||
team: {
|
||||
name: 'team',
|
||||
description: '管理复杂开发任务的项目经理和团队协调者',
|
||||
mode: 'primary',
|
||||
prompt: './.opencode/agents/team.md',
|
||||
temperature: 0.3,
|
||||
tools: { write: false, edit: false, bash: true },
|
||||
},
|
||||
planning: {
|
||||
name: 'planning',
|
||||
description: '专注于深度分析、需求拆解和实施路线图的技术架构师',
|
||||
mode: 'subagent',
|
||||
prompt: './.opencode/agents/planning.md',
|
||||
temperature: 0.2,
|
||||
tools: { write: false, edit: false, bash: false },
|
||||
},
|
||||
frontend: {
|
||||
name: 'frontend',
|
||||
description: '资深前端与 UmiJS 专家,负责从服务层到 UI/UX 的全栈实施',
|
||||
mode: 'subagent',
|
||||
prompt: './.opencode/agents/frontend.md',
|
||||
temperature: 0.3,
|
||||
tools: { write: true, edit: true, bash: true },
|
||||
},
|
||||
'code-spec': {
|
||||
name: 'code-spec',
|
||||
description: '强制执行 Ant Design 和 ProComponents 最佳实践的代码规范专家',
|
||||
mode: 'subagent',
|
||||
prompt: './.opencode/agents/code-spec.md',
|
||||
temperature: 0.1,
|
||||
tools: { write: true, edit: true, bash: false },
|
||||
},
|
||||
'qa-tester': {
|
||||
name: 'qa-tester',
|
||||
description: '进行功能测试和 i18n 验证的资深 QA 工程师',
|
||||
mode: 'subagent',
|
||||
prompt: './.opencode/agents/qa-tester.md',
|
||||
temperature: 0.2,
|
||||
tools: { write: false, edit: false, bash: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
'GET /api/agents': (req: Request, res: Response) => {
|
||||
const list = Object.keys(agents).map((key) => ({
|
||||
key,
|
||||
...agents[key],
|
||||
promptPath: agents[key].prompt,
|
||||
}));
|
||||
res.send({ data: list, success: true });
|
||||
},
|
||||
'GET /api/agents/:key/prompt': (req: Request, res: Response) => {
|
||||
res.send({
|
||||
data: `这是一个模拟的 ${req.params.key} Prompt 内容...\n# ${req.params.key.toUpperCase()} Agent\n...`,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
'POST /api/agents/update': (req: Request, res: Response) => {
|
||||
res.send({ success: true, message: '配置已更新' });
|
||||
},
|
||||
'GET /api/skills': (req: Request, res: Response) => {
|
||||
res.send({
|
||||
data: [
|
||||
{ id: '1', name: 'Ant Design Skill', description: '提供标准组件模式和样式指南', isEnabled: true },
|
||||
{ id: '2', name: 'Security Audit', description: '代码安全与 XSS 检查脚本', isEnabled: false },
|
||||
],
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
83
mock/article.mock.ts
Normal file
83
mock/article.mock.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Request, Response } from 'express';
|
||||
import type { ArticleItem } from '../src/pages/Article/data';
|
||||
|
||||
const articles: ArticleItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: '深入浅出 UmiJS 4 核心实战',
|
||||
cover: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=60',
|
||||
summary: '本文将带你深入了解 UmiJS 4 的核心特性,包括路由加载、插件机制以及与 ProComponents 的深度集成。',
|
||||
htmlContent: '<p>UmiJS 是可扩展的企业级前端框架,集成了 React 核心能力...</p>',
|
||||
category: '技术框架',
|
||||
tags: ['UmiJS', 'React', 'Frontend'],
|
||||
status: 'published',
|
||||
publishTime: '2024-02-14 10:00:00',
|
||||
createdAt: '2024-02-14 09:00:00',
|
||||
updatedAt: '2024-02-14 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Ant Design 5.0 设计趋势解析',
|
||||
cover: 'https://images.unsplash.com/photo-1586717791821-3f44a563eb4c?w=800&auto=format&fit=crop&q=60',
|
||||
summary: 'Ant Design 5.0 带来了全新的设计语言:快乐工作,本文解析了其背后的设计哲学。',
|
||||
htmlContent: '<p>快乐工作是 Ant Design 5.0 的核心理念...</p>',
|
||||
category: '设计规范',
|
||||
tags: ['Ant Design', 'Design', 'UI'],
|
||||
status: 'draft',
|
||||
createdAt: '2024-02-14 11:00:00',
|
||||
updatedAt: '2024-02-14 11:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/articles': (req: Request, res: Response) => {
|
||||
const { title } = req.query;
|
||||
let filteredData = [...articles];
|
||||
if (title) {
|
||||
filteredData = filteredData.filter(item => item.title.includes(title as string));
|
||||
}
|
||||
res.json({
|
||||
data: filteredData,
|
||||
total: filteredData.length,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
'GET /api/articles/:id': (req: Request, res: Response) => {
|
||||
const article = articles.find((i) => i.id === req.params.id);
|
||||
res.json({
|
||||
data: article,
|
||||
success: !!article,
|
||||
});
|
||||
},
|
||||
'POST /api/articles': (req: Request, res: Response) => {
|
||||
const newArticle = {
|
||||
...req.body,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
articles.unshift(newArticle);
|
||||
res.json({
|
||||
data: newArticle,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
'PUT /api/articles/:id': (req: Request, res: Response) => {
|
||||
const index = articles.findIndex((item) => item.id === req.params.id);
|
||||
if (index !== -1) {
|
||||
articles[index] = { ...articles[index], ...req.body, updatedAt: new Date().toISOString() };
|
||||
res.json({ data: articles[index], success: true });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Article not found' });
|
||||
}
|
||||
},
|
||||
'DELETE /api/articles/:id': (req: Request, res: Response) => {
|
||||
const index = articles.findIndex((item) => item.id === req.params.id);
|
||||
if (index !== -1) {
|
||||
articles.splice(index, 1);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Article not found' });
|
||||
}
|
||||
},
|
||||
};
|
||||
454
mock/interface.mock.ts
Normal file
454
mock/interface.mock.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
import type {
|
||||
InterfaceChangeLogItem,
|
||||
InterfaceDocInfo,
|
||||
InterfaceExportResponse,
|
||||
InterfaceFormValues,
|
||||
InterfaceGatewayFormValues,
|
||||
InterfaceGatewayPolicy,
|
||||
InterfaceIntegrationInfo,
|
||||
InterfaceItem,
|
||||
InterfaceListParams,
|
||||
InterfaceMockInfo,
|
||||
InterfaceRoutePreview,
|
||||
} from '@/pages/InterfaceManagement/data';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { ParamsDictionary } from 'express-serve-static-core';
|
||||
|
||||
type InterfaceListQuery = Omit<InterfaceListParams, 'current' | 'pageSize'> & {
|
||||
current?: string;
|
||||
pageSize?: string;
|
||||
};
|
||||
|
||||
const interfaceList: InterfaceItem[] = [
|
||||
{
|
||||
id: 'api-1001',
|
||||
name: '用户列表查询',
|
||||
method: 'GET',
|
||||
path: '/api/users',
|
||||
owner: '李娜',
|
||||
status: 'active',
|
||||
version: 'v1',
|
||||
description: '分页获取用户列表',
|
||||
createdAt: '2024-10-01 09:30:00',
|
||||
updatedAt: '2024-10-10 18:20:00',
|
||||
},
|
||||
{
|
||||
id: 'api-1002',
|
||||
name: '新增订单',
|
||||
method: 'POST',
|
||||
path: '/api/orders',
|
||||
owner: '王彬',
|
||||
status: 'active',
|
||||
version: 'v2',
|
||||
description: '创建新的订单记录',
|
||||
createdAt: '2024-09-18 13:10:00',
|
||||
updatedAt: '2024-10-05 11:05:00',
|
||||
},
|
||||
{
|
||||
id: 'api-1003',
|
||||
name: '库存调整',
|
||||
method: 'PUT',
|
||||
path: '/api/inventory/adjust',
|
||||
owner: '赵敏',
|
||||
status: 'deprecated',
|
||||
version: 'v1',
|
||||
description: '旧库存调整接口,计划下线',
|
||||
createdAt: '2024-08-02 08:45:00',
|
||||
updatedAt: '2024-09-21 16:40:00',
|
||||
},
|
||||
{
|
||||
id: 'api-1004',
|
||||
name: '删除文章',
|
||||
method: 'DELETE',
|
||||
path: '/api/articles/:id',
|
||||
owner: '陈曦',
|
||||
status: 'disabled',
|
||||
version: 'v1',
|
||||
description: '文章删除功能已暂时关闭',
|
||||
createdAt: '2024-07-12 10:20:00',
|
||||
updatedAt: '2024-08-20 19:10:00',
|
||||
},
|
||||
];
|
||||
|
||||
const interfaceDocMap: Record<string, InterfaceDocInfo> = {
|
||||
'api-1001': {
|
||||
docUrl: 'https://docs.example.com/interfaces/api-1001',
|
||||
exampleText:
|
||||
'curl -X GET https://api.example.com/api/users?page=1&pageSize=10',
|
||||
updatedAt: '2024-10-10 18:20:00',
|
||||
owner: '李娜',
|
||||
},
|
||||
'api-1002': {
|
||||
docUrl: 'https://docs.example.com/interfaces/api-1002',
|
||||
exampleText: 'curl -X POST https://api.example.com/api/orders -d "{}"',
|
||||
updatedAt: '2024-10-05 11:05:00',
|
||||
owner: '王彬',
|
||||
},
|
||||
};
|
||||
|
||||
const gatewayPolicyMap: Record<string, InterfaceGatewayPolicy> = {
|
||||
'api-1001': {
|
||||
routeId: 'route-api-1001',
|
||||
rateLimit: '2000 次/分钟',
|
||||
authType: 'OAuth2 + 签名校验',
|
||||
timeoutMs: 3000,
|
||||
},
|
||||
'api-1002': {
|
||||
routeId: 'route-api-1002',
|
||||
rateLimit: '1200 次/分钟',
|
||||
authType: 'API Key',
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
const mockInfoMap: Record<string, InterfaceMockInfo> = {
|
||||
'api-1001': {
|
||||
requestSample:
|
||||
'curl -X GET https://api.example.com/api/users?page=1&pageSize=10',
|
||||
responseSample: '{"success":true,"data":[{"id":"1"}],"total":1}',
|
||||
latency: '120ms',
|
||||
mockEnabled: true,
|
||||
mockUrl: 'https://mock.example.com/interfaces/api-1001',
|
||||
},
|
||||
'api-1002': {
|
||||
requestSample: 'curl -X POST https://api.example.com/api/orders -d "{}"',
|
||||
responseSample: '{"success":true,"data":{"id":"100"}}',
|
||||
latency: '210ms',
|
||||
mockEnabled: false,
|
||||
mockUrl: 'https://mock.example.com/interfaces/api-1002',
|
||||
},
|
||||
};
|
||||
|
||||
const integrationInfoMap: Record<string, InterfaceIntegrationInfo> = {
|
||||
'api-1001': {
|
||||
partner: '物流平台 A',
|
||||
channel: 'API Key + IP 白名单',
|
||||
status: '已接入',
|
||||
consoleUrl: 'https://console.example.com/integrations/api-1001',
|
||||
},
|
||||
'api-1002': {
|
||||
partner: '支付平台 B',
|
||||
channel: 'JWT + 双向 TLS',
|
||||
status: '对接中',
|
||||
consoleUrl: 'https://console.example.com/integrations/api-1002',
|
||||
},
|
||||
};
|
||||
|
||||
const changeLogMap: Record<string, InterfaceChangeLogItem[]> = {
|
||||
'api-1001': [
|
||||
{
|
||||
id: 'log-1',
|
||||
time: '2024-10-10 18:20',
|
||||
description: '调整响应字段,新增可选字段 note',
|
||||
operator: '李娜',
|
||||
},
|
||||
{
|
||||
id: 'log-2',
|
||||
time: '2024-09-18 13:10',
|
||||
description: '接口首次发布',
|
||||
operator: '王彬',
|
||||
},
|
||||
],
|
||||
'api-1002': [
|
||||
{
|
||||
id: 'log-3',
|
||||
time: '2024-10-05 11:05',
|
||||
description: '补充校验规则与异常码',
|
||||
operator: '王彬',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const parseNumber = (value: string | undefined, fallback: number) => {
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? fallback : parsed;
|
||||
};
|
||||
|
||||
const getInterfaceItem = (id: string) =>
|
||||
interfaceList.find((item) => item.id === id);
|
||||
|
||||
const ensureGatewayPolicy = (id: string): InterfaceGatewayPolicy => {
|
||||
if (!gatewayPolicyMap[id]) {
|
||||
gatewayPolicyMap[id] = {
|
||||
routeId: `route-${id}`,
|
||||
rateLimit: '2000 次/分钟',
|
||||
authType: 'OAuth2 + 签名校验',
|
||||
timeoutMs: 3000,
|
||||
};
|
||||
}
|
||||
return gatewayPolicyMap[id];
|
||||
};
|
||||
|
||||
const ensureMockInfo = (id: string): InterfaceMockInfo => {
|
||||
if (!mockInfoMap[id]) {
|
||||
const item = getInterfaceItem(id);
|
||||
mockInfoMap[id] = {
|
||||
requestSample: `curl -X ${item?.method ?? 'GET'} https://api.example.com${
|
||||
item?.path ?? '/api/example'
|
||||
}`,
|
||||
responseSample: '{"success":true}',
|
||||
latency: '120ms',
|
||||
mockEnabled: false,
|
||||
mockUrl: `https://mock.example.com/interfaces/${id}`,
|
||||
};
|
||||
}
|
||||
return mockInfoMap[id];
|
||||
};
|
||||
|
||||
export default {
|
||||
'GET /api/interfaces': (
|
||||
req: Request<ParamsDictionary, unknown, unknown, InterfaceListQuery>,
|
||||
res: Response,
|
||||
) => {
|
||||
const { current, pageSize, name, method, path, owner, status, version } =
|
||||
req.query;
|
||||
const currentPage = parseNumber(current, 1);
|
||||
const size = parseNumber(pageSize, 10);
|
||||
|
||||
let dataSource = [...interfaceList];
|
||||
|
||||
if (name) {
|
||||
dataSource = dataSource.filter((item) => item.name.includes(name));
|
||||
}
|
||||
|
||||
if (method) {
|
||||
dataSource = dataSource.filter((item) => item.method === method);
|
||||
}
|
||||
|
||||
if (path) {
|
||||
dataSource = dataSource.filter((item) => item.path.includes(path));
|
||||
}
|
||||
|
||||
if (owner) {
|
||||
dataSource = dataSource.filter((item) => item.owner.includes(owner));
|
||||
}
|
||||
|
||||
if (status) {
|
||||
dataSource = dataSource.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
if (version) {
|
||||
dataSource = dataSource.filter((item) => item.version.includes(version));
|
||||
}
|
||||
|
||||
const start = (currentPage - 1) * size;
|
||||
const end = start + size;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataSource.slice(start, end),
|
||||
total: dataSource.length,
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/interfaces': (
|
||||
req: Request<ParamsDictionary, InterfaceItem, InterfaceFormValues>,
|
||||
res: Response,
|
||||
) => {
|
||||
const now = new Date().toISOString();
|
||||
const newItem: InterfaceItem = {
|
||||
id: `api-${Date.now()}`,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...req.body,
|
||||
};
|
||||
interfaceList.unshift(newItem);
|
||||
res.json(newItem);
|
||||
},
|
||||
|
||||
'PUT /api/interfaces/:id': (
|
||||
req: Request<{ id: string }, InterfaceItem, InterfaceFormValues>,
|
||||
res: Response,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const index = interfaceList.findIndex((item) => item.id === id);
|
||||
if (index >= 0) {
|
||||
interfaceList[index] = {
|
||||
...interfaceList[index],
|
||||
...req.body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
res.json(interfaceList[index]);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
data: interfaceList,
|
||||
total: interfaceList.length,
|
||||
});
|
||||
},
|
||||
|
||||
'DELETE /api/interfaces/:id': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const index = interfaceList.findIndex((item) => item.id === id);
|
||||
if (index >= 0) {
|
||||
interfaceList.splice(index, 1);
|
||||
res.json({ success: true });
|
||||
return;
|
||||
}
|
||||
res.status(404).json({ success: false });
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/doc': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceDocInfo>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const item = getInterfaceItem(id);
|
||||
if (!item) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
const docInfo: InterfaceDocInfo = interfaceDocMap[id] ?? {
|
||||
docUrl: `https://docs.example.com/interfaces/${id}`,
|
||||
exampleText: `curl -X ${item.method} https://api.example.com${item.path}`,
|
||||
updatedAt: item.updatedAt,
|
||||
owner: item.owner,
|
||||
};
|
||||
interfaceDocMap[id] = docInfo;
|
||||
res.json(docInfo);
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/gateway': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceGatewayPolicy>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
res.json(ensureGatewayPolicy(id));
|
||||
},
|
||||
|
||||
'PUT /api/interfaces/:id/gateway': (
|
||||
req: Request<
|
||||
{ id: string },
|
||||
InterfaceGatewayPolicy,
|
||||
InterfaceGatewayFormValues
|
||||
>,
|
||||
res: Response<InterfaceGatewayPolicy>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const currentPolicy = ensureGatewayPolicy(id);
|
||||
const nextPolicy: InterfaceGatewayPolicy = {
|
||||
...currentPolicy,
|
||||
rateLimit: req.body.rateLimit,
|
||||
authType: req.body.authType,
|
||||
timeoutMs: req.body.timeoutMs,
|
||||
};
|
||||
gatewayPolicyMap[id] = nextPolicy;
|
||||
res.json(nextPolicy);
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/gateway/preview': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceRoutePreview>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const item = getInterfaceItem(id);
|
||||
if (!item) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
const policy = ensureGatewayPolicy(id);
|
||||
const preview: InterfaceRoutePreview = {
|
||||
routeId: policy.routeId,
|
||||
upstream: 'https://gateway.example.com',
|
||||
path: item.path,
|
||||
method: item.method,
|
||||
timeoutMs: policy.timeoutMs,
|
||||
rateLimit: policy.rateLimit,
|
||||
authType: policy.authType,
|
||||
};
|
||||
res.json(preview);
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/mock': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceMockInfo>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
res.json(ensureMockInfo(id));
|
||||
},
|
||||
|
||||
'POST /api/interfaces/:id/debug': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<{ success: boolean; message: string }>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const mockInfo = ensureMockInfo(id);
|
||||
mockInfo.mockEnabled = true;
|
||||
mockInfo.latency = '98ms';
|
||||
res.json({ success: true, message: 'debug-started' });
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/integrations': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceIntegrationInfo>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const info =
|
||||
integrationInfoMap[id] ??
|
||||
({
|
||||
partner: '待配置',
|
||||
channel: '未配置',
|
||||
status: '未接入',
|
||||
consoleUrl: 'https://console.example.com/integrations',
|
||||
} as InterfaceIntegrationInfo);
|
||||
integrationInfoMap[id] = info;
|
||||
res.json(info);
|
||||
},
|
||||
|
||||
'POST /api/interfaces/:id/integrations': (
|
||||
req: Request<
|
||||
{ id: string },
|
||||
InterfaceIntegrationInfo,
|
||||
Pick<InterfaceIntegrationInfo, 'partner' | 'channel'>
|
||||
>,
|
||||
res: Response<InterfaceIntegrationInfo>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const info: InterfaceIntegrationInfo = {
|
||||
partner: req.body.partner,
|
||||
channel: req.body.channel,
|
||||
status: '已接入',
|
||||
consoleUrl: `https://console.example.com/integrations/${id}`,
|
||||
};
|
||||
integrationInfoMap[id] = info;
|
||||
res.json(info);
|
||||
},
|
||||
|
||||
'POST /api/interfaces/:id/integrations/key': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceIntegrationInfo>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const info = integrationInfoMap[id] ?? {
|
||||
partner: '待配置',
|
||||
channel: '未配置',
|
||||
status: '未接入',
|
||||
consoleUrl: `https://console.example.com/integrations/${id}`,
|
||||
};
|
||||
res.json(info);
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/changelog': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceChangeLogItem[]>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
res.json(changeLogMap[id] ?? []);
|
||||
},
|
||||
|
||||
'GET /api/interfaces/:id/changelog/export': (
|
||||
req: Request<{ id: string }>,
|
||||
res: Response<InterfaceExportResponse>,
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
res.json({
|
||||
downloadUrl: `https://download.example.com/interfaces/${id}/changelog.csv`,
|
||||
fileName: `interface-${id}-changelog.csv`,
|
||||
});
|
||||
},
|
||||
};
|
||||
166
mock/inventory.mock.ts
Normal file
166
mock/inventory.mock.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
// 初始数据
|
||||
const genInventoryList = (current: number, pageSize: number) => {
|
||||
const tableListDataSource: any[] = [];
|
||||
const warehouses = ['上海一号仓', '北京东区仓', '深圳前海仓'];
|
||||
const units = ['个', '箱', '台'];
|
||||
const statusList = ['normal', 'low_stock', 'out_of_stock'];
|
||||
|
||||
for (let i = 0; i < pageSize; i += 1) {
|
||||
const quantity = Math.floor(Math.random() * 200);
|
||||
let status = 'normal';
|
||||
if (quantity === 0) status = 'out_of_stock';
|
||||
else if (quantity < 10) status = 'low_stock';
|
||||
|
||||
tableListDataSource.push({
|
||||
key: i,
|
||||
id: `inv-${i}`,
|
||||
sku: `SKU-2024-${String(i).padStart(3, '0')}`,
|
||||
name: `高性能组件-${i}`,
|
||||
quantity,
|
||||
unit: units[i % 3],
|
||||
warehouse: warehouses[i % 3],
|
||||
status,
|
||||
minStock: 10,
|
||||
maxStock: 500,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
category: '电子元器件',
|
||||
description: '关键零部件,需定期盘点',
|
||||
});
|
||||
}
|
||||
return tableListDataSource;
|
||||
};
|
||||
|
||||
let tableListDataSource = genInventoryList(1, 40);
|
||||
const logs: Record<string, any[]> = {}; // Map<InventoryId, LogItem[]>
|
||||
|
||||
export default {
|
||||
'GET /api/inventory': (req: Request, res: Response) => {
|
||||
const { current = 1, pageSize = 20, name, sku, status, warehouse } = req.query as any;
|
||||
|
||||
let dataSource = [...tableListDataSource];
|
||||
|
||||
if (name) {
|
||||
dataSource = dataSource.filter(item => item.name.includes(name));
|
||||
}
|
||||
if (sku) {
|
||||
dataSource = dataSource.filter(item => item.sku.includes(sku));
|
||||
}
|
||||
if (status) {
|
||||
dataSource = dataSource.filter(item => item.status === status);
|
||||
}
|
||||
if (warehouse) {
|
||||
dataSource = dataSource.filter(item => item.warehouse === warehouse);
|
||||
}
|
||||
|
||||
const total = dataSource.length;
|
||||
const startIndex = ((current as number) - 1) * (pageSize as number);
|
||||
const endIndex = (current as number) * (pageSize as number);
|
||||
const list = dataSource.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
data: list,
|
||||
total,
|
||||
success: true,
|
||||
pageSize,
|
||||
current: parseInt(`${current}`, 10) || 1,
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/inventory': (req: Request, res: Response) => {
|
||||
const newData = {
|
||||
...req.body,
|
||||
id: `inv-${Date.now()}`,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
status: req.body.quantity > 0 ? 'normal' : 'out_of_stock',
|
||||
};
|
||||
tableListDataSource.unshift(newData);
|
||||
res.json(newData);
|
||||
},
|
||||
|
||||
'PUT /api/inventory/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const index = tableListDataSource.findIndex(item => item.id === id);
|
||||
if (index !== -1) {
|
||||
tableListDataSource[index] = { ...tableListDataSource[index], ...req.body, lastUpdated: new Date().toISOString() };
|
||||
res.json(tableListDataSource[index]);
|
||||
} else {
|
||||
res.status(404).json({ success: false });
|
||||
}
|
||||
},
|
||||
|
||||
'POST /api/inventory/:id/adjust': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { type, quantity, remark } = req.body; // type: 'in' | 'out' | 'adjust'
|
||||
const index = tableListDataSource.findIndex(item => item.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
const item = tableListDataSource[index];
|
||||
let newQuantity = item.quantity;
|
||||
let changeAmount = 0;
|
||||
|
||||
if (type === 'in') {
|
||||
newQuantity += parseInt(quantity, 10);
|
||||
changeAmount = parseInt(quantity, 10);
|
||||
} else if (type === 'out') {
|
||||
newQuantity -= parseInt(quantity, 10);
|
||||
changeAmount = -parseInt(quantity, 10);
|
||||
} else if (type === 'adjust') {
|
||||
changeAmount = parseInt(quantity, 10) - item.quantity; // 盘点调整:直接设为新值
|
||||
newQuantity = parseInt(quantity, 10);
|
||||
}
|
||||
|
||||
if (newQuantity < 0) {
|
||||
res.status(400).json({ success: false, message: '库存不足,无法扣减' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update Status
|
||||
let status = 'normal';
|
||||
if (newQuantity === 0) status = 'out_of_stock';
|
||||
else if (newQuantity < (item.minStock || 10)) status = 'low_stock';
|
||||
|
||||
tableListDataSource[index] = { ...item, quantity: newQuantity, status, lastUpdated: new Date().toISOString() };
|
||||
|
||||
// Log
|
||||
const logId = String(id);
|
||||
if (!logs[logId]) logs[logId] = [];
|
||||
logs[logId].unshift({
|
||||
id: `log-${Date.now()}`,
|
||||
inventoryId: logId,
|
||||
type,
|
||||
quantity: changeAmount,
|
||||
beforeQuantity: item.quantity,
|
||||
afterQuantity: newQuantity,
|
||||
operator: 'Admin', // MOCKED
|
||||
remark,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.json({ success: true, data: tableListDataSource[index] });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Item not found' });
|
||||
}
|
||||
},
|
||||
|
||||
'GET /api/inventory/:id/logs': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const logId = String(id);
|
||||
const list = logs[logId] || [];
|
||||
res.json({
|
||||
data: list,
|
||||
total: list.length,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
|
||||
'DELETE /api/inventory/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
tableListDataSource = tableListDataSource.filter(item => item.id !== id);
|
||||
delete logs[String(id)];
|
||||
res.json({ success: true });
|
||||
}
|
||||
|
||||
};
|
||||
572
mock/log.mock.ts
Normal file
572
mock/log.mock.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import { Request, Response } from 'express';
|
||||
import type {
|
||||
LogItem,
|
||||
OperationModule,
|
||||
OperationStatus,
|
||||
OperationType,
|
||||
} from '../src/pages/LogAudit/data.d';
|
||||
|
||||
// 操作人姓名列表
|
||||
const operatorNames = [
|
||||
'张伟',
|
||||
'李娜',
|
||||
'王强',
|
||||
'刘洋',
|
||||
'陈静',
|
||||
'杨帆',
|
||||
'赵敏',
|
||||
'黄涛',
|
||||
'周丽',
|
||||
'吴刚',
|
||||
'徐静',
|
||||
'孙鹏',
|
||||
'马丽',
|
||||
'朱杰',
|
||||
'胡军',
|
||||
'林娜',
|
||||
'郭明',
|
||||
'何伟',
|
||||
'高峰',
|
||||
'梁雨',
|
||||
'system',
|
||||
'admin',
|
||||
'root',
|
||||
];
|
||||
|
||||
// 角色列表
|
||||
const operatorRoles = [
|
||||
'超级管理员',
|
||||
'系统管理员',
|
||||
'运营专员',
|
||||
'内容编辑',
|
||||
'普通用户',
|
||||
'审计员',
|
||||
];
|
||||
|
||||
// 操作类型
|
||||
const operationTypes: OperationType[] = [
|
||||
'CREATE',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'QUERY',
|
||||
'LOGIN',
|
||||
'LOGOUT',
|
||||
'EXPORT',
|
||||
'IMPORT',
|
||||
'ENABLE',
|
||||
'DISABLE',
|
||||
'OTHER',
|
||||
];
|
||||
|
||||
// 操作模块
|
||||
const operationModules: OperationModule[] = [
|
||||
'USER',
|
||||
'PRODUCT',
|
||||
'ORDER',
|
||||
'ARTICLE',
|
||||
'SYSTEM',
|
||||
'AUTH',
|
||||
'OTHER',
|
||||
];
|
||||
|
||||
// 操作描述模板
|
||||
const operationDescMap: Record<
|
||||
OperationModule,
|
||||
Record<OperationType, string[]>
|
||||
> = {
|
||||
USER: {
|
||||
CREATE: ['创建新用户账号', '批量导入用户数据', '注册新会员'],
|
||||
UPDATE: ['修改用户信息', '更新用户头像', '修改用户邮箱'],
|
||||
DELETE: ['删除用户账号', '批量删除用户', '注销用户'],
|
||||
QUERY: ['查询用户列表', '查看用户详情', '搜索用户'],
|
||||
LOGIN: ['用户登录系统', '扫码登录', '短信验证码登录'],
|
||||
LOGOUT: ['用户退出登录', '会话超时退出', '强制退出'],
|
||||
EXPORT: ['导出用户列表', '导出用户报表'],
|
||||
IMPORT: ['导入用户数据', '批量导入用户'],
|
||||
ENABLE: ['启用用户账号', '激活用户'],
|
||||
DISABLE: ['禁用用户账号', '冻结用户'],
|
||||
OTHER: ['重置用户密码', '发送验证邮件'],
|
||||
},
|
||||
PRODUCT: {
|
||||
CREATE: ['创建商品', '新增商品信息', '批量添加商品'],
|
||||
UPDATE: ['修改商品信息', '更新商品价格', '修改商品库存'],
|
||||
DELETE: ['删除商品', '批量删除商品'],
|
||||
QUERY: ['查询商品列表', '查看商品详情', '搜索商品'],
|
||||
LOGIN: [],
|
||||
LOGOUT: [],
|
||||
EXPORT: ['导出商品列表', '导出商品库存报表'],
|
||||
IMPORT: ['导入商品数据', '批量导入商品信息'],
|
||||
ENABLE: ['上架商品', '启用商品'],
|
||||
DISABLE: ['下架商品', '禁用商品'],
|
||||
OTHER: ['修改商品图片', '更新商品分类'],
|
||||
},
|
||||
ORDER: {
|
||||
CREATE: ['创建订单', '生成新订单'],
|
||||
UPDATE: ['修改订单状态', '更新订单信息', '修改收货地址'],
|
||||
DELETE: ['删除订单', '取消订单'],
|
||||
QUERY: ['查询订单列表', '查看订单详情', '搜索订单'],
|
||||
LOGIN: [],
|
||||
LOGOUT: [],
|
||||
EXPORT: ['导出订单列表', '导出销售报表'],
|
||||
IMPORT: ['导入订单数据'],
|
||||
ENABLE: [],
|
||||
DISABLE: [],
|
||||
OTHER: ['订单退款', '订单发货', '订单备注'],
|
||||
},
|
||||
ARTICLE: {
|
||||
CREATE: ['发布文章', '创建新文章', '保存草稿'],
|
||||
UPDATE: ['修改文章内容', '更新文章标题', '编辑文章'],
|
||||
DELETE: ['删除文章', '批量删除文章'],
|
||||
QUERY: ['查询文章列表', '查看文章详情', '搜索文章'],
|
||||
LOGIN: [],
|
||||
LOGOUT: [],
|
||||
EXPORT: ['导出文章内容'],
|
||||
IMPORT: ['导入文章'],
|
||||
ENABLE: ['发布文章', '启用文章'],
|
||||
DISABLE: ['隐藏文章', '禁用文章'],
|
||||
OTHER: ['文章审核', '文章置顶'],
|
||||
},
|
||||
SYSTEM: {
|
||||
CREATE: ['创建系统配置', '添加系统参数'],
|
||||
UPDATE: ['修改系统配置', '更新系统参数'],
|
||||
DELETE: ['删除系统配置', '清除缓存'],
|
||||
QUERY: ['查询系统日志', '查看系统状态'],
|
||||
LOGIN: [],
|
||||
LOGOUT: [],
|
||||
EXPORT: ['导出系统日志', '导出配置备份'],
|
||||
IMPORT: ['导入系统配置'],
|
||||
ENABLE: ['启用功能模块'],
|
||||
DISABLE: ['禁用功能模块'],
|
||||
OTHER: ['系统备份', '系统恢复', '清理日志'],
|
||||
},
|
||||
AUTH: {
|
||||
CREATE: ['创建角色', '添加权限', '新建菜单'],
|
||||
UPDATE: ['修改角色权限', '更新菜单', '编辑权限'],
|
||||
DELETE: ['删除角色', '移除权限', '删除菜单'],
|
||||
QUERY: ['查询角色列表', '查看权限树'],
|
||||
LOGIN: ['用户认证', '令牌刷新'],
|
||||
LOGOUT: ['令牌失效'],
|
||||
EXPORT: ['导出权限配置'],
|
||||
IMPORT: ['导入权限配置'],
|
||||
ENABLE: ['启用角色'],
|
||||
DISABLE: ['禁用角色'],
|
||||
OTHER: ['密码修改', '权限分配'],
|
||||
},
|
||||
OTHER: {
|
||||
CREATE: ['创建数据'],
|
||||
UPDATE: ['更新数据'],
|
||||
DELETE: ['删除数据'],
|
||||
QUERY: ['查询数据'],
|
||||
LOGIN: [],
|
||||
LOGOUT: [],
|
||||
EXPORT: ['导出数据'],
|
||||
IMPORT: ['导入数据'],
|
||||
ENABLE: ['启用功能'],
|
||||
DISABLE: ['禁用功能'],
|
||||
OTHER: ['其他操作'],
|
||||
},
|
||||
};
|
||||
|
||||
// 请求方法
|
||||
const requestMethods: Array<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'> = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'PATCH',
|
||||
];
|
||||
|
||||
// 请求 URL 模板
|
||||
const urlTemplates: Record<OperationModule, string[]> = {
|
||||
USER: [
|
||||
'/api/users',
|
||||
'/api/users/list',
|
||||
'/api/users/detail',
|
||||
'/api/auth/login',
|
||||
'/api/auth/logout',
|
||||
],
|
||||
PRODUCT: [
|
||||
'/api/products',
|
||||
'/api/products/list',
|
||||
'/api/products/detail',
|
||||
'/api/products/stock',
|
||||
],
|
||||
ORDER: [
|
||||
'/api/orders',
|
||||
'/api/orders/list',
|
||||
'/api/orders/detail',
|
||||
'/api/orders/status',
|
||||
],
|
||||
ARTICLE: [
|
||||
'/api/articles',
|
||||
'/api/articles/list',
|
||||
'/api/articles/detail',
|
||||
'/api/articles/publish',
|
||||
],
|
||||
SYSTEM: [
|
||||
'/api/system/config',
|
||||
'/api/system/logs',
|
||||
'/api/system/status',
|
||||
'/api/system/backup',
|
||||
],
|
||||
AUTH: [
|
||||
'/api/auth/roles',
|
||||
'/api/auth/permissions',
|
||||
'/api/auth/menus',
|
||||
'/api/auth/login',
|
||||
],
|
||||
OTHER: ['/api/misc', '/api/utils', '/api/common'],
|
||||
};
|
||||
|
||||
// IP 地址池
|
||||
const ipPrefixes = [
|
||||
'192.168.1',
|
||||
'192.168.0',
|
||||
'10.0.0',
|
||||
'172.16.0',
|
||||
'113.45',
|
||||
'36.110',
|
||||
'223.5.5',
|
||||
];
|
||||
|
||||
// User Agent 列表
|
||||
const userAgents = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
||||
];
|
||||
|
||||
// 错误信息模板
|
||||
const errorMessages = [
|
||||
'参数校验失败:必填字段为空',
|
||||
'权限不足:当前用户无操作权限',
|
||||
'数据不存在:记录已被删除或不存在',
|
||||
'数据库连接超时',
|
||||
'系统繁忙,请稍后再试',
|
||||
'请求参数格式错误',
|
||||
'接口限流,请求过于频繁',
|
||||
'Token 已过期,请重新登录',
|
||||
'密码错误,请重新输入',
|
||||
'账号已被锁定',
|
||||
];
|
||||
|
||||
// 生成随机整数
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// 生成随机日期(近30天)
|
||||
function randomDate(): string {
|
||||
const now = new Date();
|
||||
const daysAgo = randomInt(0, 30);
|
||||
const hoursAgo = randomInt(0, 23);
|
||||
const minutesAgo = randomInt(0, 59);
|
||||
const secondsAgo = randomInt(0, 59);
|
||||
|
||||
const date = new Date(
|
||||
now.getTime() -
|
||||
daysAgo * 24 * 60 * 60 * 1000 -
|
||||
hoursAgo * 60 * 60 * 1000 -
|
||||
minutesAgo * 60 * 1000 -
|
||||
secondsAgo * 1000,
|
||||
);
|
||||
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// 生成随机 IP
|
||||
function randomIP(): string {
|
||||
const prefix = ipPrefixes[randomInt(0, ipPrefixes.length - 1)];
|
||||
return `${prefix}.${randomInt(1, 254)}`;
|
||||
}
|
||||
|
||||
// 生成单条日志数据
|
||||
function generateLog(id: number): LogItem {
|
||||
const module = operationModules[randomInt(0, operationModules.length - 1)];
|
||||
const type = operationTypes[randomInt(0, operationTypes.length - 1)];
|
||||
const descList = operationDescMap[module][type];
|
||||
const desc =
|
||||
descList.length > 0
|
||||
? descList[randomInt(0, descList.length - 1)]
|
||||
: `${type}操作`;
|
||||
|
||||
// 90% 成功,10% 失败
|
||||
const isSuccess = Math.random() > 0.1;
|
||||
const status: OperationStatus = isSuccess ? 'success' : 'failure';
|
||||
|
||||
const operatorName = operatorNames[randomInt(0, operatorNames.length - 1)];
|
||||
const operatorId =
|
||||
operatorName === 'system' ? '0' : randomInt(1000, 9999).toString();
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
operatorId,
|
||||
operatorName,
|
||||
operatorRole: operatorRoles[randomInt(0, operatorRoles.length - 1)],
|
||||
operationType: type,
|
||||
operationModule: module,
|
||||
operationDesc: desc,
|
||||
requestMethod: requestMethods[randomInt(0, requestMethods.length - 1)],
|
||||
requestUrl:
|
||||
urlTemplates[module][randomInt(0, urlTemplates[module].length - 1)],
|
||||
requestParams:
|
||||
Math.random() > 0.5
|
||||
? JSON.stringify({ id: randomInt(1, 1000), page: 1, size: 20 })
|
||||
: undefined,
|
||||
responseData:
|
||||
isSuccess && Math.random() > 0.7
|
||||
? JSON.stringify({ code: 200, message: 'success', data: {} })
|
||||
: undefined,
|
||||
ipAddress: randomIP(),
|
||||
userAgent:
|
||||
Math.random() > 0.3
|
||||
? userAgents[randomInt(0, userAgents.length - 1)]
|
||||
: undefined,
|
||||
status,
|
||||
errorMessage: !isSuccess
|
||||
? errorMessages[randomInt(0, errorMessages.length - 1)]
|
||||
: undefined,
|
||||
executionTime: randomInt(10, 5000),
|
||||
createdAt: randomDate(),
|
||||
};
|
||||
}
|
||||
|
||||
// 生成 150 条日志数据
|
||||
const logs: LogItem[] = Array.from({ length: 150 }, (_, i) =>
|
||||
generateLog(i + 1),
|
||||
);
|
||||
|
||||
// 按时间倒序排序
|
||||
logs.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
|
||||
export default {
|
||||
// 获取日志列表 GET /api/logs
|
||||
'GET /api/logs': (req: Request, res: Response) => {
|
||||
const {
|
||||
current = 1,
|
||||
pageSize = 20,
|
||||
operatorName,
|
||||
operationType,
|
||||
operationModule,
|
||||
status,
|
||||
startTime,
|
||||
endTime,
|
||||
ipAddress,
|
||||
sortField = 'createdAt',
|
||||
sortOrder = 'descend',
|
||||
} = req.query;
|
||||
|
||||
let filteredData = [...logs];
|
||||
|
||||
// 操作人姓名筛选
|
||||
if (operatorName) {
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.operatorName.includes(operatorName as string),
|
||||
);
|
||||
}
|
||||
|
||||
// 操作类型筛选
|
||||
if (operationType) {
|
||||
filteredData = filteredData.filter(
|
||||
(item) => item.operationType === operationType,
|
||||
);
|
||||
}
|
||||
|
||||
// 操作模块筛选
|
||||
if (operationModule) {
|
||||
filteredData = filteredData.filter(
|
||||
(item) => item.operationModule === operationModule,
|
||||
);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
filteredData = filteredData.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startTime) {
|
||||
const start = new Date(startTime as string).getTime();
|
||||
filteredData = filteredData.filter(
|
||||
(item) => new Date(item.createdAt).getTime() >= start,
|
||||
);
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
const end = new Date(endTime as string).getTime();
|
||||
filteredData = filteredData.filter(
|
||||
(item) => new Date(item.createdAt).getTime() <= end,
|
||||
);
|
||||
}
|
||||
|
||||
// IP 地址筛选
|
||||
if (ipAddress) {
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.ipAddress.includes(ipAddress as string),
|
||||
);
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (sortField && sortOrder) {
|
||||
filteredData.sort((a, b) => {
|
||||
const aValue = a[sortField as keyof LogItem];
|
||||
const bValue = b[sortField as keyof LogItem];
|
||||
const isAscend = sortOrder === 'ascend';
|
||||
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
return isAscend
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
}
|
||||
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
return isAscend ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 分页
|
||||
const currentNum = parseInt(current as string, 10);
|
||||
const pageSizeNum = parseInt(pageSize as string, 10);
|
||||
const startIndex = (currentNum - 1) * pageSizeNum;
|
||||
const endIndex = startIndex + pageSizeNum;
|
||||
const pagedData = filteredData.slice(startIndex, endIndex);
|
||||
|
||||
// 模拟网络延迟
|
||||
setTimeout(() => {
|
||||
res.json({
|
||||
data: pagedData,
|
||||
total: filteredData.length,
|
||||
success: true,
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 获取日志详情 GET /api/logs/:id
|
||||
'GET /api/logs/:id': (req: Request, res: Response) => {
|
||||
const log = logs.find((item) => item.id === req.params.id);
|
||||
|
||||
if (!log) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '日志记录不存在',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: log,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
|
||||
// 导出日志 POST /api/logs/export
|
||||
'POST /api/logs/export': (req: Request, res: Response) => {
|
||||
const {
|
||||
operatorName,
|
||||
operationType,
|
||||
operationModule,
|
||||
status,
|
||||
startTime,
|
||||
endTime,
|
||||
ipAddress,
|
||||
} = req.body;
|
||||
|
||||
let filteredData = [...logs];
|
||||
|
||||
// 应用相同的筛选逻辑
|
||||
if (operatorName) {
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.operatorName.includes(operatorName),
|
||||
);
|
||||
}
|
||||
|
||||
if (operationType) {
|
||||
filteredData = filteredData.filter(
|
||||
(item) => item.operationType === operationType,
|
||||
);
|
||||
}
|
||||
|
||||
if (operationModule) {
|
||||
filteredData = filteredData.filter(
|
||||
(item) => item.operationModule === operationModule,
|
||||
);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
filteredData = filteredData.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
if (startTime) {
|
||||
const start = new Date(startTime).getTime();
|
||||
filteredData = filteredData.filter(
|
||||
(item) => new Date(item.createdAt).getTime() >= start,
|
||||
);
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
const end = new Date(endTime).getTime();
|
||||
filteredData = filteredData.filter(
|
||||
(item) => new Date(item.createdAt).getTime() <= end,
|
||||
);
|
||||
}
|
||||
|
||||
if (ipAddress) {
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.ipAddress.includes(ipAddress),
|
||||
);
|
||||
}
|
||||
|
||||
// 生成 CSV 内容
|
||||
const headers = [
|
||||
'ID',
|
||||
'操作人',
|
||||
'操作类型',
|
||||
'操作模块',
|
||||
'操作描述',
|
||||
'请求方法',
|
||||
'请求URL',
|
||||
'IP地址',
|
||||
'状态',
|
||||
'执行时间(ms)',
|
||||
'操作时间',
|
||||
];
|
||||
const rows = filteredData.map((log) => [
|
||||
log.id,
|
||||
log.operatorName,
|
||||
log.operationType,
|
||||
log.operationModule,
|
||||
log.operationDesc,
|
||||
log.requestMethod,
|
||||
log.requestUrl,
|
||||
log.ipAddress,
|
||||
log.status,
|
||||
log.executionTime,
|
||||
new Date(log.createdAt).toLocaleString('zh-CN'),
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
|
||||
].join('\n');
|
||||
|
||||
// 设置响应头,触发浏览器下载
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename=logs_export_${new Date()
|
||||
.toISOString()
|
||||
.slice(0, 10)}.csv`,
|
||||
);
|
||||
|
||||
// 添加 BOM 以支持中文显示
|
||||
const bom = '\uFEFF';
|
||||
res.send(bom + csvContent);
|
||||
},
|
||||
};
|
||||
253
mock/order.mock.ts
Normal file
253
mock/order.mock.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { Order } from '@/pages/Order/data';
|
||||
|
||||
// 模拟订单数据
|
||||
const orders: Order[] = [
|
||||
{
|
||||
id: '1',
|
||||
orderNo: 'ORD202402140001',
|
||||
customerName: '张三',
|
||||
customerPhone: '13800138001',
|
||||
customerEmail: 'zhangsan@example.com',
|
||||
items: [
|
||||
{
|
||||
id: '1-1',
|
||||
orderId: '1',
|
||||
productName: 'iPhone 15 Pro Max',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 1,
|
||||
price: 9999,
|
||||
subtotal: 9999,
|
||||
},
|
||||
{
|
||||
id: '1-2',
|
||||
orderId: '1',
|
||||
productName: 'AirPods Pro',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 1,
|
||||
price: 1899,
|
||||
subtotal: 1899,
|
||||
},
|
||||
],
|
||||
totalAmount: 11898,
|
||||
status: 'completed',
|
||||
paymentMethod: 'wechat',
|
||||
paymentTime: '2024-02-14 10:30:00',
|
||||
shippingAddress: '北京市朝阳区建国路88号',
|
||||
shippingCompany: '顺丰速运',
|
||||
shippingNo: 'SF1234567890',
|
||||
shippedAt: '2024-02-14 14:00:00',
|
||||
completedAt: '2024-02-15 16:30:00',
|
||||
createdAt: '2024-02-14 10:00:00',
|
||||
updatedAt: '2024-02-15 16:30:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orderNo: 'ORD202402140002',
|
||||
customerName: '李四',
|
||||
customerPhone: '13800138002',
|
||||
items: [
|
||||
{
|
||||
id: '2-1',
|
||||
orderId: '2',
|
||||
productName: 'MacBook Pro 14',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 1,
|
||||
price: 14999,
|
||||
subtotal: 14999,
|
||||
},
|
||||
],
|
||||
totalAmount: 14999,
|
||||
status: 'shipped',
|
||||
paymentMethod: 'alipay',
|
||||
paymentTime: '2024-02-14 11:00:00',
|
||||
shippingAddress: '上海市浦东新区陆家嘴环路100号',
|
||||
shippingCompany: '京东物流',
|
||||
shippingNo: 'JD0987654321',
|
||||
shippedAt: '2024-02-14 15:30:00',
|
||||
createdAt: '2024-02-14 10:45:00',
|
||||
updatedAt: '2024-02-14 15:30:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
orderNo: 'ORD202402140003',
|
||||
customerName: '王五',
|
||||
customerPhone: '13800138003',
|
||||
customerEmail: 'wangwu@example.com',
|
||||
items: [
|
||||
{
|
||||
id: '3-1',
|
||||
orderId: '3',
|
||||
productName: 'iPad Pro 12.9',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 2,
|
||||
price: 8499,
|
||||
subtotal: 16998,
|
||||
},
|
||||
],
|
||||
totalAmount: 16998,
|
||||
status: 'paid',
|
||||
paymentMethod: 'bank',
|
||||
paymentTime: '2024-02-14 13:00:00',
|
||||
shippingAddress: '广州市天河区天河路385号',
|
||||
createdAt: '2024-02-14 12:30:00',
|
||||
updatedAt: '2024-02-14 13:00:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
orderNo: 'ORD202402140004',
|
||||
customerName: '赵六',
|
||||
customerPhone: '13800138004',
|
||||
items: [
|
||||
{
|
||||
id: '4-1',
|
||||
orderId: '4',
|
||||
productName: 'Apple Watch Ultra',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 1,
|
||||
price: 5999,
|
||||
subtotal: 5999,
|
||||
},
|
||||
],
|
||||
totalAmount: 5999,
|
||||
status: 'pending',
|
||||
paymentMethod: 'wechat',
|
||||
createdAt: '2024-02-14 14:00:00',
|
||||
updatedAt: '2024-02-14 14:00:00',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
orderNo: 'ORD202402140005',
|
||||
customerName: '孙七',
|
||||
customerPhone: '13800138005',
|
||||
customerEmail: 'sunqi@example.com',
|
||||
items: [
|
||||
{
|
||||
id: '5-1',
|
||||
orderId: '5',
|
||||
productName: 'AirPods Max',
|
||||
productImage: 'https://via.placeholder.com/80',
|
||||
quantity: 1,
|
||||
price: 4399,
|
||||
subtotal: 4399,
|
||||
},
|
||||
],
|
||||
totalAmount: 4399,
|
||||
status: 'cancelled',
|
||||
paymentMethod: 'alipay',
|
||||
cancelledAt: '2024-02-14 15:00:00',
|
||||
cancelReason: '不想买了',
|
||||
createdAt: '2024-02-14 09:00:00',
|
||||
updatedAt: '2024-02-14 15:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
// 查询订单列表
|
||||
'GET /api/orders': (req: any, res: any) => {
|
||||
const { current = 1, pageSize = 10, orderNo, customerName, status, paymentMethod } = req.query;
|
||||
|
||||
let filteredOrders = [...orders];
|
||||
|
||||
// 按订单号筛选
|
||||
if (orderNo) {
|
||||
filteredOrders = filteredOrders.filter((order) =>
|
||||
order.orderNo.includes(orderNo),
|
||||
);
|
||||
}
|
||||
|
||||
// 按客户姓名筛选
|
||||
if (customerName) {
|
||||
filteredOrders = filteredOrders.filter((order) =>
|
||||
order.customerName.includes(customerName),
|
||||
);
|
||||
}
|
||||
|
||||
// 按订单状态筛选
|
||||
if (status) {
|
||||
filteredOrders = filteredOrders.filter((order) => order.status === status);
|
||||
}
|
||||
|
||||
// 按支付方式筛选
|
||||
if (paymentMethod) {
|
||||
filteredOrders = filteredOrders.filter(
|
||||
(order) => order.paymentMethod === paymentMethod,
|
||||
);
|
||||
}
|
||||
|
||||
// 分页
|
||||
const start = (current - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const paginatedOrders = filteredOrders.slice(start, end);
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
data: paginatedOrders,
|
||||
total: filteredOrders.length,
|
||||
});
|
||||
},
|
||||
|
||||
// 查询订单详情
|
||||
'GET /api/orders/:id': (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const order = orders.find((item) => item.id === id);
|
||||
|
||||
if (order) {
|
||||
res.send({
|
||||
success: true,
|
||||
data: order,
|
||||
});
|
||||
} else {
|
||||
res.status(404).send({
|
||||
success: false,
|
||||
message: '订单不存在',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 订单发货
|
||||
'POST /api/orders/:id/ship': (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const order = orders.find((item) => item.id === id);
|
||||
|
||||
if (order) {
|
||||
order.status = 'shipped';
|
||||
order.shippingCompany = req.body.shippingCompany;
|
||||
order.shippingNo = req.body.shippingNo;
|
||||
order.shippedAt = new Date().toISOString();
|
||||
order.updatedAt = new Date().toISOString();
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
message: '发货成功',
|
||||
});
|
||||
} else {
|
||||
res.status(404).send({
|
||||
success: false,
|
||||
message: '订单不存在',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 取消订单
|
||||
'POST /api/orders/:id/cancel': (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const order = orders.find((item) => item.id === id);
|
||||
|
||||
if (order) {
|
||||
order.status = 'cancelled';
|
||||
order.cancelReason = req.body.description || req.body.reason;
|
||||
order.cancelledAt = new Date().toISOString();
|
||||
order.updatedAt = new Date().toISOString();
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
message: '订单已取消',
|
||||
});
|
||||
} else {
|
||||
res.status(404).send({
|
||||
success: false,
|
||||
message: '订单不存在',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
56
mock/product.mock.ts
Normal file
56
mock/product.mock.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
const attributes = [
|
||||
{
|
||||
id: '1',
|
||||
name: '主体颜色',
|
||||
code: 'color',
|
||||
type: 'select',
|
||||
isRequired: true,
|
||||
options: ['星空灰', '珍珠白', '午夜蓝'],
|
||||
sort: 1,
|
||||
status: 'active',
|
||||
createdAt: '2024-02-14 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '重量 (g)',
|
||||
code: 'weight',
|
||||
type: 'number',
|
||||
isRequired: false,
|
||||
sort: 2,
|
||||
status: 'active',
|
||||
createdAt: '2024-02-14 10:05:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '无线连接',
|
||||
code: 'wireless',
|
||||
type: 'boolean',
|
||||
isRequired: true,
|
||||
sort: 3,
|
||||
status: 'disabled',
|
||||
createdAt: '2024-02-14 10:10:00',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/product/attributes': (req: any, res: any) => {
|
||||
res.send({
|
||||
success: true,
|
||||
data: attributes,
|
||||
total: attributes.length,
|
||||
});
|
||||
},
|
||||
'POST /api/product/attribute': (req: any, res: any) => {
|
||||
res.send({
|
||||
success: true,
|
||||
message: '操作成功',
|
||||
});
|
||||
},
|
||||
'DELETE /api/product/attribute/:id': (req: any, res: any) => {
|
||||
res.send({
|
||||
success: true,
|
||||
message: '删除成功',
|
||||
});
|
||||
},
|
||||
};
|
||||
348
mock/product.ts
Normal file
348
mock/product.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
type ProductStatus =
|
||||
| 'draft'
|
||||
| 'pending_review'
|
||||
| 'online'
|
||||
| 'offline'
|
||||
| 'rejected';
|
||||
|
||||
type ProductItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
sku: string;
|
||||
originalPrice: number;
|
||||
salePrice: number;
|
||||
costPrice?: number;
|
||||
stock: number;
|
||||
safetyStock: number;
|
||||
status: ProductStatus;
|
||||
description?: string;
|
||||
imageUrl?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ProductBody = Partial<ProductItem>;
|
||||
|
||||
const PRODUCT_STATUS_LIST: ProductStatus[] = [
|
||||
'draft',
|
||||
'pending_review',
|
||||
'online',
|
||||
'offline',
|
||||
'rejected',
|
||||
];
|
||||
|
||||
const now = () => new Date().toISOString();
|
||||
|
||||
let productList: ProductItem[] = [
|
||||
{
|
||||
id: 'prod-1001',
|
||||
name: '智能降噪耳机 Pro',
|
||||
category: 'audio',
|
||||
sku: 'AUDIO-001',
|
||||
originalPrice: 1599,
|
||||
salePrice: 1299,
|
||||
costPrice: 760,
|
||||
stock: 45,
|
||||
safetyStock: 20,
|
||||
status: 'online',
|
||||
description: '旗舰主动降噪,支持空间音频。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T09:00:00.000Z',
|
||||
updatedAt: '2024-02-14T09:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1002',
|
||||
name: '机械键盘无线版',
|
||||
category: 'peripheral',
|
||||
sku: 'KEYB-002',
|
||||
originalPrice: 799,
|
||||
salePrice: 599,
|
||||
costPrice: 320,
|
||||
stock: 0,
|
||||
safetyStock: 10,
|
||||
status: 'offline',
|
||||
description: '三模连接,热插拔轴体。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1511467687858-23d96c32e4ae?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T10:30:00.000Z',
|
||||
updatedAt: '2024-02-14T10:30:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1003',
|
||||
name: '4K 电竞显示器',
|
||||
category: 'digital',
|
||||
sku: 'DISP-003',
|
||||
originalPrice: 3999,
|
||||
salePrice: 3599,
|
||||
costPrice: 2410,
|
||||
stock: 8,
|
||||
safetyStock: 12,
|
||||
status: 'pending_review',
|
||||
description: '144Hz 刷新率,HDR 1000。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T11:15:00.000Z',
|
||||
updatedAt: '2024-02-14T11:15:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1004',
|
||||
name: '便携投影仪 Lite',
|
||||
category: 'digital',
|
||||
sku: 'PROJ-004',
|
||||
originalPrice: 2999,
|
||||
salePrice: 2599,
|
||||
stock: 22,
|
||||
safetyStock: 8,
|
||||
status: 'draft',
|
||||
description: '1080P 分辨率,自动梯形校正。',
|
||||
createdAt: '2024-02-15T08:20:00.000Z',
|
||||
updatedAt: '2024-02-15T08:20:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1005',
|
||||
name: '智能手环 X2',
|
||||
category: 'wearable',
|
||||
sku: 'WEAR-005',
|
||||
originalPrice: 399,
|
||||
salePrice: 299,
|
||||
costPrice: 120,
|
||||
stock: 15,
|
||||
safetyStock: 15,
|
||||
status: 'rejected',
|
||||
description: '支持心率、血氧与睡眠监测。',
|
||||
createdAt: '2024-02-15T10:00:00.000Z',
|
||||
updatedAt: '2024-02-15T10:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const coreFieldChanged = (
|
||||
product: ProductItem,
|
||||
patch: ProductBody,
|
||||
): boolean => {
|
||||
return (
|
||||
(typeof patch.name === 'string' && patch.name !== product.name) ||
|
||||
(typeof patch.sku === 'string' && patch.sku !== product.sku) ||
|
||||
(typeof patch.originalPrice === 'number' &&
|
||||
patch.originalPrice !== product.originalPrice) ||
|
||||
(typeof patch.salePrice === 'number' &&
|
||||
patch.salePrice !== product.salePrice)
|
||||
);
|
||||
};
|
||||
|
||||
const isProductStatus = (value: unknown): value is ProductStatus => {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
PRODUCT_STATUS_LIST.includes(value as ProductStatus)
|
||||
);
|
||||
};
|
||||
|
||||
const isPositiveNumber = (value: unknown): value is number => {
|
||||
return typeof value === 'number' && value > 0;
|
||||
};
|
||||
|
||||
const hasCreatePriceError = (payload: ProductBody): boolean => {
|
||||
if (!isPositiveNumber(payload.originalPrice)) {
|
||||
return true;
|
||||
}
|
||||
if (!isPositiveNumber(payload.salePrice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return payload.salePrice > payload.originalPrice;
|
||||
};
|
||||
|
||||
const hasPriceError = (payload: ProductBody): boolean => {
|
||||
if (typeof payload.originalPrice === 'number' && payload.originalPrice <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof payload.salePrice === 'number' && payload.salePrice <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
typeof payload.originalPrice === 'number' &&
|
||||
typeof payload.salePrice === 'number' &&
|
||||
payload.salePrice > payload.originalPrice
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const parseString = (value: unknown): string | undefined => {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
||||
};
|
||||
|
||||
export default {
|
||||
'GET /api/products': (req: Request, res: Response) => {
|
||||
const current = Number(req.query.current) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 10;
|
||||
const name = parseString(req.query.name);
|
||||
const category = parseString(req.query.category);
|
||||
const sku = parseString(req.query.sku);
|
||||
const status = parseString(req.query.status) as ProductStatus | undefined;
|
||||
const stockWarning = parseString(req.query.stockWarning);
|
||||
|
||||
let filtered = [...productList];
|
||||
|
||||
if (name) {
|
||||
filtered = filtered.filter((item) => item.name.includes(name));
|
||||
}
|
||||
if (category) {
|
||||
filtered = filtered.filter((item) => item.category === category);
|
||||
}
|
||||
if (sku) {
|
||||
filtered = filtered.filter((item) => item.sku.includes(sku));
|
||||
}
|
||||
if (status) {
|
||||
filtered = filtered.filter((item) => item.status === status);
|
||||
}
|
||||
if (stockWarning === 'warning') {
|
||||
filtered = filtered.filter(
|
||||
(item) => item.stock > 0 && item.stock < item.safetyStock,
|
||||
);
|
||||
}
|
||||
if (stockWarning === 'empty') {
|
||||
filtered = filtered.filter((item) => item.stock === 0);
|
||||
}
|
||||
|
||||
const start = (current - 1) * pageSize;
|
||||
const data = filtered.slice(start, start + pageSize);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
total: filtered.length,
|
||||
});
|
||||
},
|
||||
|
||||
'GET /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const item = productList.find((product) => product.id === id);
|
||||
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(item);
|
||||
},
|
||||
|
||||
'POST /api/products': (req: Request, res: Response) => {
|
||||
const payload = req.body as ProductBody;
|
||||
|
||||
if (hasCreatePriceError(payload)) {
|
||||
res.status(400).json({ success: false, message: '价格校验失败' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status !== undefined && !isProductStatus(payload.status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
const created: ProductItem = {
|
||||
id: `prod-${Date.now()}`,
|
||||
name: String(payload.name || ''),
|
||||
category: String(payload.category || 'digital'),
|
||||
sku: String(payload.sku || ''),
|
||||
originalPrice: Number(payload.originalPrice || 0),
|
||||
salePrice: Number(payload.salePrice || 0),
|
||||
costPrice:
|
||||
typeof payload.costPrice === 'number'
|
||||
? Number(payload.costPrice)
|
||||
: undefined,
|
||||
stock: Number(payload.stock || 0),
|
||||
safetyStock: Number(payload.safetyStock || 0),
|
||||
status: payload.status ?? 'draft',
|
||||
description: payload.description,
|
||||
imageUrl: payload.imageUrl,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
|
||||
productList = [created, ...productList];
|
||||
res.json(created);
|
||||
},
|
||||
|
||||
'PUT /api/products/status': (req: Request, res: Response) => {
|
||||
const payload = req.body as { ids?: string[]; status?: ProductStatus };
|
||||
const ids = Array.isArray(payload.ids) ? payload.ids : [];
|
||||
const status = payload.status;
|
||||
|
||||
if (!status || !['online', 'offline'].includes(status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
productList = productList.map((item) =>
|
||||
ids.includes(item.id) ? { ...item, status, updatedAt: now() } : item,
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
},
|
||||
|
||||
'PUT /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const payload = req.body as ProductBody;
|
||||
const index = productList.findIndex((product) => product.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
const current = productList[index];
|
||||
|
||||
if (payload.status !== undefined && !isProductStatus(payload.status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.status === 'online' && coreFieldChanged(current, payload)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '已上架商品修改核心字段前需先下架',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = {
|
||||
...current,
|
||||
...payload,
|
||||
updatedAt: now(),
|
||||
} as ProductItem;
|
||||
|
||||
if (hasPriceError(merged)) {
|
||||
res.status(400).json({ success: false, message: '价格校验失败' });
|
||||
return;
|
||||
}
|
||||
|
||||
productList[index] = merged;
|
||||
res.json(productList[index]);
|
||||
},
|
||||
|
||||
'DELETE /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const target = productList.find((item) => item.id === id);
|
||||
|
||||
if (!target) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['draft', 'offline'].includes(target.status)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '仅草稿和已下架商品可删除',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
productList = productList.filter((item) => item.id !== id);
|
||||
res.json({ success: true });
|
||||
},
|
||||
};
|
||||
177
mock/project.mock.ts
Normal file
177
mock/project.mock.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const appTypeList: string[] = ['pc', 'h5', 'miniapp', 'webapp'];
|
||||
const statusList: string[] = ['developing', 'online', 'offline', 'maintenance'];
|
||||
|
||||
const genAppList = () => {
|
||||
const appData = [
|
||||
{
|
||||
name: '企业官网',
|
||||
icon: '🏢',
|
||||
type: 'pc',
|
||||
desc: '企业官方网站,展示品牌形象',
|
||||
},
|
||||
{ name: '管理后台', icon: '⚙️', type: 'pc', desc: '企业内部管理系统' },
|
||||
{ name: '移动端官网', icon: '📱', type: 'h5', desc: '移动端展示网站' },
|
||||
{ name: '微信小程序', icon: '💬', type: 'miniapp', desc: '微信生态小程序' },
|
||||
{
|
||||
name: '员工Portal',
|
||||
icon: '👥',
|
||||
type: 'webapp',
|
||||
desc: '员工工作入口平台',
|
||||
},
|
||||
{ name: '客户管理系统', icon: '🤝', type: 'pc', desc: 'CRM客户关系管理' },
|
||||
{ name: '微商城', icon: '🛒', type: 'miniapp', desc: '微信小程序商城' },
|
||||
{
|
||||
name: '数据分析平台',
|
||||
icon: '📊',
|
||||
type: 'pc',
|
||||
desc: '企业数据可视化平台',
|
||||
},
|
||||
{ name: '移动OA', icon: '📋', type: 'h5', desc: '移动办公应用' },
|
||||
{ name: '知识库', icon: '📚', type: 'webapp', desc: '企业知识管理平台' },
|
||||
{ name: '招聘系统', icon: '👔', type: 'pc', desc: '在线招聘管理平台' },
|
||||
{ name: '会议系统', icon: '🎥', type: 'webapp', desc: '视频会议管理' },
|
||||
];
|
||||
|
||||
return appData.map((item, index) => ({
|
||||
id: `app-${index + 1}`,
|
||||
appName: item.name,
|
||||
appDesc: item.desc,
|
||||
appIcon: item.icon,
|
||||
appType: item.type,
|
||||
status: statusList[index % 4],
|
||||
version: `v${Math.floor(Math.random() * 3) + 1}.${Math.floor(
|
||||
Math.random() * 10,
|
||||
)}.${Math.floor(Math.random() * 20)}`,
|
||||
owner: ['张三', '李四', '王五', '赵六', '钱七'][index % 5],
|
||||
url: `https://app${index + 1}.example.com`,
|
||||
tags: [
|
||||
['企业', '重要'],
|
||||
['内部', '核心'],
|
||||
['移动端', '新项目'],
|
||||
['微信生态'],
|
||||
][index % 4],
|
||||
createdAt: new Date(
|
||||
Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
updatedAt: new Date(
|
||||
Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
}));
|
||||
};
|
||||
|
||||
let appDataSource = genAppList();
|
||||
|
||||
// 计算统计数据
|
||||
const calculateStats = () => {
|
||||
const stats = {
|
||||
total: appDataSource.length,
|
||||
online: 0,
|
||||
developing: 0,
|
||||
offline: 0,
|
||||
maintenance: 0,
|
||||
};
|
||||
appDataSource.forEach((app: { status: string }) => {
|
||||
if (app.status === 'online') stats.online++;
|
||||
else if (app.status === 'developing') stats.developing++;
|
||||
else if (app.status === 'offline') stats.offline++;
|
||||
else if (app.status === 'maintenance') stats.maintenance++;
|
||||
});
|
||||
return stats;
|
||||
};
|
||||
|
||||
export default {
|
||||
'GET /api/projects': (req: Request, res: Response) => {
|
||||
const current = Number(req.query.current) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 12;
|
||||
const appName = req.query.appName as string | undefined;
|
||||
const owner = req.query.owner as string | undefined;
|
||||
const appType = req.query.appType as string | undefined;
|
||||
const status = req.query.status as string | undefined;
|
||||
|
||||
let filtered = [...appDataSource];
|
||||
|
||||
// 关键词搜索
|
||||
if (appName) {
|
||||
filtered = filtered.filter((p) => p.appName.includes(appName));
|
||||
}
|
||||
if (owner) {
|
||||
filtered = filtered.filter((p) => p.owner.includes(owner));
|
||||
}
|
||||
if (appType) {
|
||||
filtered = filtered.filter((p) => p.appType === appType);
|
||||
}
|
||||
if (status) {
|
||||
filtered = filtered.filter((p) => p.status === status);
|
||||
}
|
||||
|
||||
// 分页
|
||||
const start = (current - 1) * pageSize;
|
||||
const data = filtered.slice(start, start + pageSize);
|
||||
|
||||
res.json({
|
||||
data,
|
||||
total: filtered.length,
|
||||
success: true,
|
||||
current,
|
||||
pageSize,
|
||||
});
|
||||
},
|
||||
|
||||
'GET /api/projects/stats': (_req: Request, res: Response) => {
|
||||
const stats = calculateStats();
|
||||
res.json({
|
||||
stats,
|
||||
success: true,
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/projects': (req: Request, res: Response) => {
|
||||
const newApp = {
|
||||
...req.body,
|
||||
id: `app-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
appDataSource.unshift(newApp);
|
||||
res.json(newApp);
|
||||
},
|
||||
|
||||
'PUT /api/projects/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const index = appDataSource.findIndex((p) => p.id === id);
|
||||
if (index !== -1) {
|
||||
appDataSource[index] = {
|
||||
...appDataSource[index],
|
||||
...req.body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
res.json(appDataSource[index]);
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: '应用不存在' });
|
||||
}
|
||||
},
|
||||
|
||||
'PUT /api/projects/:id/status': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
const index = appDataSource.findIndex((p) => p.id === id);
|
||||
if (index !== -1) {
|
||||
appDataSource[index] = {
|
||||
...appDataSource[index],
|
||||
status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
res.json(appDataSource[index]);
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: '应用不存在' });
|
||||
}
|
||||
},
|
||||
|
||||
'DELETE /api/projects/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
appDataSource = appDataSource.filter((p) => p.id !== id);
|
||||
res.json({ success: true });
|
||||
},
|
||||
};
|
||||
60
mock/ranking.mock.ts
Normal file
60
mock/ranking.mock.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
const rankingList = [
|
||||
{
|
||||
id: '1',
|
||||
rank: 1,
|
||||
title: '深夜的序章',
|
||||
subTitle: 'Premium Edition 2024',
|
||||
cover: 'https://images.unsplash.com/photo-1485846234645-a62644f84728?q=80&w=200&auto=format&fit=crop',
|
||||
rating: 9.8,
|
||||
category: '剧情',
|
||||
trend: 'up',
|
||||
viewCount: 1250000,
|
||||
releaseDate: '2024-01-15',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
rank: 2,
|
||||
title: '霓虹之舞',
|
||||
subTitle: 'Limited Director Cut',
|
||||
cover: 'https://images.unsplash.com/photo-1478720568477-152d9b164e26?q=80&w=200&auto=format&fit=crop',
|
||||
rating: 9.5,
|
||||
category: '动作',
|
||||
trend: 'stable',
|
||||
viewCount: 890000,
|
||||
releaseDate: '2024-02-01',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
rank: 3,
|
||||
title: '极速传说',
|
||||
subTitle: '4K Ultra High Speed',
|
||||
cover: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=200&auto=format&fit=crop',
|
||||
rating: 9.2,
|
||||
category: '竞技',
|
||||
trend: 'down',
|
||||
viewCount: 750000,
|
||||
releaseDate: '2023-12-20',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
rank: 4,
|
||||
title: '蓝色海洋',
|
||||
subTitle: 'Ocean Documentary Visuals',
|
||||
cover: 'https://images.unsplash.com/photo-1439405326854-014607f694d7?q=80&w=200&auto=format&fit=crop',
|
||||
rating: 9.0,
|
||||
category: '纪录片',
|
||||
trend: 'up',
|
||||
viewCount: 620000,
|
||||
releaseDate: '2024-02-10',
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/ranking/list': (req: any, res: any) => {
|
||||
res.send({
|
||||
success: true,
|
||||
data: rankingList,
|
||||
total: rankingList.length,
|
||||
});
|
||||
},
|
||||
};
|
||||
79
mock/server.mock.ts
Normal file
79
mock/server.mock.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const genServerList = (current: number, pageSize: number) => {
|
||||
const tableListDataSource: any[] = [];
|
||||
const statusList = ['online', 'offline', 'maintenance'];
|
||||
|
||||
for (let i = 0; i < pageSize; i += 1) {
|
||||
const status = statusList[i % 3];
|
||||
tableListDataSource.push({
|
||||
key: i,
|
||||
id: `srv-${i}`,
|
||||
name: `Server-${i}`,
|
||||
ip: `192.168.1.${i}`,
|
||||
status,
|
||||
os: 'Ubuntu 22.04 LTS',
|
||||
cpu: Math.floor(Math.random() * 100),
|
||||
memory: Math.floor(Math.random() * 100),
|
||||
tags: ['web', 'production'],
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
return tableListDataSource;
|
||||
};
|
||||
|
||||
let tableListDataSource = genServerList(1, 40);
|
||||
|
||||
export default {
|
||||
'GET /api/servers': (req: Request, res: Response) => {
|
||||
const { current = 1, pageSize = 20 } = req.query as any; // Cast to any to access pagination params
|
||||
|
||||
// Simulate pagination filter
|
||||
let dataSource = [...tableListDataSource].slice(
|
||||
((current as number) - 1) * (pageSize as number),
|
||||
(current as number) * (pageSize as number),
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: dataSource,
|
||||
total: tableListDataSource.length,
|
||||
success: true,
|
||||
pageSize,
|
||||
current: parseInt(`${current}`, 10) || 1,
|
||||
});
|
||||
},
|
||||
|
||||
'POST /api/servers': (req: Request, res: Response) => {
|
||||
const newData = {
|
||||
...req.body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
id: `srv-${Math.floor(Math.random() * 1000)}`,
|
||||
key: Math.floor(Math.random() * 1000),
|
||||
};
|
||||
tableListDataSource.unshift(newData);
|
||||
res.json(newData);
|
||||
},
|
||||
|
||||
'PUT /api/servers/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const index = tableListDataSource.findIndex(item => item.id === id);
|
||||
if (index !== -1) {
|
||||
tableListDataSource[index] = { ...tableListDataSource[index], ...req.body };
|
||||
res.json(tableListDataSource[index]);
|
||||
} else {
|
||||
res.status(404).json({ success: false });
|
||||
}
|
||||
},
|
||||
|
||||
'DELETE /api/servers/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
tableListDataSource = tableListDataSource.filter(item => item.id !== id);
|
||||
res.json({ success: true });
|
||||
},
|
||||
|
||||
'POST /api/servers/:id/restart': (req: Request, res: Response) => {
|
||||
res.json({ success: true, status: 'restarting' });
|
||||
},
|
||||
};
|
||||
274
mock/skill.ts
Normal file
274
mock/skill.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { SkillItem } from '@/pages/SkillManager/data';
|
||||
|
||||
// 类型映射函数
|
||||
function getSkillType(skillName: string): SkillItem['type'] {
|
||||
const typeMap: Record<string, string[]> = {
|
||||
agent: ['agent-development', 'skill-creator', 'skill-creation-guide'],
|
||||
design: [
|
||||
'frontend-design',
|
||||
'ui-design-system',
|
||||
'ui-ux-pro-max',
|
||||
'canvas-design',
|
||||
'brand-guidelines',
|
||||
],
|
||||
document: ['docx', 'pdf', 'pptx', 'xlsx', 'doc-coauthoring'],
|
||||
testing: [
|
||||
'test',
|
||||
'playwright',
|
||||
'webapp-testing',
|
||||
'test-driven-development',
|
||||
],
|
||||
integration: [
|
||||
'mcp-builder',
|
||||
'command-creator',
|
||||
'command-development',
|
||||
'figma',
|
||||
],
|
||||
workflow: [
|
||||
'brainstorming',
|
||||
'planning-with-files',
|
||||
'subagent-driven-development',
|
||||
'executing-plans',
|
||||
'dispatching-parallel-agents',
|
||||
'finishing-a-development-branch',
|
||||
],
|
||||
utility: [
|
||||
'flags',
|
||||
'fix',
|
||||
'verify',
|
||||
'extract-errors',
|
||||
'flow',
|
||||
'systematic-debugging',
|
||||
'find-skills',
|
||||
'feature-flags',
|
||||
],
|
||||
};
|
||||
|
||||
for (const [type, names] of Object.entries(typeMap)) {
|
||||
if (names.includes(skillName)) {
|
||||
return type as SkillItem['type'];
|
||||
}
|
||||
}
|
||||
return 'development';
|
||||
}
|
||||
|
||||
// 解析 frontmatter 的辅助函数
|
||||
function parseFrontmatter(content: string): {
|
||||
name: string;
|
||||
description: string;
|
||||
} {
|
||||
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
||||
const descMatch = content.match(/^description:\s*(.+)$/m);
|
||||
return {
|
||||
name: nameMatch ? nameMatch[1].trim() : '',
|
||||
description: descMatch ? descMatch[1].trim() : '',
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟从文件系统读取 skills 数据
|
||||
function getMockSkills(): SkillItem[] {
|
||||
const skillsData = [
|
||||
{ name: 'agent-development', source: 'agents' as const },
|
||||
{ name: 'algorithmic-art', source: 'agents' as const },
|
||||
{ name: 'brainstorming', source: 'agents' as const },
|
||||
{ name: 'brand-guidelines', source: 'agents' as const },
|
||||
{ name: 'canvas-design', source: 'agents' as const },
|
||||
{ name: 'command-creator', source: 'agents' as const },
|
||||
{ name: 'command-development', source: 'agents' as const },
|
||||
{ name: 'context7', source: 'agents' as const },
|
||||
{ name: 'context7-auto-research', source: 'agents' as const },
|
||||
{ name: 'dispatching-parallel-agents', source: 'agents' as const },
|
||||
{ name: 'doc-coauthoring', source: 'agents' as const },
|
||||
{ name: 'docx', source: 'agents' as const },
|
||||
{ name: 'executing-plans', source: 'agents' as const },
|
||||
{ name: 'extract-errors', source: 'agents' as const },
|
||||
{ name: 'feature-flags', source: 'agents' as const },
|
||||
{ name: 'figma', source: 'agents' as const },
|
||||
{ name: 'figma-implement-design', source: 'agents' as const },
|
||||
{ name: 'find-skills', source: 'agents' as const },
|
||||
{ name: 'finishing-a-development-branch', source: 'agents' as const },
|
||||
{ name: 'fix', source: 'agents' as const },
|
||||
{ name: 'flags', source: 'agents' as const },
|
||||
{ name: 'flow', source: 'agents' as const },
|
||||
{ name: 'frontend-design', source: 'agents' as const },
|
||||
{ name: 'internal-comms', source: 'agents' as const },
|
||||
{ name: 'mcp-builder', source: 'agents' as const },
|
||||
{ name: 'pdf', source: 'agents' as const },
|
||||
{ name: 'playwright', source: 'agents' as const },
|
||||
{ name: 'pptx', source: 'agents' as const },
|
||||
{ name: 'product-requirements', source: 'agents' as const },
|
||||
{ name: 'prototype-prompt-generator', source: 'agents' as const },
|
||||
{ name: 'receiving-code-review', source: 'agents' as const },
|
||||
{ name: 'requesting-code-review', source: 'agents' as const },
|
||||
{ name: 'skill-creation-guide', source: 'agents' as const },
|
||||
{ name: 'skill-creator', source: 'agents' as const },
|
||||
{ name: 'slack-gif-creator', source: 'agents' as const },
|
||||
{ name: 'subagent-driven-development', source: 'agents' as const },
|
||||
{ name: 'systematic-debugging', source: 'agents' as const },
|
||||
{ name: 'template-skill', source: 'agents' as const },
|
||||
{ name: 'test', source: 'agents' as const },
|
||||
{ name: 'test-driven-development', source: 'agents' as const },
|
||||
{ name: 'theme-factory', source: 'agents' as const },
|
||||
{ name: 'ui-design-system', source: 'agents' as const },
|
||||
{ name: 'ui-ux-pro-max', source: 'agents' as const },
|
||||
{ name: 'using-git-worktrees', source: 'agents' as const },
|
||||
{ name: 'using-superpowers', source: 'agents' as const },
|
||||
{ name: 'ux-researcher-designer', source: 'agents' as const },
|
||||
{ name: 'verification-before-completion', source: 'agents' as const },
|
||||
{ name: 'verify', source: 'agents' as const },
|
||||
{ name: 'web-artifacts-builder', source: 'agents' as const },
|
||||
{ name: 'web-design-guidelines', source: 'agents' as const },
|
||||
{ name: 'webapp-testing', source: 'agents' as const },
|
||||
{ name: 'writing-plans', source: 'agents' as const },
|
||||
{ name: 'writing-skills', source: 'agents' as const },
|
||||
{ name: 'xlsx', source: 'agents' as const },
|
||||
{ name: 'nodejs-backend-patterns', source: 'opencode' as const },
|
||||
{ name: 'planning-with-files', source: 'opencode' as const },
|
||||
];
|
||||
|
||||
const descriptions: Record<string, string> = {
|
||||
'agent-development':
|
||||
'Agent 开发技能,用于创建和管理 Claude Code 插件 agent',
|
||||
'algorithmic-art': '使用 p5.js 创建算法艺术,支持生成艺术、流场和粒子系统',
|
||||
brainstorming: '头脑风暴技能,在进行创造性工作前探索用户意图和需求',
|
||||
'brand-guidelines': '应用 Anthropic 官方品牌颜色和字体设计规范',
|
||||
'canvas-design': '使用设计哲学创建漂亮的视觉艺术作品',
|
||||
'command-creator': '创建 Claude Code 斜杠命令的技能',
|
||||
'command-development': '开发斜杠命令的完整指南',
|
||||
context7: '通过 Context7 API 获取最新的库/框架文档',
|
||||
'context7-auto-research': '自动获取 Claude Code 最新文档',
|
||||
'dispatching-parallel-agents': '并行代理调度技能,处理独立任务',
|
||||
'doc-coauthoring': '协作撰写文档的工作流程指南',
|
||||
docx: 'Word 文档创建、编辑和分析工具包',
|
||||
'executing-plans': '执行实现计划的技能',
|
||||
'extract-errors': '提取和处理 React 错误消息',
|
||||
'feature-flags': '功能开关管理技能',
|
||||
figma: 'Figma MCP 服务器集成',
|
||||
'figma-implement-design': '将 Figma 设计转换为生产代码',
|
||||
'find-skills': '发现和安装 agent skills',
|
||||
'finishing-a-development-branch': '完成开发分支的集成指导',
|
||||
fix: '修复 lint 错误和格式化问题',
|
||||
flags: '检查功能开关状态',
|
||||
flow: 'Flow 类型检查技能',
|
||||
'frontend-design': '创建生产级前端界面',
|
||||
'internal-comms': '内部通信文档撰写',
|
||||
'mcp-builder': 'MCP 服务器创建指南',
|
||||
pdf: 'PDF 文档处理工具包',
|
||||
playwright: '浏览器自动化测试工具',
|
||||
pptx: '演示文稿创建和编辑工具',
|
||||
'product-requirements': '产品需求文档生成',
|
||||
'prototype-prompt-generator': 'UI/UX 原型提示生成器',
|
||||
'receiving-code-review': '接收代码审查反馈',
|
||||
'requesting-code-review': '请求代码审查',
|
||||
'skill-creation-guide': '创建 skills 的完整指南',
|
||||
'skill-creator': 'Skill 创建技能',
|
||||
'slack-gif-creator': '创建 Slack 优化 GIF',
|
||||
'subagent-driven-development': '子代理驱动开发',
|
||||
'systematic-debugging': '系统化调试方法',
|
||||
'template-skill': 'Skill 模板',
|
||||
test: 'React 测试运行工具',
|
||||
'test-driven-development': '测试驱动开发',
|
||||
'theme-factory': '主题样式工具包',
|
||||
'ui-design-system': 'UI 设计系统工具包',
|
||||
'ui-ux-pro-max': 'UI/UX 设计智能工具',
|
||||
'using-git-worktrees': 'Git worktree 隔离开发',
|
||||
'using-superpowers': '使用技能系统',
|
||||
'ux-researcher-designer': 'UX 研究和设计工具包',
|
||||
'verification-before-completion': '完成前验证',
|
||||
verify: '提交前验证检查',
|
||||
'web-artifacts-builder': 'Web HTML artifacts 构建套件',
|
||||
'web-design-guidelines': 'Web 界面指南合规性审查',
|
||||
'webapp-testing': 'Web 应用测试工具包',
|
||||
'writing-plans': '编写计划文档',
|
||||
'writing-skills': '技能编写和验证',
|
||||
xlsx: '电子表格创建和分析工具包',
|
||||
'nodejs-backend-patterns': 'Node.js 后端服务开发模式',
|
||||
'planning-with-files': '基于文件的复杂任务规划',
|
||||
};
|
||||
|
||||
return skillsData.map((item, index) => ({
|
||||
id: `skill_${index + 1}`,
|
||||
name: item.name,
|
||||
description: descriptions[item.name] || `${item.name} skill`,
|
||||
path:
|
||||
item.source === 'agents'
|
||||
? `~/.agents/skills/${item.name}`
|
||||
: `~/.config/opencode/skills/${item.name}`,
|
||||
source: item.source,
|
||||
type: getSkillType(item.name),
|
||||
isEnabled: true,
|
||||
fileCount: Math.floor(Math.random() * 10) + 1,
|
||||
lastModified:
|
||||
Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000),
|
||||
}));
|
||||
}
|
||||
|
||||
const mockSkills = getMockSkills();
|
||||
|
||||
export default {
|
||||
'GET /api/skills': (req: any) => {
|
||||
const { type, keyword, current = 1, pageSize = 10 } = req.query;
|
||||
|
||||
let filteredSkills = [...mockSkills];
|
||||
|
||||
// 按类型筛选
|
||||
if (type && type !== 'all') {
|
||||
filteredSkills = filteredSkills.filter((skill) => skill.type === type);
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (keyword) {
|
||||
const kw = keyword.toLowerCase();
|
||||
filteredSkills = filteredSkills.filter(
|
||||
(skill) =>
|
||||
skill.name.toLowerCase().includes(kw) ||
|
||||
skill.description.toLowerCase().includes(kw),
|
||||
);
|
||||
}
|
||||
|
||||
const total = filteredSkills.length;
|
||||
const start = (current - 1) * pageSize;
|
||||
const data = filteredSkills.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
|
||||
'GET /api/skills/:id': (req: any) => {
|
||||
const { id } = req.params;
|
||||
const skill = mockSkills.find((s) => s.id === id);
|
||||
|
||||
if (!skill) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'Skill not found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
...skill,
|
||||
tags: [skill.type, skill.source],
|
||||
content: `# ${skill.name}\n\n${skill.description}`,
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
|
||||
'POST /api/skills/:id/status': (req: any) => {
|
||||
const { id } = req.params;
|
||||
const { isEnabled } = req.body;
|
||||
|
||||
const skill = mockSkills.find((s) => s.id === id);
|
||||
if (skill) {
|
||||
skill.isEnabled = isEnabled;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
115
mock/user.ts
Normal file
115
mock/user.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const genUserList = (current: number, pageSize: number) => {
|
||||
const tableListDataSource: any[] = [];
|
||||
|
||||
for (let i = 0; i < pageSize; i += 1) {
|
||||
const index = (current - 1) * 10 + i;
|
||||
tableListDataSource.push({
|
||||
id: `${index}`,
|
||||
username: `user_${index}`,
|
||||
realName: `用户 ${index}`,
|
||||
mobile: `138001380${index.toString().padStart(2, '0')}`,
|
||||
email: `user_${index}@antgravity.com`,
|
||||
role: index % 2 === 0 ? 'admin' : 'user',
|
||||
status: ['active', 'disabled', 'pending'][index % 3],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
return tableListDataSource;
|
||||
};
|
||||
|
||||
let tableListDataSource = genUserList(1, 20);
|
||||
|
||||
function getUserList(req: Request, res: Response, u: string) {
|
||||
let realUrl = u;
|
||||
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
|
||||
realUrl = req.url;
|
||||
}
|
||||
const { current = 1, pageSize = 10, username, realName, role, status } = req.query;
|
||||
|
||||
let dataSource = [...tableListDataSource];
|
||||
|
||||
if (username) {
|
||||
dataSource = dataSource.filter((item) => item.username.includes(username as string));
|
||||
}
|
||||
if (realName) {
|
||||
dataSource = dataSource.filter((item) => item.realName.includes(realName as string));
|
||||
}
|
||||
if (role) {
|
||||
dataSource = dataSource.filter((item) => item.role === role);
|
||||
}
|
||||
if (status) {
|
||||
dataSource = dataSource.filter((item) => item.status === status);
|
||||
}
|
||||
|
||||
const result = {
|
||||
data: dataSource,
|
||||
total: dataSource.length,
|
||||
success: true,
|
||||
pageSize,
|
||||
current: parseInt(`${current}`, 10) || 1,
|
||||
};
|
||||
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
function postUser(req: Request, res: Response, u: string, b: Request) {
|
||||
const body = (b && b.body) || req.body;
|
||||
const { method, id } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
const i = Math.ceil(Math.random() * 10000);
|
||||
const newUser = {
|
||||
id: `${i}`,
|
||||
...body,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
tableListDataSource.unshift(newUser);
|
||||
return res.json(newUser);
|
||||
|
||||
case 'PUT':
|
||||
// The id in URL is usually available as req.params.id for Express routes,
|
||||
// but Umi mock matches implementation might vary.
|
||||
// Assuming RESTful style: /api/users/:id
|
||||
// We need to parse ID from URL if not provided directly.
|
||||
// Simplification for mock: assume ID is passed or parsed.
|
||||
let updateId = id;
|
||||
if (!updateId) {
|
||||
// rough parsing for /api/users/123
|
||||
const parts = req.url.split('/');
|
||||
updateId = parts[parts.length - 1];
|
||||
}
|
||||
|
||||
tableListDataSource = tableListDataSource.map((item) => {
|
||||
if (item.id === updateId) {
|
||||
return { ...item, ...body, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return res.json({ id: updateId, ...body });
|
||||
|
||||
case 'DELETE':
|
||||
let deleteId = id;
|
||||
if (!deleteId) {
|
||||
const parts = req.url.split('/');
|
||||
deleteId = parts[parts.length - 1];
|
||||
}
|
||||
tableListDataSource = tableListDataSource.filter((item) => item.id !== deleteId);
|
||||
return res.json({ success: true });
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return res.json({ result: 'Error' });
|
||||
}
|
||||
|
||||
export default {
|
||||
'GET /api/users': getUserList,
|
||||
'POST /api/users': postUser,
|
||||
'PUT /api/users/:id': postUser,
|
||||
'DELETE /api/users/:id': postUser,
|
||||
};
|
||||
20
mock/userAPI.ts
Normal file
20
mock/userAPI.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
const users = [
|
||||
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
|
||||
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/v1/queryUserList': (req: any, res: any) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: { list: users },
|
||||
errorCode: 0,
|
||||
});
|
||||
},
|
||||
'PUT /api/v1/user/': (req: any, res: any) => {
|
||||
res.json({
|
||||
success: true,
|
||||
errorCode: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
74
mock/workflow.ts
Normal file
74
mock/workflow.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { WorkflowTask } from '../src/pages/WorkflowOrchestrator/data';
|
||||
|
||||
// 模拟内存数据库
|
||||
let workflowData: WorkflowTask[] = [
|
||||
{
|
||||
id: 'wf-001',
|
||||
name: '自动化 UI 部署任务',
|
||||
status: 'executing',
|
||||
progress: 30,
|
||||
currentStepId: 'step-2',
|
||||
createTime: '2024-03-20 10:00:00',
|
||||
steps: [
|
||||
{ id: 'step-1', agentName: 'Planning Agent', status: 'success', startTime: '10:00', logs: '已完成架构设计与需求拆解。' },
|
||||
{ id: 'step-2', agentName: 'Frontend Agent', status: 'executing', startTime: '10:05', logs: '正在编写 ProTable 组件代码,应用 Design Tokens...' },
|
||||
{ id: 'step-3', agentName: 'QA Agent', status: 'thinking', startTime: '10:10', logs: '待执行回归测试。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wf-002',
|
||||
name: '后端 API 重构',
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
currentStepId: 'step-2',
|
||||
createTime: '2024-03-20 09:30:00',
|
||||
steps: [
|
||||
{ id: 'step-1', agentName: 'Architect', status: 'success', startTime: '09:30', logs: 'API 路径规范化已通过。' },
|
||||
{ id: 'step-2', agentName: 'Backend Dev', status: 'success', startTime: '09:45', logs: '完成 Swagger 契约自动生成。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wf-003',
|
||||
name: '性能压力测试',
|
||||
status: 'failed',
|
||||
progress: 65,
|
||||
currentStepId: 'step-2',
|
||||
createTime: '2024-03-20 11:00:00',
|
||||
steps: [
|
||||
{ id: 'step-1', agentName: 'DevOps Agent', status: 'success', startTime: '11:00', logs: '环境已就绪。' },
|
||||
{ id: 'step-2', agentName: 'QA Agent', status: 'failed', startTime: '11:15', logs: '内存溢出错误:JVM heap size exceeded.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
'GET /api/workflow/list': (req: Request, res: Response) => {
|
||||
// 动态更新逻辑:每次列表请求都模拟进度步进
|
||||
workflowData = workflowData.map((task) => {
|
||||
if (task.status === 'executing' && task.progress < 100) {
|
||||
const nextProgress = Math.min(task.progress + 5, 100);
|
||||
return {
|
||||
...task,
|
||||
progress: nextProgress,
|
||||
status: nextProgress === 100 ? 'success' : 'executing',
|
||||
};
|
||||
}
|
||||
return task;
|
||||
});
|
||||
res.send({ data: workflowData, total: workflowData.length });
|
||||
},
|
||||
|
||||
'POST /api/workflow/:id/control': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { action } = req.body;
|
||||
workflowData = workflowData.map((task) => {
|
||||
if (task.id === id) {
|
||||
if (action === 'retry') return { ...task, status: 'executing', progress: 0 };
|
||||
if (action === 'stop') return { ...task, status: 'failed' };
|
||||
}
|
||||
return task;
|
||||
});
|
||||
res.send({ success: true });
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user