feat(pickup): 新增线下提号列表与详情页
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# Phase 2 验收报告(线下提号)
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 项目:解忧商行(hfb_jysh)
|
||||
> 范围:后台线下提号列表、创建、详情与结算动作
|
||||
> 结论:**页面与路由已落地,tsc/build 通过,只读接口回归 200;写操作未在验收中执行**
|
||||
|
||||
---
|
||||
|
||||
## 1. 命名口径
|
||||
|
||||
`pickup` 模块统一称「线下提号」,与旧站 `AdminLayout.vue` 菜单「线下提号」及 `AdminPickupView` 文案(创建提号 / 完成提号 / 取消提号 / 提号编号 / 提号状态 `picking_up`)一致。
|
||||
|
||||
本轮同时修正了 `frontend/src/features/admin/labels.ts` 中遗留的错误译法:
|
||||
|
||||
- `pickup: '捡漏订单'` → `线下提号`
|
||||
- `pickup_shop: '捡漏店铺'` → `提号店铺`
|
||||
- 审计动作 `pickup_create / pickup_cancel / pickup_complete / pickup_profit_update / pickup_offline_settlement / pickup_financial_adjustment_* / pickup_shop_*` 全部由「捡漏」改为「提号」口径。
|
||||
|
||||
## 2. 页面与路由
|
||||
|
||||
| 页面 | 路由 | 文件 |
|
||||
|---|---|---|
|
||||
| 线下提号列表 | `/console-jy8k3m/pickup` | `frontend/src/features/admin/AdminPickupsPage.tsx` |
|
||||
| 线下提号详情 | `/console-jy8k3m/pickup/:id` | `frontend/src/features/admin/AdminPickupDetailPage.tsx` |
|
||||
|
||||
权限:`order:pickup`(列表、创建、完成、取消、结算、利润调整、第三方单号、店铺管理均复用该权限,与后端路由一致)。
|
||||
|
||||
## 3. 列表页能力
|
||||
|
||||
- 筛选:关键字(提号/上架编号/账号标题/手机号)、提号状态(提号中/已完成/已取消)、账号来源(站内/外部)、线下结算状态(未结算/待结算/已结算)、操作人、创建时间区间。
|
||||
- 列表:提号编号、账号标题与上架编号、来源与店铺、下单/押金/利润金额、状态双标签、操作人与第三方单号、创建时间。
|
||||
- 创建提号弹窗:
|
||||
- 账号来源切换(全部 / 外部上传·平台代管 / 站内上传)驱动 `GET /admin/pickups/available-listings?source_type=`;
|
||||
- 远程搜索已发布且未在交易的账号;
|
||||
- 操作人、交易渠道、第三方单号、店铺(AutoComplete 支持手填)、下单金额、押金、线下利润、备注;
|
||||
- 金额按元录入并 `Math.round(yuan * 100)` 转分提交;
|
||||
- 创建成功后跳转详情页。
|
||||
- 店铺管理弹窗:`GET/POST/DELETE /admin/pickups/shops`,标签式展示与删除,新增后刷新 `shop-options` 下拉。
|
||||
|
||||
## 4. 详情页能力
|
||||
|
||||
- 基本信息:提号编号、上架编号、账号标题、区服/登录方式、号主手机、操作人、交易渠道、店铺、第三方单号(可改)、结算模式、创建/完成时间、备注。
|
||||
- 金额区:下单金额、押金、站点售价、号主价、线下利润(含原值与调整)、应结算(含原值与调整)。
|
||||
- 操作:
|
||||
- 完成提号 → `POST /admin/pickups/:id/complete`(结算金额、可选利润、备注)
|
||||
- 取消提号 → `POST /admin/pickups/:id/cancel`(必填原因)
|
||||
- 标记线下已结算 → `POST /admin/pickups/:id/offline-settlement`(仅 `completed` + `pending` 显示)
|
||||
- 修改线下利润 → `PUT /admin/pickups/:id/profit`
|
||||
- 修改第三方单号 → `PUT /admin/pickups/:id/third-party-order-no`
|
||||
- 新建财务调整 → `POST /admin/pickups/:id/financial-adjustments`(目标利润/目标结算,至少填一项)
|
||||
- 结算财务调整 → `POST /admin/pickup-financial-adjustments/:id/settle`
|
||||
- 流程事件:`GET /admin/pickups/:id/process-events` 以 Timeline 展示阶段、动作、内容、操作人与时间。
|
||||
|
||||
## 5. 菜单接入
|
||||
|
||||
- `frontend/src/layouts/AdminLayout.tsx`
|
||||
- 交易组新增「线下提号」→ `/pickup`(`order:pickup`)。
|
||||
- `sectionMeta` 补 `/pickup` 标题与描述。
|
||||
|
||||
## 6. 回归结果
|
||||
|
||||
```text
|
||||
前端 http://localhost:5289
|
||||
后端 http://127.0.0.1:18089
|
||||
Cookie /tmp/jysh-admin-cookie2.txt
|
||||
```
|
||||
|
||||
| 检查项 | 状态 |
|
||||
|---|---|
|
||||
| `GET /console-jy8k3m/pickup` | 200 |
|
||||
| `GET /console-jy8k3m/pickup/1` | 200 |
|
||||
| `GET /api/admin/pickups?page=1&page_size=2` | 200(空列表) |
|
||||
| `GET /api/admin/pickups/shops` | 200 |
|
||||
| `GET /api/admin/pickups/shop-options` | 200 |
|
||||
| `GET /api/admin/pickups/available-listings?page=1&page_size=2` | 200 |
|
||||
|
||||
`npx tsc --noEmit` 通过;`npm run build` 通过,主入口 `498.58 kB` / gzip `156.99 kB`,无超大包告警。
|
||||
|
||||
## 7. 未覆盖
|
||||
|
||||
- 创建、完成、取消、线下结算、利润调整、财务调整与店铺增删均未执行写操作(开发库无可用账号与历史提号数据)。
|
||||
- 财务调整的金额差值口径(目标金额 vs 差额)仅在类型层面对齐,未用真实数据验证。
|
||||
- 提号完成后对号主钱包的实际入账未验证。
|
||||
|
||||
## 8. 下一步(B2 剩余)
|
||||
|
||||
客服会话工作台 `/chats`、主站二维码池 `/qrcode-pool`、通知 `/notifications`、推送 `/push`、邀请 `/invitations`。
|
||||
+2
-2
@@ -413,8 +413,8 @@ hfb_jysh/
|
||||
- 用户端资金闭环。
|
||||
- 已落地:`/wallet`(余额 + 流水)、`/wallet/payment-accounts`、`/wallet/withdrawal`、移动端 `/m/wallet/withdrawal`、卖家 `/seller/earnings`、`/seller/handoffs`、`/seller/pickups`。
|
||||
- 申诉仲裁、手工订单、提号、客服群、快捷回复。
|
||||
- 已落地:客服群组(`/chat-support-groups`,权限 `chat:manage`),角色权限矩阵(`/roles`,权限 `role:manage`),手工录单(`/manual-entries`,权限 `cs_manual_entry:*`),快捷回复(`/chat-quick-replies`,权限 `chat:view` / `chat:send`)。
|
||||
- 待落地:无(手工录单与快捷回复已迁移完毕)。
|
||||
- 已落地:客服群组(`/chat-support-groups`,权限 `chat:manage`),角色权限矩阵(`/roles`,权限 `role:manage`),手工录单(`/manual-entries`,权限 `cs_manual_entry:*`),快捷回复(`/chat-quick-replies`,权限 `chat:view` / `chat:send`),线下提号(`/pickup`、`/pickup/:id`,权限 `order:pickup`,口径统一为「线下提号」)。
|
||||
- 待落地:客服会话工作台 `/chats`、主站二维码池 `/qrcode-pool`。
|
||||
- 分销站点、规则、订单、结算、二维码池、分销用户、公告。
|
||||
- 公告、邀请返利、推送、追踪链接。
|
||||
- 已落地:公告管理、邀请返利、推广追踪(`/tracking-links`,权限 `ad_tracking:manage`)。
|
||||
|
||||
@@ -236,6 +236,16 @@ const AdminPaymentsPage = lazy(() =>
|
||||
default: module.AdminPaymentsPage,
|
||||
}))
|
||||
)
|
||||
const AdminPickupsPage = lazy(() =>
|
||||
import('@/features/admin/AdminPickupsPage').then(module => ({
|
||||
default: module.AdminPickupsPage,
|
||||
}))
|
||||
)
|
||||
const AdminPickupDetailPage = lazy(() =>
|
||||
import('@/features/admin/AdminPickupDetailPage').then(module => ({
|
||||
default: module.AdminPickupDetailPage,
|
||||
}))
|
||||
)
|
||||
const AdminFinanceDetailsPage = lazy(() =>
|
||||
import('@/features/admin/AdminFinanceDetailsPage').then(module => ({
|
||||
default: module.AdminFinanceDetailsPage,
|
||||
@@ -396,6 +406,8 @@ export function AppRouter() {
|
||||
<Route path="admin-users" element={<AdminStaffAccountsPage />} />
|
||||
<Route path="manual-entries" element={<AdminManualEntriesPage />} />
|
||||
<Route path="payments" element={<AdminPaymentsPage />} />
|
||||
<Route path="pickup" element={<AdminPickupsPage />} />
|
||||
<Route path="pickup/:id" element={<AdminPickupDetailPage />} />
|
||||
<Route path="finance/details" element={<AdminFinanceDetailsPage />} />
|
||||
<Route path="wallet-ledger" element={<AdminWalletLedgerPage />} />
|
||||
<Route path="chat-quick-replies" element={<AdminQuickRepliesPage />} />
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Timeline,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { ArrowLeftOutlined, EditOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
cancelAdminPickup,
|
||||
completeAdminPickup,
|
||||
createAdminPickupFinancialAdjustment,
|
||||
fetchAdminPickup,
|
||||
fetchAdminPickupFinancialAdjustments,
|
||||
fetchAdminPickupProcessEvents,
|
||||
markAdminPickupOfflineSettlement,
|
||||
settleAdminPickupFinancialAdjustment,
|
||||
updateAdminPickupProfit,
|
||||
updateAdminPickupThirdPartyOrderNo,
|
||||
} from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import type { AdminPickupFinancialAdjustment } from '@/features/admin/types'
|
||||
import { offlineSettlementMeta, pickupSourceLabel, pickupStatusMeta } from '@/features/admin/AdminPickupsPage'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
const adjustmentColumns: TableColumnsType<AdminPickupFinancialAdjustment> = [
|
||||
{
|
||||
title: '调整',
|
||||
key: 'delta',
|
||||
width: 200,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>利润 {formatMoney(row.profit_delta_cent)}</span>
|
||||
<span className="admin-mono">结算 {formatMoney(row.settle_delta_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '原因',
|
||||
dataIndex: 'reason',
|
||||
ellipsis: true,
|
||||
render: value => formatText(value),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => (
|
||||
<Tag bordered={false} color={value === 'settled' ? 'green' : 'gold'}>
|
||||
{value === 'settled' ? '已结算' : '待结算'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
]
|
||||
|
||||
export function AdminPickupDetailPage() {
|
||||
const { id } = useParams()
|
||||
const pickupId = Number(id)
|
||||
const navigate = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [completeForm] = Form.useForm<{ settle_amount?: number; profit_amount?: number; remark?: string }>()
|
||||
const [cancelForm] = Form.useForm<{ reason: string }>()
|
||||
const [settleForm] = Form.useForm<{ remark?: string }>()
|
||||
const [profitForm] = Form.useForm<{ profit_amount?: number; reason?: string }>()
|
||||
const [thirdPartyForm] = Form.useForm<{ third_party_order_no: string }>()
|
||||
const [adjustForm] = Form.useForm<{ profit_amount?: number; settle_amount?: number; reason: string }>()
|
||||
const [completeOpen, setCompleteOpen] = useState(false)
|
||||
const [cancelOpen, setCancelOpen] = useState(false)
|
||||
const [settleOpen, setSettleOpen] = useState(false)
|
||||
const [profitOpen, setProfitOpen] = useState(false)
|
||||
const [thirdPartyOpen, setThirdPartyOpen] = useState(false)
|
||||
const [adjustOpen, setAdjustOpen] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('order:pickup'))
|
||||
|
||||
const pickupQuery = useQuery({
|
||||
queryKey: ['admin-pickup', pickupId],
|
||||
queryFn: () => fetchAdminPickup(pickupId),
|
||||
enabled: canManage && Number.isFinite(pickupId) && pickupId > 0,
|
||||
})
|
||||
const adjustmentsQuery = useQuery({
|
||||
queryKey: ['admin-pickup-adjustments', pickupId],
|
||||
queryFn: () => fetchAdminPickupFinancialAdjustments(pickupId),
|
||||
enabled: canManage && Number.isFinite(pickupId) && pickupId > 0,
|
||||
})
|
||||
const eventsQuery = useQuery({
|
||||
queryKey: ['admin-pickup-events', pickupId],
|
||||
queryFn: () => fetchAdminPickupProcessEvents(pickupId),
|
||||
enabled: canManage && Number.isFinite(pickupId) && pickupId > 0,
|
||||
})
|
||||
|
||||
const pickup = pickupQuery.data
|
||||
const invalidate = () =>
|
||||
Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup', pickupId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-adjustments', pickupId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-events', pickupId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickups'] }),
|
||||
])
|
||||
|
||||
const run = async (action: () => Promise<unknown>, success: string) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await action()
|
||||
message.success(success)
|
||||
await invalidate()
|
||||
return true
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
return false
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!canManage) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 线下提号" title="提号详情" />
|
||||
<Alert type="warning" showIcon message="当前账号没有线下提号权限(order:pickup)" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (pickupQuery.isError) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 线下提号" title="提号详情" />
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="提号详情加载失败"
|
||||
description={errorText(pickupQuery.error)}
|
||||
action={<Button onClick={() => navigate('/pickup')}>返回列表</Button>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const statusMeta = pickupStatusMeta[pickup?.status ?? ''] ?? {
|
||||
label: pickup?.status ?? '—',
|
||||
color: 'default',
|
||||
}
|
||||
const settlementMeta = offlineSettlementMeta[pickup?.offline_settlement_status ?? ''] ?? {
|
||||
label: pickup?.offline_settlement_status ?? '—',
|
||||
color: 'default',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 线下提号"
|
||||
title={pickup ? `线下提号 · ${pickup.pickup_no}` : '线下提号详情'}
|
||||
description="创建后账号锁定为提号中;完成后按结算金额进入号主钱包或线下结算。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag bordered={false} color={statusMeta.color}>
|
||||
{statusMeta.label}
|
||||
</Tag>
|
||||
<Tag bordered={false} color={settlementMeta.color}>
|
||||
{settlementMeta.label}
|
||||
</Tag>
|
||||
{pickup ? <Tag>{pickupSourceLabel(pickup.account_source)}</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/pickup')}>
|
||||
返回列表
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={pickupQuery.isFetching}
|
||||
onClick={() => void pickupQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
{pickup?.status === 'picking_up' ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
completeForm.setFieldsValue({
|
||||
settle_amount: pickup.effective_settle_amount_cent / 100,
|
||||
profit_amount: pickup.effective_profit_amount_cent / 100,
|
||||
remark: '',
|
||||
})
|
||||
setCompleteOpen(true)
|
||||
}}
|
||||
>
|
||||
完成提号
|
||||
</Button>
|
||||
<Button danger onClick={() => {
|
||||
cancelForm.setFieldsValue({ reason: '' })
|
||||
setCancelOpen(true)
|
||||
}}>
|
||||
取消提号
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{pickup?.status === 'completed' &&
|
||||
pickup.offline_settlement_status === 'pending' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
settleForm.setFieldsValue({ remark: '' })
|
||||
setSettleOpen(true)
|
||||
}}
|
||||
>
|
||||
标记线下已结算
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card title="基本信息" className="admin-table-card">
|
||||
<Descriptions column={2} size="middle" bordered>
|
||||
<Descriptions.Item label="提号编号">{formatText(pickup?.pickup_no)}</Descriptions.Item>
|
||||
<Descriptions.Item label="上架编号">{formatText(pickup?.listing_no)}</Descriptions.Item>
|
||||
<Descriptions.Item label="账号标题">{formatText(pickup?.account_title)}</Descriptions.Item>
|
||||
<Descriptions.Item label="区服 / 登录">
|
||||
{formatText(pickup?.server_region)} · {formatText(pickup?.login_platform)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="号主手机">{formatText(pickup?.owner_phone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人">{formatText(pickup?.operator)}</Descriptions.Item>
|
||||
<Descriptions.Item label="交易渠道">{formatText(pickup?.platform)}</Descriptions.Item>
|
||||
<Descriptions.Item label="店铺">{formatText(pickup?.shop_name)}</Descriptions.Item>
|
||||
<Descriptions.Item label="第三方单号">
|
||||
<Space size={6}>
|
||||
<span>{formatText(pickup?.third_party_order_no)}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
thirdPartyForm.setFieldsValue({
|
||||
third_party_order_no: pickup?.third_party_order_no ?? '',
|
||||
})
|
||||
setThirdPartyOpen(true)
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结算模式">{formatText(pickup?.settlement_mode)}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{formatDateTime(pickup?.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="完成时间">
|
||||
{pickup?.completed_at ? formatDateTime(pickup.completed_at) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注" span={2}>
|
||||
{formatText(pickup?.remark)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<div className="wallet-metrics" style={{ marginTop: 16 }}>
|
||||
<div className="wallet-metric">
|
||||
<span>下单金额</span>
|
||||
<strong>{formatMoney(pickup?.order_amount_cent ?? 0)}</strong>
|
||||
<small>押金 {formatMoney(pickup?.deposit_amount_cent ?? 0)}</small>
|
||||
</div>
|
||||
<div className="wallet-metric">
|
||||
<span>站点售价</span>
|
||||
<strong>{formatMoney(pickup?.listing_price_cent ?? 0)}</strong>
|
||||
<small>号主价 {formatMoney(pickup?.owner_price_cent ?? 0)}</small>
|
||||
</div>
|
||||
<div className="wallet-metric">
|
||||
<span>线下利润</span>
|
||||
<strong>{formatMoney(pickup?.effective_profit_amount_cent ?? 0)}</strong>
|
||||
<small>
|
||||
原 {formatMoney(pickup?.profit_amount_cent ?? 0)} · 调整{' '}
|
||||
{formatMoney(pickup?.profit_adjustment_cent ?? 0)}
|
||||
</small>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
profitForm.setFieldsValue({
|
||||
profit_amount: (pickup?.profit_amount_cent ?? 0) / 100,
|
||||
reason: '',
|
||||
})
|
||||
setProfitOpen(true)
|
||||
}}
|
||||
>
|
||||
修改利润
|
||||
</Button>
|
||||
</div>
|
||||
<div className="wallet-metric">
|
||||
<span>应结算金额</span>
|
||||
<strong>{formatMoney(pickup?.effective_settle_amount_cent ?? 0)}</strong>
|
||||
<small>
|
||||
原 {formatMoney(pickup?.settle_amount_cent ?? 0)} · 调整{' '}
|
||||
{formatMoney(pickup?.settle_adjustment_cent ?? 0)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="完成后财务调整"
|
||||
className="admin-table-card"
|
||||
style={{ marginTop: 16 }}
|
||||
extra={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
adjustForm.setFieldsValue({ reason: '' })
|
||||
setAdjustOpen(true)
|
||||
}}
|
||||
>
|
||||
新建调整
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table<AdminPickupFinancialAdjustment>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={[
|
||||
...adjustmentColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render: (_, row) =>
|
||||
row.status === 'settled' ? (
|
||||
<Typography.Text type="secondary">已处理</Typography.Text>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSubmitting(true)
|
||||
void settleAdminPickupFinancialAdjustment(row.id, '后台确认线下已处理')
|
||||
.then(async () => {
|
||||
message.success('调整已结算')
|
||||
await invalidate()
|
||||
})
|
||||
.catch(error => message.error(errorText(error)))
|
||||
.finally(() => setSubmitting(false))
|
||||
}}
|
||||
>
|
||||
确认结算
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
dataSource={adjustmentsQuery.data ?? []}
|
||||
loading={adjustmentsQuery.isFetching}
|
||||
pagination={false}
|
||||
locale={{ emptyText: '暂无财务调整' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="流程事件" className="admin-table-card" style={{ marginTop: 16 }}>
|
||||
{eventsQuery.data?.length ? (
|
||||
<Timeline
|
||||
items={eventsQuery.data.map(event => ({
|
||||
color: event.action?.includes('cancel') ? 'red' : 'green',
|
||||
children: (
|
||||
<div className="admin-stack">
|
||||
<strong>
|
||||
{formatText(event.stage)} · {formatText(event.action)}
|
||||
</strong>
|
||||
<span>{formatText(event.content || event.reason)}</span>
|
||||
<span className="admin-mono">
|
||||
{formatText(event.actor_name)} · {formatDateTime(event.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无流程事件</Typography.Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
open={completeOpen}
|
||||
title="完成提号"
|
||||
okText="确认完成"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setCompleteOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await completeForm.validateFields()
|
||||
const ok = await run(
|
||||
() =>
|
||||
completeAdminPickup(pickupId, {
|
||||
settle_amount_cent: Math.round(Number(values.settle_amount ?? 0) * 100),
|
||||
profit_amount_cent:
|
||||
values.profit_amount == null
|
||||
? undefined
|
||||
: Math.round(Number(values.profit_amount) * 100),
|
||||
complete_remark: values.remark?.trim(),
|
||||
}),
|
||||
'提号已完成'
|
||||
)
|
||||
if (ok) setCompleteOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={completeForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="settle_amount"
|
||||
label="结算金额(元)"
|
||||
rules={[{ required: true, message: '请输入结算金额' }]}
|
||||
extra="站内上传会进入号主钱包;平台代管按线下结算处理"
|
||||
>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="profit_amount" label="线下利润(元)">
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="完成备注">
|
||||
<Input.TextArea rows={2} maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={cancelOpen}
|
||||
title="取消提号"
|
||||
okText="确认取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="返回"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setCancelOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await cancelForm.validateFields()
|
||||
const ok = await run(() => cancelAdminPickup(pickupId, values.reason.trim()), '提号已取消')
|
||||
if (ok) setCancelOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={cancelForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="reason"
|
||||
label="取消原因"
|
||||
rules={[{ required: true, message: '请填写取消原因' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={settleOpen}
|
||||
title="标记线下已结算"
|
||||
okText="确认已结算"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setSettleOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await settleForm.validateFields()
|
||||
const ok = await run(
|
||||
() => markAdminPickupOfflineSettlement(pickupId, values.remark?.trim() ?? ''),
|
||||
'已标记线下结算'
|
||||
)
|
||||
if (ok) setSettleOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={settleForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item name="remark" label="结算备注">
|
||||
<Input.TextArea rows={3} maxLength={255} showCount placeholder="打款凭证/备注" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={profitOpen}
|
||||
title="修改线下利润"
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setProfitOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await profitForm.validateFields()
|
||||
const ok = await run(
|
||||
() =>
|
||||
updateAdminPickupProfit(pickupId, {
|
||||
profit_amount_cent: Math.round(Number(values.profit_amount ?? 0) * 100),
|
||||
reason: values.reason?.trim(),
|
||||
}),
|
||||
'利润已更新'
|
||||
)
|
||||
if (ok) setProfitOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={profitForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="profit_amount"
|
||||
label="利润金额(元)"
|
||||
rules={[{ required: true, message: '请输入利润金额' }]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="reason" label="调整原因">
|
||||
<Input placeholder="可选" maxLength={255} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={thirdPartyOpen}
|
||||
title="修改第三方单号"
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setThirdPartyOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await thirdPartyForm.validateFields()
|
||||
const ok = await run(
|
||||
() => updateAdminPickupThirdPartyOrderNo(pickupId, values.third_party_order_no.trim()),
|
||||
'第三方单号已更新'
|
||||
)
|
||||
if (ok) setThirdPartyOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={thirdPartyForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="third_party_order_no"
|
||||
label="第三方单号"
|
||||
rules={[{ required: true, message: '请输入第三方单号' }, { max: 128 }]}
|
||||
>
|
||||
<Input placeholder="淘宝/拼多多/外部订单号" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={adjustOpen}
|
||||
title="新建完成后财务调整"
|
||||
okText="创建调整"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => (submitting ? undefined : setAdjustOpen(false))}
|
||||
onOk={async () => {
|
||||
const values = await adjustForm.validateFields()
|
||||
const profit =
|
||||
values.profit_amount == null ? undefined : Math.round(Number(values.profit_amount) * 100)
|
||||
const settle =
|
||||
values.settle_amount == null ? undefined : Math.round(Number(values.settle_amount) * 100)
|
||||
if (profit == null && settle == null) {
|
||||
message.warning('至少填写利润或结算金额')
|
||||
return
|
||||
}
|
||||
const ok = await run(
|
||||
() =>
|
||||
createAdminPickupFinancialAdjustment(pickupId, {
|
||||
profit_amount_cent: profit,
|
||||
settle_amount_cent: settle,
|
||||
reason: values.reason.trim(),
|
||||
}),
|
||||
'财务调整已创建'
|
||||
)
|
||||
if (ok) setAdjustOpen(false)
|
||||
}}
|
||||
>
|
||||
<Form form={adjustForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item name="profit_amount" label="目标利润(元)" extra="填写后按差额生成调整流水">
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="settle_amount" label="目标结算(元)">
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reason"
|
||||
label="调整原因"
|
||||
rules={[{ required: true, message: '请填写调整原因' }]}
|
||||
>
|
||||
<Input.TextArea rows={2} maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
AutoComplete,
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { type Dayjs } from 'dayjs'
|
||||
|
||||
import {
|
||||
createAdminPickup,
|
||||
createAdminPickupShop,
|
||||
deleteAdminPickupShop,
|
||||
fetchAdminPickupAvailableListings,
|
||||
fetchAdminPickupShops,
|
||||
fetchAdminPickupShopOptions,
|
||||
fetchAdminPickups,
|
||||
} 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 { AdminPickup, AdminPickupCreatePayload, AdminPickupQuery } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
interface Filters {
|
||||
keyword: string
|
||||
status?: string
|
||||
account_source?: string
|
||||
offline_settlement_status?: string
|
||||
operator: string
|
||||
range: [Dayjs, Dayjs] | null
|
||||
}
|
||||
|
||||
interface CreateFormValues {
|
||||
source_type?: string
|
||||
listing_id: number
|
||||
operator?: string
|
||||
platform?: string
|
||||
third_party_order_no?: string
|
||||
shop_name?: string
|
||||
order_amount?: number
|
||||
deposit_amount?: number
|
||||
profit_amount?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const emptyFilters: Filters = {
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
account_source: undefined,
|
||||
offline_settlement_status: undefined,
|
||||
operator: '',
|
||||
range: null,
|
||||
}
|
||||
|
||||
export const pickupStatusMeta: Record<string, { label: string; color: string }> = {
|
||||
picking_up: { label: '提号中', color: 'gold' },
|
||||
completed: { label: '已完成', color: 'green' },
|
||||
cancelled: { label: '已取消', color: 'default' },
|
||||
}
|
||||
|
||||
export const offlineSettlementMeta: Record<string, { label: string; color: string }> = {
|
||||
none: { label: '未结算', color: 'default' },
|
||||
pending: { label: '待结算', color: 'gold' },
|
||||
settled: { label: '已结算', color: 'green' },
|
||||
}
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
internal: '站内上传',
|
||||
external: '外部上传',
|
||||
}
|
||||
|
||||
export function pickupSourceLabel(value?: string | null) {
|
||||
if (!value) return '未知来源'
|
||||
return sourceLabels[value] ?? value
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
export function AdminPickupsPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message, modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [createForm] = Form.useForm<CreateFormValues>()
|
||||
const [shopForm] = Form.useForm<{ name: string; platform?: string }>()
|
||||
const [filters, setFilters] = useState<Filters>(emptyFilters)
|
||||
const [applied, setApplied] = useState<Filters>(emptyFilters)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [shopOpen, setShopOpen] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [sourceType, setSourceType] = useState('')
|
||||
const [listingKeyword, setListingKeyword] = useState('')
|
||||
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canManage = Boolean(permissions?.includes('*') || permissions?.includes('order:pickup'))
|
||||
|
||||
const query: AdminPickupQuery = useMemo(
|
||||
() => ({
|
||||
keyword: applied.keyword.trim() || undefined,
|
||||
status: applied.status,
|
||||
account_source: applied.account_source,
|
||||
offline_settlement_status: applied.offline_settlement_status,
|
||||
operator: applied.operator.trim() || undefined,
|
||||
start_time: applied.range?.[0]?.format('YYYY-MM-DD 00:00:00'),
|
||||
end_time: applied.range?.[1]?.format('YYYY-MM-DD 23:59:59'),
|
||||
page,
|
||||
page_size: pageSize,
|
||||
}),
|
||||
[applied, page, pageSize]
|
||||
)
|
||||
|
||||
const pickupsQuery = useQuery({
|
||||
queryKey: ['admin-pickups', query],
|
||||
queryFn: () => fetchAdminPickups(query),
|
||||
enabled: canManage,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const listingsQuery = useQuery({
|
||||
queryKey: ['admin-pickup-available-listings', sourceType, listingKeyword],
|
||||
queryFn: () =>
|
||||
fetchAdminPickupAvailableListings({
|
||||
keyword: listingKeyword || undefined,
|
||||
source_type: sourceType || undefined,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
}),
|
||||
enabled: canManage && createOpen,
|
||||
})
|
||||
|
||||
const shopOptionsQuery = useQuery({
|
||||
queryKey: ['admin-pickup-shop-options'],
|
||||
queryFn: () => fetchAdminPickupShopOptions(),
|
||||
enabled: canManage && createOpen,
|
||||
})
|
||||
|
||||
const shopsQuery = useQuery({
|
||||
queryKey: ['admin-pickup-shops'],
|
||||
queryFn: () => fetchAdminPickupShops(),
|
||||
enabled: canManage && shopOpen,
|
||||
})
|
||||
|
||||
const invalidate = () =>
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickups'] })
|
||||
|
||||
const openCreate = () => {
|
||||
setSourceType('')
|
||||
setListingKeyword('')
|
||||
createForm.setFieldsValue({
|
||||
listing_id: undefined,
|
||||
operator: '',
|
||||
platform: '',
|
||||
third_party_order_no: '',
|
||||
shop_name: undefined,
|
||||
order_amount: 0,
|
||||
deposit_amount: 0,
|
||||
profit_amount: 0,
|
||||
remark: '',
|
||||
})
|
||||
setCreateOpen(true)
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
const values = await createForm.validateFields()
|
||||
const payload: AdminPickupCreatePayload = {
|
||||
listing_id: values.listing_id,
|
||||
operator: values.operator?.trim(),
|
||||
platform: values.platform?.trim(),
|
||||
third_party_order_no: values.third_party_order_no?.trim(),
|
||||
shop_name: values.shop_name?.trim(),
|
||||
order_amount_cent: Math.round(Number(values.order_amount ?? 0) * 100),
|
||||
deposit_amount_cent: Math.round(Number(values.deposit_amount ?? 0) * 100),
|
||||
profit_amount_cent: Math.round(Number(values.profit_amount ?? 0) * 100),
|
||||
remark: values.remark?.trim(),
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const created = await createAdminPickup(payload)
|
||||
message.success('线下提号已创建')
|
||||
setCreateOpen(false)
|
||||
await invalidate()
|
||||
if (created?.id) navigate(`/pickup/${created.id}`)
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const addShop = async () => {
|
||||
const values = await shopForm.validateFields()
|
||||
try {
|
||||
await createAdminPickupShop({ name: values.name.trim(), platform: values.platform?.trim() })
|
||||
message.success('店铺已新增')
|
||||
shopForm.setFieldsValue({ name: '', platform: '' })
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-shops'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-shop-options'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
}
|
||||
|
||||
const removeShop = (id: number, name: string) => {
|
||||
modal.confirm({
|
||||
title: `确认删除提号店铺「${name}」?`,
|
||||
content: '历史提号记录保留原店铺名,仅从可选列表中移除。',
|
||||
okText: '删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAdminPickupShop(id)
|
||||
message.success('店铺已删除')
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-shops'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-pickup-shop-options'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminPickup> = [
|
||||
{
|
||||
title: '提号编号',
|
||||
key: 'pickup',
|
||||
width: 250,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<button
|
||||
type="button"
|
||||
className="admin-order-link"
|
||||
onClick={() => navigate(`/pickup/${row.id}`)}
|
||||
>
|
||||
{row.pickup_no}
|
||||
</button>
|
||||
<strong>{row.account_title}</strong>
|
||||
<span className="admin-mono">{formatText(row.listing_no)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '来源 / 店铺',
|
||||
key: 'source',
|
||||
width: 180,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<Tag bordered={false} color={row.account_source === 'external' ? 'purple' : 'blue'}>
|
||||
{pickupSourceLabel(row.account_source)}
|
||||
</Tag>
|
||||
<span>{formatText(row.shop_name)}</span>
|
||||
<span className="admin-mono">{formatText(row.platform)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
key: 'amount',
|
||||
width: 170,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>下单 {formatMoney(row.order_amount_cent)}</span>
|
||||
<span className="admin-mono">押金 {formatMoney(row.deposit_amount_cent)}</span>
|
||||
<span className="admin-mono">利润 {formatMoney(row.effective_profit_amount_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 170,
|
||||
render: (_, row) => {
|
||||
const status = pickupStatusMeta[row.status] ?? { label: row.status, color: 'default' }
|
||||
const settlement =
|
||||
offlineSettlementMeta[row.offline_settlement_status] ?? {
|
||||
label: row.offline_settlement_status || '—',
|
||||
color: 'default',
|
||||
}
|
||||
return (
|
||||
<Space size={4} wrap>
|
||||
<Tag bordered={false} color={status.color}>
|
||||
{status.label}
|
||||
</Tag>
|
||||
<Tag bordered={false} color={settlement.color}>
|
||||
{settlement.label}
|
||||
</Tag>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
key: 'operator',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatText(row.operator)}</span>
|
||||
<span className="admin-mono">{formatText(row.third_party_order_no)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Button type="link" size="small" onClick={() => navigate(`/pickup/${row.id}`)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (!canManage) {
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader eyebrow="JIEYOU OPS · 线下提号" title="线下提号" />
|
||||
<Alert type="warning" showIcon message="当前账号没有线下提号权限(order:pickup)" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 线下提号"
|
||||
title="线下提号"
|
||||
description="管理员线下提号,站内上传结算到钱包,平台代管通过线下结算确认打款。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {pickupsQuery.data?.total ?? 0} 单</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<SettingOutlined />} onClick={() => setShopOpen(true)}>
|
||||
店铺管理
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={pickupsQuery.isFetching}
|
||||
onClick={() => void pickupsQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
创建提号
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
loading={pickupsQuery.isFetching}
|
||||
onSearch={() => {
|
||||
setPage(1)
|
||||
setApplied(filters)
|
||||
}}
|
||||
onReset={() => {
|
||||
setFilters(emptyFilters)
|
||||
setApplied(emptyFilters)
|
||||
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={[
|
||||
{ value: 'picking_up', label: '提号中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
onChange={value => setFilters(current => ({ ...current, status: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="账号来源">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.account_source}
|
||||
placeholder="全部来源"
|
||||
options={[
|
||||
{ value: 'internal', label: '站内上传' },
|
||||
{ value: 'external', label: '外部上传' },
|
||||
]}
|
||||
onChange={value => setFilters(current => ({ ...current, account_source: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="结算状态">
|
||||
<Select
|
||||
allowClear
|
||||
value={filters.offline_settlement_status}
|
||||
placeholder="全部状态"
|
||||
options={[
|
||||
{ value: 'none', label: '未结算' },
|
||||
{ value: 'pending', label: '待结算' },
|
||||
{ value: 'settled', label: '已结算' },
|
||||
]}
|
||||
onChange={value =>
|
||||
setFilters(current => ({ ...current, offline_settlement_status: value }))
|
||||
}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="操作人">
|
||||
<Input
|
||||
allowClear
|
||||
value={filters.operator}
|
||||
placeholder="经办人"
|
||||
onChange={event =>
|
||||
setFilters(current => ({ ...current, operator: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="创建时间" width={280}>
|
||||
<DatePicker.RangePicker
|
||||
style={{ width: '100%' }}
|
||||
value={filters.range}
|
||||
allowEmpty={[true, true]}
|
||||
onChange={value =>
|
||||
setFilters(current => ({
|
||||
...current,
|
||||
range: value && value[0] && value[1] ? [value[0], value[1]] : null,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{pickupsQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="提号列表加载失败"
|
||||
description={errorText(pickupsQuery.error)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminPickup>
|
||||
columns={columns}
|
||||
dataSource={pickupsQuery.data?.items ?? []}
|
||||
rowKey="id"
|
||||
loading={pickupsQuery.isFetching}
|
||||
total={pickupsQuery.data?.total ?? 0}
|
||||
page={pickupsQuery.data?.page ?? page}
|
||||
pageSize={pickupsQuery.data?.page_size ?? pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription="还没有线下提号记录"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={createOpen}
|
||||
title="创建线下提号"
|
||||
okText="创建提号"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
confirmLoading={submitting}
|
||||
onOk={() => void submitCreate()}
|
||||
onCancel={() => (submitting ? undefined : setCreateOpen(false))}
|
||||
maskClosable={!submitting}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item label="账号来源">
|
||||
<Radio.Group
|
||||
value={sourceType}
|
||||
onChange={event => setSourceType(event.target.value)}
|
||||
optionType="button"
|
||||
options={[
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'external', label: '外部上传 · 平台代管' },
|
||||
{ value: 'internal', label: '站内上传' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="listing_id"
|
||||
label="选择账号"
|
||||
rules={[{ required: true, message: '请选择要提号的账号' }]}
|
||||
extra="仅显示已发布、已审核、未在交易中的账号"
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
filterOption={false}
|
||||
loading={listingsQuery.isFetching}
|
||||
placeholder="输入上架编号或账号标题搜索"
|
||||
onSearch={value => setListingKeyword(value)}
|
||||
options={(listingsQuery.data?.items ?? []).map(item => ({
|
||||
value: item.id,
|
||||
label: `${item.listing_no} · ${item.account_title} · ${formatMoney(item.listing_price_cent)}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space size={12} style={{ width: '100%' }} align="start">
|
||||
<Form.Item name="operator" label="操作人" style={{ width: 240 }}>
|
||||
<Input placeholder="经办人/客服姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="platform" label="交易渠道" style={{ width: 240 }}>
|
||||
<Input placeholder="淘宝 / 微信 / QQ" />
|
||||
</Form.Item>
|
||||
<Form.Item name="third_party_order_no" label="第三方单号" style={{ width: 240 }}>
|
||||
<Input placeholder="外部订单号" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="shop_name" label="店铺名称" extra="可直接输入新店铺名称">
|
||||
<AutoComplete
|
||||
allowClear
|
||||
placeholder="选择或输入店铺"
|
||||
options={(shopOptionsQuery.data ?? []).map(name => ({ value: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space size={12} style={{ width: '100%' }} align="start">
|
||||
<Form.Item name="order_amount" label="下单金额(元)">
|
||||
<InputNumber min={0} precision={2} style={{ width: 200 }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="deposit_amount" label="押金金额(元)">
|
||||
<InputNumber min={0} precision={2} style={{ width: 200 }} prefix="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="profit_amount" label="线下利润(元)">
|
||||
<InputNumber min={0} precision={2} style={{ width: 200 }} prefix="¥" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={255} showCount placeholder="线下交易说明(可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message='创建后账号会被锁定为「提号中」,卖家收到通知;不会立即结算,需手动完成提号。'
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={shopOpen}
|
||||
title="提号店铺管理"
|
||||
onCancel={() => setShopOpen(false)}
|
||||
footer={<Button onClick={() => setShopOpen(false)}>关闭</Button>}
|
||||
width={620}
|
||||
>
|
||||
<Form form={shopForm} layout="inline" style={{ marginBottom: 12, gap: 8 }}>
|
||||
<Form.Item name="name" rules={[{ required: true, message: '请输入店铺名' }]}>
|
||||
<Input placeholder="店铺名称" style={{ width: 200 }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="platform">
|
||||
<Input placeholder="平台(可选)" style={{ width: 140 }} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => void addShop()}>
|
||||
新增
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Space size={6} wrap>
|
||||
{(shopsQuery.data ?? []).map(shop => (
|
||||
<Tag
|
||||
key={shop.id}
|
||||
closable
|
||||
color="geekblue"
|
||||
onClose={event => {
|
||||
event.preventDefault()
|
||||
removeShop(shop.id, shop.name)
|
||||
}}
|
||||
>
|
||||
{shop.name}
|
||||
{shop.platform ? ` · ${shop.platform}` : ''}
|
||||
</Tag>
|
||||
))}
|
||||
{shopsQuery.data?.length === 0 ? (
|
||||
<Typography.Text type="secondary">还没有配置店铺</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -102,6 +102,14 @@ import type {
|
||||
AdminPaymentQuery,
|
||||
AdminWalletLedgerPage,
|
||||
AdminWalletLedgerQuery,
|
||||
AdminPickup,
|
||||
AdminPickupAvailableListing,
|
||||
AdminPickupCreatePayload,
|
||||
AdminPickupFinancialAdjustment,
|
||||
AdminPickupPage,
|
||||
AdminPickupProcessEvent,
|
||||
AdminPickupQuery,
|
||||
AdminPickupShop,
|
||||
ManualDisbursementPayload,
|
||||
OperatingExpensePage,
|
||||
OperatingExpensePayload,
|
||||
@@ -1396,3 +1404,154 @@ export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {})
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminPickups(query: AdminPickupQuery = {}) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminPickupPage>>>('/admin/pickups', {
|
||||
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 fetchAdminPickup(id: number | string) {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminPickup>>(`/admin/pickups/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function createAdminPickup(payload: AdminPickupCreatePayload) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickup>>('/admin/pickups', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function completeAdminPickup(
|
||||
id: number | string,
|
||||
payload: { settle_amount_cent: number; profit_amount_cent?: number; complete_remark?: string }
|
||||
) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickup>>(
|
||||
`/admin/pickups/${id}/complete`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function cancelAdminPickup(id: number | string, reason: string) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickup>>(
|
||||
`/admin/pickups/${id}/cancel`,
|
||||
{ reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function markAdminPickupOfflineSettlement(id: number | string, remark: string) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickup>>(
|
||||
`/admin/pickups/${id}/offline-settlement`,
|
||||
{ remark }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateAdminPickupProfit(
|
||||
id: number | string,
|
||||
payload: { profit_amount_cent: number; reason?: string }
|
||||
) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminPickup>>(
|
||||
`/admin/pickups/${id}/profit`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateAdminPickupThirdPartyOrderNo(id: number | string, value: string) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminPickup>>(
|
||||
`/admin/pickups/${id}/third-party-order-no`,
|
||||
{ third_party_order_no: value }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminPickupShops(platform = '') {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminPickupShop[]>>('/admin/pickups/shops', {
|
||||
params: platform ? { platform } : undefined,
|
||||
})
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function createAdminPickupShop(payload: {
|
||||
name: string
|
||||
platform?: string
|
||||
sort_order?: number
|
||||
remark?: string
|
||||
}) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickupShop>>(
|
||||
'/admin/pickups/shops',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteAdminPickupShop(id: number) {
|
||||
await adminClient.delete(`/admin/pickups/shops/${id}`)
|
||||
}
|
||||
|
||||
export async function fetchAdminPickupShopOptions(platform = '') {
|
||||
const { data } = await adminClient.get<ApiResponse<string[]>>('/admin/pickups/shop-options', {
|
||||
params: platform ? { platform } : undefined,
|
||||
})
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function fetchAdminPickupAvailableListings(query: {
|
||||
keyword?: string
|
||||
source_type?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
const { data } = await adminClient.get<
|
||||
ApiResponse<{ items?: AdminPickupAvailableListing[]; total?: number }>
|
||||
>('/admin/pickups/available-listings', { params: compactParams(query) })
|
||||
return {
|
||||
items: data.data.items ?? [],
|
||||
total: data.data.total ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAdminPickupProcessEvents(id: number | string) {
|
||||
const { data } = await adminClient.get<ApiResponse<{ items: AdminPickupProcessEvent[] }>>(
|
||||
`/admin/pickups/${id}/process-events`
|
||||
)
|
||||
return data.data.items ?? []
|
||||
}
|
||||
|
||||
export async function fetchAdminPickupFinancialAdjustments(id: number | string) {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminPickupFinancialAdjustment[]>>(
|
||||
`/admin/pickups/${id}/financial-adjustments`
|
||||
)
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function createAdminPickupFinancialAdjustment(
|
||||
id: number | string,
|
||||
payload: { profit_amount_cent?: number; settle_amount_cent?: number; reason: string }
|
||||
) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickupFinancialAdjustment>>(
|
||||
`/admin/pickups/${id}/financial-adjustments`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function settleAdminPickupFinancialAdjustment(
|
||||
adjustmentId: number,
|
||||
remark: string
|
||||
) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPickupFinancialAdjustment>>(
|
||||
`/admin/pickup-financial-adjustments/${adjustmentId}/settle`,
|
||||
{ remark }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -235,8 +235,8 @@ export const auditBizTypeMap: Record<string, string> = {
|
||||
payment_config: '支付配置',
|
||||
manual_disbursement: '人工打款',
|
||||
operating_expense: '运营支出',
|
||||
pickup: '捡漏订单',
|
||||
pickup_shop: '捡漏店铺',
|
||||
pickup: '线下提号',
|
||||
pickup_shop: '提号店铺',
|
||||
distribution: '分销',
|
||||
announcement: '公告',
|
||||
}
|
||||
@@ -284,16 +284,16 @@ export const auditActionMap: Record<string, string> = {
|
||||
'manual_disbursement_void': '作废人工打款',
|
||||
'operating_expense_create': '登记运营支出',
|
||||
'operating_expense_void': '作废运营支出',
|
||||
'pickup_create': '创建捡漏订单',
|
||||
'pickup_cancel': '取消捡漏订单',
|
||||
'pickup_complete': '完成捡漏订单',
|
||||
'pickup_profit_update': '更新捡漏利润',
|
||||
'pickup_offline_settlement': '捡漏线下结算',
|
||||
'pickup_financial_adjustment_create': '新建捡漏财务调整',
|
||||
'pickup_financial_adjustment_settle': '结算捡漏财务调整',
|
||||
'pickup_shop_create': '新增捡漏店铺',
|
||||
'pickup_shop_update': '更新捡漏店铺',
|
||||
'pickup_shop_delete': '删除捡漏店铺',
|
||||
'pickup_create': '创建线下提号',
|
||||
'pickup_cancel': '取消线下提号',
|
||||
'pickup_complete': '完成线下提号',
|
||||
'pickup_profit_update': '更新提号利润',
|
||||
'pickup_offline_settlement': '提号线下结算',
|
||||
'pickup_financial_adjustment_create': '新建提号财务调整',
|
||||
'pickup_financial_adjustment_settle': '结算提号财务调整',
|
||||
'pickup_shop_create': '新增提号店铺',
|
||||
'pickup_shop_update': '更新提号店铺',
|
||||
'pickup_shop_delete': '删除提号店铺',
|
||||
'pickup_shop_map_historical': '关联历史捡漏店铺',
|
||||
'pickup_shop_unmap_historical': '解除历史捡漏店铺',
|
||||
'pickup_third_party_order_no_update': '更新第三方单号',
|
||||
|
||||
@@ -609,6 +609,146 @@ export interface AdminWalletLedgerQuery {
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminPickup {
|
||||
id: number
|
||||
pickup_no: string
|
||||
listing_id: number
|
||||
listing_no: string
|
||||
account_title: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
owner_id: number
|
||||
owner_phone: string
|
||||
admin_id: number
|
||||
operator: string
|
||||
platform: string
|
||||
third_party_order_no: string
|
||||
shop_name: string
|
||||
raw_shop_name: string
|
||||
account_source: string
|
||||
source_channel: string
|
||||
settlement_mode: string
|
||||
listing_price_cent: number
|
||||
order_amount_cent: number
|
||||
deposit_amount_cent: number
|
||||
owner_price_cent: number
|
||||
website_profit_cent: number
|
||||
profit_amount_cent: number
|
||||
profit_adjustment_cent: number
|
||||
effective_profit_amount_cent: number
|
||||
seller_ratio: number
|
||||
buyer_ratio: number
|
||||
settle_amount_cent: number
|
||||
settle_adjustment_cent: number
|
||||
effective_settle_amount_cent: number
|
||||
status: string
|
||||
offline_settlement_status: string
|
||||
offline_settlement_remark: string
|
||||
offline_settled_by?: number | null
|
||||
offline_settled_at?: string | null
|
||||
remark: string
|
||||
complete_remark: string
|
||||
created_at: string
|
||||
completed_at?: string | null
|
||||
cancelled_at?: string | null
|
||||
}
|
||||
|
||||
export interface AdminPickupPage {
|
||||
items: AdminPickup[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminPickupQuery {
|
||||
keyword?: string
|
||||
status?: string
|
||||
shop_name?: string
|
||||
account_source?: string
|
||||
offline_settlement_status?: string
|
||||
operator?: string
|
||||
third_party_order_no?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminPickupCreatePayload {
|
||||
listing_id: number
|
||||
operator?: string
|
||||
platform?: string
|
||||
third_party_order_no?: string
|
||||
shop_name?: string
|
||||
order_amount_cent?: number
|
||||
deposit_amount_cent?: number
|
||||
profit_amount_cent?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface AdminPickupShop {
|
||||
id: number
|
||||
name: string
|
||||
platform: string
|
||||
status: string
|
||||
sort_order: number
|
||||
remark: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminPickupAvailableListing {
|
||||
id: number
|
||||
listing_no: string
|
||||
account_title: string
|
||||
server_region: string
|
||||
login_platform: string
|
||||
owner_id: number
|
||||
owner_phone: string
|
||||
listing_price_cent: number
|
||||
owner_price_cent: number
|
||||
owner_total_price_cent: number
|
||||
website_profit_cent: number
|
||||
seller_ratio: number
|
||||
buyer_ratio: number
|
||||
account_source: string
|
||||
is_external_upload: boolean
|
||||
source_channel: string
|
||||
}
|
||||
|
||||
export interface AdminPickupProcessEvent {
|
||||
id: number
|
||||
business_type: string
|
||||
business_id: number
|
||||
stage: string
|
||||
action: string
|
||||
actor_type: string
|
||||
actor_id: number
|
||||
actor_name: string
|
||||
target_type: string
|
||||
target_id?: number | null
|
||||
target_name: string
|
||||
content: string
|
||||
reason: string
|
||||
attachment_urls?: string[]
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface AdminPickupFinancialAdjustment {
|
||||
id: number
|
||||
pickup_id: number
|
||||
profit_delta_cent: number
|
||||
settle_delta_cent: number
|
||||
settlement_mode: string
|
||||
status: string
|
||||
reason: string
|
||||
created_by: number
|
||||
settled_by?: number | null
|
||||
settled_at?: string | null
|
||||
settlement_remark: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface AdminAnnouncementQuery {
|
||||
category?: string
|
||||
status?: string
|
||||
|
||||
@@ -61,6 +61,7 @@ const allNavGroups: NavGroup[] = [
|
||||
children: [
|
||||
{ key: '/orders', label: '订单管理', permission: 'order:list' },
|
||||
{ key: '/orders/refund-review', label: '退款审核', permission: 'order:close' },
|
||||
{ key: '/pickup', label: '线下提号', permission: 'order:pickup' },
|
||||
{ key: '/disputes', label: '申诉仲裁', permission: 'dispute:view' },
|
||||
],
|
||||
},
|
||||
@@ -162,6 +163,7 @@ const allNavGroups: NavGroup[] = [
|
||||
const sectionMeta: Record<string, { title: string; description: string }> = {
|
||||
'/orders': { title: '订单管理', description: '查看全量订单、交接记录与结账状态。' },
|
||||
'/orders/refund-review': { title: '退款审核', description: '审核退款申请并核对资金影响。' },
|
||||
'/pickup': { title: '线下提号', description: '管理员线下提号,站内结算或线下打款确认。' },
|
||||
'/disputes': { title: '申诉仲裁', description: '处理订单争议、证据与仲裁结果。' },
|
||||
'/listings': { title: '商品管理', description: '查看商品、账号资产与上下架状态。' },
|
||||
'/listings/review': { title: '商品审核', description: '审核待上架商品与账号截图。' },
|
||||
|
||||
Reference in New Issue
Block a user