admin模块接入数据库

This commit is contained in:
comlibmb
2026-02-13 17:29:50 +08:00
parent 56209b7a75
commit ec636dc703
58 changed files with 5586 additions and 1394 deletions

View File

@@ -1,25 +1,414 @@
<template>
<AdminLayout :currentPage="currentPage">
<view class="page">
<view class="header">
<text class="title">{{ title }}</text>
<text class="sub-title">页面已修复 (UTF-8)</text>
<view class="order-management-page">
<!-- 1. 顶部筛选卡片 -->
<view class="filter-card">
<view class="filter-row">
<view class="filter-item">
<text class="label">订单号:</text>
<input class="filter-input" v-model="searchOrderNo" placeholder="请输入订单号" @confirm="onSearch" />
</view>
<view class="filter-item">
<text class="label">用户信息:</text>
<input class="filter-input" v-model="searchUser" placeholder="姓名/手机号" @confirm="onSearch" />
</view>
<view class="filter-item">
<text class="label">下单时间:</text>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
<view class="filter-btns">
<button class="btn primary" @click="onSearch">查询</button>
<button class="btn reset" @click="onReset">重置</button>
</view>
</view>
</view>
<!-- 2. 状态选项卡 -->
<view class="status-tabs-card">
<view class="status-tabs">
<view
v-for="tab in statusTabs"
:key="tab.value"
class="tab-item"
:class="{ active: activeStatus === tab.value }"
@click="onStatusChange(tab.value)"
>
<text class="tab-text">{{ tab.label }}</text>
</view>
</view>
</view>
<!-- 3. 订单列表表格 -->
<view class="content-card">
<view class="table-container">
<!-- Loading 遮罩 -->
<view v-if="isLoading" class="loading-mask">
<text class="loading-text">订单数据加载中...</text>
</view>
<view class="table-header">
<view class="col col-no"><text>订单号</text></view>
<view class="col col-user"><text>用户信息</text></view>
<view class="col col-amount"><text>支付金额</text></view>
<view class="col col-status"><text>订单状态</text></view>
<view class="col col-time"><text>下单时间</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<view class="table-body">
<view v-if="orderList.length === 0 && !isLoading" class="empty-row">
<text>未找到相关订单</text>
</view>
<view v-for="item in orderList" :key="item.id" class="table-row">
<view class="col col-no">
<text class="order-no">{{ item.order_no }}</text>
</view>
<view class="col col-user">
<view class="user-info-box">
<text class="u-name">{{ item.username || '匿名用户' }}</text>
<text class="u-phone">{{ item.phone || '-' }}</text>
</view>
</view>
<view class="col col-amount">
<text class="amount-val">¥{{ item.paid_amount?.toFixed(2) || '0.00' }}</text>
</view>
<view class="col col-status">
<text class="status-tag" :class="getStatusClass(item.order_status)">
{{ getStatusLabel(item.order_status) }}
</text>
</view>
<view class="col col-time">
<text class="time-text">{{ formatDateTime(item.created_at) }}</text>
</view>
<view class="col col-ops">
<text class="op-link" @click="goDetail(item.id)">详情</text>
<text class="op-divider">|</text>
<text class="op-link">备注</text>
</view>
</view>
</view>
</view>
<!-- 4. 分页控制 -->
<view class="pagination">
<view class="pager-btns">
<button class="btn small" :disabled="page <= 1" @click="onPrevPage">上一页</button>
<text class="page-num">第 {{ page }} 页</text>
<button class="btn small" :disabled="orderList.length < pageSize" @click="onNextPage">下一页</button>
</view>
<text class="page-info">当前页 {{ orderList.length }} 条 / 总计 {{ total }} 条</text>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
const currentPage = ref<string>('order-list')
const title = ref<string>('index')
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchOrderPage } from '@/services/orderService.uts'
import AkReq from '@/uni_modules/ak-req/ak-req.uts'
const currentPage = ref<string>('OrderList')
// --- 数据状态 ---
const orderList = ref<any[]>([])
const total = ref(0)
const isLoading = ref(false)
// --- 筛选条件 ---
const searchOrderNo = ref('')
const searchUser = ref('')
const activeStatus = ref<number | null>(null)
const startDate = ref('')
const endDate = ref('')
const page = ref(1)
const pageSize = 20
const statusTabs = [
{ label: '全部', value: null },
{ label: '待付款', value: 1 },
{ label: '待发货', value: 2 },
{ label: '待收货', value: 3 },
{ label: '已完成', value: 4 },
{ label: '已取消', value: 5 },
{ label: '退款中', value: 6 },
{ label: '已退款', value: 7 }
]
onMounted(() => {
if (AkReq.getToken() == null || AkReq.getToken() === '') return
loadOrders()
})
async function loadOrders() {
isLoading.value = true
try {
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
// 合并搜索词
const combinedSearch = (searchOrderNo.value || searchUser.value)
? `${searchOrderNo.value} ${searchUser.value}`.trim()
: null
const res = await fetchOrderPage(
page.value,
pageSize,
activeStatus.value,
combinedSearch,
st,
et
)
orderList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
isLoading.value = false
}
}
// --- 事件处理 ---
function onSearch() {
page.value = 1
loadOrders()
}
function onReset() {
searchOrderNo.value = ''
searchUser.value = ''
activeStatus.value = null
startDate.value = ''
endDate.value = ''
page.value = 1
loadOrders()
}
function onStatusChange(status: number | null) {
activeStatus.value = status
page.value = 1
loadOrders()
}
function onApplyRange(payload: any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
page.value = 1
loadOrders()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
page.value = 1
loadOrders()
}
function onPrevPage() {
if (page.value <= 1) return
page.value--
loadOrders()
}
function onNextPage() {
if (orderList.value.length < pageSize) return
page.value++
loadOrders()
}
// --- 辅助方法 ---
function getStatusLabel(status: number): string {
const found = statusTabs.find(t => t.value === status)
return found ? found.label : '未知'
}
function getStatusClass(status: number): string {
switch (status) {
case 1: return 'status-pending'
case 2: return 'status-shipping'
case 3: return 'status-delivery'
case 4: return 'status-completed'
case 5: return 'status-cancelled'
case 6: case 7: return 'status-refund'
default: return ''
}
}
function formatDateTime(iso: string | null): string {
if (!iso) return '-'
return iso.replace('T', ' ').split('.')[0]
}
function goDetail(id: string) {
uni.showToast({ title: '详情页开发中', icon: 'none' })
}
</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; }
.order-management-page {
padding: 20px;
background-color: #f5f7f9;
min-height: 100vh;
}
.filter-card {
background: #fff;
padding: 24px;
border-radius: 4px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
.label { font-size: 14px; color: #333; margin-right: 8px; }
}
.filter-input {
border: 1px solid #d9d9d9;
border-radius: 4px;
height: 32px;
width: 180px;
padding: 0 12px;
font-size: 14px;
}
.filter-btns {
display: flex;
flex-direction: row;
gap: 12px;
}
.status-tabs-card {
background: #fff;
margin-bottom: 16px;
border-radius: 4px;
}
.status-tabs {
display: flex;
flex-direction: row;
padding: 0 24px;
}
.tab-item {
padding: 16px 20px;
cursor: pointer;
position: relative;
.tab-text { font-size: 14px; color: #666; }
&.active {
.tab-text { color: #1890ff; font-weight: bold; }
&::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px; background: #1890ff;
}
}
}
.content-card {
background: #fff;
border-radius: 4px;
position: relative;
}
.table-container {
padding: 24px;
min-height: 400px;
position: relative;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8faff;
border-bottom: 1px solid #f0f0f0;
padding: 12px 0;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
padding: 16px 0;
align-items: center;
&:hover { background: #fafafa; }
}
.col { padding: 0 12px; font-size: 14px; color: #333; display: flex; align-items: center; }
.col-no { width: 200px; }
.col-user { flex: 1; }
.col-amount { width: 120px; justify-content: center; }
.col-status { width: 120px; justify-content: center; }
.col-time { width: 180px; justify-content: center; }
.col-ops { width: 150px; justify-content: flex-end; }
.order-no { font-family: monospace; color: #1890ff; }
.user-info-box { display: flex; flex-direction: column; gap: 4px; }
.u-name { font-weight: 500; }
.u-phone { font-size: 12px; color: #999; }
.amount-val { color: #f5222d; font-weight: bold; }
.status-tag {
padding: 2px 10px; border-radius: 4px; font-size: 12px;
&.status-pending { background: #fff7e6; color: #faad14; border: 1px solid #ffe58f; }
&.status-shipping { background: #e6f7ff; color: #1890ff; border: 1px solid #bae7ff; }
&.status-completed { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
&.status-cancelled { background: #f5f5f5; color: #999; border: 1px solid #d9d9d9; }
&.status-refund { background: #fff1f0; color: #f5222d; border: 1px solid #ffa39e; }
}
.op-link { color: #1890ff; cursor: pointer; }
.op-divider { margin: 0 8px; color: #eee; }
.pagination {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.pager-btns { display: flex; flex-direction: row; align-items: center; gap: 12px; }
.page-num { font-size: 14px; color: #333; }
.page-info { font-size: 14px; color: #999; }
.loading-mask {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.7);
display: flex; align-items: center; justify-content: center;
z-index: 10;
}
.empty-row { padding: 60px 0; text-align: center; color: #999; }
.btn {
height: 32px; padding: 0 16px; font-size: 14px; border-radius: 4px; cursor: pointer;
&.primary { background: #1890ff; color: #fff; border: none; }
&.reset { background: #fff; color: #666; border: 1px solid #d9d9d9; }
&.small { height: 28px; padding: 0 12px; font-size: 13px; }
&[disabled] { opacity: 0.5; cursor: not-allowed; }
}
</style>