feat(admin): 新增后台系统配置管理
This commit is contained in:
+3
-3
@@ -194,12 +194,12 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB
|
||||
## 5. 已知限制
|
||||
|
||||
1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。
|
||||
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志,其余页面仍为统一占位。
|
||||
2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志、系统配置,其余页面仍为统一占位。
|
||||
3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`;系统配置与分销站点仍需核对。
|
||||
4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`,系统配置 `system_config:view` / `system_config:update`;分销站点仍需核对。
|
||||
5. SVG 中文尚未转曲。
|
||||
6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。
|
||||
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告与审计日志已按批次提交。
|
||||
7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告、审计日志与系统配置已按批次提交。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Phase 2 验收报告(后台系统配置)
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 项目:解忧商行(hfb_jysh)
|
||||
> 范围:`/console-jy8k3m/system-configs`
|
||||
> 结论:**本切片通过,系统配置列表、筛选、JSON 格式化与更新已接真实接口**
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口与权限
|
||||
|
||||
| 能力 | 接口 | 权限码 |
|
||||
| --- | --- | --- |
|
||||
| 配置列表 | `GET /api/admin/system-configs` | `system_config:view` |
|
||||
| 更新配置 | `PUT /api/admin/system-configs/:key` | `system_config:update` |
|
||||
|
||||
更新请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"value": "配置值字符串",
|
||||
"description": "配置说明"
|
||||
}
|
||||
```
|
||||
|
||||
后端行为:
|
||||
|
||||
- 配置键不可新增或删除,只能在默认配置白名单内更新。
|
||||
- `backup.*` 只能走独立备份调度接口,不能从本页修改。
|
||||
- `integration.paddle_ocr_token` 允许保存空值。
|
||||
- `distribution.default_settlement_hold_days` 必须为 0–365 的整数。
|
||||
- `listing.publish_cooldown_minutes` 必须为不小于 0 的整数。
|
||||
- 发布选项、租客成长规则与短信模板由后端做结构校验和归一化;其余 JSON 配置主要由前端校验可解析,保存前需人工核对结构。
|
||||
- 每次更新写入 `audit_logs`,动作为 `system_config.update`。
|
||||
|
||||
## 2. 页面
|
||||
|
||||
`frontend/src/features/admin/AdminSystemConfigsPage.tsx`
|
||||
|
||||
- 一次读取全部可见配置,前端按关键字与分类即时筛选
|
||||
- 分类:聊天与群组、分销、交接与超时、第三方集成、商品发布、移动端首页、订单、个人中心、租客成长、短信
|
||||
- 列:配置键/说明、分类、类型、当前值、更新时间、操作
|
||||
- 类型标签:`JSON` / `文本`,敏感键额外显示 `敏感`
|
||||
- 敏感键(token、secret、password、api_key、private_key)在列表中只显示「已配置 / 未配置」,进入编辑后可见原值
|
||||
- 行点击或「编辑」打开配置弹窗
|
||||
- 无 `system_config:view` 时停止请求并显示权限提示;无 `system_config:update` 时只读
|
||||
|
||||
## 3. 编辑弹窗
|
||||
|
||||
- 只读展示配置键、分类、类型与敏感标记
|
||||
- 配置说明可编辑,最多 200 字
|
||||
- 配置值使用等宽字体文本域,JSON 值默认两空格缩进
|
||||
- 「格式化 JSON」仅在当前值可解析时可用
|
||||
- 保存前做前端校验:
|
||||
- 非空(OCR token 除外)
|
||||
- 分销结算冷却天数 0–365
|
||||
- 商品发布冷却分钟数不小于 0
|
||||
- 原值为 JSON 或新值以 `{` / `[` 开头时必须能解析
|
||||
- 保存前二次确认,提示配置会立即影响下一次读取并写入审计日志
|
||||
- 敏感配置额外提示不要将生产密钥写入开发库
|
||||
|
||||
## 4. 菜单与路由
|
||||
|
||||
- 路由:`frontend/src/app/AppRouter.tsx` 新增 `system-configs`
|
||||
- 菜单:`frontend/src/layouts/AdminLayout.tsx` 权限码由 `systemconfig:view` 修正为 `system_config:view`
|
||||
- 占位列表同步移除 `system-configs`
|
||||
|
||||
## 5. 开发库配置概况
|
||||
|
||||
当前可见配置 27 项,其中 JSON 配置包含:
|
||||
|
||||
```text
|
||||
listing.publish_agreements
|
||||
listing.publish_options
|
||||
listing.sale_price_config
|
||||
mobile.aw_recycle
|
||||
mobile.cooperation_feedback
|
||||
mobile.home_announcements
|
||||
mobile.home_banners
|
||||
order.agreements
|
||||
profile.post_rental_notice
|
||||
renter.growth_level_rules
|
||||
sms.business_templates
|
||||
```
|
||||
|
||||
## 6. 验证结果
|
||||
|
||||
```text
|
||||
npx tsc --noEmit 通过
|
||||
npm run build 通过(入口 497.41 kB,gzip 161.38 kB)
|
||||
GET /api/admin/system-configs ok,返回 27 项
|
||||
GET /console-jy8k3m/system-configs 200
|
||||
```
|
||||
|
||||
列表读取已实测;未执行 `PUT` 写操作,避免改动开发库运行参数。
|
||||
|
||||
## 7. 已知限制
|
||||
|
||||
1. 当前是原始键值编辑器,首页轮播、发布协议、短信模板等复杂 JSON 仍直接编辑原文,没有结构化表单。
|
||||
2. 不支持新增或删除配置键,新增配置需先在后端默认配置中登记。
|
||||
3. 备份调度不在本页,使用独立接口与后台入口。
|
||||
4. 没有配置变更历史对比,需到审计日志查看 `system_config.update` 明细。
|
||||
5. 敏感值仅在编辑弹窗展示,未接密钥轮换或加密存储改造。
|
||||
|
||||
## 8. 下一步建议
|
||||
|
||||
1. 对首页轮播、发布协议、短信模板做结构化编辑器。
|
||||
2. 线下出款与运营支出登记。
|
||||
3. 分销站点管理页。
|
||||
4. 移动端下单、支付与交接/归还/结账。
|
||||
+1
-1
@@ -395,7 +395,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 4:管理后台核心(4–5 周)
|
||||
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理与审计日志;线下出款/运营支出与系统配置待迁移。
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理、审计日志与系统配置;线下出款/运营支出待迁移。
|
||||
|
||||
- 后台登录、工作台、订单列表、订单详情。
|
||||
- 商品列表、商品详情、审核。
|
||||
|
||||
@@ -108,6 +108,11 @@ const AdminAuditLogsPage = lazy(() =>
|
||||
default: module.AdminAuditLogsPage,
|
||||
}))
|
||||
)
|
||||
const AdminSystemConfigsPage = lazy(() =>
|
||||
import('@/features/admin/AdminSystemConfigsPage').then(module => ({
|
||||
default: module.AdminSystemConfigsPage,
|
||||
}))
|
||||
)
|
||||
const AdminLayout = lazy(() =>
|
||||
import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout }))
|
||||
)
|
||||
@@ -117,7 +122,6 @@ const AdminSectionPlaceholder = lazy(() =>
|
||||
|
||||
const adminSections: Array<{ path: string; title: string; description: string }> = [
|
||||
{ path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' },
|
||||
{ path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
|
||||
]
|
||||
|
||||
export function AppRouter() {
|
||||
@@ -159,6 +163,7 @@ export function AppRouter() {
|
||||
<Route path="finance" element={<AdminFinancePage />} />
|
||||
<Route path="announcements" element={<AdminAnnouncementsPage />} />
|
||||
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||||
<Route path="system-configs" element={<AdminSystemConfigsPage />} />
|
||||
{adminSections.map(section => (
|
||||
<Route
|
||||
key={section.path}
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import {
|
||||
CodeOutlined,
|
||||
EditOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
import { fetchAdminSystemConfigs, updateAdminSystemConfig } from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
|
||||
import {
|
||||
systemConfigCategory,
|
||||
systemConfigCategoryLabel,
|
||||
systemConfigCategoryOptions,
|
||||
} from '@/features/admin/labels'
|
||||
import type { AdminSystemConfig } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatText } from '@/shared/utils/format'
|
||||
|
||||
const SENSITIVE_PATTERN = /(token|secret|password|api[_-]?key|private[_-]?key)/i
|
||||
const EMPTY_TOKEN_KEY = 'integration.paddle_ocr_token'
|
||||
|
||||
function isSensitive(item: AdminSystemConfig) {
|
||||
return SENSITIVE_PATTERN.test(item.key)
|
||||
}
|
||||
|
||||
function parseJson(value: string): { valid: boolean; parsed?: unknown } {
|
||||
const text = value.trim()
|
||||
if (!text.startsWith('{') && !text.startsWith('[')) return { valid: false }
|
||||
try {
|
||||
return { valid: true, parsed: JSON.parse(text) }
|
||||
} catch {
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
function isJsonConfig(item: AdminSystemConfig) {
|
||||
return parseJson(item.value).valid
|
||||
}
|
||||
|
||||
function formatJson(value: string) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function previewValue(item: AdminSystemConfig) {
|
||||
if (isSensitive(item)) return item.value ? '已配置,编辑时可见' : '未配置'
|
||||
const json = parseJson(item.value)
|
||||
const text = json.valid ? JSON.stringify(json.parsed) : item.value
|
||||
return text.length > 160 ? `${text.slice(0, 160)}…` : text || '空字符串'
|
||||
}
|
||||
|
||||
function validateValue(item: AdminSystemConfig, value: string) {
|
||||
if (item.key !== EMPTY_TOKEN_KEY && !value.trim()) return '配置值不能为空'
|
||||
if (item.key === 'distribution.default_settlement_hold_days') {
|
||||
const days = Number(value)
|
||||
if (!Number.isInteger(days) || days < 0 || days > 365) {
|
||||
return '结算冷却天数必须是 0-365 的整数'
|
||||
}
|
||||
}
|
||||
if (item.key === 'listing.publish_cooldown_minutes') {
|
||||
const minutes = Number(value)
|
||||
if (!Number.isInteger(minutes) || minutes < 0) {
|
||||
return '发布冷却分钟数必须是不小于 0 的整数'
|
||||
}
|
||||
}
|
||||
if (isJsonConfig(item) || value.trim().startsWith('{') || value.trim().startsWith('[')) {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
} catch {
|
||||
return 'JSON 格式不正确,请先修正或点击「格式化 JSON」'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function buildColumns(
|
||||
onEdit: (item: AdminSystemConfig) => void,
|
||||
canUpdate: boolean
|
||||
): TableColumnsType<AdminSystemConfig> {
|
||||
return [
|
||||
{
|
||||
title: '配置键',
|
||||
dataIndex: 'key',
|
||||
width: 330,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span className="admin-mono">{row.key}</span>
|
||||
<span>{formatText(row.description)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (_, row) => <Tag>{systemConfigCategoryLabel(row.key)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 110,
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
{isJsonConfig(row) ? <Tag color="blue">JSON</Tag> : <Tag>文本</Tag>}
|
||||
{isSensitive(row) ? <Tag color="red">敏感</Tag> : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
width: 380,
|
||||
render: (_, row) => (
|
||||
<Typography.Text type="secondary" ellipsis={{ tooltip: previewValue(row) }}>
|
||||
{isSensitive(row) ? (
|
||||
<span className="admin-mono">{previewValue(row)}</span>
|
||||
) : (
|
||||
<span className="admin-audit-summary">{previewValue(row)}</span>
|
||||
)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatDateTime(row.updated_at)}</span>
|
||||
<span className="admin-mono">
|
||||
{row.updated_by != null ? `操作人 #${row.updated_by}` : '系统默认值'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 96,
|
||||
fixed: 'right',
|
||||
render: (_, row) =>
|
||||
canUpdate ? (
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => onEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : (
|
||||
<Typography.Text type="secondary">只读</Typography.Text>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function AdminSystemConfigsPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [category, setCategory] = useState('')
|
||||
const [editing, setEditing] = useState<AdminSystemConfig | null>(null)
|
||||
const [draftValue, setDraftValue] = useState('')
|
||||
const [draftDescription, setDraftDescription] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view'))
|
||||
const canUpdate = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('system_config:update')
|
||||
)
|
||||
|
||||
const { data, isPending, isFetching, isError, error, refetch } = useQuery({
|
||||
queryKey: ['admin-system-configs'],
|
||||
queryFn: fetchAdminSystemConfigs,
|
||||
enabled: canView,
|
||||
})
|
||||
|
||||
const items = data ?? []
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const text = keyword.trim().toLowerCase()
|
||||
return items.filter(item => {
|
||||
const matchesCategory = !category || systemConfigCategory(item.key) === category
|
||||
const matchesKeyword =
|
||||
!text ||
|
||||
item.key.toLowerCase().includes(text) ||
|
||||
(item.description ?? '').toLowerCase().includes(text)
|
||||
return matchesCategory && matchesKeyword
|
||||
})
|
||||
}, [items, keyword, category])
|
||||
|
||||
const jsonCount = items.filter(isJsonConfig).length
|
||||
const sensitiveCount = items.filter(isSensitive).length
|
||||
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
|
||||
const hasActiveFilters = Boolean(keyword || category)
|
||||
|
||||
const openEditor = (item: AdminSystemConfig) => {
|
||||
setEditing(item)
|
||||
setDraftValue(isJsonConfig(item) ? formatJson(item.value) : item.value)
|
||||
setDraftDescription(item.description)
|
||||
}
|
||||
|
||||
const closeEditor = () => {
|
||||
if (saving) return
|
||||
setEditing(null)
|
||||
}
|
||||
|
||||
const doSave = async () => {
|
||||
if (!editing) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateAdminSystemConfig(editing.key, {
|
||||
value: draftValue,
|
||||
description: draftDescription.trim(),
|
||||
})
|
||||
message.success('系统配置已更新,并已写入审计日志')
|
||||
setEditing(null)
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : '更新失败,请稍后重试')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
if (!editing) return
|
||||
const validation = validateValue(editing, draftValue)
|
||||
if (validation) {
|
||||
message.warning(validation)
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: '确认更新系统配置?',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
content: (
|
||||
<div className="admin-stack">
|
||||
<span>
|
||||
配置键:<span className="admin-mono">{editing.key}</span>
|
||||
</span>
|
||||
<span>保存后前台或后台的下一次读取会立即使用新值,并记录一条审计日志。</span>
|
||||
<span>请确认已经核对过 JSON 结构、金额单位与布尔值。</span>
|
||||
</div>
|
||||
),
|
||||
okText: '确认更新',
|
||||
cancelText: '再检查一下',
|
||||
onOk: doSave,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(() => buildColumns(openEditor, canUpdate), [canUpdate])
|
||||
const editingJson = editing ? parseJson(draftValue).valid : false
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 系统"
|
||||
title="系统配置"
|
||||
description="维护首页、发布、订单、交接、聊天与第三方集成的运行参数;所有写入都会记录审计日志。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {items.length} 项</Tag>
|
||||
{jsonCount > 0 ? <Tag color="blue">JSON {jsonCount} 项</Tag> : null}
|
||||
{sensitiveCount > 0 ? <Tag color="red">敏感 {sensitiveCount} 项</Tag> : null}
|
||||
{isFetching && !isPending ? <Tag color="processing">正在更新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => void refetch()}
|
||||
loading={isFetching}
|
||||
disabled={!canView}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{!canView ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="当前账号没有系统配置查看权限"
|
||||
description="需要 system_config:view 权限才能读取配置,修改还需要 system_config:update。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FilterBar
|
||||
onReset={
|
||||
hasActiveFilters
|
||||
? () => {
|
||||
setKeyword('')
|
||||
setCategory('')
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
loading={isFetching}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{filtered.length === items.length
|
||||
? '显示全部配置'
|
||||
: `筛选出 ${filtered.length} / ${items.length} 项`}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<FilterField label="关键字" width={280}>
|
||||
<Input
|
||||
allowClear
|
||||
value={keyword}
|
||||
placeholder="搜索配置键或说明"
|
||||
onChange={event => setKeyword(event.target.value)}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="分类" width={200}>
|
||||
<Select
|
||||
allowClear
|
||||
value={category || undefined}
|
||||
placeholder="全部分类"
|
||||
options={systemConfigCategoryOptions}
|
||||
onChange={value => setCategory(value ?? '')}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="系统配置没有加载出来"
|
||||
description={errorText}
|
||||
action={
|
||||
<Button size="small" onClick={() => void refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminSystemConfig>
|
||||
title={<span>运行参数</span>}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{isPending ? '加载中' : `当前 ${filtered.length} 项 · 点击「编辑」修改`}
|
||||
</Typography.Text>
|
||||
}
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
loading={isFetching}
|
||||
onRowClick={canUpdate ? openEditor : undefined}
|
||||
emptyDescription={canView ? '没有匹配的系统配置' : '当前账号没有查看权限'}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={Boolean(editing)}
|
||||
title={
|
||||
<div className="admin-detail-drawer__head">
|
||||
<div className="admin-detail-drawer__title">编辑系统配置</div>
|
||||
{editing ? (
|
||||
<div className="admin-detail-drawer__subtitle">
|
||||
{systemConfigCategoryLabel(editing.key)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
width={780}
|
||||
onCancel={closeEditor}
|
||||
maskClosable={!saving}
|
||||
closable={!saving}
|
||||
destroyOnHidden
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={closeEditor} disabled={saving}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CodeOutlined />}
|
||||
disabled={!editingJson || saving}
|
||||
onClick={() => setDraftValue(formatJson(draftValue))}
|
||||
>
|
||||
格式化 JSON
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={submit} disabled={!canUpdate}>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{editing ? (
|
||||
<div className="admin-config-form">
|
||||
<Space size={8} wrap>
|
||||
<Tag className="admin-mono">{editing.key}</Tag>
|
||||
{isJsonConfig(editing) ? <Tag color="blue">JSON</Tag> : <Tag>文本</Tag>}
|
||||
{isSensitive(editing) ? <Tag color="red">敏感</Tag> : null}
|
||||
<Tag>{systemConfigCategoryLabel(editing.key)}</Tag>
|
||||
</Space>
|
||||
|
||||
<div className="admin-filter-field">
|
||||
<span className="admin-filter-field__label">配置说明</span>
|
||||
<Input
|
||||
value={draftDescription}
|
||||
maxLength={200}
|
||||
showCount
|
||||
placeholder="说明这项配置的用途与取值范围"
|
||||
onChange={event => setDraftDescription(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="admin-filter-field">
|
||||
<span className="admin-filter-field__label">配置值</span>
|
||||
<Input.TextArea
|
||||
value={draftValue}
|
||||
autoSize={{ minRows: editingJson ? 12 : 4, maxRows: 20 }}
|
||||
spellCheck={false}
|
||||
className="admin-config-editor"
|
||||
placeholder={editing.key === EMPTY_TOKEN_KEY ? '可留空' : '请输入配置值'}
|
||||
onChange={event => setDraftValue(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSensitive(editing) ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="敏感配置"
|
||||
description="该值包含令牌或密钥,保存后会用于第三方调用。请确认没有把生产密钥写入开发库或粘贴到公开渠道。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Typography.Text type="secondary">
|
||||
配置键不可修改;需要新增配置时先在后端默认配置中登记,再回到本页调整取值。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
AdminOrderDetail,
|
||||
AdminOrderPage,
|
||||
AdminOrderQuery,
|
||||
AdminSystemConfig,
|
||||
AdminSystemConfigUpdatePayload,
|
||||
AdminUser,
|
||||
AdminUserItem,
|
||||
AdminUserPage,
|
||||
@@ -422,3 +424,21 @@ export async function fetchAdminAuditLogs(query: AdminAuditQuery) {
|
||||
}
|
||||
|
||||
export type { AdminAuditLog }
|
||||
|
||||
export async function fetchAdminSystemConfigs() {
|
||||
const { data } = await adminClient.get<ApiResponse<{ items?: AdminSystemConfig[] }>>(
|
||||
'/admin/system-configs'
|
||||
)
|
||||
return data.data.items ?? []
|
||||
}
|
||||
|
||||
export async function updateAdminSystemConfig(
|
||||
key: string,
|
||||
payload: AdminSystemConfigUpdatePayload
|
||||
) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminSystemConfig>>(
|
||||
`/admin/system-configs/${encodeURIComponent(key)}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -310,3 +310,29 @@ export const auditBizTypeOptions = Object.entries(auditBizTypeMap).map(([value,
|
||||
value,
|
||||
label,
|
||||
}))
|
||||
|
||||
export const systemConfigCategoryMap: Record<string, string> = {
|
||||
chat: '聊天与群组',
|
||||
distribution: '分销',
|
||||
handoff: '交接与超时',
|
||||
integration: '第三方集成',
|
||||
listing: '商品发布',
|
||||
mobile: '移动端首页',
|
||||
order: '订单',
|
||||
profile: '个人中心',
|
||||
renter: '租客成长',
|
||||
sms: '短信',
|
||||
}
|
||||
|
||||
export const systemConfigCategoryOptions = Object.entries(systemConfigCategoryMap).map(
|
||||
([value, label]) => ({ value, label })
|
||||
)
|
||||
|
||||
export function systemConfigCategory(key: string) {
|
||||
return key.split('.')[0] || 'other'
|
||||
}
|
||||
|
||||
export function systemConfigCategoryLabel(key: string) {
|
||||
const category = systemConfigCategory(key)
|
||||
return systemConfigCategoryMap[category] ?? category
|
||||
}
|
||||
|
||||
@@ -607,3 +607,18 @@ export interface AdminAuditPage {
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminSystemConfig {
|
||||
id: number
|
||||
key: string
|
||||
value: string
|
||||
description: string
|
||||
updated_by?: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminSystemConfigUpdatePayload {
|
||||
value: string
|
||||
description: string
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ const allNavGroups: NavGroup[] = [
|
||||
label: '系统',
|
||||
icon: <SettingOutlined />,
|
||||
children: [
|
||||
{ key: '/system-configs', label: '系统配置', permission: 'systemconfig:view' },
|
||||
{ key: '/system-configs', label: '系统配置', permission: 'system_config:view' },
|
||||
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1489,6 +1489,18 @@ a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.admin-config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.admin-config-editor {
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-detail-drawer .ant-drawer-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user