This commit is contained in:
魔方熊成祥
2026-04-29 21:38:16 +08:00
commit c7481718e4
45 changed files with 9530 additions and 0 deletions

View File

@@ -0,0 +1,540 @@
import { useEffect, useState } from 'react'
import dayjs, { type Dayjs } from 'dayjs'
import {
Avatar,
Button,
Card,
DatePicker,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Table,
Typography,
} from 'antd'
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'
import { toast } from 'sonner'
import {
apiAdminMerchantPage,
apiAdminRecommendMerchantDelete,
apiAdminRecommendMerchantPage,
apiAdminRecommendMerchantSave,
} from '@/apis/apiMallAdmin'
import StatusTag from '@/components/StatusTag'
import type {
AdminRecommendMerchantItem,
AdminRecommendMerchantPageParams,
AdminRecommendMerchantSaveParams,
EnabledStatus,
MerchantInfo,
RecommendMerchantPositionCode,
} from '@/types/mall'
import { compactText, formatTimestamp } from '@/utils/format'
import {
enabledStatusOptions,
getRecommendStorePositionLabel,
recommendStorePositionOptions,
withAllOption,
} from '@/utils/mall'
const { TextArea } = Input
const DEFAULT_PAGE_SIZE = 20
const DEFAULT_STATUS: EnabledStatus = 'enabled'
const DEFAULT_POSITION_CODE: RecommendMerchantPositionCode = 'home_featured_store'
interface RecommendStoreSearchValues {
keyword?: string
positionCode?: RecommendMerchantPositionCode | ''
status?: EnabledStatus | ''
merchantId?: string
page: number
size: number
}
interface RecommendStoreFormValues {
positionCode: RecommendMerchantPositionCode
merchantId?: string
mainCategoryName?: string
recommendReason?: string
rankScore?: number
sort?: number
status: EnabledStatus
startTime?: Dayjs | null
endTime?: Dayjs | null
}
interface MerchantSelectOption {
label: string
value: string
}
const normalizeSearchValues = (values: RecommendStoreSearchValues): AdminRecommendMerchantPageParams => ({
keyword: values.keyword?.trim() || undefined,
positionCode: values.positionCode || undefined,
status: values.status || undefined,
merchantId: values.merchantId || undefined,
page: values.page,
size: values.size,
})
const buildMerchantOption = (merchant: Pick<MerchantInfo, 'id' | 'merchantName'>): MerchantSelectOption => ({
value: String(merchant.id),
label: compactText(merchant.merchantName),
})
const mergeMerchantOptions = (current: MerchantSelectOption[], next: MerchantSelectOption[]) => {
const merged = new Map<string, MerchantSelectOption>()
current.forEach(item => merged.set(item.value, item))
next.forEach(item => merged.set(item.value, item))
return Array.from(merged.values())
}
export default function RecommendStoreManagementPage() {
const [form] = Form.useForm<RecommendStoreSearchValues>()
const [modalForm] = Form.useForm<RecommendStoreFormValues>()
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [merchantLoading, setMerchantLoading] = useState(false)
const [list, setList] = useState<AdminRecommendMerchantItem[]>([])
const [total, setTotal] = useState(0)
const [pagination, setPagination] = useState({ current: 1, pageSize: DEFAULT_PAGE_SIZE })
const [modalOpen, setModalOpen] = useState(false)
const [editingRecord, setEditingRecord] = useState<AdminRecommendMerchantItem | null>(null)
const [merchantOptions, setMerchantOptions] = useState<MerchantSelectOption[]>([])
const loadData = async (page = pagination.current, size = pagination.pageSize) => {
try {
setLoading(true)
const response = await apiAdminRecommendMerchantPage(
normalizeSearchValues({
...form.getFieldsValue(),
page,
size,
})
)
setList(response.data.list)
setTotal(response.data.total)
setPagination({ current: page, pageSize: size })
} catch (error) {
toast.error(error instanceof Error ? error.message : '首页推荐商家列表加载失败')
} finally {
setLoading(false)
}
}
const loadMerchantOptions = async (keyword?: string) => {
try {
setMerchantLoading(true)
const response = await apiAdminMerchantPage({
keyword: keyword?.trim() || undefined,
page: 1,
size: 20,
})
const nextOptions = response.data.list.map(item => buildMerchantOption(item))
setMerchantOptions(current => mergeMerchantOptions(current, nextOptions))
} catch (error) {
toast.error(error instanceof Error ? error.message : '商家选项加载失败')
} finally {
setMerchantLoading(false)
}
}
useEffect(() => {
form.setFieldsValue({
page: 1,
size: DEFAULT_PAGE_SIZE,
})
void loadData(1, DEFAULT_PAGE_SIZE)
}, [])
const resetModal = () => {
setModalOpen(false)
setEditingRecord(null)
modalForm.resetFields()
}
const openCreateModal = () => {
setEditingRecord(null)
modalForm.setFieldsValue({
positionCode: DEFAULT_POSITION_CODE,
rankScore: 0,
sort: 0,
status: DEFAULT_STATUS,
startTime: null,
endTime: null,
})
setModalOpen(true)
if (!merchantOptions.length) {
void loadMerchantOptions()
}
}
const openEditModal = (record: AdminRecommendMerchantItem) => {
setEditingRecord(record)
setMerchantOptions(current =>
mergeMerchantOptions(current, [
{
value: record.merchantId,
label: compactText(record.merchantName),
},
])
)
modalForm.setFieldsValue({
positionCode: record.positionCode,
merchantId: record.merchantId,
mainCategoryName: record.mainCategoryName || '',
recommendReason: record.recommendReason || '',
rankScore: Number(record.rankScore ?? 0),
sort: record.sort ?? 0,
status: record.configStatus,
startTime: record.startTime ? dayjs.unix(record.startTime) : null,
endTime: record.endTime ? dayjs.unix(record.endTime) : null,
})
setModalOpen(true)
}
const handleDelete = async (record: AdminRecommendMerchantItem) => {
try {
await apiAdminRecommendMerchantDelete({ id: record.id })
toast.success('首页推荐商家已删除')
await loadData()
} catch (error) {
toast.error(error instanceof Error ? error.message : '首页推荐商家删除失败')
}
}
const handleSave = async () => {
try {
const values = await modalForm.validateFields()
setSaving(true)
const payload: AdminRecommendMerchantSaveParams = {
id: editingRecord?.id,
positionCode: values.positionCode,
merchantId: values.merchantId || '',
mainCategoryName: values.mainCategoryName?.trim() || undefined,
recommendReason: values.recommendReason?.trim() || undefined,
rankScore: values.rankScore ?? 0,
sort: values.sort ?? 0,
status: values.status,
startTime: values.startTime?.unix(),
endTime: values.endTime?.unix(),
}
await apiAdminRecommendMerchantSave(payload)
toast.success(editingRecord ? '首页推荐商家已更新' : '首页推荐商家已新增')
resetModal()
await loadData()
} catch (error) {
if (error instanceof Error && error.message) {
toast.error(error.message)
}
} finally {
setSaving(false)
}
}
const handleTableChange = (next: TablePaginationConfig) => {
void loadData(next.current || 1, next.pageSize || DEFAULT_PAGE_SIZE)
}
const columns: ColumnsType<AdminRecommendMerchantItem> = [
{
title: '配置 ID',
dataIndex: 'id',
width: 92,
},
{
title: '位置编码',
dataIndex: 'positionCode',
width: 140,
render: value => getRecommendStorePositionLabel(value),
},
{
title: '商家 ID',
dataIndex: 'merchantId',
width: 92,
},
{
title: '商家名称',
dataIndex: 'merchantName',
width: 200,
render: value => compactText(value),
},
{
title: '商家 Logo',
dataIndex: 'logo',
width: 100,
render: (_, record) => (
<Avatar shape="square" size={48} src={record.logo || undefined}>
{record.merchantName?.slice(0, 1)}
</Avatar>
),
},
{
title: '商家公告',
dataIndex: 'notice',
width: 220,
render: value => (
<Typography.Text title={compactText(value)}>
{compactText(value)}
</Typography.Text>
),
},
{
title: '商家状态',
dataIndex: 'merchantStatus',
width: 96,
render: value => <StatusTag value={value} />,
},
{
title: '审核状态',
dataIndex: 'auditStatus',
width: 96,
render: value => <StatusTag value={value} />,
},
{
title: '主推类目',
dataIndex: 'mainCategoryName',
width: 160,
render: value => compactText(value),
},
{
title: '推荐理由',
dataIndex: 'recommendReason',
width: 220,
render: value => (
<Typography.Text title={compactText(value)}>
{compactText(value)}
</Typography.Text>
),
},
{
title: '排行分值',
dataIndex: 'rankScore',
width: 96,
},
{
title: '排序',
dataIndex: 'sort',
width: 76,
},
{
title: '配置状态',
dataIndex: 'configStatus',
width: 96,
render: value => <StatusTag value={value} />,
},
{
title: '开始时间',
dataIndex: 'startTime',
width: 170,
render: value => formatTimestamp(value),
},
{
title: '结束时间',
dataIndex: 'endTime',
width: 170,
render: value => formatTimestamp(value),
},
{
title: '更新时间',
dataIndex: 'updatedAt',
width: 170,
render: value => formatTimestamp(value),
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 144,
render: (_, record) => (
<Space size={4}>
<Button type="link" onClick={() => openEditModal(record)}>
</Button>
<Popconfirm
title="确认删除该推荐商家配置吗?"
okText="删除"
cancelText="取消"
onConfirm={() => void handleDelete(record)}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
]
return (
<Space direction="vertical" size={16} style={{ display: 'flex' }}>
<div className="page-toolbar">
<Typography.Title level={3} style={{ margin: 0 }}>
</Typography.Title>
<Button type="primary" onClick={openCreateModal}>
</Button>
</div>
<Card>
<Form className="search-form" form={form} layout="inline" onFinish={() => void loadData(1, pagination.pageSize)}>
<Form.Item name="merchantId" label="商家筛选">
<Select
showSearch
allowClear
filterOption={false}
loading={merchantLoading}
options={merchantOptions}
style={{ width: 220 }}
placeholder="请输入商家名称搜索"
onFocus={() => {
if (!merchantOptions.length) {
void loadMerchantOptions()
}
}}
onSearch={value => {
void loadMerchantOptions(value)
}}
/>
</Form.Item>
<Form.Item name="keyword" label="关键词">
<Input allowClear placeholder="商家名称" style={{ width: 220 }} />
</Form.Item>
<Form.Item name="positionCode" label="投放位置">
<Select
allowClear
options={withAllOption(recommendStorePositionOptions)}
style={{ width: 180 }}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select allowClear options={withAllOption(enabledStatusOptions)} style={{ width: 160 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
form.resetFields()
void loadData(1, DEFAULT_PAGE_SIZE)
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card>
<Table<AdminRecommendMerchantItem>
rowKey="id"
loading={loading}
columns={columns}
dataSource={list}
className="table-top-align"
scroll={{ x: 2200 }}
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
total,
showSizeChanger: true,
showTotal: count => `${count}`,
}}
onChange={handleTableChange}
/>
</Card>
<Modal
destroyOnHidden
open={modalOpen}
title={editingRecord ? '编辑首页推荐商家' : '新增首页推荐商家'}
width={640}
confirmLoading={saving}
okText="保存"
cancelText="取消"
onCancel={resetModal}
onOk={() => void handleSave()}
>
<Form form={modalForm} layout="vertical">
<Form.Item
name="positionCode"
label="投放位置"
rules={[{ required: true, message: '请选择投放位置' }]}
>
<Select options={recommendStorePositionOptions} placeholder="请选择投放位置" />
</Form.Item>
<Form.Item
name="merchantId"
label="推荐商家"
rules={[{ required: true, message: '请选择商家' }]}
extra="当前推荐位直接选择商家。"
>
<Select
showSearch
filterOption={false}
loading={merchantLoading}
options={merchantOptions}
placeholder="请输入商家名称搜索"
onFocus={() => {
if (!merchantOptions.length) {
void loadMerchantOptions()
}
}}
onSearch={value => {
void loadMerchantOptions(value)
}}
/>
</Form.Item>
<Form.Item name="mainCategoryName" label="主推类目">
<Input maxLength={50} placeholder="首页展示用,如:精品水果 / 五金工具" />
</Form.Item>
<Form.Item name="recommendReason" label="推荐理由">
<TextArea rows={3} maxLength={200} placeholder="请输入首页展示文案" />
</Form.Item>
<Space size={12} style={{ display: 'flex' }}>
<Form.Item name="rankScore" label="排行分值" style={{ flex: 1 }}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} placeholder="默认 0" />
</Form.Item>
<Form.Item name="sort" label="排序" style={{ flex: 1 }}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} placeholder="默认 0" />
</Form.Item>
</Space>
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
<Select options={enabledStatusOptions} placeholder="请选择状态" />
</Form.Item>
<Space size={12} style={{ display: 'flex' }}>
<Form.Item name="startTime" label="开始时间" style={{ flex: 1 }}>
<DatePicker showTime style={{ width: '100%' }} placeholder="请选择开始时间" />
</Form.Item>
<Form.Item
name="endTime"
label="结束时间"
style={{ flex: 1 }}
dependencies={['startTime']}
rules={[
({ getFieldValue }) => ({
validator(_, value?: Dayjs | null) {
const startTime = getFieldValue('startTime') as Dayjs | null | undefined
if (!startTime || !value || value.valueOf() >= startTime.valueOf()) {
return Promise.resolve()
}
return Promise.reject(new Error('结束时间不能早于开始时间'))
},
}),
]}
>
<DatePicker showTime style={{ width: '100%' }} placeholder="请选择结束时间" />
</Form.Item>
</Space>
</Form>
</Modal>
</Space>
)
}