feat: 物流+评论
This commit is contained in:
parent
580cd25221
commit
f1f3b01081
@ -6,7 +6,15 @@ const order = {
|
|||||||
getOrderDetail: ['/order/detail'], // 获取订单详情
|
getOrderDetail: ['/order/detail'], // 获取订单详情
|
||||||
packOrder: ['/order/toShipping'], // 打包
|
packOrder: ['/order/toShipping'], // 打包
|
||||||
unpackOrder: ['/order/unpack'], // 取消打包
|
unpackOrder: ['/order/unpack'], // 取消打包
|
||||||
finishDeliver: ['/order/delivered'] // 妥投
|
finishDeliver: ['/order/delivered'], // 妥投
|
||||||
|
getLogisticsInfo: ['/logistics/query'], // 获取物流信息
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户评论
|
||||||
|
*/
|
||||||
|
getUserReviews: ['/comment/list'], // 获取用户评论
|
||||||
|
agreeComment: ['/comment/agree'], // 同意
|
||||||
|
rejectComment: ['/comment/reject'] // 拒绝
|
||||||
}
|
}
|
||||||
|
|
||||||
export default order
|
export default order
|
||||||
|
|||||||
@ -200,7 +200,7 @@
|
|||||||
<!-- 物流弹窗 -->
|
<!-- 物流弹窗 -->
|
||||||
<logistics-dialog
|
<logistics-dialog
|
||||||
v-model="logisticsDialogVisible"
|
v-model="logisticsDialogVisible"
|
||||||
:order-id="currentOrderId"
|
:track-number="curTrackNumber"
|
||||||
@close="handleCloseLogisticsDialog"
|
@close="handleCloseLogisticsDialog"
|
||||||
/>
|
/>
|
||||||
<!-- 打包 -->
|
<!-- 打包 -->
|
||||||
@ -412,11 +412,11 @@ const onButtonClick = (interfaceUri: string, packageData: PackageItem) => {
|
|||||||
}
|
}
|
||||||
// 物流弹窗相关
|
// 物流弹窗相关
|
||||||
const logisticsDialogVisible = ref(false)
|
const logisticsDialogVisible = ref(false)
|
||||||
const currentOrderId = ref<number>(NaN)
|
const curTrackNumber = ref<number>(NaN)
|
||||||
|
|
||||||
// 查看物流
|
// 查看物流
|
||||||
const handleViewLogistics = (packageData: PackageItem) => {
|
const handleViewLogistics = (packageData: PackageItem) => {
|
||||||
currentOrderId.value = packageData.trackNumber
|
curTrackNumber.value = packageData.trackNumber
|
||||||
logisticsDialogVisible.value = true
|
logisticsDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,7 +431,7 @@ const handlePack = (orderLines: OrderItem[]) => {
|
|||||||
// 关闭物流弹窗
|
// 关闭物流弹窗
|
||||||
const handleCloseLogisticsDialog = () => {
|
const handleCloseLogisticsDialog = () => {
|
||||||
logisticsDialogVisible.value = false
|
logisticsDialogVisible.value = false
|
||||||
currentOrderId.value = NaN
|
curTrackNumber.value = NaN
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderStatusMap = {
|
const orderStatusMap = {
|
||||||
|
|||||||
486
src/views/order/list/logistics-dialog.vue
Normal file
486
src/views/order/list/logistics-dialog.vue
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="物流跟踪"
|
||||||
|
width="800px"
|
||||||
|
:before-close="handleClose"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="logistics-dialog" v-loading="loading">
|
||||||
|
<!-- 状态时间线 -->
|
||||||
|
<div class="status-timeline">
|
||||||
|
<div
|
||||||
|
v-for="(stage, index) in statusStages"
|
||||||
|
:key="stage.key"
|
||||||
|
class="timeline-item"
|
||||||
|
:class="{ active: stage.active, completed: stage.completed }"
|
||||||
|
>
|
||||||
|
<div class="timeline-icon">
|
||||||
|
<el-icon v-if="stage.icon" :size="24">
|
||||||
|
<component :is="stage.icon" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-label">{{ stage.label }}</div>
|
||||||
|
<div v-if="index < statusStages.length - 1" class="timeline-line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物流跟踪记录 -->
|
||||||
|
<div class="tracking-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in trackingRecords"
|
||||||
|
:key="index"
|
||||||
|
class="tracking-item"
|
||||||
|
:class="{ 'is-first': index === 0 }"
|
||||||
|
>
|
||||||
|
<div class="tracking-time">{{ item.time }}</div>
|
||||||
|
<div class="tracking-content">
|
||||||
|
<div class="tracking-status">{{ item.status }}</div>
|
||||||
|
<div class="tracking-message">{{ item.context }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="index < trackingRecords.length - 1" class="tracking-line"></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && trackingRecords.length === 0" class="empty-tracking">
|
||||||
|
<el-empty description="暂无物流信息" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部信息区域 -->
|
||||||
|
<div class="bottom-info">
|
||||||
|
<!-- 物流信息 -->
|
||||||
|
<div class="logistics-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">第三方物流:</span>
|
||||||
|
<span class="info-value">{{ logisticsInfo.logisticsCompany || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">发货人:</span>
|
||||||
|
<span class="info-value"
|
||||||
|
>{{ logisticsInfo.deliveryMan || '-' }}:{{ logisticsInfo.deliveryPhone || '' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">派件人:</span>
|
||||||
|
<span class="info-value"
|
||||||
|
>{{ logisticsInfo.pickupMan || '-' }}:{{ logisticsInfo.pickupPhone || '' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 包裹信息图标 -->
|
||||||
|
<!-- <div
|
||||||
|
v-if="packageInfo && (packageInfo.dimensions || packageInfo.num)"
|
||||||
|
class="package-icon-wrapper"
|
||||||
|
>
|
||||||
|
<div class="package-icon">
|
||||||
|
<img src="" alt="package" />
|
||||||
|
</div>
|
||||||
|
<div class="package-text">
|
||||||
|
<template v-if="packageInfo.dimensions && packageInfo.num">
|
||||||
|
{{ formatPackageInfo(packageInfo.dimensions, packageInfo.num) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="packageInfo.dimensions">
|
||||||
|
{{ packageInfo.dimensions }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="packageInfo.num"> x {{ packageInfo.num }} </template>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { Box, Goods, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
interface LogisticsInfo {
|
||||||
|
logisticsCompany?: string
|
||||||
|
pickupMan?: string
|
||||||
|
pickupPhone?: string
|
||||||
|
deliveryMan?: string
|
||||||
|
deliveryPhone?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrackingRecord {
|
||||||
|
time: string
|
||||||
|
status: string
|
||||||
|
context: string
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
interface PackageInfo {
|
||||||
|
dimensions?: string
|
||||||
|
num?: string
|
||||||
|
} */
|
||||||
|
|
||||||
|
interface StatusStage {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon?: any
|
||||||
|
active: boolean
|
||||||
|
completed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
trackNumber: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 内部状态
|
||||||
|
const loading = ref(false)
|
||||||
|
// 物流信息
|
||||||
|
const logisticsInfo = ref<LogisticsInfo>({})
|
||||||
|
const trackingRecords = ref<TrackingRecord[]>([])
|
||||||
|
/* const packageInfo = ref<PackageInfo>({})
|
||||||
|
const currentStatus = ref<'' | 'pickup' | 'shipping' | 'delivered'>('') */
|
||||||
|
|
||||||
|
// 状态阶段
|
||||||
|
const statusStages = computed<StatusStage[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'pickup',
|
||||||
|
label: '提货',
|
||||||
|
icon: Box,
|
||||||
|
active: false,
|
||||||
|
completed: true
|
||||||
|
/* active: currentStatus.value === 'pickup',
|
||||||
|
completed: currentStatus.value === 'shipping' || currentStatus.value === 'delivered' */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shipping',
|
||||||
|
label: '运输中',
|
||||||
|
icon: Goods,
|
||||||
|
active: false,
|
||||||
|
completed: true
|
||||||
|
/* active: currentStatus.value === 'shipping',
|
||||||
|
completed: currentStatus.value === 'delivered' */
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delivered',
|
||||||
|
label: '交付',
|
||||||
|
icon: User,
|
||||||
|
active: false,
|
||||||
|
completed: true
|
||||||
|
/* active: currentStatus.value === 'delivered',
|
||||||
|
completed: false */
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取物流信息
|
||||||
|
const fetchLogisticsData = async (trackNumber: number) => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await api.order.getLogisticsInfo.post!<any>({ trackNumber }).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
const data = res.data
|
||||||
|
logisticsInfo.value = {
|
||||||
|
logisticsCompany: data.logisticsCompany,
|
||||||
|
pickupPhone: data.courierInfo.deliveryManPhone,
|
||||||
|
pickupMan: data.courierInfo.pickupManName,
|
||||||
|
deliveryPhone: data.courierInfo.pickupManPhone,
|
||||||
|
deliveryMan: data.courierInfo.deliveryManName
|
||||||
|
}
|
||||||
|
trackingRecords.value = data.data
|
||||||
|
/* packageInfo.value = { dimensions: '0.01 x 2', num: '2' }
|
||||||
|
currentStatus.value = 'shipping' */
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗打开和 trackNumber 变化
|
||||||
|
watch(
|
||||||
|
() => dialogVisible.value,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
fetchLogisticsData(props.trackNumber)
|
||||||
|
} else if (!visible) {
|
||||||
|
logisticsInfo.value = {
|
||||||
|
logisticsCompany: '',
|
||||||
|
pickupPhone: '',
|
||||||
|
pickupMan: '',
|
||||||
|
deliveryPhone: '',
|
||||||
|
deliveryMan: ''
|
||||||
|
}
|
||||||
|
trackingRecords.value = []
|
||||||
|
/* packageInfo.value = {}
|
||||||
|
currentStatus.value = 'pickup' */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.logistics-dialog {
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
.status-timeline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
// content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 10%;
|
||||||
|
right: 10%;
|
||||||
|
height: 2px;
|
||||||
|
background: #ebeef5;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.timeline-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 2px solid #dcdfe6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #909399;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.timeline-icon {
|
||||||
|
background: #e6f4ff;
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-label {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
.timeline-icon {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border-color: #67c23a;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-label {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: -50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #ebeef5;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
.timeline-item.active ~ &,
|
||||||
|
.timeline-item.completed ~ & {
|
||||||
|
background: #67c23a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 0 24px 0 16px;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
// 自定义滚动条样式
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c0c4cc;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox 滚动条样式
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #c0c4cc #f5f7fa;
|
||||||
|
|
||||||
|
.tracking-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 150px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
min-height: 60px;
|
||||||
|
|
||||||
|
&.is-first {
|
||||||
|
.tracking-time {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-status {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-time {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-content {
|
||||||
|
.tracking-status {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 35px;
|
||||||
|
top: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
.tracking-line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 130px;
|
||||||
|
top: 6px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #dcdfe6;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-first::before {
|
||||||
|
background: #409eff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tracking {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px 0 16px 40px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.logistics-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 60px;
|
||||||
|
|
||||||
|
.package-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -221,8 +221,8 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
await api.order.packOrder.post!(submitData)
|
await api.order.packOrder.post!(submitData)
|
||||||
ElMessage.success('打包成功')
|
ElMessage.success('打包成功')
|
||||||
|
tableData.value = tableData.value.filter((item) => !selectedRows.value.includes(item))
|
||||||
handleClose(false)
|
handleClose(false)
|
||||||
selectedRows.value = []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('打包失败')
|
ElMessage.error('打包失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
614
src/views/order/user-reviews/index.vue
Normal file
614
src/views/order/user-reviews/index.vue
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-reviews-page">
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<el-card class="filter-card" shadow="never">
|
||||||
|
<el-form :inline="true" class="filter-form">
|
||||||
|
<el-form-item label="商品id">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.productId"
|
||||||
|
placeholder="请输入商品ID"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.tradeOrderId"
|
||||||
|
placeholder="请输入订单ID"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="买家名称">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.buyerName"
|
||||||
|
placeholder="请输入买家名称"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filters.dateRange"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" @click="handleReset" style="margin-left: 8px">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 评论列表 -->
|
||||||
|
<el-card class="order-table-card" shadow="never">
|
||||||
|
<!-- INSERT_YOUR_CODE -->
|
||||||
|
<el-tabs
|
||||||
|
class="bg-white"
|
||||||
|
v-model="filters.status"
|
||||||
|
@tab-change="handleTabChange"
|
||||||
|
style="margin-bottom: 18px"
|
||||||
|
>
|
||||||
|
<el-tab-pane label="全部" name=""></el-tab-pane>
|
||||||
|
<el-tab-pane label="同意" name="approval_pass"></el-tab-pane>
|
||||||
|
<el-tab-pane label="拒绝" name="approval_not_pass"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<div class="order-table-header">
|
||||||
|
<div class="col goods">商品</div>
|
||||||
|
<div class="col total">评分</div>
|
||||||
|
<!-- <div class="col status">状态</div> -->
|
||||||
|
<div class="col actions">操作</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-list">
|
||||||
|
<div v-for="review in reviews" :key="review.id" class="order-item">
|
||||||
|
<div class="order-top">
|
||||||
|
<div class="top-left">
|
||||||
|
<el-checkbox v-model="review.checked" />
|
||||||
|
<span class="platform">订单:{{ review.tradeOrderId }}</span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<div class="platform-name">购de着</div>
|
||||||
|
<!-- <el-link :underline="false" type="primary">{{ order.summary }}</el-link> -->
|
||||||
|
</div>
|
||||||
|
<div class="top-right">
|
||||||
|
<span class="seller">{{ review.buyerName }}</span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span class="order-meta">创建时间:{{ review.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-content">
|
||||||
|
<!-- 左侧:评论详情 -->
|
||||||
|
<div class="review-left p-4 relative">
|
||||||
|
<div class="main-review">
|
||||||
|
<p class="mb-2">主评:{{ review.productComment }}</p>
|
||||||
|
<el-image
|
||||||
|
class="mr-2"
|
||||||
|
v-for="img in review.vvCommentDetailEntities"
|
||||||
|
:key="img.id"
|
||||||
|
style="width: 80px; height: 80px"
|
||||||
|
:src="img.commentUrl"
|
||||||
|
:preview-src-list="review.vvCommentDetailEntities.map((it) => it.commentUrl)"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="review-date text-xs">{{ review.modifyTime }}</div>
|
||||||
|
<div class="review-actions absolute bottom-2 right-2">
|
||||||
|
<span v-if="review.reason">拒绝原因:{{ review.reason }}</span>
|
||||||
|
<!-- <el-button link type="primary" size="small" @click="handleViewReplies(review)">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
{{ review.replyCount }} 回复
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" size="small" @click="handleReport(review)">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
举报
|
||||||
|
</el-button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 中间:评分区域 -->
|
||||||
|
<div class="review-middle p-3">
|
||||||
|
<div class="rating-item">
|
||||||
|
<span class="rating-label">总体评分:</span>
|
||||||
|
<el-rate
|
||||||
|
v-model="review.overallRating"
|
||||||
|
disabled
|
||||||
|
show-score
|
||||||
|
size="small"
|
||||||
|
text-color="#ff9900"
|
||||||
|
score-template="{value}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rating-item">
|
||||||
|
<span class="rating-label">商品分:</span>
|
||||||
|
<el-rate
|
||||||
|
v-model="review.descMatch"
|
||||||
|
disabled
|
||||||
|
show-score
|
||||||
|
size="small"
|
||||||
|
text-color="#ff9900"
|
||||||
|
score-template="{value}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rating-item">
|
||||||
|
<span class="rating-label">卖家服务评分:</span>
|
||||||
|
<el-rate
|
||||||
|
v-model="review.sellerService"
|
||||||
|
disabled
|
||||||
|
show-score
|
||||||
|
size="small"
|
||||||
|
text-color="#ff9900"
|
||||||
|
score-template="{value}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rating-item">
|
||||||
|
<span class="rating-label">物流评分:</span>
|
||||||
|
<el-rate
|
||||||
|
v-model="review.logisticsService"
|
||||||
|
disabled
|
||||||
|
show-score
|
||||||
|
size="small"
|
||||||
|
text-color="#ff9900"
|
||||||
|
score-template="{value}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 右侧:商品信息和操作 -->
|
||||||
|
<div class="review-right">
|
||||||
|
<div class="product-section p-3">
|
||||||
|
<el-image class="product-image" :src="review.productImage" fit="cover">
|
||||||
|
<template #error>
|
||||||
|
<div class="image-fallback">无图</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="product-tag">{{ review.productTitle }}</div>
|
||||||
|
<div class="product-name">{{ review.skuInfo }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<el-button
|
||||||
|
v-for="(action, i) in review.commentActionList"
|
||||||
|
:key="action.interfaceUri"
|
||||||
|
:type="i === 0 ? 'primary' : ''"
|
||||||
|
class="!ml-0 !mt-2"
|
||||||
|
@click="onButtonClick(action.interfaceUri, review)"
|
||||||
|
>{{ action.desc }}</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, jumper"
|
||||||
|
:page-size="20"
|
||||||
|
size="small"
|
||||||
|
:total="pagination.total"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-dialog v-model="dialogVisible" title="拒绝原因" width="500px">
|
||||||
|
<el-form :model="form" label-width="0">
|
||||||
|
<el-form-item label=" ">
|
||||||
|
<el-input v-model="form.reason" type="textarea" :rows="4" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="onRejectComment(form.reason)">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { Search, ChatDotRound, WarningFilled, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
// import order from '@/api/order'
|
||||||
|
// import request from '@/utils/request'
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
reason: '',
|
||||||
|
id: NaN
|
||||||
|
})
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
productId: '',
|
||||||
|
tradeOrderId: '',
|
||||||
|
buyerName: '',
|
||||||
|
dateRange: [],
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 评论数据
|
||||||
|
interface Review {
|
||||||
|
id: number
|
||||||
|
checked: boolean
|
||||||
|
tradeOrderId: string
|
||||||
|
buyerName: string
|
||||||
|
createTime: string
|
||||||
|
productComment: string
|
||||||
|
modifyTime: string
|
||||||
|
descMatch: number
|
||||||
|
sellerService: number
|
||||||
|
logisticsService: number
|
||||||
|
productImage: string
|
||||||
|
productTitle: string
|
||||||
|
skuInfo: string
|
||||||
|
reason: string
|
||||||
|
vvCommentDetailEntities: { id: string; commentUrl: string }[]
|
||||||
|
commentActionList: { desc: string; interfaceUri: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviews = ref<Review[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = async () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
await fetchReviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取评论列表
|
||||||
|
const fetchReviews = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await api.order.getUserReviews.post!<{ rows: Review[]; total: number }>({
|
||||||
|
currentPage: pagination.currentPage,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...filters,
|
||||||
|
minCreateTimestamp: filters.dateRange?.[0],
|
||||||
|
maxCreateTimestamp: filters.dateRange?.[1]
|
||||||
|
})
|
||||||
|
reviews.value = res.data.rows.map((item) => {
|
||||||
|
let skuInfo = ''
|
||||||
|
try {
|
||||||
|
skuInfo = JSON.parse(item.skuInfo)
|
||||||
|
.map((it: any) => `${it.propertyName}:${it.propertyValue}`)
|
||||||
|
.join(',')
|
||||||
|
} catch (error) {
|
||||||
|
skuInfo = item.skuInfo
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
skuInfo,
|
||||||
|
createTime: new Date(item.createTime).toLocaleString(),
|
||||||
|
modifyTime: new Date(item.modifyTime).toLocaleString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pagination.total = res.data.total
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onButtonClick = (interfaceUri: string, review: Review) => {
|
||||||
|
if (interfaceUri === '/mm/comment/agree') {
|
||||||
|
onAgreeComment(review)
|
||||||
|
} else if (interfaceUri === '/app/comment/reject') {
|
||||||
|
form.reason = ''
|
||||||
|
form.id = review.id
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同意
|
||||||
|
const onAgreeComment = (review: Review) => {
|
||||||
|
api.order.agreeComment.post!({
|
||||||
|
id: review.id
|
||||||
|
}).then(() => {
|
||||||
|
ElMessage.success('同意成功')
|
||||||
|
fetchReviews()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝
|
||||||
|
const onRejectComment = (review: Review) => {
|
||||||
|
api.order.rejectComment.post!(form).then(() => {
|
||||||
|
ElMessage.success('拒绝成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchReviews()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTabChange = () => {
|
||||||
|
fetchReviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小改变
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
pagination.currentPage = 1
|
||||||
|
fetchReviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页改变
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
pagination.currentPage = page
|
||||||
|
fetchReviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
filters.productId = ''
|
||||||
|
filters.tradeOrderId = ''
|
||||||
|
filters.buyerName = ''
|
||||||
|
filters.dateRange = []
|
||||||
|
fetchReviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchReviews()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.user-reviews-page {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 84px);
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.filter-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-reviews-page {
|
||||||
|
.review-card {
|
||||||
|
.review-content {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.review-middle {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.order-table-card {
|
||||||
|
padding: 0 0 16px;
|
||||||
|
|
||||||
|
.order-table-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(400px, 1fr) 240px 300px 150px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #f9fafc;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.col:first-child {
|
||||||
|
padding-left: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
overflow-x: scroll;
|
||||||
|
|
||||||
|
.order-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
.top-left,
|
||||||
|
.top-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(400px, 1fr) 240px 300px 150px;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
.goods {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.goods-item + .goods-item {
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
border-right: 1px solid #f0f2f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.goods-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remark {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.delivery-type {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-warehouse {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-detail {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user