feat(admin): Phase 4 行业配置管理页面 + 账号行业授权
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- 新增 Industries.tsx: 行业列表(ProTable) + 编辑弹窗(关键词/prompt/痛点种子) + 新建弹窗
- 新增 services/industries.ts: 行业 API 服务层(list/create/update/fullConfig/accountIndustries)
- 增强 Accounts.tsx: 编辑弹窗添加行业授权多选, 自动获取/同步用户行业
- 注册 /industries 路由 + 侧边栏导航(ShopOutlined)
This commit is contained in:
iven
2026-04-12 18:07:52 +08:00
parent b853978771
commit edf66ab8e6
6 changed files with 564 additions and 6 deletions

View File

@@ -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<string | null>(null)
const [searchParams, setSearchParams] = useState<Record<string, string>>({})
const [editingIndustries, setEditingIndustries] = useState<string[]>([])
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<AccountPublic> }) =>
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<AccountPublic>[] = [
{ 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 (
<div>
<PageHeader title="账号管理" description="管理系统用户账号、角色与权限" />
<PageHeader title="账号管理" description="管理系统用户账号、角色、权限与行业授权" />
<ProTable<AccountPublic>
columns={columns}
@@ -169,7 +231,6 @@ export default function Accounts() {
const filtered: Record<string, string> = {}
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={<span className="text-base font-semibold"></span>}
open={modalOpen}
onOk={handleSave}
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
onCancel={handleClose}
confirmLoading={updateMutation.isPending}
width={560}
>
<Form form={form} layout="vertical" className="mt-4">
<Form.Item name="display_name" label="显示名">
@@ -215,6 +277,21 @@ export default function Accounts() {
{ value: 'relay', label: 'SaaS 中转 (Token 池)' },
]} />
</Form.Item>
<Divider></Divider>
<Form.Item
name="industry_ids"
label="授权行业"
extra="第一个行业将设为主行业。行业决定管家可触达的知识域和技能优先级。"
>
<Select
mode="multiple"
placeholder="选择授权的行业"
options={industryOptions}
loading={!industriesData}
/>
</Form.Item>
</Form>
</Modal>
</div>