Files
hfb_jysh/frontend/src/features/admin/AdminCrashOrdersPage.tsx
T

294 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}