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

@@ -15,12 +15,12 @@
<view class="filter-item">
<text class="label-txt">退款时间:</text>
<view class="date-picker-mock">
<text class="date-txt">开始日期</text>
<text class="date-split">-</text>
<text class="date-txt">结束日期</text>
<text class="calendar-ic">📅</text>
</view>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
<view class="filter-item">
@@ -103,6 +103,7 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchRefundOrderPage } from '@/services/orderService.uts'
interface RefundOrder {
@@ -127,6 +128,9 @@ const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const startDate = ref('')
const endDate = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
const pages = Math.ceil(total.value / pageSize.value)
@@ -149,9 +153,24 @@ const onStatusChange = (e : any) => {
handleQuery()
}
function onApplyRange(payload : any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
handleQuery()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
handleQuery()
}
const loadRefundOrders = async () => {
loading.value = true
try {
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
const res = await fetchRefundOrderPage(
page.value,
pageSize.value,
@@ -159,7 +178,7 @@ const loadRefundOrders = async () => {
searchQuery.value
)
orderList.value = res.items.map((item: any): RefundOrder => {
orderList.value = res.items.map((item : any) : RefundOrder => {
const refundStatusCode = parseInt(String(item.refund_status ?? '0'))
const orderStatusCode = parseInt(String(item.order_status ?? '0'))

View File

@@ -5,12 +5,12 @@
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">创建时间:</text>
<view class="date-picker-mock">
<text class="date-txt">开始日期</text>
<text class="date-split">-</text>
<text class="date-txt">结束日期</text>
<text class="calendar-ic">📅</text>
</view>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
<view class="filter-item">
@@ -114,6 +114,7 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchCashierOrderPage } from '@/services/orderService.uts'
interface CashierOrder {
@@ -133,19 +134,28 @@ const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const startDate = ref('')
const endDate = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
return Math.ceil(total.value / pageSize.value)
const pages = Math.ceil(total.value / pageSize.value)
return pages <= 0 ? 1 : pages
})
const loadCashierOrders = async () => {
loading.value = true
try {
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
const res = await fetchCashierOrderPage(
page.value,
pageSize.value,
orderId.value ? orderId.value : null,
username.value ? username.value : null
username.value ? username.value : null,
st,
et
)
orderList.value = res.items.map((item: any): CashierOrder => {
@@ -174,6 +184,18 @@ const handleQuery = () => {
loadCashierOrders()
}
function onApplyRange(payload : any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
handleQuery()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
handleQuery()
}
const prevPage = () => {
if (page.value > 1) {
page.value--

View File

@@ -1,536 +1,42 @@
<template>
<view class="order-list-page">
<!-- 筛选区域 -->
<view class="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="请输入" v-model="searchText" @confirm="handleQuery" />
</view>
</view>
</view>
<view class="btn-row">
<button class="btn btn-primary" @click="handleQuery">查询</button>
<button class="btn btn-default" @click="handleReset">重置</button>
</view>
</view>
<!-- 列表数据区域 -->
<view class="content-card">
<!-- 状态 Tabs -->
<view class="status-tabs">
<view
v-for="(tab, index) in statusTabs"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="handleTabChange(index)"
>
<text class="tab-text">{{ tab.name }}</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-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
<text>加载中...</text>
</view>
<view v-else-if="orderData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
<text>暂无订单数据</text>
</view>
<view v-else 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.order_no }}</text>
<text class="order-type blue">[{{ getChannelName(item.channel_type) }}]</text>
</view>
<!-- 商品信息 -->
<view class="td col-product">
<view class="product-info-wrap">
<image class="p-img" :src="item.first_item_summary?.image_url" mode="aspectFill" />
<text class="p-name">{{ item.first_item_summary?.product_name || '多商品订单' }}</text>
</view>
</view>
<!-- 用户信息 -->
<view class="td col-user">
<text class="u-info">{{ item.buyer_name }} | {{ item.buyer_phone }}</text>
</view>
<!-- 实际支付 -->
<view class="td col-price">
<text class="price-val">¥{{ item.total_amount }}</text>
</view>
<!-- 支付方式 -->
<view class="td col-pay">
<text class="pay-text">{{ getPayTypeName(item.pay_type) }}</text>
</view>
<!-- 支付时间 -->
<view class="td col-time">
<text class="time-text">{{ item.paid_at || '-' }}</text>
</view>
<!-- 订单状态 -->
<view class="td col-status">
<text class="status-text">{{ getStatusName(item.order_status) }}</text>
</view>
<!-- 操作 -->
<view class="td col-op">
<view class="op-links">
<text class="op-link primary">详情</text>
<view class="op-link-more">
<text class="more-text">更多</text>
<view class="arrow-down-blue"></view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 分页区域 -->
<view class="pagination-row" style="padding: 16px 20px;">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-btns">
<view class="page-btn" :class="{ disabled: page <= 1 }" @click="prevPage"><text></text></view>
<view class="page-btn active"><text>{{ page }}</text></view>
<view class="page-btn" :class="{ disabled: page >= totalPages }" @click="nextPage"><text></text></view>
</view>
<view class="page-jump">
<text>前往</text>
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
<text>页</text>
</view>
<view class="page-container">
<view class="page-content">
<view class="placeholder-card">
<text class="placeholder-title">正在跳转...</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { fetchOrderPage } from '@/services/orderService.uts'
const activeTab = ref(0)
const searchText = ref('')
const total = ref(0)
const orderData = ref<any[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const statusTabs = [
{ name: '全部', value: null as number | null },
{ name: '待付款', value: 1 as number | null },
{ name: '待发货', value: 2 as number | null },
{ name: '待收货', value: 3 as number | null },
{ name: '已完成', value: 4 as number | null },
{ name: '已取消', value: 5 as number | null },
{ name: '退款中', value: 6 as number | null },
{ name: '已退款', value: 7 as number | null }
]
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
return Math.ceil(total.value / pageSize.value)
})
const loadOrders = async () => {
loading.value = true
try {
const res = await fetchOrderPage(
page.value,
pageSize.value,
statusTabs[activeTab.value].value,
searchText.value || null
)
orderData.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
loading.value = false
}
}
import { onMounted } from 'vue'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
onMounted(() => {
loadOrders()
// 跳转到最新的订单管理页面
openRoute('OrderList')
})
const handleQuery = () => {
page.value = 1
loadOrders()
}
const handleReset = () => {
searchText.value = ''
page.value = 1
loadOrders()
}
const handleTabChange = (index: number) => {
activeTab.value = index
page.value = 1
loadOrders()
}
const prevPage = () => {
if (page.value > 1) {
page.value--
loadOrders()
}
}
const nextPage = () => {
if (page.value < totalPages.value) {
page.value++
loadOrders()
}
}
const goToJumpPage = () => {
const targetPage = parseInt(jumpPage.value)
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
page.value = targetPage
loadOrders()
jumpPage.value = ''
} else {
uni.showToast({ title: '页码无效', icon: 'none' })
}
}
function getStatusName(status: number): string {
const tab = statusTabs.find(t => t.value === status)
return tab?.name ?? '未知'
}
function getPayTypeName(type: number | null): string {
switch (type) {
case 1: return '余额支付'
case 2: return '微信支付'
case 3: return '支付宝'
case 4: return '线下支付'
default: return '其他'
}
}
function getChannelName(type: number | null): string {
switch (type) {
case 1: return '公众号'
case 2: return '小程序'
case 3: return 'H5'
case 4: return 'PC'
case 5: return 'APP'
default: return '普通订单'
}
}
</script>
<style scoped lang="scss">
.order-list-page {
padding: 16px;
background-color: #f0f2f5;
.page-container {
padding: 20px;
min-height: 100vh;
background: #f5f5f5;
}
.filter-card {
background-color: #fff;
.page-content {
background: #fff;
border-radius: 4px;
padding: 24px;
border-radius: 4px;
margin-bottom: 16px;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 24px;
.placeholder-card {
text-align: center;
padding: 60px 20px;
}
.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 {
background-color: #fff;
border-radius: 4px;
}
.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%;
}
.thead {
display: flex;
flex-direction: row;
background-color: #f0f7ff;
}
.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;
&: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;
.placeholder-title {
display: block;
font-size: 18px;
font-weight: 600;
color: #666;
}
</style>

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>

View File

@@ -5,10 +5,12 @@
<view class="filter-card">
<view class="filter-item">
<text class="filter-label">时间选择:</text>
<view class="date-picker-mock">
<image class="calendar-icon" src="/static/icons/calendar.png" mode="aspectFit" />
<text class="date-range">2026/01/04 - 2026/02/02</text>
</view>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
</view>
@@ -123,7 +125,9 @@
import { ref, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchOrderStats, fetchOrderTrend, fetchOrderSourceStats, fetchOrderTypeStats } from '@/services/orderService.uts'
import AkReq from '@/uni_modules/ak-req/ak-req.uts'
const currentPage = ref<string>('order_statistic')
@@ -132,32 +136,57 @@ const trendOption = ref<any>({})
const sourceOption = ref<any>({})
const orderStats = ref<any>(null)
const startDate = ref<string>('')
const endDate = ref<string>('')
const orderTypeData = ref([
{ name: '普通订单', amount: '0.00', rate: '0.00' }
])
onMounted(() => {
if (AkReq.getToken() == null || AkReq.getToken() === '') {
return
}
// 默认最近 30 天
const end = new Date()
const start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
startDate.value = start.toISOString().substring(0, 10)
endDate.value = end.toISOString().substring(0, 10)
loadAllData()
})
function onApplyRange(payload: any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
loadAllData()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
loadAllData()
}
async function loadAllData() {
const endTime = new Date().toISOString()
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
const st = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
const et = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString()
try {
// 1. 加载汇总数据
orderStats.value = await fetchOrderStats(startTime, endTime)
orderStats.value = await fetchOrderStats(st, et)
// 2. 加载趋势数据
const trendData = await fetchOrderTrend(startTime, endTime)
const trendData = await fetchOrderTrend(st, et)
initTrendChart(trendData)
// 3. 加载来源数据
const sourceData = await fetchOrderSourceStats(startTime, endTime)
const sourceData = await fetchOrderSourceStats(st, et)
initSourceChart(sourceData)
// 4. 加载订单类型数据
const typeData = await fetchOrderTypeStats(startTime, endTime)
const typeData = await fetchOrderTypeStats(st, et)
orderTypeData.value = typeData
} catch (e) {
uni.showToast({ title: '加载统计数据失败', icon: 'none' })

View File

@@ -5,12 +5,12 @@
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">核销日期:</text>
<view class="date-picker-mock">
<text class="date-txt">开始日期</text>
<text class="date-split">-</text>
<text class="date-txt">结束日期</text>
<text class="calendar-ic">📅</text>
</view>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onApplyRange"
@clear="onClearRange"
/>
</view>
<view class="filter-item">
@@ -104,6 +104,7 @@
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { fetchWriteOffRecordPage } from '@/services/orderService.uts'
interface WriteOffRecord {
@@ -127,6 +128,9 @@ const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const startDate = ref('')
const endDate = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
return Math.ceil(total.value / pageSize.value)
@@ -135,10 +139,15 @@ const totalPages = computed((): number => {
const loadRecords = async () => {
loading.value = true
try {
const st = startDate.value ? (startDate.value + ' 00:00:00') : null
const et = endDate.value ? (endDate.value + ' 23:59:59') : null
const res = await fetchWriteOffRecordPage(
page.value,
pageSize.value,
searchQuery.value || null
searchQuery.value || null,
st,
et
)
recordList.value = res.items.map((item: any): WriteOffRecord => {
@@ -172,6 +181,18 @@ const handleQuery = () => {
loadRecords()
}
function onApplyRange(payload : any) {
startDate.value = payload?.start ?? ''
endDate.value = payload?.end ?? ''
handleQuery()
}
function onClearRange() {
startDate.value = ''
endDate.value = ''
handleQuery()
}
const prevPage = () => {
if (page.value > 1) {
page.value--