重构: 手续费改为百分比/固定二选一,清理旧 Skin/Order/ShipLog 演示链路
手续费: - 新增 fee_type 字段(rate/fixed),百分比与固定金额二选一,不再叠加 - calculateServiceFee 按 fee_type 分支计算 - 商户创建/更新校验 fee_type,订单落库快照 fee_type - 前端表单改为下拉选择手续费类型,动态显示对应输入框 - 测试拆为 TestFeeRate + TestFeeFixed 清理旧链路: - 删除旧 Skin/Order/ShipLog 模型及 OrderService/SkinService - Dashboard 迁移到 FulfillmentService - 上游 /api/open/v1 改为基于 FulfillmentOrder 实现,接口契约不变 - 推送留痕改用 AuditLog,不再建 ShipLog 表 - 删除前端 Skins/Orders/ShipLogs/Distributors 页面及路由、菜单、API、类型 - 新增迁移 003: 添加 fee_type 列并 DROP 旧表
This commit is contained in:
@@ -6,15 +6,10 @@ import MainLayout from './layouts/MainLayout'
|
||||
import Login from './pages/Login'
|
||||
import Register from './pages/Register'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import Skins from './pages/Skins'
|
||||
import Orders from './pages/Orders'
|
||||
import Distributors from './pages/Distributors'
|
||||
import ShipLogs from './pages/ShipLogs'
|
||||
import OpenApiDocs from './pages/OpenApiDocs'
|
||||
import MerchantCenter from './pages/MerchantCenter'
|
||||
import PlatformMerchants from './pages/PlatformMerchants'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
function PrivateRoute({ children }: { children: ReactNode }) {
|
||||
const { token } = useAuth()
|
||||
if (!token) return <Navigate to="/login" replace />
|
||||
@@ -41,17 +36,7 @@ function AppRoutes() {
|
||||
}
|
||||
>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="skins" element={<Skins />} />
|
||||
<Route path="orders" element={<Orders />} />
|
||||
<Route path="merchant-center" element={<MerchantCenter />} />
|
||||
<Route
|
||||
path="distributors"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<Distributors />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="platform-merchants"
|
||||
element={
|
||||
@@ -60,14 +45,6 @@ function AppRoutes() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="ship-logs"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<ShipLogs />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="open-api"
|
||||
element={
|
||||
|
||||
+10
-36
@@ -10,10 +10,7 @@ import type {
|
||||
Merchant,
|
||||
MerchantMember,
|
||||
MerchantProduct,
|
||||
Order,
|
||||
PageResult,
|
||||
ShipLog,
|
||||
Skin,
|
||||
User,
|
||||
WalletAccount,
|
||||
WalletLedgerEntry,
|
||||
@@ -39,33 +36,6 @@ export const dashboardApi = {
|
||||
request.get('/dashboard').then((r) => r.data.data as DashboardStats),
|
||||
}
|
||||
|
||||
export const skinApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/skins', { params }).then((r) => r.data.data as PageResult<Skin>),
|
||||
get: (id: number) =>
|
||||
request.get(`/skins/${id}`).then((r) => r.data.data as Skin),
|
||||
create: (data: Partial<Skin>) =>
|
||||
request.post('/skins', data).then((r) => r.data.data as Skin),
|
||||
update: (id: number, data: Partial<Skin>) =>
|
||||
request.put(`/skins/${id}`, data).then((r) => r.data.data),
|
||||
remove: (id: number) =>
|
||||
request.delete(`/skins/${id}`).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
export const orderApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/orders', { params }).then((r) => r.data.data as PageResult<Order>),
|
||||
create: (data: {
|
||||
skin_id: number
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
status?: string
|
||||
distributor_id?: number
|
||||
}) => request.post('/orders', data).then((r) => r.data.data as Order),
|
||||
updateStatus: (id: number, status: string) =>
|
||||
request.patch(`/orders/${id}/status`, { status }).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/users', { params }).then((r) => r.data.data as PageResult<User>),
|
||||
@@ -75,11 +45,6 @@ export const userApi = {
|
||||
request.patch(`/users/${id}/status`, { status }).then((r) => r.data.data),
|
||||
}
|
||||
|
||||
export const shipLogApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get('/ship-logs', { params }).then((r) => r.data.data as PageResult<ShipLog>),
|
||||
}
|
||||
|
||||
export const merchantApi = {
|
||||
current: () =>
|
||||
request.get('/merchant').then((r) => r.data.data as { merchant: Merchant; role: MerchantMember['role'] }),
|
||||
@@ -132,8 +97,17 @@ export const platformApi = {
|
||||
name: string
|
||||
contact_name?: string
|
||||
contact_info?: string
|
||||
owner_user_id: number
|
||||
owner_user_id?: number
|
||||
owner_username?: string
|
||||
owner_password?: string
|
||||
owner_nickname?: string
|
||||
features?: string
|
||||
fee_type?: 'rate' | 'fixed'
|
||||
fee_rate_bp?: number
|
||||
fee_fixed_amount?: number
|
||||
}) => request.post('/platform/merchants', data).then((r) => r.data.data as Merchant),
|
||||
updateMerchant: (id: number, data: Partial<Merchant>) =>
|
||||
request.patch(`/platform/merchants/${id}`, data).then((r) => r.data.data),
|
||||
addMember: (
|
||||
merchantId: number,
|
||||
data: { user_id: number; role: MerchantMember['role']; is_default?: boolean },
|
||||
|
||||
@@ -11,14 +11,10 @@ import {
|
||||
} from 'antd'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
SkinOutlined,
|
||||
ShoppingOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SendOutlined,
|
||||
ApiOutlined,
|
||||
ShopOutlined,
|
||||
} from '@ant-design/icons'
|
||||
@@ -39,15 +35,11 @@ export default function MainLayout() {
|
||||
const menuItems: MenuProps['items'] = useMemo(() => {
|
||||
const items: MenuProps['items'] = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: '数据概览' },
|
||||
{ key: '/skins', icon: <SkinOutlined />, label: '皮肤商品' },
|
||||
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单管理' },
|
||||
{ key: '/merchant-center', icon: <ShopOutlined />, label: '商户中心' },
|
||||
]
|
||||
if (isAdmin) {
|
||||
items.push(
|
||||
{ key: '/distributors', icon: <TeamOutlined />, label: '分销商' },
|
||||
{ key: '/platform-merchants', icon: <ShopOutlined />, label: '商户管理' },
|
||||
{ key: '/ship-logs', icon: <SendOutlined />, label: '发货记录' },
|
||||
{ key: '/open-api', icon: <ApiOutlined />, label: '开放接口' },
|
||||
)
|
||||
}
|
||||
@@ -81,7 +73,7 @@ export default function MainLayout() {
|
||||
letterSpacing: 1,
|
||||
}}
|
||||
>
|
||||
{collapsed ? '皮肤' : '皮肤分销系统'}
|
||||
{collapsed ? '供货' : '皮肤供货平台'}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
@@ -112,7 +104,7 @@ export default function MainLayout() {
|
||||
<Avatar size="small" icon={<UserOutlined />} />
|
||||
<Typography.Text>
|
||||
{user?.nickname || user?.username}
|
||||
{isAdmin ? '(管理员)' : '(分销商)'}
|
||||
{isAdmin ? '(平台管理员)' : '(商户账号)'}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Col, Row, Statistic, Typography, Spin, message } from 'antd'
|
||||
import {
|
||||
SkinOutlined,
|
||||
TeamOutlined,
|
||||
ShopOutlined,
|
||||
ShoppingOutlined,
|
||||
DollarOutlined,
|
||||
PercentageOutlined,
|
||||
@@ -39,12 +38,12 @@ export default function Dashboard() {
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card>
|
||||
<Statistic title="皮肤商品" value={stats?.skin_count ?? 0} prefix={<SkinOutlined />} />
|
||||
<Statistic title="商户商品" value={stats?.product_count ?? 0} prefix={<ShoppingOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card>
|
||||
<Statistic title="分销商" value={stats?.distributor_count ?? 0} prefix={<TeamOutlined />} />
|
||||
<Statistic title="平台商户" value={stats?.merchant_count ?? 0} prefix={<ShopOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
@@ -66,8 +65,8 @@ export default function Dashboard() {
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="累计佣金"
|
||||
value={stats?.total_commission ?? 0}
|
||||
title="平台手续费"
|
||||
value={stats?.total_fees ?? 0}
|
||||
precision={2}
|
||||
prefix={<PercentageOutlined />}
|
||||
suffix="元"
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { userApi } from '../api'
|
||||
import type { User } from '../types'
|
||||
|
||||
export default function Distributors() {
|
||||
const [list, setList] = useState<User[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(10)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await userApi.list({
|
||||
page,
|
||||
size,
|
||||
keyword,
|
||||
role: 'distributor',
|
||||
})
|
||||
setList(data.list || [])
|
||||
setTotal(data.total)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, size, keyword])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const onSubmit = async () => {
|
||||
const values = await form.validateFields()
|
||||
try {
|
||||
await userApi.create({ ...values, role: 'distributor' })
|
||||
message.success('创建成功')
|
||||
setOpen(false)
|
||||
form.resetFields()
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleStatus = async (record: User, checked: boolean) => {
|
||||
try {
|
||||
await userApi.updateStatus(record.id, checked ? 1 : 0)
|
||||
message.success('状态已更新')
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<User> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{ title: '昵称', dataIndex: 'nickname' },
|
||||
{
|
||||
title: '邀请码',
|
||||
dataIndex: 'invite_code',
|
||||
render: (v: string) => <Tag color="blue">{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
render: (v: number, record) => (
|
||||
<Switch
|
||||
checkedChildren="启用"
|
||||
unCheckedChildren="禁用"
|
||||
checked={v === 1}
|
||||
onChange={(checked) => toggleStatus(record, checked)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
分销商管理
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Input.Search
|
||||
placeholder="搜索用户名/昵称"
|
||||
allowClear
|
||||
onSearch={(v) => {
|
||||
setPage(1)
|
||||
setKeyword(v)
|
||||
}}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
form.resetFields()
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
新增分销商
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, s) => {
|
||||
setPage(p)
|
||||
setSize(s)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal title="新增分销商" open={open} onOk={onSubmit} onCancel={() => setOpen(false)} destroyOnClose>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="username" label="用户名" rules={[{ required: true, min: 3 }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="nickname" label="昵称">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="密码" rules={[{ required: true, min: 6 }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export default function Login() {
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
游戏皮肤分销系统
|
||||
游戏皮肤供货平台
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">登录后台管理</Typography.Text>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ export default function Login() {
|
||||
</Form>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Text type="secondary">
|
||||
还没有账号? <Link to="/register">注册分销商</Link>
|
||||
还没有账号? <Link to="/register">注册商户账号</Link>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0, textAlign: 'center' }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
@@ -103,11 +103,14 @@ export default function MerchantCenter() {
|
||||
|
||||
const canManage = merchantRole === 'owner' || merchantRole === 'operator'
|
||||
const canFinance = merchantRole === 'owner' || merchantRole === 'finance'
|
||||
const enabledFeatures = useMemo(() => new Set(featuresToList(merchant?.features)), [merchant?.features])
|
||||
const hasFeature = useCallback((feature: string) => !merchant || enabledFeatures.has(feature), [enabledFeatures, merchant])
|
||||
|
||||
const loadCurrent = useCallback(async () => {
|
||||
const data = await merchantApi.current()
|
||||
setMerchant(data.merchant)
|
||||
setMerchantRole(data.role)
|
||||
return data
|
||||
}, [])
|
||||
|
||||
const loadProducts = useCallback(async (page = products.page, size = products.size) => {
|
||||
@@ -120,37 +123,86 @@ export default function MerchantCenter() {
|
||||
setOrders(data)
|
||||
}, [orders.page, orders.size])
|
||||
|
||||
const loadWallet = useCallback(async (page = ledger.page, size = ledger.size) => {
|
||||
const [walletData, ledgerData] = await Promise.all([
|
||||
merchantApi.wallet(),
|
||||
merchantApi.ledger({ page, size }),
|
||||
])
|
||||
const loadWallet = useCallback(async (
|
||||
page = ledger.page,
|
||||
size = ledger.size,
|
||||
role = merchantRole,
|
||||
) => {
|
||||
const walletData = await merchantApi.wallet()
|
||||
setWallet(walletData)
|
||||
setLedger(ledgerData)
|
||||
}, [ledger.page, ledger.size])
|
||||
if (role === 'owner' || role === 'finance') {
|
||||
const ledgerData = await merchantApi.ledger({ page, size })
|
||||
setLedger(ledgerData)
|
||||
} else {
|
||||
setLedger({ list: [], total: 0, page, size })
|
||||
}
|
||||
}, [ledger.page, ledger.size, merchantRole])
|
||||
|
||||
const loadIntegrations = useCallback(async () => {
|
||||
const [clientData, callbackData, memberData] = await Promise.all([
|
||||
merchantApi.apiClients(),
|
||||
merchantApi.callbacks(),
|
||||
merchantApi.members(),
|
||||
])
|
||||
const loadAPIClients = useCallback(async (role = merchantRole) => {
|
||||
if (role !== 'owner' && role !== 'operator') {
|
||||
setApiClients([])
|
||||
return
|
||||
}
|
||||
const clientData = await merchantApi.apiClients()
|
||||
setApiClients(clientData || [])
|
||||
}, [merchantRole])
|
||||
|
||||
const loadCallbacks = useCallback(async (role = merchantRole) => {
|
||||
if (role !== 'owner' && role !== 'operator') {
|
||||
setCallbacks([])
|
||||
return
|
||||
}
|
||||
const callbackData = await merchantApi.callbacks()
|
||||
setCallbacks(callbackData || [])
|
||||
}, [merchantRole])
|
||||
|
||||
const loadMembers = useCallback(async (role = merchantRole) => {
|
||||
if (role !== 'owner' && role !== 'operator') {
|
||||
setMembers([])
|
||||
return
|
||||
}
|
||||
const memberData = await merchantApi.members()
|
||||
setMembers(memberData || [])
|
||||
}, [])
|
||||
}, [merchantRole])
|
||||
|
||||
const loadActiveTab = useCallback(async (role = merchantRole, tab = activeTab) => {
|
||||
switch (tab) {
|
||||
case 'products':
|
||||
await loadProducts()
|
||||
return
|
||||
case 'orders':
|
||||
await loadOrders()
|
||||
return
|
||||
case 'wallet':
|
||||
await loadWallet(undefined, undefined, role)
|
||||
return
|
||||
case 'api':
|
||||
await loadAPIClients(role)
|
||||
return
|
||||
case 'callbacks':
|
||||
await loadCallbacks(role)
|
||||
return
|
||||
case 'members':
|
||||
await loadMembers(role)
|
||||
return
|
||||
}
|
||||
}, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantRole])
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await loadCurrent()
|
||||
await Promise.all([loadProducts(), loadOrders(), loadWallet(), loadIntegrations()])
|
||||
const current = await loadCurrent()
|
||||
const nextTab = resolveEnabledTab(activeTab, current.merchant.features)
|
||||
if (nextTab !== activeTab) {
|
||||
setActiveTab(nextTab)
|
||||
}
|
||||
await loadActiveTab(current.role, nextTab)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [loadCurrent, loadProducts, loadOrders, loadWallet, loadIntegrations])
|
||||
}, [activeTab, loadActiveTab, loadCurrent])
|
||||
|
||||
useEffect(() => {
|
||||
loadAll()
|
||||
@@ -232,7 +284,7 @@ export default function MerchantCenter() {
|
||||
setApiCredential(credential)
|
||||
setApiClientOpen(false)
|
||||
message.success('API 客户端已创建')
|
||||
loadIntegrations()
|
||||
loadAPIClients()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '创建失败')
|
||||
}
|
||||
@@ -249,7 +301,7 @@ export default function MerchantCenter() {
|
||||
setCallbackCredential(credential)
|
||||
setCallbackOpen(false)
|
||||
message.success('回调已创建')
|
||||
loadIntegrations()
|
||||
loadCallbacks()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '创建失败')
|
||||
}
|
||||
@@ -265,7 +317,7 @@ export default function MerchantCenter() {
|
||||
})
|
||||
setMemberOpen(false)
|
||||
message.success('成员已添加')
|
||||
loadIntegrations()
|
||||
loadMembers()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '添加失败')
|
||||
}
|
||||
@@ -275,7 +327,7 @@ export default function MerchantCenter() {
|
||||
try {
|
||||
await merchantApi.updateApiClientStatus(record.id, record.status === 'active' ? 'disabled' : 'active')
|
||||
message.success('状态已更新')
|
||||
loadIntegrations()
|
||||
loadAPIClients()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
@@ -285,7 +337,7 @@ export default function MerchantCenter() {
|
||||
try {
|
||||
await merchantApi.updateCallbackStatus(record.id, record.status === 'active' ? 'disabled' : 'active')
|
||||
message.success('状态已更新')
|
||||
loadIntegrations()
|
||||
loadCallbacks()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
@@ -314,7 +366,9 @@ export default function MerchantCenter() {
|
||||
{ title: '商户单号', dataIndex: 'client_order_no', width: 160, ellipsis: true },
|
||||
{ title: 'SKU', dataIndex: 'product_sku', width: 150, render: (v) => <Typography.Text code>{v}</Typography.Text> },
|
||||
{ title: '商品', dataIndex: 'product_name', ellipsis: true },
|
||||
{ title: '金额', dataIndex: 'amount', width: 100, render: money },
|
||||
{ title: '基础金额', dataIndex: 'base_amount', width: 100, render: money },
|
||||
{ title: '手续费', dataIndex: 'service_fee_amount', width: 100, render: money },
|
||||
{ title: '扣款合计', dataIndex: 'amount', width: 100, render: money },
|
||||
{ title: '支付', dataIndex: 'payment_status', width: 90, render: paymentStatusTag },
|
||||
{ title: '履约', dataIndex: 'fulfillment_status', width: 100, render: fulfillmentStatusTag },
|
||||
{ title: '上游单号', dataIndex: 'provider_order_no', width: 140, ellipsis: true, render: (v) => v || '-' },
|
||||
@@ -399,6 +453,7 @@ export default function MerchantCenter() {
|
||||
{
|
||||
key: 'products',
|
||||
label: '商品',
|
||||
disabled: !hasFeature('products'),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
@@ -419,6 +474,7 @@ export default function MerchantCenter() {
|
||||
{
|
||||
key: 'orders',
|
||||
label: '履约订单',
|
||||
disabled: !hasFeature('orders'),
|
||||
children: (
|
||||
<Table
|
||||
rowKey="id"
|
||||
@@ -434,6 +490,7 @@ export default function MerchantCenter() {
|
||||
{
|
||||
key: 'wallet',
|
||||
label: '钱包',
|
||||
disabled: !hasFeature('wallet'),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
@@ -455,20 +512,25 @@ export default function MerchantCenter() {
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={ledgerColumns}
|
||||
dataSource={ledger.list}
|
||||
tableLayout="fixed"
|
||||
pagination={pageConfig(ledger, loadWallet)}
|
||||
/>
|
||||
{canFinance ? (
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={ledgerColumns}
|
||||
dataSource={ledger.list}
|
||||
tableLayout="fixed"
|
||||
pagination={pageConfig(ledger, loadWallet)}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text type="secondary">当前角色可查看余额,钱包流水仅财务或负责人可见。</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
label: 'API 客户端',
|
||||
disabled: !hasFeature('api'),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
@@ -486,6 +548,7 @@ export default function MerchantCenter() {
|
||||
{
|
||||
key: 'callbacks',
|
||||
label: '回调',
|
||||
disabled: !hasFeature('callbacks'),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
@@ -679,6 +742,31 @@ function centsToYuan(value?: number | null) {
|
||||
return Number(((value || 0) / 100).toFixed(2))
|
||||
}
|
||||
|
||||
function featuresToList(features?: string) {
|
||||
const list = (features || '')
|
||||
.split(/[,\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
return list.length > 0 ? list : ['products', 'orders', 'wallet', 'api', 'callbacks']
|
||||
}
|
||||
|
||||
function resolveEnabledTab(current: string, features?: string) {
|
||||
const enabled = new Set(featuresToList(features))
|
||||
const tabFeatures: Record<string, string | null> = {
|
||||
products: 'products',
|
||||
orders: 'orders',
|
||||
wallet: 'wallet',
|
||||
api: 'api',
|
||||
callbacks: 'callbacks',
|
||||
members: null,
|
||||
}
|
||||
const feature = tabFeatures[current]
|
||||
if (feature === null || enabled.has(feature)) {
|
||||
return current
|
||||
}
|
||||
return ['products', 'orders', 'wallet', 'api', 'callbacks'].find((key) => enabled.has(key)) || 'members'
|
||||
}
|
||||
|
||||
function money(value?: number | null) {
|
||||
return `¥${centsToYuan(value).toFixed(2)}`
|
||||
}
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { orderApi, skinApi } from '../api'
|
||||
import { useAuth } from '../store/auth'
|
||||
import type { Order, Skin } from '../types'
|
||||
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'orange', text: '待支付' },
|
||||
paid: { color: 'blue', text: '已支付' },
|
||||
delivering: { color: 'cyan', text: '发货中' },
|
||||
delivered: { color: 'green', text: '已交付' },
|
||||
ship_failed: { color: 'red', text: '发货失败' },
|
||||
cancelled: { color: 'default', text: '已取消' },
|
||||
}
|
||||
|
||||
export default function Orders() {
|
||||
const { isAdmin } = useAuth()
|
||||
const [list, setList] = useState<Order[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(10)
|
||||
const [status, setStatus] = useState<string | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [skins, setSkins] = useState<Skin[]>([])
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [createdOrder, setCreatedOrder] = useState<Order | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await orderApi.list({ page, size, status })
|
||||
setList(data.list || [])
|
||||
setTotal(data.total)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, size, status])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const openCreate = async () => {
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
buyer_name: '测试买家',
|
||||
status: 'paid',
|
||||
remark: '联调测试订单',
|
||||
})
|
||||
setCreatedOrder(null)
|
||||
setCreateOpen(true)
|
||||
try {
|
||||
const data = await skinApi.list({ page: 1, size: 100, status: 1 })
|
||||
setSkins(data.list || [])
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载商品失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onCreate = async () => {
|
||||
const values = await form.validateFields()
|
||||
setCreating(true)
|
||||
try {
|
||||
const order = await orderApi.create({
|
||||
skin_id: values.skin_id,
|
||||
buyer_name: values.buyer_name,
|
||||
remark: values.remark,
|
||||
// 管理员可指定任意初始状态,方便造异常单
|
||||
status: isAdmin ? values.status : undefined,
|
||||
})
|
||||
setCreatedOrder(order)
|
||||
message.success(`测试订单已创建:${order.order_no}`)
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '创建失败')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copyText = async (text: string, tip = '已复制') => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
message.success(tip)
|
||||
} catch {
|
||||
message.error('复制失败,请手动选择')
|
||||
}
|
||||
}
|
||||
|
||||
const changeStatus = async (id: number, next: string) => {
|
||||
try {
|
||||
await orderApi.updateStatus(id, next)
|
||||
message.success('状态已更新')
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Order> = [
|
||||
{
|
||||
title: '店铺订单号',
|
||||
dataIndex: 'order_no',
|
||||
width: 210,
|
||||
ellipsis: true,
|
||||
render: (v: string) => (
|
||||
<Space size={4}>
|
||||
<Typography.Text copyable={{ text: v }} style={{ maxWidth: 160 }} ellipsis>
|
||||
{v}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '皮肤',
|
||||
dataIndex: ['skin', 'name'],
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.skin?.name || `#${r.skin_id}`,
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, r) =>
|
||||
r.skin?.sku ? (
|
||||
<Typography.Text code copyable={{ text: r.skin.sku }}>
|
||||
{r.skin.sku}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分销商',
|
||||
dataIndex: ['distributor', 'nickname'],
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, r) => r.distributor?.nickname || r.distributor?.username || `#${r.distributor_id}`,
|
||||
},
|
||||
{ title: '买家', dataIndex: 'buyer_name', width: 90, ellipsis: true },
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
width: 90,
|
||||
render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (v: string) => {
|
||||
const s = statusMap[v] || { color: 'default', text: v }
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上游单号',
|
||||
dataIndex: 'provider_order_no',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
render: (v?: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size={0}>
|
||||
{record.status === 'pending' && (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'paid')}>
|
||||
标记已付
|
||||
</Button>
|
||||
<Button type="link" size="small" danger onClick={() => changeStatus(record.id, 'cancelled')}>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(record.status === 'paid' ||
|
||||
record.status === 'ship_failed' ||
|
||||
record.status === 'delivering') && (
|
||||
<Button type="link" size="small" onClick={() => changeStatus(record.id, 'delivered')}>
|
||||
标记交付
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
订单管理
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
店铺订单号 order_no 给源头查询发货;已支付订单 can_ship=true
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="订单状态"
|
||||
style={{ width: 140 }}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setPage(1)
|
||||
setStatus(v)
|
||||
}}
|
||||
options={[
|
||||
{ value: 'pending', label: '待支付' },
|
||||
{ value: 'paid', label: '已支付' },
|
||||
{ value: 'delivering', label: '发货中' },
|
||||
{ value: 'delivered', label: '已交付' },
|
||||
{ value: 'ship_failed', label: '发货失败' },
|
||||
{ value: 'cancelled', label: '已取消' },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
创建测试订单
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, s) => {
|
||||
setPage(p)
|
||||
setSize(s)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="创建测试订单(联调用)"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
footer={
|
||||
createdOrder
|
||||
? [
|
||||
<Button key="close" onClick={() => setCreateOpen(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
<Button
|
||||
key="again"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setCreatedOrder(null)
|
||||
form.setFieldsValue({ status: 'paid' })
|
||||
}}
|
||||
>
|
||||
再下一单
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button key="cancel" onClick={() => setCreateOpen(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="ok" type="primary" loading={creating} onClick={onCreate}>
|
||||
创建
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
destroyOnClose
|
||||
width={560}
|
||||
>
|
||||
{createdOrder ? (
|
||||
<div>
|
||||
<Typography.Paragraph>
|
||||
订单已创建。把下面的 <Typography.Text strong>店铺订单号</Typography.Text>{' '}
|
||||
发给源头,或用开放接口查询:
|
||||
</Typography.Paragraph>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<div
|
||||
style={{
|
||||
padding: 16,
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 8, color: '#666' }}>店铺订单号 order_no</div>
|
||||
<Space>
|
||||
<Typography.Title level={4} style={{ margin: 0 }} copyable>
|
||||
{createdOrder.order_no}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyText(createdOrder.order_no, '订单号已复制')}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div>
|
||||
<div>商品:{createdOrder.skin?.name || `#${createdOrder.skin_id}`}</div>
|
||||
<div>
|
||||
SKU:
|
||||
<Typography.Text code copyable>
|
||||
{createdOrder.skin?.sku || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
状态:
|
||||
<Tag color={statusMap[createdOrder.status]?.color}>
|
||||
{statusMap[createdOrder.status]?.text || createdOrder.status}
|
||||
</Tag>
|
||||
{(createdOrder.status === 'paid' || createdOrder.status === 'ship_failed') && (
|
||||
<Typography.Text type="success">(可发货 can_ship=true)</Typography.Text>
|
||||
)}
|
||||
{createdOrder.status !== 'paid' && createdOrder.status !== 'ship_failed' && (
|
||||
<Typography.Text type="secondary">(不可发货 can_ship=false)</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div>买家:{createdOrder.buyer_name}</div>
|
||||
</div>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: 12 }}>
|
||||
源头查询:GET /api/open/v1/orders/{createdOrder.order_no}
|
||||
<br />
|
||||
需带 X-Api-Key / X-Timestamp / X-Nonce / X-Sign
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
</div>
|
||||
) : (
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 8 }}>
|
||||
<Form.Item
|
||||
name="skin_id"
|
||||
label="商品皮肤"
|
||||
rules={[{ required: true, message: '请选择商品' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择要发货的皮肤"
|
||||
options={skins.map((s) => ({
|
||||
value: s.id,
|
||||
label: `${s.name}(${s.sku})`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="buyer_name" label="买家名称">
|
||||
<Input placeholder="测试买家" />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="联调说明" />
|
||||
</Form.Item>
|
||||
{isAdmin && (
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="初始状态(联调造单)"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
extra="源头 can_ship=true 的只有:已支付、发货失败"
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'pending', label: '待支付 pending — can_ship=false' },
|
||||
{ value: 'paid', label: '已支付 paid — can_ship=true(正常可发)' },
|
||||
{ value: 'delivering', label: '发货中 delivering — can_ship=false' },
|
||||
{ value: 'delivered', label: '已交付 delivered — can_ship=false' },
|
||||
{ value: 'ship_failed', label: '发货失败 ship_failed — can_ship=true(可重试)' },
|
||||
{ value: 'cancelled', label: '已取消 cancelled — can_ship=false' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0 }}>
|
||||
创建后生成店铺订单号(如 O20260720…),复制给源头用开放接口查询即可测各状态。
|
||||
</Typography.Paragraph>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -26,14 +26,24 @@ const memberRoleOptions = [
|
||||
{ value: 'viewer', label: '只读' },
|
||||
]
|
||||
|
||||
const featureOptions = [
|
||||
{ value: 'products', label: '商品' },
|
||||
{ value: 'orders', label: '订单' },
|
||||
{ value: 'wallet', label: '钱包' },
|
||||
{ value: 'api', label: 'API' },
|
||||
{ value: 'callbacks', label: '回调' },
|
||||
]
|
||||
|
||||
export default function PlatformMerchants() {
|
||||
const navigate = useNavigate()
|
||||
const [data, setData] = useState<PageResult<Merchant>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [memberOpen, setMemberOpen] = useState(false)
|
||||
const [selectedMerchant, setSelectedMerchant] = useState<Merchant | null>(null)
|
||||
const [createForm] = Form.useForm()
|
||||
const [settingsForm] = Form.useForm()
|
||||
const [memberForm] = Form.useForm()
|
||||
|
||||
const load = useCallback(async (page = data.page, size = data.size) => {
|
||||
@@ -55,7 +65,13 @@ export default function PlatformMerchants() {
|
||||
const submitCreate = async () => {
|
||||
const values = await createForm.validateFields()
|
||||
try {
|
||||
await platformApi.createMerchant(values)
|
||||
await platformApi.createMerchant({
|
||||
...values,
|
||||
features: featureListToText(values.features),
|
||||
fee_type: values.fee_type,
|
||||
fee_rate_bp: Number(values.fee_rate_bp || 0),
|
||||
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan),
|
||||
})
|
||||
message.success('商户已创建')
|
||||
setCreateOpen(false)
|
||||
createForm.resetFields()
|
||||
@@ -65,6 +81,28 @@ export default function PlatformMerchants() {
|
||||
}
|
||||
}
|
||||
|
||||
const submitSettings = async () => {
|
||||
if (!selectedMerchant) return
|
||||
const values = await settingsForm.validateFields()
|
||||
try {
|
||||
await platformApi.updateMerchant(selectedMerchant.id, {
|
||||
name: values.name,
|
||||
status: values.status,
|
||||
contact_name: values.contact_name,
|
||||
contact_info: values.contact_info,
|
||||
features: featureListToText(values.features),
|
||||
fee_type: values.fee_type,
|
||||
fee_rate_bp: Number(values.fee_rate_bp || 0),
|
||||
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan),
|
||||
})
|
||||
message.success('商户设置已更新')
|
||||
setSettingsOpen(false)
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitMember = async () => {
|
||||
if (!selectedMerchant) return
|
||||
const values = await memberForm.validateFields()
|
||||
@@ -87,6 +125,27 @@ export default function PlatformMerchants() {
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contact_name', width: 120, render: (v) => v || '-' },
|
||||
{ title: '联系方式', dataIndex: 'contact_info', width: 160, render: (v) => v || '-' },
|
||||
{
|
||||
title: '手续费',
|
||||
key: 'fee',
|
||||
width: 160,
|
||||
render: (_, r) =>
|
||||
r.fee_type === 'fixed'
|
||||
? `固定 ¥${centsToYuan(r.fee_fixed_amount).toFixed(2)}/单`
|
||||
: `${(r.fee_rate_bp / 100).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
title: '功能',
|
||||
dataIndex: 'features',
|
||||
width: 220,
|
||||
render: (v) => (
|
||||
<Space size={4} wrap>
|
||||
{featuresToList(v).map((feature) => (
|
||||
<Tag key={feature}>{featureText(feature)}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (v) => v === 'active' ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag> },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 160, render: (v) => dayjs(v).format('YYYY-MM-DD HH:mm') },
|
||||
{
|
||||
@@ -106,6 +165,22 @@ export default function PlatformMerchants() {
|
||||
>
|
||||
进入
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedMerchant(record)
|
||||
settingsForm.setFieldsValue({
|
||||
...record,
|
||||
features: featuresToList(record.features),
|
||||
fee_type: record.fee_type || 'rate',
|
||||
fee_fixed_yuan: centsToYuan(record.fee_fixed_amount),
|
||||
})
|
||||
setSettingsOpen(true)
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@@ -142,6 +217,12 @@ export default function PlatformMerchants() {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
createForm.resetFields()
|
||||
createForm.setFieldsValue({
|
||||
features: featureOptions.map((item) => item.value),
|
||||
fee_type: 'rate',
|
||||
fee_rate_bp: 0,
|
||||
fee_fixed_yuan: 0,
|
||||
})
|
||||
setCreateOpen(true)
|
||||
}}
|
||||
>
|
||||
@@ -166,7 +247,7 @@ export default function PlatformMerchants() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal title="新增商户" open={createOpen} onOk={submitCreate} onCancel={() => setCreateOpen(false)} destroyOnClose>
|
||||
<Modal title="新增商户" open={createOpen} onOk={submitCreate} onCancel={() => setCreateOpen(false)} destroyOnClose width={680}>
|
||||
<Form form={createForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="code" label="商户编码" rules={[{ required: true }]}>
|
||||
<Input placeholder="lower-case-code" />
|
||||
@@ -174,15 +255,92 @@ export default function PlatformMerchants() {
|
||||
<Form.Item name="name" label="商户名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="owner_user_id" label="负责人用户 ID" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="owner_username" label="负责人用户名" rules={[{ required: true }]} style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="owner_password" label="负责人密码" rules={[{ required: true, min: 6 }]} style={{ width: 300 }}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="owner_nickname" label="负责人昵称" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="contact_info" label="联系方式">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="features" label="开通功能" rules={[{ required: true }]}>
|
||||
<Select mode="multiple" options={featureOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fee_type" label="手续费类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
|
||||
{ value: 'fixed', label: '按固定金额(每单固定手续费)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('fee_type') === 'fixed' ? (
|
||||
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="fee_rate_bp" label="手续费比例 BP(1BP=0.01%,如 250=2.5%)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10000} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={selectedMerchant ? `商户设置:${selectedMerchant.name}` : '商户设置'} open={settingsOpen} onOk={submitSettings} onCancel={() => setSettingsOpen(false)} destroyOnClose width={680}>
|
||||
<Form form={settingsForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="name" label="商户名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="status" label="状态" style={{ width: 300 }}>
|
||||
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '禁用' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="contact_info" label="联系方式">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="features" label="开通功能" rules={[{ required: true }]}>
|
||||
<Select mode="multiple" options={featureOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fee_type" label="手续费类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
|
||||
{ value: 'fixed', label: '按固定金额(每单固定手续费)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('fee_type') === 'fixed' ? (
|
||||
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="fee_rate_bp" label="手续费比例 BP(1BP=0.01%,如 250=2.5%)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10000} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -208,3 +366,26 @@ export default function PlatformMerchants() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function featuresToList(features?: string) {
|
||||
return (features || '')
|
||||
.split(/[,\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function featureListToText(features?: string[]) {
|
||||
return (features || []).join(',')
|
||||
}
|
||||
|
||||
function featureText(feature: string) {
|
||||
return featureOptions.find((item) => item.value === feature)?.label || feature
|
||||
}
|
||||
|
||||
function yuanToCents(value?: number | null) {
|
||||
return Math.round(Number(value || 0) * 100)
|
||||
}
|
||||
|
||||
function centsToYuan(value?: number | null) {
|
||||
return Number(((value || 0) / 100).toFixed(2))
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ export default function Register() {
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
||||
注册分销商
|
||||
注册商户账号
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">创建分销账号</Typography.Text>
|
||||
<Typography.Text type="secondary">创建商户员工账号</Typography.Text>
|
||||
</div>
|
||||
<Form layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item name="username" rules={[{ required: true, min: 3, message: '用户名至少3位' }]}>
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import dayjs from 'dayjs'
|
||||
import { shipLogApi } from '../api'
|
||||
import type { ShipLog } from '../types'
|
||||
|
||||
const shipStatusMap: Record<string, { color: string; text: string }> = {
|
||||
success: { color: 'green', text: '成功' },
|
||||
failed: { color: 'red', text: '失败' },
|
||||
processing: { color: 'cyan', text: '发货中' },
|
||||
}
|
||||
|
||||
const orderStatusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'orange', text: '待支付' },
|
||||
paid: { color: 'blue', text: '已支付' },
|
||||
delivering: { color: 'cyan', text: '发货中' },
|
||||
delivered: { color: 'green', text: '已交付' },
|
||||
ship_failed: { color: 'red', text: '发货失败' },
|
||||
cancelled: { color: 'default', text: '已取消' },
|
||||
}
|
||||
|
||||
export default function ShipLogs() {
|
||||
const [list, setList] = useState<ShipLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(10)
|
||||
const [orderNo, setOrderNo] = useState('')
|
||||
const [shipStatus, setShipStatus] = useState<string | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<ShipLog | null>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await shipLogApi.list({
|
||||
page,
|
||||
size,
|
||||
order_no: orderNo || undefined,
|
||||
ship_status: shipStatus,
|
||||
})
|
||||
setList(data.list || [])
|
||||
setTotal(data.total)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, size, orderNo, shipStatus])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const columns: ColumnsType<ShipLog> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '店铺订单号', dataIndex: 'order_no', width: 200, ellipsis: true },
|
||||
{
|
||||
title: '推送状态',
|
||||
dataIndex: 'ship_status',
|
||||
width: 100,
|
||||
render: (v: string) => {
|
||||
const s = shipStatusMap[v] || { color: 'default', text: v }
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '处理后订单状态',
|
||||
dataIndex: 'result_status',
|
||||
width: 130,
|
||||
render: (v: string) => {
|
||||
if (!v) return '-'
|
||||
const s = orderStatusMap[v] || { color: 'default', text: v }
|
||||
return <Tag color={s.color}>{s.text}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上游单号',
|
||||
dataIndex: 'provider_order_no',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'message',
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'fail_reason',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
render: (_, record) => (
|
||||
<Button type="link" size="small" onClick={() => setDetail(record)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
发货推送记录
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Input.Search
|
||||
placeholder="店铺订单号"
|
||||
allowClear
|
||||
onSearch={(v) => {
|
||||
setPage(1)
|
||||
setOrderNo(v)
|
||||
}}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="推送状态"
|
||||
style={{ width: 140 }}
|
||||
value={shipStatus}
|
||||
onChange={(v) => {
|
||||
setPage(1)
|
||||
setShipStatus(v)
|
||||
}}
|
||||
options={[
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'processing', label: '发货中' },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: -4 }}>
|
||||
记录皮肤源头回调的发货结果,便于对账与排错。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p, s) => {
|
||||
setPage(p)
|
||||
setSize(s)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="推送详情"
|
||||
open={!!detail}
|
||||
onCancel={() => setDetail(null)}
|
||||
footer={null}
|
||||
width={640}
|
||||
>
|
||||
{detail && (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<div>
|
||||
<Typography.Text type="secondary">店铺订单号:</Typography.Text>
|
||||
<Typography.Text copyable>{detail.order_no}</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">上游单号:</Typography.Text>
|
||||
{detail.provider_order_no || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">推送状态 / 结果状态:</Typography.Text>
|
||||
{detail.ship_status} → {detail.result_status || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">说明:</Typography.Text>
|
||||
{detail.message || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">失败原因:</Typography.Text>
|
||||
{detail.fail_reason || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text type="secondary">原始请求:</Typography.Text>
|
||||
<pre
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
maxHeight: 280,
|
||||
overflow: 'auto',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{formatPayload(detail.payload)}
|
||||
</pre>
|
||||
</div>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatPayload(raw: string) {
|
||||
if (!raw) return '-'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(raw), null, 2)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { skinApi, orderApi } from '../api'
|
||||
import { useAuth } from '../store/auth'
|
||||
import type { Skin } from '../types'
|
||||
|
||||
export default function Skins() {
|
||||
const { isAdmin } = useAuth()
|
||||
const [list, setList] = useState<Skin[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(10)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<Skin | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await skinApi.list({ page, size, keyword })
|
||||
setList(data.list || [])
|
||||
setTotal(data.total)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, size, keyword])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
game: '和平精英',
|
||||
stock: -1,
|
||||
commission: 0,
|
||||
status: 1,
|
||||
})
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (record: Skin) => {
|
||||
setEditing(record)
|
||||
form.setFieldsValue(record)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const values = await form.validateFields()
|
||||
try {
|
||||
if (editing) {
|
||||
await skinApi.update(editing.id, values)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await skinApi.create(values)
|
||||
message.success('创建成功')
|
||||
}
|
||||
setOpen(false)
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = async (id: number) => {
|
||||
try {
|
||||
await skinApi.remove(id)
|
||||
message.success('已删除')
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onOrder = async (skin: Skin) => {
|
||||
try {
|
||||
await orderApi.create({ skin_id: skin.id, buyer_name: '演示买家' })
|
||||
message.success('下单成功')
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '下单失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Skin> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '中文名', dataIndex: 'name', width: 180, ellipsis: true },
|
||||
{
|
||||
title: '英文名',
|
||||
dataIndex: 'sku',
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
render: (v: string) => <Typography.Text code>{v}</Typography.Text>,
|
||||
},
|
||||
{ title: '游戏', dataIndex: 'game', width: 100 },
|
||||
{
|
||||
title: '售价',
|
||||
dataIndex: 'price',
|
||||
width: 100,
|
||||
render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '佣金比例',
|
||||
dataIndex: 'commission',
|
||||
width: 100,
|
||||
render: (v: number) => `${((v ?? 0) * 100).toFixed(0)}%`,
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
width: 80,
|
||||
render: (v: number) => (v < 0 ? '无限' : v),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (v: number) =>
|
||||
v === 1 ? <Tag color="green">上架</Tag> : <Tag>下架</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: isAdmin ? 140 : 80,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确认删除?" onConfirm={() => onDelete(record.id)}>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={record.status !== 1}
|
||||
onClick={() => onOrder(record)}
|
||||
>
|
||||
下单
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
皮肤商品
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Input.Search
|
||||
placeholder="搜索中文名 / 英文名"
|
||||
allowClear
|
||||
onSearch={(v) => {
|
||||
setPage(1)
|
||||
setKeyword(v)
|
||||
}}
|
||||
style={{ width: 240 }}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||
刷新
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新增皮肤
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
tableLayout="fixed"
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, s) => {
|
||||
setPage(p)
|
||||
setSize(s)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑皮肤' : '新增皮肤'}
|
||||
open={open}
|
||||
onOk={onSubmit}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="name" label="中文名" rules={[{ required: true }]}>
|
||||
<Input placeholder="如:套装-糯粉咩咩" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="sku"
|
||||
label="英文名 (sku)"
|
||||
rules={[{ required: true, message: '请填写英文固定标识' }]}
|
||||
>
|
||||
<Input placeholder="如:suit_pink_sheep" disabled={!!editing} />
|
||||
</Form.Item>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="game" label="游戏" style={{ width: 240 }}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '和平精英', label: '和平精英' },
|
||||
{ value: '王者荣耀', label: '王者荣耀' },
|
||||
{ value: '英雄联盟', label: '英雄联盟' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="category" label="品类" style={{ width: 240 }}>
|
||||
<Select
|
||||
allowClear
|
||||
options={[
|
||||
{ value: '套装', label: '套装' },
|
||||
{ value: '上衣', label: '上衣' },
|
||||
{ value: '背包', label: '背包' },
|
||||
{ value: '头盔', label: '头盔' },
|
||||
{ value: '枪械', label: '枪械' },
|
||||
{ value: '投掷物', label: '投掷物' },
|
||||
{ value: '礼包', label: '礼包' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="price" label="售价" rules={[{ required: true }]} style={{ width: 160 }}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cost_price" label="成本价" style={{ width: 160 }}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="commission" label="佣金比例" style={{ width: 160 }}>
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="stock" label="库存(-1无限)" style={{ width: 240 }}>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" style={{ width: 240 }}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 1, label: '上架' },
|
||||
{ value: 0, label: '下架' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+13
-53
@@ -2,10 +2,8 @@ export interface User {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
role: 'admin' | 'distributor'
|
||||
role: 'admin' | 'merchant'
|
||||
status: number
|
||||
invite_code: string
|
||||
parent_id?: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -16,6 +14,10 @@ export interface Merchant {
|
||||
status: 'active' | 'disabled'
|
||||
contact_name?: string
|
||||
contact_info?: string
|
||||
features: string
|
||||
fee_type: 'rate' | 'fixed'
|
||||
fee_rate_bp: number
|
||||
fee_fixed_amount: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -86,6 +88,11 @@ export interface FulfillmentOrder {
|
||||
product_sku: string
|
||||
product_name: string
|
||||
quantity: number
|
||||
base_amount: number
|
||||
fee_type: 'rate' | 'fixed'
|
||||
fee_rate_bp: number
|
||||
fee_fixed_amount: number
|
||||
service_fee_amount: number
|
||||
amount: number
|
||||
currency: string
|
||||
payment_status: string
|
||||
@@ -131,59 +138,12 @@ export interface CallbackCredential {
|
||||
secret: string
|
||||
}
|
||||
|
||||
export interface Skin {
|
||||
id: number
|
||||
name: string
|
||||
sku: string
|
||||
game: string
|
||||
category: string
|
||||
cover_url: string
|
||||
price: number
|
||||
cost_price: number
|
||||
commission: number
|
||||
stock: number
|
||||
status: number
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: number
|
||||
order_no: string
|
||||
skin_id: number
|
||||
skin?: Skin
|
||||
distributor_id: number
|
||||
distributor?: User
|
||||
buyer_name: string
|
||||
amount: number
|
||||
commission_amt: number
|
||||
status: string
|
||||
remark: string
|
||||
provider_order_no?: string
|
||||
shipped_at?: string | null
|
||||
ship_fail_reason?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ShipLog {
|
||||
id: number
|
||||
order_no: string
|
||||
order_id: number
|
||||
ship_status: string
|
||||
provider_order_no: string
|
||||
fail_reason: string
|
||||
payload: string
|
||||
result_status: string
|
||||
message: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DashboardStats {
|
||||
skin_count: number
|
||||
distributor_count: number
|
||||
product_count: number
|
||||
merchant_count: number
|
||||
order_count: number
|
||||
total_sales: number
|
||||
total_commission: number
|
||||
total_fees: number
|
||||
pending_order_count: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user