feat(jd-order): 添加商品品类中文映射和订单历史记录功能

- 新增商品品类中文映射工具,支持Apple Card、携程礼品卡等品类
- 实现京东订单变更记录侧边栏,展示订单状态变更时间线
- 优化订单详情页,添加卡号和卡密显示功能
- 移除Cookie历史记录中的状态变更和记录ID信息
- 调整支付链接展示方式,统一使用"打开支付链接"文本
- 重构订单历史记录组件,支持分页和多种变更类型图标展示
- 修复订单历史记录组件的API调用逻辑和数据类型定义
- 更新订单状态变更类型,将bind/unbind替换为rebind等新类型
This commit is contained in:
danial
2025-10-13 23:16:04 +08:00
parent 520c4b0853
commit dd45db1bcf
7 changed files with 404 additions and 156 deletions

View File

@@ -0,0 +1,23 @@
/**
* 商品品类中文映射工具
*/
// 商品品类映射表
export const CATEGORY_MAPPING: Record<string, string> = {
apple: 'Apple Card',
cTrip: '携程礼品卡',
walmart: '沃尔玛礼品卡'
};
/**
* 获取商品品类的中文名称
* @param category 原始品类名称
* @returns 中文名称,如果找不到映射则返回原名称
*/
export function getCategoryChineseName(category?: string): string {
if (!category) {
return '-';
}
return CATEGORY_MAPPING[category] || category;
}

View File

