完成订单管理的数据库链接和展示

This commit is contained in:
2026-03-16 10:40:33 +08:00
parent 500160df11
commit 05aba1fa4f
2 changed files with 316 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
<template>
<view v-if="visible" class="drawer-mask" @click="close">
<view class="drawer-container" @click.stop>
<view v-if="visible" class="drawer-mask" :class="{ closing: isClosing }" @click="close">
<view class="drawer-container" :class="{ closing: isClosing }" @click.stop>
<view class="drawer-header">
<view class="header-left">
<text class="title">订单详情</text>
@@ -36,12 +36,12 @@
<text class="value status-val">¥ {{ orderInfo['paidAmount'] }}</text>
</view>
<view class="summary-grid">
<text class="label">支付方式</text>
<text class="label">支付状态</text>
<text class="value">{{ orderInfo['payMethod'] }}</text>
</view>
<view class="summary-grid">
<text class="label">配送人员</text>
<text class="value">{{ orderInfo['delivery_name'] }}</text>
<text class="label">支付时间</text>
<text class="value">{{ orderInfo['payTime'] || '--' }}</text>
</view>
</view>
</view>
@@ -108,7 +108,7 @@
</view>
<view class="info-item">
<text class="label">商品总数:</text>
<text class="value">{{ orderInfo['total_num'] || '0' }}</text>
<text class="value">{{ totalNum > 0 ? totalNum : (orderInfo['total_num'] || '0') }}</text>
</view>
<view class="info-item">
<text class="label">产品金额:</text>
@@ -179,7 +179,7 @@
</view>
<view class="p-td p-price">¥{{ p['price'] }}</view>
<view class="p-td p-num">{{ p['quantity'] }}</view>
<view class="p-td p-total">¥{{ (parseFloat(p['price'] as string) * parseInt(p['quantity'] as string)).toFixed(2) }}</view>
<view class="p-td p-total">¥{{ ((p['price'] as number) * (p['quantity'] as number)).toFixed(2) }}</view>
</view>
</view>
</view>
@@ -187,13 +187,16 @@
<!-- 订单记录 -->
<view v-if="activeTab === 2" class="info-section">
<view class="timeline">
<view v-if="logs.length === 0" class="empty-logs">
<text class="empty-logs-text">暂无订单记录</text>
</view>
<view v-else class="timeline">
<view v-for="(log, li) in logs" :key="li" class="timeline-item">
<view class="dot"></view>
<view class="line" v-if="li !== logs.length - 1"></view>
<view class="log-content">
<text class="log-title">{{ log.title }}</text>
<text class="log-time">{{ log.time }}</text>
<text class="log-title">{{ log['title'] }}</text>
<text class="log-time">{{ log['time'] }}</text>
</view>
</view>
</view>
@@ -206,6 +209,7 @@
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
import { supabase } from '@/components/supadb/aksupainstance.uts'
const props = defineProps({
visible: { type: Boolean, default: false },
@@ -217,22 +221,97 @@ const emit = defineEmits(['update:visible'])
const activeTab = ref(0)
const tabs = ['订单信息', '商品信息', '订单记录']
const productItems = computed<UTSJSONObject[]>(() => {
return (props.orderInfo['items'] || []) as UTSJSONObject[]
const fetchedItems = ref<UTSJSONObject[]>([])
const itemsLoading = ref(false)
const isClosing = ref(false)
const productItems = computed<UTSJSONObject[]>(() => fetchedItems.value)
const totalNum = computed<number>(() => {
return fetchedItems.value.reduce((sum: number, p: UTSJSONObject) : number => {
const qty = p['quantity'] != null ? (p['quantity'] as number) : 0
return sum + qty
}, 0)
})
const logs = ref([
{ title: '订单生成', time: '2026-02-27 15:47:25' },
{ title: '支付成功', time: '2026-02-27 15:48:30' }
])
const fmtTime = (timeStr : string | null) : string => {
if (timeStr == null || timeStr == '') return ''
if (timeStr.indexOf('T') > -1) {
const parts = timeStr.split('T')
const timePart = parts[1].split('.')[0]
return parts[0] + ' ' + timePart
}
return timeStr
}
const logs = computed<UTSJSONObject[]>(() => {
const result: UTSJSONObject[] = []
const createdAt = props.orderInfo['created_at'] as string | null
if (createdAt != null && createdAt !== '' && createdAt !== '--') {
result.push({ title: '订单创建', time: createdAt } as UTSJSONObject)
}
const paidAt = props.orderInfo['payTime'] as string | null
if (paidAt != null && paidAt !== '' && paidAt !== '--') {
result.push({ title: '支付成功', time: paidAt } as UTSJSONObject)
}
const shippedAt = fmtTime(props.orderInfo['shipped_at'] as string | null)
if (shippedAt !== '') {
result.push({ title: '商家发货', time: shippedAt } as UTSJSONObject)
}
const deliveredAt = fmtTime(props.orderInfo['delivered_at'] as string | null)
if (deliveredAt !== '') {
result.push({ title: '确认收货', time: deliveredAt } as UTSJSONObject)
}
const completedAt = fmtTime(props.orderInfo['completed_at'] as string | null)
if (completedAt !== '') {
result.push({ title: '交易完成', time: completedAt } as UTSJSONObject)
}
return result
})
const fetchItems = async () => {
const orderId = props.orderInfo['id']
if (orderId == null || orderId === '') return
itemsLoading.value = true
fetchedItems.value = []
try {
const res = await supabase
.from('ml_order_items')
.select('*')
.eq('order_id', orderId as string)
.execute()
if (res.error == null && res.data != null) {
fetchedItems.value = (res.data as UTSJSONObject[]).map((p: UTSJSONObject) : UTSJSONObject => {
return {
image: p['image_url'] != null ? (p['image_url'] as string) : '/static/logo.png',
name: p['product_name'] != null ? (p['product_name'] as string) : '--',
sku_info: p['sku_name'] != null ? (p['sku_name'] as string) : (p['specifications'] != null ? (p['specifications'] as string) : '-'),
price: p['price'] != null ? p['price'] : 0,
quantity: p['quantity'] != null ? p['quantity'] : 0
} as UTSJSONObject
})
}
} catch (e) {
console.error('fetchItems error:', e)
} finally {
itemsLoading.value = false
}
}
const close = () => {
isClosing.value = true
setTimeout(() => {
isClosing.value = false
emit('update:visible', false)
}, 280)
}
watch(() => props.visible, (newVal) => {
if (newVal) {
isClosing.value = false
activeTab.value = 0
fetchedItems.value = []
fetchItems()
}
})
</script>
@@ -244,7 +323,12 @@ watch(() => props.visible, (newVal) => {
background-color: rgba(0,0,0,0.5);
z-index: 2000;
display: flex;
flex-direction: row;
justify-content: flex-end;
animation: drawerMaskFadeIn 0.3s ease-out;
&.closing {
animation: drawerMaskFadeOut 0.28s ease-in forwards;
}
}
.drawer-container {
@@ -255,6 +339,27 @@ watch(() => props.visible, (newVal) => {
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
animation: drawerSlideIn 0.3s cubic-bezier(0.23, 1, 0.32, 1);
&.closing {
animation: drawerSlideOut 0.28s cubic-bezier(0.755, 0.05, 0.855, 0.06) forwards;
}
}
@keyframes drawerSlideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes drawerSlideOut {
from { transform: translateX(0); }
to { transform: translateX(100%); }
}
@keyframes drawerMaskFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes drawerMaskFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.drawer-header {
@@ -405,6 +510,8 @@ watch(() => props.visible, (newVal) => {
/* 记录 */
.timeline { padding: 24px; background-color: #fff; }
.empty-logs { padding: 40px 24px; background-color: #fff; display: flex; align-items: center; justify-content: center; }
.empty-logs-text { font-size: 13px; color: #bfbfbf; }
.timeline-item { position: relative; padding-left: 24px; padding-bottom: 24px; }
.dot { position: absolute; left: 0; top: 4px; width: 10px; height: 10px; border-radius: 5px; background-color: #1890ff; z-index: 2; }
.line { position: absolute; left: 4.5px; top: 14px; bottom: -4px; width: 1px; background-color: #e8e8e8; }

View File

@@ -77,7 +77,7 @@
<view class="th col-product">商品信息</view>
<view class="th col-user">用户信息</view>
<view class="th col-price">实际支付</view>
<view class="th col-pay">支付方式</view>
<view class="th col-pay">支付状态</view>
<view class="th col-time">支付时间</view>
<view class="th col-status">订单状态</view>
<view class="th col-op">操作</view>
@@ -89,6 +89,12 @@
<text>加载中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="fetchError != ''" class="error-state">
<text class="error-text">{{ fetchError }}</text>
<button class="retry-btn" @click="fetchData">重新加载</button>
</view>
<!-- 无数据状态 -->
<view v-else-if="filteredOrders.length === 0" class="empty-state">
<text>暂无订单数据</text>
@@ -105,6 +111,7 @@
<view class="td col-order">
<text class="order-sn">{{ item['sn'] }}</text>
<text class="order-type" :class="item['typeColor']">[{{ item['typeName'] }}]</text>
<text class="order-time">{{ item['created_at'] }}</text>
<text v-if="item['cancelStatus'] != ''" class="cancel-text">{{ item['cancelStatus'] }}</text>
</view>
<!-- 商品信息 -->
@@ -138,26 +145,42 @@
</view>
<!-- 订单状态 -->
<view class="td col-status">
<text class="status-text">{{ item['statusName'] }}</text>
<text class="status-text" :style="{ color: item['statusColor'] }">{{ item['statusName'] }}</text>
</view>
<!-- 操作 -->
<view class="td col-op overflow-visible no-wrap">
<view class="op-links">
<text class="op-link primary" @click.stop="handleAction('edit', item['sn'] as string)">编辑</text>
<text class="op-link primary" @click.stop="viewDetail(item)">详情</text>
<template v-if="(item['orderStatus'] as number) === 2">
<view class="divider-v"></view>
<view class="op-dropdown-container">
<view class="op-link-more" @click.stop="toggleDropdown(item['sn'] as string)">
<text class="op-link primary" @click.stop="shipOrder(item)">发货</text>
</template>
<view class="divider-v"></view>
<view class="op-dropdown-container" @mouseenter="activeDropdownId = (item['sn'] as string)" @mouseleave="activeDropdownId = ''">
<view class="op-link-more">
<text class="more-text">更多</text>
<view :class="{ 'arrow-up-blue': activeDropdownId === item['sn'], 'arrow-down-blue': activeDropdownId !== item['sn'] }"></view>
</view>
<!-- 浮动菜单 -->
<view v-if="activeDropdownId === (item['sn'] as string)" class="dropdown-menu">
<view class="dropdown-item" @click.stop="viewDetail(item)">
<text class="item-text">订单详情</text>
<view class="dropdown-item" @click.stop="handleMore('print_receipt', item)">
<text class="item-text">小票打印</text>
</view>
<view class="dropdown-item" @click.stop="handleMore('edit_address', item)">
<text class="item-text">修改地址</text>
</view>
<view class="dropdown-item" @click.stop="handleMore('remark', item)">
<text class="item-text">订单备注</text>
</view>
<view class="dropdown-item" @click.stop="handleMore('refund', item)">
<text class="item-text">立即退款</text>
</view>
<view class="dropdown-item item-danger" @click.stop="deleteOrder(item)">
<text class="item-text text-red">删除订单</text>
</view>
<view class="dropdown-item" @click.stop="handleMore('packing_slip', item)">
<text class="item-text">配货单打印</text>
</view>
</view>
</view>
</view>
@@ -169,7 +192,7 @@
<!-- 分页 -->
<view class="pagination-footer">
<view class="page-left">
<text class="count-text">共 {{ orderData.length }} 条</text>
<text class="count-text">共 {{ filteredOrders.length }} 条</text>
<view class="page-size-select">
<text>10条/页</text>
<view class="arrow-down"></view>
@@ -209,6 +232,7 @@ const searchKeyword = ref('')
const activeDropdownId = ref('')
const showDetail = ref(false)
const selectedOrder = ref<UTSJSONObject>({} as UTSJSONObject)
const fetchError = ref('')
// 时间显示格式化
const formatTime = (timeStr : string | null) : string => {
@@ -236,6 +260,18 @@ const statusTabs = [
const orderData = ref<UTSJSONObject[]>([])
// 订单状态颜色映射
const getStatusColor = (status : number) : string => {
if (status === 1) return '#faad14'
if (status === 2) return '#1890ff'
if (status === 3) return '#1890ff'
if (status === 4) return '#52c41a'
if (status === 5) return '#999999'
if (status === 6) return '#fa8c16'
if (status === 7) return '#f5222d'
return '#333333'
}
const filteredOrders = computed<UTSJSONObject[]>(() => {
let list = orderData.value
@@ -249,9 +285,9 @@ const filteredOrders = computed<UTSJSONObject[]>(() => {
if (searchKeyword.value.trim() !== '') {
const kw = searchKeyword.value.toLowerCase()
list = list.filter((o : UTSJSONObject) : boolean => {
const sn = (o['sn'] as string).toLowerCase()
const sn = o['sn'] != null ? (o['sn'] as string).toLowerCase() : ''
const user = o['user'] as UTSJSONObject
const phone = user['phone'] as string
const phone = (user != null && user['phone'] != null) ? (user['phone'] as string) : ''
return sn.includes(kw) || phone.includes(kw)
})
}
@@ -261,62 +297,107 @@ const filteredOrders = computed<UTSJSONObject[]>(() => {
const fetchData = async () => {
loading.value = true
fetchError.value = ''
// 获取当前商家 IDmerchant_id = ak_users.id = Supabase Auth UUID
const currentMerchantId = supabase.getSession().user?.getString('id')
if (currentMerchantId == null || currentMerchantId == '') {
loading.value = false
fetchError.value = '未获取到商家身份信息,请重新登录后再试'
return
}
try {
// 从 ml_orders_detail_view 视图读取数据
const res = await supabase
.from('ml_orders_detail_view')
.select('*')
.eq('merchant_id', currentMerchantId)
.order('created_at', { ascending: false })
.execute()
if (res.error == null && res.data != null) {
const rawData = res.data as UTSJSONObject[]
orderData.value = rawData.map((item: UTSJSONObject) : UTSJSONObject => {
// 解析 shipping_address JSONB含 receiver_name/receiver_phone/province/city/district/address_detail
const rawAddr = item['shipping_address']
let receiverName = item['customer_name'] != null ? (item['customer_name'] as string) : ''
let receiverPhone = item['customer_phone'] != null ? (item['customer_phone'] as string) : ''
let fullAddress = '暂无地址信息'
if (rawAddr != null) {
const addr = rawAddr as UTSJSONObject
const rn = addr['receiver_name']
const rp = addr['receiver_phone']
if (rn != null && rn !== '') receiverName = rn as string
if (rp != null && rp !== '') receiverPhone = rp as string
const prov = addr['province'] != null ? (addr['province'] as string) : ''
const cit = addr['city'] != null ? (addr['city'] as string) : ''
const dist = addr['district'] != null ? (addr['district'] as string) : ''
const det = addr['address_detail'] != null ? (addr['address_detail'] as string) : ''
const combined = prov + cit + dist + det
if (combined !== '') fullAddress = combined
}
const orderStatusNum = item['order_status'] as number
// cancelStatus取消原因展示
let cancelStatus = ''
if (orderStatusNum === 5) {
const reason = item['cancel_reason']
cancelStatus = (reason != null && reason !== '') ? ('已取消:' + (reason as string)) : '用户已取消'
}
return {
sn: item['order_no'],
id: item['id'] != null ? (item['id'] as string) : '',
sn: item['order_no'] != null ? item['order_no'] : '--',
typeName: '普通订单',
typeColor: 'green',
orderStatus: item['order_status'],
cancelStatus: item['order_status'] === 5 ? '用户已取消' : '',
orderStatus: orderStatusNum,
cancelStatus: cancelStatus,
product: {
img: '/static/logo.png', // 默认占位图
name: '订单概要 (详情查看)'
img: '/static/logo.png',
name: '订单详情查看'
} as UTSJSONObject,
items: item['order_items'] as UTSJSONObject[], // 尝试获取子项
items: null, // ml_orders_detail_view 视图未聚合 order_items详情抽屉另行处理
user: {
name: (item['customer_name'] || '未知用户') as string,
id: (item['user_id'] || '--') as string,
phone: (item['customer_phone'] || '--') as string
phone: receiverPhone != '' ? receiverPhone : (item['customer_phone'] != null ? (item['customer_phone'] as string) : '--')
} as UTSJSONObject,
isPaid: item['payment_status'] === 2,
actualPrice: item['total_amount'], // 总金额
paidAmount: item['paid_amount'] != null ? item['paid_amount'] : 0.00, // 支付金额
payMethod: item['payment_status_name'] || '--',
actualPrice: item['paid_amount'] != null ? item['paid_amount'] : 0, // 已付金额(修正:原为 total_amount
paidAmount: item['paid_amount'] != null ? item['paid_amount'] : 0,
payMethod: item['payment_status_name'] != null ? (item['payment_status_name'] as string) : '--', // 支付状态ml_orders 表无支付方式字段)
payTime: formatTime(item['paid_at'] as string | null),
created_at: formatTime(item['created_at'] as string | null),
statusName: item['order_status_name'] || '未知',
primaryAction: item['order_status'] === 1 ? '立即支付' : '',
total_num: Array.isArray(item['order_items']) ? (item['order_items'] as Array<any>).length : 0,
total_price: item['product_amount'] != null ? item['product_amount'] : 0.00, // 产品价格
coupon_price: item['discount_amount'] != null ? item['discount_amount'] : 0.00, // 折扣价
shipping_fee: item['shipping_fee'] != null ? item['shipping_fee'] : 0.00, // 运费
statusName: item['order_status_name'] != null ? (item['order_status_name'] as string) : '未知',
statusColor: getStatusColor(orderStatusNum),
primaryAction: orderStatusNum === 1 ? '立即支付' : '',
total_num: 0,
total_price: item['product_amount'] != null ? item['product_amount'] : 0,
coupon_price: item['discount_amount'] != null ? item['discount_amount'] : 0,
shipping_fee: item['shipping_fee'] != null ? item['shipping_fee'] : 0,
deduction_price: 0,
user_address: item['shipping_address'] != null ? (typeof item['shipping_address'] === 'string' ? item['shipping_address'] : JSON.stringify(item['shipping_address'])) : '暂无地址信息',
real_name: item['customer_name'] != null ? item['customer_name'] : '', // 消费者名字
user_phone: item['customer_phone'] != null ? item['customer_phone'] : '',
delivery_name: item['merchant_name'] != null ? item['merchant_name'] : '--', // 配送人员
store_name: item['shop_name'] != null ? item['shop_name'] : '--', // 店铺名字
mark: item['remark'] != null ? item['remark'] : '-',
remark: item['merchant_memo'] != null ? item['merchant_memo'] : '-'
user_address: fullAddress,
real_name: receiverName,
user_phone: receiverPhone,
delivery_name: item['merchant_name'] != null ? (item['merchant_name'] as string) : '--',
store_name: item['shop_name'] != null ? (item['shop_name'] as string) : '--',
mark: item['remark'] != null ? (item['remark'] as string) : '-',
remark: item['merchant_memo'] != null ? (item['merchant_memo'] as string) : '-',
shipped_at: item['shipped_at'] != null ? (item['shipped_at'] as string) : '',
delivered_at: item['delivered_at'] != null ? (item['delivered_at'] as string) : '',
completed_at: item['completed_at'] != null ? (item['completed_at'] as string) : ''
} as UTSJSONObject
})
// 更新统计数据
updateTabCounts()
} else {
fetchError.value = '加载订单失败,请检查网络后重试'
console.error('Fetch orders error:', res.error)
}
} catch (e) {
fetchError.value = '加载订单时发生异常,请检查网络后重试'
console.error('Fetch orders exception:', e)
} finally {
loading.value = false
@@ -364,6 +445,39 @@ const viewDetail = (order: UTSJSONObject) => {
closeDropdowns()
}
// 发货操作
const shipOrder = async (order: UTSJSONObject) => {
uni.showModal({
title: '确认发货',
content: `确定将订单 ${order['sn']} 标记为已发货?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await supabase
.from('ml_orders')
.update({ order_status: 3, shipping_status: 2, shipped_at: new Date().toISOString() } as UTSJSONObject)
.eq('order_no', order['sn'] as string)
.execute()
if (result.error == null) {
uni.showToast({ title: '发货成功' })
fetchData()
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
} catch (e) {
console.error('shipOrder error:', e)
}
}
}
})
}
// 更多菜单操作(功能待接入)
const handleMore = (_action: string, _order: UTSJSONObject) => {
closeDropdowns()
uni.showToast({ title: '功能开发中', icon: 'none' })
}
// 删除订单 (权限占位代码)
const deleteOrder = async (order: UTSJSONObject) => {
closeDropdowns()
@@ -400,11 +514,6 @@ const deleteOrder = async (order: UTSJSONObject) => {
})
}
const handleAction = (action: string, sn: string) => {
uni.showToast({ title: `执行操作: ${action} - ${sn}`, icon: 'none' })
closeDropdowns()
}
onMounted(() => {
fetchData()
// 点击空白处关闭下拉
@@ -705,7 +814,7 @@ onMounted(() => {
.col-pay { width: 100px; }
.col-time { width: 160px; }
.col-status { width: 100px; }
.col-op { width: 140px; }
.col-op { width: 160px; }
.order-sn { font-size: 13px; color: #262626; margin-bottom: 4px; text-align: left; }
.order-type { font-size: 12px; text-align: left; }
@@ -879,5 +988,40 @@ onMounted(() => {
}
.page-btns-more { border: none; }
.order-time {
font-size: 12px;
color: #999;
margin-top: 2px;
display: block;
}
.price-unpaid {
color: #f5222d;
}
.error-state {
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-text {
font-size: 14px;
color: #f5222d;
}
.retry-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
background-color: #fff;
color: #1890ff;
border: 1px solid #1890ff;
margin: 0;
}
</style>