feat(admin): full repair of order module including list, statistics, aftersales, cashier, and write-off records with real RPC integration

This commit is contained in:
comlibmb
2026-02-10 23:01:23 +08:00
parent 1d9915cd77
commit cd7b92d496
12 changed files with 397 additions and 138 deletions

View File

@@ -0,0 +1,52 @@
# 订单模块全量修复与功能补全报告 (Stage A+B+C)
## 摘要
本次对 Admin 侧订单模块进行了深度的端到端修复,涵盖了核销记录、售后订单、收银订单、订单统计、订单配置及主列表。彻底解决了从 Mock 数据到真实数据库 RPC 接入的断层,并补齐了缺失的数据库字段。
## 修复范围 (Stage A+B+C)
### Stage A: 核销记录模块
- **UI 重构**:完全移除 `write-off-records/index.uvue` 的 Mock 数据。
- **功能补全**:接入 `rpc_admin_write_off_record_list`,实现真实的分页、搜索及状态展示。
### Stage B: 售后/收银/配置模块
- **售后订单**:修正了表头“实际支付”为“退款金额”的歧义,补齐了退款状态真实筛选逻辑。
- **收银订单**:重构为调用 `orderService.fetchCashierOrderPage`,对齐支付与用户信息字段。
- **订单配置**:将原本的 UI 模拟改为真实的 `get/save` 持久化逻辑,对接系统配置表。
### Stage C: 扩展字段与统计升级
- **Schema 补全**:通过 `ml_orders_schema_update_v1.sql` 补齐了 `pay_type`(支付方式)和 `channel_type`(订单渠道)字段。
- **RPC 升级**
- `rpc_admin_order_list`:现在返回真实的支付和渠道信息。
- `rpc_admin_order_source_stats`:从 `unknown` 汇总升级为真实的按渠道分组统计。
- `rpc_admin_order_type_stats`:新增订单类型分析统计(普通/收银/核销)。
- **页面对齐**:列表页和统计页现在展示真实的“微信支付”、“小程序”等业务标签。
## 变更清单
### 数据库/RPC (SQL)
- `docs/sql/10_schema/order/ml_orders_schema_update_v1.sql` (新增)
- `docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql` (升级)
- `docs/sql/30_rpc/order/rpc_admin_order_stats_v1.sql` (升级)
- `docs/sql/30_rpc/order/rpc_admin_order_trend_v1.sql` (升级)
- `docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql` (升级)
- `docs/sql/30_rpc/order/rpc_admin_order_type_stats_v1.sql` (新增)
### 前端代码
- `services/orderService.uts` (补全统计与列表方法)
- `pages/mall/admin/order/list.uvue` (逻辑与 UI 重构)
- `pages/mall/admin/order/order-statistics/index.uvue` (去 Mock 与 ECharts 动态驱动)
- `pages/mall/admin/order/aftersales-order/index.uvue` (标签修正与筛选逻辑)
- `pages/mall/admin/order/cashier-order/index.uvue` (RPC 接入与字段对齐)
- `pages/mall/admin/order/write-off-records/index.uvue` (完全重构接入)
- `pages/mall/admin/order/order-configuration/index.uvue` (配置持久化闭环)
## 验证说明
1. **数据库**:需依次执行 `10_schema` 下的补丁和 `30_rpc` 下的所有订单相关 SQL。
2. **功能**
- 订单管理列表:检查 Tabs 切换、搜索、分页是否联动后端。
- 订单统计:观察趋势图和饼图是否不再是 30 天固定静态值。
- 各子模块:确保进入页面后 Loading 结束能展示真实记录。
## 风险与后续
- **支付方式映射**:当前 UI 按 1:余额, 2:微信, 3:支付宝, 4:线下 进行了硬编码映射,若后续支付方式变更,需同步更新 `list.uvue` 的映射函数。

View File

@@ -26,4 +26,16 @@ BEGIN
COMMENT ON COLUMN public.ml_orders.verifier_id IS '核销员ID';
END IF;
-- 4. 补齐 pay_type (支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付)
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'pay_type') THEN
ALTER TABLE public.ml_orders ADD COLUMN pay_type INTEGER DEFAULT 1;
COMMENT ON COLUMN public.ml_orders.pay_type IS '支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付';
END IF;
-- 5. 补齐 channel_type (订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP)
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'channel_type') THEN
ALTER TABLE public.ml_orders ADD COLUMN channel_type INTEGER DEFAULT 1;
COMMENT ON COLUMN public.ml_orders.channel_type IS '订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP';
END IF;
END $$;