@@ -32,34 +32,6 @@
</div>
</div>
<!-- 状态变更信息 -->
<div
v-if="
item.statusBefore !== undefined &&
item.statusAfter !== undefined
"
class="status-change"
>
<div class="status-info">
<span class="label">状态变更:</span>
<a-tag
:color="getStatusColor(item.statusBefore)"
size="small"
class="status-before"
>
{{ getStatusText(item.statusBefore) }}
</a-tag>
<icon-right class="arrow-icon" />
<a-tag
:color="getStatusColor(item.statusAfter)"
size="small"
class="status-after"
>
{{ getStatusText(item.statusAfter) }}
</a-tag>
</div>
</div>
<!-- 失败次数 -->
<div
v-if="item.failureCount && item.failureCount > 0"
@@ -80,18 +52,6 @@
class="order-id"
/>
</div>
<!-- 历史记录ID -->
<div class="history-id">
<span class="label">记录ID</span>
<a-typography-text
copyable
:content="item.historyUuid || ''"
class="uuid-text"
type="secondary"
size="small"
/>
</div>
</div>
</a-timeline-item>
</a-timeline>
@@ -112,7 +72,6 @@
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { IconRight } from '@arco-design/web-vue/es/icon';
import { jdHistoryClient } from '@/api';
import type { KamiApiJdCookieV1CookieHistoryInfo } from '@/api/generated';
import { KamiApiJdCookieV1CookieHistoryInfoChangeTypeEnum } from '@/api/generated';
@@ -249,34 +208,6 @@ const getChangeTypeText = (
}
};
// 获取状态颜色
const getStatusColor = (status?: number): string => {
switch (status) {
case 1:
return 'green';
case 2:
return 'orange';
case 3:
return 'red';
default:
return 'gray';
}
};
// 获取状态文本
const getStatusText = (status?: number): string => {
switch (status) {
case 1:
return '正常';
case 2:
return '暂停';
case 3:
return '失效';
default:
return '未知';
}
};
// 获取变更记录数据
const fetchHistoryData = async (isLoadMore = false): Promise<void> => {
if (!props.cookieId) return;
@@ -370,24 +301,8 @@ watch(
color: var(--color-text-1);
}
.status-change {
margin: 8px 0;
}
.status-info {
display: flex;
align-items: center;
gap: 8px;
}
.arrow-icon {
color: var(--color-text-3);
font-size: 16px;
}
.failure-info,
.order-info,
.history-id {
.order-info {
margin: 4px 0;
display: flex;
align-items: center;
@@ -405,11 +320,6 @@ watch(
font-size: 13px;
}
.uuid-text {
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
font-size: 12px;
}
.load-more {
text-align: center;
margin-top: 24px;

View File

@@ -0,0 +1,308 @@
<template>
<a-drawer
v-model:visible="visible"
:title="drawerTitle"
width="600px"
:footer="false"
unmount-on-close
>
<div v-loading="loading" class="order-history-drawer">
<div v-if="!loading && historyList.length === 0" class="empty-state">
<a-empty description="暂无变更记录" />
</div>
<a-timeline v-else class="history-timeline">
<a-timeline-item
v-for="(item, index) in historyList"
:key="item.historyUuid || index"
:dot-color="getTimelineDotColor(item.changeType)"
:line-type="index === historyList.length - 1 ? 'dashed' : 'solid'"
>
<template #dot>
<icon-plus
v-if="item.changeType === 'create'"
:style="{ color: '#00b42a', fontSize: '16px' }"
/>
<icon-link
v-else-if="item.changeType === 'bind'"
:style="{ color: '#165dff', fontSize: '16px' }"
/>
<icon-close
v-else-if="item.changeType === 'unbind'"
:style="{ color: '#ff7d00', fontSize: '16px' }"
/>
<icon-check-circle
v-else-if="item.changeType === 'pay'"
:style="{ color: '#00b42a', fontSize: '16px' }"
/>
<icon-clock-circle
v-else-if="item.changeType === 'expire'"
:style="{ color: '#f53f3f', fontSize: '16px' }"
/>
<icon-close-circle
v-else-if="item.changeType === 'invalid'"
:style="{ color: '#86909c', fontSize: '16px' }"
/>
<icon-sync
v-else-if="item.changeType === 'replace'"
:style="{ color: '#722ed1', fontSize: '16px' }"
/>
<icon-more v-else :style="{ color: '#86909c', fontSize: '16px' }" />
</template>
<div class="timeline-content">
<div class="timeline-header">
<a-tag :color="getStatusColor(item.changeType)" size="small">
{{ getStatusText(item.changeType) }}
</a-tag>
<span class="timeline-time">
{{ formatTime(item.createdAt) }}
</span>
</div>
<div class="timeline-details">
<div v-if="item.orderId" class="detail-item">
<span class="detail-label">订单号:</span>
<a-typography-text copyable>
{{ item.orderId }}
</a-typography-text>
</div>
<div v-if="item.jdOrderId" class="detail-item">
<span class="detail-label">京东订单号:</span>
<a-typography-text copyable>
{{ item.jdOrderId }}
</a-typography-text>
</div>
</div>
</div>
</a-timeline-item>
</a-timeline>
<!-- 分页 -->
<div v-if="total > pageSize" class="pagination-wrapper">
<a-pagination
:current="currentPage"
:page-size="pageSize"
:total="total"
:show-total="true"
:show-jumper="true"
:show-page-size="true"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import {
IconPlus,
IconLink,
IconClose,
IconCheckCircle,
IconClockCircle,
IconCloseCircle,
IconSync,
IconMore
} from '@arco-design/web-vue/es/icon';
import { jdOrderClient } from '@/api';
import type { KamiApiJdCookieV1JdOrderHistoryInfo } from '@/api/generated';
interface Props {
visible: boolean;
jdOrderId?: string;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
jdOrderId: ''
});
const emit = defineEmits<Emits>();
// 数据状态
const loading = ref(false);
const historyList = ref<KamiApiJdCookieV1JdOrderHistoryInfo[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
// 计算属性
const visible = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
});
const drawerTitle = computed(() => {
return props.jdOrderId
? `京东订单变更记录 - ${props.jdOrderId}`
: '京东订单变更记录';
});
// 获取变更记录
const fetchHistory = async () => {
if (!props.jdOrderId) return;
loading.value = true;
try {
const response = await jdOrderClient.apiJdCookieOrderJdOrderHistoryGet({
jdOrderId: props.jdOrderId,
page: currentPage.value,
size: pageSize.value
});
historyList.value = response.data.list || [];
total.value = response.data.total || 0;
} catch (error) {
Message.error('获取变更记录失败');
historyList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 分页处理
const onPageChange = (page: number) => {
currentPage.value = page;
fetchHistory();
};
const onPageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1;
fetchHistory();
};
// 获取状态颜色
const getStatusColor = (changeType?: string): string => {
const colorMap: Record<string, string> = {
create: 'green', // 创建
bind: 'blue', // 绑定
unbind: 'orange', // 解绑
pay: 'green', // 支付
expire: 'red', // 过期
invalid: 'gray', // 失效
replace: 'purple' // 替换
};
return colorMap[changeType || ''] || 'gray';
};
// 获取状态文本
const getStatusText = (changeType?: string): string => {
const textMap: Record<string, string> = {
create: '创建订单',
bind: '绑定订单',
unbind: '解绑订单',
pay: '支付完成',
expire: '订单过期',
invalid: '订单失效',
replace: '替换订单'
};
return textMap[changeType || ''] || '未知状态';
};
// 获取时间线点颜色
const getTimelineDotColor = (changeType?: string): string => {
const colorMap: Record<string, string> = {
create: '#00b42a', // 创建
bind: '#165dff', // 绑定
unbind: '#ff7d00', // 解绑
pay: '#00b42a', // 支付
expire: '#f53f3f', // 过期
invalid: '#86909c', // 失效
replace: '#722ed1' // 替换
};
return colorMap[changeType || ''] || '#86909c';
};
// 格式化时间
const formatTime = (time?: string): string => {
if (!time) return '-';
return new Date(time).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
// 监听visible变化自动重置分页并获取数据
watch(
() => props.visible,
newVal => {
if (newVal && props.jdOrderId) {
currentPage.value = 1;
pageSize.value = 20;
fetchHistory();
}
}
);
</script>
<style scoped>
.order-history-drawer {
min-height: 400px;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.history-timeline {
margin-top: 16px;
}
.timeline-content {
padding-left: 8px;
}
.timeline-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.timeline-time {
font-size: 12px;
color: var(--color-text-3);
}
.timeline-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.detail-label {
color: var(--color-text-3);
min-width: 80px;
}
.pagination-wrapper {
margin-top: 24px;
display: flex;
justify-content: center;
}
</style>

View File

@@ -105,12 +105,7 @@
<template #icon>
<icon-link />
</template>
<a-tooltip :content="record.wxPayUrl" position="top">
<span>
{{ record.wxPayUrl.slice(0, 20)
}}{{ record.wxPayUrl.length > 20 ? '...' : '' }}
</span>
</a-tooltip>
打开支付链接
</a-link>
<a-typography-text v-else type="secondary"></a-typography-text>
</a-space>
@@ -170,7 +165,7 @@
<a-typography-text v-else type="secondary"></a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="商品品类">
{{ currentOrder.category || '-' }}
{{ getCategoryChineseName(currentOrder.category) }}
</a-descriptions-item>
<a-descriptions-item label="订单金额">
<span class="arco-text-primary">
@@ -203,6 +198,18 @@
<a-descriptions-item label="当前关联订单ID">
{{ currentOrder.currentOrderId || '-' }}
</a-descriptions-item>
<a-descriptions-item label="卡号">
<a-typography-text v-if="currentOrder.cardNo" copyable>
{{ currentOrder.cardNo }}
</a-typography-text>
<a-typography-text v-else type="secondary"></a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="卡密">
<a-typography-text v-if="currentOrder.cardPassword" copyable>
{{ currentOrder.cardPassword }}
</a-typography-text>
<a-typography-text v-else type="secondary"></a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="支付完成时间">
{{ currentOrder.paidAt || '-' }}
</a-descriptions-item>
@@ -225,7 +232,7 @@
<!-- 变更记录侧边栏 -->
<order-history-drawer
v-model:visible="historyVisible"
:order-id="currentOrderId"
:jd-order-id="currentOrderId"
/>
</div>
</template>
@@ -238,7 +245,8 @@ import useLoading from '@/hooks/loading';
import { Pagination } from '@/types/global';
import { jdOrderClient } from '@/api';
import type { KamiApiJdCookieV1JdOrderInfo } from '@/api/generated';
import OrderHistoryDrawer from '../components/order-history-drawer.vue';
import { getCategoryChineseName } from '@/utils/categoryMapping';
import OrderHistoryDrawer from './components/jd-order-history-drawer.vue';
const { loading, setLoading } = useLoading(true);
@@ -338,11 +346,41 @@ const columns: TableColumnData[] = [
slotName: 'status',
width: 100
},
{
title: '当前关联订单ID',
dataIndex: 'currentOrderId',
width: 140,
ellipsis: true,
tooltip: true,
render: ({ record }) => {
return record.currentOrderId || '-';
}
},
{
title: '卡号',
dataIndex: 'cardNo',
width: 160,
ellipsis: true,
tooltip: true,
render: ({ record }) => {
return record.cardNo || '-';
}
},
{
title: '卡密',
dataIndex: 'cardPassword',
width: 160,
ellipsis: true,
tooltip: true,
render: ({ record }) => {
return record.cardPassword || '-';
}
},
{
title: '支付链接',
dataIndex: 'wxPayUrl',
slotName: 'wxPayUrl',
width: 120
width: 140
},
{
title: '支付完成时间',

View File

@@ -20,7 +20,7 @@
<a-typography-text v-else type="secondary"></a-typography-text>
</a-descriptions-item>
<a-descriptions-item label="商品品类">
{{ orderData.category || '-' }}
{{ getCategoryChineseName(orderData.category) }}
</a-descriptions-item>
<a-descriptions-item label="订单金额">
<a-typography-text class="arco-text-primary">
@@ -80,6 +80,7 @@
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import type { KamiApiJdCookieV1OrderInfo } from '@/api/generated';
import { getCategoryChineseName } from '@/utils/categoryMapping';
interface Props {
visible: boolean;

View File

@@ -24,13 +24,9 @@
:style="{ color: '#00b42a', fontSize: '16px' }"
/>
<icon-link
v-else-if="item.changeType === 'bind'"
v-else-if="item.changeType === 'rebind'"
:style="{ color: '#165dff', fontSize: '16px' }"
/>
<icon-close
v-else-if="item.changeType === 'unbind'"
:style="{ color: '#ff7d00', fontSize: '16px' }"
/>
<icon-check-circle
v-else-if="item.changeType === 'pay'"
:style="{ color: '#00b42a', fontSize: '16px' }"
@@ -39,14 +35,6 @@
v-else-if="item.changeType === 'expire'"
:style="{ color: '#f53f3f', fontSize: '16px' }"
/>
<icon-close-circle
v-else-if="item.changeType === 'invalid'"
:style="{ color: '#86909c', fontSize: '16px' }"
/>
<icon-sync
v-else-if="item.changeType === 'replace'"
:style="{ color: '#722ed1', fontSize: '16px' }"
/>
<icon-more v-else :style="{ color: '#86909c', fontSize: '16px' }" />
</template>
@@ -74,16 +62,6 @@
{{ item.jdOrderId }}
</a-typography-text>
</div>
<div v-if="item.wxPayUrl" class="detail-item">
<span class="detail-label">支付链接:</span>
<a-link :href="item.wxPayUrl" target="_blank" type="primary">
<template #icon>
<icon-link />
</template>
查看支付链接
</a-link>
</div>
</div>
</div>
</a-timeline-item>
@@ -119,13 +97,12 @@ import {
IconSync,
IconMore
} from '@arco-design/web-vue/es/icon';
import { jdOrderClient } from '@/api';
import type { KamiApiJdCookieV1JdOrderHistoryInfo } from '@/api/generated';
import { jdHistoryClient } from '@/api';
import type { KamiApiJdCookieV1OrderHistoryInfo } from '@/api/generated';
interface Props {
visible: boolean;
orderId?: string;
jdOrderId?: string;
}
interface Emits {
@@ -134,15 +111,14 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
visible: false,
orderId: '',
jdOrderId: ''
orderId: ''
});
const emit = defineEmits<Emits>();
//
const loading = ref(false);
const historyList = ref<KamiApiJdCookieV1JdOrderHistoryInfo[]>([]);
const historyList = ref<KamiApiJdCookieV1OrderHistoryInfo[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
@@ -154,19 +130,17 @@ const visible = computed({
});
const drawerTitle = computed(() => {
const id = props.jdOrderId || props.orderId;
return id ? `订单变更记录 - ${id}` : '订单变更记录';
return props.orderId ? `订单变更记录 - ${props.orderId}` : '订单变更记录';
});
//
const fetchHistory = async () => {
const id = props.jdOrderId || props.orderId;
if (!id) return;
if (!props.orderId) return;
loading.value = true;
try {
const response = await jdOrderClient.apiJdCookieOrderJdOrderHistoryGet({
jdOrderId: id,
const response = await jdHistoryClient.apiJdCookieHistoryOrderGet({
orderId: props.orderId,
page: currentPage.value,
size: pageSize.value
});
@@ -198,12 +172,9 @@ const onPageSizeChange = (size: number) => {
const getStatusColor = (changeType?: string): string => {
const colorMap: Record<string, string> = {
create: 'green', //
bind: 'blue', //
unbind: 'orange', //
rebind: 'blue', //
pay: 'green', //
expire: 'red', //
invalid: 'gray', //
replace: 'purple' //
expire: 'red' //
};
return colorMap[changeType || ''] || 'gray';
};
@@ -212,12 +183,9 @@ const getStatusColor = (changeType?: string): string => {
const getStatusText = (changeType?: string): string => {
const textMap: Record<string, string> = {
create: '创建订单',
bind: '绑定订单',
unbind: '解绑订单',
rebind: '重新绑定',
pay: '支付完成',
expire: '订单过期',
invalid: '订单失效',
replace: '替换订单'
expire: '订单过期'
};
return textMap[changeType || ''] || '未知状态';
};
@@ -226,12 +194,9 @@ const getStatusText = (changeType?: string): string => {
const getTimelineDotColor = (changeType?: string): string => {
const colorMap: Record<string, string> = {
create: '#00b42a', //
bind: '#165dff', //
unbind: '#ff7d00', //
rebind: '#165dff', //
pay: '#00b42a', //
expire: '#f53f3f', //
invalid: '#86909c', //
replace: '#722ed1' //
expire: '#f53f3f' //
};
return colorMap[changeType || ''] || '#86909c';
};
@@ -253,8 +218,7 @@ const formatTime = (time?: string): string => {
watch(
() => props.visible,
newVal => {
const id = props.jdOrderId || props.orderId;
if (newVal && id) {
if (newVal && props.orderId) {
currentPage.value = 1;
pageSize.value = 20;
fetchHistory();

View File

@@ -134,8 +134,9 @@ import type {
KamiApiJdCookieV1ListOrderRes,
KamiApiJdCookieV1OrderInfo
} from '@/api/generated';
import { getCategoryChineseName } from '@/utils/categoryMapping';
import OrderDetail from './components/order-detail.vue';
import OrderHistoryDrawer from '../components/order-history-drawer.vue';
import OrderHistoryDrawer from './components/order-history-drawer.vue';
const { loading, setLoading } = useLoading(true);
@@ -202,7 +203,10 @@ const columns: TableColumnData[] = [
{
title: '商品品类',
dataIndex: 'category',
width: 120
width: 120,
render: ({ record }) => {
return getCategoryChineseName(record.category);
}
},
{
title: '订单金额',