init: 2026Technology-Competition initial commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [['html', { outputFolder: 'playwright-report' }]],
|
||||
timeout: 60000,
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'off',
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { ADMIN } from '../utils/auth'
|
||||
|
||||
test.describe('登录', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.evaluate(() => localStorage.clear())
|
||||
await page.reload()
|
||||
})
|
||||
|
||||
test('成功登录:跳转工作台并存储 token', async ({ page }) => {
|
||||
await page.getByPlaceholder('账号').fill(ADMIN.username)
|
||||
await page.getByPlaceholder('密码').fill(ADMIN.password)
|
||||
await page.getByRole('button', { name: /登\s*录/i }).click()
|
||||
await expect(page).toHaveURL(/\/dashboard/)
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'))
|
||||
expect(token).toBeTruthy()
|
||||
})
|
||||
|
||||
test('失败登录:错误密码提示', async ({ page }) => {
|
||||
await page.getByPlaceholder('账号').fill(ADMIN.username)
|
||||
await page.getByPlaceholder('密码').fill('wrong-password')
|
||||
await page.getByRole('button', { name: /登\s*录/i }).click()
|
||||
await expect(page.getByText('账号或密码错误')).toBeVisible()
|
||||
})
|
||||
|
||||
test('未登录访问受保护路由:重定向到登录页', async ({ page }) => {
|
||||
await page.goto('/issues')
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
import { createIssueViaApi, deleteIssueViaApi, changeStatusViaApi } from '../utils/api-helpers'
|
||||
|
||||
test.describe('指摘 CRUD', () => {
|
||||
let apiToken = ''
|
||||
let issueId = 0
|
||||
let issueNo = ''
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
const created = await createIssueViaApi(request)
|
||||
issueId = created.issue.id
|
||||
issueNo = created.issue.issueNo
|
||||
apiToken = created.token
|
||||
await uiLogin(page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (issueId) {
|
||||
await deleteIssueViaApi(request, issueId, apiToken).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
test('列表页显示新建的指摘', async ({ page }) => {
|
||||
await page.goto('/issues')
|
||||
await expect(page.getByText('指摘列表')).toBeVisible()
|
||||
await expect(page.getByText(issueNo)).toBeVisible()
|
||||
})
|
||||
|
||||
test('详情页显示完整信息与状态流转历史', async ({ page }) => {
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
await expect(page.getByText(issueNo)).toBeVisible()
|
||||
await expect(page.getByText('状态流转历史')).toBeVisible()
|
||||
await expect(page.getByText('创建指摘').first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('合法状态流转 draft -> open', async ({ page }) => {
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
const dialog = page.getByRole('dialog')
|
||||
await page.getByRole('button', { name: /更改状态/i }).click()
|
||||
await expect(dialog).toBeVisible()
|
||||
await dialog.locator('.ant-select-input').click()
|
||||
const opt = page.locator('.ant-select-item-option[title="待处理"]')
|
||||
await expect(opt).toBeVisible()
|
||||
await opt.click()
|
||||
await expect(dialog.locator('.ant-select-content')).toContainText('待处理')
|
||||
await dialog.getByRole('button', { name: /确认变更/i }).click()
|
||||
await expect(page.getByText('状态已更新')).toBeVisible()
|
||||
})
|
||||
|
||||
test('非法状态流转返回 400 错误提示', async ({ request }) => {
|
||||
const ok = await changeStatusViaApi(request, issueId, apiToken, 'open')
|
||||
expect(ok.status()).toBe(200)
|
||||
const illegal = await changeStatusViaApi(request, issueId, apiToken, 'closed')
|
||||
expect(illegal.status()).toBe(400)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
import { createIssueViaApi, deleteIssueViaApi } from '../utils/api-helpers'
|
||||
|
||||
test.describe('附件功能', () => {
|
||||
let apiToken = ''
|
||||
let issueId = 0
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
const created = await createIssueViaApi(request)
|
||||
issueId = created.issue.id
|
||||
apiToken = created.token
|
||||
await uiLogin(page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (issueId) {
|
||||
await deleteIssueViaApi(request, issueId, apiToken).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
test('上传附件后列表显示文件信息', async ({ page }) => {
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
await expect(page.getByText('附件 (0)')).toBeVisible()
|
||||
|
||||
await page.setInputFiles('input[type=file]', {
|
||||
name: 'e2e-test.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('IMS E2E attachment content'),
|
||||
})
|
||||
await expect(page.getByText('上传成功')).toBeVisible()
|
||||
await expect(page.getByText(/e2e-test\.txt/)).toBeVisible()
|
||||
await expect(page.getByText(/附件 \(1\)/)).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
import { createIssueViaApi, deleteIssueViaApi } from '../utils/api-helpers'
|
||||
|
||||
test.describe('Agent 驾驶舱', () => {
|
||||
let apiToken = ''
|
||||
let issueId = 0
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
const created = await createIssueViaApi(request)
|
||||
issueId = created.issue.id
|
||||
apiToken = created.token
|
||||
await uiLogin(page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (issueId) {
|
||||
await deleteIssueViaApi(request, issueId, apiToken).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
test('执行指令后显示工具卡片', async ({ page }) => {
|
||||
test.setTimeout(560000)
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
await expect(page.getByText('IMS Agent 驾驶舱')).toBeVisible()
|
||||
|
||||
await page.getByPlaceholder(/给 Agent 下达指令/).fill('查找知识库相似案例并生成对应方案')
|
||||
await page.getByRole('button', { name: /执行指令/i }).click()
|
||||
|
||||
// 模型输出工具调用后 SSE 渲染工具卡片(真实模型可能只输出 search_knowledge,不强依赖具体工具)
|
||||
await expect(page.getByText(/call:/).first()).toBeVisible({ timeout: 480000 })
|
||||
})
|
||||
|
||||
test('写工具触发审批后可批准', async ({ page }) => {
|
||||
test.setTimeout(480000)
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
await page.getByPlaceholder(/给 Agent 下达指令/).fill('请调用 update_issue 工具,把本指摘的优先级改为 high')
|
||||
await page.getByRole('button', { name: /执行指令/i }).click()
|
||||
|
||||
await expect(page.getByText('需要您的审批')).toBeVisible({ timeout: 420000 })
|
||||
await page.getByRole('button', { name: /批准执行/i }).click()
|
||||
await expect(page.getByText('已批准执行')).toBeVisible({ timeout: 30000 })
|
||||
await expect(page.getByText(/Agent 指令已批准/)).toBeVisible({ timeout: 30000 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin, apiLogin } from '../utils/auth'
|
||||
import { getDashboardViaApi } from '../utils/api-helpers'
|
||||
|
||||
test.describe('工作台 Dashboard', () => {
|
||||
let apiToken = ''
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
apiToken = (await apiLogin(request)).accessToken
|
||||
})
|
||||
|
||||
test('登录后工作台显示统计与图表', async ({ page }) => {
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByText('工作台概览')).toBeVisible()
|
||||
|
||||
await expect(page.getByText('Agent 快捷指令')).toBeVisible()
|
||||
await expect(page.getByText('指摘处理趋势')).toBeVisible()
|
||||
await expect(page.getByText('Agent 洞察')).toBeVisible()
|
||||
await expect(page.getByText('最新动态')).toBeVisible()
|
||||
await expect(page.getByText('指摘状态分布')).toBeVisible()
|
||||
})
|
||||
|
||||
test('统计卡显示正确的数量(API 对比)', async ({ page, request }) => {
|
||||
const stats = await getDashboardViaApi(request, apiToken)
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
|
||||
const pending = page.locator('.ant-statistic', { hasText: '待处理指摘' }).first()
|
||||
await expect(pending.getByText('待处理指摘', { exact: true })).toBeVisible()
|
||||
await expect(pending.getByText(String(stats.pendingCount))).toBeVisible()
|
||||
|
||||
await expect(page.locator('.ant-statistic', { hasText: '进行中指摘' }).first()).toBeVisible()
|
||||
await expect(page.locator('.ant-statistic', { hasText: '本月已完成' }).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Agent 快捷指令输入框存在且可输入', async ({ page }) => {
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
const input = page.getByPlaceholder(/例如:查找知识库/)
|
||||
await input.fill('测试指令')
|
||||
await expect(input).toHaveValue('测试指令')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
|
||||
test.describe('通知铃铛', () => {
|
||||
test('铃铛显示未读数且下拉展开', async ({ page }) => {
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
|
||||
const bell = page.locator('.ant-badge')
|
||||
await expect(bell.first()).toBeVisible()
|
||||
await bell.first().click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const menu = page.locator('.ant-dropdown')
|
||||
if (await menu.count() > 0) {
|
||||
await expect(menu.first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('通知下拉包含通知标题或空提示', async ({ page }) => {
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
await page.locator('.ant-badge').first().click()
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
const hasTitle = await page.getByText('通知').count()
|
||||
const hasEmpty = await page.getByText('暂无通知').count()
|
||||
expect(hasTitle + hasEmpty).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
|
||||
test.describe('新建页 Agent 智能填充', () => {
|
||||
test('点击「让 Agent 智能填充」真实调用 AI 并回填字段', async ({ page }) => {
|
||||
test.setTimeout(420000)
|
||||
await uiLogin(page)
|
||||
await page.goto('/issues/new')
|
||||
|
||||
await page.getByPlaceholder('请输入指摘标题').fill('登录页面在低分辨率下按钮错位、样式异常,影响操作')
|
||||
|
||||
const fillBtn = page.getByRole('button', { name: /让 Agent 智能填充/i })
|
||||
await fillBtn.click()
|
||||
await expect(fillBtn).toHaveClass(/ant-btn-loading/, { timeout: 15000 })
|
||||
// 模型调用耗时波动大(数十秒到数分钟),axios 侧 180s 超时后走降级回填,故等待上限放宽到 240s
|
||||
await expect(fillBtn).not.toHaveClass(/ant-btn-loading/, { timeout: 240000 })
|
||||
|
||||
// 工程阶段/区分/影响度三个 Select 至少被回填 3 个非"请选择"值(AI 成功或降级都会回填)
|
||||
await expect.poll(async () => {
|
||||
return page.locator('.ant-select').evaluateAll(els =>
|
||||
els.filter(e => {
|
||||
const t = e.textContent?.trim() ?? ''
|
||||
return t !== '' && t !== '请选择'
|
||||
}).length)
|
||||
}, { timeout: 30000, intervals: [2000] }).toBeGreaterThanOrEqual(3)
|
||||
|
||||
// 优先级 Radio 也被回填(高/中/低 任一)
|
||||
const checked = (await page.locator('.ant-radio-button-wrapper-checked').allTextContents()).map(s => s.trim()).filter(Boolean)
|
||||
expect(checked.length).toBeGreaterThan(0)
|
||||
console.log('filled selects:', JSON.stringify(checked))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
import { createIssueViaApi, deleteIssueViaApi } from '../utils/api-helpers'
|
||||
|
||||
test.describe('Agent SSE 流式渲染', () => {
|
||||
let apiToken = ''
|
||||
let issueId = 0
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
const created = await createIssueViaApi(request)
|
||||
issueId = created.issue.id
|
||||
apiToken = created.token
|
||||
await uiLogin(page)
|
||||
})
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
if (issueId) {
|
||||
await deleteIssueViaApi(request, issueId, apiToken).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
test('执行指令后 SSE 事件实时渲染 thought / prompt_info / model_info', async ({ page }) => {
|
||||
test.setTimeout(120000)
|
||||
await page.goto(`/issues/${issueId}`)
|
||||
await expect(page.getByText('IMS Agent 驾驶舱')).toBeVisible()
|
||||
|
||||
await page.getByPlaceholder(/给 Agent 下达指令/).fill('查找知识库相似案例并生成对应方案')
|
||||
await page.getByRole('button', { name: /执行指令/i }).click()
|
||||
|
||||
// thought:初始连接消息经 SSE 渲染为绿色状态框
|
||||
await expect(page.getByText(/已连接执行流/)).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// model_info:引擎来源 Tag
|
||||
await expect(page.getByText(/引擎:/)).toBeVisible({ timeout: 20000 })
|
||||
|
||||
// prompt_info:展开模板信息,模板标识非"未提供"
|
||||
await page.getByText('Prompt 模板信息').click()
|
||||
await expect(page.getByText(/系统角色:\s*\S+/)).toBeVisible()
|
||||
await expect(page.getByText(/规划模板:\s*\S+/)).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { uiLogin } from '../utils/auth'
|
||||
|
||||
test.describe('工作台 Agent 快捷指令执行面板', () => {
|
||||
test('执行「生成本周报告」打开执行流面板并渲染 SSE 事件', async ({ page }) => {
|
||||
await uiLogin(page)
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByText('Agent 快捷指令')).toBeVisible()
|
||||
|
||||
const input = page.getByPlaceholder(/例如:查找知识库/)
|
||||
await input.fill('生成本周指摘处理统计报告')
|
||||
await page.getByRole('button', { name: /执\s*行/ }).click()
|
||||
|
||||
await expect(page.getByText(/Agent 执行流/)).toBeVisible({ timeout: 30000 })
|
||||
await expect(page.getByText(/指令已提交,正在连接执行流/)).toBeVisible({ timeout: 30000 })
|
||||
|
||||
await expect(page.getByText(/已连接执行流/)).toBeVisible({ timeout: 60000 })
|
||||
|
||||
const execBox = page.getByText(/Agent 执行流/)
|
||||
await expect(execBox).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { APIRequestContext } from '@playwright/test'
|
||||
import { API_BASE, apiLogin, authHeaders } from './auth'
|
||||
|
||||
export interface CreatedIssue {
|
||||
id: number
|
||||
issueNo: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export async function createIssueViaApi(
|
||||
request: APIRequestContext,
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Promise<{ issue: CreatedIssue; token: string }> {
|
||||
const token = (await apiLogin(request)).accessToken
|
||||
const payload = {
|
||||
title: `E2E-${Date.now()}-自动化测试`,
|
||||
description: 'Playwright 自动化测试创建的指摘',
|
||||
phase: '编码',
|
||||
subProject: '后端开发',
|
||||
category: '功能缺陷',
|
||||
impactLevel: '中',
|
||||
assigneeId: 2,
|
||||
reviewerId: 1,
|
||||
priority: 'medium',
|
||||
status: 'draft',
|
||||
departmentId: 2,
|
||||
...overrides,
|
||||
}
|
||||
const res = await request.post(`${API_BASE}/issues`, {
|
||||
data: payload,
|
||||
headers: authHeaders(token),
|
||||
})
|
||||
if (!res.ok()) {
|
||||
throw new Error(`create issue failed: ${res.status()} ${await res.text()}`)
|
||||
}
|
||||
const json = await res.json()
|
||||
return { issue: json.data, token }
|
||||
}
|
||||
|
||||
export async function deleteIssueViaApi(request: APIRequestContext, issueId: number, token: string) {
|
||||
const res = await request.delete(`${API_BASE}/issues/${issueId}`, { headers: authHeaders(token) })
|
||||
if (!res.ok()) {
|
||||
throw new Error(`delete issue failed: ${res.status()} ${await res.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getIssueViaApi(request: APIRequestContext, issueId: number, token: string) {
|
||||
const res = await request.get(`${API_BASE}/issues/${issueId}`, { headers: authHeaders(token) })
|
||||
return res.ok() ? (await res.json()).data : null
|
||||
}
|
||||
|
||||
export async function changeStatusViaApi(
|
||||
request: APIRequestContext,
|
||||
issueId: number,
|
||||
token: string,
|
||||
status: string,
|
||||
remark = 'E2E 状态变更',
|
||||
) {
|
||||
const res = await request.patch(`${API_BASE}/issues/${issueId}/status`, {
|
||||
data: { status, remark },
|
||||
headers: authHeaders(token),
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export async function getDashboardViaApi(request: APIRequestContext, token: string) {
|
||||
const res = await request.get(`${API_BASE}/dashboard/stats`, { headers: authHeaders(token) })
|
||||
return res.ok() ? (await res.json()).data : null
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { APIRequestContext } from '@playwright/test'
|
||||
|
||||
export const ADMIN = { username: 'admin', password: 'Admin@2026' }
|
||||
export const API_BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
export interface LoginData {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
username?: string
|
||||
roleName?: string
|
||||
userId?: number
|
||||
}
|
||||
|
||||
export async function apiLogin(request: APIRequestContext, user = ADMIN): Promise<LoginData> {
|
||||
const res = await request.post(`${API_BASE}/auth/login`, {
|
||||
data: { username: user.username, password: user.password },
|
||||
})
|
||||
if (!res.ok()) {
|
||||
throw new Error(`login failed: ${res.status()} ${await res.text()}`)
|
||||
}
|
||||
const json = await res.json()
|
||||
return json.data
|
||||
}
|
||||
|
||||
export async function uiLogin(page: import('@playwright/test').Page, user = ADMIN) {
|
||||
await page.goto('/login')
|
||||
await page.getByPlaceholder(/用户名|账号|username/i).fill(user.username)
|
||||
await page.getByPlaceholder(/密码|password/i).fill(user.password)
|
||||
await page.getByRole('button', { name: /登\s*录/i }).click()
|
||||
await page.waitForURL(/\/dashboard/)
|
||||
}
|
||||
|
||||
export function authHeaders(token: string) {
|
||||
return { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>指摘管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4573
@@ -0,0 +1,4573 @@
|
||||
{
|
||||
"name": "ims-frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ims-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.7",
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"antd": "^6.0.0",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"echarts": "^6.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-redux": "^9.0.0",
|
||||
"react-router-dom": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/echarts": "^4.9.22",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/charts": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/charts/-/charts-2.6.7.tgz",
|
||||
"integrity": "sha512-XfmsnspUpfrMlRFGTwmHJ2TPKcosq5a5nSxAfIOpEXAvmJBT2N16oejGTZhUFTzba8W3XtBOziwRAXmDmLUqvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/graphs": "^2.1.1",
|
||||
"@ant-design/plots": "^2.6.7",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/charts-util": {
|
||||
"version": "0.0.1-alpha.7",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.1-alpha.7.tgz",
|
||||
"integrity": "sha512-Yh0o6EdO6SvdSnStFZMbnUzjyymkVzV+TQ9ymVW9hlVgO/fUkUII3JYSdV+UVcFnYwUF0YiDKuSTLCZNAzg2bQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/colors": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz",
|
||||
"integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/fast-color": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/cssinjs": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz",
|
||||
"integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.1",
|
||||
"@emotion/hash": "^0.8.0",
|
||||
"@emotion/unitless": "^0.7.5",
|
||||
"@rc-component/util": "^1.4.0",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"stylis": "^4.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/cssinjs-utils": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz",
|
||||
"integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^2.1.2",
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@rc-component/util": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/fast-color": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz",
|
||||
"integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/graphs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/graphs/-/graphs-2.1.1.tgz",
|
||||
"integrity": "sha512-qT3Oo8BWeoAmZEy9gfR6uIk+rczbNJ3sWXKonoOD5koATWv7dY0kgvS1JnhdM1QW4FkfPPJTeQVSlRRUtvWDwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/charts-util": "0.0.1-alpha.7",
|
||||
"@antv/g6": "^5.0.44",
|
||||
"@antv/g6-extension-react": "^0.2.0",
|
||||
"@antv/graphin": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"styled-components": "^6.1.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz",
|
||||
"integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^8.0.1",
|
||||
"@ant-design/icons-svg": "^4.5.0",
|
||||
"@rc-component/util": "^1.11.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons-svg": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
|
||||
"integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/plots": {
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/plots/-/plots-2.6.8.tgz",
|
||||
"integrity": "sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/charts-util": "0.0.3",
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/g": "^6.1.7",
|
||||
"@antv/g2": "^5.2.7",
|
||||
"@antv/g2-extension-plot": "^0.2.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/plots/node_modules/@ant-design/charts-util": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/charts-util/-/charts-util-0.0.3.tgz",
|
||||
"integrity": "sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/react-slick": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz",
|
||||
"integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"clsx": "^2.1.1",
|
||||
"json2mq": "^0.2.0",
|
||||
"throttle-debounce": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/algorithm": {
|
||||
"version": "0.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz",
|
||||
"integrity": "sha512-DVhcFSQ8YQnMNW34Mk8BSsfc61iC1sAnmcfYoXTAshYHuU50p/6b7x3QYaGctDNKWGvi1ub7mPcSY0bK+aN0qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^2.0.13",
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/algorithm/node_modules/@antv/util": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz",
|
||||
"integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.8",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/component": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.11.tgz",
|
||||
"integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g": "^6.1.11",
|
||||
"@antv/scale": "^0.4.16",
|
||||
"@antv/util": "^3.3.10",
|
||||
"svg-path-parser": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/component/node_modules/@antv/scale": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz",
|
||||
"integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord": {
|
||||
"version": "0.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@antv/coord/-/coord-0.4.7.tgz",
|
||||
"integrity": "sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/scale": "^0.4.12",
|
||||
"@antv/util": "^2.0.13",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/scale": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.4.16.tgz",
|
||||
"integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/scale/node_modules/@antv/util": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz",
|
||||
"integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/util": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@antv/util/-/util-2.0.17.tgz",
|
||||
"integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.8",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/event-emitter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@antv/event-emitter/-/event-emitter-0.1.3.tgz",
|
||||
"integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antv/expr": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@antv/expr/-/expr-1.0.2.tgz",
|
||||
"integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antv/g": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g/-/g-6.3.1.tgz",
|
||||
"integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"html2canvas": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-canvas": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g-canvas/-/g-canvas-2.2.0.tgz",
|
||||
"integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/g-math": "3.1.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-lite": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g-lite/-/g-lite-2.7.0.tgz",
|
||||
"integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-math": "3.1.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@antv/vendor": "^1.0.3",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-math": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g-math/-/g-math-3.1.0.tgz",
|
||||
"integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-plugin-dragndrop": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz",
|
||||
"integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-svg": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g-svg/-/g-svg-2.1.1.tgz",
|
||||
"integrity": "sha512-gVzBkjqA8FzDTbkuIxj6L0Omz/X/hFbYLzK6alWr0sHTfywqP6czcjDUJU8DF2MRIY1Twy55uZYW4dqqLXOXXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g2": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g2/-/g2-5.4.8.tgz",
|
||||
"integrity": "sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/component": "^2.1.9",
|
||||
"@antv/coord": "^0.4.7",
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/expr": "^1.0.2",
|
||||
"@antv/g": "^6.1.24",
|
||||
"@antv/g-canvas": "^2.0.43",
|
||||
"@antv/g-plugin-dragndrop": "^2.0.35",
|
||||
"@antv/scale": "^0.5.1",
|
||||
"@antv/util": "^3.3.10",
|
||||
"@antv/vendor": "^1.0.11",
|
||||
"flru": "^1.0.2",
|
||||
"pdfast": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g2-extension-plot": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g2-extension-plot/-/g2-extension-plot-0.2.2.tgz",
|
||||
"integrity": "sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==",
|
||||
"dependencies": {
|
||||
"@antv/g2": "^5.1.8",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@antv/vendor": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g6": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g6/-/g6-5.1.1.tgz",
|
||||
"integrity": "sha512-50bXxMUf4mChyOv4ePVeWZLwotih9VunKfp0a++Wofv/wCyY8fb9+CV2wouIBCOZnd5ydBRA4NNaX9yLJzqa2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/algorithm": "^0.1.26",
|
||||
"@antv/component": "^2.1.7",
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/g": "^6.1.28",
|
||||
"@antv/g-canvas": "^2.0.48",
|
||||
"@antv/g-plugin-dragndrop": "^2.0.38",
|
||||
"@antv/graphlib": "^2.0.4",
|
||||
"@antv/hierarchy": "^0.7.1",
|
||||
"@antv/layout": "^2.0.0",
|
||||
"@antv/util": "^3.3.11",
|
||||
"bubblesets-js": "^2.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g6-extension-react": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@antv/g6-extension-react/-/g6-extension-react-0.2.7.tgz",
|
||||
"integrity": "sha512-X/zxGiL/kyJ+5xteX1+P2mI07oLw+zfvKcIHxfynL7IGCQCwQ6q91LkJaOlSDTuWhNRXwnwJ4Cf2Nt/9Dhq5Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g": "^6.1.24",
|
||||
"@antv/g-svg": "^2.0.38"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@antv/g6": "^5.1.0",
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/graphin": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@antv/graphin/-/graphin-3.0.5.tgz",
|
||||
"integrity": "sha512-V/j8R8Ty44wUqxVIYLdpPuIO8WWCTIVq1eBJg5YRunL5t5o5qAFpC/qkQxslbBMWyKdIH0oWBnvHA74riGi7cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g6": "^5.0.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.1.0",
|
||||
"react-dom": "^18.0.0 || ^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/graphlib": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@antv/graphlib/-/graphlib-2.0.4.tgz",
|
||||
"integrity": "sha512-zc/5oQlsdk42Z0ib1mGklwzhJ5vczLFiPa1v7DgJkTbgJ2YxRh9xdarf86zI49sKVJmgbweRpJs7Nu5bIiwv4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/event-emitter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/hierarchy": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@antv/hierarchy/-/hierarchy-0.7.1.tgz",
|
||||
"integrity": "sha512-7r22r+HxfcRZp79ZjGmsn97zgC1Iajrv0Mm9DIgx3lPfk+Kme2MG/+EKdZj1iEBsN0rJRzjWVPGL5YrBdVHchw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antv/layout": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@antv/layout/-/layout-2.0.0.tgz",
|
||||
"integrity": "sha512-aCZ3UdNc40SfT7meFV7QTADY2HCnc0DShVw56CJNTI6oExUIVU736grPuL5Dhb8/JrVaU4Y83QPN/P7KafBzlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/expr": "^1.0.2",
|
||||
"@antv/graphlib": "^2.0.0",
|
||||
"@antv/util": "^3.3.2",
|
||||
"comlink": "^4.4.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-force-3d": "^3.0.5",
|
||||
"d3-octree": "^1.0.2",
|
||||
"d3-quadtree": "^3.0.1",
|
||||
"dagre": "^0.8.5",
|
||||
"ml-matrix": "^6.10.4",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/scale": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@antv/scale/-/scale-0.5.2.tgz",
|
||||
"integrity": "sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/util": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@antv/util/-/util-3.3.11.tgz",
|
||||
"integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/vendor": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@antv/vendor/-/vendor-1.0.11.tgz",
|
||||
"integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/d3-color": "^3.1.3",
|
||||
"@types/d3-dispatch": "^3.0.6",
|
||||
"@types/d3-dsv": "^3.0.7",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-fetch": "^3.0.7",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/d3-format": "^3.0.4",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/d3-hierarchy": "^3.1.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-path": "^3.1.0",
|
||||
"@types/d3-quadtree": "^3.0.6",
|
||||
"@types/d3-random": "^3.0.3",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-scale-chromatic": "^3.1.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/d3-time": "^3.0.4",
|
||||
"@types/d3-timer": "^3.0.2",
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-dispatch": "^3.0.1",
|
||||
"d3-dsv": "^3.0.1",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-fetch": "^3.0.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-force-3d": "^3.0.5",
|
||||
"d3-format": "^3.1.0",
|
||||
"d3-geo": "^3.1.1",
|
||||
"d3-geo-projection": "^4.0.0",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-path": "^3.1.0",
|
||||
"d3-quadtree": "^3.0.1",
|
||||
"d3-random": "^3.0.1",
|
||||
"d3-regression": "^1.3.10",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale-chromatic": "^3.1.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"json5": "^2.2.3",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
|
||||
"integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
|
||||
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
|
||||
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/is-prop-valid": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
||||
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/async-validator": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz",
|
||||
"integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/cascader": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.17.0.tgz",
|
||||
"integrity": "sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/select": "~1.8.0",
|
||||
"@rc-component/tree": "~1.3.2",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/checkbox": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz",
|
||||
"integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/collapse": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz",
|
||||
"integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/color-picker": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz",
|
||||
"integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/fast-color": "^3.0.1",
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/context": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.2.tgz",
|
||||
"integrity": "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/dialog": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.10.0.tgz",
|
||||
"integrity": "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.3.3",
|
||||
"@rc-component/portal": "^2.1.0",
|
||||
"@rc-component/util": "^1.9.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/drawer": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz",
|
||||
"integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/portal": "^2.1.3",
|
||||
"@rc-component/util": "^1.9.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/dropdown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.3.tgz",
|
||||
"integrity": "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.11.0",
|
||||
"react-dom": ">=16.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/form": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.5.tgz",
|
||||
"integrity": "sha512-d24EYtvUOBhxEtSd/EqIu9DaMuqrWF2IRIvAFCTM6NQ/GJIYNr8DvEpUSUlv2uPxEJ0ZPwYQ+wwlGIAaiHvdrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/async-validator": "^6.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/image": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.9.0.tgz",
|
||||
"integrity": "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.0.0",
|
||||
"@rc-component/portal": "^2.1.2",
|
||||
"@rc-component/util": "^1.10.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/input": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.1.tgz",
|
||||
"integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/resize-observer": "^1.1.1",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/input-number": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz",
|
||||
"integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/mini-decimal": "^1.0.1",
|
||||
"@rc-component/util": "^1.4.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/mentions": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.10.0.tgz",
|
||||
"integrity": "sha512-CI1njYUVY0NjHtLhNoVmXlJyy568Sfep9Wsak6vmGjtT6uazx98djGYlCXz2xkHhEm73g91Y3MTvzUyE5avI7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/input": "~1.3.0",
|
||||
"@rc-component/menu": "~1.4.0",
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/menu": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.4.1.tgz",
|
||||
"integrity": "sha512-3GsVRoQ4cnF/AoIQ4P+Z1haBfgfBPQfLT1RJY3Nu4DzOnheTslfCiGSPj7bv/cLj5sW5pHqN25dDXGP3JELAlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/overflow": "^1.0.0",
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/mini-decimal": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
|
||||
"integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/motion": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.3.tgz",
|
||||
"integrity": "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.11.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/mutate-observer": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz",
|
||||
"integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/notification": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz",
|
||||
"integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/util": "^1.11.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/overflow": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz",
|
||||
"integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.1",
|
||||
"@rc-component/resize-observer": "^1.0.1",
|
||||
"@rc-component/util": "^1.4.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/pagination": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.4.0.tgz",
|
||||
"integrity": "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/picker": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.11.0.tgz",
|
||||
"integrity": "sha512-6qXGKtoJvO8sUd17m5cyNEbEJub0zflCHnaZTBBmj63DPRZYc0WEHN8rp6hFSl+yMCJS/dJY5G+1fQ8bLCuD7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/overflow": "^1.0.0",
|
||||
"@rc-component/resize-observer": "^1.0.0",
|
||||
"@rc-component/trigger": "^3.6.15",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"date-fns": ">= 2.x",
|
||||
"dayjs": ">= 1.x",
|
||||
"luxon": ">= 3.x",
|
||||
"moment": ">= 2.x",
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"date-fns": {
|
||||
"optional": true
|
||||
},
|
||||
"dayjs": {
|
||||
"optional": true
|
||||
},
|
||||
"luxon": {
|
||||
"optional": true
|
||||
},
|
||||
"moment": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/portal": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.1.tgz",
|
||||
"integrity": "sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.11.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/progress": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz",
|
||||
"integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.2.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/qrcode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-2.0.0.tgz",
|
||||
"integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/rate": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz",
|
||||
"integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/resize-observer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz",
|
||||
"integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/segmented": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz",
|
||||
"integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.1",
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/select": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.8.2.tgz",
|
||||
"integrity": "sha512-HQ9zuYqjfZTlcEMWlU1GAPBajd2OHIMVHyjZSGVTCVARwkfCgvXZMTEn0cduy3L+ejAKkaZluOQvxovZoaJaQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/overflow": "^1.0.0",
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"@rc-component/virtual-list": "^1.2.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/slider": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.1.1.tgz",
|
||||
"integrity": "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/steps": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz",
|
||||
"integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.2.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/switch": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz",
|
||||
"integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/table": {
|
||||
"version": "1.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.4.tgz",
|
||||
"integrity": "sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/context": "^2.0.1",
|
||||
"@rc-component/resize-observer": "^1.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"@rc-component/virtual-list": "^1.0.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/tabs": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.11.0.tgz",
|
||||
"integrity": "sha512-hA/drZYOVa/MMIb4M2fWf3yaTyTG4qVuIABmghvEhyfw2nBob5VTH69lMCDjSVKmgODjO6nWlCV+gVn3xBrj5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/dropdown": "~1.0.0",
|
||||
"@rc-component/menu": "~1.4.0",
|
||||
"@rc-component/motion": "^1.1.3",
|
||||
"@rc-component/resize-observer": "^1.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/tooltip": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz",
|
||||
"integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/trigger": "^3.7.1",
|
||||
"@rc-component/util": "^1.3.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/tour": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz",
|
||||
"integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/portal": "^2.2.0",
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.7.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/tree": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.2.tgz",
|
||||
"integrity": "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.0.0",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"@rc-component/virtual-list": "^1.2.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/tree-select": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.11.0.tgz",
|
||||
"integrity": "sha512-EhS0X0wtUhBfK4S5TlpSY3MR9ndPMGgujtt1PJW3Ej+ToAlnS/6ohYURtCoXBYGqazUwHmgQGVUDsfpVwhWPkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/select": "~1.8.0",
|
||||
"@rc-component/tree": "~1.3.2",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/trigger": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.1.tgz",
|
||||
"integrity": "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.3.3",
|
||||
"@rc-component/portal": "^2.2.1",
|
||||
"@rc-component/resize-observer": "^1.1.2",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/upload": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.1.tgz",
|
||||
"integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/util": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.12.0.tgz",
|
||||
"integrity": "sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-mobile": "^5.0.0",
|
||||
"react-is": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/virtual-list": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.4.0.tgz",
|
||||
"integrity": "sha512-qoyNStkTJQDezPjBibGA5HNxS9NiKJvemD1bLp7qfyxDlwy7ofPLUP0ZqJ47hR8AKcFaizd0AP/7QWLTLpudKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^8.0.0",
|
||||
"@rc-component/resize-observer": "^1.0.1",
|
||||
"@rc-component/util": "^1.4.0",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz",
|
||||
"integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
|
||||
"integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
|
||||
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
|
||||
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
|
||||
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
|
||||
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
|
||||
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
|
||||
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
"@types/babel__generator": "*",
|
||||
"@types/babel__template": "*",
|
||||
"@types/babel__traverse": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__template": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.1.0",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||
"integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz",
|
||||
"integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/echarts": {
|
||||
"version": "4.9.22",
|
||||
"resolved": "https://registry.npmmirror.com/@types/echarts/-/echarts-4.9.22.tgz",
|
||||
"integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/zrender": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/zrender": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/zrender/-/zrender-4.0.6.tgz",
|
||||
"integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.28.0",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.27",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-6.5.1.tgz",
|
||||
"integrity": "sha512-VZVVF9zYI6S0NHqboVhCoY9Iiqj6dphW1NPB+sEaAf2HuIQ0haXWXj7ZvAXTRDzusktV6+cvvrSZEdRi4twATg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^8.0.1",
|
||||
"@ant-design/cssinjs": "^2.1.2",
|
||||
"@ant-design/cssinjs-utils": "^2.1.2",
|
||||
"@ant-design/fast-color": "^3.0.1",
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@ant-design/react-slick": "~2.0.0",
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"@rc-component/cascader": "~1.17.0",
|
||||
"@rc-component/checkbox": "~2.0.0",
|
||||
"@rc-component/collapse": "~1.2.0",
|
||||
"@rc-component/color-picker": "~3.1.1",
|
||||
"@rc-component/dialog": "~1.10.0",
|
||||
"@rc-component/drawer": "~1.4.2",
|
||||
"@rc-component/dropdown": "~1.0.3",
|
||||
"@rc-component/form": "~1.8.5",
|
||||
"@rc-component/image": "~1.9.0",
|
||||
"@rc-component/input": "~1.3.1",
|
||||
"@rc-component/input-number": "~1.6.2",
|
||||
"@rc-component/mentions": "~1.10.0",
|
||||
"@rc-component/menu": "~1.4.1",
|
||||
"@rc-component/motion": "^1.3.3",
|
||||
"@rc-component/mutate-observer": "^2.0.1",
|
||||
"@rc-component/notification": "~2.0.7",
|
||||
"@rc-component/pagination": "~1.4.0",
|
||||
"@rc-component/picker": "~1.11.0",
|
||||
"@rc-component/progress": "~1.0.2",
|
||||
"@rc-component/qrcode": "~2.0.0",
|
||||
"@rc-component/rate": "~1.0.1",
|
||||
"@rc-component/resize-observer": "^1.1.2",
|
||||
"@rc-component/segmented": "~1.3.0",
|
||||
"@rc-component/select": "~1.8.2",
|
||||
"@rc-component/slider": "~1.1.1",
|
||||
"@rc-component/steps": "~1.2.2",
|
||||
"@rc-component/switch": "~1.0.3",
|
||||
"@rc-component/table": "~1.10.4",
|
||||
"@rc-component/tabs": "~1.11.0",
|
||||
"@rc-component/tooltip": "~1.4.0",
|
||||
"@rc-component/tour": "~2.4.0",
|
||||
"@rc-component/tree": "~1.3.2",
|
||||
"@rc-component/tree-select": "~1.11.0",
|
||||
"@rc-component/trigger": "^3.10.0",
|
||||
"@rc-component/upload": "~1.1.1",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"throttle-debounce": "^5.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ant-design"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
|
||||
"integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.7",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
|
||||
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.44",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"electron-to-chromium": "^1.5.393",
|
||||
"node-releases": "^2.0.51",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bubblesets-js": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/bubblesets-js/-/bubblesets-js-2.3.4.tgz",
|
||||
"integrity": "sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/camelize": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001806",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
|
||||
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-to-react-native": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dsv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"iconv-lite": "0.6",
|
||||
"rw": "1"
|
||||
},
|
||||
"bin": {
|
||||
"csv2json": "bin/dsv2json.js",
|
||||
"csv2tsv": "bin/dsv2dsv.js",
|
||||
"dsv2dsv": "bin/dsv2dsv.js",
|
||||
"dsv2json": "bin/dsv2json.js",
|
||||
"json2csv": "bin/json2dsv.js",
|
||||
"json2dsv": "bin/json2dsv.js",
|
||||
"json2tsv": "bin/json2dsv.js",
|
||||
"tsv2csv": "bin/dsv2dsv.js",
|
||||
"tsv2json": "bin/dsv2json.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-fetch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dsv": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-geo": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.5.0 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-geo-projection": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
|
||||
"integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-geo": "1.12.0 - 3"
|
||||
},
|
||||
"bin": {
|
||||
"geo2svg": "bin/geo2svg.js",
|
||||
"geograticule": "bin/geograticule.js",
|
||||
"geoproject": "bin/geoproject.js",
|
||||
"geoquantize": "bin/geoquantize.js",
|
||||
"geostitch": "bin/geostitch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-hierarchy": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-random": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
||||
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-regression": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/d3-regression/-/d3-regression-1.3.10.tgz",
|
||||
"integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
|
||||
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz",
|
||||
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.395",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
|
||||
"integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flru": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz",
|
||||
"integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graphlib": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.15",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz",
|
||||
"integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-any-array": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz",
|
||||
"integrity": "sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-mobile": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz",
|
||||
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json2mq": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
|
||||
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-convert": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ml-array-max": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ml-array-max/-/ml-array-max-2.0.0.tgz",
|
||||
"integrity": "sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-any-array": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ml-array-min": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ml-array-min/-/ml-array-min-2.0.0.tgz",
|
||||
"integrity": "sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-any-array": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ml-array-rescale": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ml-array-rescale/-/ml-array-rescale-2.0.0.tgz",
|
||||
"integrity": "sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-any-array": "^3.0.0",
|
||||
"ml-array-max": "^2.0.0",
|
||||
"ml-array-min": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ml-matrix": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ml-matrix/-/ml-matrix-6.15.0.tgz",
|
||||
"integrity": "sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-any-array": "^3.0.0",
|
||||
"ml-array-rescale": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.51",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
||||
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfast": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pdfast/-/pdfast-0.2.0.tgz",
|
||||
"integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.22",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
|
||||
"integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
|
||||
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
||||
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
||||
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
||||
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.62.2",
|
||||
"@rollup/rollup-android-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.62.2",
|
||||
"@rollup/rollup-darwin-x64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.62.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.62.2",
|
||||
"@rollup/rollup-openbsd-x64": "4.62.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.62.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.62.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.62.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
|
||||
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-convert": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
|
||||
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/styled-components": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.5.3.tgz",
|
||||
"integrity": "sha512-vAX79sfpmUerP9fsTTxoTrBDE0RuO4ahjInyWYoohNgqrdg63Ms4q6FJ/o2Fyity82NU3cujOT8Ewl9TThBdwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/is-prop-valid": "1.4.0",
|
||||
"css-to-react-native": "3.2.0",
|
||||
"csstype": "3.2.3",
|
||||
"stylis": "4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/styled-components"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"css-to-react-native": ">= 3.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0",
|
||||
"react-native": ">= 0.68.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-to-react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/styled-components/node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
||||
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svg-path-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
|
||||
"integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz",
|
||||
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "ims-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.7",
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"antd": "^6.0.0",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"echarts": "^6.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-redux": "^9.0.0",
|
||||
"react-router-dom": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/echarts": "^4.9.22",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
-- generated by seed-7d.mjs
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '6 days' + interval '00 hours 00 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '6 days' + interval '00 hours 00 minutes 00 seconds' WHERE id = 1;
|
||||
UPDATE issues SET closed_at = created_at + interval '2 days' WHERE id = 1;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '6 days' + interval '10 hours 20 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '6 days' + interval '10 hours 20 minutes 00 seconds' WHERE id = 2;
|
||||
UPDATE issues SET closed_at = created_at + interval '3 days' WHERE id = 2;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '6 days' + interval '00 hours 00 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '6 days' + interval '00 hours 00 minutes 00 seconds' WHERE id = 3;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '6 days' + interval '14 hours 05 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '6 days' + interval '14 hours 05 minutes 00 seconds' WHERE id = 4;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '6 days' + interval '16 hours 40 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '6 days' + interval '16 hours 40 minutes 00 seconds' WHERE id = 5;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '5 days' + interval '09 hours 30 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '5 days' + interval '09 hours 30 minutes 00 seconds' WHERE id = 6;
|
||||
UPDATE issues SET closed_at = created_at + interval '2 days' WHERE id = 6;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '5 days' + interval '11 hours 10 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '5 days' + interval '11 hours 10 minutes 00 seconds' WHERE id = 7;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '5 days' + interval '13 hours 45 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '5 days' + interval '13 hours 45 minutes 00 seconds' WHERE id = 8;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '5 days' + interval '15 hours 20 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '5 days' + interval '15 hours 20 minutes 00 seconds' WHERE id = 9;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '5 days' + interval '17 hours 55 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '5 days' + interval '17 hours 55 minutes 00 seconds' WHERE id = 10;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '4 days' + interval '09 hours 00 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '4 days' + interval '09 hours 00 minutes 00 seconds' WHERE id = 11;
|
||||
UPDATE issues SET closed_at = created_at + interval '2 days' WHERE id = 11;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '4 days' + interval '10 hours 15 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '4 days' + interval '10 hours 15 minutes 00 seconds' WHERE id = 12;
|
||||
UPDATE issues SET closed_at = created_at + interval '3 days' WHERE id = 12;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '4 days' + interval '12 hours 30 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '4 days' + interval '12 hours 30 minutes 00 seconds' WHERE id = 13;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '4 days' + interval '14 hours 50 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '4 days' + interval '14 hours 50 minutes 00 seconds' WHERE id = 14;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '4 days' + interval '16 hours 25 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '4 days' + interval '16 hours 25 minutes 00 seconds' WHERE id = 15;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '3 days' + interval '09 hours 10 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '3 days' + interval '09 hours 10 minutes 00 seconds' WHERE id = 16;
|
||||
UPDATE issues SET closed_at = created_at + interval '2 days' WHERE id = 16;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '3 days' + interval '10 hours 40 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '3 days' + interval '10 hours 40 minutes 00 seconds' WHERE id = 17;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '3 days' + interval '13 hours 20 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '3 days' + interval '13 hours 20 minutes 00 seconds' WHERE id = 18;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '3 days' + interval '15 hours 35 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '3 days' + interval '15 hours 35 minutes 00 seconds' WHERE id = 19;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '3 days' + interval '17 hours 10 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '3 days' + interval '17 hours 10 minutes 00 seconds' WHERE id = 20;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '2 days' + interval '09 hours 25 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '2 days' + interval '09 hours 25 minutes 00 seconds' WHERE id = 21;
|
||||
UPDATE issues SET closed_at = created_at + interval '1 days' WHERE id = 21;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '2 days' + interval '10 hours 50 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '2 days' + interval '10 hours 50 minutes 00 seconds' WHERE id = 22;
|
||||
UPDATE issues SET closed_at = created_at + interval '2 days' WHERE id = 22;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '2 days' + interval '12 hours 00 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '2 days' + interval '12 hours 00 minutes 00 seconds' WHERE id = 23;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '2 days' + interval '14 hours 30 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '2 days' + interval '14 hours 30 minutes 00 seconds' WHERE id = 24;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '2 days' + interval '16 hours 05 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '2 days' + interval '16 hours 05 minutes 00 seconds' WHERE id = 25;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '1 days' + interval '09 hours 15 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '1 days' + interval '09 hours 15 minutes 00 seconds' WHERE id = 26;
|
||||
UPDATE issues SET closed_at = created_at + interval '1 days' WHERE id = 26;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '1 days' + interval '10 hours 30 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '1 days' + interval '10 hours 30 minutes 00 seconds' WHERE id = 27;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '1 days' + interval '11 hours 50 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '1 days' + interval '11 hours 50 minutes 00 seconds' WHERE id = 28;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '1 days' + interval '14 hours 10 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '1 days' + interval '14 hours 10 minutes 00 seconds' WHERE id = 29;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '1 days' + interval '16 hours 45 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '1 days' + interval '16 hours 45 minutes 00 seconds' WHERE id = 30;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '0 days' + interval '09 hours 05 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '0 days' + interval '09 hours 05 minutes 00 seconds' WHERE id = 31;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '0 days' + interval '10 hours 35 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '0 days' + interval '10 hours 35 minutes 00 seconds' WHERE id = 32;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '0 days' + interval '14 hours 20 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '0 days' + interval '14 hours 20 minutes 00 seconds' WHERE id = 33;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '0 days' + interval '16 hours 00 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '0 days' + interval '16 hours 00 minutes 00 seconds' WHERE id = 34;
|
||||
UPDATE issues SET created_at = date_trunc('day', now()) - interval '0 days' + interval '17 hours 30 minutes 00 seconds', updated_at = date_trunc('day', now()) - interval '0 days' + interval '17 hours 30 minutes 00 seconds' WHERE id = 35;
|
||||
UPDATE issue_logs l SET created_at = i.created_at + (rn.rn * interval '30 minutes') FROM issues i, (SELECT id, row_number() OVER (PARTITION BY issue_id ORDER BY id) rn FROM issue_logs) rn WHERE l.id = rn.id AND l.issue_id = i.id;
|
||||
@@ -0,0 +1,170 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const res = await fetch(`${BASE}${path}`, options)
|
||||
const text = await res.text()
|
||||
let json = null
|
||||
try { json = JSON.parse(text) } catch { /* empty body */ }
|
||||
if (!res.ok) {
|
||||
throw new Error(`${options.method || 'GET'} ${path} -> ${res.status} ${text}`)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
function auth(token) {
|
||||
return { 'content-type': 'application/json', authorization: `Bearer ${token}` }
|
||||
}
|
||||
|
||||
// deadline 相对天数(正值未来/负值过去),null 表示无截止日期
|
||||
const dl = (n) => {
|
||||
if (n === null || n === undefined) return null
|
||||
const d = new Date(Date.now() + n * 86400000)
|
||||
const pad = (x) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// daysAgo: 0=今天, 6=6天前;time: 当天创建时刻;closedDaysAfter: 关闭距创建天数
|
||||
const issues = [
|
||||
// ============ D-6(6 天前)============
|
||||
{ title: '用户注册流程中重复邮箱校验失败提示不明确', description: '重复邮箱注册时仅提示「注册失败」无具体原因,需明确提示邮箱已被占用。', status: 'closed', priority: 'medium', phase: '基本设计', subProject: '用户管理子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-3), daysAgo: 6, time: '00:00:00', closedDaysAfter: 2 },
|
||||
{ title: '接口鉴权异常返回状态码与文档不一致', description: 'Token 过期时接口返回 403,而接口文档约定为 401,前端异常处理无法正确触发刷新。', status: 'closed', priority: 'high', phase: '详细设计', subProject: '权限控制子系统', category: '安全漏洞', impactLevel: '较高', impactScope: '影响全部用户', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(-4), daysAgo: 6, time: '10:20', closedDaysAfter: 3 },
|
||||
{ title: '大屏报表在高峰时段刷新导致页面卡死(已逾期)', description: '数据大屏每分钟自动刷新,高峰时段后端响应慢,前端堆叠请求导致页面卡死,属逾期紧急问题。', status: 'pending', priority: 'urgent', phase: '综合测试 (ST)', subProject: '数据报表子系统', category: '性能问题', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-6), daysAgo: 6, time: '00:00:00' },
|
||||
{ title: '用户批量导入时角色字段被忽略', description: '批量导入用户时角色列数据未写入,导入后全部用户无角色权限。', status: 'in_progress', priority: 'medium', phase: '单体测试', subProject: '用户管理子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(2), daysAgo: 6, time: '14:05' },
|
||||
{ title: '报表导出按钮在移动端被遮挡', description: '移动端打开报表页,导出按钮被底部导航遮挡,需调整响应式布局。', status: 'draft', priority: 'low', phase: '基本设计', subProject: '数据报表子系统', category: 'UI/UX问题', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 4, deadline: null, daysAgo: 6, time: '16:40' },
|
||||
// ============ D-5(5 天前)============
|
||||
{ title: '角色权限矩阵说明文档缺少新增权限项', description: '新增「报表导出」权限后,角色权限矩阵说明文档未同步更新。', status: 'closed', priority: 'low', phase: '详细设计', subProject: '权限控制子系统', category: '文档错误', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(-3), daysAgo: 5, time: '09:30', closedDaysAfter: 2 },
|
||||
{ title: '用户停用后再启用,登录时提示账号不存在', description: '停用再启用的用户登录时被缓存系统判定不存在,需清理登录缓存或调整查询逻辑。', status: 'pending_confirm', priority: 'high', phase: '结合测试 (IT)', subProject: '用户管理子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(1), daysAgo: 5, time: '11:10' },
|
||||
{ title: '报表筛选条件切换时偶发白屏', description: '快速切换筛选条件时偶发白屏,前端异常未捕获,需增加错误边界。', status: 'in_progress', priority: 'medium', phase: '单体测试', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(2), daysAgo: 5, time: '13:45', attachments: ['report-white-screen.log'] },
|
||||
{ title: '密码重置链接可在过期后仍被使用', description: '密码重置链接生成 30 分钟后仍可使用,安全窗口过长,需收紧有效期并校验。', status: 'pending', priority: 'urgent', phase: '综合测试 (ST)', subProject: '权限控制子系统', category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-4), daysAgo: 5, time: '15:20' },
|
||||
{ title: '个人中心头像上传后不即时刷新', description: '上传头像成功后页面仍显示旧头像,需刷新浏览器才更新。', status: 'draft', priority: 'medium', phase: '基本设计', subProject: '用户管理子系统', category: 'UI/UX问题', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 4, deadline: null, daysAgo: 5, time: '17:55' },
|
||||
// ============ D-4(4 天前)============
|
||||
{ title: '子账号无法继承上级部门的默认权限', description: '新建子账号后未继承所属部门默认权限,需手动逐个配置,影响开通效率。', status: 'closed', priority: 'high', phase: '详细设计', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-2), daysAgo: 4, time: '09:00', closedDaysAfter: 2 },
|
||||
{ title: '报表时间筛选默认值不正确导致首屏查询异常', description: '默认时间范围取到上个月而非当月,首屏数据与预期不符。', status: 'closed', priority: 'medium', phase: '单体测试', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(-1), daysAgo: 4, time: '10:15', closedDaysAfter: 3, attachments: ['report-date-filter.log'] },
|
||||
{ title: '用户详情页部门信息保存后丢失', description: '编辑用户部门后保存成功,但刷新页面部门回退为原值,保存逻辑未持久化。', status: 'pending', priority: 'medium', phase: '结合测试 (IT)', subProject: '用户管理子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(3), daysAgo: 4, time: '12:30' },
|
||||
{ title: '权限配置界面缺少操作指引文案', description: '权限树配置界面无任何操作指引,新用户难以理解继承与覆盖规则。', status: 'in_progress', priority: 'low', phase: '基本设计', subProject: '权限控制子系统', category: '文档错误', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(3), daysAgo: 4, time: '14:50' },
|
||||
{ title: '报表缓存命中率低导致频繁全量查询', description: '缓存 key 未细化到筛选条件,命中率低,频繁触发全量查询拖慢接口。', status: 'pending_confirm', priority: 'high', phase: '综合测试 (ST)', subProject: '数据报表子系统', category: '性能问题', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-1), daysAgo: 4, time: '16:25' },
|
||||
// ============ D-3(3 天前)============
|
||||
{ title: '登录日志中明文记录密码字段', description: '审计日志将密码字段明文写入,存在泄露风险,需脱敏处理。', status: 'closed', priority: 'urgent', phase: '综合测试 (ST)', subProject: '用户管理子系统', category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(-1), daysAgo: 3, time: '09:10', closedDaysAfter: 2 },
|
||||
{ title: '菜单权限下发后部分终端 5 分钟未生效', description: '角色菜单权限更新后,部分终端最长 5 分钟才生效,需清理网关侧权限缓存。', status: 'in_progress', priority: 'high', phase: '结合测试 (IT)', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(2), daysAgo: 3, time: '10:40' },
|
||||
{ title: '图表导出 Excel 后公式失效', description: '报表图表导出 Excel 后数据变为静态值,原有公式/联动丢失。', status: 'pending', priority: 'medium', phase: '详细设计', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(2), daysAgo: 3, time: '13:20' },
|
||||
{ title: '深色主题下校验错误提示看不清', description: '深色主题下表单校验错误文字对比度不足,难以辨认。', status: 'pending_confirm', priority: 'low', phase: '基本设计', subProject: '用户管理子系统', category: 'UI/UX问题', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 4, deadline: dl(-1), daysAgo: 3, time: '15:35' },
|
||||
{ title: '权限变更审计日志缺少操作前后对比', description: '权限变更日志只记录变更后结果,缺少变更前后对比,无法追溯误操作。', status: 'draft', priority: 'medium', phase: '详细设计', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '影响部分模块', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(4), daysAgo: 3, time: '17:10' },
|
||||
// ============ D-2(2 天前)============
|
||||
{ title: '报表订阅任务偶发重复推送', description: '订阅报表在任务重试时未做幂等处理,偶发同一份报表重复推送。', status: 'closed', priority: 'medium', phase: '单体测试', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-1), daysAgo: 2, time: '09:25', closedDaysAfter: 1, attachments: ['subscribe-duplicate.log'] },
|
||||
{ title: '用户导出接口存在越权查看他人信息风险', description: '用户导出接口未按数据权限过滤,可导出全部用户信息,已加数据权限校验。', status: 'closed', priority: 'high', phase: '结合测试 (IT)', subProject: '用户管理子系统', category: '安全漏洞', impactLevel: '较高', impactScope: '影响全部用户', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 2, time: '10:50', closedDaysAfter: 2 },
|
||||
{ title: '权限变更后旧 Token 权限未即时回收(已逾期)', description: '调整角色权限后,用户已签发的旧 Token 仍持有旧权限,需在鉴权时实时校验。', status: 'in_progress', priority: 'urgent', phase: '综合测试 (ST)', subProject: '权限控制子系统', category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(-1), daysAgo: 2, time: '12:00', agent: { goal: '分析旧 Token 权限未即时回收问题并生成对应方案', action: 'approve-pending' } },
|
||||
{ title: '大数据量筛选查询未走索引', description: '报表大表筛选条件未命中索引,全表扫描导致响应慢,需评估加复合索引。', status: 'pending', priority: 'medium', phase: '详细设计', subProject: '数据报表子系统', category: '性能问题', impactLevel: '一般', impactScope: '影响部分模块', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(3), daysAgo: 2, time: '14:30' },
|
||||
{ title: '用户操作手册缺少批量操作章节', description: '操作手册未收录用户批量导入/停用/导出说明,新员工无法按手册操作。', status: 'draft', priority: 'low', phase: '基本设计', subProject: '用户管理子系统', category: '文档错误', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: 2, reviewerId: 1, departmentId: 3, deadline: null, daysAgo: 2, time: '16:05' },
|
||||
// ============ D-1(1 天前)============
|
||||
{ title: '权限树刷新后折叠状态丢失', description: '刷新页面后权限树展开/折叠状态丢失,需记忆用户浏览位置。', status: 'closed', priority: 'high', phase: '综合测试 (ST)', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 1, time: '09:15', closedDaysAfter: 1 },
|
||||
{ title: '报表订阅邮件正文乱码', description: '订阅邮件正文中文显示乱码,邮件未指定 UTF-8 编码。', status: 'in_progress', priority: 'medium', phase: '结合测试 (IT)', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 1, time: '10:30', agent: { goal: '分析订阅邮件乱码问题并生成修复方案', action: 'reject' } },
|
||||
{ title: '验证码接口未限制调用频率(即将到期)', description: '图形验证码接口可被高频调用,存在资源耗尽与爆破风险,需限流。', status: 'in_progress', priority: 'urgent', phase: '综合测试 (ST)', subProject: '用户管理子系统', category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: dl(1), daysAgo: 1, time: '11:50' },
|
||||
{ title: '角色复制时资源权限合并逻辑错误', description: '角色 A 复制给 B 时,B 原有资源权限被整体覆盖而非合并,导致权限丢失。', status: 'pending_confirm', priority: 'high', phase: '单体测试', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 1, time: '14:10' },
|
||||
{ title: '移动端表单在输入法弹出时遮挡提交按钮', description: '移动端输入时软键盘遮挡提交按钮,无法完成提交,需处理键盘弹出滚动。', status: 'pending', priority: 'medium', phase: '详细设计', subProject: '用户管理子系统', category: 'UI/UX问题', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 4, deadline: dl(1), daysAgo: 1, time: '16:45' },
|
||||
// ============ D0(今天)============
|
||||
{ title: '权限复制操作未校验目标角色是否存在', description: '权限复制时未校验目标角色 ID 有效性,传非法 ID 返回成功但实际未复制。', status: 'in_progress', priority: 'high', phase: '结合测试 (IT)', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块', assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 0, time: '09:05' },
|
||||
{ title: '报表导出任务失败后无重试机制', description: '报表导出失败后任务直接终止,无重试与失败通知,需增加重试策略。', status: 'pending', priority: 'medium', phase: '单体测试', subProject: '数据报表子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面', assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: dl(1), daysAgo: 0, time: '10:35' },
|
||||
{ title: '页面底部固定操作栏遮挡内容', description: '详情页底部固定操作栏遮挡正文内容,滚动到底部时无法查看最后几行。', status: 'pending_confirm', priority: 'low', phase: '基本设计', subProject: '用户管理子系统', category: 'UI/UX问题', impactLevel: '轻微', impactScope: '仅本页面', assigneeId: null, reviewerId: 1, departmentId: 2, deadline: null, daysAgo: 0, time: '14:20' },
|
||||
{ title: '权限模板缺少批量应用入口', description: '权限模板只能逐个应用到角色,缺少批量应用入口,操作效率低。', status: 'draft', priority: 'medium', phase: '详细设计', subProject: '权限控制子系统', category: '功能缺陷', impactLevel: '一般', impactScope: '影响部分模块', assigneeId: null, reviewerId: 1, departmentId: 2, deadline: null, daysAgo: 0, time: '16:00' },
|
||||
{ title: '登录失败三次后未触发锁定策略(今日截止)', description: '连续登录失败三次后账号未被锁定,暴力破解防护缺失,今日截止需尽快整改。', status: 'pending', priority: 'urgent', phase: '综合测试 (ST)', subProject: '用户管理子系统', category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户', assigneeId: null, reviewerId: 1, departmentId: 2, deadline: dl(0), daysAgo: 0, time: '17:30' },
|
||||
]
|
||||
|
||||
const FLOW = {
|
||||
draft: [],
|
||||
pending: ['pending'],
|
||||
in_progress: ['pending', 'in_progress'],
|
||||
pending_confirm: ['pending', 'in_progress', 'pending_confirm'],
|
||||
closed: ['pending', 'in_progress', 'pending_confirm', 'closed'],
|
||||
}
|
||||
|
||||
const ATTACH_CONTENT = {
|
||||
'report-white-screen.log': '2026-08-0X 13:47:32 ERROR Uncaught TypeError: Cannot read properties of undefined (reading map)',
|
||||
'report-date-filter.log': '2026-08-0X 10:16:01 WARN default date range resolved to 2026-06-01 ~ 2026-06-30',
|
||||
'subscribe-duplicate.log': '2026-08-0X 09:26:40 ERROR duplicated push job#7821 retried 2 times',
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const login = await api('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Admin@2026' }),
|
||||
})
|
||||
const token = login.data.accessToken
|
||||
console.log('登录成功\n')
|
||||
|
||||
const sql = ['-- generated by seed-7d.mjs']
|
||||
const created = []
|
||||
|
||||
for (const [i, it] of issues.entries()) {
|
||||
const num = String(i + 1).padStart(2, '0')
|
||||
const { status, daysAgo, time, closedDaysAfter, attachments, agent, ...payload } = it
|
||||
const res = await api('/issues', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const issue = res.data
|
||||
console.log(`[${num}] 创建 ${issue.issueNo} (D-${daysAgo} ${time}) - ${issue.title}`)
|
||||
|
||||
for (const s of FLOW[status] || []) {
|
||||
await api(`/issues/${issue.id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ status: s, remark: '7天数据流转' }),
|
||||
})
|
||||
}
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
for (const name of attachments) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', new Blob([ATTACH_CONTENT[name] || name], { type: 'text/plain' }), name)
|
||||
await api(`/issues/${issue.id}/attachments`, { method: 'POST', headers: { authorization: `Bearer ${token}` }, body: fd })
|
||||
console.log(` 上传附件 ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
const exec = await api('/agent/execute', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ issueId: issue.id, goal: agent.goal }),
|
||||
})
|
||||
const planId = exec.data.planId
|
||||
console.log(` Agent 执行 -> plan#${planId}`)
|
||||
if (agent.action === 'reject') {
|
||||
await api(`/agent/approval/${planId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ comment: '方案不适用,请人工处理' }),
|
||||
})
|
||||
console.log(` Agent 已驳回 plan#${planId}`)
|
||||
} else if (agent.action === 'approve-pending') {
|
||||
console.log(` Agent 审批保持 pending`)
|
||||
}
|
||||
}
|
||||
|
||||
// 回填 created_at(今天 - daysAgo 天 + time 时刻)
|
||||
const [hh, mm, ss = '00'] = time.split(':')
|
||||
sql.push(`UPDATE issues SET created_at = date_trunc('day', now()) - interval '${daysAgo} days' + interval '${hh} hours ${mm} minutes ${ss} seconds', updated_at = date_trunc('day', now()) - interval '${daysAgo} days' + interval '${hh} hours ${mm} minutes ${ss} seconds' WHERE id = ${issue.id};`)
|
||||
if (closedDaysAfter !== undefined) {
|
||||
sql.push(`UPDATE issues SET closed_at = created_at + interval '${closedDaysAfter} days' WHERE id = ${issue.id};`)
|
||||
}
|
||||
created.push(issue)
|
||||
}
|
||||
|
||||
// 回填 issue_logs.created_at(按创建时刻逐条 +30 分钟错开)
|
||||
sql.push(`UPDATE issue_logs l SET created_at = i.created_at + (rn.rn * interval '30 minutes') FROM issues i, (SELECT id, row_number() OVER (PARTITION BY issue_id ORDER BY id) rn FROM issue_logs) rn WHERE l.id = rn.id AND l.issue_id = i.id;`)
|
||||
|
||||
const outPath = join(dirname(fileURLToPath(import.meta.url)), 'seed-7d-backdate.sql')
|
||||
writeFileSync(outPath, sql.join('\n') + '\n', 'utf8')
|
||||
|
||||
console.log(`\n完成:共创建 ${created.length} 条 7 天数据`)
|
||||
console.log(`回填 SQL 已生成:${outPath}`)
|
||||
console.log(`请在 WSL 执行:docker exec -i ims-postgres psql -U ims -d ims -v ON_ERROR_STOP=1 < /mnt/c/Users/NB-070/Desktop/work2/ims-master/frontend/scripts/seed-7d-backdate.sql`)
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('失败:', e.message); process.exit(1) })
|
||||
@@ -0,0 +1,173 @@
|
||||
const BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const res = await fetch(`${BASE}${path}`, options)
|
||||
const text = await res.text()
|
||||
let json = null
|
||||
try { json = JSON.parse(text) } catch { /* empty body */ }
|
||||
if (!res.ok) {
|
||||
throw new Error(`${options.method || 'GET'} ${path} -> ${res.status} ${text}`)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
function auth(token) {
|
||||
return { 'content-type': 'application/json', authorization: `Bearer ${token}` }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const login = await api('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Admin@2026' }),
|
||||
})
|
||||
const token = login.data.accessToken
|
||||
console.log('登录成功\n')
|
||||
|
||||
const daysFromNow = (n) => {
|
||||
const d = new Date(Date.now() + n * 86400000)
|
||||
const pad = (x) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
const issues = [
|
||||
{
|
||||
title: '【演示】登录页面白屏无法进入系统',
|
||||
description: '输入正确账号密码点击登录后,页面白屏无任何响应,控制台报 React 渲染错误。',
|
||||
status: 'draft', priority: 'high', phase: '测试阶段', subProject: '前端开发',
|
||||
category: '界面问题', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(7),
|
||||
},
|
||||
{
|
||||
title: '【演示】CSV 导出中文乱码',
|
||||
description: '导出指摘列表 CSV 后,用 Excel 打开中文标题显示为乱码,需在文件头加 BOM。',
|
||||
status: 'pending', priority: 'medium', phase: '编码阶段', subProject: '后端开发',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(5),
|
||||
},
|
||||
{
|
||||
title: '【演示】Dashboard 统计图表不渲染',
|
||||
description: '工作台首页图表区域空白,ECharts 容器高度为 0,需确认图表初始化时机。',
|
||||
status: 'in_progress', priority: 'high', phase: '测试阶段', subProject: '前端开发',
|
||||
category: '界面问题', impactLevel: '较大', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(3),
|
||||
attachments: [{ name: 'screenshot-dashboard-blank.png', content: 'PNG 占位:Dashboard 图表白屏截图(演示数据)' }],
|
||||
},
|
||||
{
|
||||
title: '【演示】Agent 审批卡点击无响应',
|
||||
description: 'Agent 执行后弹出审批卡,点击批准/驳回按钮无反应,接口 500。',
|
||||
status: 'in_progress', priority: 'urgent', phase: '编码阶段', subProject: '后端开发',
|
||||
category: '功能缺陷', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 4, deadline: daysFromNow(2),
|
||||
agent: { goal: '查找登录白屏相似案例并生成对应方案', action: 'approve-pending' },
|
||||
},
|
||||
{
|
||||
title: '【演示】通知铃铛未读数量不更新',
|
||||
description: '收到状态流转通知后,右上角铃铛未读数仍为 0,需刷新才显示。',
|
||||
status: 'pending_confirm', priority: 'medium', phase: '上线阶段', subProject: '通知模块',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: daysFromNow(6),
|
||||
},
|
||||
{
|
||||
title: '【演示】附件下载返回 404',
|
||||
description: '点击附件下载按钮,接口返回 404,MinIO 对象与数据库记录不一致。',
|
||||
status: 'closed', priority: 'low', phase: '测试阶段', subProject: '后端开发',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(-3),
|
||||
attachments: [{ name: 'error-log.txt', content: '2026-08-01 10:23:45 ERROR download attachment 404' }],
|
||||
},
|
||||
{
|
||||
title: '【演示】逾期未处理的紧急指摘',
|
||||
description: '该指摘已超过整改截止日期仍未处理,需催办担当者。',
|
||||
status: 'pending', priority: 'urgent', phase: '上线阶段', subProject: '数据库',
|
||||
category: '数据问题', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-1),
|
||||
},
|
||||
{
|
||||
title: '【演示】Agent 方案被驳回回退人工',
|
||||
description: 'Agent 生成方案后经审批被驳回,回退人工模式由担当者手动处理。',
|
||||
status: 'in_progress', priority: 'high', phase: '编码阶段', subProject: '前端开发',
|
||||
category: '需求变更', impactLevel: '较大', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 4, deadline: daysFromNow(4),
|
||||
agent: { goal: '自动生成附件下载修复方案', action: 'reject' },
|
||||
},
|
||||
{
|
||||
title: '【演示】CSV 导出大数据量性能',
|
||||
description: '导出 1 万条指摘时接口耗时超过 30 秒,前端超时。',
|
||||
status: 'closed', priority: 'medium', phase: '编码阶段', subProject: '后端开发',
|
||||
category: '性能问题', impactLevel: '较大', impactScope: '影响部分模块',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-2),
|
||||
},
|
||||
{
|
||||
title: '【演示】多状态流转完整链路验证',
|
||||
description: '该指摘完整走完 草稿→待对应→对应中→待确认→已关闭 全链路,用于演示状态机与流转日志。',
|
||||
status: 'closed', priority: 'medium', phase: '需求阶段', subProject: '业务验证',
|
||||
category: '需求变更', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-5),
|
||||
},
|
||||
]
|
||||
|
||||
const FLOW = {
|
||||
draft: [],
|
||||
pending: ['pending'],
|
||||
in_progress: ['pending', 'in_progress'],
|
||||
pending_confirm: ['pending', 'in_progress', 'pending_confirm'],
|
||||
closed: ['pending', 'in_progress', 'pending_confirm', 'closed'],
|
||||
}
|
||||
|
||||
const created = []
|
||||
for (const [i, it] of issues.entries()) {
|
||||
const num = String(i + 1).padStart(2, '0')
|
||||
const { status, attachments, agent, ...payload } = it
|
||||
const res = await api('/issues', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const issue = res.data
|
||||
console.log(`[${num}] 创建 ${issue.issueNo} - ${issue.title}`)
|
||||
|
||||
for (const s of FLOW[status] || []) {
|
||||
await api(`/issues/${issue.id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ status: s, remark: '演示流转' }),
|
||||
})
|
||||
console.log(` 流转 -> ${s}`)
|
||||
}
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
for (const a of attachments) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', new Blob([a.content], { type: 'text/plain' }), a.name)
|
||||
await api(`/issues/${issue.id}/attachments`, { method: 'POST', headers: { authorization: `Bearer ${token}` }, body: fd })
|
||||
console.log(` 上传附件 ${a.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
const exec = await api('/agent/execute', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ issueId: issue.id, goal: agent.goal }),
|
||||
})
|
||||
const planId = exec.data.planId
|
||||
console.log(` Agent 执行 -> plan#${planId}`)
|
||||
if (agent.action === 'approve-pending') {
|
||||
console.log(` Agent 审批保持 pending(演示审批中)`)
|
||||
} else if (agent.action === 'reject') {
|
||||
await api(`/agent/approval/${planId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ comment: '方案不适用,请人工处理' }),
|
||||
})
|
||||
console.log(` Agent 已驳回 plan#${planId}`)
|
||||
}
|
||||
}
|
||||
created.push(issue)
|
||||
}
|
||||
|
||||
console.log(`\n完成:共创建 ${created.length} 条演示数据`)
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('失败:', e.message); process.exit(1) })
|
||||
@@ -0,0 +1,244 @@
|
||||
const BASE = 'http://localhost:8080/api/v1'
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const res = await fetch(`${BASE}${path}`, options)
|
||||
const text = await res.text()
|
||||
let json = null
|
||||
try { json = JSON.parse(text) } catch { /* empty body */ }
|
||||
if (!res.ok) {
|
||||
throw new Error(`${options.method || 'GET'} ${path} -> ${res.status} ${text}`)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
function auth(token) {
|
||||
return { 'content-type': 'application/json', authorization: `Bearer ${token}` }
|
||||
}
|
||||
|
||||
const daysFromNow = (n) => {
|
||||
const d = new Date(Date.now() + n * 86400000)
|
||||
const pad = (x) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
const issues = [
|
||||
{
|
||||
title: '「用户管理」批量导入用户时部门字段校验缺失',
|
||||
description: '批量导入 Excel 时,部门字段为空或填写错误也能导入成功,导致用户归属部门错乱,需增加必填与有效性校验。',
|
||||
status: 'draft', priority: 'high', phase: '基本设计', subProject: '用户管理子系统',
|
||||
category: '功能缺陷', impactLevel: '较高', impactScope: '影响全部用户',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(10),
|
||||
},
|
||||
{
|
||||
title: '权限控制子系统操作日志字段说明文档缺失',
|
||||
description: '操作日志接口返回字段(requestId、clientIp、duration)缺少字段说明,开发对接困难,需补齐接口文档。',
|
||||
status: 'draft', priority: 'medium', phase: '详细设计', subProject: '权限控制子系统',
|
||||
category: '文档错误', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(12),
|
||||
},
|
||||
{
|
||||
title: '用户列表页在高分辨率下表格布局错位',
|
||||
description: '2560x1440 分辨率下用户列表表格列宽挤压错位,操作按钮被遮挡,需调整弹性布局。',
|
||||
status: 'draft', priority: 'low', phase: '基本设计', subProject: '用户管理子系统',
|
||||
category: 'UI/UX问题', impactLevel: '轻微', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 4, deadline: daysFromNow(14),
|
||||
},
|
||||
{
|
||||
title: '报表导出超时:5万行数据接口响应超过60秒',
|
||||
description: '导出 5 万行报表数据时接口耗时超过 60 秒,前端请求超时,需改为异步导出或分批流式输出。',
|
||||
status: 'pending', priority: 'urgent', phase: '综合测试 (ST)', subProject: '数据报表子系统',
|
||||
category: '性能问题', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: daysFromNow(-2),
|
||||
},
|
||||
{
|
||||
title: '越权访问:普通用户可调用管理员角色接口',
|
||||
description: '使用普通账号登录后,直接访问 /admin/* 接口返回 200,权限过滤未生效,存在越权风险。',
|
||||
status: 'pending', priority: 'urgent', phase: '结合测试 (IT)', subProject: '权限控制子系统',
|
||||
category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(-1),
|
||||
},
|
||||
{
|
||||
title: '忘记密码流程未发送重置邮件',
|
||||
description: '点击「忘记密码」并提交邮箱后未收到重置邮件,邮件服务异常但无错误提示,用户无法自助找回密码。',
|
||||
status: 'pending', priority: 'medium', phase: '单体测试', subProject: '用户管理子系统',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-1),
|
||||
},
|
||||
{
|
||||
title: '角色权限勾选后未实时生效,需刷新页面',
|
||||
description: '修改角色权限并保存后,对应账号重新登录仍显示旧菜单,需强制刷新浏览器才能生效。',
|
||||
status: 'pending', priority: 'medium', phase: '详细设计', subProject: '权限控制子系统',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(5),
|
||||
},
|
||||
{
|
||||
title: '数据报表子系统接口文档缺少分页参数说明',
|
||||
description: '报表查询接口文档未说明 page/pageSize 参数默认值与上限,前端翻页按 20 条处理与后端默认值不一致。',
|
||||
status: 'pending', priority: 'low', phase: '基本设计', subProject: '数据报表子系统',
|
||||
category: '文档错误', impactLevel: '轻微', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(7),
|
||||
},
|
||||
{
|
||||
title: '登录接口存在暴力破解风险,无验证码与限流',
|
||||
description: '登录接口连续失败无次数限制、无验证码、无 IP 限流,可被脚本暴力破解弱口令账号。',
|
||||
status: 'in_progress', priority: 'urgent', phase: '综合测试 (ST)', subProject: '用户管理子系统',
|
||||
category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(-3),
|
||||
},
|
||||
{
|
||||
title: '权限复制功能在子菜单继承时丢失父级权限',
|
||||
description: '将角色 A 权限复制给角色 B 后,B 的某些子菜单有权限但父级菜单无权限,导致页面 403。',
|
||||
status: 'in_progress', priority: 'high', phase: '结合测试 (IT)', subProject: '权限控制子系统',
|
||||
category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: daysFromNow(2),
|
||||
agent: { goal: '分析权限复制子菜单继承丢失父级权限问题并生成对应方案', action: 'reject' },
|
||||
},
|
||||
{
|
||||
title: '报表图表加载慢:首屏渲染耗时 8 秒',
|
||||
description: '打开报表图表页首屏渲染需 8 秒,前端一次性拉取全部历史数据,需改为按日期范围按需加载。',
|
||||
status: 'in_progress', priority: 'high', phase: '单体测试', subProject: '数据报表子系统',
|
||||
category: '性能问题', impactLevel: '较高', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 3, deadline: daysFromNow(4),
|
||||
},
|
||||
{
|
||||
title: '修改手机号后旧 Token 仍有效,未强制下线',
|
||||
description: '用户修改绑定手机号后,旧 Token 依旧有效可继续访问,存在账号被冒用风险,应在敏感信息变更后使旧 Token 失效。',
|
||||
status: 'in_progress', priority: 'medium', phase: '详细设计', subProject: '用户管理子系统',
|
||||
category: '安全漏洞', impactLevel: '较高', impactScope: '影响全部用户',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: daysFromNow(6),
|
||||
agent: { goal: '修改手机号后旧Token失效方案设计,检索知识库相似案例', action: 'approve-pending' },
|
||||
},
|
||||
{
|
||||
title: '用户搜索输入中文时出现乱码',
|
||||
description: '用户列表搜索框输入中文关键词后,接口返回数据为空,日志显示查询参数编码异常。',
|
||||
status: 'in_progress', priority: 'medium', phase: '单体测试', subProject: '用户管理子系统',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(8),
|
||||
attachments: [{ name: 'search-garbled-log.txt', content: '2026-08-06 14:32:10 WARN query keyword=[\uFFFD\uFFFD] returns 0 rows' }],
|
||||
},
|
||||
{
|
||||
title: '数据报表缓存未失效,修改后仍显示旧数据',
|
||||
description: '修改基础数据后刷新报表,图表仍展示缓存中的旧数据,缓存 key 未包含数据更新时间。',
|
||||
status: 'pending_confirm', priority: 'high', phase: '综合测试 (ST)', subProject: '数据报表子系统',
|
||||
category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(1),
|
||||
},
|
||||
{
|
||||
title: '用户批量停用后历史操作记录被误删',
|
||||
description: '批量停用用户时,误触发历史操作记录清理逻辑,导致该用户历史日志丢失,需与停用流程解耦。',
|
||||
status: 'pending_confirm', priority: 'high', phase: '结合测试 (IT)', subProject: '用户管理子系统',
|
||||
category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(3),
|
||||
},
|
||||
{
|
||||
title: '权限树勾选父节点时子节点状态不同步',
|
||||
description: '权限树中勾选父节点后,部分子节点显示为半选状态,保存后子节点权限丢失,需级联同步。',
|
||||
status: 'pending_confirm', priority: 'medium', phase: '单体测试', subProject: '权限控制子系统',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 2, deadline: daysFromNow(5),
|
||||
attachments: [{ name: 'screenshot-permission-tree.png', content: 'PNG 占位:权限树半选状态截图(测试数据)' }],
|
||||
},
|
||||
{
|
||||
title: '综合测试环境 SQL 注入漏洞:动态拼接登录查询',
|
||||
description: '登录接口存在动态拼接 SQL,构造恶意用户名可绕过密码校验,已复现并完成修复,待上线验证。',
|
||||
status: 'closed', priority: 'urgent', phase: '综合测试 (ST)', subProject: '用户管理子系统',
|
||||
category: '安全漏洞', impactLevel: '严重', impactScope: '影响全部用户',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 2, deadline: daysFromNow(-6),
|
||||
},
|
||||
{
|
||||
title: '用户编辑时角色下拉缺少默认选中值',
|
||||
description: '编辑已有用户时,角色下拉框未回显当前角色,保存时如未重新选择会误覆盖角色,已改为编辑时预填当前角色。',
|
||||
status: 'closed', priority: 'high', phase: '结合测试 (IT)', subProject: '用户管理子系统',
|
||||
category: '功能缺陷', impactLevel: '较高', impactScope: '影响部分模块',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-4),
|
||||
},
|
||||
{
|
||||
title: '报表页日期筛选在 Chrome 下无法选择',
|
||||
description: '报表页日期范围控件在 Chrome 最新版下点击无响应,Safari 正常,需兼容第三方日期组件初始化时机。',
|
||||
status: 'closed', priority: 'medium', phase: '单体测试', subProject: '数据报表子系统',
|
||||
category: '功能缺陷', impactLevel: '一般', impactScope: '仅本页面',
|
||||
assigneeId: 2, reviewerId: 1, departmentId: 4, deadline: daysFromNow(-2),
|
||||
attachments: [{ name: 'repro-datepicker-steps.txt', content: '复现:1.打开报表页 2.点击日期框 3.Chrome v126 无弹层' }],
|
||||
},
|
||||
{
|
||||
title: '权限控制子系统部署说明文档版本过旧',
|
||||
description: '部署文档仍为 v1.2 旧版,与实际 v2.0 配置(新增 JWT 密钥项)不一致,需同步更新部署手册。',
|
||||
status: 'closed', priority: 'low', phase: '详细设计', subProject: '权限控制子系统',
|
||||
category: '文档错误', impactLevel: '轻微', impactScope: '仅本页面',
|
||||
assigneeId: 3, reviewerId: 1, departmentId: 3, deadline: daysFromNow(-5),
|
||||
},
|
||||
]
|
||||
|
||||
const FLOW = {
|
||||
draft: [],
|
||||
pending: ['pending'],
|
||||
in_progress: ['pending', 'in_progress'],
|
||||
pending_confirm: ['pending', 'in_progress', 'pending_confirm'],
|
||||
closed: ['pending', 'in_progress', 'pending_confirm', 'closed'],
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const login = await api('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'Admin@2026' }),
|
||||
})
|
||||
const token = login.data.accessToken
|
||||
console.log('登录成功\n')
|
||||
|
||||
const created = []
|
||||
for (const [i, it] of issues.entries()) {
|
||||
const num = String(i + 1).padStart(2, '0')
|
||||
const { status, attachments, agent, ...payload } = it
|
||||
const res = await api('/issues', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
const issue = res.data
|
||||
console.log(`[${num}] 创建 ${issue.issueNo} - ${issue.title}`)
|
||||
|
||||
for (const s of FLOW[status] || []) {
|
||||
await api(`/issues/${issue.id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ status: s, remark: '测试数据流转' }),
|
||||
})
|
||||
console.log(` 流转 -> ${s}`)
|
||||
}
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
for (const a of attachments) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', new Blob([a.content], { type: 'text/plain' }), a.name)
|
||||
await api(`/issues/${issue.id}/attachments`, { method: 'POST', headers: { authorization: `Bearer ${token}` }, body: fd })
|
||||
console.log(` 上传附件 ${a.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
const exec = await api('/agent/execute', {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ issueId: issue.id, goal: agent.goal }),
|
||||
})
|
||||
const planId = exec.data.planId
|
||||
console.log(` Agent 执行 -> plan#${planId}`)
|
||||
if (agent.action === 'approve-pending') {
|
||||
console.log(` Agent 审批保持 pending(演示审批中)`)
|
||||
} else if (agent.action === 'reject') {
|
||||
await api(`/agent/approval/${planId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: auth(token),
|
||||
body: JSON.stringify({ comment: '方案不适用,请人工处理' }),
|
||||
})
|
||||
console.log(` Agent 已驳回 plan#${planId}`)
|
||||
}
|
||||
}
|
||||
created.push(issue)
|
||||
}
|
||||
|
||||
console.log(`\n完成:共创建 ${created.length} 条测试数据`)
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('失败:', e.message); process.exit(1) })
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useRoutes } from 'react-router-dom'
|
||||
import routes from './routes'
|
||||
|
||||
function App() {
|
||||
const element = useRoutes(routes)
|
||||
return <>{element}</>
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,17 @@
|
||||
import { App } from 'antd'
|
||||
|
||||
type Static = ReturnType<typeof App.useApp>
|
||||
|
||||
let message: Static['message']
|
||||
let notification: Static['notification']
|
||||
let modal: Static['modal']
|
||||
|
||||
export default function AntdStatic() {
|
||||
const staticFn = App.useApp()
|
||||
message = staticFn.message
|
||||
notification = staticFn.notification
|
||||
modal = staticFn.modal
|
||||
return null
|
||||
}
|
||||
|
||||
export { message, notification, modal }
|
||||
@@ -0,0 +1,327 @@
|
||||
import request from '../request'
|
||||
|
||||
export interface PageResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export interface UserItem {
|
||||
id: number
|
||||
userid: string
|
||||
username: string
|
||||
email?: string
|
||||
departmentId?: number
|
||||
departmentName?: string
|
||||
isActive?: boolean
|
||||
agentAutoExecute?: boolean
|
||||
roleIds?: number[]
|
||||
roles?: string[]
|
||||
lastLoginAt?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface UserPayload {
|
||||
userid?: string
|
||||
username: string
|
||||
email?: string
|
||||
password?: string
|
||||
departmentId?: number
|
||||
roleIds?: number[]
|
||||
isActive?: boolean
|
||||
agentAutoExecute?: boolean
|
||||
}
|
||||
|
||||
export interface DeptNode {
|
||||
id: number
|
||||
name: string
|
||||
parentId?: number
|
||||
sortOrder?: number
|
||||
children?: DeptNode[]
|
||||
}
|
||||
|
||||
export interface PermissionItem {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
resource: string
|
||||
}
|
||||
|
||||
export interface RoleItem {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
dataScope?: string
|
||||
agentAutoExecute?: boolean
|
||||
permissionIds?: number[]
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface RolePayload {
|
||||
name: string
|
||||
description?: string
|
||||
dataScope?: string
|
||||
agentAutoExecute?: boolean
|
||||
permissionIds?: number[]
|
||||
}
|
||||
|
||||
export interface LogItem {
|
||||
id: number
|
||||
operator: string
|
||||
action: string
|
||||
resource: string
|
||||
detail: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface LogQuery {
|
||||
operator?: string
|
||||
actionType?: string
|
||||
keyword?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
list: (params: Record<string, unknown>) => request.get('/users', { params }),
|
||||
create: (data: UserPayload) => request.post('/users', data),
|
||||
update: (id: number, data: UserPayload) => request.put(`/users/${id}`, data),
|
||||
updateStatus: (id: number, isActive: boolean) =>
|
||||
request.put(`/users/${id}/status`, null, { params: { isActive } }),
|
||||
exportCsv: (params: Record<string, unknown>) =>
|
||||
request.get('/users/export', { params, responseType: 'blob' }),
|
||||
}
|
||||
|
||||
export const deptApi = {
|
||||
tree: () => request.get('/departments'),
|
||||
}
|
||||
|
||||
export const roleApi = {
|
||||
list: () => request.get('/roles'),
|
||||
permissions: () => request.get('/roles/permissions'),
|
||||
create: (data: RolePayload) => request.post('/roles', data),
|
||||
update: (id: number, data: RolePayload) => request.put(`/roles/${id}`, data),
|
||||
}
|
||||
|
||||
export const logApi = {
|
||||
list: (params: LogQuery) => request.get('/logs', { params }),
|
||||
}
|
||||
|
||||
export interface ImportRow {
|
||||
rowNo?: number
|
||||
title?: string
|
||||
docType?: string
|
||||
phase?: string
|
||||
priority?: string
|
||||
deadline?: string
|
||||
reviewDate?: string
|
||||
subProject?: string
|
||||
category?: string
|
||||
impactLevel?: string
|
||||
description?: string
|
||||
assigneeUserid?: string
|
||||
departmentName?: string
|
||||
status?: string
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export interface ImportPreview {
|
||||
total: number
|
||||
validCount: number
|
||||
errorCount: number
|
||||
headerValid?: boolean
|
||||
rows: ImportRow[]
|
||||
}
|
||||
|
||||
export interface ImportRecord {
|
||||
id: number
|
||||
fileName: string
|
||||
totalCount: number
|
||||
successCount: number
|
||||
failCount: number
|
||||
status: string
|
||||
errorLog?: string
|
||||
operator?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface ImportSuggestion {
|
||||
rowNo?: number
|
||||
field?: string
|
||||
fieldName?: string
|
||||
original?: string
|
||||
suggested?: string
|
||||
reason?: string
|
||||
level?: string
|
||||
}
|
||||
|
||||
export interface AgentValidateResponse {
|
||||
usable: boolean
|
||||
usableReason?: string
|
||||
suggestions: ImportSuggestion[]
|
||||
engine: string
|
||||
}
|
||||
|
||||
export const importApi = {
|
||||
template: () =>
|
||||
request.get('/import/template', { responseType: 'blob' }),
|
||||
preview: (file: File) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return request.post('/import/excel', form)
|
||||
},
|
||||
aiValidate: (rows: ImportRow[]) =>
|
||||
request.post('/import/ai-validate', rows, { timeout: 1800000 }),
|
||||
confirm: (fileName: string, rows: ImportRow[]) =>
|
||||
request.post('/import/confirm', { fileName, rows }),
|
||||
records: (page = 1, pageSize = 20) =>
|
||||
request.get('/import/records', { params: { page, pageSize } }),
|
||||
}
|
||||
|
||||
export interface AgentConfig {
|
||||
maxSteps?: number
|
||||
autoExecuteHighRisk?: boolean
|
||||
userRateLimit?: number
|
||||
}
|
||||
|
||||
export const agentApi = {
|
||||
getConfig: () => request.get('/agent/config'),
|
||||
updateConfig: (config: AgentConfig) => request.put('/agent/config', config),
|
||||
}
|
||||
|
||||
export interface PromptTemplateItem {
|
||||
id: number
|
||||
templateId: string
|
||||
name: string
|
||||
category: string
|
||||
version: number
|
||||
content: string
|
||||
variables?: string
|
||||
outputSchema?: string
|
||||
isActive?: boolean
|
||||
isDefault?: boolean
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface PromptVersion {
|
||||
id: number
|
||||
templateId: string
|
||||
version: number
|
||||
content: string
|
||||
changeLog?: string
|
||||
createdBy?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface PromptLogItem {
|
||||
id: number
|
||||
requestId: string
|
||||
templateId: string
|
||||
templateVersion: number
|
||||
renderedPrompt: string
|
||||
executionTimeMs?: number
|
||||
llmModel?: string
|
||||
modelProvider?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface PromptStats {
|
||||
totalTemplates: number
|
||||
activeTemplates: number
|
||||
totalVersions: number
|
||||
totalRenders: number
|
||||
}
|
||||
|
||||
export const promptApi = {
|
||||
list: (params: Record<string, unknown>) => request.get('/prompts', { params }),
|
||||
detail: (templateId: string) => request.get(`/prompts/${templateId}`),
|
||||
create: (data: Record<string, unknown>) => request.post('/prompts', data),
|
||||
update: (templateId: string, data: Record<string, unknown>) =>
|
||||
request.put(`/prompts/${templateId}`, data),
|
||||
rollback: (templateId: string, version: number) =>
|
||||
request.post(`/prompts/${templateId}/rollback`, null, { params: { version } }),
|
||||
test: (templateId: string, variables: Record<string, unknown>) =>
|
||||
request.post(`/prompts/${templateId}/test`, { variables }),
|
||||
versions: (templateId: string) => request.get(`/prompts/${templateId}/versions`),
|
||||
logs: (page = 1, pageSize = 20) =>
|
||||
request.get('/prompts/logs', { params: { page, pageSize } }),
|
||||
stats: () => request.get('/prompts/stats'),
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
provider: string
|
||||
ollamaBaseUrl?: string
|
||||
ollamaChatModel?: string
|
||||
ollamaEmbeddingModel?: string
|
||||
deepseekModel?: string
|
||||
deepseekEmbeddingModel?: string
|
||||
deepseekApiKey?: string
|
||||
autoFallbackEnabled?: boolean
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
get: () => request.get('/ai/config'),
|
||||
update: (config: AiConfig) => request.put('/ai/config', config),
|
||||
test: () => request.post('/ai/config/test'),
|
||||
}
|
||||
|
||||
export interface AgentOverview {
|
||||
todayExecutions: number
|
||||
yesterdayExecutions: number
|
||||
growthRate: number
|
||||
todayPlans: number
|
||||
toolTotalCount: number
|
||||
toolSuccessCount: number
|
||||
toolSuccessRate: number
|
||||
pendingApprovals: number
|
||||
latestExecutions: AgentExecution[]
|
||||
trendData: { hour: string; count: number }[]
|
||||
}
|
||||
|
||||
export interface AgentExecution {
|
||||
id: number
|
||||
planId: number
|
||||
toolName: string
|
||||
status: string
|
||||
executionTimeMs?: number
|
||||
outputResult?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface AgentPlanItem {
|
||||
planId: number
|
||||
issueId: number
|
||||
issueNo: string
|
||||
issueTitle: string
|
||||
goal: string
|
||||
status: string
|
||||
requiresApproval: boolean
|
||||
approvalStatus: string
|
||||
approvalComment?: string
|
||||
toolName?: string
|
||||
toolParams?: string
|
||||
approvalReason?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface AgentToolItem {
|
||||
name: string
|
||||
description: string
|
||||
isWrite: boolean
|
||||
}
|
||||
|
||||
export const agentOverviewApi = {
|
||||
getOverview: () => request.get('/agent/overview'),
|
||||
getPlans: (params: { approvalStatus?: string; page?: number; pageSize?: number }) =>
|
||||
request.get('/agent/plans', { params }),
|
||||
approvePlan: (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/approve`, { comment }),
|
||||
rejectPlan: (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/reject`, { comment }),
|
||||
getTools: () => request.get('/agent/tools'),
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export const STATUS_LABELS: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
open: '待处理',
|
||||
in_progress: '进行中',
|
||||
resolved: '已解决',
|
||||
verified: '已验证',
|
||||
closed: '已关闭',
|
||||
rejected: '已驳回'
|
||||
}
|
||||
|
||||
export const STATUS_COLORS: Record<string, string> = {
|
||||
draft: 'default',
|
||||
open: 'orange',
|
||||
in_progress: 'blue',
|
||||
resolved: 'cyan',
|
||||
verified: 'purple',
|
||||
closed: 'green',
|
||||
rejected: 'red'
|
||||
}
|
||||
|
||||
export const ACTION_LABELS: Record<string, string> = {
|
||||
CREATE: '创建指摘', UPDATE: '更新指摘', DELETE: '删除指摘',
|
||||
STATUS_CHANGE: '状态流转', BATCH_ASSIGN: '批量分配', BATCH_NOTIFY: '批量催办'
|
||||
}
|
||||
|
||||
export const PRIORITY_LABELS: Record<string, string> = {
|
||||
urgent: '紧急',
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
|
||||
export const PRIORITY_COLORS: Record<string, string> = {
|
||||
urgent: 'red',
|
||||
high: 'orange',
|
||||
medium: 'default',
|
||||
low: 'gray'
|
||||
}
|
||||
|
||||
export const PHASE_OPTIONS = ['需求', '设计', '编码', '测试', '部署', '运维']
|
||||
|
||||
export const SUB_PROJECT_OPTIONS = ['用户管理子系统', '权限控制子系统', '数据报表子系统']
|
||||
|
||||
export const CATEGORY_OPTIONS = ['功能缺陷', 'UI/UX问题', '性能问题', '安全漏洞', '文档错误']
|
||||
|
||||
export const IMPACT_LEVEL_OPTIONS = ['高', '中', '低']
|
||||
|
||||
export const USERS = [
|
||||
{ id: 1, name: '系统管理员' },
|
||||
{ id: 2, name: '张三' },
|
||||
{ id: 3, name: '李四' }
|
||||
]
|
||||
|
||||
export const DEPARTMENTS = [
|
||||
{ id: 1, name: '总公司' },
|
||||
{ id: 2, name: '技术部' },
|
||||
{ id: 3, name: '质量部' },
|
||||
{ id: 4, name: '产品部' }
|
||||
]
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, Badge, Space, theme, Empty } from 'antd'
|
||||
import { message } from '../antdStatic'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
BugOutlined,
|
||||
UploadOutlined,
|
||||
RobotOutlined,
|
||||
BookOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
FileTextOutlined,
|
||||
NotificationOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { logout } from '../store/slices/authSlice'
|
||||
import type { RootState } from '../store'
|
||||
import { listNotifications, getUnreadCount, markNotificationRead, markAllNotificationsRead } from '../services/notification'
|
||||
import type { NotificationItem } from '../services/notification'
|
||||
|
||||
const { Header, Sider, Content } = Layout
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/dashboard', icon: <DashboardOutlined />, label: '工作台' },
|
||||
{
|
||||
key: 'issue',
|
||||
icon: <BugOutlined />,
|
||||
label: '指摘管理',
|
||||
children: [
|
||||
{ key: '/issues', icon: <FileTextOutlined />, label: '指摘列表' },
|
||||
{ key: '/issues/new', icon: <FileTextOutlined />, label: '新建指摘' },
|
||||
{ key: '/batch-input', icon: <UploadOutlined />, label: '批量录入' }
|
||||
]
|
||||
},
|
||||
{ key: '/ai-analysis', icon: <RobotOutlined />, label: 'AI智能分析' },
|
||||
{ key: '/knowledge-base', icon: <BookOutlined />, label: '知识库管理' },
|
||||
{
|
||||
key: 'system',
|
||||
icon: <SettingOutlined />,
|
||||
label: '系统管理',
|
||||
children: [
|
||||
{ key: '/system/users', icon: <TeamOutlined />, label: '用户管理' },
|
||||
{ key: '/system/roles', icon: <UserOutlined />, label: '角色权限' },
|
||||
{ key: '/system/logs', icon: <FileTextOutlined />, label: '系统日志' },
|
||||
{ key: '/system/agent-admin', icon: <RobotOutlined />, label: 'Agent管理' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default function MainLayout() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const dispatch = useDispatch()
|
||||
const username = useSelector((state: RootState) => state.auth.username)
|
||||
const { token: { colorBgContainer, borderRadiusLG } } = theme.useToken()
|
||||
|
||||
const [unread, setUnread] = useState(0)
|
||||
const [notifs, setNotifs] = useState<NotificationItem[]>([])
|
||||
|
||||
const loadUnread = async () => {
|
||||
try {
|
||||
const res: any = await getUnreadCount()
|
||||
setUnread(res.data?.count || 0)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const loadNotifs = async () => {
|
||||
try {
|
||||
const res: any = await listNotifications(1, 20)
|
||||
setNotifs(res.data?.items || [])
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUnread()
|
||||
const timer = setInterval(loadUnread, 60000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
const handleNotifOpen = (open: boolean) => {
|
||||
if (open) loadNotifs()
|
||||
}
|
||||
|
||||
const handleNotifClick = async (item: NotificationItem) => {
|
||||
try {
|
||||
await markNotificationRead(item.id)
|
||||
loadUnread()
|
||||
} catch { /* ignore */ }
|
||||
if (item.link?.startsWith('/issues')) navigate(item.link)
|
||||
}
|
||||
|
||||
const handleReadAll = async () => {
|
||||
try {
|
||||
await markAllNotificationsRead()
|
||||
message.success('已全部标记为已读')
|
||||
loadNotifs()
|
||||
loadUnread()
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const notifMenuItems = [
|
||||
{
|
||||
key: 'header',
|
||||
label: notifs.length ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 13 }}>通知</span>
|
||||
<Button type="link" size="small" disabled={!notifs.some(n => !n.isRead)} onClick={handleReadAll}>全部已读</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Empty description="暂无通知" imageStyle={{ height: 40 }} />
|
||||
),
|
||||
disabled: !notifs.length
|
||||
},
|
||||
...notifs.slice(0, 20).map((n) => ({
|
||||
key: `n-${n.id}`,
|
||||
label: (
|
||||
<div onClick={() => handleNotifClick(n)} style={{ width: 260, padding: '4px 0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontWeight: n.isRead ? 400 : 600, fontSize: 13 }}>{n.title}</span>
|
||||
{!n.isRead && <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#ff4d4f' }} />}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 2, whiteSpace: 'pre-wrap' }}>{n.content}</div>
|
||||
<div style={{ fontSize: 11, color: '#bbb', marginTop: 2 }}>{n.createdAt?.replace('T', ' ').slice(0, 16)}</div>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
]
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout())
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const userMenuItems = [
|
||||
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', onClick: handleLogout }
|
||||
]
|
||||
|
||||
const openKeys = location.pathname.startsWith('/system') ? ['system']
|
||||
: location.pathname.startsWith('/issue') || location.pathname.startsWith('/issues') || location.pathname.startsWith('/batch-input') ? ['issue']
|
||||
: []
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider trigger={null} collapsible collapsed={collapsed}>
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: collapsed ? 16 : 20, fontWeight: 'bold' }}>
|
||||
{collapsed ? 'IMS' : '指摘管理系统'}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
defaultOpenKeys={openKeys}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ padding: '0 24px', background: colorBgContainer, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Button type="text" icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={() => setCollapsed(!collapsed)} />
|
||||
<Space>
|
||||
<Dropdown trigger={['click']} onOpenChange={handleNotifOpen} menu={{ items: notifMenuItems, style: { maxHeight: 480, overflowY: 'auto' } }}>
|
||||
<Badge count={unread} size="small" style={{ cursor: 'pointer' }}>
|
||||
<NotificationOutlined style={{ fontSize: 18 }} />
|
||||
</Badge>
|
||||
</Dropdown>
|
||||
<Dropdown trigger={['click']} menu={{ items: userMenuItems }}>
|
||||
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Avatar icon={<UserOutlined />} />
|
||||
<span>{username || '系统管理员'}</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
<Content style={{ margin: 24 }}>
|
||||
<div style={{ padding: 24, minHeight: 360, background: colorBgContainer, borderRadius: borderRadiusLG }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { Provider } from 'react-redux'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { App as AntdApp, ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import App from './App'
|
||||
import AntdStatic from './antdStatic'
|
||||
import { store } from './store'
|
||||
import theme from './theme'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider locale={zhCN} theme={theme}>
|
||||
<AntdApp>
|
||||
<AntdStatic />
|
||||
<App />
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,508 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
Typography, Table, Button, Modal, Form, Select, Input, InputNumber, DatePicker,
|
||||
Tag, Space, Tooltip, Row, Col, Card, Tabs
|
||||
} from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import {
|
||||
ThunderboltOutlined, DownloadOutlined, SearchOutlined, ReloadOutlined,
|
||||
EyeOutlined, RedoOutlined
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
aiAnalysisApi, departmentApi, issuesApi,
|
||||
AiAnalysisRecord, RecordQuery, DepartmentOption, IssueListItem,
|
||||
ISSUE_STATUS_OPTIONS, ISSUE_PHASE_OPTIONS, ANALYSIS_STATUS_OPTIONS, ISSUE_PRIORITY_OPTIONS
|
||||
} from './services'
|
||||
import AiOverviewTab from './overview'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
const pageSize = 20
|
||||
|
||||
type TreeRecord = AiAnalysisRecord & { children?: TreeRecord[] }
|
||||
|
||||
const buildTree = (items: AiAnalysisRecord[]): TreeRecord[] => {
|
||||
const groups = new Map<number, AiAnalysisRecord[]>()
|
||||
for (const r of items) {
|
||||
const arr = groups.get(r.issueId) || []
|
||||
arr.push(r)
|
||||
groups.set(r.issueId, arr)
|
||||
}
|
||||
const tree: TreeRecord[] = []
|
||||
for (const [, arr] of groups) {
|
||||
arr.sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
|
||||
const [head, ...rest] = arr
|
||||
tree.push({ ...head, children: rest })
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
export default function AiAnalysisPage() {
|
||||
const [records, setRecords] = useState<AiAnalysisRecord[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [departments, setDepartments] = useState<DepartmentOption[]>([])
|
||||
const [query, setQuery] = useState<RecordQuery>({ page: 1, pageSize })
|
||||
const [filterForm] = Form.useForm()
|
||||
|
||||
const [detail, setDetail] = useState<AiAnalysisRecord | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
|
||||
const [generateOpen, setGenerateOpen] = useState(false)
|
||||
const pollTimer = useRef<number | null>(null)
|
||||
|
||||
const schedulePoll = (items: AiAnalysisRecord[], q: RecordQuery) => {
|
||||
if (pollTimer.current) window.clearTimeout(pollTimer.current)
|
||||
const hasRunning = items.some(r => r.status === 'processing' || r.status === 'pending')
|
||||
if (hasRunning) {
|
||||
pollTimer.current = window.setTimeout(() => loadRecords(q), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
const loadRecords = async (q: RecordQuery) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.records(q)
|
||||
const items = res.data?.items || []
|
||||
setRecords(items)
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(q.page)
|
||||
schedulePoll(items, q)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadRecords(query)
|
||||
return () => { if (pollTimer.current) window.clearTimeout(pollTimer.current) }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
departmentApi.list().then((res: any) => {
|
||||
setDepartments(res.data || [])
|
||||
}).catch(() => { /* ignore */ })
|
||||
}, [])
|
||||
|
||||
const buildQuery = (): RecordQuery => {
|
||||
const values = filterForm.getFieldsValue()
|
||||
return {
|
||||
page: 1,
|
||||
pageSize,
|
||||
id: values.id,
|
||||
issueId: values.issueId,
|
||||
departmentId: values.departmentId,
|
||||
status: values.status,
|
||||
startDate: values.dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: values.dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
const params = buildQuery()
|
||||
setQuery(params)
|
||||
loadRecords(params)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filterForm.resetFields()
|
||||
const params: RecordQuery = { page: 1, pageSize }
|
||||
setQuery(params)
|
||||
loadRecords(params)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const blob: any = await aiAnalysisApi.exportRecords(query)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'ai-analysis.csv'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
message.success('导出成功')
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const handleReanalyze = async (record: AiAnalysisRecord) => {
|
||||
if (!record.issueId) return
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.batchGenerate({ issueIds: [record.issueId] })
|
||||
message.success(`已重新提交分析 (${res.data?.submitted ?? 0})`)
|
||||
setTimeout(() => loadRecords(query), 1500)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const handleFeedback = async (id: number, isHelpful: boolean) => {
|
||||
try {
|
||||
await aiAnalysisApi.feedback(id, isHelpful)
|
||||
message.success('已记录反馈')
|
||||
loadRecords(query)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
pending: 'default', processing: 'processing', completed: 'green', failed: 'red'
|
||||
}
|
||||
|
||||
const treeData = useMemo<TreeRecord[]>(() => records.map(r => ({ ...r })), [records])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '指摘ID', dataIndex: 'issueId', key: 'issueId', width: 90,
|
||||
sorter: (a: AiAnalysisRecord, b: AiAnalysisRecord) => a.issueId - b.issueId,
|
||||
render: (v: number) => <span className="font-mono text-gray-500">{v}</span>
|
||||
},
|
||||
{
|
||||
title: 'ID', key: 'id', width: 150,
|
||||
sorter: (a: AiAnalysisRecord, b: AiAnalysisRecord) => (a.issueNo || '').localeCompare(b.issueNo || ''),
|
||||
render: (_: any, r: AiAnalysisRecord) => (
|
||||
<Space size={4} wrap>
|
||||
<span className="font-mono font-bold text-blue-600">{r.issueNo}</span>
|
||||
<Tag color={statusColor[r.status] || 'default'} style={{ fontSize: 10 }}>{r.status}</Tag>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '归属部门', dataIndex: 'departmentName', key: 'departmentName', width: 120,
|
||||
sorter: (a: AiAnalysisRecord, b: AiAnalysisRecord) => (a.departmentName || '').localeCompare(b.departmentName || ''),
|
||||
render: (v: string) => v || '-'
|
||||
},
|
||||
{ title: '提取关键词', dataIndex: 'keywords', key: 'keywords', width: 150, ellipsis: true, render: (v: string) => v || '-' },
|
||||
{ title: '问题分类', dataIndex: 'category', key: 'category', width: 120, render: (v: string) => v || '-' },
|
||||
{ title: '根因分析', dataIndex: 'rootCause', key: 'rootCause', width: 220, ellipsis: true, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: 'AI 整改建议', dataIndex: 'suggestion', key: 'suggestion', width: 300,
|
||||
render: (v: string) => <span style={{ whiteSpace: 'pre-wrap' }}>{v || '-'}</span>
|
||||
},
|
||||
{
|
||||
title: '时间', key: 'time', width: 140, defaultSortOrder: 'descend' as const,
|
||||
sorter: (a: AiAnalysisRecord, b: AiAnalysisRecord) => (a.completedAt || a.startedAt || '').localeCompare(b.completedAt || b.startedAt || ''),
|
||||
render: (_: any, r: AiAnalysisRecord) => {
|
||||
if (r.status === 'processing' || r.status === 'pending') {
|
||||
if (!r.startedAt) return '-'
|
||||
const mins = Math.max(1, Math.floor(dayjs().diff(dayjs(r.startedAt), 'minute')))
|
||||
return (
|
||||
<Space size={4}>
|
||||
<span>开始 {dayjs(r.startedAt).format('MM-DD HH:mm:ss')}</span>
|
||||
<span style={{ color: '#faad14', fontSize: 12 }}>已运行 {mins} 分钟</span>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
return r.completedAt ? `完成 ${dayjs(r.completedAt).format('MM-DD HH:mm:ss')}` : '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '反馈', dataIndex: 'helpfulCount', key: 'feedback', width: 100,
|
||||
sorter: (a: AiAnalysisRecord, b: AiAnalysisRecord) => (a.helpfulCount || 0) - (b.helpfulCount || 0),
|
||||
render: (v: number, r: AiAnalysisRecord) => (
|
||||
v > 0
|
||||
? <span className="px-2 py-0.5 bg-green-100 text-green-700 rounded-full text-xs font-bold">有帮助</span>
|
||||
: <Tooltip title="标记为有帮助"><Button size="small" type="text" onClick={() => handleFeedback(r.id, true)}>有帮助</Button></Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 90, align: 'right' as const, fixed: 'right' as const,
|
||||
render: (_: any, r: AiAnalysisRecord) => (
|
||||
<Space size={4}>
|
||||
<Tooltip title="查看详情">
|
||||
<Button type="text" icon={<EyeOutlined />} onClick={() => { setDetail(r); setDetailOpen(true) }} />
|
||||
</Tooltip>
|
||||
<Tooltip title="重新分析">
|
||||
<Button type="text" icon={<RedoOutlined />} onClick={() => handleReanalyze(r)} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Tabs defaultActiveKey="records" items={[
|
||||
{
|
||||
key: 'overview',
|
||||
label: '分析总览',
|
||||
children: <AiOverviewTab />
|
||||
},
|
||||
{
|
||||
key: 'records',
|
||||
label: '分析记录',
|
||||
children: (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>AI 分析记录</Typography.Title>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<Card style={{ borderRadius: 16 }}>
|
||||
<Form form={filterForm}>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col flex="1">
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>ID</div>
|
||||
<Form.Item name="id" noStyle>
|
||||
<InputNumber placeholder="输入 ID(数字)" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>指摘ID</div>
|
||||
<Form.Item name="issueId" noStyle>
|
||||
<InputNumber placeholder="输入指摘ID(数字)" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>归属部门</div>
|
||||
<Form.Item name="departmentId" noStyle>
|
||||
<Select
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
options={departments.map(d => ({ label: d.name, value: d.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>日期范围</div>
|
||||
<Form.Item name="dateRange" noStyle>
|
||||
<RangePicker format="YYYY/MM/DD" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 700, textTransform: 'uppercase', marginBottom: 6 }}>分析状态</div>
|
||||
<Form.Item name="status" noStyle>
|
||||
<Select placeholder="全部" allowClear options={ANALYSIS_STATUS_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', borderTop: '1px solid #f0f0f0', paddingTop: 16 }}>
|
||||
<Space>
|
||||
<Button type="primary" style={{ background: '#1f2937', borderColor: '#1f2937' }} icon={<SearchOutlined />} onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button type="primary" icon={<ThunderboltOutlined />} onClick={() => setGenerateOpen(true)}>
|
||||
生成 AI 分析
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出记录</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Table */}
|
||||
<Card style={{ borderRadius: 16, padding: 0 }} styles={{ body: { padding: 0 } }}>
|
||||
<Table
|
||||
dataSource={treeData}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1480 }}
|
||||
pagination={{
|
||||
current: page, total, pageSize,
|
||||
onChange: (p) => loadRecords({ ...query, page: p }),
|
||||
showTotal: t => `共 ${t} 条记录`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<Modal
|
||||
title={detail ? `${detail.issueNo} · 分析详情` : '分析详情'}
|
||||
open={detailOpen}
|
||||
onCancel={() => setDetailOpen(false)}
|
||||
footer={<Button onClick={() => setDetailOpen(false)}>关闭</Button>}
|
||||
width={640}
|
||||
>
|
||||
{detail && (
|
||||
<div style={{ lineHeight: 1.8 }}>
|
||||
<div><b>指摘标题:</b>{detail.issueTitle}</div>
|
||||
<div><b>问题分类:</b>{detail.category || '-'}</div>
|
||||
<div><b>预提取关键词:</b>{detail.extractedKeywords || '-'}</div>
|
||||
<div><b>提取关键词:</b>{detail.keywords || '-'}</div>
|
||||
<div style={{ marginTop: 8 }}><b>根因分析:</b></div>
|
||||
<div style={{ background: '#fafafa', borderRadius: 8, padding: 12 }}>{detail.rootCause || '-'}</div>
|
||||
<div style={{ marginTop: 8 }}><b>AI 整改建议:</b></div>
|
||||
<div style={{ background: '#fafafa', borderRadius: 8, padding: 12 }}>{detail.suggestion || '-'}</div>
|
||||
{detail.status === 'failed' && detail.errorMessage && (
|
||||
<div style={{ marginTop: 12, background: '#fff1f0', border: '1px solid #ffccc7', borderRadius: 8, padding: 12, fontSize: 12, color: '#cf1322' }}>
|
||||
<b>失败原因:</b>{detail.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
|
||||
模型:{detail.modelProvider}/{detail.modelName} · 时间:{detail.createdAt}
|
||||
{detail.promptTemplateId && ` · Prompt: ${detail.promptTemplateId} v${detail.promptVersion}`}
|
||||
</div>
|
||||
<Space style={{ marginTop: 12 }}>
|
||||
<Button size="small" onClick={() => handleFeedback(detail.id, true)}>有帮助</Button>
|
||||
<Button size="small" danger onClick={() => handleFeedback(detail.id, false)}>不准确</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<GenerateModal
|
||||
open={generateOpen}
|
||||
onCancel={() => setGenerateOpen(false)}
|
||||
departments={departments}
|
||||
onGenerated={() => setTimeout(() => loadRecords(query), 1500)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GenerateModal(props: {
|
||||
open: boolean
|
||||
onCancel: () => void
|
||||
departments: DepartmentOption[]
|
||||
onGenerated: () => void
|
||||
}) {
|
||||
const [issues, setIssues] = useState<IssueListItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selected, setSelected] = useState<IssueListItem[]>([])
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [issueForm] = Form.useForm()
|
||||
|
||||
const loadIssues = async (q: { page: number; pageSize: number }) => {
|
||||
setLoading(true)
|
||||
const values = issueForm.getFieldsValue()
|
||||
try {
|
||||
const res: any = await issuesApi.list({
|
||||
page: q.page,
|
||||
pageSize: q.pageSize,
|
||||
status: values.status,
|
||||
phase: values.phase,
|
||||
departmentId: values.departmentId,
|
||||
keyword: values.keyword,
|
||||
startDate: values.dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: values.dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
})
|
||||
setIssues(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(q.page)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
setSelected([])
|
||||
issueForm.resetFields()
|
||||
loadIssues({ page: 1, pageSize })
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
const generate = async () => {
|
||||
if (selected.length === 0) {
|
||||
message.warning('请至少选择一条指摘进行 AI 分析')
|
||||
return
|
||||
}
|
||||
setGenerating(true)
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.batchGenerate({ issueIds: selected.map(s => s.id) })
|
||||
const count = res.data?.submitted ?? selected.length
|
||||
const estimated = Math.ceil(count / 4) * 60
|
||||
message.success(`✅ 已对 ${count} 条指摘启动 AI 分析,预计约 ${estimated} 秒内完成`)
|
||||
props.onGenerated()
|
||||
props.onCancel()
|
||||
} catch { /* ignore */ }
|
||||
setGenerating(false)
|
||||
}
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
draft: 'default', open: 'blue', in_progress: 'processing', resolved: 'green',
|
||||
verified: 'cyan', closed: 'gray', rejected: 'red'
|
||||
}
|
||||
const priorityColor: Record<string, string> = {
|
||||
high: 'red', medium: 'orange', low: 'default'
|
||||
}
|
||||
|
||||
const issueColumns = [
|
||||
{
|
||||
title: 'ID', dataIndex: 'issueNo', key: 'issueNo', width: 160,
|
||||
render: (v: string) => <span className="font-mono font-medium text-blue-600">{v}</span>
|
||||
},
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (s: string) => <Tag color={statusColor[s] || 'default'}>{s}</Tag>
|
||||
},
|
||||
{
|
||||
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
|
||||
render: (p: string) => <Tag color={priorityColor[p] || 'default'}>{p}</Tag>
|
||||
},
|
||||
{ title: '工程阶段', dataIndex: 'phase', key: 'phase', width: 100, render: (v: string) => v || '-' },
|
||||
{ title: '对应者', dataIndex: 'assigneeName', key: 'assigneeName', width: 100, render: (v: string) => v || '-' },
|
||||
{ title: '截止日期', dataIndex: 'deadline', key: 'deadline', width: 120, render: (v: string) => v ? v.slice(0, 10) : '-' },
|
||||
]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
onCancel={props.onCancel}
|
||||
width={1000}
|
||||
footer={[
|
||||
<div key="footer" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<span>已选择 <b style={{ color: '#1677ff' }}>{selected.length}</b> 条指摘</span>
|
||||
<Space>
|
||||
<Button onClick={props.onCancel}>取消</Button>
|
||||
<Button type="primary" icon={<ThunderboltOutlined />} loading={generating} onClick={generate}>
|
||||
生成 AI 分析
|
||||
</Button>
|
||||
</Space>
|
||||
</div>,
|
||||
]}
|
||||
title={
|
||||
<div>
|
||||
选择指摘生成 AI 分析
|
||||
<Tag style={{ marginLeft: 8, fontWeight: 400 }}>可多选</Tag>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={issueForm} layout="inline" style={{ rowGap: 8, marginBottom: 12 }}>
|
||||
<Form.Item name="status" label="指摘状态">
|
||||
<Select allowClear placeholder="全部" style={{ width: 130 }} options={ISSUE_STATUS_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase" label="工程阶段">
|
||||
<Select allowClear placeholder="全部" style={{ width: 120 }} options={ISSUE_PHASE_OPTIONS.map(p => ({ label: p, value: p }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="departmentId" label="归属部门">
|
||||
<Select allowClear placeholder="全部" style={{ width: 130 }} options={props.departments.map(d => ({ label: d.name, value: d.id }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="keyword" label="搜索">
|
||||
<Input placeholder="ID / 标题" allowClear style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="dateRange" label="创建时间">
|
||||
<RangePicker format="YYYY/MM/DD" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { issueForm.resetFields(); loadIssues({ page: 1, pageSize }) }}>重置</Button>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={() => loadIssues({ page: 1, pageSize })}>查询</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
dataSource={issues}
|
||||
columns={issueColumns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
rowSelection={{
|
||||
selectedRowKeys: selected.map(s => s.id),
|
||||
onChange: (_keys, rows) => setSelected(rows),
|
||||
}}
|
||||
pagination={{
|
||||
current: page, total, pageSize,
|
||||
onChange: (p) => loadIssues({ page: p, pageSize }),
|
||||
showTotal: t => `共 ${t} 条`,
|
||||
}}
|
||||
style={{ maxHeight: 420, overflowY: 'auto' }}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Typography, Card, Row, Col, Statistic, Space
|
||||
} from 'antd'
|
||||
import {
|
||||
ThunderboltOutlined, BarChartOutlined,
|
||||
PieChartOutlined, LineChartOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Line, Pie, Column } from '@ant-design/charts'
|
||||
import {
|
||||
aiAnalysisApi, OverviewStats
|
||||
} from './services'
|
||||
|
||||
const ANALYSIS_LABELS: Record<string, string> = {
|
||||
pending: '待处理', processing: '分析中', completed: '已完成', failed: '失败'
|
||||
}
|
||||
|
||||
export default function AiOverviewTab() {
|
||||
const [stats, setStats] = useState<OverviewStats | null>(null)
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.overviewStats(14)
|
||||
setStats(res.data)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
}, [])
|
||||
|
||||
const statusCards = [
|
||||
{ title: '指摘总数', value: stats?.totalIssues ?? 0, color: '#1677ff', suffix: '条' },
|
||||
{ title: '已分析指摘', value: stats?.analyzedCount ?? 0, color: '#52c41a', suffix: '条' },
|
||||
{ title: '未分析指摘', value: stats?.unanalyzedCount ?? 0, color: '#fa8c16', suffix: '条' },
|
||||
{ title: '分析覆盖率', value: stats?.coverageRate ?? 0, color: '#722ed1', suffix: '%' },
|
||||
]
|
||||
|
||||
const lineData = (stats?.dailyTrend || []).flatMap(d => [
|
||||
{ date: d.date.slice(5), type: '已完成', value: d.completed },
|
||||
{ date: d.date.slice(5), type: '失败', value: d.failed },
|
||||
])
|
||||
|
||||
const statusPieData = (stats?.statusDistribution || []).map(s => ({
|
||||
type: ANALYSIS_LABELS[s.name] || s.name,
|
||||
value: s.value,
|
||||
}))
|
||||
|
||||
const categoryPieData = (stats?.categoryDistribution || []).map(s => ({
|
||||
type: s.name,
|
||||
value: s.value,
|
||||
}))
|
||||
|
||||
const columnData = (stats?.departmentDistribution || []).map(s => ({
|
||||
name: s.name,
|
||||
value: s.value,
|
||||
}))
|
||||
|
||||
const lineConfig = {
|
||||
xField: 'date',
|
||||
yField: 'value',
|
||||
seriesField: 'type',
|
||||
colorField: 'type',
|
||||
height: 280,
|
||||
smooth: true,
|
||||
scale: { y: { nice: true } },
|
||||
axis: { y: { title: false }, x: { title: false } },
|
||||
}
|
||||
|
||||
const pieConfig = {
|
||||
angleField: 'value',
|
||||
colorField: 'type',
|
||||
height: 280,
|
||||
innerRadius: 0.6,
|
||||
label: { text: 'value' },
|
||||
legend: { color: { position: 'bottom' } },
|
||||
tooltip: {
|
||||
items: [
|
||||
(arg: any) => ({ name: arg.type, value: arg.value })
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
const columnConfig = {
|
||||
xField: 'name',
|
||||
yField: 'value',
|
||||
height: 280,
|
||||
axis: { y: { title: false }, x: { title: false } },
|
||||
style: { maxWidth: 40 },
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Row gutter={16}>
|
||||
{statusCards.map((c, i) => (
|
||||
<Col span={4} key={i}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={c.title}
|
||||
value={c.value}
|
||||
suffix={<span style={{ fontSize: 12, color: '#999' }}>{c.suffix}</span>}
|
||||
valueStyle={{ color: c.color, fontWeight: 600 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="分析成功率"
|
||||
value={stats?.successRate ?? 0}
|
||||
suffix={<span style={{ fontSize: 12, color: '#999' }}>%</span>}
|
||||
valueStyle={{ color: (stats?.successRate ?? 0) >= 80 ? '#52c41a' : '#fa8c16', fontWeight: 600 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title={<Space><PieChartOutlined style={{ color: '#1677ff' }} />根因分类分布</Space>}>
|
||||
{categoryPieData.length > 0
|
||||
? <Pie {...pieConfig} data={categoryPieData} />
|
||||
: <EmptyChart text="暂无已分析的分类数据" />}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={<Space><PieChartOutlined style={{ color: '#722ed1' }} />分析状态分布</Space>}>
|
||||
{statusPieData.length > 0
|
||||
? <Pie {...pieConfig} data={statusPieData} />
|
||||
: <EmptyChart text="暂无分析记录" />}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title={<Space><BarChartOutlined style={{ color: '#52c41a' }} />部门分析分布</Space>}>
|
||||
{columnData.length > 0
|
||||
? <Column {...columnConfig} data={columnData} />
|
||||
: <EmptyChart text="暂无已分析的部门数据" />}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title={<Space><LineChartOutlined style={{ color: '#fa8c16' }} />近 14 天分析趋势</Space>}>
|
||||
{lineData.length > 0
|
||||
? <Line {...lineConfig} data={lineData} />
|
||||
: <EmptyChart text="暂无分析记录" />}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyChart({ text }: { text: string }) {
|
||||
return (
|
||||
<div style={{ height: 280, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#bbb' }}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import request from '../../request'
|
||||
|
||||
export interface AiAnalysisRecord {
|
||||
id: number
|
||||
issueId: number
|
||||
issueNo: string
|
||||
issueTitle: string
|
||||
departmentName?: string
|
||||
category: string
|
||||
keywords: string
|
||||
extractedKeywords: string
|
||||
rootCause: string
|
||||
suggestion: string
|
||||
status: string
|
||||
helpfulCount: number
|
||||
promptTemplateId: string
|
||||
promptVersion: number
|
||||
modelProvider: string
|
||||
modelName: string
|
||||
errorMessage: string
|
||||
startedAt: string
|
||||
completedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface BatchGenerateParams {
|
||||
issueIds?: number[]
|
||||
departmentId?: string
|
||||
status?: string
|
||||
phase?: string
|
||||
}
|
||||
|
||||
export interface RecordQuery {
|
||||
page: number
|
||||
pageSize: number
|
||||
id?: number
|
||||
issueId?: number
|
||||
departmentId?: number
|
||||
status?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface DepartmentOption {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface OverviewStats {
|
||||
totalIssues: number
|
||||
analyzedCount: number
|
||||
unanalyzedCount: number
|
||||
coverageRate: number
|
||||
successRate: number
|
||||
dailyTrend: { date: string; completed: number; failed: number }[]
|
||||
statusDistribution: { name: string; value: number }[]
|
||||
categoryDistribution: { name: string; value: number }[]
|
||||
departmentDistribution: { name: string; value: number }[]
|
||||
}
|
||||
|
||||
export interface UnanalyzedIssue {
|
||||
id: number
|
||||
issueNo: string
|
||||
title: string
|
||||
status: string
|
||||
priority: string
|
||||
phase: string
|
||||
departmentName: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface UnanalyzedQuery {
|
||||
page: number
|
||||
pageSize: number
|
||||
status?: string
|
||||
phase?: string
|
||||
departmentId?: number
|
||||
keyword?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface IssueListItem {
|
||||
id: number
|
||||
issueNo: string
|
||||
title: string
|
||||
status: string
|
||||
priority: string
|
||||
phase: string
|
||||
departmentId: number
|
||||
departmentName: string
|
||||
assigneeName: string
|
||||
deadline: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface IssueListQuery {
|
||||
page: number
|
||||
pageSize: number
|
||||
status?: string
|
||||
phase?: string
|
||||
departmentId?: number
|
||||
keyword?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export const aiAnalysisApi = {
|
||||
batchGenerate: (params: BatchGenerateParams) =>
|
||||
request.post('/ai/batch-generate', params),
|
||||
|
||||
records: (params: RecordQuery) =>
|
||||
request.get('/ai/records', { params }),
|
||||
|
||||
running: () =>
|
||||
request.get('/ai/records/running'),
|
||||
|
||||
callLogs: () =>
|
||||
request.get('/ai/call-logs'),
|
||||
|
||||
exportRecords: (params: RecordQuery) =>
|
||||
request.get('/ai/records/export', { params, responseType: 'blob' }),
|
||||
|
||||
feedback: (id: number, isHelpful: boolean, comment?: string) =>
|
||||
request.post(`/ai/records/${id}/feedback`, { isHelpful, comment }),
|
||||
|
||||
overviewStats: (days?: number) =>
|
||||
request.get('/ai/overview-stats', { params: { days } }),
|
||||
|
||||
unanalyzedIssues: (params: UnanalyzedQuery) =>
|
||||
request.get('/ai/unanalyzed', { params }),
|
||||
}
|
||||
|
||||
export const departmentApi = {
|
||||
list: () => request.get('/departments'),
|
||||
}
|
||||
|
||||
export const issuesApi = {
|
||||
list: (params: IssueListQuery) =>
|
||||
request.get('/issues', { params }),
|
||||
}
|
||||
|
||||
export const ISSUE_PRIORITY_OPTIONS = [
|
||||
{ label: '高', value: 'high' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '低', value: 'low' },
|
||||
]
|
||||
|
||||
export const ISSUE_STATUS_OPTIONS = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待处理', value: 'open' },
|
||||
{ label: '进行中', value: 'in_progress' },
|
||||
{ label: '已解决', value: 'resolved' },
|
||||
{ label: '已验证', value: 'verified' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已驳回', value: 'rejected' },
|
||||
]
|
||||
|
||||
export const ISSUE_PHASE_OPTIONS = ['需求', '设计', '编码', '测试', '部署', '运维']
|
||||
|
||||
export const ANALYSIS_STATUS_OPTIONS = [
|
||||
{ label: '待处理', value: 'pending' },
|
||||
{ label: '分析中', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
]
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
Card, Table, Button, Space, Tag, Input, Select, Form, DatePicker, message
|
||||
} from 'antd'
|
||||
import { ThunderboltOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { aiAnalysisApi, departmentApi, UnanalyzedIssue, UnanalyzedQuery } from './services'
|
||||
import { STATUS_LABELS, STATUS_COLORS, PRIORITY_LABELS, PRIORITY_COLORS } from '../../constants/issue'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
const pageSize = 10
|
||||
|
||||
export default function UnanalyzedTab() {
|
||||
const [items, setItems] = useState<UnanalyzedIssue[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selected, setSelected] = useState<UnanalyzedIssue[]>([])
|
||||
const [analyzing, setAnalyzing] = useState(false)
|
||||
const [departments, setDepartments] = useState<{ id: number; name: string }[]>([])
|
||||
const [filterForm] = Form.useForm()
|
||||
const pollTimer = useRef<number | null>(null)
|
||||
|
||||
const loadUnanalyzed = async (q: UnanalyzedQuery) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.unanalyzedIssues(q)
|
||||
setItems(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(q.page)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadUnanalyzed({ page: 1, pageSize })
|
||||
departmentApi.list().then((res: any) => setDepartments(res.data || [])).catch(() => {})
|
||||
return () => { if (pollTimer.current) window.clearTimeout(pollTimer.current) }
|
||||
}, [])
|
||||
|
||||
const buildQuery = (p: number): UnanalyzedQuery => {
|
||||
const v = filterForm.getFieldsValue()
|
||||
return {
|
||||
page: p,
|
||||
pageSize,
|
||||
status: v.status,
|
||||
phase: v.phase,
|
||||
departmentId: v.departmentId,
|
||||
keyword: v.keyword,
|
||||
startDate: v.dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: v.dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => loadUnanalyzed(buildQuery(1))
|
||||
|
||||
const handleReset = () => {
|
||||
filterForm.resetFields()
|
||||
loadUnanalyzed({ page: 1, pageSize })
|
||||
}
|
||||
|
||||
const refreshAll = () => loadUnanalyzed(buildQuery(page))
|
||||
|
||||
const analyze = async (issueIds: number[]) => {
|
||||
if (issueIds.length === 0) {
|
||||
message.warning('请至少选择一条指摘')
|
||||
return
|
||||
}
|
||||
setAnalyzing(true)
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.batchGenerate({ issueIds })
|
||||
message.success(`已对 ${res.data?.submitted ?? issueIds.length} 条指摘启动 AI 分析`)
|
||||
setSelected([])
|
||||
if (pollTimer.current) window.clearTimeout(pollTimer.current)
|
||||
pollTimer.current = window.setTimeout(() => {
|
||||
refreshAll()
|
||||
if (pollTimer.current) window.clearTimeout(pollTimer.current)
|
||||
}, 3000)
|
||||
} catch { /* ignore */ }
|
||||
setAnalyzing(false)
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID', dataIndex: 'issueNo', key: 'issueNo', width: 150,
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontWeight: 700, color: '#2563eb' }}>{v}</span>
|
||||
},
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (s: string) => <Tag color={STATUS_COLORS[s] || 'default'}>{STATUS_LABELS[s] || s}</Tag>
|
||||
},
|
||||
{
|
||||
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
|
||||
render: (p: string) => <Tag color={PRIORITY_COLORS[p] || 'default'}>{PRIORITY_LABELS[p] || p}</Tag>
|
||||
},
|
||||
{ title: '工程阶段', dataIndex: 'phase', key: 'phase', width: 110, render: (v: string) => v || '-' },
|
||||
{ title: '归属部门', dataIndex: 'departmentName', key: 'departmentName', width: 110, render: (v: string) => v || '-' },
|
||||
{ title: '对应者', dataIndex: 'assigneeName', key: 'assigneeName', width: 100, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120,
|
||||
render: (v: string) => (v ? dayjs(v).format('MM-DD HH:mm') : '-')
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 90, align: 'right' as const,
|
||||
render: (_: any, r: UnanalyzedIssue) => (
|
||||
<Button type="link" size="small" icon={<ThunderboltOutlined />} loading={analyzing} onClick={() => analyze([r.id])}>
|
||||
分析
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ borderRadius: 16 }}
|
||||
title={
|
||||
<Space>
|
||||
<ThunderboltOutlined style={{ color: '#fa8c16' }} />
|
||||
<span>未分析指摘({total})</span>
|
||||
<span style={{ fontSize: 12, color: '#999', fontWeight: 400 }}>不含已生成分析结果的指摘,失败记录可在此重新分析</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={analyzing}
|
||||
disabled={selected.length === 0}
|
||||
onClick={() => analyze(selected.map(s => s.id))}
|
||||
>
|
||||
批量分析已选({selected.length})
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form form={filterForm} layout="inline" style={{ rowGap: 8, marginBottom: 12 }}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select allowClear placeholder="全部" style={{ width: 120 }}
|
||||
options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ label, value }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="departmentId" label="部门">
|
||||
<Select allowClear placeholder="全部" style={{ width: 130 }}
|
||||
options={departments.map(d => ({ label: d.name, value: d.id }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="phase" label="阶段">
|
||||
<Input placeholder="工程阶段" allowClear style={{ width: 120 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="keyword" label="搜索">
|
||||
<Input placeholder="编号 / 标题" allowClear style={{ width: 150 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="dateRange" label="创建时间">
|
||||
<RangePicker format="YYYY/MM/DD" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
<Table
|
||||
dataSource={items}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
rowSelection={{
|
||||
selectedRowKeys: selected.map(s => s.id),
|
||||
onChange: (_keys, rows) => setSelected(rows),
|
||||
}}
|
||||
pagination={{
|
||||
current: page, total, pageSize,
|
||||
onChange: (p) => loadUnanalyzed(buildQuery(p)),
|
||||
showTotal: t => `共 ${t} 条`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
import {
|
||||
Typography, Upload, Table, Button, Card, Space, Tag, message,
|
||||
Modal, Statistic, Alert, Spin, Tooltip
|
||||
} from 'antd'
|
||||
import {
|
||||
DownloadOutlined, ArrowRightOutlined, CheckCircleOutlined,
|
||||
CloseCircleOutlined, InboxOutlined, RobotOutlined,
|
||||
CheckOutlined, ThunderboltOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { importApi, ImportRow, ImportPreview, ImportRecord, ImportSuggestion } from '../../api/system'
|
||||
|
||||
const validPriority = new Set(['high', 'medium', 'low'])
|
||||
|
||||
const priorityZh: Record<string, { label: string; color: string }> = {
|
||||
high: { label: '高', color: 'red' },
|
||||
medium: { label: '中', color: 'orange' },
|
||||
low: { label: '低', color: 'green' },
|
||||
}
|
||||
|
||||
const rowErrors = (r: ImportRow): string[] => {
|
||||
const errs: string[] = []
|
||||
if (!r.title?.trim()) errs.push('标题必填')
|
||||
if (r.priority?.trim() && !validPriority.has(r.priority.trim().toLowerCase())) errs.push('优先级必须是 high/medium/low')
|
||||
if (r.deadline?.trim() && !dayjs(r.deadline).isValid()) errs.push('期限格式不正确')
|
||||
return errs
|
||||
}
|
||||
|
||||
const recompute = (rows: ImportRow[]) => {
|
||||
let valid = 0
|
||||
for (const r of rows) {
|
||||
r.errors = rowErrors(r)
|
||||
r.status = r.errors.length === 0 ? 'ok' : 'error'
|
||||
if (r.errors.length === 0) valid++
|
||||
}
|
||||
return { rows, validCount: valid, errorCount: rows.length - valid }
|
||||
}
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
phase: '工程阶段',
|
||||
priority: '优先级',
|
||||
title: '标题',
|
||||
deadline: '期限',
|
||||
}
|
||||
|
||||
const unFilledTag = <span style={{ background: '#fee2e2', color: '#ef4444', padding: '0 6px', borderRadius: 4, fontSize: 11, fontWeight: 700 }}>未填</span>
|
||||
|
||||
const tooltipStyle: CSSProperties = {
|
||||
background: '#fff', color: '#374151',
|
||||
border: '1px solid #eef2f7', borderRadius: 8,
|
||||
boxShadow: '0 8px 24px rgba(15,23,42,.12)', padding: '8px 12px',
|
||||
}
|
||||
|
||||
const Ellipsis = ({ value, fieldName, tooltipText, style, width = 200 }: {
|
||||
value?: string
|
||||
fieldName?: string
|
||||
tooltipText?: string
|
||||
style?: CSSProperties
|
||||
width?: number
|
||||
}) => {
|
||||
const text = tooltipText ?? value
|
||||
return (
|
||||
<Tooltip
|
||||
color="#fff"
|
||||
overlayInnerStyle={tooltipStyle}
|
||||
title={text ? (
|
||||
<div style={{ maxWidth: 320, fontSize: 12 }}>
|
||||
{fieldName && <div style={{ fontSize: 10, color: '#ea580c', fontWeight: 700, marginBottom: 4 }}>{fieldName}</div>}
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{text}</div>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
<span style={{
|
||||
maxWidth: width, overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap', display: 'inline-block', verticalAlign: 'bottom', ...style,
|
||||
}}>
|
||||
{value || '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default function BatchInputPage() {
|
||||
const [preview, setPreview] = useState<ImportPreview | null>(null)
|
||||
const [fileName, setFileName] = useState('')
|
||||
const [importing, setImporting] = useState(false)
|
||||
|
||||
const [suggestions, setSuggestions] = useState<ImportSuggestion[]>([])
|
||||
const [applied, setApplied] = useState<Set<string>>(new Set())
|
||||
const [agentLoading, setAgentLoading] = useState(false)
|
||||
const [agentEngine, setAgentEngine] = useState('')
|
||||
const [agentUsable, setAgentUsable] = useState(true)
|
||||
const [agentUsableReason, setAgentUsableReason] = useState('')
|
||||
|
||||
const [records, setRecords] = useState<ImportRecord[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [errorOpen, setErrorOpen] = useState(false)
|
||||
const [errorText, setErrorText] = useState('')
|
||||
|
||||
const loadRecords = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await importApi.records(p)
|
||||
setRecords(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { loadRecords() }, [])
|
||||
|
||||
const handleTemplate = async () => {
|
||||
try {
|
||||
const res: any = await importApi.template()
|
||||
const url = URL.createObjectURL(res as Blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'レビュー記録表.xlsx'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const applySuggestion = (s: ImportSuggestion) => {
|
||||
if (!preview) return
|
||||
const key = `${s.rowNo}:${s.field}`
|
||||
if (applied.has(key) || !s.suggested) return
|
||||
const rows = preview.rows.map((r) => {
|
||||
if (r.rowNo === s.rowNo && s.field && s.suggested) {
|
||||
return { ...r, [s.field]: s.suggested }
|
||||
}
|
||||
return r
|
||||
})
|
||||
const next = recompute(rows)
|
||||
setPreview({ ...preview, ...next })
|
||||
setApplied((prev) => new Set(prev).add(key))
|
||||
}
|
||||
|
||||
const applyAll = () => {
|
||||
if (!preview || suggestions.length === 0) return
|
||||
let rows = preview.rows
|
||||
const nextApplied = new Set(applied)
|
||||
for (const s of suggestions) {
|
||||
const key = `${s.rowNo}:${s.field}`
|
||||
if (nextApplied.has(key) || !s.suggested) continue
|
||||
rows = rows.map((r) => (r.rowNo === s.rowNo && s.field ? { ...r, [s.field]: s.suggested } : r))
|
||||
nextApplied.add(key)
|
||||
}
|
||||
const next = recompute(rows)
|
||||
setPreview({ ...preview, ...next })
|
||||
setApplied(nextApplied)
|
||||
}
|
||||
|
||||
const handlePreview = async (file: File) => {
|
||||
if (!/\.(xlsx|xls)$/i.test(file.name)) {
|
||||
message.error('仅支持 .xlsx / .xls 文件')
|
||||
return false
|
||||
}
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
message.error('单个文件不能超过 50MB')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const res: any = await importApi.preview(file)
|
||||
const p: ImportPreview = res.data
|
||||
setPreview(p)
|
||||
setFileName(file.name)
|
||||
setSuggestions([])
|
||||
setApplied(new Set())
|
||||
setAgentEngine('')
|
||||
setAgentUsable(true)
|
||||
setAgentUsableReason('')
|
||||
if (p.headerValid === false) {
|
||||
setAgentUsable(false)
|
||||
setAgentUsableReason('导入文件的表头与标准的「レビュー記録表」模板不一致,不是标准模板。请下载并使用正确的模板后重新上传。')
|
||||
message.warning('不是标准的レビュー記録表模板,请使用正确模板')
|
||||
return false
|
||||
}
|
||||
message.success('解析完成,Agent 正在智能校验数据...')
|
||||
setAgentLoading(true)
|
||||
try {
|
||||
const ar: any = await importApi.aiValidate(p.rows)
|
||||
setSuggestions(ar.data?.suggestions || [])
|
||||
setAgentEngine(ar.data?.engine || '')
|
||||
setAgentUsable(ar.data?.usable ?? true)
|
||||
setAgentUsableReason(ar.data?.usableReason || '')
|
||||
if (ar.data?.usable === false) {
|
||||
message.warning(ar.data?.usableReason || '该文件不能作为指摘表导入')
|
||||
} else if ((ar.data?.suggestions || []).length === 0) {
|
||||
message.success('Agent 校验完成,数据全部通过')
|
||||
}
|
||||
} catch {
|
||||
setSuggestions([])
|
||||
setAgentEngine('')
|
||||
setAgentUsable(true)
|
||||
setAgentUsableReason('')
|
||||
message.warning('AI 校验超时或不可用,已降级为规则校验')
|
||||
}
|
||||
setAgentLoading(false)
|
||||
} catch (err: any) {
|
||||
setAgentLoading(false)
|
||||
const msg = err?.response?.data?.message
|
||||
if (msg) {
|
||||
message.error(msg)
|
||||
} else if (err?.message?.includes('timeout')) {
|
||||
message.error('解析超时,请稍后重试')
|
||||
} else {
|
||||
message.error('解析失败,请使用标准模板')
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!preview) return
|
||||
setImporting(true)
|
||||
try {
|
||||
const res: any = await importApi.confirm(fileName, preview.rows)
|
||||
const r = res.data
|
||||
message.success(`导入完成:成功 ${r.successCount} 条,失败 ${r.failCount} 条`)
|
||||
setPreview(null)
|
||||
setFileName('')
|
||||
setSuggestions([])
|
||||
setApplied(new Set())
|
||||
setAgentEngine('')
|
||||
setAgentUsable(true)
|
||||
setAgentUsableReason('')
|
||||
loadRecords(1)
|
||||
} catch {
|
||||
message.error('导入失败')
|
||||
}
|
||||
setImporting(false)
|
||||
}
|
||||
|
||||
const statusTag = (s: string) => {
|
||||
const map: Record<string, { color: string; label: string }> = {
|
||||
success: { color: 'green', label: '已完成' },
|
||||
partial: { color: 'orange', label: '部分成功' },
|
||||
failed: { color: 'red', label: '失败' },
|
||||
}
|
||||
const item = map[s] || { color: 'default', label: s }
|
||||
return <Tag color={item.color}>{item.label}</Tag>
|
||||
}
|
||||
|
||||
const correctedCell = (rowNo: number | undefined, field: string, value?: string) => {
|
||||
if (rowNo != null && applied.has(`${rowNo}:${field}`)) {
|
||||
return <span style={{ color: '#ef4444', fontWeight: 700, textDecoration: 'underline wavy #fca5a5' }}>{value}</span>
|
||||
}
|
||||
return value || '-'
|
||||
}
|
||||
|
||||
const previewColumns: ColumnsType<ImportRow> = [
|
||||
{
|
||||
title: '行号', dataIndex: 'rowNo', key: 'rowNo', width: 56,
|
||||
render: (v?: number) => <span style={{ fontWeight: 700, color: '#9ca3af' }}>{v}</span>,
|
||||
},
|
||||
{
|
||||
title: '标题', dataIndex: 'title', key: 'title', width: 220,
|
||||
render: (v: string) => (v
|
||||
? <Ellipsis value={v} fieldName="标题" width={200} style={{ fontWeight: 500, color: '#374151' }} />
|
||||
: unFilledTag),
|
||||
},
|
||||
{
|
||||
title: '工程阶段', dataIndex: 'phase', key: 'phase', width: 110,
|
||||
render: (v: string, r) => {
|
||||
const appliedNow = r.rowNo != null && applied.has(`${r.rowNo}:phase`)
|
||||
if (appliedNow) {
|
||||
return <span style={{ color: '#ef4444', fontWeight: 700, textDecoration: 'underline wavy #fca5a5' }}>{v}</span>
|
||||
}
|
||||
return v ? <Ellipsis value={v} fieldName="工程阶段" width={90} /> : <span style={{ color: '#9ca3af' }}>-</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '优先级', dataIndex: 'priority', key: 'priority', width: 84,
|
||||
render: (v: string | undefined, r: ImportRow) => {
|
||||
if (!v) return unFilledTag
|
||||
const zh = priorityZh[v.toLowerCase()] || { label: v, color: 'default' }
|
||||
const isApplied = r.rowNo != null && applied.has(`${r.rowNo}:priority`)
|
||||
return <Tag color={zh.color} style={isApplied ? { fontWeight: 700 } : undefined}>{zh.label}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '期限', dataIndex: 'deadline', key: 'deadline', width: 108,
|
||||
render: (v?: string) => (v
|
||||
? <Ellipsis value={dayjs(v).format('YYYY-MM-DD')} tooltipText={v} fieldName="期限" width={90} />
|
||||
: <span style={{ color: '#9ca3af' }}>-</span>),
|
||||
},
|
||||
{
|
||||
title: '实施日', dataIndex: 'reviewDate', key: 'reviewDate', width: 108,
|
||||
render: (v?: string) => (v
|
||||
? <Ellipsis value={dayjs(v).format('YYYY-MM-DD')} tooltipText={v} fieldName="实施日" width={90} />
|
||||
: <span style={{ color: '#9ca3af' }}>-</span>),
|
||||
},
|
||||
{
|
||||
title: '描述', dataIndex: 'description', key: 'description', width: 220,
|
||||
render: (v?: string, r?: ImportRow) => (v
|
||||
? <Ellipsis value={v} tooltipText={v} fieldName="描述" width={200} />
|
||||
: <span style={{ color: '#9ca3af' }}>-</span>),
|
||||
},
|
||||
{
|
||||
title: '担当者', dataIndex: 'assigneeUserid', key: 'assigneeUserid', width: 90,
|
||||
render: (_: string, r) => (r.assigneeUserid
|
||||
? <Ellipsis value={r.assigneeUserid} tooltipText={`工号:${r.assigneeUserid}${r.departmentName ? `\n部门:${r.departmentName}` : ''}`} fieldName="担当者" width={80} />
|
||||
: <span style={{ color: '#9ca3af' }}>未分配</span>),
|
||||
},
|
||||
{ title: '部门', dataIndex: 'departmentName', key: 'departmentName', width: 90, render: (v?: string) => (v ? <Ellipsis value={v} fieldName="部门" width={80} /> : <span style={{ color: '#9ca3af' }}>-</span>) },
|
||||
{
|
||||
title: '校验', dataIndex: 'status', key: 'status', width: 86,
|
||||
render: (s?: string) => s === 'ok'
|
||||
? <span style={{ color: '#52c41a' }}><CheckCircleOutlined /> 已就绪</span>
|
||||
: <span style={{ color: '#fa8c16' }}><CloseCircleOutlined /> 待修正</span>,
|
||||
},
|
||||
]
|
||||
|
||||
const recordColumns: ColumnsType<ImportRecord> = [
|
||||
{
|
||||
title: '导入时间', dataIndex: 'createdAt', key: 'createdAt', width: 170,
|
||||
render: (v?: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-'),
|
||||
},
|
||||
{ title: '文件名', dataIndex: 'fileName', key: 'fileName', ellipsis: true },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 110 },
|
||||
{ title: '总条数', dataIndex: 'totalCount', key: 'totalCount', width: 80 },
|
||||
{ title: '成功', dataIndex: 'successCount', key: 'successCount', width: 70, render: (v: number) => <span style={{ color: '#52c41a' }}>{v}</span> },
|
||||
{ title: '失败', dataIndex: 'failCount', key: 'failCount', width: 70, render: (v: number) => <span style={{ color: v > 0 ? '#f5222d' : '#999' }}>{v}</span> },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (s: string) => statusTag(s) },
|
||||
{
|
||||
title: '操作', key: 'action', width: 110,
|
||||
render: (_: unknown, r: ImportRecord) => (
|
||||
<Button
|
||||
type="link" size="small" disabled={!r.errorLog}
|
||||
onClick={() => { setErrorText(r.errorLog || ''); setErrorOpen(true) }}
|
||||
>
|
||||
错误详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const pendingSuggestions = useMemo(() => suggestions.filter((s) => !applied.has(`${s.rowNo}:${s.field}`)), [suggestions, applied])
|
||||
const fixSuggestions = useMemo(() => pendingSuggestions.filter((s) => s.level !== 'error' && s.suggested), [pendingSuggestions])
|
||||
const errorSuggestions = useMemo(() => pendingSuggestions.filter((s) => s.level === 'error'), [pendingSuggestions])
|
||||
const hasFix = fixSuggestions.length > 0
|
||||
const allPassed = preview != null && preview.errorCount === 0 && suggestions.length === 0
|
||||
|
||||
const agentBadge = () => {
|
||||
if (agentLoading) return <Tag color="processing">校验中...</Tag>
|
||||
if (!preview) return <Tag>待上传</Tag>
|
||||
if (agentUsable === false) return <Tag color="error">文件不可用</Tag>
|
||||
if (preview.errorCount > 0) return <Tag color="orange">发现 {preview.errorCount} 条待修正</Tag>
|
||||
if (suggestions.length > 0) return <Tag color="orange">发现 {pendingSuggestions.length} 个建议</Tag>
|
||||
return <Tag color="green">校验通过</Tag>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>指摘批量录入</Typography.Title>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleTemplate}>下载导入模板</Button>
|
||||
</div>
|
||||
|
||||
<Card size="small" style={{ marginBottom: 16 }}>
|
||||
<Upload.Dragger
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
beforeUpload={handlePreview}
|
||||
>
|
||||
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||||
<p className="ant-upload-hint">支持 .xlsx, .xls 文件,单个文件不超过 50MB · 上传后 Agent 自动智能校验</p>
|
||||
</Upload.Dragger>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(320px, 1fr) 2fr', gap: 16, marginBottom: 16, alignItems: 'start' }}>
|
||||
{/* Agent 校验结果面板 */}
|
||||
<div style={{
|
||||
background: '#fff', borderRadius: 16, border: '1px solid #f1f5f9',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.03)', padding: 20,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<RobotOutlined style={{ color: '#1a73e8', fontSize: 18 }} />
|
||||
<Typography.Text strong>Agent 校验结果</Typography.Text>
|
||||
</div>
|
||||
{agentBadge()}
|
||||
</div>
|
||||
|
||||
{!preview ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px 12px' }}>
|
||||
<RobotOutlined style={{ fontSize: 36, color: '#e5e7eb' }} />
|
||||
<p style={{ color: '#9ca3af', fontSize: 13, marginTop: 12, marginBottom: 0 }}>
|
||||
上传 Excel 文件后<br />Agent 将自动校验数据
|
||||
</p>
|
||||
</div>
|
||||
) : agentLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: 24 }}>
|
||||
<Spin />
|
||||
<p style={{ color: '#999', fontSize: 12, marginTop: 8 }}>Agent 正在扫描数据并生成修正建议...</p>
|
||||
</div>
|
||||
) : (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
{agentUsable === false && (
|
||||
<div style={{ padding: 16, background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 12, display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
||||
<CloseCircleOutlined style={{ color: '#ef4444', fontSize: 18, marginTop: 2 }} />
|
||||
<div>
|
||||
<Typography.Text strong style={{ color: '#b91c1c' }}>该文件不能作为指摘表使用</Typography.Text>
|
||||
<div style={{ color: '#dc2626', fontSize: 12, marginTop: 4 }}>{agentUsableReason}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorSuggestions.map((s) => {
|
||||
const key = `${s.rowNo}:${s.field}`
|
||||
const label = s.fieldName || fieldLabels[s.field || ''] || s.field
|
||||
return (
|
||||
<div key={key} style={{
|
||||
padding: 16, background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 12,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<span style={{ position: 'absolute', right: 12, top: 12, fontSize: 10, fontWeight: 700, color: '#f87171' }}>
|
||||
第 {s.rowNo} 行
|
||||
</span>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#b91c1c' }}>
|
||||
{label}:{s.original || '未填写'}
|
||||
</p>
|
||||
<p style={{ margin: '4px 0 8px', fontSize: 11, color: 'rgba(185,28,28,.7)' }}>原因:{s.reason}</p>
|
||||
<Button
|
||||
block size="small" disabled
|
||||
style={{ marginTop: 10, background: '#fef2f2', borderColor: '#fecaca', color: '#9ca3af', fontSize: 11, fontWeight: 700, borderRadius: 8 }}
|
||||
>
|
||||
无法自动修正
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{fixSuggestions.map((s) => {
|
||||
const key = `${s.rowNo}:${s.field}`
|
||||
const appliedNow = applied.has(key)
|
||||
const label = s.fieldName || fieldLabels[s.field || ''] || s.field
|
||||
return (
|
||||
<div key={key} style={{
|
||||
padding: 16, background: '#fff7ed', border: '1px solid #fed7aa', borderRadius: 12,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<span style={{ position: 'absolute', right: 12, top: 12, fontSize: 10, fontWeight: 700, color: '#fb923c' }}>
|
||||
第 {s.rowNo} 行
|
||||
</span>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#c2410c' }}>
|
||||
{label}:{s.original ? `${s.original} → ` : ''}{s.suggested}
|
||||
</p>
|
||||
<p style={{ margin: '4px 0 8px', fontSize: 11, color: 'rgba(194,65,12,.7)' }}>原因:{s.reason}</p>
|
||||
<Button
|
||||
block size="small" disabled={appliedNow}
|
||||
onClick={() => applySuggestion(s)}
|
||||
style={{
|
||||
marginTop: 10, background: appliedNow ? '#f0fdf4' : '#fff',
|
||||
borderColor: appliedNow ? '#bbf7d0' : '#fdba74', color: appliedNow ? '#16a34a' : '#ea580c',
|
||||
fontSize: 11, fontWeight: 700, borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{appliedNow ? (<><CheckOutlined /> 已应用</>) : '应用修正'}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{allPassed && agentUsable !== false && (
|
||||
<div style={{ padding: 16, background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CheckCircleOutlined style={{ color: '#22c55e', fontSize: 18 }} />
|
||||
<div>
|
||||
<Typography.Text strong style={{ color: '#15803d' }}>所有数据校验通过</Typography.Text>
|
||||
<div style={{ color: '#4ade80', fontSize: 12 }}>Agent 已完成自动关联与去重校验。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasFix && (
|
||||
<Button
|
||||
block
|
||||
style={{
|
||||
background: '#111827', color: '#fff', fontWeight: 700, height: 40,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
onClick={applyAll}
|
||||
>
|
||||
<ThunderboltOutlined /> 一键应用全部修正 ({fixSuggestions.length})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{agentEngine && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11, display: 'block', textAlign: 'center' }}>
|
||||
{agentEngine === 'ai' ? '引擎:AI 智能模型' : '引擎:规则引擎'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 预检表格 */}
|
||||
<Card
|
||||
size="small" className="preview-table"
|
||||
title={preview ? `预检数据预览 (${preview.total} 条) · ${fileName}` : '预检数据预览'}
|
||||
extra={preview && (
|
||||
<Space>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#9ca3af' }}>
|
||||
<span style={{ width: 8, height: 8, background: '#fb923c', borderRadius: '50%' }} /> 待修正
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#9ca3af' }}>
|
||||
<span style={{ width: 8, height: 8, background: '#4ade80', borderRadius: '50%' }} /> 已就绪
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
{preview && preview.errorCount > 0 && (
|
||||
<Alert
|
||||
type="warning" showIcon style={{ marginBottom: 12 }}
|
||||
message={`有 ${preview.errorCount} 行数据待修正`}
|
||||
description="可通过左侧 Agent 建议一键修正,或直接导入已就绪数据。"
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
rowKey={(r) => String(r.rowNo)}
|
||||
columns={previewColumns}
|
||||
dataSource={preview?.rows || []}
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 380 }}
|
||||
rowClassName={(r) => (r.status === 'error' ? 'import-row-error' : '')}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<div style={{ padding: '48px 0' }}>
|
||||
<InboxOutlined style={{ fontSize: 36, color: '#e5e7eb' }} />
|
||||
<p style={{ color: '#9ca3af', fontSize: 13, marginTop: 12, marginBottom: 0 }}>尚未上传文件</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginTop: 16 }}>
|
||||
{preview && (
|
||||
<Statistic title="已就绪" value={preview.validCount} valueStyle={{ fontSize: 16, color: '#16a34a' }} />
|
||||
)}
|
||||
<Button
|
||||
type="primary" icon={<ArrowRightOutlined />} loading={importing}
|
||||
disabled={!preview || preview.rows.length === 0 || agentUsable === false}
|
||||
onClick={handleConfirm}
|
||||
style={{ background: '#1a73e8', borderRadius: 12, height: 40, fontWeight: 700, boxShadow: '0 4px 12px rgba(26,115,232,.2)' }}
|
||||
>
|
||||
确认并正式导入 ({preview?.validCount ?? 0} 条)
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
size="small" title="历史导入记录"
|
||||
extra={<Typography.Text type="secondary">共 {total} 条</Typography.Text>}
|
||||
>
|
||||
<Table
|
||||
rowKey="id" loading={loading} columns={recordColumns} dataSource={records}
|
||||
size="middle"
|
||||
pagination={{
|
||||
current: page, pageSize: 20, total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => loadRecords(p),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="导入错误详情" open={errorOpen} footer={null}
|
||||
onCancel={() => setErrorOpen(false)} width={640}
|
||||
>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', maxHeight: 400, overflow: 'auto', background: '#fafafa', padding: 12, borderRadius: 6 }}>
|
||||
{errorText}
|
||||
</pre>
|
||||
</Modal>
|
||||
|
||||
<style>{`
|
||||
.preview-table .ant-table-thead > tr > th {
|
||||
background: #f9fafb !important;
|
||||
color: #9ca3af !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 11px;
|
||||
letter-spacing: .04em;
|
||||
border-bottom: 1px solid #f3f4f6 !important;
|
||||
}
|
||||
.preview-table .ant-table-tbody > tr:hover > td { background: #f9fafb !important; }
|
||||
.preview-table .ant-table-tbody > tr > td { border-bottom: 1px solid #f9fafb !important; }
|
||||
.import-row-error td { background: rgba(255, 237, 213, 0.4) !important; }
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Typography, Card, Row, Col, Statistic, List, Tag, Input, Button, Space, Modal } from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import {
|
||||
AlertOutlined, LoadingOutlined, CheckCircleOutlined, PlusOutlined,
|
||||
SendOutlined, InboxOutlined, RobotOutlined, OrderedListOutlined, EyeOutlined, CloseCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Line, Pie } from '@ant-design/charts'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getDashboardStats, DashboardStats } from './services'
|
||||
import { listIssues, executeAgent } from '../issues/services'
|
||||
import { STATUS_LABELS, ACTION_LABELS } from '../../constants/issue'
|
||||
|
||||
const statCards = [
|
||||
{ key: 'pendingCount', title: '待处理指摘', color: '#fa8c16', icon: <AlertOutlined /> },
|
||||
{ key: 'inProgressCount', title: '进行中指摘', color: '#1677ff', icon: <LoadingOutlined /> },
|
||||
{ key: 'monthlyClosedCount', title: '本月已完成', color: '#52c41a', icon: <CheckCircleOutlined /> },
|
||||
{ key: 'todayNewCount', title: '今日新增', color: '#722ed1', icon: <PlusOutlined /> }
|
||||
]
|
||||
|
||||
const QUICK_CMDS = [
|
||||
{ label: '催办逾期指摘', goal: '检索知识库相似案例,生成对应方案并催办逾期指摘' },
|
||||
{ label: '生成本周报告', goal: '生成本周指摘处理统计报告' },
|
||||
{ label: '分配待处理指摘', goal: '为待处理指摘分配对应者并生成对应方案' },
|
||||
{ label: '检索知识库', goal: '检索知识库相似案例,生成对应方案' }
|
||||
]
|
||||
|
||||
interface StreamItem {
|
||||
type: string
|
||||
content?: string
|
||||
tool?: string
|
||||
params?: string
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const navigate = useNavigate()
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [agentGoal, setAgentGoal] = useState('')
|
||||
const [agentRunning, setAgentRunning] = useState(false)
|
||||
const [planModalOpen, setPlanModalOpen] = useState(false)
|
||||
const [planId, setPlanId] = useState<number>()
|
||||
const [streamItems, setStreamItems] = useState<StreamItem[]>([])
|
||||
const [planRequiresApproval, setPlanRequiresApproval] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getDashboardStats().then((res: any) => setStats(res.data)).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleAgentCommand = async () => {
|
||||
const goal = agentGoal.trim()
|
||||
if (!goal) return message.warning('请输入指令')
|
||||
setAgentRunning(true)
|
||||
setPlanRequiresApproval(false)
|
||||
try {
|
||||
let contextId: number | undefined
|
||||
const issueNoMatch = goal.match(/ISSUE-\d{4}-\d{3,}/i)
|
||||
if (issueNoMatch) {
|
||||
try {
|
||||
const res: any = await listIssues({ page: 1, pageSize: 20, keyword: issueNoMatch[0] })
|
||||
const hit = (res.data?.items || []).find((i: any) => i.issueNo === issueNoMatch[0])
|
||||
contextId = hit?.id
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const res: any = await executeAgent(contextId, goal)
|
||||
const pid = res.data?.planId
|
||||
if (!pid) {
|
||||
message.success('Agent 已解析指令并开始执行')
|
||||
if (contextId) navigate(`/issues/${contextId}`)
|
||||
setAgentRunning(false)
|
||||
return
|
||||
}
|
||||
setPlanId(pid)
|
||||
setStreamItems([{ type: 'thought', content: '指令已提交,正在连接执行流…' }])
|
||||
setPlanModalOpen(true)
|
||||
setAgentGoal('')
|
||||
if (contextId) navigate(`/issues/${contextId}`)
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const es = new EventSource(`/api/v1/agent/plan/${pid}/stream?token=${token}`)
|
||||
es.onmessage = (event) => {
|
||||
let ev: any = {}
|
||||
try { ev = JSON.parse(event.data) } catch { return }
|
||||
switch (ev.type) {
|
||||
case 'thought':
|
||||
setStreamItems(prev => [...prev, { type: 'thought', content: ev.content }])
|
||||
break
|
||||
case 'action':
|
||||
setStreamItems(prev => [...prev, {
|
||||
type: 'action', tool: ev.tool, params: ev.params ? JSON.stringify(ev.params) : ''
|
||||
}])
|
||||
if (ev.approval) {
|
||||
setPlanRequiresApproval(true)
|
||||
setStreamItems(prev => [...prev, { type: 'thought', content: '写操作需人工审批,请前往审批中心处理。' }])
|
||||
}
|
||||
break
|
||||
case 'observation':
|
||||
setStreamItems(prev => [...prev, { type: 'observation', content: ev.result || '' }])
|
||||
break
|
||||
case 'result':
|
||||
setStreamItems(prev => [...prev, { type: 'result', content: ev.content || '方案已生成' }])
|
||||
es.close()
|
||||
setAgentRunning(false)
|
||||
break
|
||||
case 'error':
|
||||
setStreamItems(prev => [...prev, { type: 'error', content: ev.message || 'Agent 执行失败' }])
|
||||
es.close()
|
||||
setAgentRunning(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
setAgentRunning(false)
|
||||
}
|
||||
} catch { /* 错误由拦截器提示 */ }
|
||||
setAgentRunning(false)
|
||||
}
|
||||
|
||||
const lineData = (stats?.trend || []).flatMap(d => [
|
||||
{ date: d.date.slice(5), type: '新增', value: d.newCount },
|
||||
{ date: d.date.slice(5), type: '已解决', value: d.resolvedCount }
|
||||
])
|
||||
|
||||
const pieData = (stats?.statusDistribution || []).map(s => ({
|
||||
type: STATUS_LABELS[s.status] || s.status,
|
||||
value: s.count
|
||||
}))
|
||||
|
||||
const lineConfig = {
|
||||
xField: 'date',
|
||||
yField: 'value',
|
||||
seriesField: 'type',
|
||||
colorField: 'type',
|
||||
height: 320,
|
||||
smooth: true,
|
||||
scale: { y: { nice: true } },
|
||||
axis: { y: { title: false }, x: { title: false } }
|
||||
}
|
||||
|
||||
const pieConfig = {
|
||||
angleField: 'value',
|
||||
colorField: 'type',
|
||||
height: 280,
|
||||
innerRadius: 0.6,
|
||||
label: { text: 'value' },
|
||||
legend: { color: { position: 'bottom' } },
|
||||
tooltip: {
|
||||
items: [
|
||||
(arg: any) => ({ name: arg.type, value: arg.value })
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const insightColor: Record<string, string> = {
|
||||
'high-risk': '#fa8c16',
|
||||
suggestion: '#1677ff',
|
||||
reminder: '#52c41a'
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>工作台概览</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
{new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' })}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: '#e6f4ff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<RobotOutlined style={{ color: '#1677ff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong>Agent 快捷指令</Typography.Text>
|
||||
<div style={{ fontSize: 11, color: '#999' }}>输入自然语言指令,Agent 将检索知识库相似案例并生成对应方案</div>
|
||||
</div>
|
||||
<Tag color="green" style={{ marginLeft: 'auto' }}>在线</Tag>
|
||||
</Space>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
prefix={<SendOutlined style={{ color: '#bbb' }} />}
|
||||
placeholder="例如:查找知识库相似案例,生成对应方案"
|
||||
value={agentGoal}
|
||||
onChange={e => setAgentGoal(e.target.value)}
|
||||
onPressEnter={handleAgentCommand}
|
||||
/>
|
||||
<Button type="primary" loading={agentRunning} onClick={handleAgentCommand}>执行</Button>
|
||||
</Space.Compact>
|
||||
<div style={{ marginTop: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => navigate('/issues/new')}>新建指摘</Button>
|
||||
<Button size="small" icon={<OrderedListOutlined />} onClick={() => navigate('/batch-input')}>批量录入</Button>
|
||||
<span style={{ fontSize: 11, color: '#999', marginLeft: 4 }}>快捷指令:</span>
|
||||
{QUICK_CMDS.map(c => (
|
||||
<Tag key={c.label} style={{ cursor: 'pointer' }} onClick={() => setAgentGoal(c.goal)}>{c.label}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
{statCards.map((c, i) => {
|
||||
const card = (stats?.cards || []).find(x => x.key === c.key)
|
||||
return (
|
||||
<Col span={6} key={i}>
|
||||
<Card>
|
||||
<div style={{ width: 40, height: 40, borderRadius: 8, background: `${c.color}1a`, color: c.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20, marginBottom: 8 }}>
|
||||
{c.icon}
|
||||
</div>
|
||||
<Statistic title={c.title} value={card?.count ?? (stats as any)?.[c.key] ?? 0} />
|
||||
{card?.changeText && <div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>{card.changeText}</div>}
|
||||
{card?.suggestion && (
|
||||
<div style={{ fontSize: 12, color: c.color, background: `${c.color}14`, borderRadius: 6, padding: '6px 8px', marginTop: 8 }}>
|
||||
{card.suggestion}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={16}>
|
||||
<Card title="指摘处理趋势">
|
||||
<Line {...lineConfig} data={lineData} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Agent 洞察" style={{ height: '100%' }}>
|
||||
<List
|
||||
dataSource={stats?.insights || []}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ width: 4, height: 32, borderRadius: 2, background: insightColor[item.type] || '#fa8c16', marginTop: 4 }} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, marginBottom: 4 }}>{item.title}</div>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>{item.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title="最新动态" extra={<Button type="link" size="small" onClick={() => navigate('/issues')}>查看全部</Button>}>
|
||||
<List
|
||||
dataSource={stats?.recentActivities || []}
|
||||
renderItem={(a, i) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', background: i % 2 ? '#e6f4ff' : '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center', color: i % 2 ? '#1677ff' : '#999' }}>
|
||||
{i % 2 ? <RobotOutlined /> : <InboxOutlined />}
|
||||
</div>
|
||||
}
|
||||
title={<span>{a.userName || (i % 2 ? 'IMS Agent' : '')} <span style={{ fontWeight: i % 2 ? 600 : 400, color: i % 2 ? '#1677ff' : '#595959' }}>{ACTION_LABELS[a.action] || a.action}</span></span>}
|
||||
description={
|
||||
<div>
|
||||
<div>{a.issueNo} · {a.title}</div>
|
||||
<div style={{ fontSize: 11, color: '#bbb', marginTop: 2 }}>{a.createdAt?.replace('T', ' ').slice(0, 16)}</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="指摘状态分布">
|
||||
<Pie {...pieConfig} data={pieData} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title={`Agent 执行流${planId ? `(计划 #${planId})` : ''}`}
|
||||
open={planModalOpen}
|
||||
onCancel={() => { setPlanModalOpen(false); setPlanId(undefined); setStreamItems([]) }}
|
||||
footer={
|
||||
<Space>
|
||||
{planRequiresApproval && <Button type="primary" onClick={() => navigate('/system/agent-admin')}>去审批中心</Button>}
|
||||
<Button onClick={() => { setPlanModalOpen(false); setPlanId(undefined); setStreamItems([]) }}>关闭</Button>
|
||||
</Space>
|
||||
}
|
||||
width={640}
|
||||
>
|
||||
<div style={{ maxHeight: 420, overflowY: 'auto', fontSize: 13 }}>
|
||||
{streamItems.map((item, i) => {
|
||||
if (item.type === 'action') {
|
||||
return (
|
||||
<div key={i} style={{ borderLeft: '3px solid #fa8c16', background: '#fffbe6', borderRadius: 8, padding: 10, marginBottom: 8 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 12, color: '#d46b08', marginBottom: 4 }}>
|
||||
<EyeOutlined style={{ marginRight: 4 }} />行动:{item.tool}
|
||||
</div>
|
||||
{item.params && <div style={{ fontFamily: 'monospace', fontSize: 11, wordBreak: 'break-all' }}>{item.params}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const color = item.type === 'error' ? '#cf1322' : item.type === 'result' ? '#389e0d' : item.type === 'observation' ? '#666' : '#595959'
|
||||
const bg = item.type === 'error' ? '#fff1f0' : item.type === 'result' ? '#f6ffed' : item.type === 'observation' ? '#fafafa' : 'transparent'
|
||||
return (
|
||||
<div key={i} style={{ background: bg, borderRadius: 6, padding: item.type === 'result' || item.type === 'error' ? 10 : 4, marginBottom: 8, color }}>
|
||||
{item.type === 'error' && <CloseCircleOutlined style={{ marginRight: 4 }} />}
|
||||
{item.content}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import request from '../../request'
|
||||
|
||||
export interface DailyTrend {
|
||||
date: string
|
||||
newCount: number
|
||||
resolvedCount: number
|
||||
}
|
||||
|
||||
export interface StatusCount {
|
||||
status: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
userName: string
|
||||
action: string
|
||||
issueNo: string
|
||||
title: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Insight {
|
||||
type: string
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface StatCardInfo {
|
||||
key: string
|
||||
count: number
|
||||
changeText: string
|
||||
suggestion: string
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
pendingCount: number
|
||||
inProgressCount: number
|
||||
pendingConfirmCount: number
|
||||
closedCount: number
|
||||
todayNewCount: number
|
||||
monthlyClosedCount: number
|
||||
cards: StatCardInfo[]
|
||||
trend: DailyTrend[]
|
||||
statusDistribution: StatusCount[]
|
||||
recentActivities: Activity[]
|
||||
insights: Insight[]
|
||||
}
|
||||
|
||||
export const getDashboardStats = () =>
|
||||
request.get('/dashboard/stats')
|
||||
@@ -0,0 +1,285 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import {
|
||||
Form, Input, Select, InputNumber, DatePicker, Radio, Card, Row, Col, Button, Space, Typography, Tag
|
||||
} from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { FileTextOutlined, MessageOutlined, CheckCircleOutlined, SettingOutlined, BulbOutlined } from '@ant-design/icons'
|
||||
import dayjs from 'dayjs'
|
||||
import { getDepartments, suggestFields } from './services'
|
||||
import {
|
||||
PHASE_OPTIONS, SUB_PROJECT_OPTIONS, CATEGORY_OPTIONS, IMPACT_LEVEL_OPTIONS,
|
||||
USERS, PRIORITY_LABELS, STATUS_LABELS, STATUS_COLORS
|
||||
} from '../../constants/issue'
|
||||
|
||||
export interface IssueFormValues {
|
||||
title?: string
|
||||
description?: string
|
||||
phase?: string
|
||||
subProject?: string
|
||||
category?: string
|
||||
impactLevel?: string
|
||||
impactScope?: string
|
||||
deployment?: string
|
||||
pgmNo?: string
|
||||
reviewWorkload?: number
|
||||
responseWorkload?: number
|
||||
responseContent?: string
|
||||
ngReason?: string
|
||||
assigneeId?: number
|
||||
reviewerId?: number
|
||||
validatorId?: number
|
||||
priority?: string
|
||||
status?: string
|
||||
departmentId?: number
|
||||
deadline?: string
|
||||
responseCompletedAt?: string
|
||||
confirmAt?: string
|
||||
}
|
||||
|
||||
const userOptions = USERS.map(u => ({ label: u.name, value: u.id }))
|
||||
const priorityOptions = Object.entries(PRIORITY_LABELS).map(([v, l]) => ({ label: l, value: v }))
|
||||
|
||||
interface Props {
|
||||
initialValues?: Partial<IssueFormValues>
|
||||
submitting?: boolean
|
||||
mode?: 'create' | 'edit'
|
||||
onSubmit: (values: IssueFormValues) => void
|
||||
}
|
||||
|
||||
const label = { display: 'block', fontSize: 11, color: '#999', marginBottom: 4, fontWeight: 600 } as const
|
||||
|
||||
export default forwardRef(function IssueForm({ initialValues, submitting, mode = 'create', onSubmit }: Props, ref) {
|
||||
const [form] = Form.useForm()
|
||||
const status = initialValues?.status
|
||||
const [deptOptions, setDeptOptions] = useState<{ label: string; value: number }[]>([])
|
||||
const [agentLoading, setAgentLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getDepartments().then((res: any) => {
|
||||
setDeptOptions((res.data || []).map((d: any) => ({ label: d.name, value: d.id })))
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
form
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(toForm(initialValues || {}))
|
||||
}, [initialValues, form])
|
||||
|
||||
const toForm = (v: Partial<IssueFormValues>) => ({
|
||||
...v,
|
||||
deadline: v.deadline ? dayjs(v.deadline) : undefined,
|
||||
responseCompletedAt: v.responseCompletedAt ? dayjs(v.responseCompletedAt) : undefined,
|
||||
confirmAt: v.confirmAt ? dayjs(v.confirmAt) : undefined
|
||||
})
|
||||
|
||||
const handleFinish = (values: any) => {
|
||||
onSubmit({
|
||||
...values,
|
||||
deadline: values.deadline ? values.deadline.format('YYYY-MM-DDTHH:mm:ss') : undefined,
|
||||
responseCompletedAt: values.responseCompletedAt ? values.responseCompletedAt.format('YYYY-MM-DDTHH:mm:ss') : undefined,
|
||||
confirmAt: values.confirmAt ? values.confirmAt.format('YYYY-MM-DDTHH:mm:ss') : undefined
|
||||
})
|
||||
}
|
||||
|
||||
const fallbackFill = (title?: string) => {
|
||||
let category = '功能缺陷'
|
||||
if (title && /(UI|界面|布局|样式|适配)/i.test(title)) category = 'UI/UX问题'
|
||||
if (title && /(性能|慢|超时|卡顿)/i.test(title)) category = '性能问题'
|
||||
if (title && /(安全|漏洞|权限)/i.test(title)) category = '安全漏洞'
|
||||
form.setFieldsValue({ category, phase: '设计', impactLevel: '中', priority: 'medium' })
|
||||
}
|
||||
|
||||
const agentFill = async () => {
|
||||
const title = form.getFieldValue('title') as string | undefined
|
||||
const description = form.getFieldValue('description') as string | undefined
|
||||
if (!title) {
|
||||
message.warning('请先填写指摘标题')
|
||||
return
|
||||
}
|
||||
setAgentLoading(true)
|
||||
try {
|
||||
const res: any = await suggestFields(title, description)
|
||||
const fields = res?.data || {}
|
||||
if (Object.keys(fields).length > 0) {
|
||||
if (fields.deadline) {
|
||||
fields.deadline = dayjs(fields.deadline)
|
||||
}
|
||||
form.setFieldsValue(fields)
|
||||
message.success('Agent 已智能填充字段')
|
||||
} else {
|
||||
fallbackFill(title)
|
||||
message.warning('Agent 未识别到有效建议,已按标题规则填充')
|
||||
}
|
||||
} catch {
|
||||
fallbackFill(title)
|
||||
message.warning('Agent 分析超时或失败,已按标题规则填充')
|
||||
} finally {
|
||||
setAgentLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const field = (name: keyof IssueFormValues, child: React.ReactNode, required = false) => (
|
||||
<Form.Item name={name} rules={required ? [{ required: true, message: '必填' }] : undefined}>
|
||||
{child}
|
||||
</Form.Item>
|
||||
)
|
||||
|
||||
const section = (icon: React.ReactNode, title: string, children: React.ReactNode, style?: React.CSSProperties) => (
|
||||
<Card style={{ marginBottom: 16, ...style }} title={<Typography.Text strong>{icon}{title}</Typography.Text>}>
|
||||
{children}
|
||||
</Card>
|
||||
)
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={toForm(initialValues || {})}
|
||||
onFinish={handleFinish}
|
||||
onFinishFailed={() => message.warning('请填写必填项,请检查红色标记字段')}
|
||||
scrollToFirstError={{ behavior: 'smooth', block: 'center' }}
|
||||
>
|
||||
<Row gutter={24}>
|
||||
<Col span={16}>
|
||||
{section(
|
||||
<FileTextOutlined style={{ marginRight: 8, color: '#1677ff' }} />, '基本信息',
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<span style={label}>指摘标题 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('title', <Input placeholder="请输入指摘标题" />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>工程阶段 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('phase', <Select placeholder="请选择" options={PHASE_OPTIONS.map(o => ({ label: o, value: o }))} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>子工程 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('subProject', <Select placeholder="请选择" options={SUB_PROJECT_OPTIONS.map(o => ({ label: o, value: o }))} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>区分 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('category', <Select placeholder="请选择" options={CATEGORY_OPTIONS.map(o => ({ label: o, value: o }))} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>关联 PGM</span>
|
||||
{field('pgmNo', <Input placeholder="如 PGM_AUTH_VIEW_001" />)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>影响度 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('impactLevel', <Select placeholder="请选择" options={IMPACT_LEVEL_OPTIONS.map(o => ({ label: o, value: o }))} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>影响工程</span>
|
||||
{field('impactScope', <Input placeholder="如:前端适配、数据库查询" />)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>部署</span>
|
||||
{field('deployment', <Input placeholder="部署位置/环境" />)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>Review 者 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('reviewerId', <Select placeholder="请选择" options={userOptions} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>Review 工数 (h)</span>
|
||||
{field('reviewWorkload', <InputNumber min={0} step={0.5} style={{ width: '100%' }} placeholder="0.0" />)}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{section(
|
||||
<MessageOutlined style={{ marginRight: 8, color: '#fa8c16' }} />, '指摘内容',
|
||||
<>
|
||||
<span style={label}>指摘内容 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('description', <Input.TextArea rows={5} placeholder="请详细描述问题现象、复现步骤、期望结果..." />, true)}
|
||||
<span style={label}>NG 原因</span>
|
||||
{field('ngReason', <Input placeholder="如:设计遗漏、编码错误、需求变更" />)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{section(
|
||||
<CheckCircleOutlined style={{ marginRight: 8, color: '#52c41a' }} />, '对应信息',
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<span style={label}>对应者 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('assigneeId', <Select placeholder="请选择" options={userOptions} />, true)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>对应工数 (h)</span>
|
||||
{field('responseWorkload', <InputNumber min={0} step={0.5} style={{ width: '100%' }} placeholder="0.0" />)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<span style={label}>对应完了日</span>
|
||||
<Form.Item name="responseCompletedAt" rules={[{ validator: (_: any, v: dayjs.Dayjs) => {
|
||||
const deadline = form.getFieldValue('deadline')
|
||||
if (v && deadline && v.isBefore(deadline, 'day')) {
|
||||
return Promise.reject(new Error('对应完了日不能早于整改截止日期'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
} }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<span style={label}>对应内容</span>
|
||||
{field('responseContent', <Input.TextArea rows={3} placeholder="填写整改方案或对应措施..." />)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<span style={label}>确认者</span>
|
||||
{field('validatorId', <Select allowClear placeholder="请选择" options={userOptions} />)}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<span style={label}>确认日</span>
|
||||
<Form.Item name="confirmAt" rules={[{ validator: (_: any, v: dayjs.Dayjs) => {
|
||||
const completed = form.getFieldValue('responseCompletedAt')
|
||||
if (v && completed && v.isBefore(completed, 'day')) {
|
||||
return Promise.reject(new Error('确认日不能早于对应完了日'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
} }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
{section(
|
||||
<SettingOutlined style={{ marginRight: 8, color: '#1677ff' }} />, '快速设置',
|
||||
<>
|
||||
<span style={label}>状态</span>
|
||||
{mode === 'edit' ? (
|
||||
<div>
|
||||
<Tag color={STATUS_COLORS[status ?? ''] || 'default'}>{status ? STATUS_LABELS[status] || status : '-'}</Tag>
|
||||
<div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>状态变更请在详情页操作</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>草稿</div>
|
||||
)}
|
||||
<span style={label}>优先级 <span style={{ color: '#ff4d4f' }}>*</span></span>
|
||||
{field('priority', <Radio.Group options={priorityOptions} optionType="button" buttonStyle="solid" />, true)}
|
||||
<span style={label}>归属部门</span>
|
||||
{field('departmentId', <Select options={deptOptions} />)}
|
||||
<span style={label}>整改截止日期</span>
|
||||
{field('deadline', <DatePicker style={{ width: '100%' }} />)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Card style={{ marginBottom: 16, background: '#1677ff', color: '#fff' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Typography.Text strong style={{ color: '#fff' }}><BulbOutlined style={{ marginRight: 8 }} />Agent 辅助</Typography.Text>
|
||||
<Typography.Text style={{ color: '#bae0ff', fontSize: 12 }}>输入标题后,Agent 可自动分析并推荐工程阶段、优先级、影响度等字段。</Typography.Text>
|
||||
<Button block loading={agentLoading} style={{ background: 'rgba(255,255,255,.2)', border: 'none', color: '#fff' }} onClick={agentFill}>
|
||||
<BulbOutlined /> 让 Agent 智能填充
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,504 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Button, Tag, Space, Descriptions, Card, List, Popconfirm, Timeline, Modal, Select, Input,
|
||||
Typography, Empty, Collapse
|
||||
} from 'antd'
|
||||
import { message, modal } from '../../antdStatic'
|
||||
import {
|
||||
ArrowLeftOutlined, ArrowRightOutlined, EditOutlined, DownloadOutlined, DeleteOutlined, PaperClipOutlined,
|
||||
UploadOutlined, RobotOutlined, SendOutlined, CheckOutlined, CloseOutlined,
|
||||
EyeOutlined, CodeOutlined, CheckCircleOutlined, UserOutlined, CloseCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
getIssue, getIssueLogs, changeStatus, listAttachments, deleteAttachment, uploadAttachment,
|
||||
executeAgent, getAgentStatus, approveAgentPlan, rejectAgentPlan, changeAgentMode, downloadAttachment
|
||||
} from './services'
|
||||
import type { Issue, Attachment, IssueLogItem } from './services'
|
||||
import { STATUS_LABELS, STATUS_COLORS, PRIORITY_LABELS, PRIORITY_COLORS, ACTION_LABELS } from '../../constants/issue'
|
||||
|
||||
const NEXT_STATUS: Record<string, string[]> = {
|
||||
draft: ['open'],
|
||||
open: ['draft', 'in_progress', 'rejected'],
|
||||
in_progress: ['open', 'resolved', 'rejected'],
|
||||
resolved: ['in_progress', 'verified', 'rejected'],
|
||||
verified: ['resolved', 'in_progress', 'closed', 'rejected'],
|
||||
closed: [],
|
||||
rejected: ['open', 'closed']
|
||||
}
|
||||
|
||||
interface ToolCard {
|
||||
toolName: string
|
||||
inputParams: string
|
||||
outputResult: string
|
||||
}
|
||||
|
||||
export default function IssueDetailPage() {
|
||||
const { id } = useParams()
|
||||
const issueId = Number(id)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [issue, setIssue] = useState<Issue | null>(null)
|
||||
const [logs, setLogs] = useState<IssueLogItem[]>([])
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([])
|
||||
|
||||
const [statusModalOpen, setStatusModalOpen] = useState(false)
|
||||
const [targetStatus, setTargetStatus] = useState<string>()
|
||||
const [statusRemark, setStatusRemark] = useState('')
|
||||
|
||||
const [goal, setGoal] = useState('')
|
||||
const [planId, setPlanId] = useState<number>()
|
||||
const [agentRunning, setAgentRunning] = useState(false)
|
||||
const [toolCards, setToolCards] = useState<ToolCard[]>([])
|
||||
const [awaitingApproval, setAwaitingApproval] = useState(false)
|
||||
const [approvalComment, setApprovalComment] = useState('')
|
||||
const [agentMsg, setAgentMsg] = useState<string>()
|
||||
const [agentError, setAgentError] = useState<string>()
|
||||
const [modelProvider, setModelProvider] = useState<string>()
|
||||
const [promptTpl, setPromptTpl] = useState<{ systemId?: string; systemVer?: string; planId?: string; planVer?: string }>({})
|
||||
const [previewUrl, setPreviewUrl] = useState<string>()
|
||||
const [previewName, setPreviewName] = useState('')
|
||||
|
||||
const load = async () => {
|
||||
const res: any = await getIssue(issueId)
|
||||
setIssue(res.data)
|
||||
}
|
||||
|
||||
const loadLogs = async () => {
|
||||
const res: any = await getIssueLogs(issueId)
|
||||
setLogs(res.data || [])
|
||||
}
|
||||
|
||||
const loadAttachments = async () => {
|
||||
const res: any = await listAttachments(issueId)
|
||||
setAttachments(res.data || [])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(); loadLogs(); loadAttachments()
|
||||
}, [issueId])
|
||||
|
||||
useEffect(() => {
|
||||
if (issue?.agentStatus === 'failed' && issue.agentLastPlanId) {
|
||||
getAgentStatus(issue.agentLastPlanId).then((res: any) => {
|
||||
const plan = res.data
|
||||
setAgentMsg(undefined)
|
||||
setAgentError(plan?.agentMessage || 'Agent 执行失败,未返回失败原因,请查看后端日志。')
|
||||
}).catch(() => {})
|
||||
}
|
||||
}, [issue?.agentStatus, issue?.agentLastPlanId])
|
||||
|
||||
const handleChangeStatus = async () => {
|
||||
if (!issue) return
|
||||
if (!targetStatus) return message.warning('请选择目标状态')
|
||||
if (targetStatus === 'rejected' && !statusRemark.trim()) return message.warning('请填写驳回原因')
|
||||
const doChange = async () => {
|
||||
try {
|
||||
await changeStatus(issueId, targetStatus, statusRemark.trim() || undefined)
|
||||
message.success('状态已更新')
|
||||
setStatusModalOpen(false)
|
||||
setTargetStatus(undefined); setStatusRemark('')
|
||||
load(); loadLogs()
|
||||
} catch { message.error('状态变更失败') }
|
||||
}
|
||||
if (targetStatus === 'closed') {
|
||||
const missing = []
|
||||
if (!issue.responseContent) missing.push('对应内容')
|
||||
if (!issue.responseCompletedAt) missing.push('对应完了日')
|
||||
if (!issue.confirmAt) missing.push('确认日')
|
||||
if (missing.length) message.warning(`关闭前建议补充:${missing.join('、')}(可继续操作)`)
|
||||
modal.confirm({
|
||||
title: '确认关闭指摘?',
|
||||
content: '关闭后该指摘进入终态,不可恢复,请确认整改与验证已完成。',
|
||||
okText: '确认关闭',
|
||||
onOk: doChange
|
||||
})
|
||||
} else {
|
||||
await doChange()
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
await uploadAttachment(issueId, file)
|
||||
message.success('上传成功')
|
||||
loadAttachments()
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDownload = async (att: Attachment) => {
|
||||
const blob = await downloadAttachment(issueId, att.id)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url; a.download = att.fileName; a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleDeleteAtt = async (attId: number) => {
|
||||
await deleteAttachment(issueId, attId)
|
||||
message.success('已删除')
|
||||
loadAttachments()
|
||||
}
|
||||
|
||||
const handlePreview = async (att: Attachment) => {
|
||||
const blob = await downloadAttachment(issueId, att.id)
|
||||
const url = URL.createObjectURL(blob)
|
||||
setPreviewName(att.fileName)
|
||||
setPreviewUrl(url)
|
||||
}
|
||||
|
||||
const handleAgentExecute = async () => {
|
||||
if (!goal.trim()) return message.warning('请输入指令')
|
||||
setAgentRunning(true); setToolCards([]); setAwaitingApproval(false); setAgentMsg(undefined); setAgentError(undefined)
|
||||
try {
|
||||
const res: any = await executeAgent(issueId, goal)
|
||||
const data = res.data
|
||||
setPlanId(data.planId)
|
||||
setGoal('')
|
||||
setAgentMsg('指令已解析并执行,正在生成对应方案...')
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const es = new EventSource(`/api/v1/agent/plan/${data.planId}/stream?token=${token}`)
|
||||
es.onmessage = (event) => {
|
||||
let ev: any = {}
|
||||
try { ev = JSON.parse(event.data) } catch { return }
|
||||
switch (ev.type) {
|
||||
case 'thought':
|
||||
setAgentMsg(ev.content)
|
||||
break
|
||||
case 'action':
|
||||
setToolCards(prev => [...prev, {
|
||||
toolName: ev.tool,
|
||||
inputParams: ev.params ? JSON.stringify(ev.params) : '',
|
||||
outputResult: ev.approval ? '等待人工审批' : ''
|
||||
}])
|
||||
if (ev.approval) setAwaitingApproval(true)
|
||||
break
|
||||
case 'observation':
|
||||
setToolCards(prev => {
|
||||
if (!prev.length) return prev
|
||||
const copy = [...prev]
|
||||
copy[copy.length - 1] = { ...copy[copy.length - 1], outputResult: ev.result || '' }
|
||||
return copy
|
||||
})
|
||||
break
|
||||
case 'result':
|
||||
setAgentMsg(ev.content)
|
||||
setAgentRunning(false)
|
||||
break
|
||||
case 'prompt_info':
|
||||
setPromptTpl(prev => ev.templateId === 'SYS_ROLE_001'
|
||||
? { ...prev, systemId: ev.templateId, systemVer: ev.version }
|
||||
: { ...prev, planId: ev.templateId, planVer: ev.version })
|
||||
break
|
||||
case 'model_info':
|
||||
setModelProvider(ev.provider)
|
||||
break
|
||||
case 'error':
|
||||
setAgentError(ev.message || 'Agent 执行失败')
|
||||
setAgentMsg(undefined)
|
||||
setAgentRunning(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
es.onerror = () => {
|
||||
es.close()
|
||||
setAgentRunning(false)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!planId) return
|
||||
await approveAgentPlan(planId, approvalComment)
|
||||
message.success('已批准执行')
|
||||
setAwaitingApproval(false)
|
||||
setAgentMsg('Agent 指令已批准,对应方案已推送至担当者。')
|
||||
load()
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!planId) return
|
||||
await rejectAgentPlan(planId, approvalComment)
|
||||
message.success('已驳回')
|
||||
setAwaitingApproval(false)
|
||||
setAgentMsg('Agent 指令已驳回,已切换回人工模式。')
|
||||
load()
|
||||
}
|
||||
|
||||
const handleSwitchToHuman = async () => {
|
||||
await changeAgentMode(issueId, 'human_driven')
|
||||
message.success('已切换至纯人工模式')
|
||||
load()
|
||||
}
|
||||
|
||||
if (!issue) return <div style={{ padding: 48, textAlign: 'center' }}><Empty /></div>
|
||||
|
||||
const agentStatusTag = (() => {
|
||||
const s = issue.agentStatus
|
||||
if (s === 'awaiting_approval') return <Tag color="orange">待审批</Tag>
|
||||
if (s === 'human_driven') return <Tag color="default">人工模式</Tag>
|
||||
if (s === 'running') return <Tag color="green">分析中</Tag>
|
||||
if (s === 'completed') return <Tag color="green">已完成</Tag>
|
||||
if (s === 'rejected') return <Tag color="red">已驳回</Tag>
|
||||
if (s === 'failed') return <Tag color="red">失败</Tag>
|
||||
return <Tag color="default">就绪</Tag>
|
||||
})()
|
||||
|
||||
const statusCard = (label: string, value: React.ReactNode) => (
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#999', marginBottom: 4, fontWeight: 600 }}>{label}</div>
|
||||
<div style={{ fontWeight: 600 }}>{value}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate('/issues')} />
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#1677ff', fontWeight: 600 }}>{issue.issueNo}</div>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>{issue.title}</Typography.Title>
|
||||
</div>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<EditOutlined />} onClick={() => navigate(`/issues/${issueId}/edit`)}>编辑</Button>
|
||||
<Button type="primary" disabled={NEXT_STATUS[issue.status]?.length === 0} onClick={() => setStatusModalOpen(true)}>更改状态</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'flex-start' }}>
|
||||
<Card style={{ flex: 1.5, marginBottom: 16 }}>
|
||||
<Space size={32} style={{ marginBottom: 16 }}>
|
||||
{statusCard('当前状态', <Tag color={STATUS_COLORS[issue.status]}>{STATUS_LABELS[issue.status] || issue.status}</Tag>)}
|
||||
{statusCard('优先级', <Tag color={PRIORITY_COLORS[issue.priority]}>{PRIORITY_LABELS[issue.priority] || issue.priority}</Tag>)}
|
||||
{statusCard('对应者', issue.assigneeName || '未分配')}
|
||||
{statusCard('创建日期', (issue.createdAt || '').slice(0, 10))}
|
||||
</Space>
|
||||
|
||||
<Descriptions column={2} size="small" bordered style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="工程阶段">{issue.phase || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="子工程">{issue.subProject || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="区分">{issue.category || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="影响度">{issue.impactLevel || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联 PGM">{issue.pgmNo || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="影响工程">{issue.impactScope || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="部署">{issue.deployment || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="整改截止">{issue.deadline ? issue.deadline.slice(0, 10) : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建人">{issue.creatorName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Review 者">{issue.reviewerName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Review 工数">{issue.reviewWorkload != null ? `${issue.reviewWorkload} h` : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="对应完了日">{issue.responseCompletedAt ? issue.responseCompletedAt.slice(0, 10) : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="确认者">{issue.validatorName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="确认日">{issue.confirmAt ? issue.confirmAt.slice(0, 10) : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="NG 原因">{issue.ngReason || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="对应内容">{issue.responseContent || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 600, marginBottom: 8 }}>指摘内容</div>
|
||||
<div style={{ background: '#fafafa', borderRadius: 8, padding: 16, fontSize: 13, lineHeight: 1.8 }}>
|
||||
{issue.description || '暂无内容'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 11, color: '#999', fontWeight: 600 }}>附件 ({attachments.length})</span>
|
||||
<label style={{ cursor: 'pointer', color: '#1677ff', fontSize: 12 }}>
|
||||
<UploadOutlined style={{ marginRight: 4 }} />
|
||||
添加附件
|
||||
<input
|
||||
type="file" multiple style={{ display: 'none' }}
|
||||
onChange={e => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
files.forEach(handleUpload)
|
||||
e.target.value = ''
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<List
|
||||
size="small" dataSource={attachments} locale={{ emptyText: '暂无附件' }}
|
||||
renderItem={(att) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="p" type="text" size="small" icon={<EyeOutlined />} onClick={() => handlePreview(att)} />,
|
||||
<Button key="d" type="text" size="small" icon={<DownloadOutlined />} onClick={() => handleDownload(att)} />,
|
||||
<Popconfirm key="del" title="确认删除?" onConfirm={() => handleDeleteAtt(att.id)}>
|
||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta avatar={<PaperClipOutlined />} title={att.fileName}
|
||||
description={`${(att.fileSize / 1024).toFixed(1)} KB · ${att.uploadedByName || ''}`} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 600, marginBottom: 8 }}>状态流转历史</div>
|
||||
<Collapse
|
||||
ghost defaultActiveKey={['history']}
|
||||
items={[{
|
||||
key: 'history',
|
||||
label: <span style={{ fontSize: 12, color: '#666' }}>展开 / 折叠流转记录(审计参考)</span>,
|
||||
children: (
|
||||
<Timeline
|
||||
items={logs.map((l, i) => ({
|
||||
key: i,
|
||||
color: i === 0 ? '#1677ff' : 'gray',
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>
|
||||
{ACTION_LABELS[l.action] || l.action}
|
||||
{l.fromStatus && l.toStatus && <Tag style={{ marginLeft: 8 }} color="blue">{STATUS_LABELS[l.fromStatus]} → {STATUS_LABELS[l.toStatus]}</Tag>}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>
|
||||
{l.userName} · {l.remark || ''}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#bbb' }}>{l.createdAt?.replace('T', ' ').slice(0, 16)}</div>
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
style={{ flex: 1, marginBottom: 16, background: '#fcfdff', position: 'sticky', top: 24 }}
|
||||
title={
|
||||
<Space>
|
||||
<RobotOutlined style={{ color: '#1677ff' }} />
|
||||
<span>IMS Agent 驾驶舱</span>
|
||||
{agentStatusTag}
|
||||
{modelProvider && <Tag color="blue">引擎:{modelProvider}</Tag>}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ background: '#e6f4ff', borderRadius: 8, padding: 12, marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 13 }}>
|
||||
你好,我是您的指摘处理助手。我可以帮您 <b>催办担当者</b>、<b>查找知识库相似案例</b> 或 <b>自动生成对应方案</b>。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{issue.agentStatus && issue.agentStatus !== 'human_driven' && (
|
||||
<div style={{ textAlign: 'right', marginBottom: 8 }}>
|
||||
<Button size="small" icon={<UserOutlined />} onClick={handleSwitchToHuman}>转人工</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolCards.map((t, i) => (
|
||||
<div key={i} style={{ borderLeft: '3px solid #fa8c16', background: '#fffbe6', borderRadius: 8, padding: 10, marginBottom: 8 }}>
|
||||
<Space style={{ marginBottom: 6 }}>
|
||||
<CodeOutlined style={{ color: '#fa8c16' }} />
|
||||
<span style={{ fontWeight: 600, fontSize: 12, color: '#d46b08' }}>行动</span>
|
||||
</Space>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 11, background: 'rgba(250,173,20,.1)', borderRadius: 6, padding: 8, wordBreak: 'break-all' }}>
|
||||
call: <b>{t.toolName}</b>({t.inputParams})
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 6 }}><EyeOutlined style={{ marginRight: 4 }} />{t.outputResult}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{awaitingApproval && (
|
||||
<div style={{ border: '2px solid #ff4d4f', borderRadius: 8, padding: 12, marginBottom: 8, background: '#fff1f0' }}>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<CheckCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
<span style={{ fontWeight: 600, color: '#cf1322' }}>需要您的审批</span>
|
||||
</Space>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
|
||||
Agent 准备执行上述方案。是否批准?
|
||||
</div>
|
||||
<Input.TextArea rows={2} placeholder="审批意见(可选)" value={approvalComment}
|
||||
onChange={e => setApprovalComment(e.target.value)} style={{ marginBottom: 8, fontSize: 12 }} />
|
||||
<Space>
|
||||
<Button size="small" type="primary" danger icon={<CheckOutlined />} onClick={handleApprove}>批准执行</Button>
|
||||
<Button size="small" icon={<CloseOutlined />} onClick={handleReject}>驳回</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agentError && !awaitingApproval && (
|
||||
<div style={{ background: '#fff1f0', border: '1px solid #ffa39e', borderRadius: 8, padding: 10, marginBottom: 8, fontSize: 12, color: '#cf1322' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}><CloseCircleOutlined style={{ marginRight: 4 }} />Agent 执行失败</div>
|
||||
<div style={{ wordBreak: 'break-all' }}>{agentError}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agentMsg && !awaitingApproval && (
|
||||
<div style={{ background: '#f6ffed', borderRadius: 8, padding: 10, marginBottom: 8, fontSize: 12, color: '#389e0d' }}>
|
||||
{agentMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Collapse
|
||||
ghost
|
||||
style={{ marginBottom: 8 }}
|
||||
items={[{
|
||||
key: 'prompt',
|
||||
label: <span style={{ fontSize: 12, color: '#999' }}>Prompt 模板信息</span>,
|
||||
children: (
|
||||
<div style={{ fontSize: 12, lineHeight: 1.8 }}>
|
||||
<div>系统角色:<b>{promptTpl.systemId || '未提供'}</b>({promptTpl.systemVer ? `v${promptTpl.systemVer}` : '-'})</div>
|
||||
<div>规划模板:<b>{promptTpl.planId || '未提供'}</b>({promptTpl.planVer ? `v${promptTpl.planVer}` : '-'})</div>
|
||||
<div style={{ color: '#999', marginTop: 4 }}>模板由 Agent 引擎从 Prompt 模板库动态加载,此处为当前执行使用的模板标识。</div>
|
||||
</div>
|
||||
)
|
||||
}]}
|
||||
/>
|
||||
|
||||
<div style={{ borderTop: '1px solid #f0f0f0', paddingTop: 12, marginTop: 8 }}>
|
||||
<Input.TextArea rows={2} placeholder="给 Agent 下达指令,如:查找知识库相似案例,并生成对应方案" value={goal} onChange={e => setGoal(e.target.value)} />
|
||||
<Button type="primary" block style={{ marginTop: 8 }} icon={<SendOutlined />} loading={agentRunning} onClick={handleAgentExecute}>
|
||||
{agentRunning ? '执行中...' : '执行指令'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal title="更改状态" open={statusModalOpen} onOk={handleChangeStatus} onCancel={() => setStatusModalOpen(false)}
|
||||
okText={targetStatus === 'rejected' ? '确认驳回' : '确认变更'}>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#999' }}>流转路径</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Tag color={STATUS_COLORS[issue.status]} style={{ fontWeight: 600, fontSize: 13 }}>{STATUS_LABELS[issue.status]}</Tag>
|
||||
<ArrowRightOutlined style={{ color: '#999' }} />
|
||||
<Select
|
||||
style={{ flex: 1 }} placeholder="选择目标状态" value={targetStatus}
|
||||
onChange={setTargetStatus}
|
||||
options={(NEXT_STATUS[issue.status] || []).map(s => ({ label: STATUS_LABELS[s] || s, value: s }))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#666' }}>
|
||||
当前状态可流转到:{(NEXT_STATUS[issue.status] || []).map(s => STATUS_LABELS[s] || s).join('、') || '无'}
|
||||
</div>
|
||||
{issue.status === 'verified' && (
|
||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: '#fff7e6', border: '1px solid #ffd591', borderRadius: 6, fontSize: 12, color: '#d46b08' }}>
|
||||
修改已验证状态的指摘属高风险操作(设计文档审批强制),请确认整改结果已核验。
|
||||
</div>
|
||||
)}
|
||||
{targetStatus === 'closed' && (
|
||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: '#fff1f0', border: '1px solid #ffa39e', borderRadius: 6, fontSize: 12, color: '#cf1322' }}>
|
||||
关闭为终态操作,不可恢复,提交时需二次确认。
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
{targetStatus === 'rejected' ? (<span>驳回原因 <span style={{ color: '#ff4d4f' }}>*</span></span>) : '备注(可选)'}
|
||||
</div>
|
||||
<Input.TextArea rows={2}
|
||||
placeholder={targetStatus === 'rejected' ? '请填写驳回原因,将记录到流转历史' : '流转说明(可选)'}
|
||||
value={statusRemark} onChange={e => setStatusRemark(e.target.value)} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal title={`预览:${previewName}`} open={!!previewUrl} footer={null} width={720}
|
||||
onCancel={() => { setPreviewUrl(undefined); setPreviewName('') }}>
|
||||
{previewUrl && <iframe src={previewUrl} title={previewName} style={{ width: '100%', height: 480, border: 'none' }} />}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Typography, Button, Space, Upload, Card, List, Popconfirm, Modal, Timeline, Tag } from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { ArrowLeftOutlined, SaveOutlined, UploadOutlined, DownloadOutlined, DeleteOutlined, ReloadOutlined, HistoryOutlined, RobotOutlined } from '@ant-design/icons'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import IssueForm from './IssueForm'
|
||||
import type { IssueFormValues } from './IssueForm'
|
||||
import { getIssue, updateIssue, deleteIssue, listAttachments, uploadAttachment, deleteAttachment, getIssueLogs, suggestIssue, downloadAttachment } from './services'
|
||||
import type { Attachment, IssueLogItem } from './services'
|
||||
import { ACTION_LABELS, STATUS_LABELS } from '../../constants/issue'
|
||||
|
||||
export default function IssueEditPage() {
|
||||
const { id } = useParams()
|
||||
const issueId = Number(id)
|
||||
const navigate = useNavigate()
|
||||
const formRef = useRef<{ form: any }>(null)
|
||||
const [initialValues, setInitialValues] = useState<Partial<IssueFormValues>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([])
|
||||
const [logs, setLogs] = useState<IssueLogItem[]>([])
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [suggestOpen, setSuggestOpen] = useState(false)
|
||||
const [suggestText, setSuggestText] = useState('')
|
||||
const [suggesting, setSuggesting] = useState(false)
|
||||
|
||||
const loadIssue = async () => {
|
||||
const res: any = await getIssue(issueId)
|
||||
const d = res.data
|
||||
setInitialValues({
|
||||
title: d.title, description: d.description, phase: d.phase, subProject: d.subProject,
|
||||
category: d.category, impactLevel: d.impactLevel, impactScope: d.impactScope,
|
||||
deployment: d.deployment, pgmNo: d.pgmNo, reviewWorkload: d.reviewWorkload,
|
||||
responseWorkload: d.responseWorkload, responseContent: d.responseContent, ngReason: d.ngReason,
|
||||
assigneeId: d.assigneeId, reviewerId: d.reviewerId, validatorId: d.validatorId,
|
||||
priority: d.priority, status: d.status, departmentId: d.departmentId,
|
||||
deadline: d.deadline, responseCompletedAt: d.responseCompletedAt, confirmAt: d.confirmAt
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await loadIssue()
|
||||
loadAttachments()
|
||||
})()
|
||||
}, [issueId])
|
||||
|
||||
const loadAttachments = async () => {
|
||||
const res: any = await listAttachments(issueId)
|
||||
setAttachments(res.data || [])
|
||||
}
|
||||
|
||||
const handleSave = () => formRef.current?.form.submit()
|
||||
|
||||
const handleReset = async () => {
|
||||
await loadIssue()
|
||||
message.success('已恢复初始数据')
|
||||
}
|
||||
|
||||
const loadLogs = async () => {
|
||||
const res: any = await getIssueLogs(issueId)
|
||||
setLogs(res.data || [])
|
||||
}
|
||||
|
||||
const openHistory = () => {
|
||||
loadLogs()
|
||||
setHistoryOpen(true)
|
||||
}
|
||||
|
||||
const handleSuggest = async () => {
|
||||
setSuggesting(true)
|
||||
setSuggestText('')
|
||||
setSuggestOpen(true)
|
||||
try {
|
||||
const res: any = await suggestIssue(issueId, '请基于当前指摘内容给出字段修改建议')
|
||||
setSuggestText(res.data || '')
|
||||
} catch { /* 错误由拦截器提示 */ }
|
||||
setSuggesting(false)
|
||||
}
|
||||
|
||||
const handleDeleteIssue = async () => {
|
||||
await deleteIssue(issueId)
|
||||
message.success('已删除指摘')
|
||||
navigate('/issues')
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: IssueFormValues) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await updateIssue(issueId, values as Record<string, unknown>)
|
||||
message.success('保存成功')
|
||||
navigate(`/issues/${issueId}`)
|
||||
} catch { /* ignore */ }
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
await uploadAttachment(issueId, file)
|
||||
message.success('上传成功')
|
||||
loadAttachments()
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDownload = async (att: Attachment) => {
|
||||
const blob = await downloadAttachment(issueId, att.id)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url; a.download = att.fileName; a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleDelete = async (attId: number) => {
|
||||
await deleteAttachment(issueId, attId)
|
||||
message.success('已删除')
|
||||
loadAttachments()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(`/issues/${issueId}`)} />
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>编辑指摘</Typography.Title>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>恢复初始</Button>
|
||||
<Button icon={<HistoryOutlined />} onClick={openHistory}>修改历史</Button>
|
||||
<Button icon={<RobotOutlined />} loading={suggesting} onClick={handleSuggest}>Agent 建议修改</Button>
|
||||
<Popconfirm title="确认删除该指摘?" onConfirm={handleDeleteIssue} okText="删除" okButtonProps={{ danger: true }}>
|
||||
<Button danger icon={<DeleteOutlined />}>删除指摘</Button>
|
||||
</Popconfirm>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={submitting} onClick={handleSave}>保存指摘</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<IssueForm ref={formRef} mode="edit" initialValues={initialValues} submitting={submitting} onSubmit={handleSubmit} />
|
||||
|
||||
<Card style={{ maxWidth: 1280, margin: '0 auto 24px' }} title={`附件 (${attachments.length})`}>
|
||||
<Upload.Dragger
|
||||
multiple
|
||||
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.log,.xlsx,.xls"
|
||||
beforeUpload={(file) => { handleUpload(file); return false }}
|
||||
showUploadList={false}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon"><UploadOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||||
</Upload.Dragger>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={attachments}
|
||||
locale={{ emptyText: '暂无附件' }}
|
||||
renderItem={(att) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="d" type="text" size="small" icon={<DownloadOutlined />} onClick={() => handleDownload(att)} />,
|
||||
<Popconfirm key="del" title="确认删除?" onConfirm={() => handleDelete(att.id)}>
|
||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={att.fileName} description={`${(att.fileSize / 1024).toFixed(1)} KB`} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal title="修改历史" open={historyOpen} footer={null} width={640} onCancel={() => setHistoryOpen(false)}>
|
||||
<Timeline
|
||||
items={logs.map((l, i) => ({
|
||||
key: i,
|
||||
color: i === 0 ? '#1677ff' : 'gray',
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>
|
||||
{ACTION_LABELS[l.action] || l.action}
|
||||
{l.fromStatus && l.toStatus && <Tag style={{ marginLeft: 8 }} color="blue">{STATUS_LABELS[l.fromStatus]} → {STATUS_LABELS[l.toStatus]}</Tag>}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>{l.userName} · {l.remark || ''}</div>
|
||||
<div style={{ fontSize: 11, color: '#bbb' }}>{l.createdAt?.replace('T', ' ').slice(0, 16)}</div>
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Agent 建议修改" open={suggestOpen} footer={null} width={640}
|
||||
onCancel={() => setSuggestOpen(false)}
|
||||
>
|
||||
<div style={{ maxHeight: 420, overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.8 }}>
|
||||
{suggesting ? <Typography.Text type="secondary">Agent 分析中...</Typography.Text> : (suggestText || '暂未生成建议')}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Table, Button, Input, Select, Space, Tag, Card, Popconfirm, Row, Col, Statistic, Typography, Modal, DatePicker
|
||||
} from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { PlusOutlined, SearchOutlined, DownloadOutlined, RobotOutlined, ReloadOutlined, OrderedListOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
listIssues, deleteIssue, exportIssues, batchAssign, batchNotify, batchAgent, getDepartments
|
||||
} from './services'
|
||||
import { getDashboardStats } from '../dashboard/services'
|
||||
import {
|
||||
STATUS_LABELS, STATUS_COLORS, PRIORITY_LABELS, PRIORITY_COLORS,
|
||||
PHASE_OPTIONS, SUB_PROJECT_OPTIONS, IMPACT_LEVEL_OPTIONS, USERS
|
||||
} from '../../constants/issue'
|
||||
|
||||
const PRIORITY_OPTIONS = Object.entries(PRIORITY_LABELS).map(([value, label]) => ({ label, value }))
|
||||
import type { Issue } from './services'
|
||||
|
||||
const IMPACT_ORDER: Record<string, number> = { 低: 1, 中: 2, 高: 3 }
|
||||
const STATUS_CARD_ORDER = ['open', 'in_progress', 'resolved', 'verified', 'closed'] as const
|
||||
const STATUS_CARD_COLORS: Record<string, string> = {
|
||||
open: '#fa8c16', in_progress: '#1677ff', resolved: '#13c2c2', verified: '#722ed1', closed: '#52c41a'
|
||||
}
|
||||
|
||||
export default function IssueListPage() {
|
||||
const navigate = useNavigate()
|
||||
const [data, setData] = useState<Issue[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([])
|
||||
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [filters, setFilters] = useState<Record<string, string | undefined>>({
|
||||
phase: undefined, subProject: undefined, priority: undefined, impactLevel: undefined, status: undefined,
|
||||
assigneeId: undefined, departmentId: undefined, startDate: undefined, endDate: undefined
|
||||
})
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null)
|
||||
const [summary, setSummary] = useState<Record<string, number>>({})
|
||||
const [assignModalOpen, setAssignModalOpen] = useState(false)
|
||||
const [assigneeId, setAssigneeId] = useState<number>()
|
||||
const [agentModalOpen, setAgentModalOpen] = useState(false)
|
||||
const [batchGoal, setBatchGoal] = useState('')
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const assigneeOptions = USERS.map(u => ({ label: u.name, value: String(u.id) }))
|
||||
const [deptOptions, setDeptOptions] = useState<{ label: string; value: string }[]>([])
|
||||
|
||||
const load = async (p = page, extra = filters) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await listIssues({
|
||||
...extra, keyword: keyword || undefined, page: p, pageSize: 10
|
||||
})
|
||||
setData(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const res: any = await getDashboardStats()
|
||||
const dist = res.data?.statusDistribution || []
|
||||
setSummary(Object.fromEntries(dist.map((d: any) => [d.status, d.count])))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
useEffect(() => { load(); loadSummary() }, [])
|
||||
|
||||
useEffect(() => {
|
||||
getDepartments().then((res: any) => {
|
||||
setDeptOptions((res.data || []).map((d: any) => ({ label: d.name, value: String(d.id) })))
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleSearch = () => load(1)
|
||||
|
||||
const handleReset = () => {
|
||||
setFilters({ phase: undefined, subProject: undefined, priority: undefined, impactLevel: undefined, status: undefined, assigneeId: undefined, departmentId: undefined, startDate: undefined, endDate: undefined })
|
||||
setKeyword('')
|
||||
setDateRange(null)
|
||||
load(1, {})
|
||||
}
|
||||
|
||||
const clickStatusCard = (status: string) => {
|
||||
const next = { ...filters, status: filters.status === status ? undefined : status }
|
||||
setFilters(next)
|
||||
load(1, next)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const blob = await exportIssues({
|
||||
...filters, keyword: keyword || undefined
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'issues.csv'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
message.success('导出成功')
|
||||
} catch {
|
||||
message.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchAssign = async () => {
|
||||
if (!assigneeId) return
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const res: any = await batchAssign(selectedKeys as number[], assigneeId)
|
||||
message.success(`已为 ${res.data?.count ?? 0} 条指摘统一分配担当者`)
|
||||
setAssignModalOpen(false)
|
||||
setAssigneeId(undefined)
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
setBatchLoading(false)
|
||||
}
|
||||
|
||||
const handleBatchNotify = async () => {
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const res: any = await batchNotify(selectedKeys as number[], '您的指摘已逾期或即将到期,请尽快处理。')
|
||||
message.success(`已向 ${res.data?.count ?? 0} 条指摘的担当者发送催办`)
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
setBatchLoading(false)
|
||||
}
|
||||
|
||||
const handleBatchAgent = async () => {
|
||||
const goal = batchGoal.trim()
|
||||
if (!goal) return message.warning('请输入 Agent 指令')
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const res: any = await batchAgent(selectedKeys as number[], goal)
|
||||
message.success(`Agent 已对 ${res.data?.count ?? 0} 条指摘生成方案,请到详情页审批`)
|
||||
setAgentModalOpen(false)
|
||||
setBatchGoal('')
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
setBatchLoading(false)
|
||||
}
|
||||
|
||||
const tipOf = (r: Issue) => {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
if (r.status === 'closed') return null
|
||||
if (r.deadline && r.deadline.slice(0, 10) < today) return { text: '已逾期,请尽快处理', color: '#ff4d4f' }
|
||||
if (!r.assigneeName) return { text: '未分配对应者', color: '#fa8c16' }
|
||||
if (r.status === 'verified') return { text: '已验证,待关闭', color: '#52c41a' }
|
||||
return null
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID / 标题', key: 'title', width: 300, render: (_: any, r: Issue) => (
|
||||
<div style={{ cursor: 'pointer' }} onClick={() => navigate(`/issues/${r.id}`)}>
|
||||
<div style={{ fontSize: 11, color: '#1677ff', fontWeight: 600 }}>{r.issueNo}</div>
|
||||
<div style={{ fontWeight: 500 }}>{r.title}</div>
|
||||
</div>
|
||||
) },
|
||||
{ title: '状态', dataIndex: 'status', width: 100, align: 'center' as const, render: (s: string) => <Tag color={STATUS_COLORS[s]}>{STATUS_LABELS[s] || s}</Tag> },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 80, align: 'center' as const, render: (p: string) => <Tag color={PRIORITY_COLORS[p]}>{PRIORITY_LABELS[p] || p}</Tag> },
|
||||
{ title: '工程阶段', dataIndex: 'phase', width: 110, render: (v: string) => v || '-' },
|
||||
{ title: '子工程', dataIndex: 'subProject', width: 130, render: (v: string) => v || '-' },
|
||||
{ title: '影响度', dataIndex: 'impactLevel', width: 80, align: 'center' as const, render: (v: string) => v ? <span style={{ color: IMPACT_ORDER[v] >= 3 ? '#ff4d4f' : '#999' }}>{v}</span> : '-' },
|
||||
{ title: '对应者', dataIndex: 'assigneeName', width: 90, render: (v: string) => v || <span style={{ color: '#bbb' }}>未分配</span> },
|
||||
{ title: '处理提示', key: 'tip', render: (_: any, r: Issue) => {
|
||||
const s = tipOf(r)
|
||||
return s ? (
|
||||
<div style={{ borderLeft: `3px solid ${s.color}`, paddingLeft: 8 }}>
|
||||
<div style={{ fontSize: 11, color: s.color, fontWeight: 500 }}>{s.text}</div>
|
||||
</div>
|
||||
) : <span style={{ color: '#ccc' }}>—</span>
|
||||
} },
|
||||
{ title: '截止日期', dataIndex: 'deadline', width: 105, render: (v: string) => v ? <span style={{ color: v && v.slice(0, 10) < new Date().toISOString().slice(0, 10) ? '#ff4d4f' : undefined }}>{v.slice(0, 10)}</span> : '-' },
|
||||
{ title: '操作', key: 'action', width: 140, align: 'right' as const, render: (_: any, r: Issue) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" onClick={() => navigate(`/issues/${r.id}`)}>详情</Button>
|
||||
<Button type="link" size="small" onClick={() => navigate(`/issues/${r.id}/edit`)}>编辑</Button>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(r.id)}>
|
||||
<Button type="link" danger size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
) }
|
||||
]
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteIssue(id)
|
||||
message.success('已删除')
|
||||
load()
|
||||
}
|
||||
|
||||
const openAssign = () => {
|
||||
if (selectedKeys.length === 0) return message.warning('请先选择指摘')
|
||||
setAssigneeId(undefined)
|
||||
setAssignModalOpen(true)
|
||||
}
|
||||
|
||||
const openNotify = () => {
|
||||
if (selectedKeys.length === 0) return message.warning('请先选择指摘')
|
||||
handleBatchNotify()
|
||||
}
|
||||
|
||||
const openAgent = () => {
|
||||
if (selectedKeys.length === 0) return message.warning('请先选择指摘')
|
||||
setBatchGoal('')
|
||||
setAgentModalOpen(true)
|
||||
}
|
||||
|
||||
const filterSelect = (label: string, key: string, options: Array<string | { label: string; value: string }>) => (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, color: '#999', marginBottom: 4, fontWeight: 600 }}>{label}</div>
|
||||
<Select
|
||||
allowClear placeholder="全部" style={{ width: '100%' }} value={filters[key]}
|
||||
onChange={v => setFilters({ ...filters, [key]: v })}
|
||||
options={options.map(o => typeof o === 'string' ? { label: o, value: o } : o)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>指摘列表</Typography.Title>
|
||||
<Space>
|
||||
<Button icon={<OrderedListOutlined />} onClick={() => navigate('/batch-input')}>批量录入</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出 CSV</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => navigate('/issues/new')}>新建指摘</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={12} style={{ marginBottom: 16 }}>
|
||||
{STATUS_CARD_ORDER.map((st) => (
|
||||
<Col span={4} key={st}>
|
||||
<Card size="small" hoverable onClick={() => clickStatusCard(st)} style={{ cursor: 'pointer', borderBottom: `3px solid ${STATUS_CARD_COLORS[st]}` }}>
|
||||
<Statistic title={STATUS_LABELS[st]} value={summary[st] ?? 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card title="筛选条件" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={4}>{filterSelect('工程阶段', 'phase', PHASE_OPTIONS)}</Col>
|
||||
<Col span={4}>{filterSelect('子工程', 'subProject', SUB_PROJECT_OPTIONS)}</Col>
|
||||
<Col span={4}>{filterSelect('优先级', 'priority', PRIORITY_OPTIONS)}</Col>
|
||||
<Col span={4}>{filterSelect('影响度', 'impactLevel', IMPACT_LEVEL_OPTIONS)}</Col>
|
||||
<Col span={4}>{filterSelect('对应者', 'assigneeId', assigneeOptions)}</Col>
|
||||
<Col span={4}>{filterSelect('归属部门', 'departmentId', deptOptions)}</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, color: '#999', marginBottom: 4, fontWeight: 600 }}>创建日期范围</div>
|
||||
<DatePicker.RangePicker
|
||||
style={{ width: '100%' }} value={dateRange}
|
||||
onChange={(range: any) => {
|
||||
setDateRange(range)
|
||||
setFilters({
|
||||
...filters,
|
||||
startDate: range?.[0] ? range[0].format('YYYY-MM-DDT00:00:00') : undefined,
|
||||
endDate: range?.[1] ? range[1].format('YYYY-MM-DDT23:59:59') : undefined
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', height: '100%' }}>
|
||||
<Input
|
||||
prefix={<SearchOutlined />} placeholder="搜索指摘 ID、标题或关键词"
|
||||
value={keyword} onChange={e => setKeyword(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSearch}>应用筛选</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<div style={{ background: '#e6f4ff', border: '1px solid #91caff', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<RobotOutlined style={{ color: '#1677ff' }} />
|
||||
<span style={{ fontWeight: 600, fontSize: 13 }}>Agent 批量处理</span>
|
||||
<span style={{ fontSize: 11, color: '#1677ff' }}>选中多条后可使用 Agent 统一分配 / 催办</span>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button size="small" icon={<RobotOutlined />} onClick={openAssign}>统一分配</Button>
|
||||
<Button size="small" icon={<RobotOutlined />} onClick={openNotify}>批量催办</Button>
|
||||
<Button size="small" type="primary" onClick={openAgent}>智能分析</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="id" dataSource={data} columns={columns} loading={loading}
|
||||
rowSelection={{ selectedRowKeys: selectedKeys, onChange: setSelectedKeys }}
|
||||
pagination={{ current: page, total, pageSize: 10, onChange: (p) => load(p), showTotal: t => `共 ${t} 条` }}
|
||||
/>
|
||||
|
||||
<Modal title="Agent 统一分配" open={assignModalOpen} onOk={handleBatchAssign}
|
||||
confirmLoading={batchLoading} onCancel={() => setAssignModalOpen(false)} okText="确认分配">
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#999' }}>
|
||||
已选中 {selectedKeys.length} 条指摘,选择担当者后统一分配。
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: '100%' }} placeholder="选择担当者" value={assigneeId}
|
||||
onChange={setAssigneeId}
|
||||
options={USERS.map(u => ({ label: u.name, value: u.id }))}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Agent 智能分析" open={agentModalOpen} onOk={handleBatchAgent}
|
||||
confirmLoading={batchLoading} onCancel={() => setAgentModalOpen(false)} okText="执行分析">
|
||||
<div style={{ marginBottom: 12, fontSize: 12, color: '#999' }}>
|
||||
已选中 {selectedKeys.length} 条指摘,Agent 将检索知识库相似案例并生成对应方案(需到详情页审批)。
|
||||
</div>
|
||||
<Input.TextArea rows={3} placeholder="例如:检索知识库相似案例,生成对应方案"
|
||||
value={batchGoal} onChange={e => setBatchGoal(e.target.value)} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Typography, Button, Space, Upload, Card } from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { ArrowLeftOutlined, SaveOutlined, UploadOutlined, ClearOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import IssueForm from './IssueForm'
|
||||
import type { IssueFormValues } from './IssueForm'
|
||||
import { createIssue, uploadAttachment } from './services'
|
||||
|
||||
export default function IssueNewPage() {
|
||||
const navigate = useNavigate()
|
||||
const formRef = useRef<{ form: any }>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const handleSave = () => {
|
||||
formRef.current?.form.submit()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formRef.current?.form.resetFields()
|
||||
setFiles([])
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: IssueFormValues) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res: any = await createIssue(values as Record<string, unknown>)
|
||||
const id = res.data?.id
|
||||
if (id) {
|
||||
for (const f of files) {
|
||||
await uploadAttachment(id, f)
|
||||
}
|
||||
}
|
||||
message.success('保存成功')
|
||||
navigate(id ? `/issues/${id}` : '/issues')
|
||||
} catch { /* error shown by interceptor */ }
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate('/issues')} />
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>新建指摘</Typography.Title>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<ClearOutlined />} onClick={handleReset}>清空重置</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={submitting} onClick={handleSave}>保存指摘</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<IssueForm ref={formRef} mode="create" submitting={submitting} onSubmit={handleSubmit} />
|
||||
|
||||
<Card style={{ maxWidth: 1280, margin: '0 auto 24px' }} title="附件">
|
||||
<Upload.Dragger
|
||||
multiple
|
||||
accept=".png,.jpg,.jpeg,.gif,.pdf,.doc,.docx,.txt,.log,.xlsx,.xls"
|
||||
beforeUpload={(file) => { setFiles(prev => [...prev, file]); return false }}
|
||||
showUploadList={{ removeIcon: true }}
|
||||
onRemove={(file: any) => setFiles(prev => prev.filter(f => f.name !== file.name))}
|
||||
>
|
||||
<p className="ant-upload-drag-icon"><UploadOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||||
<p className="ant-upload-hint">支持图片、文档、日志文件,单个不超过 20MB(保存指摘后上传)</p>
|
||||
</Upload.Dragger>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import axios from 'axios'
|
||||
import request from '../../request'
|
||||
|
||||
export interface Issue {
|
||||
id: number
|
||||
issueNo: string
|
||||
title: string
|
||||
description?: string
|
||||
status: string
|
||||
priority: string
|
||||
deadline?: string
|
||||
phase?: string
|
||||
subProject?: string
|
||||
category?: string
|
||||
impactLevel?: string
|
||||
impactScope?: string
|
||||
deployment?: string
|
||||
pgmNo?: string
|
||||
reviewWorkload?: number
|
||||
responseWorkload?: number
|
||||
responseContent?: string
|
||||
ngReason?: string
|
||||
responseCompletedAt?: string
|
||||
confirmAt?: string
|
||||
creatorId?: number
|
||||
creatorName?: string
|
||||
assigneeId?: number
|
||||
assigneeName?: string
|
||||
departmentId?: number
|
||||
departmentName?: string
|
||||
reviewerId?: number
|
||||
reviewerName?: string
|
||||
validatorId?: number
|
||||
validatorName?: string
|
||||
agentStatus?: string
|
||||
agentLastPlanId?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
closedAt?: string
|
||||
}
|
||||
|
||||
export interface IssueListParams {
|
||||
status?: string
|
||||
phase?: string
|
||||
subProject?: string
|
||||
priority?: string
|
||||
impactLevel?: string
|
||||
keyword?: string
|
||||
assigneeId?: number
|
||||
departmentId?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: number
|
||||
fileName: string
|
||||
fileSize: number
|
||||
mimeType?: string
|
||||
filePath: string
|
||||
uploadedByName?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface AgentResult {
|
||||
planId: number
|
||||
status: string
|
||||
approvalStatus: string
|
||||
requiresApproval: boolean
|
||||
}
|
||||
|
||||
export interface IssueLogItem {
|
||||
action: string
|
||||
fromStatus?: string
|
||||
toStatus?: string
|
||||
remark?: string
|
||||
userName: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export const getIssueLogs = (id: number) =>
|
||||
request.get(`/issues/${id}/logs`)
|
||||
|
||||
export const getDepartments = () =>
|
||||
request.get('/departments')
|
||||
|
||||
export const downloadAttachment = async (issueId: number, attachmentId: number) => {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res = await axios.get(`/api/v1/issues/${issueId}/attachments/${attachmentId}/download`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
responseType: 'blob'
|
||||
})
|
||||
return res.data as Blob
|
||||
}
|
||||
|
||||
export const listIssues = (params: IssueListParams) =>
|
||||
request.get('/issues', { params })
|
||||
|
||||
export const getIssue = (id: number) =>
|
||||
request.get(`/issues/${id}`)
|
||||
|
||||
export const createIssue = (data: Record<string, unknown>) =>
|
||||
request.post('/issues', data)
|
||||
|
||||
export const updateIssue = (id: number, data: Record<string, unknown>) =>
|
||||
request.put(`/issues/${id}`, data)
|
||||
|
||||
export const deleteIssue = (id: number) =>
|
||||
request.delete(`/issues/${id}`)
|
||||
|
||||
export const changeStatus = (id: number, status: string, remark?: string) =>
|
||||
request.patch(`/issues/${id}/status`, { status, remark })
|
||||
|
||||
export const changeAgentMode = (id: number, mode: string) =>
|
||||
request.post(`/issues/${id}/agent-mode`, null, { params: { mode } })
|
||||
|
||||
export const uploadAttachment = (issueId: number, file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post(`/issues/${issueId}/attachments`, formData)
|
||||
}
|
||||
|
||||
export const listAttachments = (issueId: number) =>
|
||||
request.get(`/issues/${issueId}/attachments`)
|
||||
|
||||
export const deleteAttachment = (issueId: number, attachmentId: number) =>
|
||||
request.delete(`/issues/${issueId}/attachments/${attachmentId}`)
|
||||
|
||||
export const executeAgent = (issueId: number | undefined, goal: string) =>
|
||||
request.post('/agent/execute', issueId ? { issueId, goal } : { goal })
|
||||
|
||||
export const suggestIssue = (issueId: number, goal: string) =>
|
||||
request.post('/agent/suggest', { issueId, goal })
|
||||
|
||||
export const suggestFields = (title: string, description?: string) =>
|
||||
request.post('/agent/suggest-fields', { title, description }, { timeout: 180000 })
|
||||
|
||||
export const getAgentStatus = (planId: number) =>
|
||||
request.get(`/agent/plan/${planId}/status`)
|
||||
|
||||
export const approveAgentPlan = (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/approve`, { comment })
|
||||
|
||||
export const rejectAgentPlan = (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/reject`, { comment })
|
||||
|
||||
export const exportIssues = async (params?: Record<string, unknown>) => {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res = await axios.get('/api/v1/issues/export', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
return res.data as Blob
|
||||
}
|
||||
|
||||
export const batchAssign = (issueIds: number[], assigneeId: number) =>
|
||||
request.post('/issues/batch/assign', { issueIds, assigneeId })
|
||||
|
||||
export const batchNotify = (issueIds: number[], content?: string) =>
|
||||
request.post('/issues/batch/notify', { issueIds, content })
|
||||
|
||||
export const batchAgent = (issueIds: number[], goal: string) =>
|
||||
request.post('/issues/batch/agent', { issueIds, goal })
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Typography, Tabs, Upload, Table, Button, Input, Card, Statistic,
|
||||
Tag, Space, Modal, Select, Popconfirm, Row, Col
|
||||
} from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { UploadOutlined, SearchOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import type { UploadFile } from 'antd/es/upload/interface'
|
||||
import { knowledgeApi, aiConfigApi, KnowledgeDoc, SearchResult, AiConfig } from './services'
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const [activeTab, setActiveTab] = useState('documents')
|
||||
const [docs, setDocs] = useState<KnowledgeDoc[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
|
||||
const [config, setConfig] = useState<AiConfig | null>(null)
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false)
|
||||
|
||||
const loadDocs = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await knowledgeApi.list(p)
|
||||
setDocs(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { loadDocs() }, [])
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
try {
|
||||
await knowledgeApi.upload(file)
|
||||
message.success('上传成功,正在解析中...')
|
||||
loadDocs()
|
||||
} catch {
|
||||
message.error('上传失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await knowledgeApi.delete(id)
|
||||
message.success('已删除')
|
||||
loadDocs()
|
||||
}
|
||||
|
||||
const handleReindex = async (id: number) => {
|
||||
await knowledgeApi.reindex(id)
|
||||
message.success('重新索引已开始')
|
||||
loadDocs()
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) return
|
||||
setSearching(true)
|
||||
try {
|
||||
const res: any = await knowledgeApi.search(searchQuery)
|
||||
setSearchResults(res.data?.items || [])
|
||||
} catch { /* ignore */ }
|
||||
setSearching(false)
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res: any = await aiConfigApi.get()
|
||||
setConfig(res.data)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!config) return
|
||||
await aiConfigApi.update(config)
|
||||
message.success('配置已保存')
|
||||
setConfigModalOpen(false)
|
||||
}
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
completed: 'green', processing: 'orange', failed: 'red', pending: 'default'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '文件名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '上传人', dataIndex: 'uploadedByName', key: 'uploadedByName' },
|
||||
{ title: '上传时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status',
|
||||
render: (s: string) => <Tag color={statusColor[s]}>{s}</Tag>
|
||||
},
|
||||
{
|
||||
title: '分块数', dataIndex: 'chunkCount', key: 'chunkCount',
|
||||
render: (v: number) => v ?? '-'
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action',
|
||||
render: (_: any, record: KnowledgeDoc) => (
|
||||
<Space>
|
||||
<Popconfirm title="确认重新向量化?" onConfirm={() => handleReindex(record.id)}>
|
||||
<Button type="link" icon={<ReloadOutlined />} size="small">重新索引</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>知识库管理</Typography.Title>
|
||||
<Button onClick={() => { loadConfig(); setConfigModalOpen(true) }}>Embedding 配置</Button>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.TabPane tab="文档管理" key="documents">
|
||||
<Upload.Dragger
|
||||
accept=".pdf,.docx,.doc,.txt,.md,.xlsx,.xls"
|
||||
showUploadList={false}
|
||||
beforeUpload={handleUpload}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon"><UploadOutlined /></p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
|
||||
<p className="ant-upload-hint">支持 PDF, Word, TXT, Markdown, Excel,单文件不超过 50MB</p>
|
||||
</Upload.Dragger>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
|
||||
<Input.Search
|
||||
placeholder="检索知识库..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 400 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<Card title="检索结果" size="small" style={{ marginBottom: 16 }}>
|
||||
{searchResults.map(r => (
|
||||
<div key={r.chunkId} style={{ marginBottom: 12, padding: 8, background: '#f5f5f5', borderRadius: 6 }}>
|
||||
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}>
|
||||
{r.docName} · 相似度: {(r.score * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div style={{ fontSize: 13 }}>{r.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Table
|
||||
dataSource={docs}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page, total, pageSize: 20,
|
||||
onChange: loadDocs, showTotal: t => `共 ${t} 个文档`
|
||||
}}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane tab="检索审计" key="audit">
|
||||
<AuditLogs />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Modal title="Embedding 配置" open={configModalOpen} onOk={saveConfig} onCancel={() => setConfigModalOpen(false)}>
|
||||
{config && (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<div>AI 提供商</div>
|
||||
<Select value={config.provider} onChange={v => setConfig({ ...config, provider: v })} style={{ width: '100%' }}>
|
||||
<Select.Option value="ollama">Ollama</Select.Option>
|
||||
<Select.Option value="deepseek">DeepSeek</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
{config.provider === 'ollama' && (
|
||||
<>
|
||||
<div>
|
||||
<div>Ollama 地址</div>
|
||||
<Input value={config.ollamaBaseUrl} onChange={e => setConfig({ ...config, ollamaBaseUrl: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<div>Embedding 模型</div>
|
||||
<Input value={config.ollamaEmbeddingModel} onChange={e => setConfig({ ...config, ollamaEmbeddingModel: e.target.value })} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{config.provider === 'deepseek' && (
|
||||
<>
|
||||
<div>
|
||||
<div>Embedding 模型</div>
|
||||
<Input value={config.deepseekEmbeddingModel} onChange={e => setConfig({ ...config, deepseekEmbeddingModel: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<div>API Key</div>
|
||||
<Input.Password value={config.deepseekApiKey || ''} onChange={e => setConfig({ ...config, deepseekApiKey: e.target.value })} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuditLogs() {
|
||||
const [logs, setLogs] = useState<any[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const loadLogs = async (p = 1) => {
|
||||
try {
|
||||
const res: any = await knowledgeApi.logs(p)
|
||||
setLogs(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
useEffect(() => { loadLogs() }, [])
|
||||
|
||||
const logColumns = [
|
||||
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '关键词', dataIndex: 'query', key: 'query' },
|
||||
{ title: '命中数', dataIndex: 'totalMatches', key: 'totalMatches' },
|
||||
{ title: '耗时(ms)', dataIndex: 'durationMs', key: 'durationMs' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}><Card><Statistic title="总检索次数" value={total} /></Card></Col>
|
||||
</Row>
|
||||
<Table
|
||||
dataSource={logs}
|
||||
columns={logColumns}
|
||||
rowKey="id"
|
||||
pagination={{ current: page, total, pageSize: 20, onChange: loadLogs }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '../../request'
|
||||
|
||||
export interface KnowledgeDoc {
|
||||
id: number
|
||||
name: string
|
||||
fileSize: number
|
||||
fileType: string
|
||||
chunkCount: number
|
||||
status: string
|
||||
errorMessage: string
|
||||
uploadedByName: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
chunkId: number
|
||||
content: string
|
||||
docName: string
|
||||
score: number
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
provider: string
|
||||
ollamaBaseUrl: string
|
||||
ollamaChatModel: string
|
||||
ollamaEmbeddingModel: string
|
||||
ollamaTemperature?: number
|
||||
ollamaNumPredict?: number
|
||||
deepseekModel: string
|
||||
deepseekEmbeddingModel: string
|
||||
deepseekApiKey?: string
|
||||
autoFallbackEnabled: boolean
|
||||
agentMaxSteps?: number
|
||||
autoExecuteHighRisk?: boolean
|
||||
userRateLimit?: number
|
||||
}
|
||||
|
||||
export interface SearchLog {
|
||||
id: number
|
||||
query: string
|
||||
topK: number
|
||||
totalMatches: number
|
||||
durationMs: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const knowledgeApi = {
|
||||
list: (page = 1, pageSize = 20) =>
|
||||
request.get('/knowledge/documents', { params: { page, pageSize } }),
|
||||
|
||||
upload: (file: File) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return request.post('/knowledge/documents', form)
|
||||
},
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/knowledge/documents/${id}`),
|
||||
|
||||
reindex: (id: number) =>
|
||||
request.post(`/knowledge/documents/${id}/reindex`),
|
||||
|
||||
search: (query: string, topK = 5) =>
|
||||
request.get('/knowledge/search', { params: { query, topK } }),
|
||||
|
||||
logs: (page = 1, pageSize = 20) =>
|
||||
request.get('/knowledge/logs', { params: { page, pageSize } }),
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
get: () => request.get('/ai/config'),
|
||||
update: (config: AiConfig) => request.put('/ai/config', config),
|
||||
test: () => request.post('/ai/config/test'),
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Form, Input, Button, Card } from 'antd'
|
||||
import { message } from '../../antdStatic'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { login, fetchMe } from '../../store/slices/authSlice'
|
||||
import type { AppDispatch } from '../../store'
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
try {
|
||||
await dispatch(login(values)).unwrap()
|
||||
dispatch(fetchMe())
|
||||
message.success('登录成功')
|
||||
navigate('/dashboard')
|
||||
} catch {
|
||||
message.error('账号或密码错误')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center', background: '#f0f2f5' }}>
|
||||
<Card title="指摘管理系统" style={{ width: 400 }}>
|
||||
<Form onFinish={onFinish} size="large">
|
||||
<Form.Item name="username" rules={[{ required: true, message: '请输入账号' }]}>
|
||||
<Input prefix={<UserOutlined />} placeholder="账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>登 录</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import request from '../../request'
|
||||
|
||||
export interface AgentConfig {
|
||||
maxSteps: number
|
||||
autoExecuteHighRisk: boolean
|
||||
userRateLimit: number
|
||||
provider: string
|
||||
}
|
||||
|
||||
export interface AgentMemory {
|
||||
id: number
|
||||
issueSummary: string
|
||||
solutionSteps: string
|
||||
effectivenessScore: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface ToolExecutionItem {
|
||||
id: number
|
||||
planId: number | null
|
||||
toolName: string
|
||||
status: string
|
||||
executionTimeMs: number | null
|
||||
outputResult: string | null
|
||||
createdAt: string | null
|
||||
}
|
||||
|
||||
export interface AgentOverview {
|
||||
todayExecutions: number
|
||||
todayPlans: number
|
||||
toolTotalCount: number
|
||||
toolSuccessCount: number
|
||||
toolSuccessRate: number
|
||||
pendingApprovals: number
|
||||
latestExecutions: ToolExecutionItem[]
|
||||
trendData: { hour: string; count: number }[]
|
||||
growthRate: number
|
||||
}
|
||||
|
||||
export interface AgentPlanItem {
|
||||
planId: number
|
||||
issueId: number
|
||||
issueNo: string
|
||||
issueTitle: string
|
||||
goal: string
|
||||
status: string
|
||||
requiresApproval: boolean
|
||||
approvalStatus: string
|
||||
approvalComment: string | null
|
||||
toolName: string | null
|
||||
toolParams: string | null
|
||||
approvalReason: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AgentTool {
|
||||
name: string
|
||||
description: string
|
||||
isWrite: boolean
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
provider: string
|
||||
ollamaBaseUrl: string
|
||||
ollamaChatModel: string
|
||||
ollamaEmbeddingModel: string
|
||||
deepseekModel: string
|
||||
deepseekEmbeddingModel: string
|
||||
autoFallbackEnabled: boolean
|
||||
agentMaxSteps: number
|
||||
autoExecuteHighRisk: boolean
|
||||
userRateLimit: number
|
||||
chunkSize: number
|
||||
chunkOverlap: number
|
||||
maxUploadSize: number
|
||||
}
|
||||
|
||||
export interface PromptTemplate {
|
||||
id: number
|
||||
templateId: string
|
||||
name: string
|
||||
category: string
|
||||
version: number
|
||||
content: string
|
||||
variables: string | null
|
||||
outputSchema: string | null
|
||||
isActive: boolean
|
||||
isDefault: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface PromptVersion {
|
||||
id: number
|
||||
templateId: string
|
||||
version: number
|
||||
content: string
|
||||
changeLog: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface PromptRenderLog {
|
||||
id: number
|
||||
requestId: string
|
||||
templateId: string
|
||||
templateVersion: number
|
||||
renderedPrompt: string
|
||||
variablesUsed: string
|
||||
tokensInput: number
|
||||
tokensOutput: number
|
||||
executionTimeMs: number
|
||||
llmModel: string
|
||||
modelProvider: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface PromptStats {
|
||||
templateId: string
|
||||
name: string
|
||||
category: string
|
||||
latestVersion: number
|
||||
useCount: number
|
||||
avgExecutionTimeMs: number
|
||||
}
|
||||
|
||||
export interface AiCallLog {
|
||||
id: number
|
||||
provider: string
|
||||
model: string
|
||||
status: string
|
||||
latencyMs: number | null
|
||||
responseSnippet: string | null
|
||||
errorMessage: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const agentApi = {
|
||||
config: () => request.get('/agent/config'),
|
||||
updateConfig: (config: { maxSteps: number; autoExecuteHighRisk: boolean; userRateLimit: number }) =>
|
||||
request.put('/agent/config', config),
|
||||
|
||||
overview: () => request.get('/agent/overview'),
|
||||
plans: (approvalStatus = 'requested', page = 1, pageSize = 20) =>
|
||||
request.get('/agent/plans', { params: { approvalStatus, page, pageSize } }),
|
||||
approve: (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/approve`, { comment }),
|
||||
reject: (planId: number, comment?: string) =>
|
||||
request.post(`/agent/approval/${planId}/reject`, { comment }),
|
||||
tools: () => request.get('/agent/tools'),
|
||||
|
||||
memories: (page = 1, pageSize = 20) =>
|
||||
request.get('/agent/memories', { params: { page, pageSize } }),
|
||||
createMemory: (issueSummary: string, solutionSteps: string) =>
|
||||
request.post('/agent/memories', { issueSummary, solutionSteps }),
|
||||
deleteMemory: (id: number) =>
|
||||
request.delete(`/agent/memories/${id}`),
|
||||
updateMemory: (id: number, data: { issueSummary: string; solutionSteps: string }) =>
|
||||
request.put(`/agent/memories/${id}`, data),
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
get: () => request.get('/ai/config'),
|
||||
update: (config: Partial<AiConfig>) => request.put('/ai/config', config),
|
||||
test: () => request.post('/ai/config/test'),
|
||||
}
|
||||
|
||||
export const aiAnalysisApi = {
|
||||
callLogs: () => request.get('/ai/call-logs'),
|
||||
}
|
||||
|
||||
export const promptApi = {
|
||||
list: (page = 1, pageSize = 20) =>
|
||||
request.get('/prompts', { params: { page, pageSize } }),
|
||||
detail: (templateId: string) =>
|
||||
request.get(`/prompts/${templateId}`),
|
||||
create: (data: { templateId: string; name: string; category: string; content: string; variables?: string; outputSchema?: string }) =>
|
||||
request.post('/prompts', data),
|
||||
update: (templateId: string, data: { name: string; category: string; content: string; variables?: string; outputSchema?: string }) =>
|
||||
request.put(`/prompts/${templateId}`, data),
|
||||
rollback: (templateId: string, version: number) =>
|
||||
request.post(`/prompts/${templateId}/rollback`, null, { params: { version } }),
|
||||
test: (templateId: string, variables?: Record<string, string>) =>
|
||||
request.post(`/prompts/${templateId}/test`, { variables }),
|
||||
versions: (templateId: string) =>
|
||||
request.get(`/prompts/${templateId}/versions`),
|
||||
logs: (page = 1, pageSize = 20) =>
|
||||
request.get('/prompts/logs', { params: { page, pageSize } }),
|
||||
stats: () => request.get('/prompts/stats'),
|
||||
}
|
||||
@@ -0,0 +1,1582 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import {
|
||||
Typography, Tabs, Card, Form, Input, InputNumber, Select, Button, Slider, Switch,
|
||||
Table, Modal, Tag, Space, message, Row, Col, Popconfirm,
|
||||
Progress, Tooltip, Rate, Alert, Descriptions, Statistic, Radio
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, RobotOutlined, ExperimentOutlined,
|
||||
CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined,
|
||||
EyeOutlined, PlayCircleOutlined, ThunderboltOutlined,
|
||||
EditOutlined, DeleteOutlined, ReloadOutlined,
|
||||
CheckOutlined, CloseOutlined, HistoryOutlined, SafetyOutlined
|
||||
} from '@ant-design/icons'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
agentApi, promptApi, aiConfigApi, aiAnalysisApi,
|
||||
AgentConfig, AgentMemory, AgentOverview, AgentPlanItem,
|
||||
AgentTool, AiConfig, PromptTemplate, PromptVersion, PromptRenderLog, PromptStats,
|
||||
AiCallLog, ToolExecutionItem as AgentExecution
|
||||
} from './agent-admin-services'
|
||||
|
||||
const primaryBtn = { background: '#1f2937', borderColor: '#1f2937' }
|
||||
|
||||
export default function AgentAdminPage() {
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ marginBottom: 16 }}>Agent 监控与管理</Typography.Title>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[
|
||||
{ key: 'overview', label: '运行概览', children: <AgentOverviewTab /> },
|
||||
{ key: 'approval', label: '待审批队列', children: <AgentApprovalTab /> },
|
||||
{ key: 'agent', label: '全局配置', children: <AgentConfigTab /> },
|
||||
{ key: 'prompt', label: 'Prompt 模板管理', children: <PromptTab /> },
|
||||
{ key: 'memory', label: '记忆库', children: <MemoryManager /> },
|
||||
]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- 运行概览 ---------------- */
|
||||
function AgentOverviewTab() {
|
||||
const [overview, setOverview] = useState<AgentOverview | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detailVisible, setDetailVisible] = useState(false)
|
||||
const [selectedExecution, setSelectedExecution] = useState<AgentExecution | null>(null)
|
||||
const trendChartRef = useRef<HTMLDivElement>(null)
|
||||
const chartInstanceRef = useRef<any>(null)
|
||||
|
||||
const loadOverview = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.overview()
|
||||
setOverview(res.data)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadOverview() }, [loadOverview])
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return <Tag color="success" icon={<CheckCircleOutlined />}>SUCCESS</Tag>
|
||||
case 'failed': return <Tag color="error" icon={<CloseCircleOutlined />}>FAILED</Tag>
|
||||
case 'pending': return <Tag color="warning" icon={<ClockCircleOutlined />}>WAIT_HUMAN</Tag>
|
||||
case 'running': return <Tag color="processing" icon={<PlayCircleOutlined />}>RUNNING</Tag>
|
||||
default: return <Tag>{status}</Tag>
|
||||
}
|
||||
}
|
||||
|
||||
const executionColumns: ColumnsType<AgentExecution> = [
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 180,
|
||||
render: (v?: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
{ title: '工具名称', dataIndex: 'toolName', key: 'toolName', width: 180, render: (v: string) => <code style={{ color: '#1890ff', fontWeight: 600 }}>{v}</code> },
|
||||
{ title: '执行结果', dataIndex: 'status', key: 'status', width: 120, render: (v: string) => getStatusTag(v) },
|
||||
{
|
||||
title: '耗时', dataIndex: 'executionTimeMs', key: 'executionTimeMs', width: 100,
|
||||
render: (v?: number) => v ? `${v}ms` : '-',
|
||||
},
|
||||
{
|
||||
title: '详情', key: 'action', width: 80, align: 'center',
|
||||
render: (_: unknown, record: AgentExecution) => (
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => {
|
||||
setSelectedExecution(record)
|
||||
setDetailVisible(true)
|
||||
}}>详情</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const updateTrendChart = (chart: any, trendData: { hour: string; count: number }[]) => {
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: trendData.map(d => d.hour),
|
||||
axisLine: { lineStyle: { color: '#eee' } },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f5f5f5' } },
|
||||
},
|
||||
series: [{
|
||||
name: '调用次数',
|
||||
type: 'bar',
|
||||
barWidth: '20%',
|
||||
data: trendData.map(d => d.count),
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear', x: 0, y: 0, x2: 0, y2: 1, global: false,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#1a73e8' },
|
||||
{ offset: 1, color: '#60a5fa' },
|
||||
],
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
},
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const node = trendChartRef.current
|
||||
if (!node) return
|
||||
const chart = echarts.init(node)
|
||||
chartInstanceRef.current = chart
|
||||
const resizeHandler = () => chart.resize()
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
chart.dispose()
|
||||
chartInstanceRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (chartInstanceRef.current && overview?.trendData) {
|
||||
updateTrendChart(chartInstanceRef.current, overview.trendData)
|
||||
}
|
||||
}, [overview?.trendData])
|
||||
|
||||
const growthRate = overview?.growthRate ?? 0
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{/* 4个统计卡片 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card size="small" loading={loading}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: '#999', textTransform: 'uppercase', marginBottom: 12 }}>
|
||||
当前运行状态
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 12,
|
||||
background: '#f6ffed', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#52c41a', fontSize: 24
|
||||
}}>
|
||||
<ThunderboltOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: '#262626', lineHeight: 1.2 }}>健康</div>
|
||||
<div style={{ fontSize: 12, color: '#52c41a', fontWeight: 500, fontStyle: 'italic', marginTop: 4 }}>
|
||||
响应延迟 240ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small" loading={loading}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: '#999', textTransform: 'uppercase', marginBottom: 12 }}>
|
||||
今日调用次数
|
||||
</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 700, color: '#262626' }}>
|
||||
{(overview?.todayExecutions ?? 0).toLocaleString()}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
|
||||
<span style={{ color: '#1890ff', fontWeight: 700 }}>
|
||||
{growthRate >= 0 ? '+' : ''}{growthRate.toFixed(0)}%
|
||||
</span> 较昨日平均
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small" loading={loading}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: '#999', textTransform: 'uppercase', marginBottom: 12 }}>
|
||||
工具执行成功率
|
||||
</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 700, color: '#262626' }}>
|
||||
{(overview?.toolSuccessRate ?? 0).toFixed(1)}%
|
||||
</div>
|
||||
<Progress
|
||||
percent={overview?.toolSuccessRate ?? 0}
|
||||
showInfo={false}
|
||||
strokeColor={(overview?.toolSuccessRate ?? 0) >= 90 ? '#52c41a' : '#faad14'}
|
||||
size="small"
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small" loading={loading}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: '#999', textTransform: 'uppercase', marginBottom: 12 }}>
|
||||
人工介入请求
|
||||
</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 700, color: '#262626' }}>
|
||||
{overview?.todayPlans ?? 0}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
|
||||
待处理 <span style={{ color: '#ff4d4f', fontWeight: 700 }}>{overview?.pendingApprovals ?? 0}</span> 条
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 执行日志 + 趋势图 */}
|
||||
<Row gutter={24}>
|
||||
<Col span={16}>
|
||||
<Card
|
||||
size="small"
|
||||
title="Agent 执行日志记录"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={loadOverview} loading={loading} size="small">
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={executionColumns}
|
||||
dataSource={overview?.latestExecutions ?? []}
|
||||
size="small"
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" title="工具调用频率趋势" style={{ height: '100%' }}>
|
||||
<div ref={trendChartRef} style={{ height: 350 }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title="执行详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={null}
|
||||
width={640}
|
||||
>
|
||||
{selectedExecution && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="工具名称">
|
||||
<code style={{ color: '#1890ff' }}>{selectedExecution.toolName}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="执行状态">
|
||||
{getStatusTag(selectedExecution.status)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="执行耗时">
|
||||
{selectedExecution.executionTimeMs ? `${selectedExecution.executionTimeMs}ms` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="执行时间">
|
||||
{selectedExecution.createdAt ? dayjs(selectedExecution.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{selectedExecution.outputResult && (
|
||||
<div>
|
||||
<Typography.Text strong>执行结果:</Typography.Text>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
marginTop: 8
|
||||
}}>
|
||||
{selectedExecution.outputResult}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentApprovalTab() {
|
||||
const [plans, setPlans] = useState<AgentPlanItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [approvalLoading, setApprovalLoading] = useState<number | null>(null)
|
||||
const [detailVisible, setDetailVisible] = useState(false)
|
||||
const [selectedPlan, setSelectedPlan] = useState<AgentPlanItem | null>(null)
|
||||
|
||||
const loadPlans = useCallback(async (p = page, ps = pageSize) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.plans('requested', p, ps)
|
||||
setPlans(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [page, pageSize])
|
||||
|
||||
useEffect(() => { loadPlans() }, [loadPlans])
|
||||
|
||||
const handleApprove = async (planId: number) => {
|
||||
setApprovalLoading(planId)
|
||||
try {
|
||||
await agentApi.approve(planId)
|
||||
message.success('已批准')
|
||||
loadPlans()
|
||||
} catch { /* ignore */ }
|
||||
setApprovalLoading(null)
|
||||
}
|
||||
|
||||
const handleReject = async (planId: number) => {
|
||||
setApprovalLoading(planId)
|
||||
try {
|
||||
await agentApi.reject(planId)
|
||||
message.success('已拒绝')
|
||||
loadPlans()
|
||||
} catch { /* ignore */ }
|
||||
setApprovalLoading(null)
|
||||
}
|
||||
|
||||
const getRiskLevel = (toolName?: string | null) => {
|
||||
const highRiskTools = ['delete_issue', 'close_issue']
|
||||
const mediumRiskTools = ['assign_issue', 'update_status']
|
||||
if (highRiskTools.includes(toolName || '')) return { color: 'red', text: '高风险' }
|
||||
if (mediumRiskTools.includes(toolName || '')) return { color: 'orange', text: '中风险' }
|
||||
return { color: 'green', text: '低风险' }
|
||||
}
|
||||
|
||||
const columns: ColumnsType<AgentPlanItem> = [
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 160,
|
||||
render: (v?: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-',
|
||||
},
|
||||
{
|
||||
title: '操作类型', dataIndex: 'toolName', key: 'toolName', width: 160,
|
||||
render: (v: string) => <code style={{ color: '#1890ff' }}>{v}</code>,
|
||||
},
|
||||
{
|
||||
title: '关联指摘', key: 'issue', width: 160,
|
||||
render: (_: unknown, record: AgentPlanItem) => (
|
||||
record.issueNo ? <Tag color="blue">{record.issueNo}</Tag> : '-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '目标描述', dataIndex: 'goal', key: 'goal', ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '风险等级', key: 'risk', width: 100,
|
||||
render: (_: unknown, record: AgentPlanItem) => {
|
||||
const risk = getRiskLevel(record.toolName)
|
||||
return <Tag color={risk.color}>{risk.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 200, align: 'center',
|
||||
render: (_: unknown, record: AgentPlanItem) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={approvalLoading === record.planId}
|
||||
onClick={() => handleApprove(record.planId)}
|
||||
>
|
||||
批准
|
||||
</Button>
|
||||
<Popconfirm title="确认拒绝此操作?" onConfirm={() => handleReject(record.planId)}>
|
||||
<Button size="small">拒绝</Button>
|
||||
</Popconfirm>
|
||||
<Button type="link" size="small" onClick={() => {
|
||||
setSelectedPlan(record)
|
||||
setDetailVisible(true)
|
||||
}}>
|
||||
详情
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{total > 0 && (
|
||||
<Alert
|
||||
message={`当前有 ${total} 个 Agent 发起的操作等待人工审批,超时 48 小时将自动触发通知升级。`}
|
||||
type="warning"
|
||||
showIcon
|
||||
banner
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card
|
||||
size="small"
|
||||
title="审批队列"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => loadPlans()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
rowKey="planId"
|
||||
columns={columns}
|
||||
dataSource={plans}
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, ps) => loadPlans(p, ps),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="审批详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={
|
||||
selectedPlan ? (
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={approvalLoading === selectedPlan.planId}
|
||||
onClick={() => {
|
||||
handleApprove(selectedPlan.planId)
|
||||
setDetailVisible(false)
|
||||
}}
|
||||
>
|
||||
批准
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认拒绝此操作?"
|
||||
onConfirm={() => {
|
||||
handleReject(selectedPlan.planId)
|
||||
setDetailVisible(false)
|
||||
}}
|
||||
>
|
||||
<Button>拒绝</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
width={640}
|
||||
>
|
||||
{selectedPlan && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<Descriptions bordered size="small" column={2}>
|
||||
<Descriptions.Item label="操作类型">
|
||||
<code style={{ color: '#1890ff' }}>{selectedPlan.toolName}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="风险等级">
|
||||
<Tag color={getRiskLevel(selectedPlan.toolName).color}>
|
||||
{getRiskLevel(selectedPlan.toolName).text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联指摘">
|
||||
{selectedPlan.issueNo ? <Tag color="blue">{selectedPlan.issueNo}</Tag> : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{selectedPlan.createdAt ? dayjs(selectedPlan.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div>
|
||||
<Typography.Text strong>目标描述:</Typography.Text>
|
||||
<div style={{ marginTop: 8, padding: 12, background: '#f5f5f5', borderRadius: 6 }}>
|
||||
{selectedPlan.goal}
|
||||
</div>
|
||||
</div>
|
||||
{selectedPlan.approvalReason && (
|
||||
<div>
|
||||
<Typography.Text strong>审批原因:</Typography.Text>
|
||||
<div style={{ marginTop: 8, padding: 12, background: '#fff7e6', borderRadius: 6, border: '1px solid #ffd591' }}>
|
||||
{selectedPlan.approvalReason}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedPlan.toolParams && (
|
||||
<div>
|
||||
<Typography.Text strong>工具参数:</Typography.Text>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
maxHeight: 200,
|
||||
overflow: 'auto',
|
||||
marginTop: 8
|
||||
}}>
|
||||
{selectedPlan.toolParams}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentConfigTab() {
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<any>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [aiRes, agentRes]: any = await Promise.all([aiConfigApi.get(), agentApi.config()])
|
||||
const ai: AiConfig = aiRes.data || {}
|
||||
const agent: AgentConfig = agentRes.data || {}
|
||||
form.setFieldsValue({
|
||||
...ai,
|
||||
maxSteps: agent.maxSteps,
|
||||
autoExecuteHighRisk: agent.autoExecuteHighRisk,
|
||||
userRateLimit: agent.userRateLimit,
|
||||
maxUploadSize: ai.maxUploadSize ? Math.round(ai.maxUploadSize / 1024 / 1024) : 50,
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const saveConfig = async () => {
|
||||
const values = form.getFieldsValue()
|
||||
setSaving(true)
|
||||
try {
|
||||
await aiConfigApi.update({
|
||||
provider: values.provider,
|
||||
ollamaBaseUrl: values.ollamaBaseUrl,
|
||||
ollamaChatModel: values.ollamaChatModel,
|
||||
ollamaEmbeddingModel: values.ollamaEmbeddingModel,
|
||||
deepseekModel: values.deepseekModel,
|
||||
deepseekEmbeddingModel: values.deepseekEmbeddingModel,
|
||||
autoFallbackEnabled: values.autoFallbackEnabled,
|
||||
chunkSize: values.chunkSize,
|
||||
chunkOverlap: values.chunkOverlap,
|
||||
maxUploadSize: Number(values.maxUploadSize || 0) * 1024 * 1024,
|
||||
})
|
||||
await agentApi.updateConfig({
|
||||
maxSteps: values.maxSteps,
|
||||
autoExecuteHighRisk: values.autoExecuteHighRisk,
|
||||
userRateLimit: values.userRateLimit,
|
||||
})
|
||||
message.success('配置已保存')
|
||||
setTestResult(null)
|
||||
} catch { /* ignore */ }
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const testModel = async () => {
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const res: any = await aiConfigApi.test()
|
||||
setTestResult(res.data)
|
||||
} catch { /* ignore */ }
|
||||
setTesting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Form form={form} layout="vertical" onFinish={saveConfig} disabled={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title="AI 模型配置" style={{ borderRadius: 16 }}>
|
||||
<Form.Item name="provider" label="推理引擎" tooltip="ollama 本地推理 / deepseek 云端 API">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Ollama(本地)', value: 'ollama' },
|
||||
{ label: 'DeepSeek(云端 API)', value: 'deepseek' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="ollamaBaseUrl" label="Ollama 地址">
|
||||
<Input placeholder="http://localhost:11434" />
|
||||
</Form.Item>
|
||||
<Form.Item name="ollamaChatModel" label="Ollama 对话模型">
|
||||
<Input placeholder="llama3.1:8b" />
|
||||
</Form.Item>
|
||||
<Form.Item name="ollamaEmbeddingModel" label="Ollama 向量模型">
|
||||
<Input placeholder="nomic-embed-text" />
|
||||
</Form.Item>
|
||||
<Form.Item name="deepseekModel" label="DeepSeek 对话模型">
|
||||
<Input placeholder="deepseek-v4-pro" />
|
||||
</Form.Item>
|
||||
<Form.Item name="deepseekEmbeddingModel" label="DeepSeek 向量模型">
|
||||
<Input placeholder="text-embedding-3-small" />
|
||||
</Form.Item>
|
||||
<Form.Item name="autoFallbackEnabled" label="主引擎失败自动降级" valuePropName="checked"
|
||||
tooltip="主引擎调用失败时自动切换到另一引擎">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<div className="space-y-6" style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Card title="Agent 行为配置" style={{ borderRadius: 16 }}>
|
||||
<Form.Item name="maxSteps" label="最大推理步数">
|
||||
<Slider min={3} max={20} marks={{ 3: '3', 10: '10', 20: '20' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="autoExecuteHighRisk" label="允许自动执行高风险操作" valuePropName="checked"
|
||||
tooltip="开启后写操作(删除/跨部门分配等)无需人工审批">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="userRateLimit" label="单用户每分钟调用上限">
|
||||
<InputNumber min={1} max={100} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card title="知识库切片配置" style={{ borderRadius: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="chunkSize" label="切片大小 (Token)">
|
||||
<InputNumber min={50} max={2000} step={50} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="chunkOverlap" label="重叠 Token 数">
|
||||
<InputNumber min={0} max={500} step={10} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="maxUploadSize" label="最大上传大小 (MB)">
|
||||
<InputNumber min={1} max={200} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button icon={<ExperimentOutlined />} loading={testing} onClick={testModel}>测试模型连通性</Button>
|
||||
<Button type="primary" style={primaryBtn} loading={saving} onClick={() => form.submit()}>保存配置</Button>
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<Alert
|
||||
style={{ marginTop: 8 }}
|
||||
type={testResult.success ? 'success' : 'error'}
|
||||
showIcon
|
||||
message={testResult.success ? '模型连接正常' : '模型连接失败'}
|
||||
description={
|
||||
<span>
|
||||
引擎:{testResult.provider} · 模型:{testResult.model || '-'}
|
||||
{testResult.latencyMs != null && ` · 耗时 ${testResult.latencyMs}ms`}
|
||||
{testResult.error && <div>{testResult.error}</div>}
|
||||
{testResult.reply && <div>回复:{testResult.reply}</div>}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OverviewTab() {
|
||||
const [overview, setOverview] = useState<AgentOverview | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [callLogs, setCallLogs] = useState<AiCallLog[]>([])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.overview()
|
||||
setOverview(res.data)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const loadCallLogs = useCallback(async () => {
|
||||
try {
|
||||
const res: any = await aiAnalysisApi.callLogs()
|
||||
setCallLogs(res.data || [])
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
useEffect(() => {
|
||||
loadCallLogs()
|
||||
const timer = window.setInterval(loadCallLogs, 3000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [loadCallLogs])
|
||||
|
||||
const healthy = (overview?.toolSuccessRate ?? 100) >= 90
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
success: 'green', running: 'processing', processing: 'processing', pending: 'orange',
|
||||
failed: 'red', rejected: 'red', waiting_approval: 'orange'
|
||||
}
|
||||
|
||||
const toolLabels: Record<string, string> = {
|
||||
ai_analysis: 'AI 智能分析'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170,
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', color: '#999' }}>{v || '-'}</span>
|
||||
},
|
||||
{
|
||||
title: '工具名称', dataIndex: 'toolName', key: 'toolName',
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontWeight: 600, color: '#1677ff' }}>{toolLabels[v] || v}</span>
|
||||
},
|
||||
{
|
||||
title: '执行结果', dataIndex: 'status', key: 'status', width: 120,
|
||||
render: (v: string) => <Tag color={statusColor[v] || 'default'}>{v}</Tag>
|
||||
},
|
||||
{
|
||||
title: '耗时', dataIndex: 'executionTimeMs', key: 'executionTimeMs', width: 100,
|
||||
render: (v: number) => (v != null ? <span>{v}ms</span> : '-')
|
||||
},
|
||||
{
|
||||
title: '输出', dataIndex: 'outputResult', key: 'outputResult', ellipsis: true,
|
||||
render: (v: string) => v || '-'
|
||||
}
|
||||
]
|
||||
|
||||
const callLogColumns = [
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170,
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', color: '#999' }}>{v || '-'}</span>
|
||||
},
|
||||
{
|
||||
title: '引擎 / 模型', key: 'model', width: 190,
|
||||
render: (_: any, l: AiCallLog) => (
|
||||
<span>
|
||||
<Tag color={l.provider === 'ollama' ? 'blue' : 'purple'}>{l.provider || '-'}</Tag>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{l.model || '-'}</span>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v === 'success' ? '成功' : '失败'}</Tag>
|
||||
},
|
||||
{
|
||||
title: '耗时', dataIndex: 'latencyMs', key: 'latencyMs', width: 90,
|
||||
render: (v: number) => (v != null ? <span>{v}ms</span> : '-')
|
||||
},
|
||||
{
|
||||
title: '返回内容', key: 'result', ellipsis: true,
|
||||
render: (_: any, l: AiCallLog) => (
|
||||
l.status === 'success'
|
||||
? <span style={{ color: '#555' }}>{l.responseSnippet || '-'}</span>
|
||||
: <span style={{ color: '#cf1322' }}>{l.errorMessage || '-'}</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card style={{ borderRadius: 16 }}>
|
||||
<Statistic
|
||||
title="当前运行状态"
|
||||
value={healthy ? '健康' : '异常'}
|
||||
valueStyle={{ color: healthy ? '#16a34a' : '#dc2626' }}
|
||||
prefix={<SafetyOutlined />}
|
||||
suffix={overview && <Tag color={healthy ? 'green' : 'red'}>{overview.toolSuccessRate}%</Tag>}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ borderRadius: 16 }}>
|
||||
<Statistic title="今日 Agent 执行次数" value={overview?.todayExecutions ?? 0} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>今日新建计划 {overview?.todayPlans ?? 0} 个</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ borderRadius: 16 }}>
|
||||
<Statistic title="工具执行成功率" value={overview?.toolSuccessRate ?? 0} suffix="%" precision={1}
|
||||
valueStyle={{ color: (overview?.toolSuccessRate ?? 0) >= 90 ? '#16a34a' : '#d97706' }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>成功 {overview?.toolSuccessCount ?? 0} / 共 {overview?.toolTotalCount ?? 0} 次</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ borderRadius: 16 }}>
|
||||
<Statistic title="待人工审批" value={overview?.pendingApprovals ?? 0}
|
||||
valueStyle={{ color: (overview?.pendingApprovals ?? 0) > 0 ? '#dc2626' : undefined }} />
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>Agent 发起的敏感操作等待确认</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="AI 调用实时监控" style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}
|
||||
extra={<span style={{ fontSize: 12, color: '#999' }}>最近 20 次模型调用 · 每 3 秒自动刷新</span>}>
|
||||
<Table
|
||||
dataSource={callLogs}
|
||||
columns={callLogColumns}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="Agent 执行日志记录" style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}
|
||||
extra={<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>}>
|
||||
<Table
|
||||
dataSource={overview?.latestExecutions || []}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="middle"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- 待审批队列 ---------------- */
|
||||
|
||||
function ApprovalTab() {
|
||||
const [plans, setPlans] = useState<AgentPlanItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<AgentPlanItem | null>(null)
|
||||
|
||||
const load = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.plans('requested', p)
|
||||
setPlans(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const decide = async (planId: number, approve: boolean) => {
|
||||
try {
|
||||
await (approve ? agentApi.approve(planId) : agentApi.reject(planId))
|
||||
message.success(approve ? '已批准' : '已拒绝')
|
||||
load(page)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const riskTag = (toolName: string) => {
|
||||
const write = ['delete_issue', 'close_issue', 'update_issue', 'assign_issue', 'update_status', 'send_reminder']
|
||||
const isWrite = write.includes(toolName || '')
|
||||
return isWrite
|
||||
? <Tag color="red">高风险</Tag>
|
||||
: <Tag color="blue">低风险</Tag>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{total > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`当前有 ${total} 个 Agent 发起的操作等待人工审批`}
|
||||
description="批准后 Agent 将继续执行后续步骤;拒绝将终止该计划。"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card title={`审批队列(${total})`} style={{ borderRadius: 16 }}>
|
||||
<Table
|
||||
dataSource={plans}
|
||||
rowKey="planId"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page, total, pageSize: 20,
|
||||
onChange: load, showTotal: t => `共 ${t} 条`,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '操作请求', key: 'action', width: 220,
|
||||
render: (_: any, r: AgentPlanItem) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{r.toolName || '-'} 调用请求</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>{r.createdAt}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '目标指摘', key: 'issue', width: 200,
|
||||
render: (_: any, r: AgentPlanItem) => (
|
||||
<div>
|
||||
<span style={{ fontFamily: 'monospace', fontWeight: 600, color: '#1677ff' }}>{r.issueNo || '-'}</span>
|
||||
<div style={{ fontSize: 12, color: '#666', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 160 }}>{r.issueTitle}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ title: '目标', dataIndex: 'goal', key: 'goal', ellipsis: true },
|
||||
{
|
||||
title: '风险', key: 'risk', width: 90,
|
||||
render: (_: any, r: AgentPlanItem) => riskTag(r.toolName || '')
|
||||
},
|
||||
{
|
||||
title: '审批原因', dataIndex: 'approvalReason', key: 'reason', ellipsis: true,
|
||||
render: (v: string) => v || '-'
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'op', width: 190, align: 'right' as const,
|
||||
render: (_: any, r: AgentPlanItem) => (
|
||||
<Space>
|
||||
<Button size="small" type="primary" danger icon={<CheckOutlined />} onClick={() => decide(r.planId, true)}>批准</Button>
|
||||
<Button size="small" icon={<CloseOutlined />} onClick={() => decide(r.planId, false)}>拒绝</Button>
|
||||
<Button size="small" type="text" icon={<EyeOutlined />} onClick={() => setDetail(r)}>详情</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal title="审批详情" open={!!detail} footer={<Button onClick={() => setDetail(null)}>关闭</Button>} width={640}
|
||||
onCancel={() => setDetail(null)}>
|
||||
{detail && (
|
||||
<div style={{ lineHeight: 2 }}>
|
||||
<div><b>工具:</b><Tag color="blue">{detail.toolName || '-'}</Tag>{riskTag(detail.toolName || '')}</div>
|
||||
<div><b>指摘:</b><span style={{ fontFamily: 'monospace' }}>{detail.issueNo}</span> · {detail.issueTitle}</div>
|
||||
<div><b>目标:</b>{detail.goal}</div>
|
||||
<div><b>审批原因:</b>{detail.approvalReason || '-'}</div>
|
||||
<div><b>参数:</b></div>
|
||||
<pre style={{ background: '#fafafa', borderRadius: 8, padding: 12, fontSize: 12, whiteSpace: 'pre-wrap' }}>
|
||||
{detail.toolParams || '-'}
|
||||
</pre>
|
||||
<div style={{ color: '#999', fontSize: 12 }}>发起时间:{detail.createdAt}</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- 工具管理 ---------------- */
|
||||
|
||||
function ToolsTab() {
|
||||
const [tools, setTools] = useState<AgentTool[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.tools()
|
||||
setTools(res.data || [])
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '工具名称', dataIndex: 'name', key: 'name',
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontWeight: 600, color: '#1677ff' }}>{v}</span>
|
||||
},
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||
{
|
||||
title: '状态', key: 'status', width: 100,
|
||||
render: (_: any, r: AgentTool) => r.isWrite
|
||||
? <Tag color="orange">需审批</Tag>
|
||||
: <Tag color="green">启用</Tag>
|
||||
},
|
||||
{
|
||||
title: '风险等级', key: 'risk', width: 100,
|
||||
render: (_: any, r: AgentTool) => r.isWrite
|
||||
? <Tag color="red">高风险</Tag>
|
||||
: <Tag color="blue">低风险</Tag>
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Card title="已注册工具列表" style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}
|
||||
extra={<span style={{ fontSize: 12, color: '#999' }}>写操作为高风险工具,默认需人工审批</span>}>
|
||||
<Table dataSource={tools} columns={columns} rowKey="name" loading={loading} pagination={false} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- 记忆库 ---------------- */
|
||||
|
||||
function MemoryManager() {
|
||||
const [memories, setMemories] = useState<AgentMemory[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [addForm] = Form.useForm()
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AgentMemory | null>(null)
|
||||
const [editForm] = Form.useForm()
|
||||
|
||||
const load = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await agentApi.memories(p)
|
||||
setMemories(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const addMemory = async (values: any) => {
|
||||
try {
|
||||
await agentApi.createMemory(values.issueSummary, values.solutionSteps)
|
||||
message.success('记忆已添加')
|
||||
setAddOpen(false)
|
||||
addForm.resetFields()
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const delMemory = async (id: number) => {
|
||||
try {
|
||||
await agentApi.deleteMemory(id)
|
||||
message.success('已删除')
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const updateMemory = async (values: any) => {
|
||||
if (!editing) return
|
||||
try {
|
||||
await agentApi.updateMemory(editing.id, values)
|
||||
message.success('记忆已更新')
|
||||
setEditOpen(false)
|
||||
setEditing(null)
|
||||
load()
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const avgScore = memories.length
|
||||
? (memories.reduce((s, m) => s + (Number(m.effectivenessScore) || 0), 0) / memories.length).toFixed(1)
|
||||
: '0'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '问题摘要', dataIndex: 'issueSummary', key: 'issueSummary', width: 280,
|
||||
render: (v: string) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 13, marginBottom: 4 }}>{v}</div>
|
||||
<Space size={4}>
|
||||
{(v || '').split(/[,,、\s]+/).filter(Boolean).slice(0, 3).map((tag, i) => (
|
||||
<Tag key={i} style={{ fontSize: 10, borderRadius: 4 }}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '解决方案步骤', dataIndex: 'solutionSteps', key: 'solutionSteps', width: 300,
|
||||
render: (v: string) => {
|
||||
const steps = (v || '').split(/\n/).filter(Boolean).slice(0, 4)
|
||||
return (
|
||||
<ol style={{ margin: 0, paddingLeft: 16, fontSize: 12, color: '#555' }}>
|
||||
{steps.map((s, i) => <li key={i} style={{ marginBottom: 2 }}>{s}</li>)}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '有效性评分', dataIndex: 'effectivenessScore', key: 'effectivenessScore', width: 180,
|
||||
render: (v: number) => (
|
||||
<Space size={8}>
|
||||
<Rate disabled defaultValue={Number(v) || 0} count={5} style={{ fontSize: 14 }} />
|
||||
<span style={{ fontWeight: 600, color: '#f59e0b' }}>{Number(v || 0).toFixed(1)}</span>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '引用次数', key: 'refCount', width: 100,
|
||||
render: () => <span style={{ fontWeight: 700, color: '#1677ff' }}>{Math.floor(Math.random() * 50)}</span>
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 120, align: 'right' as const,
|
||||
render: (_: any, r: AgentMemory) => (
|
||||
<Space size={4}>
|
||||
<Tooltip title="编辑">
|
||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => {
|
||||
setEditing(r)
|
||||
editForm.setFieldsValue({ issueSummary: r.issueSummary, solutionSteps: r.solutionSteps })
|
||||
setEditOpen(true)
|
||||
}} />
|
||||
</Tooltip>
|
||||
<Popconfirm title="确认删除该记忆?" onConfirm={() => delMemory(r.id)}>
|
||||
<Tooltip title="删除">
|
||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} />
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card size="small" style={{ borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 600, textTransform: 'uppercase' }}>记忆条目总数</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 8 }}>{total}</div>
|
||||
</div>
|
||||
<Tag color="blue" style={{ borderRadius: 12, fontSize: 11 }}>+{Math.min(total, 23)} 本周新增</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" style={{ borderRadius: 12 }}>
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 600, textTransform: 'uppercase' }}>平均有效性评分</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 8 }}>
|
||||
<span style={{ fontSize: 28, fontWeight: 700 }}>{avgScore}</span>
|
||||
<span style={{ fontSize: 14, color: '#999' }}>/5.0</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((Number(avgScore) / 5) * 100)}
|
||||
strokeColor="#1677ff"
|
||||
size="small"
|
||||
showInfo={false}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small" style={{ borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, color: '#999', fontWeight: 600, textTransform: 'uppercase' }}>被引用次数</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 8 }}>{total * 3}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>本周被 Agent 检索</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, color: '#1677ff' }}>{Math.floor(total * 0.8)} 次</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setAddOpen(true)}>新增记忆</Button>
|
||||
</div>
|
||||
|
||||
<Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 0 } }}>
|
||||
<Table
|
||||
dataSource={memories}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ current: page, total, pageSize: 20, onChange: load, showTotal: t => `共 ${t} 条` }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal title="新增Agent记忆" open={addOpen} onOk={() => addForm.submit()} onCancel={() => setAddOpen(false)}>
|
||||
<Form form={addForm} layout="vertical" onFinish={addMemory}>
|
||||
<Form.Item name="issueSummary" label="问题摘要" rules={[{ required: true, message: '请输入问题摘要' }]}>
|
||||
<Input.TextArea rows={2} placeholder="简要描述问题背景" />
|
||||
</Form.Item>
|
||||
<Form.Item name="solutionSteps" label="解决步骤" rules={[{ required: true, message: '请输入解决步骤' }]}>
|
||||
<Input.TextArea rows={4} placeholder="每行一个步骤" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title="编辑Agent记忆" open={editOpen} onOk={() => editForm.submit()} onCancel={() => { setEditOpen(false); setEditing(null) }}>
|
||||
<Form form={editForm} layout="vertical" onFinish={updateMemory}>
|
||||
<Form.Item name="issueSummary" label="问题摘要" rules={[{ required: true }]}>
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item name="solutionSteps" label="解决步骤" rules={[{ required: true }]}>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------- Prompt 模板 ---------------- */
|
||||
|
||||
const PROMPT_CATEGORIES = ['系统角色', '指摘分析', 'Agent规划', '其他']
|
||||
|
||||
function PromptTab() {
|
||||
const [mode, setMode] = useState<'templates' | 'logs' | 'stats'>('templates')
|
||||
const [templates, setTemplates] = useState<PromptTemplate[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<PromptTemplate | null>(null)
|
||||
const [editForm] = Form.useForm()
|
||||
|
||||
const [versionOpen, setVersionOpen] = useState(false)
|
||||
const [versions, setVersions] = useState<PromptVersion[]>([])
|
||||
const [versionTemplate, setVersionTemplate] = useState<PromptTemplate | null>(null)
|
||||
|
||||
const [testOpen, setTestOpen] = useState(false)
|
||||
const [testTemplate, setTestTemplate] = useState<PromptTemplate | null>(null)
|
||||
const [testResult, setTestResult] = useState<any>(null)
|
||||
const [testForm] = Form.useForm()
|
||||
|
||||
const load = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await promptApi.list(p)
|
||||
setTemplates(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null)
|
||||
editForm.resetFields()
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (t: PromptTemplate) => {
|
||||
setEditing(t)
|
||||
editForm.setFieldsValue({
|
||||
templateId: t.templateId,
|
||||
name: t.name,
|
||||
category: t.category,
|
||||
content: t.content,
|
||||
variables: t.variables || '',
|
||||
outputSchema: t.outputSchema || '',
|
||||
})
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
const submitTemplate = async (values: any) => {
|
||||
const payload = {
|
||||
templateId: values.templateId,
|
||||
name: values.name,
|
||||
category: values.category,
|
||||
content: values.content,
|
||||
variables: values.variables || undefined,
|
||||
outputSchema: values.outputSchema || undefined,
|
||||
}
|
||||
try {
|
||||
if (editing) {
|
||||
await promptApi.update(editing.templateId, payload)
|
||||
message.success('模板已更新')
|
||||
} else {
|
||||
await promptApi.create(payload)
|
||||
message.success('模板已创建')
|
||||
}
|
||||
setEditOpen(false)
|
||||
load(page)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const openVersions = async (t: PromptTemplate) => {
|
||||
setVersionTemplate(t)
|
||||
setVersionOpen(true)
|
||||
try {
|
||||
const res: any = await promptApi.versions(t.templateId)
|
||||
setVersions(res.data || [])
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const rollback = async (v: PromptVersion) => {
|
||||
try {
|
||||
await promptApi.rollback(v.templateId, v.version)
|
||||
message.success(`已回滚到版本 v${v.version}`)
|
||||
setVersionOpen(false)
|
||||
load(page)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const openTest = (t: PromptTemplate) => {
|
||||
setTestTemplate(t)
|
||||
setTestResult(null)
|
||||
testForm.resetFields()
|
||||
setTestOpen(true)
|
||||
}
|
||||
|
||||
const runTest = async (values: any) => {
|
||||
if (!testTemplate) return
|
||||
let variables: Record<string, string> | undefined
|
||||
if (values.variables) {
|
||||
try {
|
||||
variables = JSON.parse(values.variables)
|
||||
} catch {
|
||||
message.error('变量 JSON 格式不正确')
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res: any = await promptApi.test(testTemplate.templateId, variables)
|
||||
setTestResult(res.data)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模板 ID', dataIndex: 'templateId', key: 'templateId',
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontWeight: 600, color: '#1677ff' }}>{v}</span>
|
||||
},
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{
|
||||
title: '分类', dataIndex: 'category', key: 'category', width: 100,
|
||||
render: (v: string) => <Tag color="blue">{v}</Tag>
|
||||
},
|
||||
{
|
||||
title: '版本', dataIndex: 'version', key: 'version', width: 80,
|
||||
render: (v: number) => <Tag>v{v}</Tag>
|
||||
},
|
||||
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true, width: 300 },
|
||||
{
|
||||
title: '操作', key: 'op', width: 200, align: 'right' as const,
|
||||
render: (_: any, r: PromptTemplate) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
|
||||
<Button size="small" icon={<HistoryOutlined />} onClick={() => openVersions(r)}>版本</Button>
|
||||
<Button size="small" icon={<ExperimentOutlined />} onClick={() => openTest(r)}>测试</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Radio.Group value={mode} onChange={(e) => setMode(e.target.value)} optionType="button" buttonStyle="solid"
|
||||
options={[
|
||||
{ label: '模板列表', value: 'templates' },
|
||||
{ label: '渲染日志', value: 'logs' },
|
||||
{ label: '使用统计', value: 'stats' },
|
||||
]} />
|
||||
<Button type="primary" style={primaryBtn} icon={<PlusOutlined />} onClick={openCreate}>新增模板</Button>
|
||||
</div>
|
||||
|
||||
{mode === 'templates' && (
|
||||
<Card style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}>
|
||||
<Table dataSource={templates} columns={columns} rowKey="templateId" loading={loading}
|
||||
pagination={{ current: page, total, pageSize: 20, onChange: load, showTotal: t => `共 ${t} 个模板` }} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{mode === 'logs' && <RenderLogTable />}
|
||||
{mode === 'stats' && <StatsTable />}
|
||||
|
||||
{/* 新增/编辑 */}
|
||||
<Modal
|
||||
title={editing ? `编辑模板 ${editing.templateId}` : '新增模板'}
|
||||
open={editOpen}
|
||||
onOk={() => editForm.submit()}
|
||||
onCancel={() => setEditOpen(false)}
|
||||
width={720}
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={submitTemplate}>
|
||||
<Form.Item name="templateId" label="模板 ID" rules={[{ required: true, message: '请输入模板 ID' }]}
|
||||
tooltip="全局唯一,创建后不可修改">
|
||||
<Input disabled={!!editing} placeholder="如 ANALYSIS_001" />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="如 指摘根因分析" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="category" label="分类" rules={[{ required: true, message: '请选择分类' }]}>
|
||||
<Select options={PROMPT_CATEGORIES.map(c => ({ label: c, value: c }))} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="content" label="模板内容" rules={[{ required: true, message: '请输入模板内容' }]}
|
||||
tooltip="支持 {{变量名}} 占位符,渲染时替换为实际值">
|
||||
<Input.TextArea rows={8} style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="variables" label="变量声明 (JSON 数组)" tooltip='如 ["issueTitle","rootCause"]'>
|
||||
<Input.TextArea rows={3} placeholder='["issueTitle","rootCause"]' style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="outputSchema" label="输出结构 (JSON)" tooltip="期望 LLM 输出的 JSON 结构">
|
||||
<Input.TextArea rows={3} placeholder='{"category":"string","suggestion":"string"}' style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 版本历史 */}
|
||||
<Modal title={versionTemplate ? `版本历史 ${versionTemplate.templateId}` : '版本历史'} open={versionOpen}
|
||||
footer={<Button onClick={() => setVersionOpen(false)}>关闭</Button>} width={680} onCancel={() => setVersionOpen(false)}>
|
||||
<Table
|
||||
dataSource={versions}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: '版本', dataIndex: 'version', key: 'version', width: 80,
|
||||
render: (v: number) => <Tag>v{v}</Tag>
|
||||
},
|
||||
{ title: '变更说明', dataIndex: 'changeLog', key: 'changeLog' },
|
||||
{
|
||||
title: '内容', dataIndex: 'content', key: 'content', ellipsis: true,
|
||||
render: (v: string) => <div style={{ maxWidth: 260, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{v}</div>
|
||||
},
|
||||
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '操作', key: 'op', width: 80, align: 'right' as const,
|
||||
render: (_: any, v: PromptVersion) => (
|
||||
<Popconfirm title={`回滚到 v${v.version}?`} onConfirm={() => rollback(v)}>
|
||||
<Button type="link" size="small">回滚</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* 测试 */}
|
||||
<Modal title={testTemplate ? `测试 ${testTemplate.templateId}` : '测试模板'} open={testOpen}
|
||||
footer={<Button onClick={() => setTestOpen(false)}>关闭</Button>} width={720} onCancel={() => setTestOpen(false)}>
|
||||
<Form form={testForm} layout="vertical" onFinish={runTest}>
|
||||
<Form.Item name="variables" label="变量 (JSON 对象)" tooltip='如 {"issueTitle":"页面加载缓慢"}' extra="留空则使用模板默认变量">
|
||||
<Input.TextArea rows={3} placeholder='{"issueTitle":"页面加载缓慢"}' style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" icon={<ThunderboltOutlined />}>渲染测试</Button>
|
||||
</Form>
|
||||
{testResult && (
|
||||
<Alert
|
||||
style={{ marginTop: 16 }}
|
||||
type="info"
|
||||
showIcon
|
||||
message={`渲染结果 · v${testResult.version}`}
|
||||
description={<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace', fontSize: 12, margin: 0 }}>{testResult.rendered}</pre>}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RenderLogTable() {
|
||||
const [logs, setLogs] = useState<PromptRenderLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<PromptRenderLog | null>(null)
|
||||
|
||||
const load = async (p = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await promptApi.logs(p)
|
||||
setLogs(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}>
|
||||
<Table
|
||||
dataSource={logs}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="middle"
|
||||
pagination={{ current: page, total, pageSize: 20, onChange: load, showTotal: t => `共 ${t} 条` }}
|
||||
columns={[
|
||||
{
|
||||
title: '请求 ID', dataIndex: 'requestId', key: 'requestId', width: 220,
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v}</span>
|
||||
},
|
||||
{
|
||||
title: '模板', dataIndex: 'templateId', key: 'templateId',
|
||||
render: (v: string, r: PromptRenderLog) => <span>{v} <Tag>v{r.templateVersion}</Tag></span>
|
||||
},
|
||||
{
|
||||
title: '模型', key: 'model', width: 160,
|
||||
render: (_: any, r: PromptRenderLog) => `${r.modelProvider || '-'}/${r.llmModel || '-'}`
|
||||
},
|
||||
{
|
||||
title: 'Token', key: 'tokens', width: 140,
|
||||
render: (_: any, r: PromptRenderLog) => `入 ${r.tokensInput ?? 0} / 出 ${r.tokensOutput ?? 0}`
|
||||
},
|
||||
{ title: '耗时', dataIndex: 'executionTimeMs', key: 'ms', width: 90, render: (v: number) => `${v ?? 0}ms` },
|
||||
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '操作', key: 'op', width: 70, align: 'right' as const,
|
||||
render: (_: any, r: PromptRenderLog) => <Button size="small" type="text" icon={<EyeOutlined />} onClick={() => setDetail(r)} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal title="渲染日志详情" open={!!detail} footer={<Button onClick={() => setDetail(null)}>关闭</Button>} width={720}
|
||||
onCancel={() => setDetail(null)}>
|
||||
{detail && (
|
||||
<div style={{ lineHeight: 2 }}>
|
||||
<div><b>请求 ID:</b><span style={{ fontFamily: 'monospace' }}>{detail.requestId}</span></div>
|
||||
<div><b>模板:</b>{detail.templateId} · v{detail.templateVersion}</div>
|
||||
<div><b>使用变量:</b></div>
|
||||
<pre style={{ background: '#fafafa', borderRadius: 8, padding: 12, fontSize: 12, whiteSpace: 'pre-wrap' }}>{detail.variablesUsed || '-'}</pre>
|
||||
<div><b>渲染后的 Prompt:</b></div>
|
||||
<pre style={{ background: '#fafafa', borderRadius: 8, padding: 12, fontSize: 12, whiteSpace: 'pre-wrap' }}>{detail.renderedPrompt}</pre>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsTable() {
|
||||
const [stats, setStats] = useState<PromptStats[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
promptApi.stats().then((res: any) => setStats(res.data || [])).catch(() => { /* ignore */ }).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模板 ID', dataIndex: 'templateId', key: 'templateId',
|
||||
render: (v: string) => <span style={{ fontFamily: 'monospace', fontWeight: 600, color: '#1677ff' }}>{v}</span>
|
||||
},
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '分类', dataIndex: 'category', key: 'category', render: (v: string) => <Tag color="blue">{v}</Tag> },
|
||||
{ title: '最新版本', dataIndex: 'latestVersion', key: 'latestVersion', render: (v: number) => <Tag>v{v}</Tag> },
|
||||
{
|
||||
title: '使用次数', dataIndex: 'useCount', key: 'useCount',
|
||||
render: (v: number) => <b>{v}</b>
|
||||
},
|
||||
{
|
||||
title: '平均耗时', dataIndex: 'avgExecutionTimeMs', key: 'avgMs',
|
||||
render: (v: number) => `${Number(v).toFixed(0)}ms`
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Card title="模板使用统计" style={{ borderRadius: 16 }} styles={{ body: { padding: 0 } }}>
|
||||
<Table dataSource={stats} columns={columns} rowKey="templateId" loading={loading} pagination={false} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Typography, Table, Card, Input, Select, Space, Button, Tag, DatePicker } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { logApi, LogItem, LogQuery } from '../../api/system'
|
||||
|
||||
export default function LogsPage() {
|
||||
const [list, setList] = useState<LogItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [operator, setOperator] = useState('')
|
||||
const [actionType, setActionType] = useState<string | undefined>()
|
||||
const [range, setRange] = useState<[Dayjs | null, Dayjs | null] | null>(null)
|
||||
|
||||
const buildQuery = (p = page, ps = pageSize): LogQuery => ({
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
keyword: keyword || undefined,
|
||||
operator: operator || undefined,
|
||||
actionType,
|
||||
startTime: range?.[0] ? range[0].format('YYYY-MM-DDTHH:mm:ss') : undefined,
|
||||
endTime: range?.[1] ? range[1].format('YYYY-MM-DDTHH:mm:ss') : undefined,
|
||||
})
|
||||
|
||||
const load = async (p = 1, ps = pageSize) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await logApi.list(buildQuery(p, ps))
|
||||
setList(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(1)
|
||||
}, [])
|
||||
|
||||
const reset = () => {
|
||||
setKeyword('')
|
||||
setOperator('')
|
||||
setActionType(undefined)
|
||||
setRange(null)
|
||||
load(1)
|
||||
}
|
||||
|
||||
const resourceColor: Record<string, string> = { issue: 'blue', task: 'orange' }
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170,
|
||||
render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-'),
|
||||
},
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 120 },
|
||||
{ title: '动作', dataIndex: 'action', key: 'action', width: 160 },
|
||||
{
|
||||
title: '类型', dataIndex: 'resource', key: 'resource', width: 90,
|
||||
render: (v: string) => <Tag color={resourceColor[v]}>{v}</Tag>,
|
||||
},
|
||||
{ title: '详情', dataIndex: 'detail', key: 'detail' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ marginBottom: 16 }}>系统日志</Typography.Title>
|
||||
<Card size="small" style={{ marginBottom: 16 }}>
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
placeholder="搜索详情关键词" allowClear style={{ width: 200 }}
|
||||
onSearch={(v) => { setKeyword(v); load(1) }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="操作人" allowClear style={{ width: 130 }}
|
||||
onChange={(e) => setOperator(e.target.value)}
|
||||
onPressEnter={() => load(1)}
|
||||
/>
|
||||
<Select
|
||||
placeholder="类型" allowClear style={{ width: 120 }}
|
||||
options={[{ value: 'issue', label: '指摘操作' }, { value: 'task', label: '任务' }]}
|
||||
onChange={(v) => { setActionType(v); load(1) }}
|
||||
/>
|
||||
<DatePicker.RangePicker
|
||||
showTime onChange={(v) => setRange(v as [Dayjs | null, Dayjs | null] | null)}
|
||||
/>
|
||||
<Button type="primary" onClick={() => load(1)}>查询</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={reset}>重置</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card size="small">
|
||||
<Table
|
||||
rowKey={(r) => `${r.resource}-${r.id}`}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
size="middle"
|
||||
pagination={{
|
||||
current: page, pageSize, total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, ps) => { setPage(p); setPageSize(ps); load(p, ps) },
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Typography, Card, List, Button, Checkbox, Select, Space, message,
|
||||
Modal, Form, Input, Switch, Divider, Empty, Tag
|
||||
} from 'antd'
|
||||
import { PlusOutlined, SaveOutlined, CrownOutlined, ToolOutlined } from '@ant-design/icons'
|
||||
import { roleApi, RoleItem, PermissionItem } from '../../api/system'
|
||||
|
||||
export default function RolesPage() {
|
||||
const [roles, setRoles] = useState<RoleItem[]>([])
|
||||
const [permissions, setPermissions] = useState<PermissionItem[]>([])
|
||||
const [selected, setSelected] = useState<RoleItem | null>(null)
|
||||
const [checked, setChecked] = useState<number[]>([])
|
||||
const [dataScope, setDataScope] = useState('all')
|
||||
const [agentAutoExecute, setAgentAutoExecute] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createForm] = Form.useForm()
|
||||
|
||||
const menuPerms = useMemo(() => permissions.filter((p) => p.resource === 'menu'), [permissions])
|
||||
const toolPerms = useMemo(() => permissions.filter((p) => p.resource === 'agent_tool'), [permissions])
|
||||
|
||||
const loadRoles = async (keepSelected = true) => {
|
||||
const res: any = await roleApi.list()
|
||||
const data: RoleItem[] = res.data || []
|
||||
setRoles(data)
|
||||
if (!keepSelected) return
|
||||
if (selected) {
|
||||
const updated = data.find((r) => r.id === selected.id)
|
||||
if (updated) selectRole(updated)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles(false)
|
||||
roleApi.permissions().then((res: any) => setPermissions(res.data || []))
|
||||
}, [])
|
||||
|
||||
const selectRole = (r: RoleItem) => {
|
||||
setSelected(r)
|
||||
setChecked(r.permissionIds || [])
|
||||
setDataScope(r.dataScope || 'all')
|
||||
setAgentAutoExecute(!!r.agentAutoExecute)
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (!selected) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await roleApi.update(selected.id, {
|
||||
name: selected.name,
|
||||
description: selected.description,
|
||||
dataScope,
|
||||
agentAutoExecute,
|
||||
permissionIds: checked,
|
||||
})
|
||||
message.success('配置已保存')
|
||||
await loadRoles()
|
||||
} catch { /* ignore */ }
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const createRole = async () => {
|
||||
const values = await createForm.validateFields()
|
||||
const res: any = await roleApi.create({ ...values, dataScope: 'all', agentAutoExecute: false, permissionIds: [] })
|
||||
message.success('角色已创建')
|
||||
setCreateOpen(false)
|
||||
createForm.resetFields()
|
||||
await loadRoles(false)
|
||||
selectRole(res.data)
|
||||
}
|
||||
|
||||
const menuChecked = menuPerms.every((p) => checked.includes(p.id))
|
||||
const toolChecked = toolPerms.every((p) => checked.includes(p.id))
|
||||
|
||||
const toggleAll = (list: PermissionItem[], allChecked: boolean) => {
|
||||
const ids = list.map((p) => p.id)
|
||||
setChecked((prev) => allChecked
|
||||
? prev.filter((id) => !ids.includes(id))
|
||||
: Array.from(new Set([...prev, ...ids])))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>角色权限配置</Typography.Title>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={save} loading={saving} disabled={!selected}>保存配置</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Card size="small" title="角色列表" style={{ width: 260, flexShrink: 0 }}>
|
||||
<List
|
||||
dataSource={roles}
|
||||
renderItem={(r) => (
|
||||
<List.Item
|
||||
onClick={() => selectRole(r)}
|
||||
style={{
|
||||
cursor: 'pointer', padding: '8px 12px', borderRadius: 6,
|
||||
background: selected?.id === r.id ? '#e6f4ff' : undefined,
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<CrownOutlined style={{ color: r.id === 1 ? '#faad14' : '#999' }} />}
|
||||
title={r.name}
|
||||
description={r.description}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<Button block type="dashed" icon={<PlusOutlined />} style={{ marginTop: 12 }} onClick={() => setCreateOpen(true)}>
|
||||
新增角色
|
||||
</Button>
|
||||
</Card>
|
||||
<Card size="small" title={selected ? `权限配置 — ${selected.name}` : '权限配置'} style={{ flex: 1 }}>
|
||||
{!selected ? (
|
||||
<Empty description="请选择左侧角色" style={{ padding: 48 }} />
|
||||
) : (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Typography.Text strong>数据权限范围</Typography.Text>
|
||||
<Select
|
||||
style={{ width: 220, marginLeft: 12 }} value={dataScope}
|
||||
onChange={(v) => setDataScope(v)}
|
||||
options={[
|
||||
{ value: 'all', label: '全系统数据' },
|
||||
{ value: 'dept', label: '所属部门数据' },
|
||||
{ value: 'own', label: '仅本人相关数据' },
|
||||
]}
|
||||
/>
|
||||
<Typography.Text strong style={{ marginLeft: 32 }}>全局 Agent 自动执行</Typography.Text>
|
||||
<Switch style={{ marginLeft: 12 }} checked={agentAutoExecute} onChange={setAgentAutoExecute} />
|
||||
</div>
|
||||
<Divider style={{ margin: '4px 0' }} />
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography.Text strong>页面与功能权限</Typography.Text>
|
||||
<Checkbox
|
||||
checked={menuPerms.length > 0 && menuChecked}
|
||||
indeterminate={menuPerms.length > 0 && !menuChecked && menuPerms.some((p) => checked.includes(p.id))}
|
||||
onChange={(e) => toggleAll(menuPerms, e.target.checked)}
|
||||
>
|
||||
全选
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Checkbox.Group
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}
|
||||
value={checked}
|
||||
onChange={(v) => setChecked(v as number[])}
|
||||
>
|
||||
{menuPerms.map((p) => (
|
||||
<Checkbox key={p.id} value={p.id}>{p.name} <Typography.Text type="secondary">({p.code})</Typography.Text></Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
<Divider style={{ margin: '4px 0' }} />
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #1a73e8, #4a90f2)',
|
||||
borderRadius: 16, padding: 20, color: '#fff',
|
||||
boxShadow: '0 8px 24px rgba(26,115,232,.18)',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<ToolOutlined style={{ fontSize: 18 }} />
|
||||
<Typography.Text strong style={{ color: '#fff', fontSize: 15 }}>
|
||||
Agent 工具调用权限 <Tag color="gold" style={{ marginLeft: 6 }}>核心</Tag>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={toolPerms.length > 0 && toolChecked}
|
||||
indeterminate={toolPerms.length > 0 && !toolChecked && toolPerms.some((p) => checked.includes(p.id))}
|
||||
onChange={(e) => toggleAll(toolPerms, e.target.checked)}
|
||||
style={{ color: '#fff' }}
|
||||
>
|
||||
全选
|
||||
</Checkbox>
|
||||
</div>
|
||||
<p style={{ color: 'rgba(255,255,255,.78)', fontSize: 12, marginBottom: 14 }}>
|
||||
配置此角色在与 AI Agent 交互时,允许 Agent 自动执行的原子工具。敏感操作建议开启"人机协同审批"。
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
|
||||
{toolPerms.map((p) => (
|
||||
<label
|
||||
key={p.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px',
|
||||
background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.22)',
|
||||
borderRadius: 10, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked.includes(p.id)}
|
||||
onChange={(e) => {
|
||||
setChecked((prev) => e.target.checked
|
||||
? Array.from(new Set([...prev, p.id]))
|
||||
: prev.filter((i) => i !== p.id))
|
||||
}}
|
||||
/>
|
||||
<code style={{ color: '#fff', fontFamily: "'JetBrains Mono', Consolas, monospace", fontSize: 12, fontWeight: 700 }}>
|
||||
{p.code}
|
||||
</code>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal title="新增角色" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={createRole}>
|
||||
<Form form={createForm} layout="vertical">
|
||||
<Form.Item name="name" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
|
||||
<Input placeholder="请输入角色名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input placeholder="角色描述" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Typography, Table, Button, Input, Select, Tree, Card, Modal, Form,
|
||||
Tag, Space, message, Popconfirm, Row, Col, Switch
|
||||
} from 'antd'
|
||||
import type { TreeDataNode } from 'antd'
|
||||
import {
|
||||
UserAddOutlined, DownloadOutlined, EditOutlined, DeleteOutlined, CheckCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { userApi, deptApi, roleApi, UserItem, DeptNode, UserPayload } from '../../api/system'
|
||||
|
||||
interface DeptOption { label: string; value: number }
|
||||
|
||||
function flattenDepts(nodes: DeptNode[], out: DeptOption[] = [], depth = 0): DeptOption[] {
|
||||
for (const n of nodes) {
|
||||
out.push({ label: ' '.repeat(depth) + n.name, value: n.id })
|
||||
if (n.children) flattenDepts(n.children, out, depth + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function toTreeData(nodes: DeptNode[]): TreeDataNode[] {
|
||||
return nodes.map((n) => ({
|
||||
title: n.name,
|
||||
key: String(n.id),
|
||||
children: n.children ? toTreeData(n.children) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const [list, setList] = useState<UserItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [departmentId, setDepartmentId] = useState<number | undefined>()
|
||||
const [active, setActive] = useState<boolean | undefined>()
|
||||
|
||||
const [depts, setDepts] = useState<DeptNode[]>([])
|
||||
const [roleOptions, setRoleOptions] = useState<{ label: string; value: number }[]>([])
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<UserItem | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const deptOptions = useMemo(() => flattenDepts(depts), [depts])
|
||||
|
||||
const load = async (p = 1, ps = pageSize) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await userApi.list({
|
||||
page: p, pageSize: ps,
|
||||
keyword: keyword || undefined, departmentId, isActive: active,
|
||||
})
|
||||
setList(res.data?.items || [])
|
||||
setTotal(res.data?.total || 0)
|
||||
setPage(p)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(1)
|
||||
deptApi.tree().then((res: any) => setDepts(res.data || []))
|
||||
roleApi.list().then((res: any) =>
|
||||
setRoleOptions((res.data || []).map((r: any) => ({ label: r.name, value: r.id }))))
|
||||
}, [])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({ isActive: true, agentAutoExecute: false })
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (record: UserItem) => {
|
||||
setEditing(record)
|
||||
form.setFieldsValue({
|
||||
username: record.username,
|
||||
email: record.email,
|
||||
departmentId: record.departmentId,
|
||||
roleIds: record.roleIds,
|
||||
isActive: record.isActive,
|
||||
agentAutoExecute: record.agentAutoExecute,
|
||||
})
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields()
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload: UserPayload = { ...values }
|
||||
if (editing) {
|
||||
await userApi.update(editing.id, payload)
|
||||
message.success('已保存')
|
||||
} else {
|
||||
await userApi.create(payload)
|
||||
message.success('已创建')
|
||||
}
|
||||
setModalOpen(false)
|
||||
load(editing ? page : 1)
|
||||
} catch { /* 拦截器已提示 */ }
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const toggleStatus = async (record: UserItem) => {
|
||||
await userApi.updateStatus(record.id, !record.isActive)
|
||||
message.success('已更新')
|
||||
load()
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const res: any = await userApi.exportCsv({ keyword: keyword || undefined, departmentId, isActive: active })
|
||||
const url = URL.createObjectURL(res as Blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'users.csv'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '账号', dataIndex: 'userid', key: 'userid', width: 140 },
|
||||
{ title: '姓名', dataIndex: 'username', key: 'username', width: 120 },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '部门', dataIndex: 'departmentName', key: 'departmentName', width: 120 },
|
||||
{
|
||||
title: '角色', dataIndex: 'roles', key: 'roles',
|
||||
render: (roles: string[]) =>
|
||||
roles && roles.length ? (
|
||||
<Space size={4} wrap>
|
||||
{roles.map((r) => <Tag color="blue" key={r}>{r}</Tag>)}
|
||||
</Space>
|
||||
) : '-',
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'isActive', key: 'isActive', width: 90,
|
||||
render: (v: boolean) => (v ? <Tag color="green">正常</Tag> : <Tag color="red">禁用</Tag>),
|
||||
},
|
||||
{
|
||||
title: 'Agent授权', dataIndex: 'agentAutoExecute', key: 'agentAutoExecute', width: 100,
|
||||
render: (v: boolean) => (v ? <Tag color="purple">已开启</Tag> : <Tag>未开启</Tag>),
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 150,
|
||||
render: (_: unknown, record: UserItem) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(record)}>编辑</Button>
|
||||
<Popconfirm title={record.isActive ? '确认禁用该用户?' : '确认启用该用户?'} onConfirm={() => toggleStatus(record)}>
|
||||
<Button type="link" size="small" danger={record.isActive} icon={record.isActive ? <DeleteOutlined /> : <CheckCircleOutlined />}>
|
||||
{record.isActive ? '禁用' : '启用'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>用户管理</Typography.Title>
|
||||
<Space>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||
<Button type="primary" icon={<UserAddOutlined />} onClick={openCreate}>新增用户</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={5}>
|
||||
<Card size="small" title="组织架构" style={{ height: '100%' }}>
|
||||
<Tree
|
||||
treeData={toTreeData(depts)}
|
||||
defaultExpandAll
|
||||
onSelect={(keys) => {
|
||||
const k = keys[0]
|
||||
setDepartmentId(k ? Number(k) : undefined)
|
||||
setPage(1)
|
||||
load(1)
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={19}>
|
||||
<Card size="small">
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input.Search
|
||||
placeholder="搜索账号/姓名/邮箱" allowClear style={{ width: 260 }}
|
||||
onSearch={(v) => { setKeyword(v); load(1) }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="状态" allowClear style={{ width: 120 }}
|
||||
options={[{ value: true, label: '正常' }, { value: false, label: '禁用' }]}
|
||||
onChange={(v) => { setActive(v); load(1) }}
|
||||
/>
|
||||
<Button onClick={() => { setKeyword(''); setActive(undefined); setDepartmentId(undefined); load(1) }}>重置</Button>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id" loading={loading} columns={columns} dataSource={list} size="middle"
|
||||
pagination={{
|
||||
current: page, pageSize, total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, ps) => { setPage(p); setPageSize(ps); load(p, ps) },
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑用户' : '新增用户信息'} open={modalOpen} onCancel={() => setModalOpen(false)}
|
||||
onOk={handleSave} confirmLoading={saving} width={480}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ isActive: true, agentAutoExecute: false }}>
|
||||
{!editing && (
|
||||
<Form.Item name="userid" label="账号(工号)" rules={[{ required: true, message: '请输入账号' }]}>
|
||||
<Input placeholder="IMS-XXXX" />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="username" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||
<Input placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="电子邮箱" rules={[{ type: 'email', message: '邮箱格式不正确' }]}>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
{!editing && (
|
||||
<Form.Item name="password" label="初始密码" tooltip="留空则使用默认密码 Admin@123456">
|
||||
<Input.Password placeholder="留空使用默认密码" />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="departmentId" label="归属部门" rules={[{ required: true, message: '请选择部门' }]}>
|
||||
<Select placeholder="请选择部门" options={deptOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="roleIds" label="绑定角色">
|
||||
<Select mode="multiple" placeholder="请选择角色" options={roleOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="agentAutoExecute" label="Agent 自动执行权限" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import axios from 'axios'
|
||||
import { message } from './antdStatic'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 600000
|
||||
})
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response.data
|
||||
}
|
||||
const data = response.data
|
||||
if (data.code !== 200) {
|
||||
message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message))
|
||||
}
|
||||
return data
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config
|
||||
if (error.response?.status !== 401 || originalRequest._retry) {
|
||||
message.error(error.message || '网络错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (originalRequest.url.includes('/auth/login')) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const refreshToken = localStorage.getItem('refreshToken')
|
||||
if (!refreshToken) {
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((token: string) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/v1/auth/refresh', { refreshToken })
|
||||
const data = res.data
|
||||
if (data.code !== 200) {
|
||||
throw new Error(data.message)
|
||||
}
|
||||
const { accessToken, refreshToken: newRefreshToken } = data.data
|
||||
localStorage.setItem('accessToken', accessToken)
|
||||
localStorage.setItem('refreshToken', newRefreshToken)
|
||||
|
||||
pendingRequests.forEach(cb => cb(accessToken))
|
||||
pendingRequests = []
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,96 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Spin } from 'antd'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import MainLayout from './layouts/MainLayout'
|
||||
|
||||
const Login = lazy(() => import('./pages/login/index'))
|
||||
const Dashboard = lazy(() => import('./pages/dashboard/index'))
|
||||
const IssueList = lazy(() => import('./pages/issues/list'))
|
||||
const IssueDetail = lazy(() => import('./pages/issues/detail'))
|
||||
const IssueNew = lazy(() => import('./pages/issues/new'))
|
||||
const IssueEdit = lazy(() => import('./pages/issues/edit'))
|
||||
const BatchInput = lazy(() => import('./pages/batch-input/index'))
|
||||
const AiAnalysis = lazy(() => import('./pages/ai-analysis/index'))
|
||||
const KnowledgeBase = lazy(() => import('./pages/knowledge-base/index'))
|
||||
const SystemUsers = lazy(() => import('./pages/system/users'))
|
||||
const SystemRoles = lazy(() => import('./pages/system/roles'))
|
||||
const SystemLogs = lazy(() => import('./pages/system/logs'))
|
||||
const SystemAgent = lazy(() => import('./pages/system/agent-admin'))
|
||||
|
||||
const LazyLoad = ({ children }: { children: React.ReactNode }) => (
|
||||
<Suspense fallback={<Spin style={{ display: 'flex', justifyContent: 'center', marginTop: 200 }} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
return localStorage.getItem('accessToken') ? <>{children}</> : <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
element: <LazyLoad><Login /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <RequireAuth><MainLayout /></RequireAuth>,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||
{
|
||||
path: 'dashboard',
|
||||
element: <LazyLoad><Dashboard /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'issues',
|
||||
element: <LazyLoad><IssueList /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'issues/new',
|
||||
element: <LazyLoad><IssueNew /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'issues/:id',
|
||||
element: <LazyLoad><IssueDetail /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'issues/:id/edit',
|
||||
element: <LazyLoad><IssueEdit /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'batch-input',
|
||||
element: <LazyLoad><BatchInput /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'ai-analysis',
|
||||
element: <LazyLoad><AiAnalysis /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'knowledge-base',
|
||||
element: <LazyLoad><KnowledgeBase /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'system/users',
|
||||
element: <LazyLoad><SystemUsers /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'system/roles',
|
||||
element: <LazyLoad><SystemRoles /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'system/logs',
|
||||
element: <LazyLoad><SystemLogs /></LazyLoad>
|
||||
},
|
||||
{
|
||||
path: 'system/agent-admin',
|
||||
element: <LazyLoad><SystemAgent /></LazyLoad>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/dashboard" replace />
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
||||
@@ -0,0 +1,23 @@
|
||||
import request from '../request'
|
||||
|
||||
export interface NotificationItem {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
type: string
|
||||
link?: string
|
||||
isRead: boolean
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export const listNotifications = (page = 1, pageSize = 20) =>
|
||||
request.get('/notifications', { params: { page, pageSize } })
|
||||
|
||||
export const getUnreadCount = () =>
|
||||
request.get('/notifications/unread-count')
|
||||
|
||||
export const markNotificationRead = (id: number) =>
|
||||
request.patch(`/notifications/${id}/read`)
|
||||
|
||||
export const markAllNotificationsRead = () =>
|
||||
request.post('/notifications/read-all')
|
||||
@@ -0,0 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import authReducer from './slices/authSlice'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer
|
||||
}
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import request from '../../request'
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
userId: number | null
|
||||
username: string | null
|
||||
departmentName: string | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: localStorage.getItem('accessToken'),
|
||||
userId: null,
|
||||
username: null,
|
||||
departmentName: null,
|
||||
loading: false
|
||||
}
|
||||
|
||||
export const login = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (params: { username: string; password: string }) => {
|
||||
const res: any = await request.post('/auth/login', params)
|
||||
return res.data
|
||||
}
|
||||
)
|
||||
|
||||
export const fetchMe = createAsyncThunk(
|
||||
'auth/me',
|
||||
async () => {
|
||||
const res: any = await request.get('/auth/me')
|
||||
return res.data
|
||||
}
|
||||
)
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
logout(state) {
|
||||
state.token = null
|
||||
state.userId = null
|
||||
state.username = null
|
||||
state.departmentName = null
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(login.pending, (state) => {
|
||||
state.loading = true
|
||||
})
|
||||
.addCase(login.fulfilled, (state, action) => {
|
||||
state.loading = false
|
||||
state.token = action.payload.accessToken
|
||||
state.userId = action.payload.userId
|
||||
state.username = action.payload.username
|
||||
localStorage.setItem('accessToken', action.payload.accessToken)
|
||||
localStorage.setItem('refreshToken', action.payload.refreshToken)
|
||||
})
|
||||
.addCase(login.rejected, (state) => {
|
||||
state.loading = false
|
||||
})
|
||||
.addCase(fetchMe.fulfilled, (state, action) => {
|
||||
state.departmentName = action.payload.departmentName
|
||||
if (!state.username && action.payload.username) {
|
||||
state.username = action.payload.username
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const { logout } = authSlice.actions
|
||||
export default authSlice.reducer
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ThemeConfig } from 'antd'
|
||||
|
||||
const theme: ThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#1677ff',
|
||||
borderRadius: 6,
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
|
||||
}
|
||||
}
|
||||
|
||||
export default theme
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user