detail.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251
  1. <script setup>
  2. import { computed, ref } from 'vue';
  3. import { onLoad } from '@dcloudio/uni-app';
  4. import { useUserStore } from '@/stores/user';
  5. import { request, parseResponseJson } from '@/utils/request';
  6. import { formatDateTime } from '@/utils/date';
  7. import { API_BASE, TOKEN_KEY } from '@/config/api';
  8. import PhoneDialInline from '@/components/phone-dial-inline.vue';
  9. import { DEFAULT_PLACEHOLDER_IMAGE_URL, staticAssetUrl } from '@/utils/staticAsset';
  10. import ClientImageLoader from '@/components/client/client-image-loader.vue';
  11. import SqCopyBtn from '@/components/sq-copy-btn.vue';
  12. const userStore = useUserStore();
  13. const ICON_SECTION_PROOF = staticAssetUrl('funct.svg');
  14. const ICON_SECTION_DELIVER = staticAssetUrl('trant.svg');
  15. const ICON_SECTION_GOODS = staticAssetUrl('product-icon.svg');
  16. const taskId = ref('');
  17. const loading = ref(true);
  18. const loadError = ref('');
  19. const taskNo = ref('');
  20. const status = ref('');
  21. const supplierPrepareStatus = ref('');
  22. const effectiveTime = ref('');
  23. const order = ref(null);
  24. const deliver = ref(null);
  25. const items = ref([]);
  26. /** 备货明细报价小计之和(与列表卡片 supplierQuoteTotal 口径一致) */
  27. const supplierQuoteTotal = computed(() => {
  28. let sum = 0;
  29. for (const row of items.value) {
  30. const v = row.supplierQuoteLine;
  31. if (v != null && !Number.isNaN(Number(v))) {
  32. sum += Number(v);
  33. }
  34. }
  35. return Math.round(sum * 100) / 100;
  36. });
  37. /** 备货明细总数量(用于供货合计前置展示) */
  38. const totalQuantityText = computed(() => {
  39. let sum = 0;
  40. for (const row of items.value) {
  41. const n = Number(row.quantity ?? 0);
  42. if (Number.isFinite(n))
  43. sum += n;
  44. }
  45. return Number.isInteger(sum) ? String(sum) : String(sum).replace(/\.?0+$/, '');
  46. });
  47. const statusText = {
  48. pending: '待分配',
  49. assigned: '待备货',
  50. supplier_stocking: '备货中',
  51. supplier_ready: '备货完成(待取货)',
  52. picking: '取货/配送中',
  53. picked: '待取货',
  54. delivering: '配送中',
  55. completed: '已完成',
  56. cancelled: '已取消'
  57. };
  58. const prepareStatusText = {
  59. pending_prepare: '待备货',
  60. stocking: '备货中',
  61. prepared: '备货完成(待取货)',
  62. picked_up: '已取货'
  63. };
  64. function statusLabel(s) {
  65. if (!s)
  66. return '-';
  67. return statusText[String(s).toLowerCase()] || s;
  68. }
  69. function headlinePrepareLabel() {
  70. const ps = String(supplierPrepareStatus.value || '').toLowerCase();
  71. if (ps && prepareStatusText[ps])
  72. return prepareStatusText[ps];
  73. return statusLabel(status.value);
  74. }
  75. function fmtMoney(n) {
  76. if (n == null || Number.isNaN(Number(n)))
  77. return '-';
  78. return `¥${Number(n).toFixed(2)}`;
  79. }
  80. /** 与询价单商品列表规格展示一致 */
  81. function formatMeasureText(measureValue, unitName) {
  82. const unitText = (unitName || '').trim();
  83. if (measureValue == null) {
  84. return unitText ? `- ${unitText}` : '-';
  85. }
  86. const measureNum = Number(measureValue);
  87. if (!Number.isNaN(measureNum)) {
  88. const measureText = String(measureNum).replace(/\.?0+$/, '');
  89. return unitText ? `${measureText} ${unitText}` : measureText;
  90. }
  91. const measureText = String(measureValue).trim();
  92. return unitText ? `${measureText} ${unitText}` : measureText || '-';
  93. }
  94. function lineSpecText(row) {
  95. const u = row.productUnitName || row.unitName || '';
  96. return formatMeasureText(row.measureValue ?? null, u);
  97. }
  98. /** 商品行右侧数量角标:X1、X2(大写 X + 数字) */
  99. function qtyXText(q) {
  100. const n = q == null ? NaN : Number(q);
  101. if (!Number.isFinite(n))
  102. return 'X0';
  103. const s = Number.isInteger(n) ? String(n) : String(n).replace(/\.?0+$/, '');
  104. return `X${s}`;
  105. }
  106. const acting = ref(false);
  107. const preparedImages = ref([]);
  108. const preparedImageFailed = ref({});
  109. function normalizeImageUrl(u) {
  110. const s = String(u || '').trim();
  111. if (!s)
  112. return '';
  113. if (/^https?:\/\//i.test(s) || s.startsWith('//'))
  114. return s;
  115. const origin = String(API_BASE || '').replace(/\/$/, '');
  116. if (!origin)
  117. return s;
  118. const path = s.startsWith('/') ? s : `/${s}`;
  119. return `${origin}${path}`;
  120. }
  121. const canEditPreparedEvidence = computed(() => {
  122. const ps = String(supplierPrepareStatus.value || '').toLowerCase();
  123. return ps === 'prepared';
  124. });
  125. const isPickedUpReadonly = computed(() => {
  126. const ps = String(supplierPrepareStatus.value || '').toLowerCase();
  127. return ps === 'picked_up';
  128. });
  129. function canStartStockDetail() {
  130. const ps = String(supplierPrepareStatus.value || '').toLowerCase();
  131. if (ps === 'pending_prepare')
  132. return true;
  133. return !supplierPrepareStatus.value && String(status.value || '').toLowerCase() === 'assigned';
  134. }
  135. function canFinishStockDetail() {
  136. const ps = String(supplierPrepareStatus.value || '').toLowerCase();
  137. if (ps === 'stocking')
  138. return true;
  139. return !supplierPrepareStatus.value && String(status.value || '').toLowerCase() === 'supplier_stocking';
  140. }
  141. async function onStartStockDetail() {
  142. if (!taskId.value || acting.value)
  143. return;
  144. acting.value = true;
  145. try {
  146. const res = await request({
  147. url: `/mini/supplier/tasks/${taskId.value}/start-stock`,
  148. method: 'POST',
  149. data: {}
  150. });
  151. const body = parseResponseJson(res);
  152. if (body.code === 200) {
  153. uni.showToast({ title: '已开始备货', icon: 'success' });
  154. await loadDetail();
  155. }
  156. else {
  157. uni.showToast({ title: body.message || '操作失败', icon: 'none' });
  158. }
  159. }
  160. catch {
  161. uni.showToast({ title: '网络异常', icon: 'none' });
  162. }
  163. finally {
  164. acting.value = false;
  165. }
  166. }
  167. async function onFinishStockDetail() {
  168. if (!taskId.value || acting.value)
  169. return;
  170. if (!preparedImages.value.length) {
  171. uni.showToast({ title: '请先上传备货完成凭证照片', icon: 'none' });
  172. return;
  173. }
  174. acting.value = true;
  175. try {
  176. const res = await request({
  177. url: `/mini/supplier/tasks/${taskId.value}/finish-stock`,
  178. method: 'POST',
  179. data: { imageUrls: preparedImages.value, preparedRemark: '备货完成' }
  180. });
  181. const body = parseResponseJson(res);
  182. if (body.code === 200) {
  183. uni.showToast({
  184. title: canEditPreparedEvidence.value ? '备货凭证已更新' : '已确认备货完成',
  185. icon: 'success'
  186. });
  187. await loadDetail();
  188. }
  189. else {
  190. uni.showToast({ title: body.message || '操作失败', icon: 'none' });
  191. }
  192. }
  193. catch {
  194. uni.showToast({ title: '网络异常', icon: 'none' });
  195. }
  196. finally {
  197. acting.value = false;
  198. }
  199. }
  200. function parseUploadResult(raw) {
  201. try {
  202. return JSON.parse(raw);
  203. }
  204. catch {
  205. return {};
  206. }
  207. }
  208. async function uploadPreparedImages() {
  209. const remain = 6 - preparedImages.value.length;
  210. if (remain <= 0) {
  211. uni.showToast({ title: '最多上传6张', icon: 'none' });
  212. return;
  213. }
  214. const choose = await new Promise((resolve) => {
  215. uni.chooseImage({
  216. count: remain,
  217. sizeType: ['compressed'],
  218. sourceType: ['album', 'camera'],
  219. success: (res) => resolve(res),
  220. fail: () => resolve(null)
  221. });
  222. });
  223. if (!choose?.tempFilePaths?.length)
  224. return;
  225. const rawToken = uni.getStorageSync(TOKEN_KEY);
  226. const token = typeof rawToken === 'string' ? rawToken.trim() : '';
  227. uni.showLoading({ title: '上传中...' });
  228. try {
  229. for (const filePath of choose.tempFilePaths) {
  230. const uploadRes = await new Promise((resolve, reject) => {
  231. uni.uploadFile({
  232. url: `${API_BASE}/upload/image`,
  233. filePath,
  234. name: 'file',
  235. header: {
  236. Authorization: token ? `Bearer ${token}` : ''
  237. },
  238. formData: { folder: 'delivery-evidence' },
  239. success: resolve,
  240. fail: reject
  241. });
  242. });
  243. const body = parseUploadResult(typeof uploadRes.data === 'string' ? uploadRes.data : String(uploadRes.data));
  244. if (body.code !== 200 || !body.data?.url) {
  245. uni.showToast({ title: body.message || '上传失败', icon: 'none' });
  246. return;
  247. }
  248. preparedImages.value.push(normalizeImageUrl(body.data.url));
  249. }
  250. }
  251. finally {
  252. uni.hideLoading();
  253. }
  254. }
  255. function removePreparedImage(idx) {
  256. const removed = preparedImages.value[idx];
  257. if (removed) {
  258. const key = String(removed);
  259. if (preparedImageFailed.value[key]) {
  260. delete preparedImageFailed.value[key];
  261. }
  262. }
  263. preparedImages.value.splice(idx, 1);
  264. }
  265. function previewPreparedImage(idx) {
  266. if (!preparedImages.value.length)
  267. return;
  268. uni.previewImage({
  269. current: preparedImages.value[idx],
  270. urls: preparedImages.value
  271. });
  272. }
  273. function onPreparedImageError(url) {
  274. const key = String(url || '').trim();
  275. if (!key)
  276. return;
  277. preparedImageFailed.value[key] = true;
  278. }
  279. function onPreparedImageLoad(url) {
  280. const key = String(url || '').trim();
  281. if (!key)
  282. return;
  283. if (preparedImageFailed.value[key]) {
  284. delete preparedImageFailed.value[key];
  285. }
  286. }
  287. function goProductDetail(productId) {
  288. if (!productId) {
  289. uni.showToast({ title: '暂无商品信息', icon: 'none' });
  290. return;
  291. }
  292. uni.navigateTo({
  293. url: `/pages/supplier/product/detail?productId=${encodeURIComponent(productId)}&viewOnly=1`
  294. });
  295. }
  296. /** 订单侧与平台(商家结算)结算状态,与列表卡片口径一致 */
  297. function supplierSettlementSettled() {
  298. return String(order.value?.supplierSettlementStatus || '').toLowerCase() === 'settled';
  299. }
  300. function supplierSettlementStatusText() {
  301. return supplierSettlementSettled() ? '已与平台结算' : '待平台结算';
  302. }
  303. function supplierSettlementTimeText() {
  304. if (!supplierSettlementSettled())
  305. return '—';
  306. const t = order.value?.supplierSettlementAt;
  307. if (t == null || String(t).trim() === '')
  308. return '—';
  309. return formatDateTime(String(t));
  310. }
  311. async function loadDetail() {
  312. const id = taskId.value;
  313. if (!id) {
  314. loadError.value = '缺少任务ID';
  315. loading.value = false;
  316. return;
  317. }
  318. loading.value = true;
  319. loadError.value = '';
  320. try {
  321. const res = await request({
  322. url: `/mini/supplier/tasks/${id}`,
  323. method: 'GET'
  324. });
  325. const body = parseResponseJson(res);
  326. if (body.code !== 200 || !body.data) {
  327. loadError.value = body.message || '加载失败';
  328. return;
  329. }
  330. const d = body.data;
  331. taskNo.value = d.taskNo || '';
  332. status.value = d.status || '';
  333. supplierPrepareStatus.value = d.supplierPrepareStatus || '';
  334. effectiveTime.value = d.effectiveTime || '';
  335. order.value = d.order || null;
  336. deliver.value = d.deliver || null;
  337. items.value = Array.isArray(d.items) ? d.items : [];
  338. preparedImages.value = Array.isArray(d.preparedImages)
  339. ? d.preparedImages
  340. .map((x) => normalizeImageUrl(x))
  341. .filter((x) => !!x)
  342. : [];
  343. preparedImageFailed.value = {};
  344. }
  345. catch {
  346. loadError.value = '网络异常,请稍后重试';
  347. }
  348. finally {
  349. loading.value = false;
  350. }
  351. }
  352. onLoad((query) => {
  353. taskId.value = query.id || '';
  354. if (!userStore.isLoggedIn) {
  355. uni.reLaunch({ url: '/pages/login/login' });
  356. return;
  357. }
  358. if (!userStore.isSupplier) {
  359. uni.showToast({ title: '仅供应商可访问', icon: 'none' });
  360. setTimeout(() => userStore.navigateToHome(), 1200);
  361. return;
  362. }
  363. void loadDetail();
  364. });
  365. </script>
  366. <template>
  367. <view class="page">
  368. <view v-if="loading" class="state">
  369. <ClientSkeleton variant="detail" :rows="3" />
  370. </view>
  371. <view v-else-if="loadError" class="state">
  372. <text>{{ loadError }}</text>
  373. </view>
  374. <scroll-view v-else scroll-y class="scroll">
  375. <view class="hero">
  376. <view class="hero-title-row">
  377. <text class="hero-title">{{ taskNo ? `任务号:${taskNo}` : '备货任务' }}</text>
  378. <SqCopyBtn v-if="taskNo" class="hero-copy" :text="taskNo" label="任务号" icon-only />
  379. </view>
  380. <text class="hero-sub">{{ headlinePrepareLabel() }}</text>
  381. <text v-if="effectiveTime" class="hero-time">送达客户截止时间:{{ effectiveTime }}</text>
  382. <view v-if="canStartStockDetail() || canFinishStockDetail()" class="hero-actions" @click.stop>
  383. <view v-if="canFinishStockDetail()" class="evidence-box">
  384. <text class="evidence-title">请先在下方「备货完成凭证」区域上传照片</text>
  385. </view>
  386. <button
  387. v-if="canStartStockDetail()"
  388. class="hero-btn hero-btn--outline"
  389. :disabled="acting"
  390. @click="onStartStockDetail"
  391. >
  392. 开始备货
  393. </button>
  394. <button
  395. v-if="canFinishStockDetail()"
  396. class="hero-btn hero-btn--solid"
  397. :disabled="acting"
  398. @click="onFinishStockDetail"
  399. >
  400. 备货完成
  401. </button>
  402. </view>
  403. </view>
  404. <view class="card">
  405. <view class="card-title-row">
  406. <image class="card-title-icon" :src="ICON_SECTION_PROOF" mode="aspectFit" />
  407. <text class="card-title">备货完成凭证</text>
  408. </view>
  409. <text class="card-hint">
  410. {{ isPickedUpReadonly ? '配送员已取货,凭证已锁定为只读。' : '放在客户订单信息前,便于先上传并核对备货照片。' }}
  411. </text>
  412. <view class="evidence-list">
  413. <view
  414. v-for="(url, idx) in preparedImages"
  415. :key="`proof-${idx}`"
  416. class="evidence-item"
  417. >
  418. <ClientImageLoader
  419. class="evidence-img"
  420. :src="url"
  421. mode="aspectFill"
  422. radius="8rpx"
  423. @click="previewPreparedImage(idx)"
  424. @error="onPreparedImageError(url)"
  425. @load="onPreparedImageLoad(url)"
  426. />
  427. <view v-if="preparedImageFailed[url]" class="evidence-img-error">加载失败</view>
  428. <view
  429. v-if="canFinishStockDetail() || canEditPreparedEvidence"
  430. class="evidence-del-btn"
  431. @click.stop="removePreparedImage(idx)"
  432. >
  433. 删除
  434. </view>
  435. </view>
  436. <view
  437. v-if="canFinishStockDetail() || canEditPreparedEvidence"
  438. class="evidence-add"
  439. @click="uploadPreparedImages"
  440. >
  441. + 上传
  442. </view>
  443. </view>
  444. <view v-if="canEditPreparedEvidence" class="evidence-actions">
  445. <button
  446. class="save-proof-btn"
  447. :disabled="acting || preparedImages.length === 0"
  448. @click="onFinishStockDetail"
  449. >
  450. 保存凭证
  451. </button>
  452. </view>
  453. <view v-if="order" class="settlement-under-proof">
  454. <view class="cell">
  455. <text class="k">平台结算状态</text>
  456. <view class="v settlement-val">
  457. <view
  458. class="settlement-tag"
  459. :class="supplierSettlementSettled() ? 'settlement-tag--done' : 'settlement-tag--wait'"
  460. >
  461. <text class="settlement-tag-text">{{ supplierSettlementStatusText() }}</text>
  462. </view>
  463. </view>
  464. </view>
  465. <view class="cell">
  466. <text class="k">结算时间</text>
  467. <view class="v settlement-val">
  468. <text class="settlement-time-text">{{ supplierSettlementTimeText() }}</text>
  469. </view>
  470. </view>
  471. </view>
  472. </view>
  473. <view v-if="(order && order.orderNo) || (deliver && (deliver.deliverName || deliver.phone))" class="card">
  474. <view class="card-title-row">
  475. <image class="card-title-icon" :src="ICON_SECTION_DELIVER" mode="aspectFit" />
  476. <text class="card-title">配送信息</text>
  477. </view>
  478. <view v-if="order && order.orderNo" class="cell">
  479. <text class="k">客户订单号</text>
  480. <view class="v copy-v">
  481. <text class="copy-v-text">{{ order.orderNo }}</text>
  482. <SqCopyBtn class="copy-btn" :text="order.orderNo" label="客户订单号" icon-only />
  483. </view>
  484. </view>
  485. <view v-if="deliver && (deliver.deliverName || deliver.phone)" class="cell">
  486. <text class="k">配送员姓名</text>
  487. <text class="v">{{ deliver.deliverName || '-' }}</text>
  488. </view>
  489. <view v-if="deliver && (deliver.deliverName || deliver.phone)" class="cell">
  490. <text class="k">配送员电话</text>
  491. <view class="v phone-line">
  492. <text class="phone-line-text">{{ deliver.phone || '-' }}</text>
  493. <PhoneDialInline class="dial" :phone="deliver.phone" empty-text="-" icon-only color="#189b65" />
  494. </view>
  495. </view>
  496. </view>
  497. <view class="goods-panel-head">
  498. <view class="gdp-head-line">
  499. <view class="gdp-title-row">
  500. <image class="gdp-title-icon" :src="ICON_SECTION_GOODS" mode="aspectFit" />
  501. <text class="gdp-title">备货明细</text>
  502. </view>
  503. </view>
  504. </view>
  505. <view class="card card--goods-block">
  506. <view v-if="items.length > 0" class="goods-total-bar goods-total-bar--top">
  507. <view class="goods-total-inline">
  508. <text class="goods-total-label">总数量</text>
  509. <text class="goods-total-qty">{{ totalQuantityText }}</text>
  510. <text class="goods-total-sep">|</text>
  511. <text class="goods-total-label">供货合计(元)</text>
  512. <text class="goods-total-value">{{ fmtMoney(supplierQuoteTotal) }}</text>
  513. </view>
  514. </view>
  515. <view class="goods-pane">
  516. <view
  517. v-for="row in items"
  518. :key="row.id"
  519. class="goods-card"
  520. @click="goProductDetail(row.productId)"
  521. >
  522. <ClientImageLoader
  523. class="goods-image"
  524. :src="row.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL"
  525. mode="aspectFill"
  526. radius="18rpx"
  527. />
  528. <view class="goods-main">
  529. <view class="goods-name-row">
  530. <text class="goods-name">{{ row.productName || '—' }}</text>
  531. <text class="goods-line-tag">备货</text>
  532. </view>
  533. <view class="goods-spec-row">
  534. <text class="goods-spec">{{ lineSpecText(row) }}</text>
  535. <view class="goods-qty-wrap">
  536. <text class="goods-qty-label">数量</text>
  537. <text class="goods-qty-value">{{ qtyXText(row.quantity) }}</text>
  538. </view>
  539. </view>
  540. <view class="goods-line">
  541. <text v-if="(row.deliveredQuantity ?? 0) > 0" class="goods-delivered-tag">
  542. 已送达 {{ row.deliveredQuantity }}{{ row.unitName || '' }}
  543. </text>
  544. </view>
  545. <view class="price-block">
  546. <view class="price-row-inner price-row-inner--value">
  547. <text class="price-val">{{ fmtMoney(row.supplierQuoteUnit ?? null) }}</text>
  548. </view>
  549. <view class="price-row-inner price-row-inner--value strong">
  550. <text class="price-val">{{ fmtMoney(row.supplierQuoteLine ?? null) }}</text>
  551. </view>
  552. </view>
  553. </view>
  554. </view>
  555. <view v-if="items.length === 0" class="empty-lines">
  556. <text>暂无明细</text>
  557. </view>
  558. </view>
  559. </view>
  560. </scroll-view>
  561. </view>
  562. </template>
  563. <style lang="scss" scoped>
  564. .page {
  565. min-height: 100vh;
  566. background: #f3f7f5;
  567. }
  568. .state {
  569. padding: 24rpx 20rpx;
  570. min-height: 0;
  571. }
  572. .scroll {
  573. height: 100vh;
  574. box-sizing: border-box;
  575. }
  576. .hero {
  577. margin: 20rpx;
  578. padding: 28rpx 24rpx;
  579. border-radius: 16rpx;
  580. background: linear-gradient(145deg, #087f57, #0a9f6d);
  581. color: #fff;
  582. }
  583. .hero-title {
  584. display: block;
  585. font-size: 32rpx;
  586. font-weight: 600;
  587. margin-bottom: 8rpx;
  588. }
  589. .hero-title-row {
  590. display: flex;
  591. align-items: center;
  592. justify-content: space-between;
  593. gap: 10px;
  594. }
  595. .hero-title-row .hero-title {
  596. flex: 0 1 auto;
  597. min-width: 0;
  598. margin-bottom: 0;
  599. overflow: hidden;
  600. text-overflow: ellipsis;
  601. white-space: nowrap;
  602. }
  603. .hero-sub {
  604. display: block;
  605. font-size: 26rpx;
  606. opacity: 0.95;
  607. }
  608. .hero-time {
  609. display: block;
  610. margin-top: 12rpx;
  611. font-size: 24rpx;
  612. opacity: 0.9;
  613. }
  614. .hero-actions {
  615. display: flex;
  616. flex-wrap: wrap;
  617. gap: 16rpx;
  618. margin-top: 20rpx;
  619. }
  620. .hero-btn {
  621. flex: 1;
  622. min-width: 200rpx;
  623. height: 72rpx;
  624. line-height: 72rpx;
  625. font-size: 28rpx;
  626. border-radius: 12rpx;
  627. padding: 0;
  628. margin: 0;
  629. border: none;
  630. }
  631. .hero-btn::after {
  632. border: none;
  633. }
  634. .hero-btn--outline {
  635. background: rgba(255, 255, 255, 0.95);
  636. color: #087f57;
  637. font-weight: 600;
  638. }
  639. .hero-btn--solid {
  640. background: #fff;
  641. color: #087f57;
  642. font-weight: 600;
  643. }
  644. .evidence-box {
  645. width: 100%;
  646. background: rgba(255, 255, 255, 0.16);
  647. border-radius: 12rpx;
  648. padding: 14rpx;
  649. }
  650. .evidence-title {
  651. display: block;
  652. font-size: 22rpx;
  653. margin-bottom: 0;
  654. }
  655. .evidence-list {
  656. display: flex;
  657. flex-wrap: wrap;
  658. gap: 10rpx;
  659. }
  660. .evidence-item,
  661. .evidence-img,
  662. .evidence-add {
  663. width: 96rpx;
  664. height: 96rpx;
  665. border-radius: 8rpx;
  666. }
  667. .evidence-item {
  668. position: relative;
  669. display: flex;
  670. flex-direction: column;
  671. align-items: stretch;
  672. gap: 8rpx;
  673. width: 96rpx;
  674. height: auto;
  675. }
  676. .evidence-img {
  677. width: 100%;
  678. height: 96rpx;
  679. border-radius: 8rpx;
  680. }
  681. .evidence-del-btn {
  682. height: 34rpx;
  683. border-radius: 8rpx;
  684. background: #dc2626;
  685. color: #ffffff;
  686. font-size: 20rpx;
  687. font-weight: 600;
  688. display: flex;
  689. align-items: center;
  690. justify-content: center;
  691. }
  692. .evidence-img-error {
  693. height: 30rpx;
  694. border-radius: 6rpx;
  695. background: #fee2e2;
  696. color: #b91c1c;
  697. font-size: 20rpx;
  698. display: flex;
  699. align-items: center;
  700. justify-content: center;
  701. }
  702. .evidence-add {
  703. background: rgba(255, 255, 255, 0.9);
  704. color: #087f57;
  705. display: flex;
  706. align-items: center;
  707. justify-content: center;
  708. font-size: 22rpx;
  709. }
  710. .evidence-actions {
  711. margin-top: 14rpx;
  712. }
  713. .save-proof-btn {
  714. margin: 0;
  715. height: 64rpx;
  716. line-height: 64rpx;
  717. border: none;
  718. border-radius: 10rpx;
  719. background: #0a9f6d;
  720. color: #fff;
  721. font-size: 26rpx;
  722. }
  723. .card {
  724. margin: 0 20rpx 20rpx;
  725. padding: 24rpx;
  726. border-radius: 16rpx;
  727. background: #fff;
  728. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  729. }
  730. .card--goods {
  731. padding-bottom: 8rpx;
  732. }
  733. .phone-line {
  734. display: flex;
  735. align-items: center;
  736. justify-content: space-between;
  737. gap: 12rpx;
  738. }
  739. .phone-line-text {
  740. flex: 1;
  741. min-width: 0;
  742. word-break: break-all;
  743. }
  744. .dial :deep(.phone-dial-inline__btn) {
  745. width: 44rpx;
  746. height: 44rpx;
  747. background: transparent;
  748. border-radius: 0;
  749. }
  750. /* 同配送员详情页商品区域:标题头 + 商品块 */
  751. .goods-panel-head {
  752. margin: 0 20rpx;
  753. padding: 8rpx 16rpx 12rpx;
  754. border-radius: 14rpx 14rpx 0 0;
  755. background: linear-gradient(180deg, #eefaf5 0%, #f8fcfa 100%);
  756. border: 1rpx solid #d9e9e1;
  757. border-bottom: none;
  758. }
  759. .gdp-head-line {
  760. display: flex;
  761. align-items: center;
  762. justify-content: space-between;
  763. gap: 12rpx;
  764. }
  765. .gdp-title-row {
  766. display: inline-flex;
  767. align-items: center;
  768. gap: 10rpx;
  769. min-width: 0;
  770. }
  771. .gdp-title-icon {
  772. width: 30rpx;
  773. height: 30rpx;
  774. flex-shrink: 0;
  775. }
  776. .gdp-title {
  777. display: block;
  778. font-size: 28rpx;
  779. font-weight: 700;
  780. color: #1f2a26;
  781. }
  782. .gdp-tag {
  783. min-height: 40rpx;
  784. padding: 0 14rpx;
  785. border-radius: 999rpx;
  786. background: #e9f9f2;
  787. color: #0a9f6d;
  788. font-size: 20rpx;
  789. display: inline-flex;
  790. align-items: center;
  791. }
  792. .gdp-hint {
  793. display: block;
  794. margin-top: 6rpx;
  795. font-size: 22rpx;
  796. color: #6f7e77;
  797. line-height: 1.45;
  798. }
  799. .card--goods-block {
  800. margin: 0 20rpx 20rpx;
  801. padding: 0;
  802. border-radius: 0 0 14rpx 14rpx;
  803. border: 1rpx solid #d9e9e1;
  804. border-top: none;
  805. box-shadow: 0 4rpx 12rpx rgba(10, 159, 109, 0.06);
  806. }
  807. .card--goods-block .goods-pane {
  808. margin: 0;
  809. background: #fff;
  810. border-radius: 0 0 14rpx 14rpx;
  811. overflow: hidden;
  812. }
  813. .card--goods-block .goods-card {
  814. display: flex;
  815. padding: 20rpx 24rpx;
  816. border-bottom: 1rpx solid #e6efeb;
  817. background: #fff;
  818. }
  819. .card--goods-block .goods-card:active {
  820. background: #f4fbf8;
  821. }
  822. .card--goods-block .goods-image {
  823. width: 132rpx;
  824. height: 132rpx;
  825. border-radius: 14rpx;
  826. background: #f1f5f3;
  827. flex-shrink: 0;
  828. }
  829. .card--goods-block .goods-main {
  830. flex: 1;
  831. margin-left: 16rpx;
  832. min-width: 0;
  833. }
  834. .goods-name-row {
  835. display: flex;
  836. align-items: flex-start;
  837. justify-content: space-between;
  838. gap: 12rpx;
  839. }
  840. .goods-name {
  841. display: block;
  842. flex: 1;
  843. min-width: 0;
  844. font-size: 28rpx;
  845. color: #1f2a26;
  846. font-weight: 600;
  847. line-height: 1.35;
  848. overflow: hidden;
  849. text-overflow: ellipsis;
  850. white-space: nowrap;
  851. }
  852. .goods-line-tag {
  853. flex-shrink: 0;
  854. min-height: 34rpx;
  855. padding: 0 10rpx;
  856. border-radius: 999rpx;
  857. background: #f1f5f3;
  858. color: #6f7e77;
  859. font-size: 18rpx;
  860. display: inline-flex;
  861. align-items: center;
  862. }
  863. .goods-spec {
  864. display: block;
  865. margin-top: 0;
  866. font-size: 22rpx;
  867. color: #6f7e77;
  868. flex: 1;
  869. min-width: 0;
  870. }
  871. .goods-spec-row {
  872. margin-top: 8rpx;
  873. display: flex;
  874. align-items: center;
  875. justify-content: space-between;
  876. gap: 12rpx;
  877. }
  878. .goods-line {
  879. margin-top: 10rpx;
  880. display: flex;
  881. align-items: center;
  882. justify-content: flex-end;
  883. gap: 16rpx;
  884. }
  885. .goods-qty-wrap {
  886. min-height: 44rpx;
  887. padding: 0 12rpx;
  888. border-radius: 22rpx;
  889. border: 1rpx solid #b9decf;
  890. display: inline-flex;
  891. align-items: center;
  892. gap: 8rpx;
  893. background: #ffffff;
  894. }
  895. .goods-qty-label {
  896. font-size: 20rpx;
  897. color: #6f7e77;
  898. }
  899. .goods-qty-value {
  900. font-size: 28rpx;
  901. font-weight: 700;
  902. color: #087f57;
  903. }
  904. .goods-delivered-tag {
  905. min-height: 44rpx;
  906. padding: 0 12rpx;
  907. border-radius: 22rpx;
  908. background: #fff4dd;
  909. color: #cf8a19;
  910. font-size: 20rpx;
  911. display: inline-flex;
  912. align-items: center;
  913. }
  914. .goods-total-bar {
  915. padding: 20rpx 24rpx 24rpx;
  916. display: flex;
  917. align-items: center;
  918. justify-content: space-between;
  919. border-top: 1rpx solid #e6efeb;
  920. background: #f8fcfa;
  921. }
  922. .goods-total-label {
  923. font-size: 28rpx;
  924. color: #52615b;
  925. font-weight: 600;
  926. }
  927. .goods-total-value {
  928. font-size: 34rpx;
  929. color: #087f57;
  930. font-weight: 700;
  931. }
  932. .card-title {
  933. display: block;
  934. font-size: 30rpx;
  935. font-weight: 600;
  936. color: #303133;
  937. margin-bottom: 16rpx;
  938. }
  939. .card-title-row {
  940. display: flex;
  941. align-items: center;
  942. gap: 10rpx;
  943. margin-bottom: 16rpx;
  944. }
  945. .card-title-icon {
  946. width: 30rpx;
  947. height: 30rpx;
  948. flex-shrink: 0;
  949. }
  950. .card-title-row .card-title {
  951. margin-bottom: 0;
  952. }
  953. .card-hint {
  954. display: block;
  955. font-size: 22rpx;
  956. color: #909399;
  957. margin-bottom: 16rpx;
  958. line-height: 1.5;
  959. }
  960. .settlement-under-proof {
  961. margin-top: 20rpx;
  962. padding-top: 8rpx;
  963. border-top: 1rpx solid #eef5f2;
  964. }
  965. .settlement-under-proof .cell:first-child {
  966. padding-top: 8rpx;
  967. }
  968. .settlement-val {
  969. display: flex;
  970. justify-content: flex-end;
  971. align-items: center;
  972. min-width: 0;
  973. }
  974. .settlement-time-text {
  975. font-size: 26rpx;
  976. color: #303133;
  977. text-align: right;
  978. word-break: break-all;
  979. }
  980. .settlement-tag {
  981. padding: 6rpx 18rpx;
  982. border-radius: 8rpx;
  983. flex-shrink: 0;
  984. max-width: 100%;
  985. }
  986. .settlement-tag-text {
  987. font-size: 22rpx;
  988. font-weight: 600;
  989. line-height: 1.3;
  990. }
  991. .settlement-tag--done {
  992. background: #ecfdf5;
  993. }
  994. .settlement-tag--done .settlement-tag-text {
  995. color: #059669;
  996. }
  997. .settlement-tag--wait {
  998. background: #fff7e8;
  999. }
  1000. .settlement-tag--wait .settlement-tag-text {
  1001. color: #d97706;
  1002. }
  1003. .cell {
  1004. display: flex;
  1005. align-items: flex-start;
  1006. padding: 14rpx 0;
  1007. border-bottom: 1rpx solid #f0f0f0;
  1008. font-size: 26rpx;
  1009. }
  1010. .cell:last-child {
  1011. border-bottom: none;
  1012. }
  1013. .k {
  1014. width: 200rpx;
  1015. flex-shrink: 0;
  1016. color: #909399;
  1017. }
  1018. .v {
  1019. flex: 1;
  1020. color: #303133;
  1021. word-break: break-all;
  1022. }
  1023. .copy-v {
  1024. display: flex;
  1025. align-items: center;
  1026. justify-content: space-between;
  1027. gap: 12rpx;
  1028. }
  1029. .copy-v-text {
  1030. flex: 1;
  1031. min-width: 0;
  1032. overflow: hidden;
  1033. text-overflow: ellipsis;
  1034. white-space: nowrap;
  1035. }
  1036. .copy-btn {
  1037. flex-shrink: 0;
  1038. }
  1039. /* 对齐询价单商品区域:左图右文、底部分割线 */
  1040. .goods-pane {
  1041. margin: 0;
  1042. padding: 16rpx;
  1043. background: #f8fcfa;
  1044. border-radius: 0 0 16rpx 16rpx;
  1045. }
  1046. .goods-card {
  1047. display: flex;
  1048. padding: 20rpx;
  1049. margin-bottom: 14rpx;
  1050. border: 1rpx solid #e2efe9;
  1051. border-radius: 14rpx;
  1052. background: #fff;
  1053. box-shadow: 0 6rpx 14rpx rgba(10, 159, 109, 0.06);
  1054. }
  1055. .goods-card:active {
  1056. background: #f4fbf8;
  1057. border-color: #cfe8dd;
  1058. }
  1059. .goods-card:last-child {
  1060. margin-bottom: 0;
  1061. }
  1062. .goods-image {
  1063. width: 132rpx;
  1064. height: 132rpx;
  1065. border-radius: 14rpx;
  1066. background: #eef4f1;
  1067. border: 1rpx solid #e1ece7;
  1068. flex-shrink: 0;
  1069. }
  1070. .goods-main {
  1071. flex: 1;
  1072. margin-left: 16rpx;
  1073. min-width: 0;
  1074. }
  1075. .goods-name {
  1076. display: block;
  1077. font-size: 28rpx;
  1078. color: #1f2a26;
  1079. font-weight: 600;
  1080. line-height: 1.35;
  1081. overflow: hidden;
  1082. text-overflow: ellipsis;
  1083. white-space: nowrap;
  1084. }
  1085. .goods-spec {
  1086. display: block;
  1087. margin-top: 8rpx;
  1088. font-size: 22rpx;
  1089. color: #6f7e77;
  1090. }
  1091. .task-meta {
  1092. margin-top: 12rpx;
  1093. }
  1094. .task-meta-line {
  1095. display: block;
  1096. font-size: 24rpx;
  1097. color: #606266;
  1098. line-height: 1.45;
  1099. }
  1100. .task-meta-line--qty {
  1101. display: flex;
  1102. flex-wrap: nowrap;
  1103. align-items: center;
  1104. justify-content: space-between;
  1105. gap: 16rpx;
  1106. width: 100%;
  1107. }
  1108. .task-qty-label {
  1109. flex-shrink: 0;
  1110. font-size: 24rpx;
  1111. color: #606266;
  1112. }
  1113. .task-qty-value {
  1114. flex-shrink: 0;
  1115. margin-left: auto;
  1116. font-size: 34rpx;
  1117. font-weight: 700;
  1118. color: #087f57;
  1119. letter-spacing: 0.02em;
  1120. }
  1121. .price-block {
  1122. margin-top: 12rpx;
  1123. padding: 12rpx 14rpx;
  1124. background: linear-gradient(180deg, #f8fcfa 0%, #f3f9f6 100%);
  1125. border: 1rpx solid #e3f0ea;
  1126. border-radius: 12rpx;
  1127. }
  1128. .price-row-inner {
  1129. display: flex;
  1130. justify-content: space-between;
  1131. align-items: center;
  1132. padding: 4rpx 0;
  1133. }
  1134. .price-row-inner--value {
  1135. justify-content: flex-end;
  1136. }
  1137. .price-row-inner.strong .price-val {
  1138. font-weight: 700;
  1139. font-size: 30rpx;
  1140. }
  1141. .price-label {
  1142. font-size: 24rpx;
  1143. color: #909399;
  1144. }
  1145. .price-val {
  1146. font-size: 26rpx;
  1147. color: #0a9f6d;
  1148. }
  1149. .price-val--strong {
  1150. font-weight: 700;
  1151. font-size: 30rpx;
  1152. }
  1153. .empty-lines {
  1154. padding: 44rpx 24rpx 36rpx;
  1155. text-align: center;
  1156. color: #9cb0a6;
  1157. font-size: 26rpx;
  1158. }
  1159. .goods-total-bar {
  1160. margin: 0 -24rpx 0;
  1161. padding: 20rpx 24rpx 24rpx;
  1162. display: flex;
  1163. align-items: center;
  1164. justify-content: flex-start;
  1165. border-top: 1rpx solid #eceff3;
  1166. background: #f8fcfa;
  1167. }
  1168. .goods-total-bar--top {
  1169. margin: 0;
  1170. border-top: none;
  1171. border-bottom: 1rpx solid #eceff3;
  1172. border-radius: 0;
  1173. }
  1174. .goods-total-inline {
  1175. display: inline-flex;
  1176. align-items: baseline;
  1177. gap: 10rpx;
  1178. }
  1179. .goods-total-qty {
  1180. font-size: 30rpx;
  1181. color: #1f2a26;
  1182. font-weight: 700;
  1183. }
  1184. .goods-total-sep {
  1185. font-size: 24rpx;
  1186. color: #b4c3bc;
  1187. padding: 0 4rpx;
  1188. }
  1189. .goods-total-label {
  1190. font-size: 28rpx;
  1191. color: #606266;
  1192. font-weight: 500;
  1193. }
  1194. .goods-total-value {
  1195. font-size: 34rpx;
  1196. color: #087f57;
  1197. font-weight: 700;
  1198. }
  1199. </style>