| | |
| | | <view class="page"> |
| | | <view class="header"> |
| | | <view class="status-safe-space"></view> |
| | | <view class="title-row"><text class="page-title">订单管理</text><view v-if="isDevelopment" class="demo-control"><text class="demo-control-text">演示数据</text><switch class="demo-switch" :checked="demoDataEnabled" color="#FF7924" @change="toggleDemoData"></switch></view></view> |
| | | <view class="title-row"><text class="page-title">订单管理</text></view> |
| | | <view class="category-tabs"> |
| | | <view class="category-tab" :class="activeCategory == 'group' ? 'active' : ''" @tap="selectCategory('group')"><text>团购</text></view> |
| | | <view class="category-tab" :class="activeCategory == 'store' ? 'active' : ''" @tap="selectCategory('store')"><text>到店消费</text></view> |
| | |
| | | |
| | | <view v-if="listLoadState == 'success'" class="order-list"> |
| | | <view v-for="order in orders" :key="order.id" class="order-card" :class="order.category == 'stay' ? 'stay-order-card' : (order.category == 'store' ? 'store-order-card' : '')" @tap="openOrderItem(order)"> |
| | | <view class="customer-row"><view class="customer-info"><image class="customer-icon" src="/static/icons/lucide/circle-user-round.svg" mode="aspectFit"></image><text class="customer-name">{{order.customerName}}</text><text class="customer-phone">{{order.customerMobile}}</text></view><text class="status" :class="order.status == 'completed' ? 'verified' : 'pending'">{{order.statusText}}</text></view> |
| | | <text v-if="order.category != 'stay'" class="order-name">{{order.title}}</text> |
| | | <view v-if="order.category == 'stay'" class="stay-title-row"><text class="stay-order-name">{{order.title}}</text><text class="card-tag">{{order.cardLevel}}</text></view> |
| | | <view class="customer-row"><view class="customer-info"><image class="customer-icon" src="/static/icons/lucide/circle-user-round.svg" mode="aspectFit"></image><text class="customer-name">{{order.customerName}}</text><text class="customer-phone">{{order.customerMobile}}</text></view><text class="status" :class="order.status == 'completed' ? 'verified' : ((order.status == 'cancelled' || order.status == 'refunded') ? 'closed' : 'pending')">{{order.statusText}}</text></view> |
| | | <text v-if="order.category != 'stay' && !isTicketOrder(order)" class="order-name">{{order.title}}</text> |
| | | <view v-if="order.category == 'stay' || isTicketOrder(order)" class="stay-title-row"><text class="stay-order-name">{{order.title}}</text><text class="card-tag">{{order.cardLevel}}</text></view> |
| | | <view v-else class="price-row" :class="order.category == 'store' ? 'store-price-row' : ''"><view class="price-group"><text class="price">¥{{formatAmount(order.payAmount)}}</text><text v-if="order.orderAmount > order.payAmount" class="original-price">¥{{formatAmount(order.orderAmount)}}</text></view><text class="card-tag">{{order.cardLevel}}</text></view> |
| | | <text class="time-text">{{order.status == 'completed' && order.verifyTime.length > 0 ? '核销时间:' + formatDateTime(order.verifyTime) : '下单时间:' + formatDateTime(order.createdAt)}}</text> |
| | | <text class="time-text">{{orderTimeText(order)}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | <view class="status-option" :class="statusFilter == 'all' ? 'selected' : ''" @tap="selectStatus('all')"><text class="status-option-text">全部</text><text v-if="statusFilter == 'all'" class="status-selected-mark">已选</text></view> |
| | | <view class="status-option" :class="statusFilter == pendingStatus ? 'selected' : ''" @tap="selectStatus(pendingStatus)"><text class="status-option-text">{{pendingStatusText}}</text><text v-if="statusFilter == pendingStatus" class="status-selected-mark">已选</text></view> |
| | | <view class="status-option" :class="statusFilter == 'completed' ? 'selected' : ''" @tap="selectStatus('completed')"><text class="status-option-text">已完成</text><text v-if="statusFilter == 'completed'" class="status-selected-mark">已选</text></view> |
| | | <view class="status-option" :class="statusFilter == 'closed' ? 'selected' : ''" @tap="selectStatus('closed')"><text class="status-option-text">已取消/已退款</text><text v-if="statusFilter == 'closed'" class="status-selected-mark">已选</text></view> |
| | | </view> |
| | | <view class="status-cancel" @tap="closeStatusFilter"><text class="status-cancel-text">取消</text></view> |
| | | </view> |
| | |
| | | <view v-if="datePickerVisible" class="date-sheet"> |
| | | <view class="date-sheet-head"><text class="date-sheet-title">选择日期区间</text><image class="close-icon" src="/static/icons/lucide/x.svg" mode="aspectFit" @tap="closeDateFilter"></image></view> |
| | | <view class="date-fields"> |
| | | <view class="date-field" :class="activeDateField == 'start' ? 'active' : ''" @tap="selectDateField('start')"><text class="date-field-label">起始日期</text><text class="date-field-value">{{startDate}}</text></view> |
| | | <view class="date-field" :class="activeDateField == 'start' ? 'active' : ''" @tap="selectDateField('start')"><text class="date-field-label">起始日期</text><text class="date-field-value" :class="startDate == datePlaceholder ? 'placeholder' : ''">{{startDate}}</text></view> |
| | | <view class="date-field" :class="activeDateField == 'end' ? 'active' : ''" @tap="selectDateField('end')"><text class="date-field-label">截止日期</text><text class="date-field-value" :class="endDate == datePlaceholder ? 'placeholder' : ''">{{endDate}}</text></view> |
| | | </view> |
| | | <picker-view class="date-picker" :value="pickerIndexes" :indicator-style="pickerIndicatorStyle" :mask-style="pickerMaskStyle" @change="onPickerChange"> |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { getOrderStatusText, getPendingStatusByCategory } from '../../config/order.uts' |
| | | import { loadDemoMerchantOrders, loadMerchantOrders, MerchantOrder, MerchantOrderPage, MerchantOrderQuery } from '../../services/orders.uts' |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { loadMerchantBusinessDate, loadMerchantOrders, MerchantOrder, MerchantOrderPage, MerchantOrderQuery } from '../../services/orders.uts' |
| | | import { hasActiveSession } from '../../services/session.uts' |
| | | |
| | | const activeCategory = ref('group') |
| | |
| | | const currentPage = ref(1) |
| | | const loadMoreStep = ref(0) |
| | | const orders = ref<Array<MerchantOrder>>([]) |
| | | const isDevelopment = apiEnvironment == 'development' |
| | | const demoDataEnabled = ref(isDevelopment && uni.getStorageSync('merchant-orders-demo-data') != false) |
| | | const dateFilterApplied = ref(false) |
| | | const pageSize = 3 |
| | | let cancelOrderRequest: (() => void) | null = null |
| | |
| | | const allMonths = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] |
| | | const allDays = ['1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日', '13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日', '25日', '26日', '27日', '28日', '29日', '30日', '31日'] |
| | | |
| | | // 当前静态阶段使用设备日期;接口接入后应以后台业务日期覆盖该返回值。 |
| | | const getBusinessDate = () => { |
| | | return new Date() |
| | | const deviceDate = new Date() |
| | | const currentYear = ref(deviceDate.getFullYear()) |
| | | const currentMonth = ref(deviceDate.getMonth() + 1) |
| | | const currentDay = ref(deviceDate.getDate()) |
| | | const buildYears = (fromYear: number): Array<string> => { |
| | | const values: Array<string> = [] |
| | | for (let year = fromYear; year <= 2099; year++) values.push(year.toString() + '年') |
| | | return values |
| | | } |
| | | |
| | | const businessDate = getBusinessDate() |
| | | const currentYear = businessDate.getFullYear() |
| | | const currentMonth = businessDate.getMonth() + 1 |
| | | const currentDay = businessDate.getDate() |
| | | const years = [(currentYear - 2).toString() + '年', (currentYear - 1).toString() + '年', currentYear.toString() + '年'] |
| | | const months = ref(allMonths.slice(0, currentMonth)) |
| | | const days = ref(allDays.slice(0, currentDay)) |
| | | const defaultDate = currentYear.toString() + '年' + currentMonth.toString() + '月' + currentDay.toString() + '日' |
| | | const defaultDateValue = currentYear * 10000 + currentMonth * 100 + currentDay |
| | | const startDate = ref(defaultDate) |
| | | const years = ref(buildYears(currentYear.value - 2)) |
| | | const months = ref(allMonths.slice()) |
| | | const days = ref(allDays.slice(0, new Date(currentYear.value, currentMonth.value, 0).getDate())) |
| | | const defaultDate = ref(currentYear.value.toString() + '年' + currentMonth.value.toString() + '月' + currentDay.value.toString() + '日') |
| | | const defaultDateValue = ref(currentYear.value * 10000 + currentMonth.value * 100 + currentDay.value) |
| | | const startDate = ref(datePlaceholder) |
| | | const endDate = ref(datePlaceholder) |
| | | const startDateValue = ref(defaultDateValue) |
| | | const startDateValue = ref(0) |
| | | const endDateValue = ref(0) |
| | | const dateFilterText = ref('时间筛选') |
| | | const pickerIndexes = ref([2, currentMonth - 1, currentDay - 1]) |
| | | const pickerIndexes = ref([2, currentMonth.value - 1, currentDay.value - 1]) |
| | | const pickerIndicatorStyle = 'height: 39px; background-color: rgba(255, 241, 228, 0.22); border-top: 1px solid #FFE1C8; border-bottom: 1px solid #FFE1C8;' |
| | | const pickerMaskStyle = 'background: transparent;' |
| | | |
| | |
| | | const month = Math.floor(value / 100) % 100 |
| | | const day = value % 100 |
| | | return year.toString() + '-' + (month < 10 ? '0' : '') + month.toString() + '-' + (day < 10 ? '0' : '') + day.toString() |
| | | } |
| | | |
| | | const applyBusinessDate = (value: string) => { |
| | | const parts = value.split('-') |
| | | if (parts.length != 3) return |
| | | const year = Number(parts[0]); const month = Number(parts[1]); const day = Number(parts[2]) |
| | | if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day) || month < 1 || month > 12 || day < 1 || day > 31) return |
| | | currentYear.value = year; currentMonth.value = month; currentDay.value = day |
| | | years.value = buildYears(year - 2) |
| | | months.value = allMonths.slice(); days.value = allDays.slice(0, new Date(year, month, 0).getDate()) |
| | | defaultDate.value = year.toString() + '年' + month.toString() + '月' + day.toString() + '日' |
| | | defaultDateValue.value = year * 10000 + month * 100 + day |
| | | if (!dateFilterApplied.value) { startDate.value = datePlaceholder; startDateValue.value = 0; endDate.value = datePlaceholder; endDateValue.value = 0; pickerIndexes.value = [2, month - 1, day - 1] } |
| | | } |
| | | |
| | | const buildOrderQuery = (page: number): MerchantOrderQuery => ({ |
| | |
| | | loadMoreState.value = 'loading' |
| | | } |
| | | const query = buildOrderQuery(page) |
| | | if (demoDataEnabled.value) { |
| | | setTimeout(() => { |
| | | applyOrderPage(loadDemoMerchantOrders(query), append) |
| | | }, 180) |
| | | return |
| | | } |
| | | if (!hasActiveSession()) { |
| | | showOrderError('请登录后加载真实订单', '', append) |
| | | return |
| | |
| | | |
| | | const reloadOrders = () => requestOrders(1, false) |
| | | |
| | | const toggleDemoData = (event: any) => { |
| | | demoDataEnabled.value = event.detail != null && event.detail.value == true |
| | | uni.setStorageSync('merchant-orders-demo-data', demoDataEnabled.value) |
| | | reloadOrders() |
| | | } |
| | | |
| | | const formatAmount = (amount: number): string => (amount / 100).toFixed(2) |
| | | const formatDateTime = (value: string): string => value.length > 0 ? value.replace('T', ' ') : '—' |
| | | |
| | | /** 观影券没有金额,列表里按「标题 + 人才卡」一行展示,和免费入住同一套排版。 */ |
| | | const isTicketOrder = (order: MerchantOrder): boolean => order.sourceType == 'TICKET_PACKAGE' |
| | | const orderTimeText = (order: MerchantOrder): string => { |
| | | if (order.status == 'completed' && order.verifyTime.length > 0) return '核销时间:' + formatDateTime(order.verifyTime) |
| | | return (isTicketOrder(order) ? '领取时间:' : '下单时间:') + formatDateTime(order.createdAt) |
| | | } |
| | | |
| | | const openOrderItem = (order: MerchantOrder) => { |
| | | if (order.category == 'stay') return |
| | | const query = 'id=' + encodeURIComponent(order.id) + '&orderNo=' + encodeURIComponent(order.orderNo) |
| | | if (isTicketOrder(order)) { |
| | | const rawClaimId = order.claimId != null ? order.claimId as string : '' |
| | | const claimId = rawClaimId.length > 0 ? rawClaimId : order.id.replace('TP', '') |
| | | uni.navigateTo({ url: '/pages/ticket-package-detail/ticket-package-detail?claimId=' + encodeURIComponent(claimId) }) |
| | | return |
| | | } |
| | | if (order.status == 'pending_verification') { |
| | | uni.navigateTo({ url: '/pages/verification-detail/verification-detail?' + query }) |
| | | return |
| | | } |
| | | // 已取消/已退款的订单进详情看状态,不能按「已完成」打开,否则会把取消说成完成。 |
| | | if (order.status == 'cancelled' || order.status == 'refunded') { |
| | | uni.navigateTo({ url: '/pages/order-detail/order-detail?status=' + order.status + '&' + query }) |
| | | return |
| | | } |
| | | uni.navigateTo({ url: '/pages/order-detail/order-detail?status=completed&' + query }) |
| | |
| | | const selectDateField = (field: string) => { |
| | | activeDateField.value = field |
| | | if (field == 'start') { |
| | | setPickerDate(startDateValue.value) |
| | | } else { |
| | | if (endDateValue.value == 0) { |
| | | endDateValue.value = startDateValue.value |
| | | endDate.value = startDate.value |
| | | } |
| | | setPickerDate(endDateValue.value) |
| | | setPickerDate(startDateValue.value > 0 ? startDateValue.value : defaultDateValue.value) |
| | | return |
| | | } |
| | | // 闭区间:点截止日期时若还没选过,先用起始日期的值垫上(查同一天就不用滚两遍)。 |
| | | if (endDateValue.value == 0 && startDateValue.value > 0) { |
| | | endDateValue.value = startDateValue.value |
| | | endDate.value = startDate.value |
| | | } |
| | | setPickerDate(endDateValue.value > 0 ? endDateValue.value : (startDateValue.value > 0 ? startDateValue.value : defaultDateValue.value)) |
| | | } |
| | | |
| | | const getMaxDay = (year: number, month: number) => { |
| | | if (year == currentYear && month == currentMonth) { |
| | | return currentDay |
| | | } |
| | | if (month == 2) { |
| | | return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? 29 : 28 |
| | | } |
| | |
| | | } |
| | | |
| | | const setPickerDate = (dateValue: number) => { |
| | | const year = Math.floor(dateValue / 10000) |
| | | const month = Math.floor(dateValue / 100) % 100 |
| | | const day = dateValue % 100 |
| | | const yearIndex = year - (currentYear - 2) |
| | | months.value = year == currentYear ? allMonths.slice(0, currentMonth) : allMonths |
| | | // dateValue 为 0 表示这一头还没选过(占位符状态)。以前直接按 0 算,滚轮会跳到"0 年 0 月 0 日" |
| | | // 这种不存在的列上,表现为"点进来滚轮是空的"。统一回落到业务日期。 |
| | | const value = dateValue > 0 ? dateValue : defaultDateValue.value |
| | | const year = Math.floor(value / 10000) |
| | | const month = Math.floor(value / 100) % 100 |
| | | const day = value % 100 |
| | | const yearIndex = year - (currentYear.value - 2) |
| | | months.value = allMonths.slice() |
| | | days.value = allDays.slice(0, getMaxDay(year, month)) |
| | | pickerIndexes.value = [yearIndex, month - 1, day - 1] |
| | | } |
| | |
| | | const yearIndex = indexes[0] |
| | | let monthIndex = indexes[1] |
| | | let dayIndex = indexes[2] |
| | | const selectedYear = currentYear - 2 + yearIndex |
| | | months.value = selectedYear == currentYear ? allMonths.slice(0, currentMonth) : allMonths |
| | | const selectedYear = currentYear.value - 2 + yearIndex |
| | | months.value = allMonths.slice() |
| | | if (monthIndex >= months.value.length) { |
| | | monthIndex = months.value.length - 1 |
| | | } |
| | |
| | | return |
| | | } |
| | | pickerIndexes.value = [yearIndex, monthIndex, dayIndex] |
| | | const selectedDate = years[yearIndex] + months.value[monthIndex] + days.value[dayIndex] |
| | | const selectedDate = years.value[yearIndex] + months.value[monthIndex] + days.value[dayIndex] |
| | | if (activeDateField.value == 'start') { |
| | | startDate.value = selectedDate |
| | | startDateValue.value = selectedDateValue |
| | |
| | | |
| | | const resetDateFilter = () => { |
| | | activeDateField.value = 'start' |
| | | startDate.value = defaultDate |
| | | startDate.value = datePlaceholder |
| | | endDate.value = datePlaceholder |
| | | startDateValue.value = defaultDateValue |
| | | startDateValue.value = 0 |
| | | endDateValue.value = 0 |
| | | months.value = allMonths.slice(0, currentMonth) |
| | | days.value = allDays.slice(0, currentDay) |
| | | pickerIndexes.value = [2, currentMonth - 1, currentDay - 1] |
| | | months.value = allMonths.slice() |
| | | days.value = allDays.slice(0, getMaxDay(currentYear.value, currentMonth.value)) |
| | | pickerIndexes.value = [2, currentMonth.value - 1, currentDay.value - 1] |
| | | dateFilterText.value = '时间筛选' |
| | | dateFilterApplied.value = false |
| | | } |
| | | |
| | | const confirmDateFilter = () => { |
| | | if (endDateValue.value > 0 && startDateValue.value <= endDateValue.value) { |
| | | datePickerVisible.value = false |
| | | dateFilterText.value = '已选日期' |
| | | dateFilterApplied.value = true |
| | | reloadOrders() |
| | | uni.showToast({ title: '日期区间已应用', icon: 'none' }) |
| | | } else { |
| | | uni.showToast({ title: '请选择截止日期', icon: 'none' }) |
| | | // 闭区间:起始日期和截止日期都要选。缺哪一头就提示哪一头,并把焦点挪到那一头 |
| | | // (以前不管缺哪一头都提示"请选择截止日期":只选了截止日的人会以为系统没认下他选的那天)。 |
| | | if (startDateValue.value == 0) { |
| | | activeDateField.value = 'start' |
| | | setPickerDate(defaultDateValue.value) |
| | | uni.showToast({ title: '请选择起始日期', icon: 'none' }) |
| | | return |
| | | } |
| | | if (endDateValue.value == 0) { |
| | | activeDateField.value = 'end' |
| | | setPickerDate(startDateValue.value) |
| | | uni.showToast({ title: '请选择截止日期', icon: 'none' }) |
| | | return |
| | | } |
| | | if (startDateValue.value > endDateValue.value) { |
| | | uni.showToast({ title: '起始日不能晚于截止日', icon: 'none' }) |
| | | return |
| | | } |
| | | datePickerVisible.value = false |
| | | dateFilterText.value = '已选日期' |
| | | dateFilterApplied.value = true |
| | | reloadOrders() |
| | | uni.showToast({ title: '日期区间已应用', icon: 'none' }) |
| | | } |
| | | |
| | | const openOrder = (orderId: string) => { |
| | |
| | | uni.navigateTo({ url: '/pages/order-detail/order-detail?status=completed&id=' + orderId }) |
| | | return |
| | | } |
| | | uni.showToast({ title: '订单详情待接入', icon: 'none' }) |
| | | uni.showToast({ title: '该演示订单不可查看详情', icon: 'none' }) |
| | | } |
| | | |
| | | onLoad(() => { |
| | | if (hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {}) |
| | | }) |
| | | |
| | | // 列表用 onShow 拉:核销成功后从结果页/详情页返回时,状态才是最新的。 |
| | | // 之前只在 onLoad 拉一次,返回列表还停在「待核销」,要切一次分类标签才刷新。 |
| | | onShow(() => { |
| | | reloadOrders() |
| | | }) |
| | | |
| | |
| | | .status { font-size: 24rpx; line-height: 34rpx; font-weight: 600; } |
| | | .pending { color: #FF5A42; } |
| | | .verified { color: #98A3B4; } |
| | | .closed { color: #98A3B4; } |
| | | .order-name { color: #182236; font-size: 31rpx; line-height: 44rpx; font-weight: 700; margin-top: 26rpx; } |
| | | .price-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-top: 18rpx; } |
| | | .store-price-row { margin-top: 24rpx; } |