|
|
@@ -1,15 +1,35 @@
|
|
|
<script setup lang="ts">
|
|
|
import type { SystemCwModel } from '@/model/care.model';
|
|
|
import { PlayCircleOutlined } from '@ant-design/icons-vue';
|
|
|
-import { computed, h } from 'vue';
|
|
|
+import { computed, h, onMounted, ref } from 'vue';
|
|
|
import { notification } from 'ant-design-vue';
|
|
|
import VxeUI from 'vxe-table';
|
|
|
import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
|
|
|
import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
|
|
|
+import seeEvaluate from '@/service/seeEvaluate.vue';
|
|
|
+import { getEvaluateDetailMethod } from '@/request/api/order.api';
|
|
|
+
|
|
|
|
|
|
const props = defineProps<{
|
|
|
data: SystemCwModel['items'][number];
|
|
|
}>();
|
|
|
+// 服务记录--查看评价
|
|
|
+function openSeeEvaluate(row: any) {
|
|
|
+ VxeUI.modal.open({
|
|
|
+ title: '用户评价',
|
|
|
+ width: 900,
|
|
|
+ height: 600,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(seeEvaluate, {
|
|
|
+ data: row,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
// 复制物流信息
|
|
|
function handleCopyTracking() {
|
|
|
const trackingNumber = mockLogisticsData.value.trackingNumber;
|
|
|
@@ -68,21 +88,63 @@ const sellTypeText: Record<string, string> = {
|
|
|
'3': '线上权益',
|
|
|
};
|
|
|
|
|
|
-// 用户评价假数据(淘宝式:每条评价有多张图/多个视频,先展示缩略图排列,点击可左右滑动预览)
|
|
|
-const mockReviewList = [
|
|
|
- {
|
|
|
- criterion: '描述相符',
|
|
|
- score: 5,
|
|
|
- content: '描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符描述相符 描述相符描述相符描述相符描述相符描述相符描述相符 描述相符',
|
|
|
- //图片/视频列表
|
|
|
- mediaList: [
|
|
|
- { type: 'image', url: 'https://picsum.photos/id/1/400/400' },
|
|
|
- { type: 'image', url: 'https://picsum.photos/id/10/400/400' },
|
|
|
- { type: 'video', url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' },
|
|
|
- { type: 'image', url: 'https://picsum.photos/id/20/400/400' },
|
|
|
- ] as MediaItem[],
|
|
|
- },
|
|
|
-];
|
|
|
+function toScore(value: unknown) {
|
|
|
+ const n = Number(value);
|
|
|
+ return Number.isFinite(n) ? n : 0;
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeMediaList(input: unknown): MediaItem[] {
|
|
|
+ if (!Array.isArray(input)) return [];
|
|
|
+ return input
|
|
|
+ .map((it) => {
|
|
|
+ if (typeof it === 'string') {
|
|
|
+ const url = it;
|
|
|
+ const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
|
|
|
+ return { type, url } as MediaItem;
|
|
|
+ }
|
|
|
+ if (it && typeof it === 'object' && 'url' in it) {
|
|
|
+ const anyIt = it as any;
|
|
|
+ const url = String(anyIt.url ?? '');
|
|
|
+ if (!url) return null;
|
|
|
+ const type: MediaItem['type'] =
|
|
|
+ anyIt.type === 'video' || anyIt.type === 'image'
|
|
|
+ ? anyIt.type
|
|
|
+ : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
|
|
|
+ ? 'video'
|
|
|
+ : 'image';
|
|
|
+ return { type, url } as MediaItem;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ })
|
|
|
+ .filter((x): x is MediaItem => Boolean(x));
|
|
|
+}
|
|
|
+
|
|
|
+// 用户评价(与 seeEvaluate.vue 逻辑一致)
|
|
|
+const scroeType = ref([
|
|
|
+ { name: '描述相符', score: 0 },
|
|
|
+]);
|
|
|
+
|
|
|
+const evaluateDetail = ref({
|
|
|
+ depict: '',
|
|
|
+ mediaList: [] as MediaItem[],
|
|
|
+});
|
|
|
+
|
|
|
+const hasEvaluate = computed(() => {
|
|
|
+ const depict = (evaluateDetail.value.depict ?? '').trim();
|
|
|
+ const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
|
|
|
+ const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
|
|
|
+ return Boolean(depict) || hasMedia || hasScore;
|
|
|
+});
|
|
|
+
|
|
|
+async function getEvaluateDetail() {
|
|
|
+ const id = (props.data as any)?.id;
|
|
|
+ if (!id) return;
|
|
|
+ const res = await getEvaluateDetailMethod('1', id);
|
|
|
+ if (!res) return;
|
|
|
+ if (scroeType.value[0]) scroeType.value[0].score = toScore(res.complianceScore);
|
|
|
+ evaluateDetail.value.depict = res.depict ?? '';
|
|
|
+ evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
|
|
|
+}
|
|
|
|
|
|
const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
|
|
|
//预览图片/视频
|
|
|
@@ -104,6 +166,10 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getEvaluateDetail();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -191,57 +257,57 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
<!-- <h3 class="info-title">{{ data.receiptType === '0' ? '物流信息' : data.receiptType === '1' ? '线下取货' : '' }}</h3> -->
|
|
|
<h3 class="info-title">物流信息</h3>
|
|
|
<div v-if="data.receiptType !== null">
|
|
|
- <div class="info-content-wrapper" v-if="data.receiptStatus !== null">
|
|
|
- <!-- 物流信息 -->
|
|
|
- <div class="logistics-content" v-if="data.receiptType === '0'">
|
|
|
- <div class="logistics-tracking">
|
|
|
- <span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
|
|
|
- <a @click="handleCopyTracking" class="copy-link">复制</a>
|
|
|
+ <div class="info-content-wrapper" v-if="data.receiptStatus !== null">
|
|
|
+ <!-- 物流信息 -->
|
|
|
+ <div class="logistics-content" v-if="data.receiptType === '0'">
|
|
|
+ <div class="logistics-tracking">
|
|
|
+ <span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
|
|
|
+ <a @click="handleCopyTracking" class="copy-link">复制</a>
|
|
|
+ </div>
|
|
|
+ <div class="logistics-recipient">
|
|
|
+ <span class="receive-icon">收</span>
|
|
|
+ <span class="recipient-info">
|
|
|
+ {{ mockLogisticsData.recipientName
|
|
|
+ || '' }} {{ mockLogisticsData.recipientPhone || '' }} {{
|
|
|
+ mockLogisticsData.recipientAddress || '' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="logistics-recipient">
|
|
|
- <span class="receive-icon">收</span>
|
|
|
- <span class="recipient-info">
|
|
|
- {{ mockLogisticsData.recipientName
|
|
|
- || '' }} {{ mockLogisticsData.recipientPhone || '' }} {{
|
|
|
- mockLogisticsData.recipientAddress || '' }}
|
|
|
- </span>
|
|
|
+ <div class="logistics-content" v-if="data.receiptType === '1'">
|
|
|
+ 线下取货
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="logistics-content" v-if="data.receiptType === '1'">
|
|
|
- 线下取货
|
|
|
- </div>
|
|
|
- <!-- 包裹内商品 -->
|
|
|
- <div class="package-items-wrapper" v-if="mockPackageItems.length > 0">
|
|
|
- <h3 class="info-title package-title" v-if="data.receiptType === '0'">
|
|
|
- <span class="star-icon">★</span>
|
|
|
- 包裹内商品
|
|
|
- </h3>
|
|
|
- <div class="package-items">
|
|
|
-
|
|
|
- <div v-for="(item, index) in mockPackageItems" :key="index" class="package-item">
|
|
|
- <div class="package-item-image" v-if="item.conditioningProgramPhoto">
|
|
|
- <a-image v-if="item.conditioningProgramPhoto" :width="60" :height="60" style="border-radius: 4px;"
|
|
|
- :src="item.conditioningProgramPhoto" class="item-img" />
|
|
|
- </div>
|
|
|
- <div class="package-item-placeholder" v-else>
|
|
|
- <text class="placeholder-icon">📦</text>
|
|
|
- </div>
|
|
|
- <div class="package-item-details">
|
|
|
- <div class="package-item-name">{{ item.conditioningProgramName }}</div>
|
|
|
- <div class="package-item-spec">{{ item.convertDose }} {{ item.convertUnit }}</div>
|
|
|
- </div>
|
|
|
- <div class="package-item-details">
|
|
|
- <div class="package-item-price">¥{{ item.unitPrice }}</div>
|
|
|
- <div class="package-item-quantity">x{{ item.totalMeasure }}</div>
|
|
|
+ <!-- 包裹内商品 -->
|
|
|
+ <div class="package-items-wrapper" v-if="mockPackageItems.length > 0">
|
|
|
+ <h3 class="info-title package-title" v-if="data.receiptType === '0'">
|
|
|
+ <span class="star-icon">★</span>
|
|
|
+ 包裹内商品
|
|
|
+ </h3>
|
|
|
+ <div class="package-items">
|
|
|
+
|
|
|
+ <div v-for="(item, index) in mockPackageItems" :key="index" class="package-item">
|
|
|
+ <div class="package-item-image" v-if="item.conditioningProgramPhoto">
|
|
|
+ <a-image v-if="item.conditioningProgramPhoto" :width="60" :height="60" style="border-radius: 4px;"
|
|
|
+ :src="item.conditioningProgramPhoto" class="item-img" />
|
|
|
+ </div>
|
|
|
+ <div class="package-item-placeholder" v-else>
|
|
|
+ <text class="placeholder-icon">📦</text>
|
|
|
+ </div>
|
|
|
+ <div class="package-item-details">
|
|
|
+ <div class="package-item-name">{{ item.conditioningProgramName }}</div>
|
|
|
+ <div class="package-item-spec">{{ item.convertDose }} {{ item.convertUnit }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="package-item-details">
|
|
|
+ <div class="package-item-price">¥{{ item.unitPrice }}</div>
|
|
|
+ <div class="package-item-quantity">x{{ item.totalMeasure }}</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="info-content-wrapper" v-else>
|
|
|
+ 暂无信息
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="info-content-wrapper" v-else>
|
|
|
- 暂无信息
|
|
|
- </div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 服务记录 线下服务才显示 1-实体商品 2-线下服务 3-线上权益-->
|
|
|
@@ -279,6 +345,11 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
</vxe-column>
|
|
|
<vxe-column field="operateBy" title="操作人" align="center" />
|
|
|
<vxe-column field="feedback" title="治疗备注" align="center" />
|
|
|
+ <vxe-column field="acuPointNames" title="用户评价" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <vxe-button @click="openSeeEvaluate(row)" content="查看" status="primary" />
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
</vxe-table>
|
|
|
</div>
|
|
|
<!-- 用户评价 -->
|
|
|
@@ -287,42 +358,40 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
<h3 class="info-title">用户评价</h3>
|
|
|
</div>
|
|
|
<div class="info-content-wrapper">
|
|
|
- <div v-for="(item, index) in mockReviewList" :key="index" class="review-item">
|
|
|
- <div class="review-top-row">
|
|
|
- <div class="review-rating-row">
|
|
|
- <span class="review-criterion">{{ item.criterion }}</span>
|
|
|
- <a-rate :value="item.score" disabled allow-half class="review-stars" />
|
|
|
- <span class="review-score">{{ item.score }}分</span>
|
|
|
- </div>
|
|
|
- <div class="review-content-block">
|
|
|
- <span class="review-content-label">评价内容:</span>
|
|
|
- <p class="review-content-text">{{ item.content }}</p>
|
|
|
- <div v-if="item.mediaList?.length" class="review-media-row">
|
|
|
- <div
|
|
|
- v-for="(media, i) in item.mediaList"
|
|
|
- :key="i"
|
|
|
- class="review-media-thumb"
|
|
|
- :class="{ 'is-video': media.type === 'video' }"
|
|
|
- role="button"
|
|
|
- tabindex="0"
|
|
|
- @click.stop.prevent="openPreview(item.mediaList, i)"
|
|
|
- @keydown.enter.space.prevent="openPreview(item.mediaList, i)"
|
|
|
- >
|
|
|
- <img
|
|
|
- v-if="media.type === 'image'"
|
|
|
- :src="media.url"
|
|
|
- class="review-thumb-img"
|
|
|
- alt=""
|
|
|
- />
|
|
|
- <div v-else class="review-thumb-video">
|
|
|
- <video :src="media.url" muted preload="metadata" />
|
|
|
- <span class="review-thumb-play"><PlayCircleOutlined /></span>
|
|
|
+ <template v-if="hasEvaluate">
|
|
|
+ <div class="review-item">
|
|
|
+ <div class="review-top-row">
|
|
|
+ <div class="review-rating-box">
|
|
|
+ <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
|
|
|
+ <span class="review-criterion">{{ scroe.name }}</span>
|
|
|
+ <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
|
|
|
+ <span class="review-score">{{ scroe.score }}分</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="review-content-block">
|
|
|
+ <div class="review-content-line">
|
|
|
+ <span class="review-content-label">评价内容:</span>
|
|
|
+ <span class="review-content-text">{{ evaluateDetail.depict }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
|
|
|
+ <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
|
|
|
+ :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
|
|
|
+ @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
|
|
|
+ @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
|
|
|
+ <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
|
|
|
+ <div v-else class="review-thumb-video">
|
|
|
+ <video :src="media.url" muted preload="metadata" />
|
|
|
+ <span class="review-thumb-play">
|
|
|
+ <PlayCircleOutlined />
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
+ <div v-else class="empty-text">无</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -392,17 +461,28 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
+ .review-rating-box {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 240px;
|
|
|
+ }
|
|
|
+
|
|
|
.review-rating-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-shrink: 0;
|
|
|
- margin-right: 40px;
|
|
|
}
|
|
|
|
|
|
.review-criterion {
|
|
|
font-size: 14px;
|
|
|
color: #333;
|
|
|
+ width: 72px;
|
|
|
+ text-align: left;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.review-stars {
|
|
|
@@ -411,6 +491,7 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
:deep(.ant-rate-star-full .ant-rate-star-second) {
|
|
|
color: #fadb14;
|
|
|
}
|
|
|
+
|
|
|
:deep(.ant-rate-star-full .ant-rate-star-first) {
|
|
|
color: #fadb14;
|
|
|
}
|
|
|
@@ -426,17 +507,24 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
+ .review-content-line {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
.review-content-label {
|
|
|
font-size: 14px;
|
|
|
color: #333;
|
|
|
margin-right: 4px;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.review-content-text {
|
|
|
font-size: 14px;
|
|
|
color: #333;
|
|
|
line-height: 22px;
|
|
|
- margin: 8px 0 12px;
|
|
|
+ margin: 0;
|
|
|
white-space: pre-wrap;
|
|
|
max-width: 560px;
|
|
|
word-break: break-word;
|
|
|
@@ -447,6 +535,7 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
gap: 8px;
|
|
|
flex-wrap: wrap;
|
|
|
margin-top: 12px;
|
|
|
+ margin-left: 70px;
|
|
|
}
|
|
|
|
|
|
.review-media-thumb {
|
|
|
@@ -517,6 +606,13 @@ function openPreview(list: MediaItem[], index: number) {
|
|
|
padding: 10px 20px;
|
|
|
}
|
|
|
|
|
|
+ .empty-text {
|
|
|
+ color: #999;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 22px;
|
|
|
+ padding: 8px 0;
|
|
|
+ }
|
|
|
+
|
|
|
.product-info {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|