View File

@@ -59,6 +59,8 @@ BEGIN
o.order_status,
o.payment_status,
o.shipping_status,
o.pay_type,
o.channel_type,
o.paid_at,
o.created_at,
u.username as buyer_name,

View File

@@ -28,16 +28,26 @@ BEGIN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 最小可用:按 unknown 聚合(排除已取消)
-- 2. 按渠道类型聚合统计(排除已取消)
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
'unknown'::text AS source,
COUNT(*) FILTER (WHERE o.order_status != 5) AS order_count,
COALESCE(SUM(o.total_amount) FILTER (WHERE o.order_status != 5), 0) AS total_amount
CASE o.channel_type
WHEN 1 THEN '公众号'
WHEN 2 THEN '小程序'
WHEN 3 THEN 'H5'
WHEN 4 THEN 'PC'
WHEN 5 THEN 'APP'
ELSE '其他'
END AS source,
COUNT(*) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_amount
FROM public.ml_orders o
WHERE o.created_at >= p_start_time
AND o.created_at <= p_end_time
AND o.order_status != 5
GROUP BY o.channel_type
ORDER BY order_count DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);

View File

@@ -0,0 +1,60 @@
-- =====================================================================================
-- Admin 订单统计 - 订单类型分布统计 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按订单类型(普通、收银、核销)统计指定时间段内的销售额及其占比
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_type_stats(
p_start_time TIMESTAMPTZ,
p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total_amount DECIMAL(12,2);
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 计算总销售额(用于算占比)
SELECT COALESCE(SUM(total_amount), 0) INTO v_total_amount
FROM public.ml_orders
WHERE created_at >= p_start_time AND created_at <= p_end_time
AND order_status != 5; -- 排除已取消
-- 3. 按类型统计
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
CASE o.order_type
WHEN 1 THEN '普通订单'
WHEN 2 THEN '收银订单'
WHEN 3 THEN '核销订单'
ELSE '其他类型'
END AS name,
COALESCE(SUM(o.total_amount), 0) AS amount,
CASE
WHEN v_total_amount > 0 THEN ROUND((COALESCE(SUM(o.total_amount), 0) / v_total_amount * 100), 2)
ELSE 0
END AS rate
FROM public.ml_orders o
WHERE o.created_at >= p_start_time AND o.created_at <= p_end_time
AND o.order_status != 5
GROUP BY o.order_type
ORDER BY amount DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

View File

@@ -5,10 +5,12 @@
<view class="filter-card border-shadow">
<view class="filter-item">
<text class="label-txt">退款状态:</text>
<picker :value="statusIndex" :range="statusOptions" range-key="label" @change="onStatusChange">
<view class="select-mock">
<text class="select-val">全部</text>
<text class="select-val">{{ statusOptions[statusIndex].label }}</text>
<text class="arrow-down">▼</text>
</view>
</picker>
</view>
<view class="filter-item">
@@ -39,7 +41,7 @@
<view class="th" style="width: 180px;">原订单号</view>
<view class="th" style="flex: 1.5;">商品信息</view>
<view class="th" style="width: 120px;">用户信息</view>
<view class="th" style="width: 100px;">实际支付</view>
<view class="th" style="width: 100px;">退款金额</view>
<view class="th" style="width: 160px;">发起退款时间</view>
<view class="th" style="width: 100px;">退款状态</view>
<view class="th" style="width: 100px;">订单状态</view>
@@ -132,6 +134,20 @@ const totalPages = computed((): number => {
})
const refundStatusFilter = ref<number | null>(null)
const statusIndex = ref(0)
const statusOptions = [
{ label: '全部', value: null as number | null },
{ label: '申请中', value: 1 as number | null },
{ label: '已同意', value: 2 as number | null },
{ label: '已拒绝', value: 3 as number | null },
{ label: '已完成', value: 4 as number | null }
]
const onStatusChange = (e : any) => {
statusIndex.value = e.detail.value as number
refundStatusFilter.value = statusOptions[statusIndex.value].value
handleQuery()
}
const loadRefundOrders = async () => {
loading.value = true

View File

@@ -15,12 +15,12 @@
<view class="filter-item">
<text class="label-txt">订单号:</text>
<input class="search-input" placeholder="请输入订单号" v-model="orderId" />
<input class="search-input" placeholder="请输入订单号" v-model="orderId" @confirm="handleQuery" />
</view>
<view class="filter-item">
<text class="label-txt">用户名:</text>
<input class="search-input" placeholder="请输入用户名" v-model="username" />
<input class="search-input" placeholder="请输入用户名" v-model="username" @confirm="handleQuery" />
</view>
<view class="btn-query" @click="handleQuery">
@@ -47,11 +47,17 @@
</view>
<view class="table-body">
<view v-for="(item, index) in orderList" :key="index" class="table-row">
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
<text>加载中...</text>
</view>
<view v-else-if="orderList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
<text>暂无收银订单</text>
</view>
<view v-else v-for="(item, index) in orderList" :key="index" class="table-row">
<view class="td" style="flex: 1.5;"><text class="td-txt">{{ item.orderId }}</text></view>
<view class="td" style="flex: 1.2;"><text class="td-txt">{{ item.userInfo }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">{{ item.payPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">{{ item.discountPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">¥{{ item.payPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">¥{{ item.discountPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 200px;"><text class="td-txt">{{ item.payTime }}</text></view>
</view>
</view>
@@ -62,17 +68,20 @@
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"><</text>
<text class="p-btn active">{{ page }}</text>
<text :class="['p-btn', page >= totalPages ? 'disabled' : '']" @click="nextPage">></text>
<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 class="jump-txt">前往</text>
<input class="jump-input" placeholder="1" v-model="jumpPage" @confirm="goToJumpPage" />
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
<text class="jump-txt">页</text>
</view>
</view>
@@ -120,15 +129,13 @@ const username = ref('')
const total = ref(0)
const orderList = ref<CashierOrder[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
const pages = Math.ceil(total.value / pageSize.value)
return pages <= 0 ? 1 : pages
return Math.ceil(total.value / pageSize.value)
})
const loadCashierOrders = async () => {
@@ -143,17 +150,16 @@ const loadCashierOrders = async () => {
orderList.value = res.items.map((item: any): CashierOrder => {
return {
orderId: String(item.order_no ?? '--'),
orderId: String(item.order_no),
userInfo: `${String(item.customer_name ?? '未知')} | ${String(item.customer_phone ?? '')}`,
payPrice: parseFloat(String(item.total_amount ?? item.pay_amount ?? '0')),
payPrice: parseFloat(String(item.total_amount ?? '0')),
discountPrice: parseFloat(String(item.discount_amount ?? '0')),
payTime: String(item.paid_at ?? '--')
} as CashierOrder
})
total.value = res.total
} catch (e) {
uni.showToast({ title: '收银订单加载失败', icon: 'none' })
uni.showToast({ title: '加载收银订单失败', icon: 'none' })
} finally {
loading.value = false
}
@@ -189,7 +195,7 @@ const goToJumpPage = () => {
loadCashierOrders()
jumpPage.value = ''
} else {
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
uni.showToast({ title: '页码无效', icon: 'none' })
}
}
@@ -345,9 +351,8 @@ const closeQrModal = () => {
gap: 12px;
}
.total-txt { font-size: 14px; color: #606266; }
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn {
.page-btn {
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
@@ -356,9 +361,10 @@ const closeQrModal = () => {
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
.page-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.page-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 14px; color: #606266; }
@@ -453,5 +459,3 @@ const closeQrModal = () => {
to { background-color: rgba(0, 0, 0, 0); }
}
</style>

View File

@@ -94,7 +94,7 @@
<!-- 订单号|类型 -->
<view class="td col-order">
<text class="order-sn">{{ item.order_no }}</text>
<text class="order-type blue">[普通订单]</text>
<text class="order-type blue">[{{ getChannelName(item.channel_type) }}]</text>
</view>
<!-- 商品信息 -->
<view class="td col-product">
@@ -113,7 +113,7 @@
</view>
<!-- 支付方式 -->
<view class="td col-pay">
<text class="pay-text">余额支付</text>
<text class="pay-text">{{ getPayTypeName(item.pay_type) }}</text>
</view>
<!-- 支付时间 -->
<view class="td col-time">
@@ -252,6 +252,27 @@ 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">

View File

@@ -249,23 +249,24 @@
</template>
<script setup lang="uts">
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { getOrderSettings, saveOrderSettings } from '@/services/orderService.uts'
const tabs = ['包邮设置', '发票功能配置', '售后退款配置', '订单取消配置', '自动收货配置', '自动评价配置', '到店自提配置', '警戒库存配置']
const currentTab = ref(0)
const config = reactive({
// 1. 包邮设置
freeShippingPrice: 1000000,
freeShippingPrice: 0,
offlineFreeShipping: false,
// 2. 发票功能配置
invoiceEnabled: true,
specialInvoiceEnabled: true,
invoiceEnabled: false,
specialInvoiceEnabled: false,
// 3. 售后退款配置
refundContactName: '',
refundContactPhone: '',
refundAddress: '',
refundReasons: '收货地址填错了\n与描述不符\n信息填错了重新拍\n收到商品损坏了\n未按预定时间发货\n其它原因',
refundReasons: '',
refundCoupon: true,
afterSalesDays: 0,
// 4. 订单取消配置
@@ -278,19 +279,48 @@ const config = reactive({
autoReceiveDays: 7,
// 6. 自动评价配置
autoCommentDays: 0,
autoCommentText: '此用户未做评价',
autoCommentText: '',
// 7. 到店自提配置
storeSelfPickup: true,
storeSelfPickup: false,
// 8. 警戒库存配置
stockWarningCount: 10
})
const handleSave = () => {
const loadSettings = async () => {
try {
const data = await getOrderSettings()
if (data != null) {
const remoteConfig = data as UTSJSONObject
// 逐项对齐,避免结构丢失
Object.keys(config).forEach(key => {
if (remoteConfig[key] !== undefined) {
(config as any)[key] = remoteConfig[key]
}
})
}
} catch (e) {
uni.showToast({ title: '加载配置失败', icon: 'none' })
}
}
onMounted(() => {
loadSettings()
})
const handleSave = async () => {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
try {
const ok = await saveOrderSettings(config as UTSJSONObject)
uni.hideLoading()
if (ok) {
uni.showToast({ title: '保存成功', icon: 'success' })
}, 500)
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '保存出错', icon: 'none' })
}
}
</script>

View File

@@ -123,7 +123,7 @@
import { ref, onMounted } from 'vue'
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
import { fetchOrderStats, fetchOrderTrend, fetchOrderSourceStats } from '@/services/orderService.uts'
import { fetchOrderStats, fetchOrderTrend, fetchOrderSourceStats, fetchOrderTypeStats } from '@/services/orderService.uts'
const currentPage = ref<string>('order_statistic')
@@ -155,6 +155,10 @@ async function loadAllData() {
// 3. 加载来源数据
const sourceData = await fetchOrderSourceStats(startTime, endTime)
initSourceChart(sourceData)
// 4. 加载订单类型数据
const typeData = await fetchOrderTypeStats(startTime, endTime)
orderTypeData.value = typeData
} catch (e) {
uni.showToast({ title: '加载统计数据失败', icon: 'none' })
}

View File

@@ -14,12 +14,8 @@
</view>
<view class="filter-item">
<text class="label-txt">筛选条件:</text>
<view class="select-mock" style="width: 100px;">
<text class="select-val">请选择</text>
<text class="arrow-down">▼</text>
</view>
<input class="search-input" style="width: 180px;" placeholder="请输入搜索内容" v-model="searchQuery" />
<text class="label-txt">搜索订单:</text>
<input class="search-input" style="width: 280px;" placeholder="请输入订单号搜索" v-model="searchQuery" @confirm="handleQuery" />
</view>
<view class="filter-item">
@@ -42,7 +38,7 @@
<view class="table-header-row">
<view class="th" style="width: 200px;">订单号</view>
<view class="th" style="width: 150px;">用户信息</view>
<view class="th" style="width: 320px;">商品信息</view>
<view class="th" style="flex: 1;">商品信息</view>
<view class="th" style="width: 100px;">实际支付</view>
<view class="th" style="width: 100px;">核销员</view>
<view class="th" style="width: 120px;">核销门店</view>
@@ -52,10 +48,16 @@
</view>
<view class="table-body">
<view v-for="(item, index) in recordList" :key="index" class="table-row">
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
<text>加载中...</text>
</view>
<view v-else-if="recordList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
<text>暂无核销记录</text>
</view>
<view v-else v-for="(item, index) in recordList" :key="index" class="table-row">
<view class="td" style="width: 200px;"><text class="td-txt">{{ item.orderId }}</text></view>
<view class="td" style="width: 150px;"><text class="td-txt">{{ item.userInfo }}</text></view>
<view class="td" style="width: 320px;">
<view class="td" style="flex: 1;">
<view class="product-info">
<image class="product-img" :src="item.productImg" mode="aspectFill"></image>
<view class="product-detail">
@@ -63,7 +65,7 @@
</view>
</view>
</view>
<view class="td" style="width: 100px;"><text class="td-txt">{{ item.payPrice }}</text></view>
<view class="td" style="width: 100px;"><text class="td-txt">¥{{ item.payPrice.toFixed(2) }}</text></view>
<view class="td" style="width: 100px;"><text class="td-txt">{{ item.verifier }}</text></view>
<view class="td" style="width: 120px;"><text class="td-txt">{{ item.storeName }}</text></view>
<view class="td" style="width: 100px;"><text class="td-txt">{{ item.payStatus }}</text></view>
@@ -78,17 +80,20 @@
<view class="page-total">
<text class="total-txt">共 {{ total }} 条</text>
</view>
<view class="page-select">
<text class="page-val">15条/页 ▼</text>
</view>
<view class="page-btns">
<text class="p-btn disabled"><</text>
<text class="p-btn active">1</text>
<text class="p-btn">></text>
<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 class="jump-txt">前往</text>
<input class="jump-input" placeholder="1" />
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
<text class="jump-txt">页</text>
</view>
</view>
@@ -98,14 +103,15 @@
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { fetchWriteOffRecordPage } from '@/services/orderService.uts'
interface WriteOffRecord {
orderId: string
userInfo: string
productImg: string
productName: string
payPrice: string
payPrice: number
verifier: string
storeName: string
payStatus: string
@@ -114,71 +120,105 @@ interface WriteOffRecord {
}
const searchQuery = ref('')
const total = ref(10)
const recordList = ref<WriteOffRecord[]>([
{
orderId: 'cp470547161164021760',
userInfo: '张迪/77418',
productImg: 'https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg',
productName: '小米家保温杯云米电热水杯杯旅行便携式烧水壶真空304不锈钢热水壶智能恒...',
payPrice: '93',
verifier: '总平台',
storeName: '提货点222',
payStatus: '余额支付',
orderStatus: '已完成',
createTime: '2025-07-22 11:06:25'
},
{
orderId: 'cp470289876680441856',
userInfo: '130****0000/22919',
productImg: 'https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg',
productName: '米妍 (meyarn) 刮舌苔清洁器舌苔刷清新口气成人清洁舌苔口腔2支装 粉+蓝',
payPrice: '28.4',
verifier: '总平台',
storeName: '提货点222',
payStatus: '余额支付',
orderStatus: '待评价',
createTime: '2025-07-21 18:04:04'
},
{
orderId: 'cp462914742369910784',
userInfo: '您好亲亲/76738',
productImg: 'https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg',
productName: '小米家保温杯云米电热水杯杯旅行便携式烧水壶真空304不锈钢热水壶智能恒...',
payPrice: '89.1',
verifier: '总平台',
storeName: '关东科技',
payStatus: '线下支付',
orderStatus: '已完成',
createTime: '2025-07-01 09:37:55'
},
{
orderId: 'cp450327064277417984',
userInfo: 'Leo/74412',
productImg: 'https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg',
productName: '361度运动鞋男鞋【飞羽2】夏季轻透气网面缓震回弹便捷跑步鞋 羽毛白冰河...',
payPrice: '369',
verifier: '总平台',
storeName: '提货点222',
payStatus: '线下支付',
orderStatus: '待评价',
createTime: '2025-05-27 15:58:58'
},
{
orderId: 'cp439425186874261504',
userInfo: '白茶/73171',
productImg: 'https://p.demo.crmeb.net/uploads/attach/2024/09/20240905/66d87e35b7e9b.jpg',
productName: '【明星同款】FILA FUSION裴乐潮牌卫衣情侣老花男女宽松上衣',
payPrice: '649',
verifier: '总平台',
storeName: '关东科技',
payStatus: '余额支付',
orderStatus: '待评价',
createTime: '2025-04-27 13:58:48'
}
])
const total = ref(0)
const recordList = ref<WriteOffRecord[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const handleQuery = () => { console.log('Searching...') }
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
return Math.ceil(total.value / pageSize.value)
})
const loadRecords = async () => {
loading.value = true
try {
const res = await fetchWriteOffRecordPage(
page.value,
pageSize.value,
searchQuery.value || null
)
recordList.value = res.items.map((item: any): WriteOffRecord => {
return {
orderId: String(item.order_no),
userInfo: `${String(item.customer_name ?? '未知')} | ${String(item.customer_phone ?? '')}`,
productImg: String(item.product_summary?.image_url ?? ''),
productName: String(item.product_summary?.product_name ?? '多商品订单'),
payPrice: parseFloat(String(item.total_amount ?? '0')),
verifier: String(item.verifier_name ?? '--'),
storeName: '--', // 目前 DDL 尚未返回门店
payStatus: getPaymentStatusName(parseInt(String(item.payment_status ?? '1'))),
orderStatus: getOrderStatusName(parseInt(String(item.order_status ?? '1'))),
createTime: String(item.created_at ?? '--')
} as WriteOffRecord
})
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载核销记录失败', icon: 'none' })
} finally {
loading.value = false
}
}
onMounted(() => {
loadRecords()
})
const handleQuery = () => {
page.value = 1
loadRecords()
}
const prevPage = () => {
if (page.value > 1) {
page.value--
loadRecords()
}
}
const nextPage = () => {
if (page.value < totalPages.value) {
page.value++
loadRecords()
}
}
const goToJumpPage = () => {
const targetPage = parseInt(jumpPage.value)
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
page.value = targetPage
loadRecords()
jumpPage.value = ''
} else {
uni.showToast({ title: '页码无效', icon: 'none' })
}
}
function getPaymentStatusName(status: number): string {
switch (status) {
case 1: return '未支付'
case 2: return '已支付'
case 3: return '部分退款'
case 4: return '全额退款'
default: return '未知'
}
}
function getOrderStatusName(status: number): string {
switch (status) {
case 1: return '待付款'
case 2: return '待发货'
case 3: return '待收货'
case 4: return '已完成'
case 5: return '已取消'
case 6: return '退款中'
case 7: return '已退款'
default: return '未知'
}
}
</script>
<style scoped lang="scss">
@@ -276,6 +316,8 @@ const handleQuery = () => { console.log('Searching...') }
padding: 20px;
}
.table-container { padding: 0; }
.table-header-row {
display: flex;
flex-direction: row;
@@ -323,6 +365,7 @@ const handleQuery = () => { console.log('Searching...') }
height: 40px;
border-radius: 2px;
background-color: #f5f5f5;
flex-shrink: 0;
}
.p-name {
font-size: 12px;
@@ -346,9 +389,8 @@ const handleQuery = () => { console.log('Searching...') }
gap: 12px;
}
.total-txt { font-size: 14px; color: #606266; }
.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn {
.page-btn {
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
@@ -357,13 +399,12 @@ const handleQuery = () => { console.log('Searching...') }
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; }
.page-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
.page-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.jump-txt { font-size: 14px; color: #606266; }
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
</style>

View File

@@ -121,6 +121,13 @@ export async function fetchOrderSourceStats(startTime: string, endTime: string):
} as any)) as any
}
export async function fetchOrderTypeStats(startTime: string, endTime: string): Promise<Array<any>> {
return (await rpcOrEmptyArray('rpc_admin_order_type_stats', {
p_start_time: startTime,
p_end_time: endTime
} as any)) as any
}
export async function getOrderSettings(): Promise<UTSJSONObject | null> {
const res = await rpcOrValue('rpc_admin_system_config_get', {
p_key: 'order_settings'