引入订单数据

This commit is contained in:
2026-02-27 19:42:30 +08:00
parent b79222fbd8
commit 1673022ea3
7 changed files with 1237 additions and 571 deletions

View File

@@ -1,481 +0,0 @@
<template>
<view class="admin-page">
<view class="admin-sections">
<!-- 筛选区域 -->
<view class="admin-card filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">订单类型:</text>
<view class="mock-select">
<text>全部订单</text>
<view class="arrow-down"></view>
</view>
</view>
<view class="filter-item">
<text class="label">支付方式:</text>
<view class="mock-select">
<text>全部</text>
<view class="arrow-down"></view>
</view>
</view>
<view class="filter-item long">
<text class="label">创建时间:</text>
<view class="mock-date-range">
<image class="cal-icon" src="/static/icons/calendar.png" mode="aspectFit" />
<text class="placeholder">开始日期 - 结束日期</text>
</view>
</view>
<view class="filter-item search">
<text class="label">订单搜索:</text>
<view class="search-group">
<view class="search-select">
<text>全部</text>
<view class="arrow-down"></view>
</view>
<input class="search-input" placeholder="请输入" />
</view>
</view>
</view>
<view class="btn-row">
<button class="btn btn-primary">查询</button>
<button class="btn btn-default">重置</button>
</view>
</view>
<!-- 列表数据区域 -->
<view class="admin-card content-card">
<!-- 状态 Tabs -->
<view class="status-tabs">
<view
v-for="(tab, index) in statusTabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="activeTab = index"
>
<text class="tab-text">{{ tab.name }}</text>
<text v-if="tab.count" class="tab-count">({{ tab.count }})</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<button class="action-btn btn-blue">订单核销</button>
<button class="action-btn btn-outline">批量发货</button>
<button class="action-btn btn-outline">批量删除</button>
<button class="action-btn btn-outline">订单导出</button>
</view>
<!-- 数据表格 -->
<view class="order-table">
<view class="thead">
<view class="th col-check">
<checkbox :checked="false" color="#1890ff" />
</view>
<view class="th col-order">订单号 | 类型</view>
<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-time">支付时间</view>
<view class="th col-status">订单状态</view>
<view class="th col-op">操作</view>
</view>
<view class="tbody">
<view v-for="(item, index) in orderData" :key="index" class="tr">
<view class="td col-check">
<checkbox :checked="false" color="#1890ff" />
</view>
<!-- 订单号|类型 -->
<view class="td col-order">
<text class="order-sn">{{ item.sn }}</text>
<text class="order-type" :class="item.typeColor">[{{ item.typeName }}]</text>
<text v-if="item.cancelStatus" class="cancel-text">{{ item.cancelStatus }}</text>
</view>
<!-- 商品信息 -->
<view class="td col-product">
<view class="product-info-wrap">
<image class="p-img" :src="item.product.img" mode="aspectFill" />
<text class="p-name">{{ item.product.name }}</text>
</view>
</view>
<!-- 用户信息 -->
<view class="td col-user">
<text class="u-info">{{ item.user.phone }} | {{ item.user.id }}</text>
</view>
<!-- 实际支付 -->
<view class="td col-price">
<text class="price-val">{{ item.actualPrice }}</text>
</view>
<!-- 支付方式 -->
<view class="td col-pay">
<text class="pay-text">{{ item.payMethod }}</text>
</view>
<!-- 支付时间 -->
<view class="td col-time">
<text class="time-text">{{ item.payTime }}</text>
</view>
<!-- 订单状态 -->
<view class="td col-status">
<text class="status-text">{{ item.statusName }}</text>
</view>
<!-- 操作 -->
<view class="td col-op">
<view class="op-links">
<text class="op-link primary" v-if="item.primaryAction">{{ item.primaryAction }}</text>
<view class="op-link-more">
<text class="more-text">更多</text>
<view class="arrow-down-blue"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const activeTab = ref(2) // 默认选中待发货或待核销(手动对齐截图)
const statusTabs = [
{ name: '全部', count: null },
{ name: '待支付', count: 793 },
{ name: '待发货', count: 3695 },
{ name: '待核销', count: null },
{ name: '待收货', count: null },
{ name: '待评价', count: null },
{ name: '已完成', count: null },
{ name: '已退款', count: null },
{ name: '已删除', count: null }
]
const orderData = ref([
{
sn: 'cp541336970228400128',
typeName: '秒杀订单',
typeColor: 'blue',
cancelStatus: '用户已取消',
product: {
img: '/static/logo.png',
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室 超高清手机便携投影机 (4K超清 支持...'
},
user: { phone: '188****4074', id: '82694' },
actualPrice: '未支付',
payMethod: '--',
payTime: '--',
statusName: '未支付',
primaryAction: ''
},
{
sn: 'cp541289248708362240',
typeName: '核销订单',
typeColor: 'purple',
cancelStatus: '',
product: {
img: '/static/logo.png',
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水...'
},
user: { phone: '你就给', id: '82703' },
actualPrice: '90.1',
payMethod: '余额支付',
payTime: '2026-02-02 16:10:17',
statusName: '未核销',
primaryAction: '立即核销'
},
{
sn: 'cp541268226856714240',
typeName: '普通订单',
typeColor: 'green',
cancelStatus: '',
product: {
img: '/static/logo.png',
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060'
},
user: { phone: '王毅不睡了', id: '82689' },
actualPrice: '未支付',
payMethod: '--',
payTime: '--',
statusName: '未支付',
primaryAction: '编辑'
},
{
sn: 'cp541262080745930752',
typeName: '秒杀订单',
typeColor: 'blue',
cancelStatus: '',
product: {
img: '/static/logo.png',
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室 超高清手机便携投影机 (4K超清 支持...'
},
user: { phone: '177****8361', id: '82697' },
actualPrice: '未支付',
payMethod: '--',
payTime: '--',
statusName: '未支付',
primaryAction: '编辑'
}
])
</script>
<style scoped lang="scss">
.admin-page {
/* 使用 Layout 的背景和内边距 */
min-height: 100vh;
}
.filter-card {
padding: var(--admin-card-padding);
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
.label {
font-size: 14px;
color: #333;
width: 70px;
}
}
.mock-select {
width: 160px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
text { font-size: 14px; color: #595959; }
}
.mock-date-range {
width: 240px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
.cal-icon { width: 14px; height: 14px; margin-right: 8px; opacity: 0.4; }
.placeholder { font-size: 14px; color: #bfbfbf; }
}
.search-group {
display: flex;
flex-direction: row;
border: 1px solid #d9d9d9;
border-radius: 4px;
height: 32px;
overflow: hidden;
}
.search-select {
width: 80px;
border-right: 1px solid #d9d9d9;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 4px;
background-color: #fafafa;
text { font-size: 14px; color: #595959; }
}
.search-input {
flex: 1;
border: none;
padding: 0 12px;
font-size: 14px;
width: 120px;
}
.arrow-down {
width: 0; height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #bfbfbf;
}
.btn-row {
margin-top: 16px;
display: flex;
flex-direction: row;
gap: 8px;
}
.btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.btn-primary { background-color: #1890ff; color: #fff; border: none; }
.btn-default { background-color: #fff; color: #595959; border: 1px solid #d9d9d9; }
.content-card {
}
.status-tabs {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
padding: 0 16px;
}
.tab-item {
padding: 16px 20px;
cursor: pointer;
position: relative;
display: flex;
flex-direction: row;
gap: 2px;
.tab-text { font-size: 14px; color: #595959; }
.tab-count { font-size: 14px; color: #595959; }
&.active {
.tab-text, .tab-count { color: #1890ff; font-weight: 500; }
&::after {
content: '';
position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: #1890ff;
}
}
}
.action-bar {
padding: 16px 20px;
display: flex;
flex-direction: row;
gap: 12px;
}
.action-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
margin: 0;
display: flex;
align-items: center;
}
.btn-blue { background-color: #1890ff; color: #fff; border: none; }
.btn-outline { background-color: #fff; color: #595959; border: 1px solid #d9d9d9; }
/* 表格样式 */
.order-table {
width: 100%;
overflow-x: auto;
}
.thead {
display: flex;
flex-direction: row;
background-color: #f0f7ff;
min-width: 1200px;
}
.th {
padding: 12px 8px;
font-size: 14px;
color: #595959;
font-weight: 500;
display: flex;
align-items: center;
}
.tr {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
min-width: 1200px;
&:hover { background-color: #fafafa; }
}
.td {
padding: 16px 8px;
display: flex;
flex-direction: column;
justify-content: center;
}
/* 列宽控制 */
.col-check { width: 50px; justify-content: center; align-items: center; }
.col-order { width: 220px; }
.col-product { flex: 1; }
.col-user { width: 160px; }
.col-price { width: 100px; }
.col-pay { width: 100px; }
.col-time { width: 160px; }
.col-status { width: 100px; }
.col-op { width: 120px; }
/* 单元格具体内容样式 */
.order-sn { font-size: 13px; color: #262626; margin-bottom: 4px; }
.order-type { font-size: 12px; }
.blue { color: #1890ff; }
.purple { color: #722ed1; }
.green { color: #52c41a; }
.cancel-text { font-size: 12px; color: #ff4d4f; margin-top: 4px; }
.product-info-wrap {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
}
.p-img { width: 44px; height: 44px; border-radius: 4px; background-color: #f5f5f5; flex-shrink: 0; }
.p-name { font-size: 13px; color: #595959; line-height: 1.5; }
.u-info { font-size: 13px; color: #595959; }
.price-val { font-size: 14px; color: #262626; }
.pay-text, .time-text, .status-text { font-size: 13px; color: #595959; }
.op-links {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.op-link { font-size: 13px; cursor: pointer; }
.primary { color: #1890ff; }
.op-link-more {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
.more-text { font-size: 13px; color: #1890ff; }
.arrow-down-blue {
width: 0; height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 4px solid #1890ff;
}
</style>

View File

@@ -0,0 +1,414 @@
<template>
<view v-if="visible" class="drawer-mask" @click="close">
<view class="drawer-container" @click.stop>
<view class="drawer-header">
<view class="header-left">
<text class="title">订单详情</text>
</view>
<view class="close-btn" @click="close">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view class="drawer-body" scroll-y>
<!-- 订单概况 KPIS -->
<view class="order-summary-card">
<view class="order-type-icon">
<image src="/static/icons/order_blue.png" mode="aspectFit" class="type-icon" />
</view>
<view class="summary-info">
<view class="top-row">
<text class="order-type-text">{{ orderInfo['typeName'] || '普通订单' }}</text>
<text class="order-sn-text">订单号:{{ orderInfo['sn'] }}</text>
<text class="shop-tag" v-if="orderInfo['store_name'] != '--'">{{ orderInfo['store_name'] }}</text>
</view>
<view class="bottom-grids">
<view class="summary-grid">
<text class="label">订单状态</text>
<text class="value status-val">{{ orderInfo['statusName'] }}</text>
</view>
<view class="summary-grid">
<text class="label">总金额</text>
<text class="value price-val">¥ {{ orderInfo['actualPrice'] }}</text>
</view>
<view class="summary-grid">
<text class="label">已支付</text>
<text class="value status-val">¥ {{ orderInfo['paidAmount'] }}</text>
</view>
<view class="summary-grid">
<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>
</view>
</view>
</view>
</view>
<!-- Tabs -->
<view class="drawer-tabs">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: activeTab === index }" @click="activeTab = index">
<text class="tab-text">{{ tab }}</text>
</view>
</view>
<!-- Tab Content -->
<view class="tab-content">
<!-- 订单信息 -->
<view v-if="activeTab === 0" class="info-section">
<view class="section-block">
<view class="section-title">
<view class="blue-bar"></view>
<text>用户信息</text>
</view>
<view class="info-grid">
<view class="info-item">
<text class="label">用户名称:</text>
<text class="value">{{ orderInfo['user']['name'] }}</text>
</view>
<view class="info-item">
<text class="label">绑定电话:</text>
<text class="value">{{ orderInfo['user']['phone'] }}</text>
</view>
</view>
</view>
<view class="section-block">
<view class="section-title">
<view class="blue-bar"></view>
<text>收货信息</text>
</view>
<view class="info-grid">
<view class="info-item">
<text class="label">收货人:</text>
<text class="value">{{ orderInfo['real_name'] || orderInfo['user']['name'] }}</text>
</view>
<view class="info-item">
<text class="label">收货电话:</text>
<text class="value">{{ orderInfo['user_phone'] || orderInfo['user']['phone'] }}</text>
</view>
<view class="info-item full">
<text class="label">收货地址:</text>
<text class="value">{{ orderInfo['user_address'] || '暂无地址信息' }}</text>
</view>
</view>
</view>
<view class="section-block">
<view class="section-title">
<view class="blue-bar"></view>
<text>订单信息</text>
</view>
<view class="info-grid">
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{ orderInfo['created_at'] || '--' }}</text>
</view>
<view class="info-item">
<text class="label">商品总数:</text>
<text class="value">{{ orderInfo['total_num'] || '0' }}</text>
</view>
<view class="info-item">
<text class="label">产品金额:</text>
<text class="value">¥ {{ orderInfo['total_price'] || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">运费:</text>
<text class="value">¥ {{ orderInfo['shipping_fee'] || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">折扣金额:</text>
<text class="value">- ¥ {{ orderInfo['coupon_price'] || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">总金额:</text>
<text class="value">¥ {{ orderInfo['actualPrice'] }}</text>
</view>
<view class="info-item">
<text class="label">支付金额:</text>
<text class="value price-red">¥ {{ orderInfo['paidAmount'] }}</text>
</view>
</view>
</view>
<view class="section-block">
<view class="section-title">
<view class="blue-bar"></view>
<text>买家留言</text>
</view>
<view class="info-grid">
<view class="info-item full">
<text class="value">{{ orderInfo['mark'] || '-' }}</text>
</view>
</view>
</view>
<view class="section-block">
<view class="section-title">
<view class="blue-bar"></view>
<text>订单备注</text>
</view>
<view class="info-grid">
<view class="info-item full">
<text class="value">{{ orderInfo['remark'] || '-' }}</text>
</view>
</view>
</view>
</view>
<!-- 商品信息 -->
<view v-if="activeTab === 1" class="info-section">
<view class="product-table">
<view class="product-thead">
<text class="p-th p-info">商品信息</text>
<text class="p-th p-sku">规格</text>
<text class="p-th p-price">单价</text>
<text class="p-th p-num">数量</text>
<text class="p-th p-total">小计</text>
</view>
<view class="product-tbody">
<view v-for="(p, pi) in productItems" :key="pi" class="p-tr">
<view class="p-td p-info">
<image :src="p['image'] || '/static/logo.png'" mode="aspectFill" class="p-img" />
<text class="p-name">{{ p['name'] }}</text>
</view>
<view class="p-td p-sku">
<text class="p-sku-txt">{{ p['sku_info'] || '-' }}</text>
</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>
</view>
</view>
</view>
<!-- 订单记录 -->
<view v-if="activeTab === 2" class="info-section">
<view 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>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
const props = defineProps({
visible: { type: Boolean, default: false },
orderInfo: { type: Object, default: () : UTSJSONObject => ({}) as UTSJSONObject }
})
const emit = defineEmits(['update:visible'])
const activeTab = ref(0)
const tabs = ['订单信息', '商品信息', '订单记录']
const productItems = computed<UTSJSONObject[]>(() => {
return (props.orderInfo['items'] || []) as UTSJSONObject[]
})
const logs = ref([
{ title: '订单生成', time: '2026-02-27 15:47:25' },
{ title: '支付成功', time: '2026-02-27 15:48:30' }
])
const close = () => {
emit('update:visible', false)
}
watch(() => props.visible, (newVal) => {
if (newVal) {
activeTab.value = 0
}
})
</script>
<style scoped lang="scss">
.drawer-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 2000;
display: flex;
justify-content: flex-end;
}
.drawer-container {
width: 800px;
max-width: 90%;
height: 100vh;
background-color: #fff;
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.drawer-header {
height: 56px;
padding: 0 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.title { font-size: 16px; font-weight: 600; color: #333; }
.close-btn {
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.close-icon { font-size: 24px; color: #999; line-height: 1; }
.drawer-body {
flex: 1;
background-color: #f5f7f9;
}
.order-summary-card {
background-color: #fff;
padding: 24px;
margin-bottom: 12px;
display: flex;
flex-direction: row;
gap: 16px;
}
.type-icon { width: 48px; height: 48px; }
.summary-info {
flex: 1;
}
.top-row {
margin-bottom: 16px;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.order-type-text { font-size: 16px; font-weight: 600; color: #333; }
.order-sn-text { font-size: 14px; color: #666; }
.shop-tag {
font-size: 12px; color: #1890ff; background: #e6f7ff;
border: 1px solid #91d5ff; padding: 2px 8px; border-radius: 2px;
}
.bottom-grids {
display: flex;
flex-direction: row;
gap: 40px;
}
.summary-grid {
display: flex;
flex-direction: column;
gap: 4px;
.label { font-size: 12px; color: #999; }
.value { font-size: 14px; color: #333; }
.status-val { font-weight: 600; }
.price-val { color: #f5222d; font-weight: 600; }
}
.drawer-tabs {
background-color: #fff;
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
padding: 0 24px;
}
.tab-item {
padding: 12px 20px;
margin-right: 12px;
position: relative;
cursor: pointer;
.tab-text { font-size: 14px; color: #595959; }
&.active {
.tab-text { color: #1890ff; font-weight: 500; }
&::after {
content: '';
position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: #1890ff;
}
}
}
.tab-content {
padding: 16px;
}
.section-block {
background-color: #fff;
padding: 24px;
margin-bottom: 16px;
border-radius: 4px;
}
.section-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 20px;
text { font-size: 14px; font-weight: 600; color: #333; }
}
.blue-bar { width: 3px; height: 14px; background-color: #1890ff; }
.info-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: y;
}
.info-item {
width: 33.33%;
margin-bottom: 12px;
display: flex;
flex-direction: row;
.label { font-size: 13px; color: #666; width: 80px; flex-shrink: 0; }
.value { font-size: 13px; color: #333; line-height: 1.4; word-break: break-all; }
.price-red { color: #f5222d; font-weight: 600; }
&.full { width: 100%; }
}
/* 商品表格 */
.product-table { padding: 8px; background-color: #fff; }
.product-thead { display: flex; flex-direction: row; background-color: #fafafa; border-bottom: 1px solid #f0f0f0; }
.p-th { padding: 12px 8px; font-size: 13px; font-weight: 500; color: #333; text-align: left; }
.p-tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; }
.p-td { padding: 12px 8px; font-size: 13px; color: #595959; display: flex; align-items: center; }
.p-info { flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px; }
.p-img { width: 40px; height: 40px; border-radius: 4px; }
.p-sku { width: 120px; }
.p-price { width: 100px; }
.p-num { width: 80px; }
.p-total { width: 100px; }
/* 记录 */
.timeline { padding: 24px; background-color: #fff; }
.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; }
.log-content { display: flex; flex-direction: column; gap: 4px; }
.log-title { font-size: 14px; color: #333; }
.log-time { font-size: 12px; color: #999; }
</style>

View File

@@ -1,25 +1,828 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
<view class="admin-page">
<view class="admin-sections" @click="closeDropdowns">
<!-- 筛选区域 -->
<view class="admin-card filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">订单类型:</text>
<view class="mock-select">
<text>全部订单</text>
<view class="arrow-down"></view>
</view>
</view>
<view class="filter-item">
<text class="label">支付方式:</text>
<view class="mock-select">
<text>全部</text>
<view class="arrow-down"></view>
</view>
</view>
<view class="filter-item long">
<text class="label">创建时间:</text>
<view class="mock-date-range">
<image class="cal-icon" src="/static/icons/calendar.png" mode="aspectFit" />
<text class="placeholder">开始日期 - 结束日期</text>
</view>
</view>
<view class="filter-item search">
<text class="label">订单搜索:</text>
<view class="search-group">
<view class="search-select">
<text>全部</text>
<view class="arrow-down"></view>
</view>
<input class="search-input" v-model="searchKeyword" placeholder="请输入订单号/手机号" />
</view>
</view>
</view>
<view class="btn-row">
<button class="btn btn-primary" @click="fetchData">查询</button>
<button class="btn btn-default" @click="resetQuery">重置</button>
</view>
</view>
<!-- 列表数据区域 -->
<view class="admin-card content-card">
<!-- 状态 Tabs -->
<view class="status-tabs">
<view
v-for="(tab, index) in statusTabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="handleTabClick(index)"
>
<text class="tab-text">{{ tab.name }}</text>
<text v-if="tab.count !== null" class="tab-count">({{ tab.count }})</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<button class="action-btn btn-blue">订单核销</button>
<button class="action-btn btn-outline">批量发货</button>
<button class="action-btn btn-outline">批量删除</button>
<button class="action-btn btn-outline">订单导出</button>
</view>
<!-- 数据表格 -->
<view class="order-table">
<view class="thead">
<view class="th col-expand"></view>
<view class="th col-check">
<checkbox :checked="false" color="#1890ff" />
</view>
<view class="th col-order">订单号 | 类型</view>
<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-time">支付时间</view>
<view class="th col-status">订单状态</view>
<view class="th col-op">操作</view>
</view>
<view class="tbody">
<!-- Loading 状态 -->
<view v-if="loading" class="loading-state">
<text>加载中...</text>
</view>
<!-- 无数据状态 -->
<view v-else-if="filteredOrders.length === 0" class="empty-state">
<text>暂无订单数据</text>
</view>
<view v-for="(item, index) in filteredOrders" :key="index" class="tr">
<view class="td col-expand">
<text class="expand-arrow"></text>
</view>
<view class="td col-check">
<checkbox :checked="false" color="#1890ff" />
</view>
<!-- 订单号|类型 -->
<view class="td col-order">
<text class="order-sn">{{ item['sn'] }}</text>
<text class="order-type" :class="item['typeColor']">[{{ item['typeName'] }}]</text>
<text v-if="item['cancelStatus'] != ''" class="cancel-text">{{ item['cancelStatus'] }}</text>
</view>
<!-- 商品信息 -->
<view class="td col-product">
<view class="product-info-list" v-if="item['items'] != null">
<view v-for="(prod, pidx) in (item['items'] as UTSJSONObject[])" :key="pidx" class="product-info-wrap">
<image class="p-img" :src="prod['image'] != null ? (prod['image'] as string) : '/static/logo.png'" mode="aspectFill" />
<text class="p-name">{{ prod['name'] }}</text>
</view>
</view>
<view v-else class="product-info-wrap">
<image class="p-img" :src="item['product']['img']" mode="aspectFill" />
<text class="p-name">{{ item['product']['name'] }}</text>
</view>
</view>
<!-- 用户信息 -->
<view class="td col-user">
<text class="u-info">{{ item['user']['name'] }} | {{ item['user']['id'] || '--' }}</text>
</view>
<!-- 实际支付 -->
<view class="td col-price">
<text class="price-val">{{ item['isPaid'] === true ? '¥' + item['actualPrice'] : '未支付' }}</text>
</view>
<!-- 支付方式 -->
<view class="td col-pay">
<text class="pay-text">{{ item['payMethod'] }}</text>
</view>
<!-- 支付时间 -->
<view class="td col-time">
<text class="time-text">{{ item['payTime'] }}</text>
</view>
<!-- 订单状态 -->
<view class="td col-status">
<text class="status-text">{{ 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>
<view class="op-dropdown-container">
<view class="op-link-more" @click.stop="toggleDropdown(item['sn'] as string)">
<text :class="{ 'more-text-active': activeDropdownId === item['sn'] }" 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>
<view class="dropdown-item danger" @click.stop="deleteOrder(item)">
<text class="item-text text-red">删除订单</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination-footer">
<view class="page-left">
<text class="count-text">共 {{ orderData.length }} 条</text>
<view class="page-size-select">
<text>10条/页</text>
<view class="arrow-down"></view>
</view>
</view>
<view class="page-right">
<view class="page-btn disabled"><text></text></view>
<view class="page-num active"><text>1</text></view>
<view class="page-num"><text>2</text></view>
<view class="page-num"><text>3</text></view>
<view class="page-btns-more"><text>...</text></view>
<view class="page-num"><text>10</text></view>
<view class="page-btn"><text></text></view>
</view>
</view>
</view>
</view>
</AdminLayout>
<!-- 抽屉详情 -->
<OrderDetailDrawer
v-model:visible="showDetail"
:order-info="selectedOrder"
/>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('order-list')
const title = ref<string>('index')
import { ref, computed, onMounted } from 'vue'
import { supabase } from '@/components/supadb/aksupainstance.uts'
import OrderDetailDrawer from './components/OrderDetailDrawer.uvue'
const activeTab = ref(0)
const loading = ref(false)
const searchKeyword = ref('')
// Dropdown 状态
const activeDropdownId = ref('')
const showDetail = ref(false)
const selectedOrder = ref<UTSJSONObject>({} as UTSJSONObject)
// 时间显示格式化
const formatTime = (timeStr : string | null) : string => {
if (timeStr == null || timeStr == '' || timeStr == '--') return '--'
if (timeStr.indexOf('T') > -1) {
const parts = timeStr.split('T')
const date = parts[0]
const timeFull = parts[1]
const time = timeFull.split('.')[0]
return date + ' ' + time
}
return timeStr
}
const statusTabs = [
{ name: '全部', count: null, status: 0 },
{ name: '待支付', count: null, status: 1 },
{ name: '待发货', count: null, status: 2 },
{ name: '待收货', count: null, status: 3 },
{ name: '已完成', count: null, status: 4 },
{ name: '已取消', count: null, status: 5 },
{ name: '退款中', count: null, status: 6 },
{ name: '已退款', count: null, status: 7 }
]
const orderData = ref<UTSJSONObject[]>([])
const filteredOrders = computed<UTSJSONObject[]>(() => {
let list = orderData.value
// Tab 过滤
if (activeTab.value > 0) {
const targetStatus = statusTabs[activeTab.value].status
list = list.filter((o : UTSJSONObject) : boolean => (o['orderStatus'] as number) === targetStatus)
}
// 搜索过滤
if (searchKeyword.value.trim() !== '') {
const kw = searchKeyword.value.toLowerCase()
list = list.filter((o : UTSJSONObject) : boolean => {
const sn = (o['sn'] as string).toLowerCase()
const user = o['user'] as UTSJSONObject
const phone = user['phone'] as string
return sn.includes(kw) || phone.includes(kw)
})
}
return list
})
const fetchData = async () => {
loading.value = true
try {
// 从 ml_orders_detail_view 视图读取数据
const res = await supabase
.from('ml_orders_detail_view')
.select('*')
.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 => {
return {
sn: item['order_no'],
typeName: '普通订单',
typeColor: 'green',
orderStatus: item['order_status'],
cancelStatus: item['order_status'] === 5 ? '用户已取消' : '',
product: {
img: '/static/logo.png', // 默认占位图
name: '订单概要 (详情查看)'
} as UTSJSONObject,
items: item['order_items'] as UTSJSONObject[], // 尝试获取子项
user: {
name: (item['customer_name'] || '未知用户') as string,
id: (item['user_id'] || '--') as string,
phone: (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'] || '--',
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, // 运费
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'] : '-'
} as UTSJSONObject
})
// 更新统计数据
updateTabCounts()
} else {
console.error('Fetch orders error:', res.error)
}
} catch (e) {
console.error('Fetch orders exception:', e)
} finally {
loading.value = false
}
}
const updateTabCounts = () => {
statusTabs.forEach((tab, index) => {
if (index === 0) {
tab.count = orderData.value.length
} else {
tab.count = orderData.value.filter((o : UTSJSONObject) : boolean => (o['orderStatus'] as number) === tab.status).length
}
})
}
const handleTabClick = (index: number) => {
activeTab.value = index
}
const resetQuery = () => {
searchKeyword.value = ''
activeTab.value = 0
fetchData()
}
// 更多菜单控制
const toggleDropdown = (id: string) => {
if (activeDropdownId.value === id) {
activeDropdownId.value = ''
} else {
activeDropdownId.value = id
}
}
// 关闭下拉
const closeDropdowns = () => {
activeDropdownId.value = ''
}
// 查看详情
const viewDetail = (order: UTSJSONObject) => {
selectedOrder.value = order
showDetail.value = true
closeDropdowns()
}
// 删除订单 (权限占位代码)
const deleteOrder = async (order: UTSJSONObject) => {
closeDropdowns()
// 模拟权限验证: admin-order-storeOrder-index
const hasAuth = true // 实际项目中应通过 store 读取权限
if (!hasAuth) {
uni.showToast({ title: '无权限操作', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除订单 ${order['sn']} 吗?删除后不可恢复。`,
success: async (res) => {
if (res.confirm) {
try {
const { error } = await supabase
.from('ml_orders')
.delete()
.eq('order_no', order['sn'])
.execute()
if (!error) {
uni.showToast({ title: '删除成功' })
fetchData()
} else {
uni.showToast({ title: '删除失败: ' + error.message, icon: 'none' })
}
} catch (e) {
console.error(e)
}
}
}
})
}
const handleAction = (action: string, sn: string) => {
uni.showToast({ title: `执行操作: ${action} - ${sn}`, icon: 'none' })
closeDropdowns()
}
onMounted(() => {
fetchData()
// 点击空白处关闭下拉
// uni.onWindowClick(() => closeDropdowns())
// UVUE 不支持 window click可用 view 遮罩或在主容器加点击
})
</script>
<style scoped lang="scss">
@import '@/uni.scss';
.page { padding: $space-lg; }
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
.admin-page {
/* 基础背景由 Layout 提供 */
min-height: calc(100vh - 120px);
padding: 16px;
}
.admin-card {
background-color: #ffffff;
border-radius: 4px;
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
overflow: visible !important; /* 关键:确保内部内容不被裁切 */
}
.content-card {
overflow: visible !important;
}
.filter-card {
padding: 24px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 24px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
.label {
font-size: 14px;
color: #333;
width: 80px;
flex-shrink: 0;
}
}
.mock-select {
width: 160px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
text { font-size: 14px; color: #595959; }
}
.mock-date-range {
width: 240px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
.cal-icon { width: 14px; height: 14px; margin-right: 8px; opacity: 0.4; }
.placeholder { font-size: 14px; color: #bfbfbf; }
}
.search-group {
display: flex;
flex-direction: row;
border: 1px solid #d9d9d9;
border-radius: 4px;
height: 32px;
overflow: hidden;
}
.search-select {
width: 80px;
border-right: 1px solid #d9d9d9;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 4px;
background-color: #fafafa;
text { font-size: 14px; color: #595959; }
}
.search-input {
flex: 1;
border: none;
padding: 0 12px;
font-size: 14px;
width: 180px;
}
.arrow-down {
width: 0; height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #bfbfbf;
}
.btn-row {
margin-top: 16px;
display: flex;
flex-direction: row;
gap: 8px;
}
.btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.btn-primary { background-color: #1890ff; color: #fff; border: none; }
.btn-default { background-color: #fff; color: #595959; border: 1px solid #d9d9d9; }
.status-tabs {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
padding: 0 16px;
}
.tab-item {
padding: 16px 20px;
cursor: pointer;
position: relative;
display: flex;
flex-direction: row;
gap: 2px;
.tab-text { font-size: 14px; color: #595959; }
.tab-count { font-size: 14px; color: #595959; }
&.active {
.tab-text, .tab-count { color: #1890ff; font-weight: 500; }
&::after {
content: '';
position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: #1890ff;
}
}
}
.action-bar {
padding: 16px 20px;
display: flex;
flex-direction: row;
gap: 12px;
}
.action-btn {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 4px;
margin: 0;
display: flex;
align-items: center;
}
.btn-blue { background-color: #1890ff; color: #fff; border: none; }
.btn-outline { background-color: #fff; color: #595959; border: 1px solid #d9d9d9; }
/* 表格样式 */
.order-table {
width: 100%;
overflow-x: auto;
overflow-y: visible !important; /* 关键:确保垂直方向不被裁切 */
position: relative;
}
.thead {
display: flex;
flex-direction: row;
background-color: #f0f7ff;
min-width: 1300px;
position: relative;
z-index: 10;
}
.th {
padding: 12px 8px;
font-size: 14px;
color: #595959;
font-weight: 500;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start; /* 左对齐内容 */
}
.tbody {
overflow: visible !important;
}
.tr {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
min-width: 1300px;
position: relative; /* 定位参考 */
z-index: 10;
overflow: visible !important; /* 关键:确保子菜单可溢出 */
&:hover { background-color: #fafafa; z-index: 100; }
}
.td {
padding: 16px 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start; /* 左对齐内容 */
}
.no-wrap {
flex-direction: row !important;
flex-wrap: nowrap;
}
.overflow-visible {
overflow: visible !important;
}
/* 更多下拉菜单 */
.op-dropdown-container {
position: relative;
display: inline-block;
cursor: pointer;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
width: 100px;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
margin-top: 5px;
z-index: 99999; /* 极大层级 */
border: 1px solid #f0f0f0;
pointer-events: auto;
display: block; /* 显式设置为 block */
}
.dropdown-item {
height: 36px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
transition: background-color 0.2s;
cursor: pointer;
.item-text { font-size: 13px; color: #595959; }
&:hover {
background-color: #f5f5f5;
}
}
.text-red { color: #ff4d4f !important; }
.loading-state, .empty-state {
padding: 40px;
text-align: center;
color: #999;
font-size: 14px;
}
/* 列宽控制 */
.col-expand { width: 40px; justify-content: center; align-items: center; }
.expand-arrow { color: #bfbfbf; font-size: 18px; }
.col-check { width: 50px; justify-content: center; align-items: center; }
.col-order { width: 220px; }
.col-product { flex: 1; min-width: 300px; }
.col-user { width: 180px; }
.col-price { width: 100px; }
.col-pay { width: 100px; }
.col-time { width: 160px; }
.col-status { width: 100px; }
.col-op { width: 140px; }
.order-sn { font-size: 13px; color: #262626; margin-bottom: 4px; text-align: left; }
.order-type { font-size: 12px; text-align: left; }
.product-info-list {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.product-info-wrap {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px; /* 间距拉开一点 */
text-align: left;
}
.p-img { width: 44px; height: 44px; border-radius: 4px; background-color: #f5f5f5; flex-shrink: 0; }
.p-name { font-size: 13px; color: #595959; line-height: 1.5; }
.u-info { font-size: 13px; color: #595959; }
.price-val { font-size: 14px; color: #262626; }
.pay-text, .time-text, .status-text { font-size: 13px; color: #595959; }
.op-links {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.op-link { font-size: 13px; cursor: pointer; }
.primary { color: #1890ff; }
.op-link-more {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
.more-text { font-size: 13px; color: #1890ff; transition: color 0.1s; }
.more-text-active { font-weight: bold; }
.arrow-down-blue {
width: 0; height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 4px solid #1890ff;
margin-top: 2px;
}
.arrow-up-blue {
width: 0; height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-bottom: 4px solid #1890ff;
}
/* 分页 */
.pagination-footer {
padding: 16px 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.page-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
.count-text { font-size: 14px; color: #595959; }
}
.page-size-select {
height: 28px;
padding: 0 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
text { font-size: 12px; color: #595959; }
}
.page-right {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.page-num, .page-btn, .page-btns-more {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d9d9d9;
border-radius: 4px;
text { font-size: 14px; color: #595959; }
cursor: pointer;
}
.page-num.active {
border-color: #1890ff;
text { color: #1890ff; }
}
.page-btn.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-btns-more { border: none; }
</style>