SingleItemDetail.vue 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. <script setup lang="ts">
  2. import type { SystemCwModel } from '@/model/care.model';
  3. import { PlayCircleOutlined } from '@ant-design/icons-vue';
  4. import { computed, h, onMounted, ref } from 'vue';
  5. import { notification } from 'ant-design-vue';
  6. import VxeUI from 'vxe-table';
  7. import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
  8. import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
  9. import seeEvaluate from '@/service/seeEvaluate.vue';
  10. import { getEvaluateDetailMethod } from '@/request/api/order.api';
  11. const props = defineProps<{
  12. data: SystemCwModel['items'][number];
  13. }>();
  14. // 服务记录--查看评价
  15. function openSeeEvaluate(row: any) {
  16. VxeUI.modal.open({
  17. title: '用户评价',
  18. width: 900,
  19. height: 600,
  20. escClosable: true,
  21. destroyOnClose: true,
  22. slots: {
  23. default() {
  24. return h(seeEvaluate, {
  25. data: row,
  26. });
  27. },
  28. },
  29. });
  30. }
  31. // 复制物流信息
  32. function handleCopyTracking() {
  33. const trackingNumber = mockLogisticsData.value.trackingNumber;
  34. if (trackingNumber) {
  35. navigator.clipboard.writeText(trackingNumber).then(() => {
  36. notification.success({ message: '复制成功' });
  37. }).catch(() => {
  38. notification.error({ message: '复制失败' });
  39. });
  40. }
  41. }
  42. // 分账信息数据
  43. const mockSplitAccountList = computed(() => {
  44. return (props.data as any).profitSharings;
  45. });
  46. // 物流信息数据
  47. const mockLogisticsData = computed(() => {
  48. console.log('props.data===', props.data);
  49. return {
  50. trackingNumber: `${expressTypeText[props.data.expressType || '']} ${props.data.expressNo || ''}`,
  51. recipientName: props.data.liaison ? props.data.liaison + ', ' : '',
  52. recipientPhone: props.data.phone ? props.data.phone + ', ' : '',
  53. recipientAddress: (props.data.provinceName !== null ? props.data.provinceName + ' ' : '') + (props.data.cityName !== null ? props.data.cityName + ' ' : '') + (props.data.areaName !== null ? props.data.areaName + ' ' : '') + (props.data.detailAddress !== null ? props.data.detailAddress : '')
  54. };
  55. });
  56. const expressTypeText: Record<string, string> = {
  57. '0': '邮政速递',
  58. '1': '顺丰速运',
  59. '2': '京东快递',
  60. '3': '中通快递',
  61. '4': '圆通速递',
  62. '5': '申通快递',
  63. '6': '韵达快递',
  64. '7': '极兔速递',
  65. };
  66. // 包裹数据
  67. const mockPackageItems = computed(() => {
  68. return (props.data as any).sameExpress ?? []
  69. });
  70. // 商品状态映射 - 收货状态(实体商品使用)
  71. const receiptStatusText: Record<string, string> = {
  72. '0': '待发货',
  73. '1': '已发货',
  74. '2': '已收货',
  75. };
  76. // 进度状态映射(线下服务和线上权益使用)
  77. const progressText: Record<string, string> = {
  78. '0': '进行中',
  79. '1': '已完成',
  80. '2': '未开始',
  81. };
  82. // 商品类型映射
  83. const sellTypeText: Record<string, string> = {
  84. '1': '实体商品',
  85. '2': '线下服务',
  86. '3': '线上权益',
  87. };
  88. function toScore(value: unknown) {
  89. const n = Number(value);
  90. return Number.isFinite(n) ? n : 0;
  91. }
  92. function normalizeMediaList(input: unknown): MediaItem[] {
  93. if (!Array.isArray(input)) return [];
  94. return input
  95. .map((it) => {
  96. if (typeof it === 'string') {
  97. const url = it;
  98. const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
  99. return { type, url } as MediaItem;
  100. }
  101. if (it && typeof it === 'object' && 'url' in it) {
  102. const anyIt = it as any;
  103. const url = String(anyIt.url ?? '');
  104. if (!url) return null;
  105. const type: MediaItem['type'] =
  106. anyIt.type === 'video' || anyIt.type === 'image'
  107. ? anyIt.type
  108. : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
  109. ? 'video'
  110. : 'image';
  111. return { type, url } as MediaItem;
  112. }
  113. return null;
  114. })
  115. .filter((x): x is MediaItem => Boolean(x));
  116. }
  117. // 用户评价(与 seeEvaluate.vue 逻辑一致)
  118. const scroeType = ref([
  119. { name: '描述相符', score: 0 },
  120. ]);
  121. const evaluateDetail = ref({
  122. depict: '',
  123. mediaList: [] as MediaItem[],
  124. });
  125. const hasEvaluate = computed(() => {
  126. const depict = (evaluateDetail.value.depict ?? '').trim();
  127. const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
  128. const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
  129. return Boolean(depict) || hasMedia || hasScore;
  130. });
  131. async function getEvaluateDetail() {
  132. const id = (props.data as any)?.id;
  133. if (!id) return;
  134. const res = await getEvaluateDetailMethod('1', id);
  135. if (!res) return;
  136. if (scroeType.value[0]) scroeType.value[0].score = toScore(res.complianceScore);
  137. evaluateDetail.value.depict = res.depict ?? '';
  138. evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
  139. }
  140. const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
  141. //预览图片/视频
  142. function openPreview(list: MediaItem[], index: number) {
  143. if (!list?.length) return;
  144. VxeUI.modal.open({
  145. id: REVIEW_PREVIEW_MODAL_ID,
  146. title: '预览',
  147. width: 720,
  148. escClosable: true,
  149. destroyOnClose: true,
  150. slots: {
  151. default() {
  152. return h(ReviewMediaPreview, {
  153. mediaList: list,
  154. initialIndex: index,
  155. });
  156. },
  157. },
  158. });
  159. }
  160. onMounted(() => {
  161. getEvaluateDetail();
  162. });
  163. </script>
  164. <template>
  165. <div class="service-detail">
  166. <!-- 商品信息 -->
  167. <div class="info-section">
  168. <h3 class="info-title">商品信息</h3>
  169. <div class="info-content-wrapper">
  170. <div class="product-info">
  171. <div class="product-image">
  172. <!-- 商品图片 -->
  173. <a-image v-if="data?.conditioningProgramPhoto" :width="80" :height="80" style="border-radius: 5px;"
  174. :src="data?.conditioningProgramPhoto" class="product-img" />
  175. </div>
  176. <div class="product-details">
  177. <!-- 商品名称 -->
  178. <div class="product-name">{{ data?.conditioningProgramName }}</div>
  179. <!-- 一口价计价 -->
  180. <div class="product-spec" v-if="data?.conditioningProgramDetail?.pricingType === '0'">
  181. {{ data?.convertDose }} {{ data?.convertUnit || '次' }}
  182. </div>
  183. <!-- 按穴位/经络/部位计价 -->
  184. <div class="product-spec" v-if="data?.conditioningProgramDetail?.pricingType === '1'">
  185. 1次
  186. </div>
  187. <!-- 商品价格 -->
  188. <div class="product-price" v-if="data?.unitPrice">
  189. ¥{{ data?.unitPrice }}
  190. </div>
  191. </div>
  192. </div>
  193. <div class="info-content">
  194. <div class="info-row">
  195. <div class="info-item">
  196. <span class="info-label">商品类型:</span>
  197. <span class="info-value">{{ sellTypeText[data.sellType] || '-' }}</span>
  198. </div>
  199. <div class="info-item">
  200. <span class="info-label">方案类型:</span>
  201. <span class="info-value">{{ data.conditioningProgramType || '-' }}</span>
  202. </div>
  203. </div>
  204. <div class="info-row">
  205. <div class="info-item">
  206. <span class="info-label">数量:</span>
  207. <span class="info-value">{{ data?.totalMeasure || '-' }}</span>
  208. </div>
  209. <div class="info-item">
  210. <span class="info-label">商品总价:</span>
  211. <span class="info-value">¥{{ data?.totalPrice || '-' }}</span>
  212. </div>
  213. <div class="info-item" v-if="data?.sellType === '1' ? data?.receiptStatus : data?.progress">
  214. <span class="info-label">商品状态:</span>
  215. <span class="info-value">
  216. {{ data?.sellType === '1'
  217. ? (data?.receiptStatus ? receiptStatusText[data.receiptStatus] : '-')
  218. : (data?.progress ? progressText[data.progress] : '-') }}
  219. </span>
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. </div>
  225. <!-- 分账信息 -->
  226. <div class="info-section">
  227. <h3 class="info-title">分账信息</h3>
  228. <vxe-table
  229. class="split-account-table"
  230. :data="mockSplitAccountList"
  231. border
  232. >
  233. <vxe-column field="profitSharingTime" title="分账时间" align="center" />
  234. <vxe-column field="conditioningProgramSupplierName" title="供应商" align="center" />
  235. <vxe-column field="profitSharingStatus" title="分账状态" align="center" >
  236. <template #default="{ row }">
  237. {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}
  238. </template>
  239. </vxe-column>
  240. <vxe-column field="profitSharing" title="分账比例" align="center">
  241. <template #default="{ row }">
  242. {{ row.profitSharing || '-' }}%
  243. </template>
  244. </vxe-column>
  245. <vxe-column field="profitSharingAmount" title="预计分账金额" align="center" >
  246. <template #default="{ row }">
  247. {{ row.profitSharingAmount ? row.profitSharingAmount + '元' : '' }}
  248. </template>
  249. </vxe-column>
  250. <vxe-column field="realAmount" title="到账金额" align="center">
  251. <template #default="{ row }">
  252. {{ row.realAmount ? row.realAmount + '元' : '' }}
  253. </template>
  254. </vxe-column>
  255. </vxe-table>
  256. </div>
  257. <!-- 物流信息和包裹内商品 实体商品才显示 sellType==='1'-->
  258. <div class="info-section" v-if="data.sellType === '1'">
  259. <!-- receiptType 收货方式 0-快递 1-线下取货 -->
  260. <!-- <h3 class="info-title">{{ data.receiptType === '0' ? '物流信息' : data.receiptType === '1' ? '线下取货' : '' }}</h3> -->
  261. <h3 class="info-title">物流信息</h3>
  262. <div class="info-content-wrapper" v-if="data.receiptType">
  263. <!-- 物流信息 -->
  264. <div class="logistics-content" v-if="data.receiptType === '0'">
  265. <template v-if="mockPackageItems.length > 0">
  266. <div class="logistics-tracking">
  267. <span class="tracking-number">{{ mockLogisticsData.trackingNumber }}</span>
  268. <a @click="handleCopyTracking" class="copy-link">复制</a>
  269. </div>
  270. <div class="logistics-recipient">
  271. <span class="receive-icon">收</span>
  272. <span class="recipient-info">
  273. {{ mockLogisticsData.recipientName
  274. || '' }} {{ mockLogisticsData.recipientPhone || '' }} {{
  275. mockLogisticsData.recipientAddress || '' }}
  276. </span>
  277. </div>
  278. </template>
  279. <template v-else>
  280. 暂无
  281. </template>
  282. </div>
  283. <div class="logistics-content" v-if="data.receiptType === '1'">
  284. 线下取货
  285. </div>
  286. <!-- 包裹内商品 -->
  287. <div class="package-items-wrapper" v-if="mockPackageItems.length > 0">
  288. <h3 class="info-title package-title" v-if="data.receiptType === '0'">
  289. <span class="star-icon">★</span>
  290. 包裹内商品
  291. </h3>
  292. <div class="package-items">
  293. <div v-for="(item, index) in mockPackageItems" :key="index" class="package-item">
  294. <div class="package-item-image" v-if="item.conditioningProgramPhoto">
  295. <a-image v-if="item.conditioningProgramPhoto" :width="60" :height="60" style="border-radius: 4px;"
  296. :src="item.conditioningProgramPhoto" class="item-img" />
  297. </div>
  298. <div class="package-item-placeholder" v-else>
  299. <text class="placeholder-icon">📦</text>
  300. </div>
  301. <div class="package-item-details">
  302. <div class="package-item-name">{{ item.conditioningProgramName }}</div>
  303. <div class="package-item-spec">{{ item.convertDose }} {{ item.convertUnit }}</div>
  304. </div>
  305. <div class="package-item-details">
  306. <div class="package-item-price">¥{{ item.unitPrice }}</div>
  307. <div class="package-item-quantity">x{{ item.totalMeasure }}</div>
  308. </div>
  309. </div>
  310. </div>
  311. </div>
  312. </div>
  313. <div class="info-content-wrapper" v-else>
  314. 暂无
  315. </div>
  316. </div>
  317. <!-- 服务记录 线下服务才显示 1-实体商品 2-线下服务 3-线上权益-->
  318. <div class="info-section" v-if="data?.sellType === '2'">
  319. <h3 class="info-title">
  320. 服务记录
  321. <span class="title-count">({{ data?.patientConditioningOfflines?.length || 0 }}/{{ data?.totalMeasure || 0
  322. }})</span>
  323. </h3>
  324. <vxe-table :data="data?.patientConditioningOfflines" border>
  325. <vxe-column type="seq" title="序号" width="60" align="center" />
  326. <vxe-column field="arrangeDate" title="服务日期" align="center" />
  327. <vxe-column field="arrangePeriod" title="服务时间段" align="center" />
  328. <vxe-column field="verifyStatus" title="服务状态" align="center">
  329. <template #default="{ row }">
  330. {{ row.operateTime ? '已核销' : '已预约' }}
  331. </template>
  332. </vxe-column>
  333. <vxe-column field="applyTime" title="预约时间" align="center" />
  334. <vxe-column field="pieBy" title="派单员" align="center" />
  335. <vxe-column field="pieTime" title="派单时间" align="center" />
  336. <vxe-column field="conditioningProgramSupplierName" title="服务机构" align="center" />
  337. <vxe-column field="operateTime" title="核销时间" align="center" />
  338. <vxe-column field="startTime" title="操作开始时间" align="center" />
  339. <vxe-column field="endTime" title="操作结束时间" align="center" />
  340. <vxe-column field="operateDuration" title="治疗时长" align="center">
  341. <template #default="{ row }">
  342. {{ row.operateDuration ? row.operateDuration + '分钟' : '' }}
  343. </template>
  344. </vxe-column>
  345. <vxe-column field="arrangeDuration" title="预定服务时长" align="center">
  346. <template #default="{ row }">
  347. {{ row.arrangeDuration ? row.arrangeDuration + '分钟' : '' }}
  348. </template>
  349. </vxe-column>
  350. <vxe-column field="operateBy" title="操作人" align="center" />
  351. <vxe-column field="feedback" title="治疗备注" align="center" />
  352. <vxe-column field="acuPointNames" title="用户评价" align="center">
  353. <template #default="{ row }">
  354. <vxe-button @click="openSeeEvaluate(row)" content="查看" status="primary" />
  355. </template>
  356. </vxe-column>
  357. </vxe-table>
  358. </div>
  359. <!-- 用户评价 -->
  360. <div class="info-section">
  361. <div class="info-title-row">
  362. <h3 class="info-title">用户评价</h3>
  363. </div>
  364. <div class="info-content-wrapper">
  365. <template v-if="hasEvaluate">
  366. <div class="review-item">
  367. <div class="review-top-row">
  368. <div class="review-rating-box">
  369. <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
  370. <span class="review-criterion">{{ scroe.name }}</span>
  371. <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
  372. <span class="review-score">{{ scroe.score }}分</span>
  373. </div>
  374. </div>
  375. <div class="review-content-block">
  376. <div class="review-content-line">
  377. <span class="review-content-label">评价内容:</span>
  378. <span class="review-content-text">{{ evaluateDetail.depict }}</span>
  379. </div>
  380. <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
  381. <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
  382. :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
  383. @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
  384. @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
  385. <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
  386. <div v-else class="review-thumb-video">
  387. <video :src="media.url" muted preload="metadata" />
  388. <span class="review-thumb-play">
  389. <PlayCircleOutlined />
  390. </span>
  391. </div>
  392. </div>
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. </template>
  398. <div v-else class="empty-text">无</div>
  399. </div>
  400. </div>
  401. </div>
  402. </template>
  403. <style scoped lang="scss">
  404. .service-detail {
  405. padding: 20px;
  406. color: black;
  407. background: #fff;
  408. .info-section {
  409. margin-top: 24px;
  410. margin-bottom: 24px;
  411. background: #fff;
  412. }
  413. .service-package-placeholder {
  414. width: 120rpx;
  415. height: 120rpx;
  416. border-radius: 8rpx;
  417. margin-right: 20rpx;
  418. background-color: #f5f5f5;
  419. border: 1px solid #e8e8e8;
  420. display: flex;
  421. align-items: center;
  422. justify-content: center;
  423. flex-shrink: 0;
  424. }
  425. .placeholder-icon {
  426. font-size: 40rpx;
  427. opacity: 0.3;
  428. }
  429. .info-title {
  430. font-size: 16px;
  431. font-weight: 600;
  432. margin-bottom: 12px;
  433. color: #333;
  434. }
  435. .info-title-row {
  436. display: flex;
  437. align-items: center;
  438. gap: 8px;
  439. margin-bottom: 12px;
  440. .info-title {
  441. margin-bottom: 0;
  442. }
  443. }
  444. .review-item {
  445. padding: 12px 0;
  446. border-bottom: 1px solid #f0f0f0;
  447. &:last-child {
  448. border-bottom: none;
  449. }
  450. }
  451. .review-top-row {
  452. display: flex;
  453. align-items: flex-start;
  454. gap: 24px;
  455. margin-bottom: 12px;
  456. }
  457. .review-rating-box {
  458. flex-shrink: 0;
  459. display: flex;
  460. flex-direction: column;
  461. align-items: flex-start;
  462. gap: 12px;
  463. min-width: 240px;
  464. }
  465. .review-rating-row {
  466. display: flex;
  467. align-items: center;
  468. gap: 8px;
  469. flex-shrink: 0;
  470. }
  471. .review-criterion {
  472. font-size: 14px;
  473. color: #333;
  474. width: 72px;
  475. text-align: left;
  476. white-space: nowrap;
  477. }
  478. .review-stars {
  479. font-size: 16px;
  480. :deep(.ant-rate-star-full .ant-rate-star-second) {
  481. color: #fadb14;
  482. }
  483. :deep(.ant-rate-star-full .ant-rate-star-first) {
  484. color: #fadb14;
  485. }
  486. }
  487. .review-score {
  488. font-size: 13px;
  489. color: #999;
  490. }
  491. .review-content-block {
  492. flex: 1;
  493. min-width: 0;
  494. }
  495. .review-content-line {
  496. display: flex;
  497. align-items: flex-start;
  498. gap: 6px;
  499. }
  500. .review-content-label {
  501. font-size: 14px;
  502. color: #333;
  503. margin-right: 4px;
  504. flex-shrink: 0;
  505. }
  506. .review-content-text {
  507. font-size: 14px;
  508. color: #333;
  509. line-height: 22px;
  510. margin: 0;
  511. white-space: pre-wrap;
  512. max-width: 560px;
  513. word-break: break-word;
  514. }
  515. .review-media-row {
  516. display: flex;
  517. gap: 8px;
  518. flex-wrap: wrap;
  519. margin-top: 12px;
  520. margin-left: 70px;
  521. }
  522. .review-media-thumb {
  523. width: 72px;
  524. height: 72px;
  525. border-radius: 6px;
  526. border: 1px solid #e8e8e8;
  527. overflow: hidden;
  528. flex-shrink: 0;
  529. cursor: pointer;
  530. background: #f5f5f5;
  531. transition: border-color 0.2s, box-shadow 0.2s;
  532. &:hover {
  533. border-color: #1890ff;
  534. box-shadow: 0 0 0 1px #1890ff;
  535. }
  536. .review-thumb-img {
  537. width: 100%;
  538. height: 100%;
  539. object-fit: cover;
  540. display: block;
  541. }
  542. .review-thumb-video {
  543. position: relative;
  544. width: 100%;
  545. height: 100%;
  546. background: #2a2a2a;
  547. video {
  548. width: 100%;
  549. height: 100%;
  550. object-fit: cover;
  551. display: block;
  552. }
  553. .review-thumb-play {
  554. position: absolute;
  555. inset: 0;
  556. display: flex;
  557. align-items: center;
  558. justify-content: center;
  559. color: rgba(255, 255, 255, 0.9);
  560. font-size: 28px;
  561. pointer-events: none;
  562. }
  563. }
  564. }
  565. .info-content-wrapper .info-title {
  566. margin-top: 20px;
  567. margin-bottom: 12px;
  568. padding-top: 20px;
  569. border-top: 1px solid #f0f0f0;
  570. }
  571. .info-content-wrapper .info-title:first-child {
  572. margin-top: 0;
  573. padding-top: 0;
  574. border-top: none;
  575. }
  576. .info-content-wrapper {
  577. border: 1px solid #e8e8e8;
  578. border-radius: 4px;
  579. padding: 10px 20px;
  580. }
  581. /* 分账表格仅按内容自适应高度,避免一行数据时出现多余空白 */
  582. .split-account-table {
  583. :deep(.vxe-table--wrapper),
  584. :deep(.vxe-table) {
  585. height: auto !important;
  586. }
  587. :deep(.vxe-table--body-wrapper) {
  588. height: auto !important;
  589. min-height: 0 !important;
  590. max-height: none !important;
  591. flex: unset !important;
  592. }
  593. }
  594. .empty-text {
  595. color: #999;
  596. font-size: 14px;
  597. line-height: 22px;
  598. padding: 8px 0;
  599. }
  600. .product-info {
  601. display: flex;
  602. align-items: flex-start;
  603. // margin-bottom: 20px;
  604. gap: 16px;
  605. padding-bottom: 10px;
  606. // border-bottom: 1px solid #f0f0f0;
  607. }
  608. .product-image {
  609. flex-shrink: 0;
  610. }
  611. .product-img {
  612. border-radius: 4px;
  613. object-fit: cover;
  614. }
  615. .product-details {
  616. flex: 1;
  617. display: flex;
  618. flex-direction: column;
  619. gap: 6px;
  620. }
  621. .product-name {
  622. font-size: 14px;
  623. font-weight: 500;
  624. color: #333;
  625. line-height: 20px;
  626. }
  627. .product-spec {
  628. font-size: 13px;
  629. color: #666;
  630. line-height: 18px;
  631. }
  632. .product-price {
  633. font-size: 14px;
  634. font-weight: 500;
  635. color: #333;
  636. line-height: 20px;
  637. }
  638. .info-content {
  639. display: flex;
  640. flex-direction: column;
  641. gap: 12px;
  642. }
  643. .info-row {
  644. display: flex;
  645. flex-wrap: wrap;
  646. align-items: center;
  647. gap: 24px;
  648. margin-top: 4px;
  649. }
  650. .info-item {
  651. display: flex;
  652. align-items: center;
  653. line-height: 24px;
  654. }
  655. .info-label {
  656. color: #666;
  657. margin-right: 8px;
  658. white-space: nowrap;
  659. // min-width: 80px;
  660. }
  661. .info-value {
  662. color: #333;
  663. font-weight: 400;
  664. }
  665. .title-badge {
  666. display: inline-block;
  667. width: 18px;
  668. height: 18px;
  669. line-height: 18px;
  670. text-align: center;
  671. background: #ffc107;
  672. color: #333;
  673. border-radius: 2px;
  674. margin-left: 6px;
  675. font-size: 12px;
  676. font-weight: 600;
  677. vertical-align: middle;
  678. }
  679. .title-count {
  680. margin-left: 8px;
  681. font-size: 14px;
  682. font-weight: 400;
  683. color: #666;
  684. }
  685. .logistics-content {
  686. display: flex;
  687. flex-direction: column;
  688. gap: 12px;
  689. }
  690. .logistics-tracking {
  691. display: flex;
  692. align-items: center;
  693. gap: 12px;
  694. line-height: 24px;
  695. }
  696. .tracking-number {
  697. font-weight: 600;
  698. color: #333;
  699. }
  700. .copy-link {
  701. color: #1890ff;
  702. cursor: pointer;
  703. text-decoration: none;
  704. font-weight: 400;
  705. }
  706. .copy-link:hover {
  707. text-decoration: underline;
  708. }
  709. .logistics-recipient {
  710. display: flex;
  711. align-items: flex-start;
  712. line-height: 24px;
  713. }
  714. .receive-icon {
  715. color: #ff4d4f;
  716. font-weight: 500;
  717. margin-right: 8px;
  718. flex-shrink: 0;
  719. }
  720. .recipient-info {
  721. color: #333;
  722. font-weight: 400;
  723. flex: 1;
  724. }
  725. .package-title {
  726. display: flex;
  727. align-items: center;
  728. gap: 6px;
  729. }
  730. .star-icon {
  731. color: #ffc107;
  732. font-size: 20px;
  733. margin-right: 0;
  734. }
  735. .package-items {
  736. display: flex;
  737. flex-direction: column;
  738. gap: 16px;
  739. width: 25%;
  740. }
  741. .package-item {
  742. display: flex;
  743. align-items: flex-start;
  744. gap: 12px;
  745. }
  746. .package-item-image {
  747. flex-shrink: 0;
  748. }
  749. .package-item-placeholder {
  750. flex-shrink: 0;
  751. width: 60px;
  752. height: 60px;
  753. display: flex;
  754. align-items: center;
  755. justify-content: center;
  756. background: #f5f5f5;
  757. border-radius: 4px;
  758. }
  759. .item-img {
  760. border-radius: 4px;
  761. object-fit: cover;
  762. }
  763. .package-item-details {
  764. flex: 1;
  765. display: flex;
  766. flex-direction: column;
  767. gap: 6px;
  768. min-width: 0;
  769. }
  770. .package-item-name {
  771. font-size: 14px;
  772. font-weight: 500;
  773. color: #333;
  774. line-height: 20px;
  775. }
  776. .package-item-spec {
  777. font-size: 13px;
  778. color: #666;
  779. line-height: 18px;
  780. }
  781. .package-item-price {
  782. font-size: 14px;
  783. font-weight: 500;
  784. color: #333;
  785. line-height: 20px;
  786. text-align: end;
  787. }
  788. .package-item-quantity {
  789. font-size: 14px;
  790. color: #666;
  791. // margin-left: auto;
  792. flex-shrink: 0;
  793. padding-top: 2px;
  794. text-align: end;
  795. }
  796. .detail-item {
  797. display: flex;
  798. margin-bottom: 20px;
  799. .label {
  800. // width: 80px;
  801. margin-right: 10px;
  802. }
  803. .content {
  804. // flex: 1;
  805. .service-image {
  806. max-width: 200px;
  807. height: auto;
  808. }
  809. }
  810. }
  811. }
  812. .derivation-label {
  813. font-weight: 500;
  814. color: #222;
  815. margin-right: 8px;
  816. white-space: nowrap;
  817. font-size: 13px;
  818. }
  819. .derivation-item {
  820. display: flex;
  821. align-items: center;
  822. min-width: 200px;
  823. max-width: 300px;
  824. padding: 6px 12px;
  825. background: #f5f5f5;
  826. border-radius: 4px;
  827. border: 1px solid #e8e8e8;
  828. }
  829. .derivation-content {
  830. color: #555;
  831. font-size: 13px;
  832. flex: 1;
  833. word-break: break-all;
  834. }
  835. .derivation-container {
  836. display: flex;
  837. flex-wrap: wrap;
  838. gap: 16px;
  839. align-items: flex-start;
  840. }
  841. .video-preview-container {
  842. display: flex;
  843. flex-direction: column;
  844. align-items: center;
  845. justify-content: center;
  846. width: 120px;
  847. height: 100%;
  848. padding: 10px;
  849. background: #fafafa;
  850. border-radius: 8px;
  851. }
  852. .video-thumbnail {
  853. position: relative;
  854. width: 100%;
  855. height: 100px;
  856. border-radius: 6px;
  857. overflow: hidden;
  858. background: #000;
  859. display: flex;
  860. align-items: center;
  861. justify-content: center;
  862. margin-bottom: 8px;
  863. }
  864. .video-thumbnail .video-preview {
  865. width: 100%;
  866. height: 100%;
  867. object-fit: cover;
  868. }
  869. .video-overlay {
  870. position: absolute;
  871. top: 0;
  872. left: 0;
  873. width: 100%;
  874. height: 100%;
  875. background: rgba(0, 0, 0, 0.7);
  876. border-radius: 6px;
  877. display: flex;
  878. align-items: center;
  879. justify-content: center;
  880. gap: 8px;
  881. opacity: 0;
  882. transition: opacity 0.3s ease;
  883. }
  884. .video-preview-container:hover .video-overlay {
  885. opacity: 1;
  886. }
  887. .video-overlay .ant-btn {
  888. background: rgba(255, 255, 255, 0.95);
  889. border: none;
  890. color: #333;
  891. font-size: 11px;
  892. padding: 3px 8px;
  893. border-radius: 4px;
  894. transition: all 0.3s ease;
  895. min-width: 40px;
  896. height: 28px;
  897. line-height: 1.2;
  898. display: flex;
  899. align-items: center;
  900. justify-content: center;
  901. white-space: nowrap;
  902. }
  903. .video-overlay .ant-btn:hover {
  904. background: rgba(255, 255, 255, 1);
  905. transform: scale(1.05);
  906. }
  907. .video-overlay .ant-btn-danger {
  908. background: rgba(255, 77, 79, 0.95);
  909. color: #fff;
  910. }
  911. .video-overlay .ant-btn-danger:hover {
  912. background: rgba(255, 77, 79, 1);
  913. }
  914. .video-info {
  915. text-align: center;
  916. font-size: 11px;
  917. color: #666;
  918. }
  919. .video-name {
  920. font-weight: 500;
  921. color: #333;
  922. margin-bottom: 2px;
  923. }
  924. .video-size {
  925. color: #999;
  926. font-size: 10px;
  927. }
  928. .package-items-wrapper {
  929. border: 1px solid #e8e8e8;
  930. padding: 10px;
  931. margin-top: 10px;
  932. }
  933. </style>