|
|
@@ -1,10 +1,35 @@
|
|
|
<script setup lang="ts">
|
|
|
import type { SystemCwModel } from '@/model/care.model';
|
|
|
-import { computed } from 'vue';
|
|
|
+import type { ApplyRecordModel } from '@/model/order.model';
|
|
|
+import { PlayCircleOutlined } from '@ant-design/icons-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, getApplyRecordMethod } 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;
|
|
|
@@ -42,8 +67,6 @@ const expressTypeText: Record<string, string> = {
|
|
|
};
|
|
|
// 包裹数据
|
|
|
const mockPackageItems = computed(() => {
|
|
|
- console.log('props.data===', props.data);
|
|
|
-
|
|
|
return (props.data as any).sameExpress ?? []
|
|
|
});
|
|
|
// 商品状态映射 - 收货状态(实体商品使用)
|
|
|
@@ -64,6 +87,100 @@ const sellTypeText: Record<string, string> = {
|
|
|
'2': '线下服务',
|
|
|
'3': '线上权益',
|
|
|
};
|
|
|
+
|
|
|
+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 applyRecordList = ref<ApplyRecordModel[]>([]);
|
|
|
+// 获取预约
|
|
|
+async function getApplyRecord() {
|
|
|
+ const id = (props.data as any)?.id;
|
|
|
+ if (!id) return;
|
|
|
+ const res = await getApplyRecordMethod(id);
|
|
|
+ if (!res) return;
|
|
|
+ console.log(res, "获取预约");
|
|
|
+ applyRecordList.value = res ?? [];
|
|
|
+ console.log(applyRecordList.value, "applyRecordList");
|
|
|
+}
|
|
|
+const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
|
|
|
+//预览图片/视频
|
|
|
+function openPreview(list: MediaItem[], index: number) {
|
|
|
+ if (!list?.length) return;
|
|
|
+ VxeUI.modal.open({
|
|
|
+ id: REVIEW_PREVIEW_MODAL_ID,
|
|
|
+ title: '预览',
|
|
|
+ width: 720,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(ReviewMediaPreview, {
|
|
|
+ mediaList: list,
|
|
|
+ initialIndex: index,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await getEvaluateDetail();
|
|
|
+ await getApplyRecord();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -130,16 +247,13 @@ const sellTypeText: Record<string, string> = {
|
|
|
<!-- 分账信息 -->
|
|
|
<div class="info-section">
|
|
|
<h3 class="info-title">分账信息</h3>
|
|
|
- <vxe-table
|
|
|
- class="split-account-table"
|
|
|
- :data="mockSplitAccountList"
|
|
|
- border
|
|
|
- >
|
|
|
+ <vxe-table class="split-account-table" :data="mockSplitAccountList" border>
|
|
|
<vxe-column field="profitSharingTime" title="分账时间" align="center" />
|
|
|
<vxe-column field="conditioningProgramSupplierName" title="供应商" align="center" />
|
|
|
- <vxe-column field="profitSharingStatus" title="分账状态" align="center" >
|
|
|
+ <vxe-column field="profitSharingStatus" title="分账状态" align="center">
|
|
|
<template #default="{ row }">
|
|
|
- {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}
|
|
|
+ {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' :
|
|
|
+ row.profitSharingStatus === '3' ? '分账异常' : '' }}
|
|
|
</template>
|
|
|
</vxe-column>
|
|
|
<vxe-column field="profitSharing" title="分账比例" align="center">
|
|
|
@@ -147,7 +261,7 @@ const sellTypeText: Record<string, string> = {
|
|
|
{{ row.profitSharing || '-' }}%
|
|
|
</template>
|
|
|
</vxe-column>
|
|
|
- <vxe-column field="profitSharingAmount" title="预计分账金额" align="center" >
|
|
|
+ <vxe-column field="profitSharingAmount" title="预计分账金额" align="center">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.profitSharingAmount ? row.profitSharingAmount + '元' : '' }}
|
|
|
</template>
|
|
|
@@ -164,7 +278,7 @@ const sellTypeText: Record<string, string> = {
|
|
|
<!-- receiptType 收货方式 0-快递 1-线下取货 -->
|
|
|
<!-- <h3 class="info-title">{{ data.receiptType === '0' ? '物流信息' : data.receiptType === '1' ? '线下取货' : '' }}</h3> -->
|
|
|
<h3 class="info-title">物流信息</h3>
|
|
|
-
|
|
|
+
|
|
|
<div class="info-content-wrapper" v-if="data.receiptType">
|
|
|
<!-- 物流信息 -->
|
|
|
<div class="logistics-content" v-if="data.receiptType === '0'">
|
|
|
@@ -196,7 +310,7 @@ const sellTypeText: Record<string, string> = {
|
|
|
包裹内商品
|
|
|
</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;"
|
|
|
@@ -221,19 +335,41 @@ const sellTypeText: Record<string, string> = {
|
|
|
暂无
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
+ <!-- 预约派单记录 线下服务才显示 1-实体商品 2-线下服务 3-线上权益-->
|
|
|
+ <div class="info-section" v-if="data?.sellType === '2'">
|
|
|
+ <h3 class="info-title">
|
|
|
+ 预约派单记录
|
|
|
+ </h3>
|
|
|
+ <vxe-table :data="applyRecordList" border>
|
|
|
+ <vxe-column type="seq" title="序号" width="60" align="center" />
|
|
|
+ <vxe-column field="applyTime" title="预约时间" align="center" />
|
|
|
+ <vxe-column field="updateTime" title="最新修改预约时间" align="center" />
|
|
|
+ <vxe-column field="cancelTime" title="取消预约时间" align="center" />
|
|
|
+ <vxe-column field="arrangeTime" title="预约服务日期" align="center" />
|
|
|
+ <vxe-column field="arrangePeriod" title="预约服务时间段" align="center" />
|
|
|
+ <vxe-column field="pieTime" title="是否派单" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.pieTime ? '是' : '否' }}
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
+ <vxe-column field="pieBy" title="派单员" align="center" />
|
|
|
+ <vxe-column field="pieTime" title="派单时间" align="center" />
|
|
|
+ <vxe-column field="conditioningProgramSupplierName" title="服务机构" align="center" />
|
|
|
+ <vxe-column field="status" title="服务状态" align="center" />
|
|
|
+ </vxe-table>
|
|
|
+ </div>
|
|
|
<!-- 服务记录 线下服务才显示 1-实体商品 2-线下服务 3-线上权益-->
|
|
|
<div class="info-section" v-if="data?.sellType === '2'">
|
|
|
<h3 class="info-title">
|
|
|
服务记录
|
|
|
<span class="title-count">({{ data?.patientConditioningOfflines?.length || 0 }}/{{ data?.totalMeasure || 0
|
|
|
- }})</span>
|
|
|
+ }})</span>
|
|
|
</h3>
|
|
|
<vxe-table :data="data?.patientConditioningOfflines" border>
|
|
|
<vxe-column type="seq" title="序号" width="60" align="center" />
|
|
|
<vxe-column field="arrangeDate" title="服务日期" align="center" />
|
|
|
<vxe-column field="arrangePeriod" title="服务时间段" align="center" />
|
|
|
- <vxe-column field="operateTime" title="服务状态" align="center">
|
|
|
+ <vxe-column field="verifyStatus" title="服务状态" align="center">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.operateTime ? '已核销' : '已预约' }}
|
|
|
</template>
|
|
|
@@ -257,8 +393,55 @@ const sellTypeText: Record<string, string> = {
|
|
|
</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>
|
|
|
+ <!-- 用户评价 -->
|
|
|
+ <div class="info-section">
|
|
|
+ <div class="info-title-row">
|
|
|
+ <h3 class="info-title">用户评价</h3>
|
|
|
+ </div>
|
|
|
+ <div class="info-content-wrapper">
|
|
|
+ <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>
|
|
|
+ </template>
|
|
|
+ <div v-else class="empty-text">无</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -299,6 +482,159 @@ const sellTypeText: Record<string, string> = {
|
|
|
color: #333;
|
|
|
}
|
|
|
|
|
|
+ .info-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ .info-title {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-item {
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-top-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 24px;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-criterion {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ width: 72px;
|
|
|
+ text-align: left;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-stars {
|
|
|
+ font-size: 16px;
|
|
|
+
|
|
|
+ :deep(.ant-rate-star-full .ant-rate-star-second) {
|
|
|
+ color: #fadb14;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-rate-star-full .ant-rate-star-first) {
|
|
|
+ color: #fadb14;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-score {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-content-block {
|
|
|
+ flex: 1;
|
|
|
+ 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: 0;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ max-width: 560px;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-media-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin-top: 12px;
|
|
|
+ margin-left: 70px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-media-thumb {
|
|
|
+ width: 72px;
|
|
|
+ height: 72px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #e8e8e8;
|
|
|
+ overflow: hidden;
|
|
|
+ flex-shrink: 0;
|
|
|
+ cursor: pointer;
|
|
|
+ background: #f5f5f5;
|
|
|
+ transition: border-color 0.2s, box-shadow 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #1890ff;
|
|
|
+ box-shadow: 0 0 0 1px #1890ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-thumb-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-thumb-video {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #2a2a2a;
|
|
|
+
|
|
|
+ video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .review-thumb-play {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ font-size: 28px;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.info-content-wrapper .info-title {
|
|
|
margin-top: 20px;
|
|
|
margin-bottom: 12px;
|
|
|
@@ -320,6 +656,7 @@ const sellTypeText: Record<string, string> = {
|
|
|
|
|
|
/* 分账表格仅按内容自适应高度,避免一行数据时出现多余空白 */
|
|
|
.split-account-table {
|
|
|
+
|
|
|
:deep(.vxe-table--wrapper),
|
|
|
:deep(.vxe-table) {
|
|
|
height: auto !important;
|
|
|
@@ -333,6 +670,13 @@ const sellTypeText: Record<string, string> = {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .empty-text {
|
|
|
+ color: #999;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 22px;
|
|
|
+ padding: 8px 0;
|
|
|
+ }
|
|
|
+
|
|
|
.product-info {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|