feat(mohong): 新增摸红商城后台四页
This commit is contained in:
@@ -281,6 +281,26 @@ const AdminBackupsPage = lazy(() =>
|
||||
default: module.AdminBackupsPage,
|
||||
}))
|
||||
)
|
||||
const AdminCrashCategoriesPage = lazy(() =>
|
||||
import('@/features/admin/AdminCrashCategoriesPage').then(module => ({
|
||||
default: module.AdminCrashCategoriesPage,
|
||||
}))
|
||||
)
|
||||
const AdminCrashProductsPage = lazy(() =>
|
||||
import('@/features/admin/AdminCrashProductsPage').then(module => ({
|
||||
default: module.AdminCrashProductsPage,
|
||||
}))
|
||||
)
|
||||
const AdminCrashOrdersPage = lazy(() =>
|
||||
import('@/features/admin/AdminCrashOrdersPage').then(module => ({
|
||||
default: module.AdminCrashOrdersPage,
|
||||
}))
|
||||
)
|
||||
const AdminCrashConfigPage = lazy(() =>
|
||||
import('@/features/admin/AdminCrashConfigPage').then(module => ({
|
||||
default: module.AdminCrashConfigPage,
|
||||
}))
|
||||
)
|
||||
const AdminManualEntriesPage = lazy(() =>
|
||||
import('@/features/admin/AdminManualEntriesPage').then(module => ({
|
||||
default: module.AdminManualEntriesPage,
|
||||
@@ -495,6 +515,10 @@ export function AppRouter() {
|
||||
<Route path="roles" element={<AdminRolesPage />} />
|
||||
<Route path="admin-users" element={<AdminStaffAccountsPage />} />
|
||||
<Route path="backups" element={<AdminBackupsPage />} />
|
||||
<Route path="crash/categories" element={<AdminCrashCategoriesPage />} />
|
||||
<Route path="crash/products" element={<AdminCrashProductsPage />} />
|
||||
<Route path="crash/orders" element={<AdminCrashOrdersPage />} />
|
||||
<Route path="crash/config" element={<AdminCrashConfigPage />} />
|
||||
<Route path="manual-entries" element={<AdminManualEntriesPage />} />
|
||||
<Route path="payments" element={<AdminPaymentsPage />} />
|
||||
<Route path="pickup" element={<AdminPickupsPage />} />
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Alert, App, Button, Form, Input, InputNumber, Modal, Select, Space, Tag, type TableColumnsType } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
createCrashCategory,
|
||||
deleteCrashCategory,
|
||||
fetchCrashCategories,
|
||||
updateCrashCategory,
|
||||
} from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import type { AdminCrashCategory } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatText } from '@/shared/utils/format'
|
||||
|
||||
interface FormValues {
|
||||
name: string
|
||||
code?: string
|
||||
sort_order?: number
|
||||
status: string
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
|
||||
}
|
||||
|
||||
export function AdminCrashCategoriesPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<FormValues>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AdminCrashCategory>()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_view'))
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:category'))
|
||||
|
||||
const categoriesQuery = useQuery({
|
||||
queryKey: ['admin-crash-categories'],
|
||||
queryFn: fetchCrashCategories,
|
||||
enabled: canView,
|
||||
})
|
||||
|
||||
const invalidate = () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-crash-categories'] })
|
||||
|
||||
const save = async () => {
|
||||
const values = await form.validateFields()
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (editing) {
|
||||
await updateCrashCategory(editing.id, {
|
||||
name: values.name.trim(),
|
||||
sort_order: values.sort_order ?? 0,
|
||||
status: values.status,
|
||||
})
|
||||
message.success('分类已更新')
|
||||
} else {
|
||||
await createCrashCategory({
|
||||
name: values.name.trim(),
|
||||
code: values.code?.trim(),
|
||||
sort_order: values.sort_order ?? 0,
|
||||
status: values.status,
|
||||
})
|
||||
message.success('分类已创建')
|
||||
}
|
||||
setOpen(false)
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminCrashCategory> = [
|
||||
{
|
||||
title: '分类',
|
||||
key: 'name',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{row.name}</strong>
|
||||
<span className="admin-mono">{formatText(row.code)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '商品数',
|
||||
dataIndex: 'product_count',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
width: 90,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => (
|
||||
<Tag bordered={false} color={value === 'active' ? 'green' : 'default'}>
|
||||
{value === 'active' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
setEditing(row)
|
||||
form.setFieldsValue({
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
sort_order: row.sort_order,
|
||||
status: row.status,
|
||||
})
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
disabled={!canManage}
|
||||
onClick={() =>
|
||||
modal.confirm({
|
||||
title: `确认删除分类「${row.name}」?`,
|
||||
content: '分类下若仍有商品,后端会拒绝删除。',
|
||||
okText: '删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteCrashCategory(row.id)
|
||||
message.success('分类已删除')
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 摸红" title="摸红分类" />
|
||||
<Alert type="warning" showIcon message="当前账号没有摸红商品权限" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 摸红"
|
||||
title="摸红分类"
|
||||
description="维护撞车商城的商品分类与排序。"
|
||||
meta={<Tag color="geekblue">共 {categoriesQuery.data?.length ?? 0} 个分类</Tag>}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={categoriesQuery.isFetching}
|
||||
onClick={() => void categoriesQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
setEditing(undefined)
|
||||
form.setFieldsValue({ name: '', code: '', sort_order: 0, status: 'active' })
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
新建分类
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<DataTable<AdminCrashCategory>
|
||||
columns={columns}
|
||||
dataSource={categoriesQuery.data ?? []}
|
||||
rowKey="id"
|
||||
loading={categoriesQuery.isFetching}
|
||||
emptyDescription="还没有分类"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
title={editing ? `编辑分类 · ${editing.name}` : '新建分类'}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onOk={() => void save()}
|
||||
onCancel={() => (submitting ? undefined : setOpen(false))}
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="编码" extra="创建后不可修改">
|
||||
<Input maxLength={64} disabled={Boolean(editing)} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sort_order" label="排序">
|
||||
<InputNumber min={0} style={{ width: 160 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'active', label: '启用' },
|
||||
{ value: 'disabled', label: '停用' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Alert, App, Button, Card, Form, Input, Space, Typography } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { fetchCrashConfig, updateCrashConfig } from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
|
||||
}
|
||||
|
||||
export function AdminCrashConfigPage() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<{ default_qrcode_url: string; order_copy_template: string }>()
|
||||
const [saving, setSaving] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:config'))
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:config'))
|
||||
|
||||
const configQuery = useQuery({
|
||||
queryKey: ['admin-crash-config'],
|
||||
queryFn: fetchCrashConfig,
|
||||
enabled: canView,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!configQuery.data) return
|
||||
form.setFieldsValue(configQuery.data)
|
||||
}, [configQuery.data, form])
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 摸红" title="摸红配置" />
|
||||
<Alert type="warning" showIcon message="当前账号没有摸红配置权限" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 摸红"
|
||||
title="摸红配置"
|
||||
description="维护撞车订单默认固定二维码与订单一键复制模板。"
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={configQuery.isFetching}
|
||||
onClick={() => void configQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Form form={form} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="default_qrcode_url"
|
||||
label="默认固定二维码图片 URL"
|
||||
extra="订单未指定商品二维码时使用该图片"
|
||||
>
|
||||
<Input placeholder="https://..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="order_copy_template"
|
||||
label="订单一键复制模板"
|
||||
extra="支持变量:{{order_no}} {{created_at}} {{items}} {{quantity}} {{amount}} {{buyer_name}} {{buyer_phone}}"
|
||||
>
|
||||
<Input.TextArea rows={8} />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={saving}
|
||||
disabled={!canManage}
|
||||
onClick={async () => {
|
||||
const values = await form.validateFields()
|
||||
setSaving(true)
|
||||
try {
|
||||
await updateCrashConfig(values)
|
||||
message.success('配置已保存')
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-crash-config'] })
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
<Typography.Text type="secondary">
|
||||
模板留空将回退到后端默认模板
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { useState } from 'react'
|
||||
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Alert, App, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
cancelCrashOrder,
|
||||
completeCrashOrder,
|
||||
fetchCrashOrders,
|
||||
receiveCrashOrder,
|
||||
refundCrashOrder,
|
||||
} 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 type { AdminCrashOrder } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
const statusMeta: Record<string, { label: string; color: string }> = {
|
||||
pending_payment: { label: '待支付', color: 'gold' },
|
||||
paid: { label: '已支付', color: 'blue' },
|
||||
received: { label: '已接单', color: 'cyan' },
|
||||
completed: { label: '已完成', color: 'green' },
|
||||
cancelled: { label: '已取消', color: 'default' },
|
||||
refunded: { label: '已退款', color: 'red' },
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
|
||||
}
|
||||
|
||||
export function AdminCrashOrdersPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [filters, setFilters] = useState<{ keyword: string; status?: string }>({ keyword: '' })
|
||||
const [applied, setApplied] = useState(filters)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:order_view'))
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:order_manage'))
|
||||
|
||||
const query = {
|
||||
keyword: applied.keyword.trim() || undefined,
|
||||
status: applied.status,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
const ordersQuery = useQuery({
|
||||
queryKey: ['admin-crash-orders', query],
|
||||
queryFn: () => fetchCrashOrders(query),
|
||||
enabled: canView,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin-crash-orders'] })
|
||||
|
||||
const act = async (action: () => Promise<unknown>, success: string) => {
|
||||
try {
|
||||
await action()
|
||||
message.success(success)
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminCrashOrder> = [
|
||||
{
|
||||
title: '订单',
|
||||
key: 'order',
|
||||
width: 240,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{row.order_no}</strong>
|
||||
<span className="admin-mono">{row.product_title}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '买家',
|
||||
key: 'buyer',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatText(row.buyer_nickname)}</span>
|
||||
<span className="admin-mono">{formatText(row.buyer_phone)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '数量 / 金额',
|
||||
key: 'amount',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{formatMoney(row.amount_cent)}</strong>
|
||||
<span className="admin-mono">× {row.quantity}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => {
|
||||
const meta = statusMeta[value] ?? { label: value, color: 'default' }
|
||||
return (
|
||||
<Tag bordered={false} color={meta.color}>
|
||||
{meta.label}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'remark',
|
||||
render: (_, row) => formatText(row.admin_remark || row.cancel_reason),
|
||||
},
|
||||
{
|
||||
title: '下单时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
{row.status === 'paid' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!canManage}
|
||||
onClick={() => void act(() => receiveCrashOrder(row.id), '已接单')}
|
||||
>
|
||||
接单
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'received' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!canManage}
|
||||
onClick={() => void act(() => completeCrashOrder(row.id), '订单已完成')}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'paid' || row.status === 'received' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
disabled={!canManage}
|
||||
onClick={() =>
|
||||
modal.confirm({
|
||||
title: `确认取消订单 ${row.order_no}?`,
|
||||
content: (
|
||||
<Input
|
||||
placeholder="填写取消原因"
|
||||
id={`cancel-reason-${row.id}`}
|
||||
defaultValue="后台取消"
|
||||
/>
|
||||
),
|
||||
okText: '确认取消',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '返回',
|
||||
onOk: async () => {
|
||||
const el = document.getElementById(`cancel-reason-${row.id}`) as HTMLInputElement | null
|
||||
await act(() => cancelCrashOrder(row.id, el?.value || '后台取消'), '订单已取消')
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'paid' || row.status === 'received' || row.status === 'completed' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!canManage}
|
||||
onClick={() =>
|
||||
modal.confirm({
|
||||
title: `确认退款订单 ${row.order_no}?`,
|
||||
content: '将调用支付渠道发起退款,操作不可撤销。',
|
||||
okText: '确认退款',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '返回',
|
||||
onOk: async () => act(() => refundCrashOrder(row.id, { reason: '后台退款' }), '退款已提交'),
|
||||
})
|
||||
}
|
||||
>
|
||||
退款
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 摸红" title="摸红订单" />
|
||||
<Alert type="warning" showIcon message="当前账号没有摸红订单权限" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 摸红"
|
||||
title="摸红订单"
|
||||
description="处理撞车商城订单:接单、完成、取消与退款。"
|
||||
meta={<Tag color="geekblue">共 {ordersQuery.data?.total ?? 0} 单</Tag>}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ordersQuery.isFetching}
|
||||
onClick={() => void ordersQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
loading={ordersQuery.isFetching}
|
||||
onSearch={() => {
|
||||
setPage(1)
|
||||
setApplied(filters)
|
||||
}}
|
||||
onReset={() => {
|
||||
const empty = { keyword: '' }
|
||||
setFilters(empty)
|
||||
setApplied(empty)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<FilterField label="关键字">
|
||||
<Input
|
||||
allowClear
|
||||
value={filters.keyword}
|
||||
placeholder="订单号 / 商品 / 手机号"
|
||||
onChange={event => setFilters(current => ({ ...current, keyword: event.target.value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="状态">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.status}
|
||||
placeholder="全部状态"
|
||||
options={Object.entries(statusMeta).map(([value, meta]) => ({ value, label: meta.label }))}
|
||||
onChange={(value: string | undefined) => setFilters(current => ({ ...current, status: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{ordersQuery.isError ? (
|
||||
<Alert type="error" showIcon className="admin-table-alert" message="订单加载失败" />
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminCrashOrder>
|
||||
columns={columns}
|
||||
dataSource={ordersQuery.data?.items ?? []}
|
||||
rowKey="id"
|
||||
loading={ordersQuery.isFetching}
|
||||
total={ordersQuery.data?.total ?? 0}
|
||||
page={ordersQuery.data?.page ?? page}
|
||||
pageSize={ordersQuery.data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription="还没有订单"
|
||||
/>
|
||||
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 10 }}>
|
||||
提示:退款会走支付渠道,若渠道未配置会由后端返回错误;开发环境请勿执行真实退款。
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
import { useState } from 'react'
|
||||
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Upload,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
createCrashProduct,
|
||||
deleteCrashProduct,
|
||||
fetchCrashCategories,
|
||||
fetchCrashProducts,
|
||||
updateCrashProduct,
|
||||
} 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 type { AdminCrashProduct } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
interface ProductFormValues {
|
||||
title: string
|
||||
category_id?: number
|
||||
cover_url?: string
|
||||
price_yuan: number
|
||||
original_price_yuan?: number
|
||||
unit?: string
|
||||
stock?: number
|
||||
sort_order?: number
|
||||
status: string
|
||||
qrcode_image_url?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
|
||||
}
|
||||
|
||||
export function AdminCrashProductsPage() {
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm<ProductFormValues>()
|
||||
const [filters, setFilters] = useState<{ keyword: string; status?: string; category_id?: number }>({
|
||||
keyword: '',
|
||||
})
|
||||
const [applied, setApplied] = useState(filters)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AdminCrashProduct>()
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_view'))
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('mohong:product_manage'))
|
||||
|
||||
const query = {
|
||||
keyword: applied.keyword.trim() || undefined,
|
||||
status: applied.status,
|
||||
category_id: applied.category_id,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
const productsQuery = useQuery({
|
||||
queryKey: ['admin-crash-products', query],
|
||||
queryFn: () => fetchCrashProducts(query),
|
||||
enabled: canView,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
const categoriesQuery = useQuery({
|
||||
queryKey: ['admin-crash-categories'],
|
||||
queryFn: fetchCrashCategories,
|
||||
enabled: canView,
|
||||
})
|
||||
|
||||
const invalidate = () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-crash-products'] })
|
||||
|
||||
const save = async () => {
|
||||
const values = await form.validateFields()
|
||||
const payload: Record<string, unknown> = {
|
||||
title: values.title.trim(),
|
||||
category_id: values.category_id ?? null,
|
||||
cover_url: values.cover_url ?? '',
|
||||
price_cent: Math.round(Number(values.price_yuan ?? 0) * 100),
|
||||
original_price_cent: Math.round(Number(values.original_price_yuan ?? 0) * 100),
|
||||
unit: values.unit?.trim() || '',
|
||||
stock: values.stock ?? 0,
|
||||
sort_order: values.sort_order ?? 0,
|
||||
status: values.status,
|
||||
qrcode_image_url: values.qrcode_image_url?.trim() || '',
|
||||
description: values.description?.trim() || '',
|
||||
}
|
||||
if (editing) payload.clear_category = values.category_id == null
|
||||
setSubmitting(true)
|
||||
try {
|
||||
if (editing) {
|
||||
await updateCrashProduct(editing.id, payload)
|
||||
message.success('商品已更新')
|
||||
} else {
|
||||
await createCrashProduct(payload)
|
||||
message.success('商品已创建')
|
||||
}
|
||||
setOpen(false)
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminCrashProduct> = [
|
||||
{
|
||||
title: '商品',
|
||||
key: 'product',
|
||||
width: 300,
|
||||
render: (_, row) => (
|
||||
<Space size={10} align="start">
|
||||
{row.cover_url ? <Image src={row.cover_url} width={48} height={48} /> : null}
|
||||
<div className="admin-stack">
|
||||
<strong>{row.title}</strong>
|
||||
<span className="admin-mono">{formatText(row.category_name)}</span>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{formatMoney(row.price_cent)}</strong>
|
||||
<span className="admin-mono">原价 {formatMoney(row.original_price_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
width: 90,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
width: 90,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => (
|
||||
<Tag bordered={false} color={value === 'active' ? 'green' : 'default'}>
|
||||
{value === 'active' ? '上架' : '下架'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
setEditing(row)
|
||||
form.setFieldsValue({
|
||||
title: row.title,
|
||||
category_id: row.category_id ?? undefined,
|
||||
cover_url: row.cover_url,
|
||||
price_yuan: row.price_cent / 100,
|
||||
original_price_yuan: row.original_price_cent / 100,
|
||||
unit: row.unit,
|
||||
stock: row.stock,
|
||||
sort_order: row.sort_order,
|
||||
status: row.status,
|
||||
qrcode_image_url: row.qrcode_image_url,
|
||||
description: row.description,
|
||||
})
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
disabled={!canManage}
|
||||
onClick={() =>
|
||||
modal.confirm({
|
||||
title: `确认删除商品「${row.title}」?`,
|
||||
okText: '删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteCrashProduct(row.id)
|
||||
message.success('商品已删除')
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 摸红" title="摸红商品" />
|
||||
<Alert type="warning" showIcon message="当前账号没有摸红商品权限" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 摸红"
|
||||
title="摸红商品"
|
||||
description="维护撞车商城商品、价格、库存与固定二维码。"
|
||||
meta={<Tag color="geekblue">共 {productsQuery.data?.total ?? 0} 个商品</Tag>}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={productsQuery.isFetching}
|
||||
onClick={() => void productsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
setEditing(undefined)
|
||||
form.setFieldsValue({
|
||||
title: '',
|
||||
price_yuan: 0,
|
||||
original_price_yuan: 0,
|
||||
unit: '份',
|
||||
stock: 0,
|
||||
sort_order: 0,
|
||||
status: 'active',
|
||||
})
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
新建商品
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
loading={productsQuery.isFetching}
|
||||
onSearch={() => {
|
||||
setPage(1)
|
||||
setApplied(filters)
|
||||
}}
|
||||
onReset={() => {
|
||||
const empty = { keyword: '' }
|
||||
setFilters(empty)
|
||||
setApplied(empty)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<FilterField label="关键字">
|
||||
<Input
|
||||
allowClear
|
||||
value={filters.keyword}
|
||||
placeholder="商品标题"
|
||||
onChange={event => setFilters(current => ({ ...current, keyword: event.target.value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="分类">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.category_id}
|
||||
placeholder="全部分类"
|
||||
options={(categoriesQuery.data ?? []).map(item => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
onChange={value => setFilters(current => ({ ...current, category_id: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="状态">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.status}
|
||||
placeholder="全部状态"
|
||||
options={[
|
||||
{ value: 'active', label: '上架' },
|
||||
{ value: 'disabled', label: '下架' },
|
||||
]}
|
||||
onChange={value => setFilters(current => ({ ...current, status: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
<DataTable<AdminCrashProduct>
|
||||
columns={columns}
|
||||
dataSource={productsQuery.data?.items ?? []}
|
||||
rowKey="id"
|
||||
loading={productsQuery.isFetching}
|
||||
total={productsQuery.data?.total ?? 0}
|
||||
page={productsQuery.data?.page ?? page}
|
||||
pageSize={productsQuery.data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription="还没有商品"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
title={editing ? `编辑商品 · ${editing.title}` : '新建商品'}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
confirmLoading={submitting}
|
||||
onOk={() => void save()}
|
||||
onCancel={() => (submitting ? undefined : setOpen(false))}
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false}>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Space size={12} align="start" style={{ width: '100%' }}>
|
||||
<Form.Item name="category_id" label="分类" style={{ width: 220 }}>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="选择分类"
|
||||
options={(categoriesQuery.data ?? []).map(item => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" style={{ width: 160 }}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'active', label: '上架' },
|
||||
{ value: 'disabled', label: '下架' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="unit" label="单位" style={{ width: 120 }}>
|
||||
<Input placeholder="份" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size={12} align="start" style={{ width: '100%' }}>
|
||||
<Form.Item name="price_yuan" label="售价(元)" rules={[{ required: true }]} style={{ width: 180 }}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="original_price_yuan" label="原价(元)" style={{ width: 180 }}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="stock" label="库存" style={{ width: 140 }}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sort_order" label="排序" style={{ width: 140 }}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="cover_url" label="封面图">
|
||||
<Input placeholder="图片 URL" />
|
||||
</Form.Item>
|
||||
<Space size={12} align="start">
|
||||
<Upload
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={file => {
|
||||
setUploading(true)
|
||||
void uploadAdminFile(file as File, 'listing')
|
||||
.then(uploaded => {
|
||||
form.setFieldValue('cover_url', uploaded.url)
|
||||
message.success('封面已上传')
|
||||
})
|
||||
.catch(error => message.error(errorText(error)))
|
||||
.finally(() => setUploading(false))
|
||||
return false
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={uploading}>
|
||||
上传封面
|
||||
</Button>
|
||||
</Upload>
|
||||
<Typography.Text type="secondary">上传后自动填入封面 URL</Typography.Text>
|
||||
</Space>
|
||||
<Form.Item name="qrcode_image_url" label="固定二维码图片 URL" extra="留空使用全局默认二维码">
|
||||
<Input placeholder="https://..." />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} maxLength={2000} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -129,6 +129,11 @@ import type {
|
||||
AdminInviteRebateSummary,
|
||||
AdminBackupStatus,
|
||||
AdminBackupSchedule,
|
||||
AdminCrashCategory,
|
||||
AdminCrashProduct,
|
||||
AdminCrashOrder,
|
||||
AdminCrashConfig,
|
||||
AdminPaged,
|
||||
ManualDisbursementPayload,
|
||||
OperatingExpensePage,
|
||||
OperatingExpensePayload,
|
||||
@@ -1900,3 +1905,124 @@ export async function updateAdminBackupSchedule(payload: AdminBackupSchedule) {
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchCrashCategories() {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminCrashCategory[]>>(
|
||||
'/admin/crash/categories'
|
||||
)
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function createCrashCategory(payload: {
|
||||
name: string
|
||||
code?: string
|
||||
sort_order?: number
|
||||
status?: string
|
||||
}) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminCrashCategory>>(
|
||||
'/admin/crash/categories',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateCrashCategory(
|
||||
id: number,
|
||||
payload: { name?: string; sort_order?: number; status?: string }
|
||||
) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminCrashCategory>>(
|
||||
`/admin/crash/categories/${id}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteCrashCategory(id: number) {
|
||||
await adminClient.delete(`/admin/crash/categories/${id}`)
|
||||
}
|
||||
|
||||
export async function fetchCrashProducts(query: {
|
||||
keyword?: string
|
||||
status?: string
|
||||
category_id?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminPaged<AdminCrashProduct>>>>(
|
||||
'/admin/crash/products',
|
||||
{ params: compactParams(query) }
|
||||
)
|
||||
return {
|
||||
items: data.data.items ?? [],
|
||||
total: data.data.total ?? 0,
|
||||
page: data.data.page ?? query.page ?? 1,
|
||||
page_size: data.data.page_size ?? query.page_size ?? 20,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCrashProduct(payload: Record<string, unknown>) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminCrashProduct>>(
|
||||
'/admin/crash/products',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateCrashProduct(id: number, payload: Record<string, unknown>) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminCrashProduct>>(
|
||||
`/admin/crash/products/${id}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteCrashProduct(id: number) {
|
||||
await adminClient.delete(`/admin/crash/products/${id}`)
|
||||
}
|
||||
|
||||
export async function fetchCrashOrders(query: {
|
||||
keyword?: string
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminPaged<AdminCrashOrder>>>>(
|
||||
'/admin/crash/orders',
|
||||
{ params: compactParams(query) }
|
||||
)
|
||||
return {
|
||||
items: data.data.items ?? [],
|
||||
total: data.data.total ?? 0,
|
||||
page: data.data.page ?? query.page ?? 1,
|
||||
page_size: data.data.page_size ?? query.page_size ?? 20,
|
||||
}
|
||||
}
|
||||
|
||||
export async function receiveCrashOrder(id: number, remark = '') {
|
||||
await adminClient.post(`/admin/crash/orders/${id}/receive`, { remark })
|
||||
}
|
||||
|
||||
export async function completeCrashOrder(id: number, remark = '') {
|
||||
await adminClient.post(`/admin/crash/orders/${id}/complete`, { remark })
|
||||
}
|
||||
|
||||
export async function cancelCrashOrder(id: number, reason: string) {
|
||||
await adminClient.post(`/admin/crash/orders/${id}/cancel`, { reason })
|
||||
}
|
||||
|
||||
export async function refundCrashOrder(id: number, payload: { reason?: string; amount_cent?: number }) {
|
||||
await adminClient.post(`/admin/crash/orders/${id}/refund`, payload)
|
||||
}
|
||||
|
||||
export async function fetchCrashConfig() {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminCrashConfig>>('/admin/crash/config')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateCrashConfig(payload: Partial<AdminCrashConfig>) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminCrashConfig>>(
|
||||
'/admin/crash/config',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -2060,3 +2060,67 @@ export interface AdminBackupSchedule {
|
||||
full_minute: number
|
||||
binlog_interval_minutes: number
|
||||
}
|
||||
|
||||
export interface AdminCrashCategory {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
sort_order: number
|
||||
status: string
|
||||
product_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminCrashProduct {
|
||||
id: number
|
||||
category_id?: number | null
|
||||
category_name?: string
|
||||
title: string
|
||||
cover_url: string
|
||||
image_urls: string[]
|
||||
description: string
|
||||
price_cent: number
|
||||
original_price_cent: number
|
||||
unit: string
|
||||
stock: number
|
||||
sort_order: number
|
||||
status: string
|
||||
qrcode_image_url?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminCrashOrder {
|
||||
id: number
|
||||
order_no: string
|
||||
user_id: number
|
||||
product_id: number
|
||||
quantity: number
|
||||
unit_price_cent: number
|
||||
amount_cent: number
|
||||
status: string
|
||||
product_title: string
|
||||
product_cover_url: string
|
||||
buyer_nickname?: string
|
||||
buyer_phone?: string
|
||||
admin_remark?: string
|
||||
cancel_reason?: string
|
||||
copy_text?: string
|
||||
paid_at?: string | null
|
||||
completed_at?: string | null
|
||||
cancelled_at?: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface AdminCrashConfig {
|
||||
default_qrcode_url: string
|
||||
order_copy_template: string
|
||||
}
|
||||
|
||||
export interface AdminPaged<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CustomerServiceOutlined,
|
||||
DashboardOutlined,
|
||||
FileDoneOutlined,
|
||||
GiftOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
@@ -130,6 +131,17 @@ const allNavGroups: NavGroup[] = [
|
||||
{ key: '/distribution/invitation', label: '邀请返利', permission: 'distribution:site_view' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'crash',
|
||||
label: '摸红',
|
||||
icon: <GiftOutlined />,
|
||||
children: [
|
||||
{ key: '/crash/categories', label: '分类管理', permission: 'mohong:product_view' },
|
||||
{ key: '/crash/products', label: '商品管理', permission: 'mohong:product_view' },
|
||||
{ key: '/crash/orders', label: '订单管理', permission: 'mohong:order_view' },
|
||||
{ key: '/crash/config', label: '摸红配置', permission: 'mohong:config' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'operations',
|
||||
label: '运营',
|
||||
@@ -212,6 +224,10 @@ const sectionMeta: Record<string, { title: string; description: string }> = {
|
||||
title: '推广追踪',
|
||||
description: '管理推广短链、渠道成本与注册下单转化。',
|
||||
},
|
||||
'/crash/categories': { title: '摸红分类', description: '维护撞车商城商品分类与排序。' },
|
||||
'/crash/products': { title: '摸红商品', description: '维护撞车商城商品、价格与固定二维码。' },
|
||||
'/crash/orders': { title: '摸红订单', description: '处理撞车订单的接单、完成、取消与退款。' },
|
||||
'/crash/config': { title: '摸红配置', description: '维护默认二维码与订单一键复制模板。' },
|
||||
'/notifications': { title: '通知中心', description: '查看后台系统通知并批量标记已读。' },
|
||||
'/push': { title: '推送通知', description: '维护推送渠道与通知规则。' },
|
||||
'/invitations': {
|
||||
|
||||
Reference in New Issue
Block a user