杂项配置面板管理本地Mock自定义店铺支持增删改名
This commit is contained in:
@@ -2,10 +2,12 @@ import { Router } from 'express'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
acceptAdminWorkOrders,
|
acceptAdminWorkOrders,
|
||||||
|
addAdminMockCustomShop,
|
||||||
assignAdminWorkOrderToWorker,
|
assignAdminWorkOrderToWorker,
|
||||||
cancelAdminWorkOrder,
|
cancelAdminWorkOrder,
|
||||||
confirmAdminWorkOrderFriendAdded,
|
confirmAdminWorkOrderFriendAdded,
|
||||||
createAdminMockWorkOrder,
|
createAdminMockWorkOrder,
|
||||||
|
deleteAdminMockCustomShop,
|
||||||
deleteAdminWorkOrder,
|
deleteAdminWorkOrder,
|
||||||
getAdminWorkOrderEvents,
|
getAdminWorkOrderEvents,
|
||||||
getAdminWorkOrderSharing,
|
getAdminWorkOrderSharing,
|
||||||
@@ -13,6 +15,7 @@ import {
|
|||||||
listAdminWorkOrders,
|
listAdminWorkOrders,
|
||||||
pinAdminWorkOrder,
|
pinAdminWorkOrder,
|
||||||
publishAdminWorkOrder,
|
publishAdminWorkOrder,
|
||||||
|
renameAdminMockCustomShop,
|
||||||
reopenAdminCancelledWorkOrder,
|
reopenAdminCancelledWorkOrder,
|
||||||
reprocessAdminKuaishouSendCodeWorkOrders,
|
reprocessAdminKuaishouSendCodeWorkOrders,
|
||||||
resetAdminWorkOrderGiftMaterial,
|
resetAdminWorkOrderGiftMaterial,
|
||||||
@@ -46,6 +49,61 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/orders/mock/custom-shops',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
|
createJsonHandler((req) => addAdminMockCustomShop(String(req.body?.name || '')), {
|
||||||
|
successMessage: '自定义店铺已新增',
|
||||||
|
errorMessage: '新增自定义店铺失败',
|
||||||
|
scope: '[admin/worker-platform/orders/mock/custom-shops]',
|
||||||
|
audit: (req) => ({
|
||||||
|
action: 'mock_custom_shop_added',
|
||||||
|
targetType: 'mock_custom_shop',
|
||||||
|
targetId: String(req.body?.name || ''),
|
||||||
|
data: {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/worker-platform/orders/mock/custom-shops',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
renameAdminMockCustomShop(
|
||||||
|
String(req.body?.name || ''),
|
||||||
|
String(req.body?.newName || req.body?.nextName || ''),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '自定义店铺已重命名',
|
||||||
|
errorMessage: '重命名自定义店铺失败',
|
||||||
|
scope: '[admin/worker-platform/orders/mock/custom-shops]',
|
||||||
|
audit: (req) => ({
|
||||||
|
action: 'mock_custom_shop_renamed',
|
||||||
|
targetType: 'mock_custom_shop',
|
||||||
|
targetId: String(req.body?.name || ''),
|
||||||
|
data: { newName: String(req.body?.newName || '') },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/worker-platform/orders/mock/custom-shops',
|
||||||
|
requireAdminRoles(['admin']),
|
||||||
|
createJsonHandler((req) => deleteAdminMockCustomShop(String(req.query?.name || '')), {
|
||||||
|
successMessage: '自定义店铺已删除',
|
||||||
|
errorMessage: '删除自定义店铺失败',
|
||||||
|
scope: '[admin/worker-platform/orders/mock/custom-shops]',
|
||||||
|
audit: (req) => ({
|
||||||
|
action: 'mock_custom_shop_deleted',
|
||||||
|
targetType: 'mock_custom_shop',
|
||||||
|
targetId: String(req.query?.name || ''),
|
||||||
|
data: {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/mock',
|
'/worker-platform/orders/mock',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||||
|
|
||||||
/** 记忆的自定义店铺上限,超出后淘汰最早录入的。 */
|
/** 记忆的自定义店铺上限,超出后淘汰最早录入的。 */
|
||||||
@@ -24,12 +25,49 @@ export async function rememberAdminMockCustomShop(shopName: string): Promise<str
|
|||||||
0,
|
0,
|
||||||
MAX_CUSTOM_SHOPS,
|
MAX_CUSTOM_SHOPS,
|
||||||
)
|
)
|
||||||
const saved = await saveAppConfigEntry({
|
return saveMockCustomShops(nextShops)
|
||||||
configKey: APP_CONFIG_KEYS.workerMockShops,
|
}
|
||||||
value: { shops: nextShops },
|
|
||||||
normalize: normalizeMockCustomShops,
|
/** 手动新增自定义店铺,重名时视为成功(幂等)。 */
|
||||||
|
export async function addAdminMockCustomShop(shopName: string): Promise<string[]> {
|
||||||
|
const name = requireShopName(shopName, '新增')
|
||||||
|
return saveMockCustomShops([name, ...listAdminMockCustomShops().filter((item) => item !== name)])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重命名自定义店铺;新名称已存在时合并(移除旧名称)。 */
|
||||||
|
export async function renameAdminMockCustomShop(
|
||||||
|
oldName: string,
|
||||||
|
nextName: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const currentShops = listAdminMockCustomShops()
|
||||||
|
const from = requireShopName(oldName, '重命名')
|
||||||
|
const to = requireShopName(nextName, '重命名为')
|
||||||
|
if (!currentShops.includes(from)) {
|
||||||
|
throw createHttpError('该自定义店铺不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'mock_custom_shop_not_found',
|
||||||
})
|
})
|
||||||
return saved.shops
|
}
|
||||||
|
return saveMockCustomShops(
|
||||||
|
currentShops.flatMap((item) => {
|
||||||
|
if (item === from) return [to]
|
||||||
|
if (item === to) return [] // 新名称原本已存在时合并,避免重复
|
||||||
|
return [item]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除自定义店铺,不影响已用它生成的工单。 */
|
||||||
|
export async function deleteAdminMockCustomShop(shopName: string): Promise<string[]> {
|
||||||
|
const currentShops = listAdminMockCustomShops()
|
||||||
|
const name = normalizeShopName(shopName)
|
||||||
|
if (!name || !currentShops.includes(name)) {
|
||||||
|
throw createHttpError('该自定义店铺不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'mock_custom_shop_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return saveMockCustomShops(currentShops.filter((item) => item !== name))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeMockCustomShops(rawValue: unknown): MockCustomShopsConfig {
|
export function normalizeMockCustomShops(rawValue: unknown): MockCustomShopsConfig {
|
||||||
@@ -40,6 +78,26 @@ export function normalizeMockCustomShops(rawValue: unknown): MockCustomShopsConf
|
|||||||
return { shops: normalized.slice(0, MAX_CUSTOM_SHOPS) }
|
return { shops: normalized.slice(0, MAX_CUSTOM_SHOPS) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMockCustomShops(shops: string[]): Promise<string[]> {
|
||||||
|
const saved = await saveAppConfigEntry({
|
||||||
|
configKey: APP_CONFIG_KEYS.workerMockShops,
|
||||||
|
value: { shops },
|
||||||
|
normalize: normalizeMockCustomShops,
|
||||||
|
})
|
||||||
|
return saved.shops
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireShopName(value: unknown, action: string): string {
|
||||||
|
const name = normalizeShopName(value)
|
||||||
|
if (!name) {
|
||||||
|
throw createHttpError(`请填写要${action}的店铺名称`, {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'mock_custom_shop_name_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
function createDefaultMockCustomShops(): MockCustomShopsConfig {
|
function createDefaultMockCustomShops(): MockCustomShopsConfig {
|
||||||
return { shops: [] }
|
return { shops: [] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
{ key: 'feedbacks', label: '问题反馈', children: <FeedbacksPanel /> },
|
||||||
afterSalesTab,
|
afterSalesTab,
|
||||||
...(isAdmin
|
...(isAdmin
|
||||||
? [{ key: 'announcement', label: '公告管理', children: <AnnouncementPanel /> }]
|
? [{ key: 'announcement', label: '杂项配置', children: <AnnouncementPanel /> }]
|
||||||
: []),
|
: []),
|
||||||
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
|
||||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { App, Button, Card, Form, Input, Space, Switch, Typography } from 'antd'
|
import { App, Button, Card, Form, Input, Popconfirm, Space, Switch, Typography } from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { fetchAdminWorkerAnnouncement, saveAdminWorkerAnnouncement } from '@/services/admin'
|
import {
|
||||||
|
addAdminMockCustomShop,
|
||||||
|
deleteAdminMockCustomShop,
|
||||||
|
fetchAdminMockCustomShops,
|
||||||
|
fetchAdminWorkerAnnouncement,
|
||||||
|
renameAdminMockCustomShop,
|
||||||
|
saveAdminWorkerAnnouncement,
|
||||||
|
} from '@/services/admin'
|
||||||
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
|
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
|
||||||
|
|
||||||
export default function AnnouncementPanel() {
|
export default function AnnouncementPanel() {
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
|
<AnnouncementCard />
|
||||||
|
<MockCustomShopsCard />
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnnouncementCard() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [form] = Form.useForm<WorkerAnnouncementConfig>()
|
const [form] = Form.useForm<WorkerAnnouncementConfig>()
|
||||||
@@ -59,3 +75,116 @@ export default function AnnouncementPanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MockCustomShopsCard() {
|
||||||
|
const { message, modal } = App.useApp()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [adding, setAdding] = useState(false)
|
||||||
|
const [addForm] = Form.useForm<{ name: string }>()
|
||||||
|
const customShopsQuery = useQuery({
|
||||||
|
queryKey: ['admin-mock-custom-shops'],
|
||||||
|
queryFn: fetchAdminMockCustomShops,
|
||||||
|
})
|
||||||
|
const shops = customShopsQuery.data?.data.items || []
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-mock-custom-shops'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runShopAction(action: () => Promise<unknown>, successMessage: string) {
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
await refresh()
|
||||||
|
message.success(successMessage)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addShop(values: { name: string }) {
|
||||||
|
setAdding(true)
|
||||||
|
try {
|
||||||
|
await runShopAction(
|
||||||
|
() => addAdminMockCustomShop(String(values.name || '').trim()),
|
||||||
|
'自定义店铺已新增',
|
||||||
|
)
|
||||||
|
addForm.resetFields()
|
||||||
|
} finally {
|
||||||
|
setAdding(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameShop(name: string) {
|
||||||
|
let nextName = ''
|
||||||
|
modal.confirm({
|
||||||
|
title: '重命名自定义店铺',
|
||||||
|
content: (
|
||||||
|
<Input
|
||||||
|
defaultValue={name}
|
||||||
|
maxLength={64}
|
||||||
|
placeholder="请输入新的店铺名称"
|
||||||
|
onChange={(event) => {
|
||||||
|
nextName = String(event.target.value || '')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onOk: async () => {
|
||||||
|
const trimmed = nextName.trim()
|
||||||
|
if (!trimmed || trimmed === name) return
|
||||||
|
await runShopAction(() => renameAdminMockCustomShop(name, trimmed), '自定义店铺已重命名')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="本地 Mock 自定义店铺" bordered={false}>
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||||
|
在接单工单「本地 Mock」中录入过的外部平台店铺会自动记忆到这里;也可以直接维护,
|
||||||
|
维护后的店铺会出现在 Mock 表单的下拉选项中。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Form form={addForm} layout="inline" onFinish={addShop} style={{ marginBottom: 12 }}>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, whitespace: true, message: '请输入店铺名称' }]}
|
||||||
|
style={{ minWidth: 260 }}
|
||||||
|
>
|
||||||
|
<Input maxLength={64} placeholder="输入店铺名称,如:抖音-某某小店" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={adding}>
|
||||||
|
新增店铺
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
{shops.length === 0 ? (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
还没有自定义店铺,先在上方新增或通过本地 Mock 录入。
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<Space size={[8, 8]} wrap>
|
||||||
|
{shops.map((name) => (
|
||||||
|
<Space key={name} size={4} wrap>
|
||||||
|
<Typography.Text>{name}</Typography.Text>
|
||||||
|
<Button size="small" type="text" onClick={() => renameShop(name)}>
|
||||||
|
重命名
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title={`删除自定义店铺「${name}」?`}
|
||||||
|
description="不影响已用它生成的工单,仅从 Mock 下拉选项移除。"
|
||||||
|
okText="删除"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
onConfirm={() =>
|
||||||
|
runShopAction(() => deleteAdminMockCustomShop(name), '自定义店铺已删除')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button size="small" type="text" danger>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -526,6 +526,25 @@ export function fetchAdminMockCustomShops() {
|
|||||||
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/orders/mock/custom-shops')
|
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/orders/mock/custom-shops')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addAdminMockCustomShop(name: string) {
|
||||||
|
return apiPost<string[]>('/api/v1/admin/worker-platform/orders/mock/custom-shops', {
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renameAdminMockCustomShop(name: string, newName: string) {
|
||||||
|
return apiPut<string[]>('/api/v1/admin/worker-platform/orders/mock/custom-shops', {
|
||||||
|
name,
|
||||||
|
newName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAdminMockCustomShop(name: string) {
|
||||||
|
return apiDelete<string[]>(
|
||||||
|
`/api/v1/admin/worker-platform/orders/mock/custom-shops?name=${encodeURIComponent(name)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function createAdminMockWorkOrder(payload: {
|
export function createAdminMockWorkOrder(payload: {
|
||||||
platformOrderId: string
|
platformOrderId: string
|
||||||
sellerId: string
|
sellerId: string
|
||||||
|
|||||||
Reference in New Issue
Block a user