1
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
import type { BaseResponse } from '@/apis/base/types'
|
||||
import axiosInstance from '@/apis/base/axiosInstance'
|
||||
import type { MallCategoryTreeData, MallCategoryTreeParams } from '@/types/mall'
|
||||
import { compactObject } from '@/utils/mall'
|
||||
|
||||
export const mallCategoryTree = async (
|
||||
payload: MallCategoryTreeParams = {},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const response = await axiosInstance.post<BaseResponse<MallCategoryTreeData>>(
|
||||
'/api/v1/mall/admin/category_tree',
|
||||
compactObject(payload),
|
||||
{ signal }
|
||||
)
|
||||
|
||||
const businessPayload = response.data
|
||||
if (!businessPayload) {
|
||||
throw new Error('服务端未返回业务数据')
|
||||
}
|
||||
|
||||
if (businessPayload.code !== 200) {
|
||||
throw new Error(businessPayload.message || businessPayload.msg || '请求失败')
|
||||
}
|
||||
|
||||
return businessPayload.data
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export * from './profile'
|
||||
export * from './category'
|
||||
export * from './goods'
|
||||
export * from './featuredGoods'
|
||||
export * from './options'
|
||||
export * from './orders'
|
||||
export * from './afterSale'
|
||||
|
||||
71
src/apis/mallMerchant/options.ts
Normal file
71
src/apis/mallMerchant/options.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type {
|
||||
MerchantDeliveryOptionPageData,
|
||||
MerchantDeliveryOptionSaveParams,
|
||||
MerchantOptionDeleteData,
|
||||
MerchantOptionDeleteParams,
|
||||
MerchantOptionPageParams,
|
||||
MerchantOptionSaveData,
|
||||
MerchantPackageOptionPageData,
|
||||
MerchantPackageOptionSaveParams,
|
||||
} from '@/types/mall'
|
||||
import { merchantPost } from './shared'
|
||||
|
||||
export const merchantDeliveryOptionPage = (
|
||||
payload: MerchantOptionPageParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantDeliveryOptionPageData, MerchantOptionPageParams>(
|
||||
'/api/v1/mall/merchant/delivery_option_page',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
|
||||
export const merchantDeliveryOptionSave = (
|
||||
payload: MerchantDeliveryOptionSaveParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantOptionSaveData, MerchantDeliveryOptionSaveParams>(
|
||||
'/api/v1/mall/merchant/delivery_option_save',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
|
||||
export const merchantDeliveryOptionDelete = (
|
||||
payload: MerchantOptionDeleteParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantOptionDeleteData, MerchantOptionDeleteParams>(
|
||||
'/api/v1/mall/merchant/delivery_option_delete',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
|
||||
export const merchantPackageOptionPage = (
|
||||
payload: MerchantOptionPageParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantPackageOptionPageData, MerchantOptionPageParams>(
|
||||
'/api/v1/mall/merchant/package_option_page',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
|
||||
export const merchantPackageOptionSave = (
|
||||
payload: MerchantPackageOptionSaveParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantOptionSaveData, MerchantPackageOptionSaveParams>(
|
||||
'/api/v1/mall/merchant/package_option_save',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
|
||||
export const merchantPackageOptionDelete = (
|
||||
payload: MerchantOptionDeleteParams,
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
merchantPost<MerchantOptionDeleteData, MerchantOptionDeleteParams>(
|
||||
'/api/v1/mall/merchant/package_option_delete',
|
||||
payload,
|
||||
signal
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { mallCategoryTree } from '@/apis/mallCategory'
|
||||
import { merchantCategoryTree } from '@/apis/mallMerchant'
|
||||
import type { MallCategoryItem } from '@/types/mall'
|
||||
|
||||
interface MallCategoryTreeNode {
|
||||
@@ -20,7 +20,7 @@ const loadMallCategoryTree = async () => {
|
||||
}
|
||||
|
||||
if (!pendingCategoryTreeRequest) {
|
||||
pendingCategoryTreeRequest = mallCategoryTree({ status: 'enabled' })
|
||||
pendingCategoryTreeRequest = merchantCategoryTree({ status: 'enabled' })
|
||||
.then(data => {
|
||||
cachedCategoryTree = data.list ?? []
|
||||
return cachedCategoryTree
|
||||
|
||||
@@ -618,6 +618,64 @@ a {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.order-detail-product {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-detail-product__image {
|
||||
flex: 0 0 52px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.order-detail-product__placeholder {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
flex: 0 0 52px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--ant-color-border);
|
||||
background: #fafafa;
|
||||
color: #a0a6ad;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.order-detail-product__content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.order-detail-product__title {
|
||||
display: -webkit-box;
|
||||
max-height: 36px;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 13px !important;
|
||||
line-height: 18px !important;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.after-sale-evidence {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.after-sale-evidence__image {
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.goods-detail-editor {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
InboxOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
@@ -48,6 +49,11 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
label: '首页精选商品',
|
||||
icon: <StarOutlined />,
|
||||
},
|
||||
{
|
||||
key: '/fulfillment-options',
|
||||
label: '物流包装配置',
|
||||
icon: <InboxOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography } from 'antd'
|
||||
import { Button, Card, Form, Image, Input, Select, Space, Table, Tag, Typography } from 'antd'
|
||||
import { toast } from 'sonner'
|
||||
import { merchantAfterSalePage } from '@/apis/mallMerchant'
|
||||
import AuditModal from './AuditModal'
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
formatUnixTime,
|
||||
getStatusColor,
|
||||
getStatusLabel,
|
||||
prettyJsonText,
|
||||
} from '@/utils/mall'
|
||||
|
||||
interface AfterSaleSearchValues {
|
||||
@@ -20,6 +19,54 @@ interface AfterSaleSearchValues {
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
const isUrlText = (value: string) => /^https?:\/\//i.test(value) || value.startsWith('//')
|
||||
|
||||
const normalizeEvidenceUrl = (value: string) => {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed.startsWith('//')) {
|
||||
return `https:${trimmed}`
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const collectEvidenceUrls = (value: unknown, result: string[]) => {
|
||||
if (typeof value === 'string') {
|
||||
const normalized = normalizeEvidenceUrl(value)
|
||||
if (isUrlText(normalized)) {
|
||||
result.push(normalized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => collectEvidenceUrls(item, result))
|
||||
return
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
Object.values(value as Record<string, unknown>).forEach(item => collectEvidenceUrls(item, result))
|
||||
}
|
||||
}
|
||||
|
||||
const parseEvidenceUrls = (value?: string) => {
|
||||
if (!value?.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result: string[] = []
|
||||
|
||||
try {
|
||||
collectEvidenceUrls(JSON.parse(value) as unknown, result)
|
||||
} catch {
|
||||
collectEvidenceUrls(value, result)
|
||||
}
|
||||
|
||||
return Array.from(new Set(result))
|
||||
}
|
||||
|
||||
const isImageEvidence = (url: string) => /\.(png|jpe?g|webp|gif|avif)(?:[?#].*)?$/i.test(url)
|
||||
|
||||
export default function AfterSalePage() {
|
||||
const merchantId = useSessionStore(state => state.merchantId)
|
||||
const [form] = Form.useForm<AfterSaleSearchValues>()
|
||||
@@ -115,14 +162,15 @@ export default function AfterSalePage() {
|
||||
|
||||
<Card className="page-card" title="售后列表">
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowKey={record => String(record.id)}
|
||||
loading={loading}
|
||||
dataSource={list}
|
||||
expandable={{
|
||||
expandedRowRender: record => (
|
||||
expandedRowRender: record => {
|
||||
const evidenceUrls = parseEvidenceUrls(record.evidenceJson)
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Typography.Text>售后单 ID:{record.id}</Typography.Text>
|
||||
<Typography.Text>会员 ID:{record.uid}</Typography.Text>
|
||||
<Typography.Text>原因:{record.reason || '-'}</Typography.Text>
|
||||
<Typography.Text>说明:{record.description || '-'}</Typography.Text>
|
||||
<Typography.Text>驳回原因:{record.rejectReason || '-'}</Typography.Text>
|
||||
@@ -130,14 +178,34 @@ export default function AfterSalePage() {
|
||||
<Typography.Text>审核时间:{formatUnixTime(record.auditTime)}</Typography.Text>
|
||||
<Typography.Text>完成时间:{formatUnixTime(record.successTime)}</Typography.Text>
|
||||
<Typography.Text>关闭时间:{formatUnixTime(record.closeTime)}</Typography.Text>
|
||||
<div>
|
||||
凭证 JSON:
|
||||
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{prettyJsonText(record.evidenceJson) || '-'}
|
||||
</Typography.Text>
|
||||
<div className="after-sale-evidence">
|
||||
<Typography.Text>凭证:</Typography.Text>
|
||||
{evidenceUrls.length ? (
|
||||
<Space size={8} wrap>
|
||||
{evidenceUrls.map(url =>
|
||||
isImageEvidence(url) ? (
|
||||
<Image
|
||||
key={url}
|
||||
src={url}
|
||||
width={64}
|
||||
height={64}
|
||||
className="after-sale-evidence__image"
|
||||
preview={{ src: url }}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Link key={url} href={url} target="_blank">
|
||||
查看凭证
|
||||
</Typography.Link>
|
||||
)
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text type="secondary">-</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
)
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
|
||||
@@ -19,8 +19,8 @@ interface GoodsPickerSearchValues {
|
||||
interface GoodsPickerModalProps {
|
||||
open: boolean
|
||||
merchantId: string
|
||||
configuredSpuIds: number[]
|
||||
currentSpuId?: number
|
||||
configuredSpuIds: string[]
|
||||
currentSpuId?: string
|
||||
onCancel: () => void
|
||||
onSelect: (goods: GoodsSummary) => void
|
||||
}
|
||||
@@ -138,7 +138,7 @@ export default function GoodsPickerModal({
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowKey={record => String(record.id)}
|
||||
loading={loading}
|
||||
dataSource={list}
|
||||
scroll={{ x: 1080 }}
|
||||
@@ -211,10 +211,10 @@ export default function GoodsPickerModal({
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
disabled={disabledSpuIds.has(record.id)}
|
||||
disabled={disabledSpuIds.has(String(record.id))}
|
||||
onClick={() => onSelect(record)}
|
||||
>
|
||||
{disabledSpuIds.has(record.id) ? '已配置' : '选择'}
|
||||
{disabledSpuIds.has(String(record.id)) ? '已配置' : '选择'}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ interface EditorValues {
|
||||
}
|
||||
|
||||
interface SelectedGoodsInfo {
|
||||
id: number
|
||||
id: string
|
||||
spuNo: string
|
||||
spuName: string
|
||||
subTitle: string
|
||||
@@ -99,7 +99,7 @@ export default function FeaturedGoodsPage() {
|
||||
const [editingRecord, setEditingRecord] = useState<MerchantFeaturedGoodsItem | null>(null)
|
||||
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsInfo | null>(null)
|
||||
|
||||
const configuredSpuIds = useMemo(() => list.map(item => item.spuId), [list])
|
||||
const configuredSpuIds = useMemo(() => list.map(item => String(item.spuId)), [list])
|
||||
|
||||
const loadList = async (nextPage = page, nextSize = size) => {
|
||||
if (!merchantId) {
|
||||
@@ -148,7 +148,7 @@ export default function FeaturedGoodsPage() {
|
||||
const handleOpenEdit = (record: MerchantFeaturedGoodsItem) => {
|
||||
setEditingRecord(record)
|
||||
setSelectedGoods({
|
||||
id: record.spuId,
|
||||
id: String(record.spuId),
|
||||
spuNo: record.spuNo,
|
||||
spuName: record.spuName,
|
||||
subTitle: record.subTitle,
|
||||
@@ -168,7 +168,7 @@ export default function FeaturedGoodsPage() {
|
||||
try {
|
||||
await merchantFeaturedGoodsDelete({
|
||||
merchantId,
|
||||
id: record.id,
|
||||
id: String(record.id),
|
||||
})
|
||||
toast.success('精选商品已删除')
|
||||
await loadList()
|
||||
@@ -193,8 +193,8 @@ export default function FeaturedGoodsPage() {
|
||||
setSaving(true)
|
||||
await merchantFeaturedGoodsSave({
|
||||
merchantId,
|
||||
id: editingRecord?.id,
|
||||
spuId: selectedGoods.id,
|
||||
id: editingRecord?.id ? String(editingRecord.id) : undefined,
|
||||
spuId: String(selectedGoods.id),
|
||||
sort: values.sort ?? 0,
|
||||
status: values.status,
|
||||
})
|
||||
@@ -447,11 +447,11 @@ export default function FeaturedGoodsPage() {
|
||||
open={pickerOpen}
|
||||
merchantId={merchantId}
|
||||
configuredSpuIds={configuredSpuIds}
|
||||
currentSpuId={editingRecord?.spuId}
|
||||
currentSpuId={editingRecord?.spuId ? String(editingRecord.spuId) : undefined}
|
||||
onCancel={() => setPickerOpen(false)}
|
||||
onSelect={(goods: GoodsSummary) => {
|
||||
setSelectedGoods({
|
||||
id: goods.id,
|
||||
id: String(goods.id),
|
||||
spuNo: goods.spuNo,
|
||||
spuName: goods.spuName,
|
||||
subTitle: goods.subTitle,
|
||||
|
||||
422
src/pages/fulfillmentOptions/index.tsx
Normal file
422
src/pages/fulfillmentOptions/index.tsx
Normal file
@@ -0,0 +1,422 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
merchantDeliveryOptionDelete,
|
||||
merchantDeliveryOptionPage,
|
||||
merchantDeliveryOptionSave,
|
||||
merchantPackageOptionDelete,
|
||||
merchantPackageOptionPage,
|
||||
merchantPackageOptionSave,
|
||||
} from '@/apis/mallMerchant'
|
||||
import { useSessionStore } from '@/store/session'
|
||||
import type {
|
||||
EnableStatus,
|
||||
MerchantDeliveryOptionItem,
|
||||
MerchantPackageOptionItem,
|
||||
} from '@/types/mall'
|
||||
import { ENABLE_STATUS_OPTIONS, formatUnixTime, getStatusColor, getStatusLabel } from '@/utils/mall'
|
||||
|
||||
type OptionKind = 'delivery' | 'package'
|
||||
type OptionRecord = MerchantDeliveryOptionItem | MerchantPackageOptionItem
|
||||
|
||||
interface SearchValues {
|
||||
status?: EnableStatus | ''
|
||||
}
|
||||
|
||||
interface EditorValues {
|
||||
optionName: string
|
||||
optionCode?: string
|
||||
feeAmount?: number | string
|
||||
description?: string
|
||||
sort?: number
|
||||
status: EnableStatus
|
||||
}
|
||||
|
||||
const KIND_META: Record<
|
||||
OptionKind,
|
||||
{
|
||||
title: string
|
||||
createText: string
|
||||
codeLabel: string
|
||||
codePlaceholder: string
|
||||
emptyCode: string
|
||||
}
|
||||
> = {
|
||||
delivery: {
|
||||
title: '物流方式',
|
||||
createText: '新增物流方式',
|
||||
codeLabel: '物流编码',
|
||||
codePlaceholder: '例如 normal / sf',
|
||||
emptyCode: '未配置物流编码',
|
||||
},
|
||||
package: {
|
||||
title: '包装袋',
|
||||
createText: '新增包装袋',
|
||||
codeLabel: '包装编码',
|
||||
codePlaceholder: '例如 normal_bag / bubble_bag',
|
||||
emptyCode: '未配置包装编码',
|
||||
},
|
||||
}
|
||||
|
||||
const getOptionCode = (record: OptionRecord, kind: OptionKind) =>
|
||||
kind === 'delivery'
|
||||
? (record as MerchantDeliveryOptionItem).deliveryCode
|
||||
: (record as MerchantPackageOptionItem).packageCode
|
||||
|
||||
const normalizeAmount = (value?: number | string) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '0.00'
|
||||
}
|
||||
|
||||
const amount = Number(value)
|
||||
if (!Number.isFinite(amount) || amount < 0) {
|
||||
return '0.00'
|
||||
}
|
||||
|
||||
return amount.toFixed(2)
|
||||
}
|
||||
|
||||
export default function FulfillmentOptionsPage() {
|
||||
const merchantId = useSessionStore(state => state.merchantId)
|
||||
const [activeKind, setActiveKind] = useState<OptionKind>('delivery')
|
||||
const [searchForm] = Form.useForm<SearchValues>()
|
||||
const [editorForm] = Form.useForm<EditorValues>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [size, setSize] = useState(20)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [deliveryList, setDeliveryList] = useState<MerchantDeliveryOptionItem[]>([])
|
||||
const [packageList, setPackageList] = useState<MerchantPackageOptionItem[]>([])
|
||||
const [editingRecord, setEditingRecord] = useState<OptionRecord | null>(null)
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
|
||||
const currentMeta = KIND_META[activeKind]
|
||||
const currentList = useMemo(
|
||||
() => (activeKind === 'delivery' ? deliveryList : packageList),
|
||||
[activeKind, deliveryList, packageList]
|
||||
)
|
||||
|
||||
const loadList = async (kind = activeKind, nextPage = page, nextSize = size) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const values = searchForm.getFieldsValue()
|
||||
const payload = {
|
||||
merchantId: merchantId || undefined,
|
||||
status: values.status || undefined,
|
||||
page: nextPage,
|
||||
size: nextSize,
|
||||
}
|
||||
|
||||
if (kind === 'delivery') {
|
||||
const result = await merchantDeliveryOptionPage(payload)
|
||||
setDeliveryList(result.list ?? [])
|
||||
setTotal(result.total ?? 0)
|
||||
} else {
|
||||
const result = await merchantPackageOptionPage(payload)
|
||||
setPackageList(result.list ?? [])
|
||||
setTotal(result.total ?? 0)
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : `加载${KIND_META[kind].title}失败`
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
searchForm.setFieldsValue({ status: '' })
|
||||
setPage(1)
|
||||
void loadList(activeKind, 1, size)
|
||||
}, [activeKind, merchantId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
setEditingRecord(null)
|
||||
editorForm.setFieldsValue({
|
||||
optionName: '',
|
||||
optionCode: '',
|
||||
feeAmount: 0,
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 'enabled',
|
||||
})
|
||||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
const handleOpenEdit = (record: OptionRecord) => {
|
||||
setEditingRecord(record)
|
||||
editorForm.setFieldsValue({
|
||||
optionName: record.optionName,
|
||||
optionCode: getOptionCode(record, activeKind),
|
||||
feeAmount: Number(record.feeAmount || 0),
|
||||
description: record.description,
|
||||
sort: record.sort,
|
||||
status: record.status,
|
||||
})
|
||||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await editorForm.validateFields()
|
||||
setSaving(true)
|
||||
const basePayload = {
|
||||
merchantId: merchantId || undefined,
|
||||
id: editingRecord?.id,
|
||||
optionName: values.optionName.trim(),
|
||||
feeAmount: normalizeAmount(values.feeAmount),
|
||||
description: values.description?.trim(),
|
||||
sort: values.sort ?? 0,
|
||||
status: values.status,
|
||||
}
|
||||
|
||||
if (activeKind === 'delivery') {
|
||||
await merchantDeliveryOptionSave({
|
||||
...basePayload,
|
||||
deliveryCode: values.optionCode?.trim(),
|
||||
})
|
||||
} else {
|
||||
await merchantPackageOptionSave({
|
||||
...basePayload,
|
||||
packageCode: values.optionCode?.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
toast.success(editingRecord ? `${currentMeta.title}已更新` : `${currentMeta.title}已新增`)
|
||||
setEditorOpen(false)
|
||||
await loadList()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : `保存${currentMeta.title}失败`
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (record: OptionRecord) => {
|
||||
try {
|
||||
const payload = {
|
||||
merchantId: merchantId || undefined,
|
||||
id: record.id,
|
||||
}
|
||||
|
||||
if (activeKind === 'delivery') {
|
||||
await merchantDeliveryOptionDelete(payload)
|
||||
} else {
|
||||
await merchantPackageOptionDelete(payload)
|
||||
}
|
||||
|
||||
toast.success(`${currentMeta.title}已删除`)
|
||||
await loadList()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : `删除${currentMeta.title}失败`
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
||||
<Card
|
||||
className="page-card"
|
||||
title="物流和包装配置"
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenCreate}>
|
||||
{currentMeta.createText}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeKind}
|
||||
onChange={key => {
|
||||
setActiveKind(key as OptionKind)
|
||||
setPage(1)
|
||||
}}
|
||||
items={[
|
||||
{ key: 'delivery', label: '物流方式' },
|
||||
{ key: 'package', label: '包装袋' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form form={searchForm} layout="inline">
|
||||
<Form.Item name="status" label="状态">
|
||||
<Radio.Group
|
||||
options={ENABLE_STATUS_OPTIONS.map(item => ({ ...item }))}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPage(1)
|
||||
void loadList(activeKind, 1, size)
|
||||
}}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
searchForm.resetFields()
|
||||
setPage(1)
|
||||
void loadList(activeKind, 1, size)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card className="page-card" title={`${currentMeta.title}列表`}>
|
||||
<Table<OptionRecord>
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={currentList}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
onChange: (nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setSize(nextSize)
|
||||
void loadList(activeKind, nextPage, nextSize)
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'optionName',
|
||||
width: 220,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Typography.Text strong>{record.optionName}</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{getOptionCode(record, activeKind) || currentMeta.emptyCode}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '费用',
|
||||
dataIndex: 'feeAmount',
|
||||
width: 120,
|
||||
render: value => value || '0.00',
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
render: value => value || '-',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: value => <Tag color={getStatusColor(value)}>{getStatusLabel(value)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updatedAt',
|
||||
width: 170,
|
||||
render: value => formatUnixTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => handleOpenEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`删除${currentMeta.title}`}
|
||||
description="删除后下单页不再展示该配置,历史订单不受影响。"
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void handleDelete(record)}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingRecord ? `编辑${currentMeta.title}` : currentMeta.createText}
|
||||
open={editorOpen}
|
||||
confirmLoading={saving}
|
||||
onOk={() => void handleSave()}
|
||||
onCancel={() => setEditorOpen(false)}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={editorForm} layout="vertical" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="optionName"
|
||||
label="展示名称"
|
||||
rules={[{ required: true, message: `请输入${currentMeta.title}名称` }]}
|
||||
>
|
||||
<Input placeholder={activeKind === 'delivery' ? '例如 普通快递' : '例如 普通包装袋'} />
|
||||
</Form.Item>
|
||||
<Form.Item name="optionCode" label={currentMeta.codeLabel}>
|
||||
<Input placeholder={currentMeta.codePlaceholder} />
|
||||
</Form.Item>
|
||||
<Form.Item name="feeAmount" label="费用">
|
||||
<InputNumber min={0} precision={2} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={3} placeholder="可填写用户下单时需要看到的说明" />
|
||||
</Form.Item>
|
||||
<Space size={16} style={{ width: '100%' }}>
|
||||
<Form.Item name="sort" label="排序值" style={{ flex: 1 }}>
|
||||
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" style={{ flex: 1 }}>
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ label: '启用', value: 'enabled' },
|
||||
{ label: '停用', value: 'disabled' },
|
||||
]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import type {
|
||||
interface GoodsEditorDrawerProps {
|
||||
open: boolean
|
||||
merchantId: string
|
||||
spuId?: number | null
|
||||
spuId?: string | null
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
@@ -94,7 +94,7 @@ interface GoodsVariantSpecFormValue {
|
||||
}
|
||||
|
||||
interface GoodsSkuFormValue {
|
||||
skuId?: number
|
||||
skuId?: string
|
||||
skuNo: string
|
||||
skuName: string
|
||||
barcode?: string
|
||||
@@ -152,7 +152,7 @@ const emptyVariantSpec = (attribute?: MerchantCategoryAttributeItem): GoodsVaria
|
||||
})
|
||||
|
||||
const defaultSku = (): GoodsSkuFormValue => ({
|
||||
skuId: 0,
|
||||
skuId: '0',
|
||||
skuNo: '',
|
||||
skuName: '',
|
||||
barcode: '',
|
||||
@@ -581,7 +581,7 @@ const buildSkusFromVariantSpecs = (
|
||||
return {
|
||||
...defaultSku(),
|
||||
...previousSku,
|
||||
skuId: matchedSku?.skuId ?? 0,
|
||||
skuId: matchedSku?.skuId ?? '0',
|
||||
skuName: previousSku?.skuName?.trim() || specName || '默认规格',
|
||||
specs,
|
||||
}
|
||||
@@ -989,7 +989,7 @@ export default function GoodsEditorDrawer({
|
||||
setLoading(true)
|
||||
const result = await merchantGoodsDetail({
|
||||
merchantId,
|
||||
spuId,
|
||||
spuId: String(spuId),
|
||||
})
|
||||
|
||||
if (cancelled) {
|
||||
@@ -1005,7 +1005,7 @@ export default function GoodsEditorDrawer({
|
||||
const extraInfo = parseExtraInfo(data.extraInfoJson)
|
||||
const skus = data.skus.length
|
||||
? data.skus.map(sku => ({
|
||||
skuId: sku.id,
|
||||
skuId: String(sku.id || '0'),
|
||||
skuNo: sku.skuNo,
|
||||
skuName: sku.skuName,
|
||||
barcode: sku.barcode,
|
||||
@@ -1083,7 +1083,7 @@ export default function GoodsEditorDrawer({
|
||||
|
||||
await merchantGoodsSave({
|
||||
merchantId,
|
||||
spuId: spuId ?? 0,
|
||||
spuId: spuId ? String(spuId) : '0',
|
||||
categoryId: Number(values.categoryId),
|
||||
spuName: values.spuName.trim(),
|
||||
subTitle: values.subTitle?.trim() || undefined,
|
||||
@@ -1114,7 +1114,7 @@ export default function GoodsEditorDrawer({
|
||||
}
|
||||
|
||||
return {
|
||||
skuId: Number(sku.skuId ?? 0),
|
||||
skuId: sku.skuId ? String(sku.skuId) : '0',
|
||||
skuNo: sku.skuNo?.trim() ?? '',
|
||||
skuName: sku.skuName?.trim() || specs.map(spec => spec.valueName).join('-') || '默认规格',
|
||||
barcode: sku.barcode?.trim() ?? '',
|
||||
@@ -1423,7 +1423,7 @@ export default function GoodsEditorDrawer({
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item name={[field.name, 'skuId']} hidden>
|
||||
<InputNumber />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={[field.name, 'skuName']} hidden>
|
||||
<Input />
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function GoodsPage() {
|
||||
try {
|
||||
await merchantGoodsPublish({
|
||||
merchantId: merchantId ?? '',
|
||||
spuId: record.id,
|
||||
spuId: String(record.id),
|
||||
saleStatus: record.saleStatus === 'on' ? 'off' : 'on',
|
||||
})
|
||||
toast.success(`商品已${record.saleStatus === 'on' ? '下架' : '上架'}`)
|
||||
@@ -186,7 +186,7 @@ export default function GoodsPage() {
|
||||
|
||||
<Card className="page-card" title="商品列表">
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowKey={record => String(record.id)}
|
||||
loading={loading}
|
||||
dataSource={list}
|
||||
pagination={{
|
||||
@@ -313,7 +313,7 @@ export default function GoodsPage() {
|
||||
<GoodsEditorDrawer
|
||||
open={drawerOpen}
|
||||
merchantId={merchantId ?? ''}
|
||||
spuId={editingRecord?.id}
|
||||
spuId={editingRecord?.id ? String(editingRecord.id) : undefined}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
onSuccess={() => void loadList()}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Descriptions, Drawer, Space, Spin, Table, Tag, Typography } from 'antd'
|
||||
import { Card, Descriptions, Drawer, Image, Space, Spin, Table, Tag, Typography } from 'antd'
|
||||
import { toast } from 'sonner'
|
||||
import { merchantOrderDetail } from '@/apis/mallMerchant'
|
||||
import type { OrderDetailInfo } from '@/types/mall'
|
||||
import { formatUnixTime, getStatusColor, getStatusLabel, prettyJsonText } from '@/utils/mall'
|
||||
import { formatUnixTime, getStatusColor, getStatusLabel } from '@/utils/mall'
|
||||
|
||||
interface OrderDetailDrawerProps {
|
||||
open: boolean
|
||||
@@ -13,6 +13,49 @@ interface OrderDetailDrawerProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const normalizeSpecText = (value: unknown) => {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
const parseSkuSpecs = (value?: string) => {
|
||||
if (!value?.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown
|
||||
if (!Array.isArray(parsed)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return parsed
|
||||
.map(item => {
|
||||
const record = item as Record<string, unknown>
|
||||
const attributeName =
|
||||
normalizeSpecText(record.attributeName) || normalizeSpecText(record.name)
|
||||
const valueName =
|
||||
normalizeSpecText(record.valueName) || normalizeSpecText(record.value)
|
||||
|
||||
if (!attributeName && !valueName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { attributeName, valueName }
|
||||
})
|
||||
.filter((item): item is { attributeName: string; valueName: string } => Boolean(item))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export default function OrderDetailDrawer({
|
||||
open,
|
||||
merchantId,
|
||||
@@ -56,8 +99,10 @@ export default function OrderDetailDrawer({
|
||||
}
|
||||
}, [merchantId, open, orderId, refreshToken])
|
||||
|
||||
const drawerTitle = data?.orderNo ? `订单详情 ${data.orderNo}` : '订单详情'
|
||||
|
||||
return (
|
||||
<Drawer title={orderId ? `订单详情 #${orderId}` : '订单详情'} width={920} open={open} onClose={onClose}>
|
||||
<Drawer title={drawerTitle} width={1080} open={open} onClose={onClose}>
|
||||
<Spin spinning={loading}>
|
||||
{!data ? null : (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
@@ -65,9 +110,6 @@ export default function OrderDetailDrawer({
|
||||
<Descriptions column={2} size="small">
|
||||
<Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="交易号">{data.tradeNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="订单 ID">{data.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="会员 ID">{data.uid}</Descriptions.Item>
|
||||
<Descriptions.Item label="商家 ID">{data.merchantId}</Descriptions.Item>
|
||||
<Descriptions.Item label="商家名称">{data.merchantName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="订单状态">
|
||||
<Tag color={getStatusColor(data.orderStatus)}>{getStatusLabel(data.orderStatus)}</Tag>
|
||||
@@ -90,11 +132,6 @@ export default function OrderDetailDrawer({
|
||||
<Descriptions.Item label="支付渠道">{data.payChannel || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{formatUnixTime(data.createdAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{formatUnixTime(data.updatedAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label="扩展 JSON" span={2}>
|
||||
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{prettyJsonText(data.extraInfoJson) || '-'}
|
||||
</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
@@ -125,29 +162,66 @@ export default function OrderDetailDrawer({
|
||||
|
||||
<Card className="page-card" title="商品明细">
|
||||
<Table
|
||||
rowKey="id"
|
||||
rowKey={record => String(record.id)}
|
||||
pagination={false}
|
||||
scroll={{ x: 960 }}
|
||||
dataSource={data.items}
|
||||
columns={[
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'spuName',
|
||||
width: 280,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Typography.Text strong>{record.spuName}</Typography.Text>
|
||||
<Typography.Text type="secondary">{record.skuName}</Typography.Text>
|
||||
<div className="order-detail-product">
|
||||
{record.coverUrl ? (
|
||||
<Image
|
||||
src={record.coverUrl}
|
||||
alt={record.spuName}
|
||||
width={52}
|
||||
height={52}
|
||||
className="order-detail-product__image"
|
||||
preview={{ src: record.coverUrl }}
|
||||
/>
|
||||
) : (
|
||||
<div className="order-detail-product__placeholder">无图</div>
|
||||
)}
|
||||
<Space direction="vertical" size={2} className="order-detail-product__content">
|
||||
<Typography.Text
|
||||
strong
|
||||
className="order-detail-product__title"
|
||||
title={record.spuName}
|
||||
>
|
||||
{record.spuName}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis={{ tooltip: record.skuName }}>
|
||||
{record.skuName || '-'}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '规格 JSON',
|
||||
title: '规格',
|
||||
dataIndex: 'skuSpecJson',
|
||||
width: 260,
|
||||
render: value => (
|
||||
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{prettyJsonText(value) || '-'}
|
||||
</Typography.Text>
|
||||
),
|
||||
width: 220,
|
||||
render: (_, record) => {
|
||||
const specs = parseSkuSpecs(record.skuSpecJson)
|
||||
|
||||
if (!specs.length) {
|
||||
return record.skuName || '-'
|
||||
}
|
||||
|
||||
return (
|
||||
<Space size={[4, 4]} wrap>
|
||||
{specs.map(spec => (
|
||||
<Tag key={`${spec.attributeName}-${spec.valueName}`}>
|
||||
{spec.attributeName ? `${spec.attributeName}: ` : ''}
|
||||
{spec.valueName}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
|
||||
@@ -8,6 +8,7 @@ import LoginPage from '@/pages/login'
|
||||
import ProfilePage from '@/pages/profile'
|
||||
import GoodsPage from '@/pages/goods'
|
||||
import FeaturedGoodsPage from '@/pages/featuredGoods'
|
||||
import FulfillmentOptionsPage from '@/pages/fulfillmentOptions'
|
||||
import OrdersPage from '@/pages/orders'
|
||||
import AfterSalePage from '@/pages/afterSale'
|
||||
|
||||
@@ -44,6 +45,10 @@ export const routeConfig: RouteObject[] = [
|
||||
path: 'featured-goods',
|
||||
element: <FeaturedGoodsPage />,
|
||||
},
|
||||
{
|
||||
path: 'fulfillment-options',
|
||||
element: <FulfillmentOptionsPage />,
|
||||
},
|
||||
{
|
||||
path: 'orders',
|
||||
element: <OrdersPage />,
|
||||
|
||||
@@ -22,7 +22,7 @@ export type AfterSaleStatus =
|
||||
| 'closed'
|
||||
|
||||
export interface MerchantInfo {
|
||||
id: string | number
|
||||
id: string
|
||||
uid: number
|
||||
merchantNo: string
|
||||
merchantName: string
|
||||
@@ -53,8 +53,8 @@ export interface MerchantInfo {
|
||||
}
|
||||
|
||||
export interface GoodsSkuInfo {
|
||||
id: number
|
||||
spuId: number
|
||||
id: string
|
||||
spuId: string
|
||||
merchantId: string
|
||||
skuNo: string
|
||||
skuName: string
|
||||
@@ -74,7 +74,7 @@ export interface GoodsSkuInfo {
|
||||
}
|
||||
|
||||
export interface GoodsSummary {
|
||||
id: number
|
||||
id: string
|
||||
merchantId: string
|
||||
categoryId: number
|
||||
spuNo: string
|
||||
@@ -98,7 +98,7 @@ export interface GoodsSummary {
|
||||
}
|
||||
|
||||
export interface GoodsDetailInfo {
|
||||
id: number
|
||||
id: string
|
||||
merchantId: string
|
||||
categoryId: number
|
||||
spuNo: string
|
||||
@@ -197,8 +197,8 @@ export interface OrderItemInfo {
|
||||
orderId: number
|
||||
tradeId: number
|
||||
orderNo: string
|
||||
spuId: number
|
||||
skuId: number
|
||||
spuId: string
|
||||
skuId: string
|
||||
spuName: string
|
||||
skuName: string
|
||||
coverUrl: string
|
||||
@@ -303,7 +303,7 @@ export interface AfterSaleSummary {
|
||||
}
|
||||
|
||||
export interface MerchantGoodsSkuInput {
|
||||
skuId: number
|
||||
skuId: string
|
||||
skuNo: string
|
||||
skuName: string
|
||||
barcode: string
|
||||
@@ -361,7 +361,7 @@ export interface MerchantGoodsPageData {
|
||||
|
||||
export interface MerchantGoodsDetailParams {
|
||||
merchantId: string
|
||||
spuId: number
|
||||
spuId: string
|
||||
}
|
||||
|
||||
export interface MerchantGoodsDetailData {
|
||||
@@ -370,7 +370,7 @@ export interface MerchantGoodsDetailData {
|
||||
|
||||
export interface MerchantGoodsSaveParams {
|
||||
merchantId: string
|
||||
spuId?: number
|
||||
spuId?: string
|
||||
categoryId: number
|
||||
spuName: string
|
||||
subTitle?: string
|
||||
@@ -387,24 +387,24 @@ export interface MerchantGoodsSaveParams {
|
||||
}
|
||||
|
||||
export interface MerchantGoodsSaveData {
|
||||
spuId: number
|
||||
skuIds: number[]
|
||||
spuId: string
|
||||
skuIds: string[]
|
||||
}
|
||||
|
||||
export interface MerchantGoodsPublishParams {
|
||||
merchantId: string
|
||||
spuId: number
|
||||
spuId: string
|
||||
saleStatus: SaleStatus
|
||||
}
|
||||
|
||||
export interface MerchantGoodsPublishData {
|
||||
spuId: number
|
||||
spuId: string
|
||||
saleStatus: SaleStatus
|
||||
}
|
||||
|
||||
export interface MerchantFeaturedGoodsItem {
|
||||
id: number
|
||||
spuId: number
|
||||
id: string
|
||||
spuId: string
|
||||
spuNo: string
|
||||
spuName: string
|
||||
subTitle: string
|
||||
@@ -434,23 +434,99 @@ export interface MerchantFeaturedGoodsPageData {
|
||||
|
||||
export interface MerchantFeaturedGoodsSaveParams {
|
||||
merchantId: string
|
||||
id?: number
|
||||
spuId: number
|
||||
id?: string
|
||||
spuId: string
|
||||
sort?: number
|
||||
status?: EnableStatus
|
||||
}
|
||||
|
||||
export interface MerchantFeaturedGoodsSaveData {
|
||||
id: number
|
||||
spuId: number
|
||||
id: string
|
||||
spuId: string
|
||||
}
|
||||
|
||||
export interface MerchantFeaturedGoodsDeleteParams {
|
||||
merchantId: string
|
||||
id: number
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface MerchantFeaturedGoodsDeleteData {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface MerchantDeliveryOptionItem {
|
||||
id: number
|
||||
merchantId: string
|
||||
optionName: string
|
||||
deliveryCode: string
|
||||
feeAmount: string
|
||||
description: string
|
||||
sort: number
|
||||
status: EnableStatus
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
export interface MerchantPackageOptionItem {
|
||||
id: number
|
||||
merchantId: string
|
||||
optionName: string
|
||||
packageCode: string
|
||||
feeAmount: string
|
||||
description: string
|
||||
sort: number
|
||||
status: EnableStatus
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
export interface MerchantOptionPageParams extends PageRequest {
|
||||
merchantId?: string
|
||||
status?: EnableStatus
|
||||
}
|
||||
|
||||
export interface MerchantDeliveryOptionPageData {
|
||||
total: number
|
||||
list: MerchantDeliveryOptionItem[]
|
||||
}
|
||||
|
||||
export interface MerchantPackageOptionPageData {
|
||||
total: number
|
||||
list: MerchantPackageOptionItem[]
|
||||
}
|
||||
|
||||
export interface MerchantDeliveryOptionSaveParams {
|
||||
merchantId?: string
|
||||
id?: number
|
||||
optionName: string
|
||||
deliveryCode?: string
|
||||
feeAmount?: string
|
||||
description?: string
|
||||
sort?: number
|
||||
status?: EnableStatus
|
||||
}
|
||||
|
||||
export interface MerchantPackageOptionSaveParams {
|
||||
merchantId?: string
|
||||
id?: number
|
||||
optionName: string
|
||||
packageCode?: string
|
||||
feeAmount?: string
|
||||
description?: string
|
||||
sort?: number
|
||||
status?: EnableStatus
|
||||
}
|
||||
|
||||
export interface MerchantOptionSaveData {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface MerchantOptionDeleteParams {
|
||||
merchantId?: string
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface MerchantOptionDeleteData {
|
||||
id: number
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ const mapHqbSku = (
|
||||
const salePrice = toMoneyText(sku.price ?? goods.price ?? goods.yprice ?? goods.maxPrice)
|
||||
|
||||
return {
|
||||
skuId: 0,
|
||||
skuId: '0',
|
||||
skuNo: normalizeText(sku.skuCode || sku.id),
|
||||
skuName: buildSkuName(title, specs, sku.keyName),
|
||||
barcode: '',
|
||||
@@ -279,7 +279,7 @@ const mapHqbSku = (
|
||||
}
|
||||
|
||||
const buildDefaultSku = (goods: HqbGoodsInfo, title: string): MerchantGoodsSkuInput => ({
|
||||
skuId: 0,
|
||||
skuId: '0',
|
||||
skuNo: normalizeText(goods.goodsSn || goods.id),
|
||||
skuName: '默认 SKU',
|
||||
barcode: '',
|
||||
@@ -326,7 +326,7 @@ export const buildMallGoodsFromHqb = (
|
||||
|
||||
const payload: MerchantGoodsSaveParams = {
|
||||
merchantId,
|
||||
spuId: 0,
|
||||
spuId: '0',
|
||||
categoryId: HQB_TARGET_CATEGORY_ID,
|
||||
spuName: title,
|
||||
subTitle,
|
||||
|
||||
@@ -102,12 +102,17 @@ export const getStatusLabel = (value?: string) => {
|
||||
|
||||
export const getStatusColor = (value?: string) => STATUS_COLOR_MAP[value ?? ''] ?? 'default'
|
||||
|
||||
export const formatUnixTime = (value?: number) => {
|
||||
if (!value) {
|
||||
export const formatUnixTime = (value?: number | string | null) => {
|
||||
if (value === undefined || value === null) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
const timestamp = Number(typeof value === 'string' ? value.trim() : value)
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return dayjs.unix(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
export const prettyJsonText = (value?: string) => {
|
||||
|
||||
Reference in New Issue
Block a user