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 './category'
|
||||||
export * from './goods'
|
export * from './goods'
|
||||||
export * from './featuredGoods'
|
export * from './featuredGoods'
|
||||||
|
export * from './options'
|
||||||
export * from './orders'
|
export * from './orders'
|
||||||
export * from './afterSale'
|
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 { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { mallCategoryTree } from '@/apis/mallCategory'
|
import { merchantCategoryTree } from '@/apis/mallMerchant'
|
||||||
import type { MallCategoryItem } from '@/types/mall'
|
import type { MallCategoryItem } from '@/types/mall'
|
||||||
|
|
||||||
interface MallCategoryTreeNode {
|
interface MallCategoryTreeNode {
|
||||||
@@ -20,7 +20,7 @@ const loadMallCategoryTree = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!pendingCategoryTreeRequest) {
|
if (!pendingCategoryTreeRequest) {
|
||||||
pendingCategoryTreeRequest = mallCategoryTree({ status: 'enabled' })
|
pendingCategoryTreeRequest = merchantCategoryTree({ status: 'enabled' })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
cachedCategoryTree = data.list ?? []
|
cachedCategoryTree = data.list ?? []
|
||||||
return cachedCategoryTree
|
return cachedCategoryTree
|
||||||
|
|||||||
@@ -618,6 +618,64 @@ a {
|
|||||||
white-space: normal;
|
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 {
|
.goods-detail-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
|||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
|
InboxOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
@@ -48,6 +49,11 @@ const NAV_SECTIONS: NavSection[] = [
|
|||||||
label: '首页精选商品',
|
label: '首页精选商品',
|
||||||
icon: <StarOutlined />,
|
icon: <StarOutlined />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '/fulfillment-options',
|
||||||
|
label: '物流包装配置',
|
||||||
|
icon: <InboxOutlined />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 { toast } from 'sonner'
|
||||||
import { merchantAfterSalePage } from '@/apis/mallMerchant'
|
import { merchantAfterSalePage } from '@/apis/mallMerchant'
|
||||||
import AuditModal from './AuditModal'
|
import AuditModal from './AuditModal'
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
formatUnixTime,
|
formatUnixTime,
|
||||||
getStatusColor,
|
getStatusColor,
|
||||||
getStatusLabel,
|
getStatusLabel,
|
||||||
prettyJsonText,
|
|
||||||
} from '@/utils/mall'
|
} from '@/utils/mall'
|
||||||
|
|
||||||
interface AfterSaleSearchValues {
|
interface AfterSaleSearchValues {
|
||||||
@@ -20,6 +19,54 @@ interface AfterSaleSearchValues {
|
|||||||
keyword?: string
|
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() {
|
export default function AfterSalePage() {
|
||||||
const merchantId = useSessionStore(state => state.merchantId)
|
const merchantId = useSessionStore(state => state.merchantId)
|
||||||
const [form] = Form.useForm<AfterSaleSearchValues>()
|
const [form] = Form.useForm<AfterSaleSearchValues>()
|
||||||
@@ -115,29 +162,50 @@ export default function AfterSalePage() {
|
|||||||
|
|
||||||
<Card className="page-card" title="售后列表">
|
<Card className="page-card" title="售后列表">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey={record => String(record.id)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
expandable={{
|
expandable={{
|
||||||
expandedRowRender: record => (
|
expandedRowRender: record => {
|
||||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
const evidenceUrls = parseEvidenceUrls(record.evidenceJson)
|
||||||
<Typography.Text>售后单 ID:{record.id}</Typography.Text>
|
|
||||||
<Typography.Text>会员 ID:{record.uid}</Typography.Text>
|
return (
|
||||||
<Typography.Text>原因:{record.reason || '-'}</Typography.Text>
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
<Typography.Text>说明:{record.description || '-'}</Typography.Text>
|
<Typography.Text>原因:{record.reason || '-'}</Typography.Text>
|
||||||
<Typography.Text>驳回原因:{record.rejectReason || '-'}</Typography.Text>
|
<Typography.Text>说明:{record.description || '-'}</Typography.Text>
|
||||||
<Typography.Text>处理备注:{record.handleRemark || '-'}</Typography.Text>
|
<Typography.Text>驳回原因:{record.rejectReason || '-'}</Typography.Text>
|
||||||
<Typography.Text>审核时间:{formatUnixTime(record.auditTime)}</Typography.Text>
|
<Typography.Text>处理备注:{record.handleRemark || '-'}</Typography.Text>
|
||||||
<Typography.Text>完成时间:{formatUnixTime(record.successTime)}</Typography.Text>
|
<Typography.Text>审核时间:{formatUnixTime(record.auditTime)}</Typography.Text>
|
||||||
<Typography.Text>关闭时间:{formatUnixTime(record.closeTime)}</Typography.Text>
|
<Typography.Text>完成时间:{formatUnixTime(record.successTime)}</Typography.Text>
|
||||||
<div>
|
<Typography.Text>关闭时间:{formatUnixTime(record.closeTime)}</Typography.Text>
|
||||||
凭证 JSON:
|
<div className="after-sale-evidence">
|
||||||
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
<Typography.Text>凭证:</Typography.Text>
|
||||||
{prettyJsonText(record.evidenceJson) || '-'}
|
{evidenceUrls.length ? (
|
||||||
</Typography.Text>
|
<Space size={8} wrap>
|
||||||
</div>
|
{evidenceUrls.map(url =>
|
||||||
</Space>
|
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={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ interface GoodsPickerSearchValues {
|
|||||||
interface GoodsPickerModalProps {
|
interface GoodsPickerModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
merchantId: string
|
merchantId: string
|
||||||
configuredSpuIds: number[]
|
configuredSpuIds: string[]
|
||||||
currentSpuId?: number
|
currentSpuId?: string
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onSelect: (goods: GoodsSummary) => void
|
onSelect: (goods: GoodsSummary) => void
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export default function GoodsPickerModal({
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey={record => String(record.id)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
scroll={{ x: 1080 }}
|
scroll={{ x: 1080 }}
|
||||||
@@ -211,10 +211,10 @@ export default function GoodsPickerModal({
|
|||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
disabled={disabledSpuIds.has(record.id)}
|
disabled={disabledSpuIds.has(String(record.id))}
|
||||||
onClick={() => onSelect(record)}
|
onClick={() => onSelect(record)}
|
||||||
>
|
>
|
||||||
{disabledSpuIds.has(record.id) ? '已配置' : '选择'}
|
{disabledSpuIds.has(String(record.id)) ? '已配置' : '选择'}
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ interface EditorValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SelectedGoodsInfo {
|
interface SelectedGoodsInfo {
|
||||||
id: number
|
id: string
|
||||||
spuNo: string
|
spuNo: string
|
||||||
spuName: string
|
spuName: string
|
||||||
subTitle: string
|
subTitle: string
|
||||||
@@ -99,7 +99,7 @@ export default function FeaturedGoodsPage() {
|
|||||||
const [editingRecord, setEditingRecord] = useState<MerchantFeaturedGoodsItem | null>(null)
|
const [editingRecord, setEditingRecord] = useState<MerchantFeaturedGoodsItem | null>(null)
|
||||||
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsInfo | 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) => {
|
const loadList = async (nextPage = page, nextSize = size) => {
|
||||||
if (!merchantId) {
|
if (!merchantId) {
|
||||||
@@ -148,7 +148,7 @@ export default function FeaturedGoodsPage() {
|
|||||||
const handleOpenEdit = (record: MerchantFeaturedGoodsItem) => {
|
const handleOpenEdit = (record: MerchantFeaturedGoodsItem) => {
|
||||||
setEditingRecord(record)
|
setEditingRecord(record)
|
||||||
setSelectedGoods({
|
setSelectedGoods({
|
||||||
id: record.spuId,
|
id: String(record.spuId),
|
||||||
spuNo: record.spuNo,
|
spuNo: record.spuNo,
|
||||||
spuName: record.spuName,
|
spuName: record.spuName,
|
||||||
subTitle: record.subTitle,
|
subTitle: record.subTitle,
|
||||||
@@ -168,7 +168,7 @@ export default function FeaturedGoodsPage() {
|
|||||||
try {
|
try {
|
||||||
await merchantFeaturedGoodsDelete({
|
await merchantFeaturedGoodsDelete({
|
||||||
merchantId,
|
merchantId,
|
||||||
id: record.id,
|
id: String(record.id),
|
||||||
})
|
})
|
||||||
toast.success('精选商品已删除')
|
toast.success('精选商品已删除')
|
||||||
await loadList()
|
await loadList()
|
||||||
@@ -193,8 +193,8 @@ export default function FeaturedGoodsPage() {
|
|||||||
setSaving(true)
|
setSaving(true)
|
||||||
await merchantFeaturedGoodsSave({
|
await merchantFeaturedGoodsSave({
|
||||||
merchantId,
|
merchantId,
|
||||||
id: editingRecord?.id,
|
id: editingRecord?.id ? String(editingRecord.id) : undefined,
|
||||||
spuId: selectedGoods.id,
|
spuId: String(selectedGoods.id),
|
||||||
sort: values.sort ?? 0,
|
sort: values.sort ?? 0,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
})
|
})
|
||||||
@@ -447,11 +447,11 @@ export default function FeaturedGoodsPage() {
|
|||||||
open={pickerOpen}
|
open={pickerOpen}
|
||||||
merchantId={merchantId}
|
merchantId={merchantId}
|
||||||
configuredSpuIds={configuredSpuIds}
|
configuredSpuIds={configuredSpuIds}
|
||||||
currentSpuId={editingRecord?.spuId}
|
currentSpuId={editingRecord?.spuId ? String(editingRecord.spuId) : undefined}
|
||||||
onCancel={() => setPickerOpen(false)}
|
onCancel={() => setPickerOpen(false)}
|
||||||
onSelect={(goods: GoodsSummary) => {
|
onSelect={(goods: GoodsSummary) => {
|
||||||
setSelectedGoods({
|
setSelectedGoods({
|
||||||
id: goods.id,
|
id: String(goods.id),
|
||||||
spuNo: goods.spuNo,
|
spuNo: goods.spuNo,
|
||||||
spuName: goods.spuName,
|
spuName: goods.spuName,
|
||||||
subTitle: goods.subTitle,
|
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 {
|
interface GoodsEditorDrawerProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
merchantId: string
|
merchantId: string
|
||||||
spuId?: number | null
|
spuId?: string | null
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ interface GoodsVariantSpecFormValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface GoodsSkuFormValue {
|
interface GoodsSkuFormValue {
|
||||||
skuId?: number
|
skuId?: string
|
||||||
skuNo: string
|
skuNo: string
|
||||||
skuName: string
|
skuName: string
|
||||||
barcode?: string
|
barcode?: string
|
||||||
@@ -152,7 +152,7 @@ const emptyVariantSpec = (attribute?: MerchantCategoryAttributeItem): GoodsVaria
|
|||||||
})
|
})
|
||||||
|
|
||||||
const defaultSku = (): GoodsSkuFormValue => ({
|
const defaultSku = (): GoodsSkuFormValue => ({
|
||||||
skuId: 0,
|
skuId: '0',
|
||||||
skuNo: '',
|
skuNo: '',
|
||||||
skuName: '',
|
skuName: '',
|
||||||
barcode: '',
|
barcode: '',
|
||||||
@@ -581,7 +581,7 @@ const buildSkusFromVariantSpecs = (
|
|||||||
return {
|
return {
|
||||||
...defaultSku(),
|
...defaultSku(),
|
||||||
...previousSku,
|
...previousSku,
|
||||||
skuId: matchedSku?.skuId ?? 0,
|
skuId: matchedSku?.skuId ?? '0',
|
||||||
skuName: previousSku?.skuName?.trim() || specName || '默认规格',
|
skuName: previousSku?.skuName?.trim() || specName || '默认规格',
|
||||||
specs,
|
specs,
|
||||||
}
|
}
|
||||||
@@ -989,7 +989,7 @@ export default function GoodsEditorDrawer({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
const result = await merchantGoodsDetail({
|
const result = await merchantGoodsDetail({
|
||||||
merchantId,
|
merchantId,
|
||||||
spuId,
|
spuId: String(spuId),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -1005,7 +1005,7 @@ export default function GoodsEditorDrawer({
|
|||||||
const extraInfo = parseExtraInfo(data.extraInfoJson)
|
const extraInfo = parseExtraInfo(data.extraInfoJson)
|
||||||
const skus = data.skus.length
|
const skus = data.skus.length
|
||||||
? data.skus.map(sku => ({
|
? data.skus.map(sku => ({
|
||||||
skuId: sku.id,
|
skuId: String(sku.id || '0'),
|
||||||
skuNo: sku.skuNo,
|
skuNo: sku.skuNo,
|
||||||
skuName: sku.skuName,
|
skuName: sku.skuName,
|
||||||
barcode: sku.barcode,
|
barcode: sku.barcode,
|
||||||
@@ -1083,7 +1083,7 @@ export default function GoodsEditorDrawer({
|
|||||||
|
|
||||||
await merchantGoodsSave({
|
await merchantGoodsSave({
|
||||||
merchantId,
|
merchantId,
|
||||||
spuId: spuId ?? 0,
|
spuId: spuId ? String(spuId) : '0',
|
||||||
categoryId: Number(values.categoryId),
|
categoryId: Number(values.categoryId),
|
||||||
spuName: values.spuName.trim(),
|
spuName: values.spuName.trim(),
|
||||||
subTitle: values.subTitle?.trim() || undefined,
|
subTitle: values.subTitle?.trim() || undefined,
|
||||||
@@ -1114,7 +1114,7 @@ export default function GoodsEditorDrawer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skuId: Number(sku.skuId ?? 0),
|
skuId: sku.skuId ? String(sku.skuId) : '0',
|
||||||
skuNo: sku.skuNo?.trim() ?? '',
|
skuNo: sku.skuNo?.trim() ?? '',
|
||||||
skuName: sku.skuName?.trim() || specs.map(spec => spec.valueName).join('-') || '默认规格',
|
skuName: sku.skuName?.trim() || specs.map(spec => spec.valueName).join('-') || '默认规格',
|
||||||
barcode: sku.barcode?.trim() ?? '',
|
barcode: sku.barcode?.trim() ?? '',
|
||||||
@@ -1423,7 +1423,7 @@ export default function GoodsEditorDrawer({
|
|||||||
<tr key={field.key}>
|
<tr key={field.key}>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item name={[field.name, 'skuId']} hidden>
|
<Form.Item name={[field.name, 'skuId']} hidden>
|
||||||
<InputNumber />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name={[field.name, 'skuName']} hidden>
|
<Form.Item name={[field.name, 'skuName']} hidden>
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function GoodsPage() {
|
|||||||
try {
|
try {
|
||||||
await merchantGoodsPublish({
|
await merchantGoodsPublish({
|
||||||
merchantId: merchantId ?? '',
|
merchantId: merchantId ?? '',
|
||||||
spuId: record.id,
|
spuId: String(record.id),
|
||||||
saleStatus: record.saleStatus === 'on' ? 'off' : 'on',
|
saleStatus: record.saleStatus === 'on' ? 'off' : 'on',
|
||||||
})
|
})
|
||||||
toast.success(`商品已${record.saleStatus === 'on' ? '下架' : '上架'}`)
|
toast.success(`商品已${record.saleStatus === 'on' ? '下架' : '上架'}`)
|
||||||
@@ -186,7 +186,7 @@ export default function GoodsPage() {
|
|||||||
|
|
||||||
<Card className="page-card" title="商品列表">
|
<Card className="page-card" title="商品列表">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey={record => String(record.id)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
pagination={{
|
pagination={{
|
||||||
@@ -313,7 +313,7 @@ export default function GoodsPage() {
|
|||||||
<GoodsEditorDrawer
|
<GoodsEditorDrawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
merchantId={merchantId ?? ''}
|
merchantId={merchantId ?? ''}
|
||||||
spuId={editingRecord?.id}
|
spuId={editingRecord?.id ? String(editingRecord.id) : undefined}
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
onSuccess={() => void loadList()}
|
onSuccess={() => void loadList()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 { toast } from 'sonner'
|
||||||
import { merchantOrderDetail } from '@/apis/mallMerchant'
|
import { merchantOrderDetail } from '@/apis/mallMerchant'
|
||||||
import type { OrderDetailInfo } from '@/types/mall'
|
import type { OrderDetailInfo } from '@/types/mall'
|
||||||
import { formatUnixTime, getStatusColor, getStatusLabel, prettyJsonText } from '@/utils/mall'
|
import { formatUnixTime, getStatusColor, getStatusLabel } from '@/utils/mall'
|
||||||
|
|
||||||
interface OrderDetailDrawerProps {
|
interface OrderDetailDrawerProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -13,6 +13,49 @@ interface OrderDetailDrawerProps {
|
|||||||
onClose: () => void
|
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({
|
export default function OrderDetailDrawer({
|
||||||
open,
|
open,
|
||||||
merchantId,
|
merchantId,
|
||||||
@@ -56,8 +99,10 @@ export default function OrderDetailDrawer({
|
|||||||
}
|
}
|
||||||
}, [merchantId, open, orderId, refreshToken])
|
}, [merchantId, open, orderId, refreshToken])
|
||||||
|
|
||||||
|
const drawerTitle = data?.orderNo ? `订单详情 ${data.orderNo}` : '订单详情'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer title={orderId ? `订单详情 #${orderId}` : '订单详情'} width={920} open={open} onClose={onClose}>
|
<Drawer title={drawerTitle} width={1080} open={open} onClose={onClose}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{!data ? null : (
|
{!data ? null : (
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
@@ -65,9 +110,6 @@ export default function OrderDetailDrawer({
|
|||||||
<Descriptions column={2} size="small">
|
<Descriptions column={2} size="small">
|
||||||
<Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
|
<Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
|
||||||
<Descriptions.Item label="交易号">{data.tradeNo}</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="商家名称">{data.merchantName || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="订单状态">
|
<Descriptions.Item label="订单状态">
|
||||||
<Tag color={getStatusColor(data.orderStatus)}>{getStatusLabel(data.orderStatus)}</Tag>
|
<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="支付渠道">{data.payChannel || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建时间">{formatUnixTime(data.createdAt)}</Descriptions.Item>
|
<Descriptions.Item label="创建时间">{formatUnixTime(data.createdAt)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="更新时间">{formatUnixTime(data.updatedAt)}</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>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -125,29 +162,66 @@ export default function OrderDetailDrawer({
|
|||||||
|
|
||||||
<Card className="page-card" title="商品明细">
|
<Card className="page-card" title="商品明细">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey={record => String(record.id)}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
scroll={{ x: 960 }}
|
||||||
dataSource={data.items}
|
dataSource={data.items}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: '商品',
|
title: '商品',
|
||||||
dataIndex: 'spuName',
|
dataIndex: 'spuName',
|
||||||
|
width: 280,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space direction="vertical" size={2}>
|
<div className="order-detail-product">
|
||||||
<Typography.Text strong>{record.spuName}</Typography.Text>
|
{record.coverUrl ? (
|
||||||
<Typography.Text type="secondary">{record.skuName}</Typography.Text>
|
<Image
|
||||||
</Space>
|
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',
|
dataIndex: 'skuSpecJson',
|
||||||
width: 260,
|
width: 220,
|
||||||
render: value => (
|
render: (_, record) => {
|
||||||
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
const specs = parseSkuSpecs(record.skuSpecJson)
|
||||||
{prettyJsonText(value) || '-'}
|
|
||||||
</Typography.Text>
|
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: '数量',
|
title: '数量',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import LoginPage from '@/pages/login'
|
|||||||
import ProfilePage from '@/pages/profile'
|
import ProfilePage from '@/pages/profile'
|
||||||
import GoodsPage from '@/pages/goods'
|
import GoodsPage from '@/pages/goods'
|
||||||
import FeaturedGoodsPage from '@/pages/featuredGoods'
|
import FeaturedGoodsPage from '@/pages/featuredGoods'
|
||||||
|
import FulfillmentOptionsPage from '@/pages/fulfillmentOptions'
|
||||||
import OrdersPage from '@/pages/orders'
|
import OrdersPage from '@/pages/orders'
|
||||||
import AfterSalePage from '@/pages/afterSale'
|
import AfterSalePage from '@/pages/afterSale'
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ export const routeConfig: RouteObject[] = [
|
|||||||
path: 'featured-goods',
|
path: 'featured-goods',
|
||||||
element: <FeaturedGoodsPage />,
|
element: <FeaturedGoodsPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'fulfillment-options',
|
||||||
|
element: <FulfillmentOptionsPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'orders',
|
path: 'orders',
|
||||||
element: <OrdersPage />,
|
element: <OrdersPage />,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type AfterSaleStatus =
|
|||||||
| 'closed'
|
| 'closed'
|
||||||
|
|
||||||
export interface MerchantInfo {
|
export interface MerchantInfo {
|
||||||
id: string | number
|
id: string
|
||||||
uid: number
|
uid: number
|
||||||
merchantNo: string
|
merchantNo: string
|
||||||
merchantName: string
|
merchantName: string
|
||||||
@@ -53,8 +53,8 @@ export interface MerchantInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsSkuInfo {
|
export interface GoodsSkuInfo {
|
||||||
id: number
|
id: string
|
||||||
spuId: number
|
spuId: string
|
||||||
merchantId: string
|
merchantId: string
|
||||||
skuNo: string
|
skuNo: string
|
||||||
skuName: string
|
skuName: string
|
||||||
@@ -74,7 +74,7 @@ export interface GoodsSkuInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsSummary {
|
export interface GoodsSummary {
|
||||||
id: number
|
id: string
|
||||||
merchantId: string
|
merchantId: string
|
||||||
categoryId: number
|
categoryId: number
|
||||||
spuNo: string
|
spuNo: string
|
||||||
@@ -98,7 +98,7 @@ export interface GoodsSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsDetailInfo {
|
export interface GoodsDetailInfo {
|
||||||
id: number
|
id: string
|
||||||
merchantId: string
|
merchantId: string
|
||||||
categoryId: number
|
categoryId: number
|
||||||
spuNo: string
|
spuNo: string
|
||||||
@@ -197,8 +197,8 @@ export interface OrderItemInfo {
|
|||||||
orderId: number
|
orderId: number
|
||||||
tradeId: number
|
tradeId: number
|
||||||
orderNo: string
|
orderNo: string
|
||||||
spuId: number
|
spuId: string
|
||||||
skuId: number
|
skuId: string
|
||||||
spuName: string
|
spuName: string
|
||||||
skuName: string
|
skuName: string
|
||||||
coverUrl: string
|
coverUrl: string
|
||||||
@@ -303,7 +303,7 @@ export interface AfterSaleSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantGoodsSkuInput {
|
export interface MerchantGoodsSkuInput {
|
||||||
skuId: number
|
skuId: string
|
||||||
skuNo: string
|
skuNo: string
|
||||||
skuName: string
|
skuName: string
|
||||||
barcode: string
|
barcode: string
|
||||||
@@ -361,7 +361,7 @@ export interface MerchantGoodsPageData {
|
|||||||
|
|
||||||
export interface MerchantGoodsDetailParams {
|
export interface MerchantGoodsDetailParams {
|
||||||
merchantId: string
|
merchantId: string
|
||||||
spuId: number
|
spuId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantGoodsDetailData {
|
export interface MerchantGoodsDetailData {
|
||||||
@@ -370,7 +370,7 @@ export interface MerchantGoodsDetailData {
|
|||||||
|
|
||||||
export interface MerchantGoodsSaveParams {
|
export interface MerchantGoodsSaveParams {
|
||||||
merchantId: string
|
merchantId: string
|
||||||
spuId?: number
|
spuId?: string
|
||||||
categoryId: number
|
categoryId: number
|
||||||
spuName: string
|
spuName: string
|
||||||
subTitle?: string
|
subTitle?: string
|
||||||
@@ -387,24 +387,24 @@ export interface MerchantGoodsSaveParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantGoodsSaveData {
|
export interface MerchantGoodsSaveData {
|
||||||
spuId: number
|
spuId: string
|
||||||
skuIds: number[]
|
skuIds: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantGoodsPublishParams {
|
export interface MerchantGoodsPublishParams {
|
||||||
merchantId: string
|
merchantId: string
|
||||||
spuId: number
|
spuId: string
|
||||||
saleStatus: SaleStatus
|
saleStatus: SaleStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantGoodsPublishData {
|
export interface MerchantGoodsPublishData {
|
||||||
spuId: number
|
spuId: string
|
||||||
saleStatus: SaleStatus
|
saleStatus: SaleStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFeaturedGoodsItem {
|
export interface MerchantFeaturedGoodsItem {
|
||||||
id: number
|
id: string
|
||||||
spuId: number
|
spuId: string
|
||||||
spuNo: string
|
spuNo: string
|
||||||
spuName: string
|
spuName: string
|
||||||
subTitle: string
|
subTitle: string
|
||||||
@@ -434,23 +434,99 @@ export interface MerchantFeaturedGoodsPageData {
|
|||||||
|
|
||||||
export interface MerchantFeaturedGoodsSaveParams {
|
export interface MerchantFeaturedGoodsSaveParams {
|
||||||
merchantId: string
|
merchantId: string
|
||||||
id?: number
|
id?: string
|
||||||
spuId: number
|
spuId: string
|
||||||
sort?: number
|
sort?: number
|
||||||
status?: EnableStatus
|
status?: EnableStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFeaturedGoodsSaveData {
|
export interface MerchantFeaturedGoodsSaveData {
|
||||||
id: number
|
id: string
|
||||||
spuId: number
|
spuId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFeaturedGoodsDeleteParams {
|
export interface MerchantFeaturedGoodsDeleteParams {
|
||||||
merchantId: string
|
merchantId: string
|
||||||
id: number
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MerchantFeaturedGoodsDeleteData {
|
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
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ const mapHqbSku = (
|
|||||||
const salePrice = toMoneyText(sku.price ?? goods.price ?? goods.yprice ?? goods.maxPrice)
|
const salePrice = toMoneyText(sku.price ?? goods.price ?? goods.yprice ?? goods.maxPrice)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skuId: 0,
|
skuId: '0',
|
||||||
skuNo: normalizeText(sku.skuCode || sku.id),
|
skuNo: normalizeText(sku.skuCode || sku.id),
|
||||||
skuName: buildSkuName(title, specs, sku.keyName),
|
skuName: buildSkuName(title, specs, sku.keyName),
|
||||||
barcode: '',
|
barcode: '',
|
||||||
@@ -279,7 +279,7 @@ const mapHqbSku = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildDefaultSku = (goods: HqbGoodsInfo, title: string): MerchantGoodsSkuInput => ({
|
const buildDefaultSku = (goods: HqbGoodsInfo, title: string): MerchantGoodsSkuInput => ({
|
||||||
skuId: 0,
|
skuId: '0',
|
||||||
skuNo: normalizeText(goods.goodsSn || goods.id),
|
skuNo: normalizeText(goods.goodsSn || goods.id),
|
||||||
skuName: '默认 SKU',
|
skuName: '默认 SKU',
|
||||||
barcode: '',
|
barcode: '',
|
||||||
@@ -326,7 +326,7 @@ export const buildMallGoodsFromHqb = (
|
|||||||
|
|
||||||
const payload: MerchantGoodsSaveParams = {
|
const payload: MerchantGoodsSaveParams = {
|
||||||
merchantId,
|
merchantId,
|
||||||
spuId: 0,
|
spuId: '0',
|
||||||
categoryId: HQB_TARGET_CATEGORY_ID,
|
categoryId: HQB_TARGET_CATEGORY_ID,
|
||||||
spuName: title,
|
spuName: title,
|
||||||
subTitle,
|
subTitle,
|
||||||
|
|||||||
@@ -102,12 +102,17 @@ export const getStatusLabel = (value?: string) => {
|
|||||||
|
|
||||||
export const getStatusColor = (value?: string) => STATUS_COLOR_MAP[value ?? ''] ?? 'default'
|
export const getStatusColor = (value?: string) => STATUS_COLOR_MAP[value ?? ''] ?? 'default'
|
||||||
|
|
||||||
export const formatUnixTime = (value?: number) => {
|
export const formatUnixTime = (value?: number | string | null) => {
|
||||||
if (!value) {
|
if (value === undefined || value === null) {
|
||||||
return '-'
|
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) => {
|
export const prettyJsonText = (value?: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user