Files
medical-mall/pages/mall/admin/order/cashier-order/index.uvue
2026-02-13 17:29:50 +08:00

484 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="admin-cashier-order">
<view class="content-body">
<!-- 顶部过滤栏 -->
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">创建时间:</text>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
<view class="filter-item">
<text class="label-txt">订单号:</text>
<input class="search-input" placeholder="请输入订单号" v-model="orderId" @confirm="handleQuery" />
</view>
<view class="filter-item">
<text class="label-txt">用户名:</text>
<input class="search-input" placeholder="请输入用户名" v-model="username" @confirm="handleQuery" />
</view>
<view class="btn-query" @click="handleQuery">
<text class="query-txt">查询</text>
</view>
</view>
<!-- 主要内容区域 -->
<view class="table-card border-shadow">
<view class="card-header">
<view class="btn-primary-blue" @click="openQrModal">
<text class="btn-txt">查看收款二维码</text>
</view>
</view>
<!-- 数据表格 -->
<view class="table-container">
<view class="table-header-row">
<view class="th" style="flex: 1.5;">订单号</view>
<view class="th" style="flex: 1.2;">用户信息</view>
<view class="th" style="width: 150px;">实际支付</view>
<view class="th" style="width: 150px;">优惠价格</view>
<view class="th" style="width: 200px;">支付时间</view>
</view>
<view class="table-body">
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
<text>加载中...</text>
</view>
<view v-else-if="orderList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
<text>暂无收银订单</text>
</view>
<view v-else v-for="(item, index) in orderList" :key="index" class="table-row">
<view class="td" style="flex: 1.5;"><text class="td-txt">{{ item.orderId }}</text></view>
<view class="td" style="flex: 1.2;"><text class="td-txt">{{ item.userInfo }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">¥{{ item.payPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">¥{{ item.discountPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 200px;"><text class="td-txt">{{ item.payTime }}</text></view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination-footer">
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-btns">
<view class="page-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
<text><</text>
</view>
<view class="page-btn active">
<text>{{ page }}</text>
</view>
<view class="page-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
<text>></text>
</view>
</view>
<view class="page-jump">
<text class="jump-txt">前往</text>
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
<text class="jump-txt">页</text>
</view>
</view>
</view>
</view>
<!-- 收款码弹窗 -->
<view v-if="showQrModal" :class="['modal-mask', isClosing ? 'mask-fade-out' : '']" @click="closeQrModal">
<view :class="['modal-content', isClosing ? 'scale-out' : 'scale-in']" @click.stop="">
<view class="modal-header">
<text class="modal-title">收款码</text>
<text class="close-btn" @click="closeQrModal">×</text>
</view>
<view class="modal-body">
<view class="qr-item">
<image class="qr-img" src="/static/logo.png" mode="aspectFit"></image>
<text class="qr-label">公众号二维码</text>
</view>
<view class="qr-item">
<view class="qr-placeholder-mp">
<image class="mp-qr-mock" src="/static/logo.png" mode="aspectFit"></image>
</view>
<text class="qr-label">小程序二维码</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchCashierOrderPage } from '@/services/orderService.uts'
interface CashierOrder {
orderId: string
userInfo: string
payPrice: number
discountPrice: number
payTime: string
}
const orderId = ref('')
const username = ref('')
const total = ref(0)
const orderList = ref<CashierOrder[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const startDate = ref('')
const endDate = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
const pages = Math.ceil(total.value / pageSize.value)
return pages <= 0 ? 1 : pages
})
const loadCashierOrders = async () => {
loading.value = true
try {
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
const res = await fetchCashierOrderPage(
page.value,
pageSize.value,
orderId.value ? orderId.value : null,
username.value ? username.value : null,
st,
et
)
orderList.value = res.items.map((item: any): CashierOrder => {
return {
orderId: String(item.order_no),
userInfo: `${String(item.customer_name ?? '未知')} | ${String(item.customer_phone ?? '')}`,
payPrice: parseFloat(String(item.total_amount ?? '0')),
discountPrice: parseFloat(String(item.discount_amount ?? '0')),
payTime: String(item.paid_at ?? '--')
} as CashierOrder
})
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载收银订单失败', icon: 'none' })
} finally {
loading.value = false
}
}
onMounted(() => {
loadCashierOrders()
})
const handleQuery = () => {
page.value = 1
loadCashierOrders()
}
function onApplyRange(payload : any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
handleQuery()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
handleQuery()
}
const prevPage = () => {
if (page.value > 1) {
page.value--
loadCashierOrders()
}
}
const nextPage = () => {
if (page.value < totalPages.value) {
page.value++
loadCashierOrders()
}
}
const goToJumpPage = () => {
const targetPage = parseInt(jumpPage.value)
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
page.value = targetPage
loadCashierOrders()
jumpPage.value = ''
} else {
uni.showToast({ title: '页码无效', icon: 'none' })
}
}
const showQrModal = ref(false)
const isClosing = ref(false)
const openQrModal = () => {
showQrModal.value = true
isClosing.value = false
}
const closeQrModal = () => {
isClosing.value = true
setTimeout(() => {
showQrModal.value = false
isClosing.value = false
}, 300)
}
</script>
<style scoped lang="scss">
.admin-cashier-order {
background-color: #f0f2f5;
min-height: 100vh;
padding: 24px;
}
.border-shadow {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.content-body {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 过滤栏 */
.filter-card {
padding: 24px;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.label-txt { font-size: 14px; color: #606266; }
.date-picker-mock {
width: 240px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
gap: 8px;
}
.date-txt { font-size: 14px; color: #c0c4cc; }
.date-split { color: #dcdfe6; }
.calendar-ic { font-size: 14px; color: #c0c4cc; margin-left: auto; }
.search-input {
width: 220px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 14px;
}
.btn-query {
background-color: #2d8cf0;
padding: 0 24px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
}
.query-txt { color: #fff; font-size: 14px; }
/* 表格区域 */
.table-card {
background-color: #fff;
display: flex;
flex-direction: column;
}
.card-header { padding: 20px; }
.btn-primary-blue {
background-color: #2d8cf0;
padding: 8px 16px;
border-radius: 4px;
display: inline-flex;
cursor: pointer;
}
.btn-txt { color: #fff; font-size: 14px; }
.table-container {
display: flex;
flex-direction: column;
}
.table-header-row {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #e8eaec;
}
.th {
padding: 15px 10px;
font-size: 14px;
color: #515a6e;
font-weight: bold;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e8eaec;
}
.td {
padding: 15px 10px;
display: flex;
align-items: center;
}
.td-txt { font-size: 14px; color: #515a6e; }
/* 分页 */
.pagination-footer {
padding: 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.total-txt { font-size: 14px; color: #606266; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.page-btn {
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
.page-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.page-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 14px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
/* Modal 弹窗逻辑 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
width: 600px;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; line-height: 1; }
.modal-body {
padding: 40px;
display: flex;
flex-direction: row;
justify-content: center;
gap: 60px;
}
.qr-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.qr-img {
width: 160px;
height: 160px;
border: 1px solid #f0f0f0;
}
.qr-placeholder-mp {
width: 160px;
height: 160px;
border: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 80px;
overflow: hidden;
}
.mp-qr-mock { width: 140px; height: 140px; border-radius: 70px; }
.qr-label { font-size: 14px; color: #666; }
/* 动画 */
.scale-in { animation: scaleIn 0.3s ease-out forwards; }
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.scale-out { animation: scaleOut 0.3s ease-in forwards; }
@keyframes scaleOut {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.9); }
}
.mask-fade-out { animation: fadeOut 0.3s ease-in forwards; }
@keyframes fadeOut {
from { background-color: rgba(0, 0, 0, 0.4); }
to { background-color: rgba(0, 0, 0, 0); }
}
</style>