delivery.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. <script setup>
  2. import { computed, onUnmounted, ref, watch } from 'vue';
  3. import { onLoad, onShow } 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 { formatYuan } from '@/utils/format';
  8. import PhoneDialInline from '@/components/phone-dial-inline.vue';
  9. import { DEFAULT_PLACEHOLDER_IMAGE_URL, IMG_EMPTY } from '@/utils/staticAsset';
  10. import ClientSkeleton from '@/components/client/client-skeleton.vue';
  11. import SqCopyBtn from '@/components/sq-copy-btn.vue';
  12. const userStore = useUserStore();
  13. const PAGE_SIZE = 10;
  14. function taskItemImage(it) {
  15. return it.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL;
  16. }
  17. function taskQtyText(it) {
  18. const q = Number(it?.quantity ?? 0);
  19. if (!Number.isFinite(q))
  20. return 'x0';
  21. const n = Number.isInteger(q) ? String(q) : String(q).replace(/\.?0+$/, '');
  22. return `x${n}`;
  23. }
  24. function formatTaskTotalQty(row) {
  25. const n = row.totalQuantity;
  26. if (n != null && !Number.isNaN(Number(n))) {
  27. const num = Number(n);
  28. if (Number.isInteger(num))
  29. return String(num);
  30. return num.toFixed(3).replace(/\.?0+$/, '') || '0';
  31. }
  32. const list = row.items || [];
  33. const sum = list.reduce((acc, it) => acc + (Number(it.quantity) || 0), 0);
  34. if (Number.isInteger(sum))
  35. return String(sum);
  36. return String(sum);
  37. }
  38. function taskFooterTime(row) {
  39. if (row.effectiveTime)
  40. return `送达截止 ${row.effectiveTime}`;
  41. if (row.createAt)
  42. return formatDateTime(row.createAt);
  43. return '';
  44. }
  45. const list = ref([]);
  46. const page = ref(0);
  47. const total = ref(0);
  48. const totalPages = ref(0);
  49. const loading = ref(false);
  50. const pageLoading = ref(true);
  51. const initialLoaded = ref(false);
  52. const keyword = ref('');
  53. /** 与接口同步的检索关键字(防抖后触发请求) */
  54. const keywordQuery = ref('');
  55. /** loading 期间收到重置请求时,当前请求结束后立即重拉 */
  56. const pendingResetFetch = ref(false);
  57. /** 请求序号:只允许最新请求回填列表,避免旧请求覆盖新筛选 */
  58. let fetchSeq = 0;
  59. const status = ref('');
  60. /** Tab 与 supplier_prepare_status 对应(逗号多选由后端 IN 查询) */
  61. const statusTabs = [
  62. { key: '', label: '全部' },
  63. { key: 'pending_prepare', label: '待备货' },
  64. { key: 'stocking,prepared', label: '备货中' },
  65. { key: 'picked_up', label: '已完成' }
  66. ];
  67. const prepareStatusText = {
  68. pending_prepare: '待备货',
  69. stocking: '备货中',
  70. prepared: '备货完成',
  71. picked_up: '已取货'
  72. };
  73. const statusText = {
  74. pending: '待分配',
  75. assigned: '待备货',
  76. supplier_stocking: '备货中',
  77. supplier_ready: '备货完成',
  78. picking: '取货/配送中',
  79. picked: '待取货',
  80. delivering: '配送中',
  81. completed: '已完成',
  82. cancelled: '已取消'
  83. };
  84. function displayStatus(row) {
  85. const ps = String(row.supplierPrepareStatus || '').toLowerCase();
  86. if (ps && prepareStatusText[ps])
  87. return prepareStatusText[ps];
  88. const code = String(row.status || '').toLowerCase();
  89. if (!code)
  90. return '-';
  91. return statusText[code] || code;
  92. }
  93. /** onLoad 入参优先;H5 等场景若为空则从当前页 options 兜底 */
  94. function resolvePageRouteOptions(options) {
  95. const o = options && typeof options === 'object' ? options : {};
  96. if (Object.keys(o).length > 0)
  97. return o;
  98. try {
  99. const pages = getCurrentPages();
  100. const cur = pages[pages.length - 1];
  101. return { ...(cur?.options ?? {}) };
  102. }
  103. catch {
  104. return {};
  105. }
  106. }
  107. function applyStatusFromRouteOptions(opts) {
  108. const rawPrep = opts.supplierPrepareStatus;
  109. const raw = opts.status;
  110. if (rawPrep != null && String(rawPrep).trim() !== '') {
  111. try {
  112. status.value = decodeURIComponent(String(rawPrep));
  113. }
  114. catch {
  115. status.value = String(rawPrep);
  116. }
  117. }
  118. else if (raw != null && String(raw).trim() !== '') {
  119. try {
  120. const s = decodeURIComponent(String(raw));
  121. if (s === 'assigned')
  122. status.value = 'pending_prepare';
  123. else if (['supplier_stocking', 'supplier_ready', 'picking', 'delivering'].some((x) => s.includes(x)))
  124. status.value = 'stocking,prepared';
  125. else if (s === 'completed')
  126. status.value = 'picked_up';
  127. else
  128. status.value = s;
  129. }
  130. catch {
  131. status.value = String(raw);
  132. }
  133. }
  134. }
  135. async function fetchPage(reset) {
  136. if (loading.value) {
  137. if (reset)
  138. pendingResetFetch.value = true;
  139. return;
  140. }
  141. if (!reset) {
  142. if (!initialLoaded.value)
  143. return;
  144. if (total.value === 0)
  145. return;
  146. const tp = totalPages.value || (total.value > 0 ? Math.ceil(total.value / PAGE_SIZE) : 0);
  147. if (tp > 0 && page.value >= tp)
  148. return;
  149. if (list.value.length >= total.value)
  150. return;
  151. }
  152. if (reset) {
  153. list.value = [];
  154. page.value = 0;
  155. total.value = 0;
  156. totalPages.value = 0;
  157. initialLoaded.value = false;
  158. }
  159. loading.value = true;
  160. pendingResetFetch.value = false;
  161. const nextPage = reset ? 1 : page.value + 1;
  162. const reqStatus = status.value;
  163. const reqKeyword = keywordQuery.value.trim();
  164. const currentFetchSeq = ++fetchSeq;
  165. try {
  166. const data = {
  167. page: nextPage,
  168. pageSize: PAGE_SIZE
  169. };
  170. if (reqStatus)
  171. data.supplierPrepareStatus = reqStatus;
  172. if (reqKeyword)
  173. data.keyword = reqKeyword;
  174. const res = await request({
  175. url: '/mini/supplier/tasks',
  176. method: 'GET',
  177. data
  178. });
  179. const body = parseResponseJson(res);
  180. if (body.code !== 200) {
  181. if (reset)
  182. list.value = [];
  183. return;
  184. }
  185. // 仅处理最新请求,防止旧请求结果覆盖最新筛选
  186. if (currentFetchSeq !== fetchSeq)
  187. return;
  188. const rawList = body.data?.list || [];
  189. if (reset) {
  190. list.value = rawList;
  191. }
  192. else {
  193. const ids = new Set(list.value.map((r) => r.id));
  194. for (const row of rawList) {
  195. if (row.id && !ids.has(row.id)) {
  196. list.value.push(row);
  197. ids.add(row.id);
  198. }
  199. }
  200. }
  201. page.value = nextPage;
  202. total.value = Number(body.data?.total ?? 0);
  203. totalPages.value = Number(body.data?.totalPages ?? 0);
  204. initialLoaded.value = true;
  205. }
  206. finally {
  207. loading.value = false;
  208. if (pendingResetFetch.value) {
  209. pendingResetFetch.value = false;
  210. void fetchPage(true);
  211. }
  212. }
  213. }
  214. function onScrollToLower() {
  215. if (!initialLoaded.value || loading.value)
  216. return;
  217. if (total.value === 0)
  218. return;
  219. const tp = totalPages.value || (total.value > 0 ? Math.ceil(total.value / PAGE_SIZE) : 0);
  220. if (tp > 0 && page.value >= tp)
  221. return;
  222. if (list.value.length >= total.value)
  223. return;
  224. void fetchPage(false);
  225. }
  226. function setStatus(key) {
  227. status.value = key;
  228. void fetchPage(true);
  229. }
  230. const emptyHint = computed(() => {
  231. if (keywordQuery.value.trim() && list.value.length === 0 && initialLoaded.value) {
  232. return '未找到匹配任务';
  233. }
  234. return '暂无备货任务';
  235. });
  236. function goDetail(id) {
  237. if (!id)
  238. return;
  239. uni.navigateTo({
  240. url: `/pages/supplier/delivery/detail/detail?id=${encodeURIComponent(id)}`
  241. });
  242. }
  243. function canStartStock(row) {
  244. const ps = String(row.supplierPrepareStatus || '').toLowerCase();
  245. if (ps === 'pending_prepare')
  246. return true;
  247. return !row.supplierPrepareStatus && String(row.status || '').toLowerCase() === 'assigned';
  248. }
  249. function canFinishStock(row) {
  250. const ps = String(row.supplierPrepareStatus || '').toLowerCase();
  251. if (ps === 'stocking')
  252. return true;
  253. return !row.supplierPrepareStatus && String(row.status || '').toLowerCase() === 'supplier_stocking';
  254. }
  255. function onGoTaskDetail(row, e) {
  256. e?.stopPropagation?.();
  257. goDetail(row?.id);
  258. }
  259. let keywordDebounce = null;
  260. function onSearchConfirm() {
  261. if (keywordDebounce) {
  262. clearTimeout(keywordDebounce);
  263. keywordDebounce = null;
  264. }
  265. keywordQuery.value = keyword.value.trim();
  266. void fetchPage(true);
  267. }
  268. function clearKeyword() {
  269. if (keywordDebounce) {
  270. clearTimeout(keywordDebounce);
  271. keywordDebounce = null;
  272. }
  273. keyword.value = '';
  274. keywordQuery.value = '';
  275. void fetchPage(true);
  276. }
  277. watch(keyword, () => {
  278. if (keywordDebounce)
  279. clearTimeout(keywordDebounce);
  280. keywordDebounce = setTimeout(() => {
  281. keywordDebounce = null;
  282. keywordQuery.value = keyword.value.trim();
  283. void fetchPage(true);
  284. }, 400);
  285. });
  286. onUnmounted(() => {
  287. if (keywordDebounce)
  288. clearTimeout(keywordDebounce);
  289. });
  290. /**
  291. * 必须在 onShow 之前执行:原先 onShow 先发起无 supplierPrepareStatus 的请求并占用 loading,
  292. * onMounted 再设 Tab 后 fetch会被 loading 短路,导致 URL 带参仍展示「全部」。
  293. */
  294. onLoad((options) => {
  295. if (!userStore.isLoggedIn) {
  296. uni.reLaunch({ url: '/pages/login/login' });
  297. return;
  298. }
  299. if (!userStore.isSupplier) {
  300. uni.showToast({ title: '仅供应商可访问', icon: 'none' });
  301. setTimeout(() => userStore.navigateToHome(), 1200);
  302. return;
  303. }
  304. try {
  305. applyStatusFromRouteOptions(resolvePageRouteOptions(options));
  306. }
  307. catch {
  308. // ignore
  309. }
  310. keywordQuery.value = keyword.value.trim();
  311. (async () => {
  312. try {
  313. await fetchPage(true);
  314. }
  315. finally {
  316. pageLoading.value = false;
  317. }
  318. })();
  319. });
  320. onShow(() => {
  321. if (!userStore.isLoggedIn || !userStore.isSupplier)
  322. return;
  323. if (!initialLoaded.value)
  324. return;
  325. void fetchPage(true);
  326. });
  327. </script>
  328. <template>
  329. <view class="delivery-root">
  330. <view v-if="pageLoading" class="page-loading-wrap">
  331. <ClientSkeleton variant="hero-list" :rows="3" />
  332. </view>
  333. <view v-else class="delivery-page">
  334. <view class="delivery-top">
  335. <view class="search-wrap">
  336. <view class="search-bar">
  337. <input
  338. v-model="keyword"
  339. class="search-input"
  340. type="text"
  341. confirm-type="search"
  342. placeholder="配送单号 / 订单号 / 联系人 / 电话"
  343. @confirm="onSearchConfirm"
  344. />
  345. <text class="search-action" @click="onSearchConfirm">搜索</text>
  346. </view>
  347. </view>
  348. <scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false" :enable-flex="true">
  349. <view class="tabs-row">
  350. <view
  351. v-for="t in statusTabs"
  352. :key="t.key || 'all'"
  353. class="tab"
  354. :class="{ active: (t.key || '') === status }"
  355. @click="setStatus(t.key)"
  356. >
  357. <text>{{ t.label }}</text>
  358. </view>
  359. </view>
  360. </scroll-view>
  361. </view>
  362. <scroll-view
  363. scroll-y
  364. class="list"
  365. :enable-flex="true"
  366. :lower-threshold="100"
  367. :show-scrollbar="true"
  368. @scrolltolower="onScrollToLower"
  369. >
  370. <view v-for="(row, idx) in list" :key="row.id" class="card" @click="goDetail(row.id)">
  371. <view class="row top-row">
  372. <view class="no-line">
  373. <text class="no">{{ idx + 1 }}. 任务号:{{ row.taskNo || row.id }}</text>
  374. <SqCopyBtn class="no-copy" :text="row.taskNo || row.id" label="任务号" />
  375. </view>
  376. <text class="st" :class="String(row.status || '')">{{ displayStatus(row) }}</text>
  377. </view>
  378. <view
  379. v-if="canStartStock(row) || canFinishStock(row)"
  380. class="action-row"
  381. @click.stop
  382. >
  383. <button
  384. v-if="canStartStock(row)"
  385. class="sq-stock-btn sq-stock-btn--outline"
  386. @click="onGoTaskDetail(row, $event)"
  387. >
  388. 开始备货
  389. </button>
  390. <button
  391. v-if="canFinishStock(row)"
  392. class="btn-act btn-done"
  393. @click="onGoTaskDetail(row, $event)"
  394. >
  395. 备货完成
  396. </button>
  397. </view>
  398. <view v-if="row.deliverName" class="meta">配送员:{{ row.deliverName }}</view>
  399. <view v-if="row.deliverPhone" class="meta meta-with-dial">
  400. <text>联系电话:</text>
  401. <PhoneDialInline :phone="row.deliverPhone" color="#303133" expand />
  402. </view>
  403. <view v-if="(row.deliveredItems ?? 0) > 0" class="meta delivered-hint">
  404. 已送达行:{{ row.deliveredItems }}
  405. </view>
  406. <view v-if="(row.items || []).length" class="product-scroll">
  407. <view class="product-scroll-inner">
  408. <view v-for="it in row.items || []" :key="it.id" class="product-card">
  409. <ClientImageLoader class="product-image" :src="taskItemImage(it)" mode="aspectFill" />
  410. <view class="product-meta">
  411. <text class="product-name">{{ it.productName || '-' }}</text>
  412. <text class="product-qty">{{ taskQtyText(it) }}</text>
  413. </view>
  414. </view>
  415. </view>
  416. </view>
  417. <view class="task-card-footer">
  418. <view class="footer-left">
  419. <text v-if="row.effectiveTime || row.createAt" class="footer-time">{{ taskFooterTime(row) }}</text>
  420. </view>
  421. <view class="footer-amount">
  422. <text class="amount-count">共{{ formatTaskTotalQty(row) }}件</text>
  423. <view class="amount-row">
  424. <text class="amount-label">供货合计(元):</text>
  425. <text class="amount-value">¥{{ formatYuan(row.supplierQuoteTotal) }}</text>
  426. </view>
  427. </view>
  428. </view>
  429. </view>
  430. <view v-if="list.length === 0 && !loading && initialLoaded" class="empty-state">
  431. <view class="empty-inner">
  432. <image class="empty-image" :src="IMG_EMPTY" mode="aspectFit" />
  433. <text class="empty-text">{{ emptyHint }}</text>
  434. </view>
  435. </view>
  436. <ClientSkeleton v-if="loading && list.length === 0" variant="list" :rows="2" />
  437. <view v-if="loading && list.length > 0" class="loading-more">
  438. <text>加载中...</text>
  439. </view>
  440. <view
  441. v-else-if="
  442. initialLoaded &&
  443. total > 0 &&
  444. (totalPages > 0 ? page >= totalPages : list.length >= total)
  445. "
  446. class="nomore"
  447. >
  448. <text>没有更多了</text>
  449. </view>
  450. </scroll-view>
  451. </view>
  452. </view>
  453. </template>
  454. <style lang="scss">
  455. page {
  456. height: 100%;
  457. overflow: hidden;
  458. }
  459. </style>
  460. <style lang="scss" scoped>
  461. .page-loading-wrap {
  462. min-height: 100vh;
  463. }
  464. .delivery-root {
  465. position: relative;
  466. width: 100%;
  467. min-height: 100%;
  468. }
  469. /* #ifdef H5 */
  470. .delivery-page {
  471. position: fixed;
  472. left: 0;
  473. right: 0;
  474. top: var(--window-top, 0px);
  475. bottom: var(--window-bottom, 0px);
  476. width: 100%;
  477. max-width: 100%;
  478. box-sizing: border-box;
  479. z-index: 1;
  480. display: flex;
  481. flex-direction: column;
  482. background: #f3f7f5;
  483. overflow: hidden;
  484. padding: 20rpx;
  485. }
  486. /* #endif */
  487. /* #ifndef H5 */
  488. .delivery-page {
  489. height: 100%;
  490. max-height: 100%;
  491. min-height: 0;
  492. width: 100%;
  493. max-width: 100%;
  494. box-sizing: border-box;
  495. display: flex;
  496. flex-direction: column;
  497. background: #f3f7f5;
  498. overflow: hidden;
  499. padding: 20rpx;
  500. }
  501. /* #endif */
  502. .delivery-top {
  503. flex-shrink: 0;
  504. width: 100%;
  505. max-width: 100%;
  506. box-sizing: border-box;
  507. }
  508. .search-wrap {
  509. margin-bottom: 8rpx;
  510. width: 100%;
  511. max-width: 100%;
  512. box-sizing: border-box;
  513. }
  514. .search-bar {
  515. display: flex;
  516. align-items: center;
  517. width: 100%;
  518. max-width: 100%;
  519. box-sizing: border-box;
  520. height: 72rpx;
  521. background: #fff;
  522. border: 1rpx solid #d9e9e1;
  523. border-radius: 36rpx;
  524. padding: 0 8rpx 0 24rpx;
  525. }
  526. .search-input {
  527. flex: 1;
  528. min-width: 0;
  529. height: 72rpx;
  530. box-sizing: border-box;
  531. background: transparent;
  532. font-size: 28rpx;
  533. color: #303133;
  534. border: none;
  535. padding: 0;
  536. }
  537. .search-action {
  538. flex-shrink: 0;
  539. padding: 0 16rpx 0 8rpx;
  540. font-size: 24rpx;
  541. color: #0a9f6d;
  542. font-weight: 600;
  543. }
  544. .tabs-scroll {
  545. width: 100%;
  546. max-width: 100%;
  547. box-sizing: border-box;
  548. margin-bottom: 16rpx;
  549. background: transparent;
  550. padding: 8rpx 0 0;
  551. }
  552. .tabs-row {
  553. display: flex;
  554. flex-direction: row;
  555. flex-wrap: nowrap;
  556. justify-content: flex-start;
  557. align-items: center;
  558. width: max-content;
  559. min-width: 100%;
  560. box-sizing: border-box;
  561. }
  562. .tabs-row .tab {
  563. display: flex;
  564. align-items: center;
  565. justify-content: center;
  566. flex-shrink: 0;
  567. padding: 8rpx 12rpx 12rpx 0;
  568. margin-right: 24rpx;
  569. font-size: 26rpx;
  570. color: #606266;
  571. background: transparent;
  572. }
  573. .tabs-row .tab:last-child {
  574. margin-right: 0;
  575. }
  576. .tabs-row .tab.active {
  577. color: #0a9f6d;
  578. font-weight: 600;
  579. }
  580. .list {
  581. flex: 1;
  582. min-height: 0;
  583. height: 100%;
  584. width: 100%;
  585. max-width: 100%;
  586. box-sizing: border-box;
  587. overflow: hidden;
  588. }
  589. .card {
  590. width: 100%;
  591. max-width: 100%;
  592. box-sizing: border-box;
  593. overflow: hidden;
  594. background: #fff;
  595. border-radius: 14px;
  596. border: 1px solid #d9e9e1;
  597. padding: 24rpx;
  598. margin-bottom: 16rpx;
  599. box-shadow: 0 4px 12px rgba(10, 159, 109, 0.06);
  600. .row {
  601. display: flex;
  602. justify-content: space-between;
  603. align-items: flex-start;
  604. gap: 16rpx;
  605. margin-bottom: 12rpx;
  606. }
  607. .top-row {
  608. align-items: center;
  609. }
  610. .no {
  611. font-size: 28rpx;
  612. color: #303133;
  613. font-weight: 500;
  614. word-break: break-all;
  615. }
  616. .no-line {
  617. flex: 1;
  618. min-width: 0;
  619. display: flex;
  620. align-items: center;
  621. gap: 10rpx;
  622. }
  623. .no-line .no {
  624. flex: 1;
  625. min-width: 0;
  626. overflow: hidden;
  627. text-overflow: ellipsis;
  628. white-space: nowrap;
  629. }
  630. .no-copy {
  631. flex-shrink: 0;
  632. }
  633. .st {
  634. flex-shrink: 0;
  635. font-size: 22rpx;
  636. color: #cf8a19;
  637. padding: 4rpx 12rpx;
  638. border-radius: 8rpx;
  639. background: #fff4dd;
  640. max-width: 42%;
  641. text-align: center;
  642. overflow: hidden;
  643. text-overflow: ellipsis;
  644. white-space: nowrap;
  645. }
  646. .st.completed {
  647. color: #07c160;
  648. background: #f0f9eb;
  649. }
  650. .st.cancelled {
  651. color: #909399;
  652. background: #f4f4f5;
  653. }
  654. .meta {
  655. font-size: 24rpx;
  656. color: #606266;
  657. line-height: 1.5;
  658. word-break: break-all;
  659. max-width: 100%;
  660. }
  661. .meta-with-dial {
  662. display: flex;
  663. align-items: center;
  664. flex-wrap: wrap;
  665. gap: 8rpx;
  666. }
  667. .meta-with-dial :deep(.phone-dial-inline__text) {
  668. font-size: 24rpx;
  669. color: #303133;
  670. }
  671. .action-row {
  672. display: flex;
  673. flex-wrap: wrap;
  674. gap: 16rpx;
  675. margin-bottom: 8rpx;
  676. }
  677. .btn-act {
  678. flex: 1;
  679. min-width: 200rpx;
  680. height: 64rpx;
  681. line-height: 64rpx;
  682. font-size: 26rpx;
  683. border-radius: 12rpx;
  684. padding: 0;
  685. margin: 0;
  686. border: none;
  687. }
  688. .btn-act::after {
  689. border: none;
  690. }
  691. .btn-done {
  692. background: linear-gradient(145deg, #087f57, #0a9f6d);
  693. color: #fff;
  694. font-weight: 500;
  695. }
  696. /* 对齐备货详情页「开始备货」按钮样式 */
  697. .sq-stock-btn {
  698. flex: 1;
  699. min-width: 200rpx;
  700. height: 72rpx;
  701. line-height: 72rpx;
  702. font-size: 28rpx;
  703. border-radius: 12rpx;
  704. padding: 0;
  705. margin: 0;
  706. border: none;
  707. }
  708. .sq-stock-btn::after {
  709. border: none;
  710. }
  711. .sq-stock-btn--outline {
  712. background: #ecfdf5;
  713. color: #087f57;
  714. font-weight: 600;
  715. }
  716. .delivered-hint {
  717. margin-top: 6rpx;
  718. color: #07c160;
  719. }
  720. .product-scroll {
  721. margin-top: 12rpx;
  722. padding-bottom: 16rpx;
  723. border-bottom: 1rpx solid #f5f5f5;
  724. width: 100%;
  725. overflow-x: auto;
  726. overflow-y: hidden;
  727. -webkit-overflow-scrolling: touch;
  728. }
  729. .product-scroll-inner {
  730. display: flex;
  731. flex-direction: row;
  732. flex-wrap: nowrap;
  733. gap: 16rpx;
  734. padding: 0 2rpx;
  735. width: max-content;
  736. max-width: none;
  737. }
  738. .product-card {
  739. width: 168rpx;
  740. border-radius: 12rpx;
  741. overflow: hidden;
  742. background: #ffffff;
  743. border: 1rpx solid #e8f3ee;
  744. flex: 0 0 auto;
  745. }
  746. .product-image {
  747. width: 100%;
  748. height: 120rpx;
  749. background: #ebeef5;
  750. display: block;
  751. }
  752. .product-meta {
  753. display: flex;
  754. align-items: center;
  755. padding: 12rpx;
  756. gap: 8rpx;
  757. }
  758. .product-name {
  759. flex: 1;
  760. min-width: 0;
  761. font-size: 24rpx;
  762. color: #303133;
  763. overflow: hidden;
  764. text-overflow: ellipsis;
  765. white-space: nowrap;
  766. }
  767. .product-qty {
  768. font-size: 24rpx;
  769. color: #909399;
  770. flex: 0 0 auto;
  771. }
  772. .task-card-footer {
  773. display: flex;
  774. justify-content: space-between;
  775. align-items: flex-end;
  776. margin-top: 16rpx;
  777. gap: 16rpx;
  778. }
  779. .footer-left {
  780. flex: 1;
  781. min-width: 0;
  782. }
  783. .footer-time {
  784. font-size: 24rpx;
  785. color: #909399;
  786. line-height: 1.4;
  787. word-break: break-all;
  788. }
  789. .footer-amount {
  790. display: flex;
  791. flex-direction: column;
  792. align-items: flex-end;
  793. flex-shrink: 0;
  794. }
  795. .amount-count {
  796. font-size: 24rpx;
  797. color: #909399;
  798. margin-bottom: 6rpx;
  799. }
  800. .amount-row {
  801. display: flex;
  802. align-items: baseline;
  803. }
  804. .amount-label {
  805. font-size: 26rpx;
  806. color: #606266;
  807. }
  808. .amount-value {
  809. font-size: 32rpx;
  810. color: #be7a12;
  811. font-weight: bold;
  812. }
  813. }
  814. .empty-state {
  815. width: 100%;
  816. max-width: 100%;
  817. box-sizing: border-box;
  818. display: flex;
  819. flex-direction: column;
  820. align-items: center;
  821. justify-content: center;
  822. padding: 88rpx 32rpx 112rpx;
  823. }
  824. .empty-inner {
  825. width: 100%;
  826. max-width: 100%;
  827. box-sizing: border-box;
  828. display: flex;
  829. flex-direction: column;
  830. align-items: center;
  831. justify-content: center;
  832. }
  833. .empty-image {
  834. display: block;
  835. width: 160rpx;
  836. height: 160rpx;
  837. margin-bottom: 10rpx;
  838. }
  839. .empty-text {
  840. display: block;
  841. width: 100%;
  842. max-width: 560rpx;
  843. box-sizing: border-box;
  844. text-align: center;
  845. font-size: 28rpx;
  846. color: #909399;
  847. line-height: 1.45;
  848. padding: 0 16rpx;
  849. margin-top: 0;
  850. }
  851. .loading-state,
  852. .loading-more,
  853. .nomore {
  854. display: flex;
  855. align-items: center;
  856. justify-content: center;
  857. padding: 32rpx 0;
  858. color: #909399;
  859. font-size: 26rpx;
  860. }
  861. </style>