From ae2885831846cf0140fb016fde4b2a193a7af01d Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 18 Jun 2026 23:19:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E7=95=8C=E9=9D=A2=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/auto-imports.d.ts | 2 +- .../listings/views/ListingDetailView.vue | 221 ++++++++++++++---- .../components/MobilePayWaySelectPopup.vue | 164 +++++++++++++ .../composables/useMobilePayWaySelect.ts | 33 +++ .../orders/views/MobileOrderDetailView.vue | 30 +-- .../orders/views/MobileOrdersView.vue | 32 ++- .../features/orders/views/OrderDetailView.vue | 154 ++++++++++-- 7 files changed, 543 insertions(+), 93 deletions(-) create mode 100644 frontend/src/features/orders/components/MobilePayWaySelectPopup.vue create mode 100644 frontend/src/features/orders/composables/useMobilePayWaySelect.ts diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts index 9d24007..e16d21f 100644 --- a/frontend/auto-imports.d.ts +++ b/frontend/auto-imports.d.ts @@ -6,5 +6,5 @@ // biome-ignore lint: disable export {} declare global { - + const ElMessageBox: typeof import('element-plus/es').ElMessageBox } diff --git a/frontend/src/features/listings/views/ListingDetailView.vue b/frontend/src/features/listings/views/ListingDetailView.vue index c02ca52..5d87c03 100644 --- a/frontend/src/features/listings/views/ListingDetailView.vue +++ b/frontend/src/features/listings/views/ListingDetailView.vue @@ -3,7 +3,7 @@ import { readError } from '@/shared/utils/error' import { ElMessage, ElMessageBox } from 'element-plus' import { computed, nextTick, onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { Star, StarFilled } from '@element-plus/icons-vue' +import { ShoppingCart, Star, StarFilled } from '@element-plus/icons-vue' import { fetchListing, type Listing } from '@/features/listings/api/listings' import { useListingCollections } from '@/features/listings/composables/useListingCollections' @@ -468,12 +468,21 @@ function handleToggleFavorite() { @@ -903,10 +923,78 @@ function handleToggleFavorite() { .pc-order-card { max-width: none; border-radius: 20px; + padding: 24px; + overflow: hidden; +} + +.order-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + margin-bottom: 10px; +} + +.order-card-head h2 { + margin: 4px 0 0; + color: #17233d; + font-size: 22px; + line-height: 1.2; +} + +.order-eyebrow { + display: inline-flex; + align-items: center; + min-height: 24px; + border-radius: 999px; + background: #f1f5f9; + padding: 0 10px; + color: #64748b; + font-size: 12px; + font-weight: 900; +} + +.order-state { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 58px; + min-height: 28px; + border-radius: 999px; + background: #ecfdf5; + color: #16a34a; + font-size: 12px; + font-weight: 900; + white-space: nowrap; +} + +.order-state.is-busy { + background: #fff7ed; + color: #ea580c; +} + +.order-form { + display: grid; + gap: 14px; +} + +.order-safe-text { + margin-bottom: 18px; + color: #7a89a5; + font-size: 15px; + font-weight: 700; } .order-total-box { - margin-bottom: 14px; + position: relative; + display: grid; + gap: 12px; + margin-bottom: 0; + border: 1px solid rgba(245, 158, 11, 0.2); + background: + linear-gradient(135deg, rgba(255, 251, 235, 0.96), rgba(255, 247, 237, 0.92)), + #fffbeb; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86); } .order-total-head { @@ -914,12 +1002,12 @@ function handleToggleFavorite() { align-items: flex-start; justify-content: space-between; gap: 16px; - padding-bottom: 12px; + padding-bottom: 14px; border-bottom: 1px solid rgba(217, 119, 6, 0.14); } .order-total-head span { - color: #92400e; + color: #9a5a16; font-size: 14px; font-weight: 900; } @@ -927,14 +1015,15 @@ function handleToggleFavorite() { .order-total-head strong { margin: 0; color: #ef4444; - font-size: 32px; + font-size: 34px; line-height: 1; + text-align: right; } .order-price-breakdown { display: grid; - gap: 8px; - padding: 12px 0; + gap: 10px; + padding: 0; } .order-price-breakdown div { @@ -944,42 +1033,51 @@ function handleToggleFavorite() { gap: 12px; } -.order-price-breakdown span { +.order-price-breakdown span, +.order-deposit-row span { color: #8a5a12; font-size: 13px; font-weight: 900; } -.order-price-breakdown strong { +.order-price-breakdown strong, +.order-deposit-row strong { margin: 0; color: #17233d; font-size: 16px; line-height: 1.2; } -.order-total-box > em { - padding-top: 10px; +.order-deposit-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-top: 12px; border-top: 1px solid rgba(217, 119, 6, 0.14); +} + +.order-deposit-row strong { color: #92400e; - font-size: 13px; - font-weight: 900; + font-size: 18px; } .order-check-list { display: grid; gap: 10px; - margin: 0 0 18px; + margin: 0; } .order-check-list div { - display: flex; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; gap: 16px; - min-height: 40px; - padding: 0 12px; - border-radius: 10px; + min-height: 44px; + border: 1px solid #eef2f7; + border-radius: 12px; background: #f8fafc; + padding: 0 14px; } .order-check-list dt { @@ -994,16 +1092,57 @@ function handleToggleFavorite() { font-size: 14px; font-weight: 900; text-align: right; + overflow-wrap: anywhere; } .full-control { width: 100%; } +.order-action-group { + display: grid; + gap: 10px; + padding-top: 2px; +} + +.order-primary-btn { + min-height: 48px; + border: 0; + background: #ff8a00; + color: #ffffff; + font-size: 15px; + font-weight: 900; + box-shadow: 0 12px 24px rgba(255, 106, 0, 0.24); +} + +.order-primary-btn:hover, +.order-primary-btn:focus { + background: #ff7a00; + color: #ffffff; +} + +.order-primary-btn.is-disabled, +.order-primary-btn.is-disabled:hover { + background: #f3a94a; + color: rgba(255, 255, 255, 0.86); + box-shadow: none; +} + .favorite-order-btn { - margin-top: 10px; + min-height: 46px; + margin-left: 0; border-color: #ff6a00; + background: #ffffff; color: #ff6a00; + font-size: 15px; + font-weight: 900; +} + +.favorite-order-btn:hover, +.favorite-order-btn:focus { + border-color: #ff6a00; + background: #fff7ed; + color: #ea580c; } .agreement-dialog-body { diff --git a/frontend/src/features/orders/components/MobilePayWaySelectPopup.vue b/frontend/src/features/orders/components/MobilePayWaySelectPopup.vue new file mode 100644 index 0000000..38a3a94 --- /dev/null +++ b/frontend/src/features/orders/components/MobilePayWaySelectPopup.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/frontend/src/features/orders/composables/useMobilePayWaySelect.ts b/frontend/src/features/orders/composables/useMobilePayWaySelect.ts new file mode 100644 index 0000000..27370cb --- /dev/null +++ b/frontend/src/features/orders/composables/useMobilePayWaySelect.ts @@ -0,0 +1,33 @@ +import { ref } from 'vue' + +import type { PaymentPayWay } from '@/features/orders/api/orders' + +// useMobilePayWaySelect 提供移动端"选择支付方式"底部弹层的状态与 Promise 化选择逻辑。 +// 与 MobilePayWaySelectPopup 配合:select() 打开弹层并返回用户所选渠道,关闭即视为取消。 +export function useMobilePayWaySelect() { + const visible = ref(false) + let resolver: ((value: PaymentPayWay | null) => void) | null = null + + // select 打开弹层并返回 Promise,resolve 为所选渠道;取消则为 null。 + function select(): Promise { + visible.value = true + return new Promise(resolve => { + resolver = resolve + }) + } + + // choose 用户点选某渠道,关闭弹层并 resolve。 + function choose(payWay: PaymentPayWay) { + resolver?.(payWay) + resolver = null + visible.value = false + } + + // handleClosed 弹层关闭(含遮罩/返回)时若未选择则按取消处理。 + function handleClosed() { + resolver?.(null) + resolver = null + } + + return { visible, select, choose, handleClosed } +} diff --git a/frontend/src/features/orders/views/MobileOrderDetailView.vue b/frontend/src/features/orders/views/MobileOrderDetailView.vue index ed8a2b8..7514f16 100644 --- a/frontend/src/features/orders/views/MobileOrderDetailView.vue +++ b/frontend/src/features/orders/views/MobileOrderDetailView.vue @@ -4,8 +4,10 @@ import { useRouter } from 'vue-router' import { showDialog, showToast } from 'vant' import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue' +import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue' import { useOrderActions, CONFIRMABLE_ACTIONS } from '@/features/orders/composables/useOrderActions' import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier' +import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect' import { amountYuan, money, @@ -19,11 +21,14 @@ import { } from '@/features/orders' import { handoffStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels' import { formatDateTime } from '@/shared/utils/time' -import type { PaymentPayWay } from '@/features/orders/api/orders' // 移动端支付收银台(基于通用 useOrderPaymentCashier 的薄封装:注入 vant toast + App 浏览器跳转)。 const cashier = useMobilePaymentCashier() const router = useRouter() + +// 支付方式选择底部弹层(与 PC 端一致的渠道卡片体验)。 +const payWaySelect = useMobilePayWaySelect() +const selectPayWay = payWaySelect.select const { paymentPopupVisible, activePayment, @@ -117,23 +122,6 @@ const showDisputePopup = ref(false) const showCounterPopup = ref(false) const showRejectPopup = ref(false) -// selectPayWay 在创建支付单前让移动端用户选择支付宝或微信。 -async function selectPayWay(): Promise { - try { - await showDialog({ - title: '选择支付方式', - message: '选择后将生成对应渠道的支付二维码。', - showCancelButton: true, - confirmButtonText: '支付宝支付', - cancelButtonText: '微信支付', - closeOnClickOverlay: false, - }) - return 'ZFBZF' - } catch (action) { - return action === 'cancel' ? 'WXZF' : null - } -} - // 账号快照展示(PC 不用,保留在本视图)。 function readSnapshot() { return readOrderSnapshot(order.value) as Record | null @@ -465,6 +453,12 @@ async function copyListingCode() { + + { loadOrders() if (route.query.tab) { @@ -123,22 +128,7 @@ async function openOrderChatAfterPayment(order: Order) { } } -// selectPayWay 在列表快捷支付前让用户选择支付宝或微信。 -async function selectPayWay(): Promise { - try { - await showDialog({ - title: '选择支付方式', - message: '选择后将生成对应渠道的支付二维码。', - showCancelButton: true, - confirmButtonText: '支付宝支付', - cancelButtonText: '微信支付', - closeOnClickOverlay: false, - }) - return 'ZFBZF' - } catch (action) { - return action === 'cancel' ? 'WXZF' : null - } -} +// selectPayWay 在列表快捷支付前让用户选择支付宝或微信(见 payWaySelect)。 function money(value: unknown) { return formatMoney(Number(value || 0)) @@ -293,6 +283,12 @@ async function copyListingCode(order: Order) { + + -import { computed } from 'vue' -import { ElMessage, ElMessageBox } from 'element-plus' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' import { ChatDotRound, CopyDocument, Loading } from '@element-plus/icons-vue' import { @@ -106,19 +106,27 @@ const paymentPayURL = cashier.payURL const activePayWayLabel = computedPayWayLabel(activePayment) // selectPayWay 在创建支付单前让用户选择支付宝或微信。 -async function selectPayWay(): Promise { - try { - await ElMessageBox.confirm('选择后将生成对应渠道的支付二维码。', '选择支付方式', { - type: 'info', - confirmButtonText: '支付宝支付', - cancelButtonText: '微信支付', - distinguishCancelAndClose: true, - closeOnClickModal: false, - }) - return 'ZFBZF' - } catch (action) { - return action === 'cancel' ? 'WXZF' : null - } +// 通过自定义对话框展示两个平级渠道卡片,返回 null 表示用户取消。 +const payWayDialogVisible = ref(false) +let payWayResolver: ((value: PaymentPayWay | null) => void) | null = null + +function selectPayWay(): Promise { + payWayDialogVisible.value = true + return new Promise(resolve => { + payWayResolver = resolve + }) +} + +function choosePayWay(payWay: PaymentPayWay) { + payWayResolver?.(payWay) + payWayResolver = null + payWayDialogVisible.value = false +} + +// 对话框关闭(含点 X、按 ESC)时,未选择则视为取消。 +function handlePayWayDialogClosed() { + payWayResolver?.(null) + payWayResolver = null } // computedPayWayLabel 根据当前支付单展示支付方式名称。 @@ -603,6 +611,38 @@ async function copyListingCode() { + +

选择渠道后将生成对应的支付二维码

+
+ + +
+
+