1028 lines
30 KiB
Plaintext
1028 lines
30 KiB
Plaintext
<template>
|
||
<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="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>
|
||
</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 class="order-time">{{ item['created_at'] }}</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" :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="viewDetail(item)">详情</text>
|
||
<template v-if="(item['orderStatus'] as number) === 2">
|
||
<view class="divider-v"></view>
|
||
<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="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>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分页 -->
|
||
<view class="pagination-footer">
|
||
<view class="page-left">
|
||
<text class="count-text">共 {{ filteredOrders.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>
|
||
|
||
<!-- 抽屉详情 -->
|
||
<OrderDetailDrawer
|
||
v-model:visible="showDetail"
|
||
:order-info="selectedOrder"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
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 fetchError = ref('')
|
||
|
||
// 时间显示格式化
|
||
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 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
|
||
|
||
// 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'] != null ? (o['sn'] as string).toLowerCase() : ''
|
||
const user = o['user'] as UTSJSONObject
|
||
const phone = (user != null && user['phone'] != null) ? (user['phone'] as string) : ''
|
||
return sn.includes(kw) || phone.includes(kw)
|
||
})
|
||
}
|
||
|
||
return list
|
||
})
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
fetchError.value = ''
|
||
|
||
// 获取当前商家 ID(merchant_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 {
|
||
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 {
|
||
id: item['id'] != null ? (item['id'] as string) : '',
|
||
sn: item['order_no'] != null ? item['order_no'] : '--',
|
||
typeName: '普通订单',
|
||
typeColor: 'green',
|
||
orderStatus: orderStatusNum,
|
||
cancelStatus: cancelStatus,
|
||
product: {
|
||
img: '/static/logo.png',
|
||
name: '订单详情查看'
|
||
} as UTSJSONObject,
|
||
items: null, // ml_orders_detail_view 视图未聚合 order_items,详情抽屉另行处理
|
||
user: {
|
||
name: (item['customer_name'] || '未知用户') as string,
|
||
id: (item['user_id'] || '--') as string,
|
||
phone: receiverPhone != '' ? receiverPhone : (item['customer_phone'] != null ? (item['customer_phone'] as string) : '--')
|
||
} as UTSJSONObject,
|
||
isPaid: item['payment_status'] === 2,
|
||
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'] != 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: 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
|
||
}
|
||
}
|
||
|
||
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 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()
|
||
// 模拟权限验证: 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)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData()
|
||
// 点击空白处关闭下拉
|
||
// uni.onWindowClick(() => closeDropdowns())
|
||
// UVUE 不支持 window click,可用 view 遮罩或在主容器加点击
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.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: 160px; }
|
||
|
||
.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;
|
||
}
|
||
|
||
.op-link { font-size: 13px; cursor: pointer; }
|
||
.primary { color: #1890ff; padding-right: 8px; }
|
||
|
||
.divider-v {
|
||
width: 1px;
|
||
height: 12px;
|
||
background-color: #dcdfe6;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.op-dropdown-container {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding-left: 0px;
|
||
}
|
||
|
||
.op-link-more {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.more-text { font-size: 13px; color: #1890ff; }
|
||
|
||
.dropdown-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 110px;
|
||
background-color: #ffffff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
margin-top: 8px;
|
||
z-index: 100000;
|
||
border: 1px solid #ebeef5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.dropdown-item {
|
||
height: 32px;
|
||
padding: 0 16px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background-color 0.2s;
|
||
cursor: pointer;
|
||
|
||
.item-text { font-size: 13px; color: #606266; text-align: center; }
|
||
|
||
&:hover {
|
||
background-color: #f5f7fa;
|
||
.item-text { color: #1890ff; }
|
||
}
|
||
}
|
||
|
||
.item-danger:hover {
|
||
.item-text { color: #ff4d4f !important; }
|
||
}
|
||
|
||
.text-red { color: #ff4d4f !important; }
|
||
|
||
/* 箭头 */
|
||
.arrow-down-blue {
|
||
width: 0; height: 0;
|
||
border-left: 3px solid transparent;
|
||
border-right: 3px solid transparent;
|
||
border-top: 4px solid #1890ff;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.arrow-up-blue {
|
||
width: 0; height: 0;
|
||
border-left: 3px solid transparent;
|
||
border-right: 3px solid transparent;
|
||
border-bottom: 4px solid #1890ff;
|
||
margin-bottom: 3px;
|
||
}
|
||
.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; }
|
||
|
||
.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>
|
||
|