1
This commit is contained in:
540
src/pages/admin/recommendStores/index.tsx
Normal file
540
src/pages/admin/recommendStores/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user