From edf66ab8e6543384925fe4a8a0444c33d0c84d9e Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 12 Apr 2026 18:07:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20Phase=204=20=E8=A1=8C=E4=B8=9A?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=20+=20?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=A1=8C=E4=B8=9A=E6=8E=88=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Industries.tsx: 行业列表(ProTable) + 编辑弹窗(关键词/prompt/痛点种子) + 新建弹窗 - 新增 services/industries.ts: 行业 API 服务层(list/create/update/fullConfig/accountIndustries) - 增强 Accounts.tsx: 编辑弹窗添加行业授权多选, 自动获取/同步用户行业 - 注册 /industries 路由 + 侧边栏导航(ShopOutlined) --- admin-v2/src/layouts/AdminLayout.tsx | 2 + admin-v2/src/pages/Accounts.tsx | 89 ++++++- admin-v2/src/pages/Industries.tsx | 350 +++++++++++++++++++++++++++ admin-v2/src/router/index.tsx | 1 + admin-v2/src/services/industries.ts | 104 ++++++++ admin-v2/src/types/index.ts | 24 ++ 6 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 admin-v2/src/pages/Industries.tsx create mode 100644 admin-v2/src/services/industries.ts diff --git a/admin-v2/src/layouts/AdminLayout.tsx b/admin-v2/src/layouts/AdminLayout.tsx index 56da9c8..e28eb0f 100644 --- a/admin-v2/src/layouts/AdminLayout.tsx +++ b/admin-v2/src/layouts/AdminLayout.tsx @@ -21,6 +21,7 @@ import { SafetyOutlined, FieldTimeOutlined, SyncOutlined, + ShopOutlined, } from '@ant-design/icons' import { Avatar, Dropdown, Tooltip, Drawer } from 'antd' import { useAuthStore } from '@/stores/authStore' @@ -50,6 +51,7 @@ const navItems: NavItem[] = [ { path: '/relay', name: '中转任务', icon: , permission: 'relay:use', group: '运维' }, { path: '/scheduled-tasks', name: '定时任务', icon: , permission: 'scheduler:read', group: '运维' }, { path: '/knowledge', name: '知识库', icon: , permission: 'knowledge:read', group: '资源管理' }, + { path: '/industries', name: '行业配置', icon: , permission: 'config:read', group: '资源管理' }, { path: '/billing', name: '计费管理', icon: , permission: 'billing:read', group: '核心' }, { path: '/logs', name: '操作日志', icon: , permission: 'admin:full', group: '运维' }, { path: '/config-sync', name: '同步日志', icon: , permission: 'config:read', group: '运维' }, diff --git a/admin-v2/src/pages/Accounts.tsx b/admin-v2/src/pages/Accounts.tsx index be04617..a50372d 100644 --- a/admin-v2/src/pages/Accounts.tsx +++ b/admin-v2/src/pages/Accounts.tsx @@ -2,12 +2,13 @@ // 账号管理 // ============================================================ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space } from 'antd' +import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space, Divider } from 'antd' import type { ProColumns } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' import { accountService } from '@/services/accounts' +import { industryService } from '@/services/industries' import { PageHeader } from '@/components/PageHeader' import type { AccountPublic } from '@/types' @@ -41,12 +42,35 @@ export default function Accounts() { const [modalOpen, setModalOpen] = useState(false) const [editingId, setEditingId] = useState(null) const [searchParams, setSearchParams] = useState>({}) + const [editingIndustries, setEditingIndustries] = useState([]) const { data, isLoading } = useQuery({ queryKey: ['accounts', searchParams], queryFn: ({ signal }) => accountService.list(searchParams, signal), }) + // 获取行业列表(用于下拉选择) + const { data: industriesData } = useQuery({ + queryKey: ['industries-all'], + queryFn: ({ signal }) => industryService.list({ page: 1, page_size: 100, status: 'active' }, signal), + }) + + // 获取当前编辑用户的行业授权 + const { data: accountIndustries } = useQuery({ + queryKey: ['account-industries', editingId], + queryFn: ({ signal }) => industryService.getAccountIndustries(editingId!, signal), + enabled: !!editingId, + }) + + // 当账户行业数据加载完,同步到表单 + useEffect(() => { + if (accountIndustries) { + const ids = accountIndustries.map((item) => item.industry_id) + setEditingIndustries(ids) + form.setFieldValue('industry_ids', ids) + } + }, [accountIndustries, form]) + const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => accountService.update(id, data), @@ -68,6 +92,18 @@ export default function Accounts() { onError: (err: Error) => message.error(err.message || '状态更新失败'), }) + // 设置用户行业授权 + const setIndustriesMutation = useMutation({ + mutationFn: ({ accountId, industries }: { accountId: string; industries: string[] }) => + industryService.setAccountIndustries(accountId, { + industries: industries.map((id, idx) => ({ + industry_id: id, + is_primary: idx === 0, + })), + }), + onError: (err: Error) => message.error(err.message || '行业授权更新失败'), + }) + const columns: ProColumns[] = [ { title: '用户名', dataIndex: 'username', width: 120, tooltip: '搜索用户名、邮箱或显示名' }, { title: '显示名', dataIndex: 'display_name', width: 120, hideInSearch: true }, @@ -150,13 +186,39 @@ export default function Accounts() { const handleSave = async () => { const values = await form.validateFields() if (editingId) { - updateMutation.mutate({ id: editingId, data: values }) + // 更新基础信息 + const { industry_ids, ...accountData } = values + updateMutation.mutate({ id: editingId, data: accountData }) + + // 更新行业授权(如果变更了) + const newIndustryIds: string[] = industry_ids || [] + const oldIndustryIds = accountIndustries?.map((i) => i.industry_id) || [] + const changed = newIndustryIds.length !== oldIndustryIds.length + || newIndustryIds.some((id) => !oldIndustryIds.includes(id)) + + if (changed) { + await setIndustriesMutation.mutateAsync({ accountId: editingId, industries: newIndustryIds }) + message.success('行业授权已更新') + queryClient.invalidateQueries({ queryKey: ['account-industries'] }) + } } } + const handleClose = () => { + setModalOpen(false) + setEditingId(null) + setEditingIndustries([]) + form.resetFields() + } + + const industryOptions = (industriesData?.items || []).map((item) => ({ + value: item.id, + label: `${item.icon} ${item.name}`, + })) + return (
- + columns={columns} @@ -169,7 +231,6 @@ export default function Accounts() { const filtered: Record = {} for (const [k, v] of Object.entries(values)) { if (v !== undefined && v !== null && v !== '') { - // Map 'username' search field to backend 'search' param if (k === 'username') { filtered.search = String(v) } else { @@ -192,8 +253,9 @@ export default function Accounts() { title={编辑账号} open={modalOpen} onOk={handleSave} - onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }} + onCancel={handleClose} confirmLoading={updateMutation.isPending} + width={560} >
@@ -215,6 +277,21 @@ export default function Accounts() { { value: 'relay', label: 'SaaS 中转 (Token 池)' }, ]} /> + + 行业授权 + + + + + + + + +