From 29f588a2b26c3140a85596446d6e20012706b8f8 Mon Sep 17 00:00:00 2001 From: huangzhenbao <17818024429@163.com> Date: Fri, 20 Mar 2026 15:24:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analytics/AnalyticsDateRangePicker.uvue | 1 + layouts/admin/AdminLayout.uvue | 5 +- pages/mall/admin/decoration/category.uvue | 270 --------- .../admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md | 93 +-- pages/mall/admin/finance/balance_record.uvue | 326 ---------- pages/mall/admin/finance/commission.uvue | 286 --------- .../finance/transaction-statistics/index.uvue | 22 +- pages/mall/admin/order/list.uvue | 42 -- .../admin/order/order-statistics/index.uvue | 520 +++++++--------- pages/mall/admin/product/list.uvue | 76 --- .../product/product-statistics/index.uvue | 18 +- pages/mall/admin/service/autoReply.uvue | 561 ------------------ pages/mall/admin/错误信息.txt | 104 +--- 13 files changed, 321 insertions(+), 2003 deletions(-) delete mode 100644 pages/mall/admin/decoration/category.uvue delete mode 100644 pages/mall/admin/finance/balance_record.uvue delete mode 100644 pages/mall/admin/finance/commission.uvue delete mode 100644 pages/mall/admin/order/list.uvue delete mode 100644 pages/mall/admin/product/list.uvue delete mode 100644 pages/mall/admin/service/autoReply.uvue diff --git a/components/analytics/AnalyticsDateRangePicker.uvue b/components/analytics/AnalyticsDateRangePicker.uvue index 57e5e91c..3ddbcafb 100644 --- a/components/analytics/AnalyticsDateRangePicker.uvue +++ b/components/analytics/AnalyticsDateRangePicker.uvue @@ -106,6 +106,7 @@ .actions { display: flex; + flex-direction: row; gap: 8px; } diff --git a/layouts/admin/AdminLayout.uvue b/layouts/admin/AdminLayout.uvue index fa7f4908..970d5628 100644 --- a/layouts/admin/AdminLayout.uvue +++ b/layouts/admin/AdminLayout.uvue @@ -68,7 +68,10 @@ - + + diff --git a/pages/mall/admin/decoration/category.uvue b/pages/mall/admin/decoration/category.uvue deleted file mode 100644 index d62af6d6..00000000 --- a/pages/mall/admin/decoration/category.uvue +++ /dev/null @@ -1,270 +0,0 @@ - - - - - diff --git a/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md b/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md index f070fd82..cfbe967d 100644 --- a/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md +++ b/pages/mall/admin/docs/UNI_APP_X_PAGE_FIX_GUIDE.md @@ -281,31 +281,35 @@ ❌ select() 调用没有 limit/offset 参数 ``` - **正确实现(服务端分页模板)**: + ```typescript // ✅ state - const total = ref(0) // 来自服务端,不是 .length - const pagedList = computed(() => userList.value) // 无前端 slice,服务端已分页 + const total = ref(0); // 来自服务端,不是 .length + const pagedList = computed(() => userList.value); // 无前端 slice,服务端已分页 // ✅ fetch:每次翻页都重新请求 const fetchUsers = async (page: number, ps: number) => { - const offset = (page - 1) * ps - const offsetFilter = offset > 0 ? `offset=${offset}` : null - const res = await supabase.select('table', offsetFilter, { - limit: ps, order: 'created_at.desc', count: 'exact' - }) - userList.value = (res.data as UTSJSONObject[]).map(mapDbRow) + const offset = (page - 1) * ps; + const offsetFilter = offset > 0 ? `offset=${offset}` : null; + const res = await supabase.select("table", offsetFilter, { + limit: ps, + order: "created_at.desc", + count: "exact", + }); + userList.value = (res.data as UTSJSONObject[]).map(mapDbRow); // 从 Content-Range 头解析总数(见原因二十八) - total.value = parseTotalFromContentRange(res) - } + total.value = parseTotalFromContentRange(res); + }; // ✅ 翻页 handler:每次翻页都调用 fetch const handlePageChange = (p: number) => { - currentPage.value = p - fetchUsers(p, pageSize.value) - } + currentPage.value = p; + fetchUsers(p, pageSize.value); + }; - onMounted(() => fetchUsers(1, pageSize.value)) + onMounted(() => fetchUsers(1, pageSize.value)); ``` + - **强制规则**: 任何使用 Supabase 的列表页面,`total` 必须来自后端 `Content-Range` 响应头或 `res.count`,严禁使用 `computed(() => localArray.value.length)` 作为总数。 #### **原因二十七(补充):已在另一位置记录(响应式预览网格)** @@ -321,51 +325,56 @@ 3. UI 层的"禁用"保护(`if (p > totalPage) return`)在 `total` 来自错误来源时本身就不可靠(见原因二十六),因此无法阻止越界请求。 4. 即使 UI 保护正确,在数据量恰好是页大小整数倍时(如共 15 条、每页 15 条),请求第 2 页仍会触发 416(`rangeFrom=15 >= total=15`)。 - **错误修复思路(踩坑路径)**: + ``` ❌ 第一轮尝试:在 handlePageChange / onPageBtnClick 中加 if (p > totalPage) return 结果:仍然 416。因为 UI 保护只能拦截按钮点击,无法修复协议层行为。 - + ❌ 第二轮尝试:在 fetchUsers 中加 if (res.status === 416) { ... } 分支处理 结果:错误被吞但数据仍为空,用户体验差,根本原因未消除。 ``` + - **正确修复方案**: + ```typescript // ❌ 禁止使用 Range 头分页 const res = await supabase - .from('ak_users') + .from("ak_users") .select(columns) - .page(page) // ← 产生 Range 头 - .limit(pageSize) // ← 产生 Range 头 - .execute() + .page(page) // ← 产生 Range 头 + .limit(pageSize) // ← 产生 Range 头 + .execute(); // ✅ 使用 URL 参数分页(永远不返回 416) - const offset = (page - 1) * ps - const offsetFilter = offset > 0 ? `offset=${offset}` : null - const res = await supabase.select('ak_users', offsetFilter, { - columns: 'id, username, ...', + const offset = (page - 1) * ps; + const offsetFilter = offset > 0 ? `offset=${offset}` : null; + const res = await supabase.select("ak_users", offsetFilter, { + columns: "id, username, ...", limit: ps, - order: 'created_at.desc', - count: 'exact' // → Prefer: count=exact → 响应包含 Content-Range: 0-14/26 - }) + order: "created_at.desc", + count: "exact", // → Prefer: count=exact → 响应包含 Content-Range: 0-14/26 + }); // PostgREST 对 ?offset=N&limit=N 参数:offset 超出 total 时返回 200 + 空数组,绝不 416 ``` + - **总数解析(Content-Range 手动解析)**: ```typescript // 当 aksupa 使用 count: 'exact' 时,响应头会包含 Content-Range: 0-14/26 // 必须手动解析,因为绕过了 .page().limit() 的自动汇总逻辑 const parseTotal = (res: AkReqResponse): number => { - const cr = (res.headers?.['content-range'] ?? res.headers?.['Content-Range']) as string | null + const cr = (res.headers?.["content-range"] ?? + res.headers?.["Content-Range"]) as string | null; if (cr) { - const slash = cr.lastIndexOf('/') + const slash = cr.lastIndexOf("/"); if (slash !== -1) { - const n = parseInt(cr.substring(slash + 1), 10) - if (!isNaN(n)) return n + const n = parseInt(cr.substring(slash + 1), 10); + if (!isNaN(n)) return n; } } // 降级:至少知道 offset + 当前页行数 - return offset + (res.data as UTSJSONObject[]).length - } - total.value = parseTotal(res) + return offset + (res.data as UTSJSONObject[]).length; + }; + total.value = parseTotal(res); ``` - **强制规则**: 1. **项目内所有列表页禁止使用 `.page().limit()` 链式调用**(aksupa 或其他类似封装)。 @@ -378,28 +387,30 @@ - **现象**: 在第 1 页时点击"上一页"按钮,或在最后一页时点击"下一页"按钮,虽然按钮视觉上呈灰色/禁用态,但仍会向父组件 `emit('page-change', 0)` 或 `emit('page-change', totalPage+1)`,导致 `fetchUsers(0, ps)` 被调用、产生 `offset=-ps`(负数),请求异常。 - **根本原因**: `disabled` class 仅控制 CSS 样式(`opacity`、`cursor: not-allowed`),没有在事件处理函数中加入边界检查,点击事件照常触发和冒泡。 - **修复方案**: + ```typescript // CommonPagination.uvue — onPageBtnClick // ❌ 修复前:直接 emit,无边界保护 const onPageBtnClick = (p: number) => { - if (p !== -1) emit('page-change', p) - } + if (p !== -1) emit("page-change", p); + }; // ✅ 修复后:加入 p >= 1 && p <= props.totalPage 双向边界检查 const onPageBtnClick = (p: number) => { if (p !== -1 && p >= 1 && p <= props.totalPage) { - emit('page-change', p) + emit("page-change", p); } - } + }; ``` + - **父页面的双重保护(纵深防御)**: ```typescript // 在 handlePageChange 中同样加入边界检查,防止其他调用路径绕过组件保护 const handlePageChange = (p: number) => { - if (p < 1 || p > totalPage.value) return - currentPage.value = p - fetchUsers(p, pageSize.value) - } + if (p < 1 || p > totalPage.value) return; + currentPage.value = p; + fetchUsers(p, pageSize.value); + }; ``` - **推广规则**: 1. 任何分页组件,`disabled` 状态必须同时在 CSS **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。 diff --git a/pages/mall/admin/finance/balance_record.uvue b/pages/mall/admin/finance/balance_record.uvue deleted file mode 100644 index dad6af02..00000000 --- a/pages/mall/admin/finance/balance_record.uvue +++ /dev/null @@ -1,326 +0,0 @@ - - - - - diff --git a/pages/mall/admin/finance/commission.uvue b/pages/mall/admin/finance/commission.uvue deleted file mode 100644 index 41e58f45..00000000 --- a/pages/mall/admin/finance/commission.uvue +++ /dev/null @@ -1,286 +0,0 @@ - - - - - diff --git a/pages/mall/admin/finance/transaction-statistics/index.uvue b/pages/mall/admin/finance/transaction-statistics/index.uvue index c72970a4..95cbd79b 100644 --- a/pages/mall/admin/finance/transaction-statistics/index.uvue +++ b/pages/mall/admin/finance/transaction-statistics/index.uvue @@ -201,8 +201,10 @@ async function loadData() { const endTime = new Date().toISOString() const startTime = getStartTime() + // 各区块独立 try-catch,单个接口失败不影响其他数据展示 + + // 1. 获取财务概况 try { - // 1. 获取财务概况 const finRes = await fetchFinanceOverview(startTime, endTime) if (finRes != null) { stats.rechargeAmount = finRes.recharge_amount.toFixed(2) @@ -212,8 +214,12 @@ async function loadData() { stats.balancePay = finRes.total_user_balance.toFixed(2) stats.commissionPay = finRes.total_user_brokerage.toFixed(2) } + } catch (e) { + console.error('[finance-stats] 财务概况加载失败', e) + } - // 2. 获取订单统计 + // 2. 获取订单统计(与订单统计页共用同一 RPC,此处独立请求) + try { const orderRes = await fetchOrderStats(startTime, endTime) if (orderRes != null) { stats.revenue = orderRes.total_amount.toFixed(2) @@ -221,15 +227,19 @@ async function loadData() { stats.orderCount = String(orderRes.order_count) stats.refundAmount = orderRes.refund_amount.toFixed(2) } + } catch (e) { + console.error('[finance-stats] 订单统计加载失败', e) + } - // 3. 获取趋势数据驱动图表 + // 3. 获取趋势数据驱动图表 + try { const trendRes = await fetchFinanceBillSummary(startTime, endTime, 'day') updateChart(trendRes) } catch (e) { - uni.showToast({ title: '加载统计失败', icon: 'none' }) - } finally { - loading.value = false + console.error('[finance-stats] 趋势图加载失败', e) } + + loading.value = false } function handleDateTabChange(index : number) { diff --git a/pages/mall/admin/order/list.uvue b/pages/mall/admin/order/list.uvue deleted file mode 100644 index 686ded08..00000000 --- a/pages/mall/admin/order/list.uvue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/pages/mall/admin/order/order-statistics/index.uvue b/pages/mall/admin/order/order-statistics/index.uvue index 601a651d..37d60a36 100644 --- a/pages/mall/admin/order/order-statistics/index.uvue +++ b/pages/mall/admin/order/order-statistics/index.uvue @@ -1,124 +1,125 @@ diff --git a/pages/mall/admin/product/product-statistics/index.uvue b/pages/mall/admin/product/product-statistics/index.uvue index d4220541..1cc2634d 100644 --- a/pages/mall/admin/product/product-statistics/index.uvue +++ b/pages/mall/admin/product/product-statistics/index.uvue @@ -173,8 +173,10 @@ async function loadAllData() { const startTime = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() const endTime = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString() + // 各图表独立 try-catch,单个接口失败(如 404)不影响其他区块展示 + + // 1. 加载核心指标 try { - // 1. 加载核心指标 const stats = await fetchAdminProductStats(startTime, endTime) if (stats != null) { statItems.value.forEach(item => { @@ -182,16 +184,24 @@ async function loadAllData() { item.value = typeof val === 'number' ? (item.key.includes('amount') ? val.toFixed(2) : String(val)) : String(val ?? '0') }) } + } catch (e) { + console.error('[product-stats] 核心指标加载失败', e) + } - // 2. 加载趋势图 + // 2. 加载趋势图(rpc_admin_product_trend 404 时返回空数组,图表为空但不崩溃) + try { const trendData = await fetchAdminProductTrend(startTime, endTime) initChart(trendData) + } catch (e) { + console.error('[product-stats] 趋势图加载失败', e) + } - // 3. 加载排行 + // 3. 加载排行(rpc_admin_product_ranking 404 时返回空数组) + try { const rankingData = await fetchAdminProductRanking(startTime, endTime, 'sales', 10) rankingList.value = rankingData } catch (e) { - uni.showToast({ title: '加载统计失败', icon: 'none' }) + console.error('[product-stats] 排行加载失败', e) } finally { loading.value = false } diff --git a/pages/mall/admin/service/autoReply.uvue b/pages/mall/admin/service/autoReply.uvue deleted file mode 100644 index 34a6559c..00000000 --- a/pages/mall/admin/service/autoReply.uvue +++ /dev/null @@ -1,561 +0,0 @@ - - - - - - - diff --git a/pages/mall/admin/错误信息.txt b/pages/mall/admin/错误信息.txt index 900d9c01..1c406600 100644 --- a/pages/mall/admin/错误信息.txt +++ b/pages/mall/admin/错误信息.txt @@ -1,99 +1,5 @@ -main.uts:30 [Vue warn]: Unhandled error during execution of async component loader - at -at -at -at -at -at -at -at -warnHandler @ uni-h5.es.js:19975 -callWithErrorHandling @ vue.runtime.esm.js:1381 -warn$1 @ vue.runtime.esm.js:1207 -logError @ vue.runtime.esm.js:1438 -errorHandler @ uni-h5.es.js:19600 -callWithErrorHandling @ vue.runtime.esm.js:1381 -handleError @ vue.runtime.esm.js:1421 -onError @ vue.runtime.esm.js:3724 -(anonymous) @ vue.runtime.esm.js:3767 -Promise.catch -setup @ vue.runtime.esm.js:3766 -callWithErrorHandling @ vue.runtime.esm.js:1381 -setupStatefulComponent @ vue.runtime.esm.js:8985 -setupComponent @ vue.runtime.esm.js:8946 -mountComponent @ vue.runtime.esm.js:7262 -processComponent @ vue.runtime.esm.js:7228 -patch @ vue.runtime.esm.js:6694 -mountChildren @ vue.runtime.esm.js:6942 -processFragment @ vue.runtime.esm.js:7158 -patch @ vue.runtime.esm.js:6668 -mountChildren @ vue.runtime.esm.js:6942 -processFragment @ vue.runtime.esm.js:7158 -patch @ vue.runtime.esm.js:6668 -mountChildren @ vue.runtime.esm.js:6942 -mountElement @ vue.runtime.esm.js:6849 -processElement @ vue.runtime.esm.js:6814 -patch @ vue.runtime.esm.js:6682 -mountChildren @ vue.runtime.esm.js:6942 -mountElement @ vue.runtime.esm.js:6849 -processElement @ vue.runtime.esm.js:6814 -patch @ vue.runtime.esm.js:6682 -mountChildren @ vue.runtime.esm.js:6942 -processFragment @ vue.runtime.esm.js:7158 -patch @ vue.runtime.esm.js:6668 -componentUpdateFn @ vue.runtime.esm.js:7372 -run @ vue.runtime.esm.js:153 -instance.update @ vue.runtime.esm.js:7497 -setupRenderEffect @ vue.runtime.esm.js:7507 -mountComponent @ vue.runtime.esm.js:7274 -processComponent @ vue.runtime.esm.js:7228 -patch @ vue.runtime.esm.js:6694 -mountChildren @ vue.runtime.esm.js:6942 -mountElement @ vue.runtime.esm.js:6849 -processElement @ vue.runtime.esm.js:6814 -patch @ vue.runtime.esm.js:6682 -componentUpdateFn @ vue.runtime.esm.js:7372 -run @ vue.runtime.esm.js:153 -instance.update @ vue.runtime.esm.js:7497 -setupRenderEffect @ vue.runtime.esm.js:7507 -mountComponent @ vue.runtime.esm.js:7274 -processComponent @ vue.runtime.esm.js:7228 -patch @ vue.runtime.esm.js:6694 -componentUpdateFn @ vue.runtime.esm.js:7372 -run @ vue.runtime.esm.js:153 -instance.update @ vue.runtime.esm.js:7497 -setupRenderEffect @ vue.runtime.esm.js:7507 -mountComponent @ vue.runtime.esm.js:7274 -processComponent @ vue.runtime.esm.js:7228 -patch @ vue.runtime.esm.js:6694 -componentUpdateFn @ vue.runtime.esm.js:7453 -run @ vue.runtime.esm.js:153 -instance.update @ vue.runtime.esm.js:7497 -updateComponent @ vue.runtime.esm.js:7305 -processComponent @ vue.runtime.esm.js:7239 -patch @ vue.runtime.esm.js:6694 -componentUpdateFn @ vue.runtime.esm.js:7453 -run @ vue.runtime.esm.js:153 -instance.update @ vue.runtime.esm.js:7497 -callWithErrorHandling @ vue.runtime.esm.js:1381 -flushJobs @ vue.runtime.esm.js:1585 -Promise.then -queueFlush @ vue.runtime.esm.js:1494 -queueJob @ vue.runtime.esm.js:1488 -scheduler @ vue.runtime.esm.js:3179 -resetScheduling @ vue.runtime.esm.js:236 -triggerEffects @ vue.runtime.esm.js:280 -triggerRefValue @ vue.runtime.esm.js:1033 -set value @ vue.runtime.esm.js:1078 -finalizeNavigation @ vue-router.mjs?v=ed041164:2474 -(anonymous) @ vue-router.mjs?v=ed041164:2384 -Promise.then -pushWithRedirect @ vue-router.mjs?v=ed041164:2352 -push @ vue-router.mjs?v=ed041164:2278 -install @ vue-router.mjs?v=ed041164:2631 -use @ vue.runtime.esm.js:5190 -initRouter @ uni-h5.es.js:19886 -install @ uni-h5.es.js:19955 -use @ vue.runtime.esm.js:5190 -(anonymous) @ main.uts:30 -main.uts:30 SyntaxError: The requested module '/services/admin/financeService.uts?import' does not provide an export named 'fetchAdminBalanceDistribution' (at index.uvue:150:3) \ No newline at end of file +ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_ranking 404 (Not Found) +ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_trend 404 (Not Found) +ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_trend 404 (Not Found) +[AkReq.request] headers: {"apikey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890","Content-Type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczOTc4NDY1LCJpYXQiOjE3NzM5NzQ4NjUsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3Mzk2NzY4Mn1dLCJzZXNzaW9uX2lkIjoiODhkMDhhNmEtNzBiOS00M2RhLWEwNmQtZWQ5ODIzZTM1MTQwIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.WitVmu_GTafuNQ5mxQbQPTmsEldjU0HA6qtCy0p6utM","Accept":"application/json"} +ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_order_type_stats 400 (Bad Request) \ No newline at end of file