添加mock数据

This commit is contained in:
2026-04-13 11:32:31 +08:00
parent 334e5936c9
commit 37141c1d6b
17 changed files with 1843 additions and 330 deletions

View File

@@ -2,6 +2,8 @@
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.62
// Kong HTTP Port: 8000
//自己的配置自己解开即可
// export const SUPA_URL: string = 'http://192.168.1.61:18000'
// export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
@@ -10,8 +12,23 @@
// export const SUPA_URL: string = 'http://192.168.1.61:18000'
// export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU'
export const SUPA_URL: string = 'http://119.146.131.237:9126'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
// export const SUPA_URL: string = 'http://119.146.131.237:9126'
// export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
// ★ 医疗项目专属 Supabase 实例
// Studio 地址http://119.146.131.237:9127/project/default/editor/26894?schema=public
// Kong API 网关REST/Auth/Storagehttp://119.146.131.237:9126
// SUPA_URL 必须填 Kong API 网关地址,不是 Studio 地址
// ※ Connect 弹窗显示 localhost:8000 是 self-hosted 实例的默认元数据SUPABASE_PUBLIC_URL 未改),
// 不代表前端请求地址;前端始终用 http://119.146.131.237:9126
export const SUPA_URL: string = 'http://119.146.131.237:9129'
// anon (public) key来自 Studio → Project Settings → API → Project API keys → anon / public
// ※ 此 key 必须与 Kong 里 ANON_KEY 环境变量一致,否则 Kong 会返回 401 Invalid authentication credentials
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
// export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
@@ -22,6 +39,10 @@ export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
// export const WS_URL: string = 'ws://119.146.131.237:9126/realtime/v1/websocket'
//export const WS_URL: string = 'ws://localhost:18000/realtime/v1/websocket'
//hzb
// 医疗项目 9127 实例 WebSocket如需启用实时功能请取消注释并填写真实 key 后启用)
// export const WS_URL: string = 'ws://119.146.131.237:9127/realtime/v1/websocket'
// 推送服务地址(用于本地调试,可改为 http://<your-ip>:7301
export const PUSH_SERVER_URL: string = 'http://192.168.1.62:7301'
@@ -46,4 +67,17 @@ export const HOME_REDIRECT: string = '/pages/main/index'
export const TABORPAGE: string = '/pages/main/index'
// 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向)
export const IS_TEST_MODE: boolean = true
export const IS_TEST_MODE: boolean = true
/**
* 打印当前 Supabase 配置key 脱敏只打印前10位+后8位
* 在页面 onLoad 时调用,用于确认运行时实际使用的是哪套配置
*/
export function logSupaConfig() {
const keyLen = SUPA_KEY.length
const masked = keyLen > 20
? SUPA_KEY.substring(0, 10) + '...' + SUPA_KEY.substring(keyLen - 8)
: '(too short)'
console.log('[SupaConfig] SUPA_URL :', SUPA_URL)
console.log('[SupaConfig] SUPA_KEY :', masked)
}

View File

@@ -78,6 +78,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MERCHANT_ID, MOCK_BALANCE, MOCK_FINANCE_STATS, getMockFinanceRecords } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type RecordType = {
id: string
@@ -113,6 +114,10 @@
methods: {
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
@@ -120,6 +125,11 @@
},
async loadBalance() {
if (USE_MOCK) {
this.balance = MOCK_BALANCE
this.stats = MOCK_FINANCE_STATS
return
}
try {
const response = await supa.from('ml_shops').select('balance').eq('merchant_id', this.merchantId).single().execute()
@@ -138,7 +148,12 @@
async loadRecords() {
this.loading = true
if (USE_MOCK) {
this.records = getMockFinanceRecords(this.currentTab) as RecordType[]
this.loading = false
this.refreshing = false
return
}
try {
const response = await supa
.from('ml_wallet_transactions')

View File

@@ -96,6 +96,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockInventoryStats, MOCK_INVENTORY_PRODUCTS } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type ProductType = {
id: string
@@ -137,6 +138,10 @@
methods: {
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
if (session != null && session.user != null) {
@@ -151,7 +156,21 @@
async loadProducts() {
if (this.loading && this.page === 1) return
this.loading = true
if (USE_MOCK) {
const all = MOCK_INVENTORY_PRODUCTS
const filtered: typeof all = []
for (let i = 0; i < all.length; i++) {
const p = all[i]
if (this.currentFilter === 'low' && (p.total_stock <= 0 || p.total_stock > p.warning_stock)) continue
if (this.currentFilter === 'out' && p.total_stock !== 0) continue
filtered.push(p)
}
this.products = filtered as ProductType[]
this.hasMore = false
this.loading = false
this.refreshing = false
return
}
try {
let query = supa.from('ml_products')
.select('id, name, main_image_url, total_stock, warning_stock')
@@ -204,6 +223,10 @@
},
async loadStats() {
if (USE_MOCK) {
this.stats = getMockInventoryStats()
return
}
try {
const response = await supa.from('ml_products').select('id, total_stock, warning_stock', { count: 'exact' }).eq('merchant_id', this.merchantId).execute()

View File

@@ -97,6 +97,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MEMBER_LEVELS, MOCK_SERVICE_USERS } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type MemberLevel = {
id: string
@@ -138,6 +139,11 @@
}
},
onLoad() {
if (USE_MOCK) {
this.merchantId = 'mock-merchant'
this.loadLevels()
return
}
this.merchantId = uni.getStorageSync('user_id') || ''
this.loadLevels()
},
@@ -154,6 +160,10 @@
this.loadUsers();
},
async loadLevels() {
if (USE_MOCK) {
this.levels = MOCK_MEMBER_LEVELS as MemberLevel[]
return
}
const res = await supa.from('ml_member_levels').select('*').order('level_rank', { ascending: true }).execute()
if (res.data != null) {
const raw = res.data as any[]
@@ -172,6 +182,10 @@
},
async loadUsers() {
console.log('--- 启动 ak_users 全量加载 (不带 limit 限制) ---');
if (USE_MOCK) {
this.users = MOCK_SERVICE_USERS as UserInfo[]
return
}
try {
// 1. 移除 limit 限制或设置极大值,确保读到全部数据
// 同时通过 count 参数确认数据库到底给了多少条

View File

@@ -0,0 +1,940 @@
/**
* 机构端统一 mock 数据源
*
* 开关说明:
* USE_MOCK = true → 各页面使用此文件数据,不请求 Supabase
* USE_MOCK = false → 各页面走原有 Supabase 请求逻辑
*
* 联动锚点(改动此处会自动同步各页面):
* 待接单订单数 = PENDING_SHIPMENT_COUNT = 3
* 退款中订单数 = REFUND_COUNT = 2
* 低库存商品数 = LOW_STOCK_COUNT = 3
* 已售罄商品数 = OUT_OF_STOCK_COUNT = 2
* 未回复评价数 = PENDING_REVIEW_COUNT = 4
*/
// ─── Mock 总开关 ───────────────────────────────────────────────
export const USE_MOCK = true
// ─── 稳定 mock merchantId无登录时使用 ──────────────────────
export const MOCK_MERCHANT_ID = 'mock-merchant-0001-0000-000000000001'
// ─── 联动锚点常量 ───────────────────────────────────────────────
export const PENDING_SHIPMENT_COUNT = 3
export const REFUND_COUNT = 2
export const LOW_STOCK_COUNT = 3
export const OUT_OF_STOCK_COUNT = 2
export const PENDING_REVIEW_COUNT = 4
// ════════════════════════════════════════════════════════════
// 一、店铺信息
// ════════════════════════════════════════════════════════════
export type MockShopInfoType = {
id: string
merchant_id: string
shop_name: string
shop_logo: string
shop_banner: string
description: string
contact_name: string
contact_phone: string
rating_avg: number
total_sales: number
status: number
}
export const MOCK_SHOP_INFO: MockShopInfoType = {
id: 'mock-shop-0001',
merchant_id: MOCK_MERCHANT_ID,
shop_name: '骅锋医养综合服务中心',
shop_logo: '/static/images/default-shop.png',
shop_banner: '/static/images/default-banner.png',
description: '专注居家养老、慢病管理与康复护理一体化服务',
contact_name: '李医师',
contact_phone: '020-88888888',
rating_avg: 4.8,
total_sales: 386,
status: 1
}
export const MOCK_TODAY_STATS = {
orders: 7,
sales: 2340.00,
visitors: 42,
conversion: 17
}
export const MOCK_PENDING_COUNTS = {
pending_shipment: PENDING_SHIPMENT_COUNT,
refund_requests: REFUND_COUNT,
low_stock: LOW_STOCK_COUNT,
pending_reviews: PENDING_REVIEW_COUNT
}
// ════════════════════════════════════════════════════════════
// 二、订单 mock 数据
// ════════════════════════════════════════════════════════════
export type MockOrderItemType = {
id: string
order_id: string
product_id: string
sku_id: string
product_name: string
sku_name: string
price: number
quantity: number
image_url: string
sku_snapshot: string
}
export type MockOrderType = {
id: string
order_no: string
user_id: string
merchant_id: string
order_status: number
total_amount: number
product_amount: number
shipping_fee: number
paid_amount: number
shipping_address: string
remark: string
created_at: string
updated_at: string
items: MockOrderItemType[]
}
// 服务订单 mockstatus: 2=待接单/待上门, 3=服务中, 4=已完成)
// 其中 待接单=3条, 服务中=4条, 已完成=8条 → tab计数对齐
export const MOCK_ORDERS: MockOrderType[] = [
// ── 待接单 3 条 ──────────────────────────────────────────
{
id: 'order-001', order_no: 'SV2026041300001',
user_id: 'user-001', merchant_id: MOCK_MERCHANT_ID,
order_status: 2, total_amount: 380.00, product_amount: 380.00,
shipping_fee: 0, paid_amount: 380.00,
shipping_address: '广州市天河区天河路123号 李奶奶',
remark: '老人行动不便,请护士上午到访',
created_at: '2026-04-13T08:15:00+08:00', updated_at: '2026-04-13T08:15:00+08:00',
items: [{
id: 'item-001-1', order_id: 'order-001', product_id: 'prod-001',
sku_id: '', product_name: '居家上门护理(半日)', sku_name: '标准护理套餐',
price: 380.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-002', order_no: 'SV2026041300002',
user_id: 'user-002', merchant_id: MOCK_MERCHANT_ID,
order_status: 2, total_amount: 260.00, product_amount: 260.00,
shipping_fee: 0, paid_amount: 260.00,
shipping_address: '广州市越秀区东风东路456号 张爷爷',
remark: '高血压患者,需携带血压计',
created_at: '2026-04-13T09:30:00+08:00', updated_at: '2026-04-13T09:30:00+08:00',
items: [
{
id: 'item-002-1', order_id: 'order-002', product_id: 'prod-002',
sku_id: '', product_name: '血压血糖监测服务', sku_name: '',
price: 180.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
},
{
id: 'item-002-2', order_id: 'order-002', product_id: 'prod-003',
sku_id: '', product_name: '慢病随访服务', sku_name: '',
price: 80.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}
]
},
{
id: 'order-003', order_no: 'SV2026041300003',
user_id: 'user-003', merchant_id: MOCK_MERCHANT_ID,
order_status: 2, total_amount: 520.00, product_amount: 520.00,
shipping_fee: 0, paid_amount: 520.00,
shipping_address: '广州市海珠区工业大道789号 王阿姨',
remark: '术后第7天需换药处理',
created_at: '2026-04-13T10:00:00+08:00', updated_at: '2026-04-13T10:00:00+08:00',
items: [{
id: 'item-003-1', order_id: 'order-003', product_id: 'prod-004',
sku_id: '', product_name: '术后伤口护理', sku_name: '标准换药套餐',
price: 260.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
// ── 服务中 4 条 ──────────────────────────────────────────
{
id: 'order-004', order_no: 'SV2026041200004',
user_id: 'user-004', merchant_id: MOCK_MERCHANT_ID,
order_status: 3, total_amount: 1200.00, product_amount: 1200.00,
shipping_fee: 0, paid_amount: 1200.00,
shipping_address: '广州市番禺区市桥街101号 陈老先生',
remark: '每周三次康复训练',
created_at: '2026-04-12T14:00:00+08:00', updated_at: '2026-04-12T14:30:00+08:00',
items: [{
id: 'item-004-1', order_id: 'order-004', product_id: 'prod-005',
sku_id: '', product_name: '上门康复护理(月套餐)', sku_name: '基础康复12次',
price: 1200.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-005', order_no: 'SV2026041200005',
user_id: 'user-005', merchant_id: MOCK_MERCHANT_ID,
order_status: 3, total_amount: 450.00, product_amount: 450.00,
shipping_fee: 0, paid_amount: 450.00,
shipping_address: '广州市白云区棠景街202号 刘奶奶',
remark: '糖尿病足护理,注意消毒',
created_at: '2026-04-12T09:00:00+08:00', updated_at: '2026-04-12T11:00:00+08:00',
items: [
{
id: 'item-005-1', order_id: 'order-005', product_id: 'prod-006',
sku_id: '', product_name: '糖尿病足专项护理', sku_name: '',
price: 280.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
},
{
id: 'item-005-2', order_id: 'order-005', product_id: 'prod-002',
sku_id: '', product_name: '血压血糖监测服务', sku_name: '',
price: 170.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}
]
},
{
id: 'order-006', order_no: 'SV2026041100006',
user_id: 'user-006', merchant_id: MOCK_MERCHANT_ID,
order_status: 3, total_amount: 680.00, product_amount: 680.00,
shipping_fee: 0, paid_amount: 680.00,
shipping_address: '广州市荔湾区荔湾路303号 赵大伯',
remark: '卒中后遗症,肢体功能训练',
created_at: '2026-04-11T10:30:00+08:00', updated_at: '2026-04-11T14:00:00+08:00',
items: [{
id: 'item-006-1', order_id: 'order-006', product_id: 'prod-005',
sku_id: '', product_name: '上门康复护理(月套餐)', sku_name: '进阶康复8次',
price: 680.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-007', order_no: 'SV2026041100007',
user_id: 'user-007', merchant_id: MOCK_MERCHANT_ID,
order_status: 3, total_amount: 320.00, product_amount: 320.00,
shipping_fee: 0, paid_amount: 320.00,
shipping_address: '广州市黄埔区科学城404号 孙阿姨',
remark: '鼻饲护理,按时喂食',
created_at: '2026-04-11T08:00:00+08:00', updated_at: '2026-04-11T08:00:00+08:00',
items: [{
id: 'item-007-1', order_id: 'order-007', product_id: 'prod-007',
sku_id: '', product_name: '鼻饲营养护理', sku_name: '',
price: 320.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
// ── 已完成 8 条 ──────────────────────────────────────────
{
id: 'order-008', order_no: 'SV2026041000008',
user_id: 'user-001', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 180.00, product_amount: 180.00,
shipping_fee: 0, paid_amount: 180.00,
shipping_address: '广州市天河区天河路123号 李奶奶',
remark: '', created_at: '2026-04-10T09:00:00+08:00', updated_at: '2026-04-10T12:00:00+08:00',
items: [{
id: 'item-008-1', order_id: 'order-008', product_id: 'prod-002',
sku_id: '', product_name: '血压血糖监测服务', sku_name: '',
price: 180.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-009', order_no: 'SV2026040900009',
user_id: 'user-008', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 560.00, product_amount: 560.00,
shipping_fee: 0, paid_amount: 560.00,
shipping_address: '广州市增城区荔城街505号 郑爷爷',
remark: '', created_at: '2026-04-09T10:00:00+08:00', updated_at: '2026-04-09T15:00:00+08:00',
items: [{
id: 'item-009-1', order_id: 'order-009', product_id: 'prod-008',
sku_id: '', product_name: '术后营养指导4次', sku_name: '',
price: 280.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-010', order_no: 'SV2026040800010',
user_id: 'user-009', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 260.00, product_amount: 260.00,
shipping_fee: 0, paid_amount: 260.00,
shipping_address: '广州市从化区街口街606号 吴阿姨',
remark: '', created_at: '2026-04-08T14:00:00+08:00', updated_at: '2026-04-08T16:30:00+08:00',
items: [{
id: 'item-010-1', order_id: 'order-010', product_id: 'prod-003',
sku_id: '', product_name: '慢病随访服务', sku_name: '',
price: 130.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-011', order_no: 'SV2026040700011',
user_id: 'user-010', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 980.00, product_amount: 980.00,
shipping_fee: 0, paid_amount: 980.00,
shipping_address: '广州市南沙区南沙街707号 何老先生',
remark: '', created_at: '2026-04-07T09:00:00+08:00', updated_at: '2026-04-07T17:00:00+08:00',
items: [
{
id: 'item-011-1', order_id: 'order-011', product_id: 'prod-001',
sku_id: '', product_name: '居家上门护理(半日)', sku_name: '',
price: 380.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
},
{
id: 'item-011-2', order_id: 'order-011', product_id: 'prod-008',
sku_id: '', product_name: '术后营养指导4次', sku_name: '',
price: 300.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}
]
},
{
id: 'order-012', order_no: 'SV2026040600012',
user_id: 'user-002', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 430.00, product_amount: 430.00,
shipping_fee: 0, paid_amount: 430.00,
shipping_address: '广州市越秀区东风东路456号 张爷爷',
remark: '', created_at: '2026-04-06T10:00:00+08:00', updated_at: '2026-04-06T12:30:00+08:00',
items: [{
id: 'item-012-1', order_id: 'order-012', product_id: 'prod-009',
sku_id: '', product_name: '中医调理推拿3次', sku_name: '',
price: 430.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-013', order_no: 'SV2026040500013',
user_id: 'user-011', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 350.00, product_amount: 350.00,
shipping_fee: 0, paid_amount: 350.00,
shipping_address: '广州市花都区新华街808号 冯奶奶',
remark: '', created_at: '2026-04-05T14:30:00+08:00', updated_at: '2026-04-05T16:00:00+08:00',
items: [{
id: 'item-013-1', order_id: 'order-013', product_id: 'prod-006',
sku_id: '', product_name: '糖尿病足专项护理', sku_name: '',
price: 350.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-014', order_no: 'SV2026040400014',
user_id: 'user-012', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 480.00, product_amount: 480.00,
shipping_fee: 0, paid_amount: 480.00,
shipping_address: '广州市番禺区大石街909号 蒋大伯',
remark: '', created_at: '2026-04-04T09:00:00+08:00', updated_at: '2026-04-04T14:00:00+08:00',
items: [{
id: 'item-014-1', order_id: 'order-014', product_id: 'prod-004',
sku_id: '', product_name: '术后伤口护理', sku_name: '',
price: 240.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-015', order_no: 'SV2026040300015',
user_id: 'user-003', merchant_id: MOCK_MERCHANT_ID,
order_status: 4, total_amount: 680.00, product_amount: 680.00,
shipping_fee: 0, paid_amount: 680.00,
shipping_address: '广州市海珠区工业大道789号 王阿姨',
remark: '', created_at: '2026-04-03T10:00:00+08:00', updated_at: '2026-04-03T15:30:00+08:00',
items: [{
id: 'item-015-1', order_id: 'order-015', product_id: 'prod-005',
sku_id: '', product_name: '上门康复护理(月套餐)', sku_name: '',
price: 680.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
}
]
// 售后订单 mockstatus: 0=退款中, 6=退款完成)
// 退款中=2条, 退款完成=3条
export const MOCK_AFTERSALE_ORDERS: MockOrderType[] = [
{
id: 'order-ref-001', order_no: 'SV2026041300R01',
user_id: 'user-004', merchant_id: MOCK_MERCHANT_ID,
order_status: 0, total_amount: 380.00, product_amount: 380.00,
shipping_fee: 0, paid_amount: 380.00,
shipping_address: '广州市番禺区市桥街101号 陈老先生',
remark: '家属临时有事,申请取消本次上门服务',
created_at: '2026-04-13T07:00:00+08:00', updated_at: '2026-04-13T07:30:00+08:00',
items: [{
id: 'item-ref-001-1', order_id: 'order-ref-001', product_id: 'prod-001',
sku_id: '', product_name: '居家上门护理(半日)', sku_name: '',
price: 380.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-ref-002', order_no: 'SV2026041200R02',
user_id: 'user-005', merchant_id: MOCK_MERCHANT_ID,
order_status: 0, total_amount: 180.00, product_amount: 180.00,
shipping_fee: 0, paid_amount: 180.00,
shipping_address: '广州市白云区棠景街202号 刘奶奶',
remark: '重复下单需退一单',
created_at: '2026-04-12T16:00:00+08:00', updated_at: '2026-04-12T16:10:00+08:00',
items: [{
id: 'item-ref-002-1', order_id: 'order-ref-002', product_id: 'prod-002',
sku_id: '', product_name: '血压血糖监测服务', sku_name: '',
price: 180.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-ref-003', order_no: 'SV2026041000R03',
user_id: 'user-008', merchant_id: MOCK_MERCHANT_ID,
order_status: 6, total_amount: 260.00, product_amount: 260.00,
shipping_fee: 0, paid_amount: 260.00,
shipping_address: '广州市增城区荔城街505号 郑爷爷',
remark: '服务已退款', created_at: '2026-04-10T10:00:00+08:00', updated_at: '2026-04-11T09:00:00+08:00',
items: [{
id: 'item-ref-003-1', order_id: 'order-ref-003', product_id: 'prod-003',
sku_id: '', product_name: '慢病随访服务', sku_name: '',
price: 130.00, quantity: 2, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-ref-004', order_no: 'SV2026040800R04',
user_id: 'user-009', merchant_id: MOCK_MERCHANT_ID,
order_status: 6, total_amount: 430.00, product_amount: 430.00,
shipping_fee: 0, paid_amount: 430.00,
shipping_address: '广州市从化区街口街606号 吴阿姨',
remark: '', created_at: '2026-04-08T09:00:00+08:00', updated_at: '2026-04-09T10:00:00+08:00',
items: [{
id: 'item-ref-004-1', order_id: 'order-ref-004', product_id: 'prod-009',
sku_id: '', product_name: '中医调理推拿3次', sku_name: '',
price: 430.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
},
{
id: 'order-ref-005', order_no: 'SV2026040600R05',
user_id: 'user-010', merchant_id: MOCK_MERCHANT_ID,
order_status: 6, total_amount: 320.00, product_amount: 320.00,
shipping_fee: 0, paid_amount: 320.00,
shipping_address: '广州市南沙区南沙街707号 何老先生',
remark: '', created_at: '2026-04-06T14:00:00+08:00', updated_at: '2026-04-07T09:00:00+08:00',
items: [{
id: 'item-ref-005-1', order_id: 'order-ref-005', product_id: 'prod-007',
sku_id: '', product_name: '鼻饲营养护理', sku_name: '',
price: 320.00, quantity: 1, image_url: '/static/images/default-product.png', sku_snapshot: ''
}]
}
]
// 最近5条订单供 profile 使用,复用上面 MOCK_ORDERS 前5条数据结构
export type MockProfileOrderType = {
id: string
order_no: string
order_status: number
total_amount: number
created_at: string
items: MockOrderItemType[]
}
export function getMockRecentOrders() : MockProfileOrderType[] {
const result: MockProfileOrderType[] = []
const src = MOCK_ORDERS
const cnt = src.length < 5 ? src.length : 5
for (let i = 0; i < cnt; i++) {
result.push({
id: src[i].id,
order_no: src[i].order_no,
order_status: src[i].order_status,
total_amount: src[i].total_amount,
created_at: src[i].created_at,
items: src[i].items
} as MockProfileOrderType)
}
return result
}
// 订单 tab 计数(由 MOCK_ORDERS 推导,不手写)
export function getMockOrderTabCounts() : { pending: number, inprogress: number, completed: number, total: number } {
let pending = 0, inprogress = 0, completed = 0
for (let i = 0; i < MOCK_ORDERS.length; i++) {
const s = MOCK_ORDERS[i].order_status
if (s === 2) pending++
else if (s === 3) inprogress++
else if (s === 4) completed++
}
return { pending, inprogress, completed, total: MOCK_ORDERS.length }
}
// 售后 tab 计数
export function getMockAftersaleTabCounts() : { refunding: number, refunded: number, total: number } {
let refunding = 0, refunded = 0
for (let i = 0; i < MOCK_AFTERSALE_ORDERS.length; i++) {
const s = MOCK_AFTERSALE_ORDERS[i].order_status
if (s === 0) refunding++
else if (s === 6) refunded++
}
return { refunding, refunded, total: MOCK_AFTERSALE_ORDERS.length }
}
// ════════════════════════════════════════════════════════════
// 三、库存 mock 数据
// ════════════════════════════════════════════════════════════
export type MockInventoryProductType = {
id: string
name: string
main_image_url: string
total_stock: number
warning_stock: number
}
// 12条覆盖正常/低库存/售罄
// 低库存3条stock<=warning_stock且>0售罄2条stock=0
export const MOCK_INVENTORY_PRODUCTS: MockInventoryProductType[] = [
{ id: 'prod-001', name: '居家上门护理(半日)', main_image_url: '/static/images/default-product.png', total_stock: 20, warning_stock: 5 },
{ id: 'prod-002', name: '血压血糖监测服务', main_image_url: '/static/images/default-product.png', total_stock: 30, warning_stock: 8 },
{ id: 'prod-003', name: '慢病随访服务', main_image_url: '/static/images/default-product.png', total_stock: 25, warning_stock: 5 },
{ id: 'prod-004', name: '术后伤口护理', main_image_url: '/static/images/default-product.png', total_stock: 4, warning_stock: 5 }, // 低库存1
{ id: 'prod-005', name: '上门康复护理(月套餐)', main_image_url: '/static/images/default-product.png', total_stock: 8, warning_stock: 10 }, // 低库存2
{ id: 'prod-006', name: '糖尿病足专项护理', main_image_url: '/static/images/default-product.png', total_stock: 3, warning_stock: 5 }, // 低库存3
{ id: 'prod-007', name: '鼻饲营养护理', main_image_url: '/static/images/default-product.png', total_stock: 0, warning_stock: 3 }, // 售罄1
{ id: 'prod-008', name: '术后营养指导4次', main_image_url: '/static/images/default-product.png', total_stock: 0, warning_stock: 3 }, // 售罄2
{ id: 'prod-009', name: '中医调理推拿3次', main_image_url: '/static/images/default-product.png', total_stock: 15, warning_stock: 5 },
{ id: 'prod-010', name: '老年心理疏导服务', main_image_url: '/static/images/default-product.png', total_stock: 18, warning_stock: 5 },
{ id: 'prod-011', name: '居家药品管理服务', main_image_url: '/static/images/default-product.png', total_stock: 22, warning_stock: 5 },
{ id: 'prod-012', name: '紧急呼叫响应服务', main_image_url: '/static/images/default-product.png', total_stock: 50, warning_stock: 10 }
]
// 从 mock 推导 stats与页面 loadStats 逻辑完全一致)
export function getMockInventoryStats() : { totalProducts: number, lowStock: number, outOfStock: number } {
let total = 0, low = 0, out = 0
for (let i = 0; i < MOCK_INVENTORY_PRODUCTS.length; i++) {
const p = MOCK_INVENTORY_PRODUCTS[i]
total++
if (p.total_stock === 0) out++
else if (p.total_stock <= p.warning_stock) low++
}
return { totalProducts: total, lowStock: low, outOfStock: out }
}
// ════════════════════════════════════════════════════════════
// 四、评价 mock 数据
// ════════════════════════════════════════════════════════════
export type MockReviewType = {
id: string
product_id: string
product_name: string
user_id: string
user_name: string
user_avatar: string
rating: number
content: string
images: string // JSON 字符串数组,与 parseImages() 兼容
reply: string
created_at: string
}
// 10条4条待回复reply=''6条已回复reply非空
// PENDING_REVIEW_COUNT = 4 与此处一致
export const MOCK_REVIEWS: MockReviewType[] = [
// ── 待回复 4 条 ──
{
id: 'review-001', product_id: 'prod-001', product_name: '居家上门护理(半日)',
user_id: 'user-001', user_name: '李*敏', user_avatar: '',
rating: 5, content: '护士非常专业,老人很配合,感谢!态度好服务细心,以后还会预约。',
images: '[]', reply: '', created_at: '2026-04-13T11:00:00+08:00'
},
{
id: 'review-002', product_id: 'prod-005', product_name: '上门康复护理(月套餐)',
user_id: 'user-006', user_name: '赵*强', user_avatar: '',
rating: 4, content: '康复师技术不错,父亲恢复得挺好,就是希望每次时长能再长一点。',
images: '[]', reply: '', created_at: '2026-04-12T16:00:00+08:00'
},
{
id: 'review-003', product_id: 'prod-002', product_name: '血压血糖监测服务',
user_id: 'user-007', user_name: '孙*华', user_avatar: '',
rating: 5, content: '每次都准时上门,测量数据详细,还会给我们讲解注意事项,非常满意。',
images: '[]', reply: '', created_at: '2026-04-11T09:30:00+08:00'
},
{
id: 'review-004', product_id: 'prod-009', product_name: '中医调理推拿3次',
user_id: 'user-002', user_name: '张*国', user_avatar: '',
rating: 4, content: '推拿师手法到位,推完肩颈轻松很多,就是预约时间有点难排。',
images: '[]', reply: '', created_at: '2026-04-10T14:00:00+08:00'
},
// ── 已回复 6 条 ──
{
id: 'review-005', product_id: 'prod-004', product_name: '术后伤口护理',
user_id: 'user-003', user_name: '王*兰', user_avatar: '',
rating: 5, content: '术后第一次用这个服务,护士很专业,换药很痛但很快,恢复良好。',
images: '[]', reply: '感谢您的信任!我们护理团队会继续为您提供专业服务,祝恢复顺利!',
created_at: '2026-04-09T10:00:00+08:00'
},
{
id: 'review-006', product_id: 'prod-006', product_name: '糖尿病足专项护理',
user_id: 'user-005', user_name: '刘*珍', user_avatar: '',
rating: 5, content: '护士很有耐心,每次都会叮嘱注意事项,妈妈的伤口恢复得很好。',
images: '["\/static\/images\/default-product.png"]',
reply: '您好!感谢反馈,糖尿病足护理需要长期坚持,我们会持续陪伴您和家人。',
created_at: '2026-04-08T11:00:00+08:00'
},
{
id: 'review-007', product_id: 'prod-008', product_name: '术后营养指导4次',
user_id: 'user-009', user_name: '吴*秀', user_avatar: '',
rating: 5, content: '营养师给出的饮食方案很合理,家人按方案调整饮食后状态好多了!',
images: '[]', reply: '谢谢您的认可!合理饮食是术后恢复的关键,有任何问题随时联系我们。',
created_at: '2026-04-07T09:00:00+08:00'
},
{
id: 'review-008', product_id: 'prod-003', product_name: '慢病随访服务',
user_id: 'user-008', user_name: '郑*明', user_avatar: '',
rating: 4, content: '随访很规律,每次记录都很详细,给老人建立了完整的健康档案。',
images: '[]', reply: '感谢您的评价!定期随访是管理慢性病的重要手段,我们会继续做好记录工作。',
created_at: '2026-04-06T14:00:00+08:00'
},
{
id: 'review-009', product_id: 'prod-007', product_name: '鼻饲营养护理',
user_id: 'user-011', user_name: '冯*莲', user_avatar: '',
rating: 5, content: '护士操作非常规范,每次来都会检查设备是否正常,很放心。',
images: '[]', reply: '您好!鼻饲护理需要严格操作规范,感谢您的信任,我们会坚持做好每一次服务。',
created_at: '2026-04-05T10:00:00+08:00'
},
{
id: 'review-010', product_id: 'prod-001', product_name: '居家上门护理(半日)',
user_id: 'user-012', user_name: '蒋*华', user_avatar: '',
rating: 5, content: '服务很贴心,护士会提前电话确认时间,上门也十分准时,赞!',
images: '[]', reply: '感谢您的五星好评!准时守约是我们的基本承诺,期待下次继续为您服务。',
created_at: '2026-04-04T15:00:00+08:00'
}
]
// filter 逻辑(与页面 currentFilter 对齐)
export function getMockReviews(filter: string) : MockReviewType[] {
if (filter === 'pending') {
const result: MockReviewType[] = []
for (let i = 0; i < MOCK_REVIEWS.length; i++) {
if (MOCK_REVIEWS[i].reply === '') result.push(MOCK_REVIEWS[i])
}
return result
}
if (filter === 'replied') {
const result: MockReviewType[] = []
for (let i = 0; i < MOCK_REVIEWS.length; i++) {
if (MOCK_REVIEWS[i].reply !== '') result.push(MOCK_REVIEWS[i])
}
return result
}
return MOCK_REVIEWS
}
// ════════════════════════════════════════════════════════════
// 五、统计 mock 数据
// ════════════════════════════════════════════════════════════
export type MockTrendItemType = {
day: string
amount: number
}
// 趋势数据today/week/month 三档
export const MOCK_TREND_TODAY: MockTrendItemType[] = [
{ day: '08时', amount: 260 }, { day: '09时', amount: 380 },
{ day: '10时', amount: 520 }, { day: '11时', amount: 180 },
{ day: '14时', amount: 350 }, { day: '15时', amount: 430 },
{ day: '16时', amount: 220 }
]
export const MOCK_TREND_WEEK: MockTrendItemType[] = [
{ day: '周一', amount: 680 }, { day: '周二', amount: 1200 },
{ day: '周三', amount: 980 }, { day: '周四', amount: 1560 },
{ day: '周五', amount: 2340 }, { day: '周六', amount: 800 },
{ day: '周日', amount: 420 }
]
export const MOCK_TREND_MONTH: MockTrendItemType[] = [
{ day: '4/1', amount: 2100 }, { day: '4/2', amount: 1800 },
{ day: '4/3', amount: 2680 }, { day: '4/4', amount: 3200 },
{ day: '4/5', amount: 1950 }, { day: '4/6', amount: 2460 },
{ day: '4/7', amount: 980 }
]
// 热门服务ID与inventory对齐由orders中统计派生
export type MockStatProductType = {
id: string
name: string
image: string
sales: number
revenue: number
}
export const MOCK_HOT_PRODUCTS: MockStatProductType[] = [
{ id: 'prod-005', name: '上门康复护理(月套餐)', image: '/static/images/default-product.png', sales: 38, revenue: 45600 },
{ id: 'prod-001', name: '居家上门护理(半日)', image: '/static/images/default-product.png', sales: 52, revenue: 19760 },
{ id: 'prod-002', name: '血压血糖监测服务', image: '/static/images/default-product.png', sales: 67, revenue: 12060 },
{ id: 'prod-004', name: '术后伤口护理', image: '/static/images/default-product.png', sales: 29, revenue: 13920 },
{ id: 'prod-009', name: '中医调理推拿3次', image: '/static/images/default-product.png', sales: 24, revenue: 10320 }
]
// 统计概览today/week/month 三档)
export function getMockStats(range: string) : { todaySales: string, todayOrders: number, todayVisitors: number, conversionRate: number } {
if (range === 'today') {
return { todaySales: '2340.00', todayOrders: 7, todayVisitors: 42, conversionRate: 17 }
}
if (range === 'week') {
return { todaySales: '7980.00', todayOrders: 28, todayVisitors: 165, conversionRate: 17 }
}
// month
return { todaySales: '31250.00', todayOrders: 98, todayVisitors: 620, conversionRate: 16 }
}
export function getMockTrendData(range: string) : MockTrendItemType[] {
if (range === 'today') return MOCK_TREND_TODAY
if (range === 'week') return MOCK_TREND_WEEK
return MOCK_TREND_MONTH
}
// ════════════════════════════════════════════════════════════
// 六、结算 mock 数据
// ════════════════════════════════════════════════════════════
export type MockRecordType = {
id: string
title: string
amount: number
type: string // 'order'=收支记录, 'withdraw'=结算/补贴记录
created_at: string
}
export const MOCK_BALANCE = '12680.50'
export const MOCK_FINANCE_STATS = {
todayRevenue: '2340.00',
monthRevenue: '31250.00',
pendingWithdraw: '4200.00'
}
export const MOCK_FINANCE_RECORDS: MockRecordType[] = [
// ── type=order 收支记录 ──
{ id: 'rec-001', title: '订单收入 SV2026041300001', amount: 380.00, type: 'order', created_at: '2026-04-13T08:20:00+08:00' },
{ id: 'rec-002', title: '订单收入 SV2026041300002', amount: 260.00, type: 'order', created_at: '2026-04-13T09:35:00+08:00' },
{ id: 'rec-003', title: '订单收入 SV2026041300003', amount: 520.00, type: 'order', created_at: '2026-04-13T10:05:00+08:00' },
{ id: 'rec-004', title: '退款扣减 SV2026041300R01', amount: -380.00, type: 'order', created_at: '2026-04-13T07:35:00+08:00' },
{ id: 'rec-005', title: '退款扣减 SV2026041200R02', amount: -180.00, type: 'order', created_at: '2026-04-12T16:15:00+08:00' },
{ id: 'rec-006', title: '订单收入 SV2026041200004', amount: 1200.00, type: 'order', created_at: '2026-04-12T14:05:00+08:00' },
{ id: 'rec-007', title: '订单收入 SV2026041200005', amount: 450.00, type: 'order', created_at: '2026-04-12T09:05:00+08:00' },
{ id: 'rec-008', title: '订单收入 SV2026041100006', amount: 680.00, type: 'order', created_at: '2026-04-11T10:35:00+08:00' },
// ── type=withdraw 结算/补贴记录 ──
{ id: 'rec-009', title: '长者关怀补贴拨付', amount: 2000.00, type: 'withdraw', created_at: '2026-04-10T09:00:00+08:00' },
{ id: 'rec-010', title: '月度服务结算', amount: 8600.00, type: 'withdraw', created_at: '2026-04-01T10:00:00+08:00' },
{ id: 'rec-011', title: '医保结算入账', amount: 3200.00, type: 'withdraw', created_at: '2026-03-28T14:00:00+08:00' },
{ id: 'rec-012', title: '申请提现', amount: -5000.00, type: 'withdraw', created_at: '2026-03-25T09:00:00+08:00' },
{ id: 'rec-013', title: '医保结算入账', amount: 2800.00, type: 'withdraw', created_at: '2026-03-15T10:00:00+08:00' }
]
// tab 过滤record 或 withdraw
export function getMockFinanceRecords(tab: string) : MockRecordType[] {
if (tab === 'record') {
const result: MockRecordType[] = []
for (let i = 0; i < MOCK_FINANCE_RECORDS.length; i++) {
if (MOCK_FINANCE_RECORDS[i].type === 'order') result.push(MOCK_FINANCE_RECORDS[i])
}
return result
}
if (tab === 'withdraw') {
const result: MockRecordType[] = []
for (let i = 0; i < MOCK_FINANCE_RECORDS.length; i++) {
if (MOCK_FINANCE_RECORDS[i].type === 'withdraw') result.push(MOCK_FINANCE_RECORDS[i])
}
return result
}
return MOCK_FINANCE_RECORDS
}
// ════════════════════════════════════════════════════════════
// 七、促销活动 mock 数据
// ════════════════════════════════════════════════════════════
export type MockPromotionType = {
id: string
name: string
type: string // 'coupon' | 'seckill' | 'group'(与 tab 对应)
discount_text: string
start_time: string
end_time: string
status: number // 0=未开始, 1=进行中, 2=已结束
received_count: number
}
export const MOCK_PROMOTIONS: MockPromotionType[] = [
// ── coupon 护理套餐券 ──
{ id: 'promo-001', name: '居家护理体验满减券', type: 'coupon', discount_text: '满300减50', start_time: '2026-04-01T00:00:00+08:00', end_time: '2026-04-30T23:59:59+08:00', status: 1, received_count: 42 },
{ id: 'promo-002', name: '康复护理月套餐优惠券', type: 'coupon', discount_text: '满1000减120', start_time: '2026-04-10T00:00:00+08:00', end_time: '2026-05-10T23:59:59+08:00', status: 1, received_count: 18 },
{ id: 'promo-003', name: '护理新人首单优惠', type: 'coupon', discount_text: '首单立减80元', start_time: '2026-03-01T00:00:00+08:00', end_time: '2026-03-31T23:59:59+08:00', status: 2, received_count: 156 },
// ── seckill 康复体验 ──
{ id: 'promo-004', name: '上门康复体验特惠', type: 'seckill', discount_text: '原价380限时198', start_time: '2026-04-13T10:00:00+08:00', end_time: '2026-04-13T22:00:00+08:00', status: 1, received_count: 7 },
{ id: 'promo-005', name: '血压血糖检测体验价', type: 'seckill', discount_text: '9.9元体验', start_time: '2026-04-15T09:00:00+08:00', end_time: '2026-04-15T18:00:00+08:00', status: 0, received_count: 0 },
{ id: 'promo-006', name: '长者推拿体验', type: 'seckill', discount_text: '原价150体验价59', start_time: '2026-03-20T09:00:00+08:00', end_time: '2026-03-20T20:00:00+08:00', status: 2, received_count: 23 },
// ── group 慢病管理服务包 ──
{ id: 'promo-007', name: '高血压慢病三人拼团包', type: 'group', discount_text: '3人拼团享8.5折', start_time: '2026-04-05T00:00:00+08:00', end_time: '2026-04-20T23:59:59+08:00', status: 1, received_count: 9 },
{ id: 'promo-008', name: '糖尿病管理服务季包', type: 'group', discount_text: '2人拼团9折', start_time: '2026-04-20T00:00:00+08:00', end_time: '2026-05-20T23:59:59+08:00', status: 0, received_count: 0 },
{ id: 'promo-009', name: '慢病管理年度健康计划', type: 'group', discount_text: '3人拼团享7折', start_time: '2026-02-01T00:00:00+08:00', end_time: '2026-03-31T23:59:59+08:00', status: 2, received_count: 34 }
]
// tab 过滤(与 currentTab 对应)
export function getMockPromotions(tab: string) : MockPromotionType[] {
const result: MockPromotionType[] = []
for (let i = 0; i < MOCK_PROMOTIONS.length; i++) {
if (MOCK_PROMOTIONS[i].type === tab) result.push(MOCK_PROMOTIONS[i])
}
return result
}
// 订单筛选(供 orders.uvue 使用)
export function getMockOrdersByStatus(status: number) : MockOrderType[] {
if (status === -2) return MOCK_ORDERS // 全部
const result: MockOrderType[] = []
for (let i = 0; i < MOCK_ORDERS.length; i++) {
if (MOCK_ORDERS[i].order_status === status) result.push(MOCK_ORDERS[i])
}
return result
}
export function getMockAftersaleByStatus(status: number) : MockOrderType[] {
if (status === -3) return MOCK_AFTERSALE_ORDERS // 全部售后
const result: MockOrderType[] = []
for (let i = 0; i < MOCK_AFTERSALE_ORDERS.length; i++) {
if (MOCK_AFTERSALE_ORDERS[i].order_status === status) result.push(MOCK_AFTERSALE_ORDERS[i])
}
return result
}
// ════════════════════════════════════════════════════════════
// 八、关怀等级 & 服务对象 mock 数据
// ════════════════════════════════════════════════════════════
export type MockMemberLevelType = {
id: string
name: string
discount_rate: number
level_rank: number
}
export type MockUserInfoType = {
id: string
username: string
email: string
nickname: string | null
avatar_url: string | null
phone: string | null
tier_id: string | null
tier_name: string | null
}
// 关怀等级 4 档
export const MOCK_MEMBER_LEVELS: MockMemberLevelType[] = [
{ id: 'level-001', name: '普通关怀', discount_rate: 1.0, level_rank: 1 },
{ id: 'level-002', name: '银发关怀', discount_rate: 0.95, level_rank: 2 },
{ id: 'level-003', name: '金牌关怀', discount_rate: 0.90, level_rank: 3 },
{ id: 'level-004', name: '白金尊享', discount_rate: 0.85, level_rank: 4 }
]
// 服务对象 12 人4 人有等级tier_id 非空8 人暂无等级
// tier_name 通过 tier_id 与 MOCK_MEMBER_LEVELS 对应
export const MOCK_SERVICE_USERS: MockUserInfoType[] = [
// ── 已设置关怀等级 4 人(排在前面) ──
{
id: 'user-001', username: 'li_min',
email: 'limin@example.com',
nickname: '李敏奶奶',
avatar_url: '',
phone: '13800000001',
tier_id: 'level-004',
tier_name: '白金尊享'
},
{
id: 'user-004', username: 'chen_lao',
email: '',
nickname: '陈伯伯',
avatar_url: '',
phone: '13800000004',
tier_id: 'level-003',
tier_name: '金牌关怀'
},
{
id: 'user-005', username: 'liu_zhen',
email: '',
nickname: '刘珍阿姨',
avatar_url: '',
phone: '13800000005',
tier_id: 'level-002',
tier_name: '银发关怀'
},
{
id: 'user-006', username: 'zhao_qiang',
email: 'zhaoqiang@example.com',
nickname: '赵强大伯',
avatar_url: '',
phone: '13800000006',
tier_id: 'level-001',
tier_name: '普通关怀'
},
// ── 未设置关怀等级 8 人 ──
{
id: 'user-002', username: 'zhang_guo',
email: '',
nickname: '张国爷爷',
avatar_url: '',
phone: '13800000002',
tier_id: null,
tier_name: null
},
{
id: 'user-003', username: 'wang_lan',
email: '',
nickname: '王兰阿姨',
avatar_url: '',
phone: '13800000003',
tier_id: null,
tier_name: null
},
{
id: 'user-007', username: 'sun_hua',
email: 'sunhua@example.com',
nickname: '孙华叔叔',
avatar_url: '',
phone: '13800000007',
tier_id: null,
tier_name: null
},
{
id: 'user-008', username: 'zheng_ming',
email: '',
nickname: '郑明爷爷',
avatar_url: '',
phone: '13800000008',
tier_id: null,
tier_name: null
},
{
id: 'user-009', username: 'wu_xiu',
email: '',
nickname: '吴秀奶奶',
avatar_url: '',
phone: '13800000009',
tier_id: null,
tier_name: null
},
{
id: 'user-010', username: 'he_lao',
email: '',
nickname: '何老先生',
avatar_url: '',
phone: '13800000010',
tier_id: null,
tier_name: null
},
{
id: 'user-011', username: 'feng_lian',
email: '',
nickname: '冯莲奶奶',
avatar_url: '',
phone: '13800000011',
tier_id: null,
tier_name: null
},
{
id: 'user-012', username: 'jiang_hua',
email: '',
nickname: '蒋华大伯',
avatar_url: '',
phone: '13800000012',
tier_id: null,
tier_name: null
}
]

View File

@@ -348,6 +348,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import MerchantTabBar from '@/components/merchant-tabbar/MerchantTabBar.uvue'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockOrdersByStatus, getMockAftersaleByStatus, getMockOrderTabCounts, getMockAftersaleTabCounts } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type OrderItemType = {
id: string
@@ -531,6 +532,10 @@
},
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
if (session != null && session.user != null) {
@@ -547,7 +552,14 @@
async loadOrders() {
if (this.loading) return
this.loading = true
if (USE_MOCK) {
const filtered = getMockOrdersByStatus(this.currentTab)
this.orders = filtered as OrderType[]
this.hasMore = false
this.loading = false
this.refreshing = false
return
}
try {
let query = supa
.from('ml_orders')
@@ -664,6 +676,18 @@
},
async loadOrderCounts() {
if (USE_MOCK) {
const c = getMockOrderTabCounts()
this.orderTabs[0].count = c.pending
this.orderTabs[1].count = c.inprogress
this.orderTabs[2].count = c.completed
this.orderTabs[3].count = c.total
const ac = getMockAftersaleTabCounts()
this.aftersaleTabs[0].count = ac.total
this.aftersaleTabs[1].count = ac.refunding
this.aftersaleTabs[2].count = ac.refunded
return
}
try {
const response = await supa
.from('ml_orders')
@@ -740,6 +764,14 @@
async loadAftersaleOrders() {
if (this.aftersaleLoading) return
this.aftersaleLoading = true
if (USE_MOCK) {
const filtered = getMockAftersaleByStatus(this.currentAftersaleTab)
this.aftersaleOrders = filtered as OrderType[]
this.aftersaleHasMore = false
this.aftersaleLoading = false
this.aftersaleRefreshing = false
return
}
try {
let query = supa
.from('ml_orders')

View File

@@ -48,20 +48,6 @@
</picker>
</view>
<view class="form-item">
<text class="label">商品品牌</text>
<picker
class="picker"
:range="brands"
range-key="name"
:value="brandIndex"
@change="onBrandChange"
>
<view class="picker-value">
{{ selectedBrand?.name || '请选择品牌' }}
</view>
</picker>
</view>
</view>
<!-- 商品图片 -->
@@ -175,25 +161,6 @@
</view>
</view>
<!-- 会员阶梯价 -->
<view class="section">
<view class="section-title">长者关怀价/专属补贴价 (选填)</view>
<view class="section-desc">若不填写则按照服务参考价或默认折扣计算</view>
<view v-for="(level, index) in memberLevels" :key="index" class="form-item">
<text class="label">{{ level.name }}价格</text>
<view class="price-input">
<text class="unit">¥</text>
<input
class="input"
v-model="level.price"
type="digit"
placeholder="长者关怀价"
/>
</view>
</view>
</view>
<!-- 服务属性 -->
<view class="section">
<view class="section-title">服务属性</view>
@@ -261,24 +228,16 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { SUPA_URL, logSupaConfig } from '@/ak/config.uts'
// 严格对齐 categories.sql 真实字段,禁止自行添加或兼容不存在的字段
type CategoryType = {
id: string
name: string
}
type BrandType = {
id: string
name: string
logo_url: string
}
type MemberLevelType = {
id: string
name: string
level_rank: number
discount_rate: number
price: string // 绑定输入框用
icon: string
description: string
color: string
created_at: string
}
export default {
@@ -289,15 +248,10 @@
categories: [] as CategoryType[],
categoryIndex: -1,
selectedCategory: null as CategoryType | null,
brands: [] as BrandType[],
brandIndex: -1,
selectedBrand: null as BrandType | null,
memberLevels: [] as MemberLevelType[],
product: {
name: '',
subtitle: '',
category_id: '',
brand_id: '',
main_image_url: '',
imageList: [] as string[],
base_price: '',
@@ -317,7 +271,7 @@
}
},
onLoad(options: any) {
async onLoad(options: any) {
let productId = ''
if (options) {
const keys = Object.keys(options as object)
@@ -341,18 +295,32 @@
}
}
// ── 启动诊断日志 ──
console.log('[product-edit] onLoad start')
console.log('[product-edit] mode:', productId ? '编辑' : '新增')
console.log('[product-edit] productId:', productId || '(none)')
logSupaConfig()
if (productId && productId !== '') {
this.productId = productId
this.isEdit = true
uni.setNavigationBarTitle({ title: '编辑服务' })
this.loadProductDetail(productId)
} else {
uni.setNavigationBarTitle({ title: '发布服务' })
}
this.initMerchantId()
this.loadCategories()
this.loadBrands()
this.loadMemberLevels()
// ── 先探活,通过后再加载业务数据 ──
const endpointOk = await this.checkSupabaseEndpoint()
if (!endpointOk) {
console.error('[product-edit] Supabase 端点不可用,跳过业务数据加载')
return
}
await this.loadCategories()
if (this.isEdit && productId !== '') {
this.loadProductDetail(productId)
}
},
methods: {
@@ -370,8 +338,79 @@
}
},
async loadMemberLevels() {
/**
* Supabase 端点联通检查:直接用真实业务表 categories 做 limit(1) 探测。
* - 200端点正常anon 有读权限
* - 401/403端点可达但权限不足打日志仍返回 true 表示端点在线)
* - HTML/非 JSONSUPA_URL 指向了非 PostgREST 服务 → false
* - 网络错误:服务不可达 → false
*/
async checkSupabaseEndpoint() : Promise<boolean> {
const baseUrl = supa.baseUrl
console.log('[supa-check] SUPA_URL:', baseUrl)
try {
const response = await supa
.from('categories')
.select('*')
.limit(1)
.execute()
const status = response.status
console.log('[supa-check] categories probe status:', status)
// ak-req 遇到非 JSON 响应会包装 { raw: '...' }
const d = response.data as any
const rawText = (d != null && typeof d === 'object' && d['raw'] != null)
? String(d['raw'])
: null
if (rawText != null) {
const looksHtml = rawText.trimStart().startsWith('<')
console.error('[supa-check] ✗ 收到非 JSON 响应SUPA_URL 可能指向了错误服务(反向代理/Studio UI')
console.error('[supa-check] content looks like html:', looksHtml)
console.error('[supa-check] raw preview:', rawText.substring(0, 250))
uni.showToast({ title: 'Supabase连接失败查看日志', icon: 'none', duration: 4000 })
return false
}
if (status === 200) {
const arr = response.data as any[]
console.log('[supa-check] ✓ categories 可读,行数:', arr != null ? arr.length : 0)
return true
}
if (status === 401 || status === 403) {
console.error('[supa-check] ⚠ 端点可达但 categories 查询返回', status)
console.error('[supa-check] ★ 这是认证层问题,不是 categories 表字段问题')
console.error('[supa-check] 请运维核查 self-hosted 实例配置:')
console.error('[supa-check] 1. SUPA_KEY 是否属于 SUPA_URL 对应的 Supabase 实例9126 Kong API 网关)')
console.error('[supa-check] 2. 9126 和 9127 是否属于同一套 docker-compose / 同一实例')
console.error('[supa-check] 3. Kong 是否已 reload 最新 consumer 配置ANON_KEY 与 SUPA_KEY 一致)')
console.error('[supa-check] 4. categories 表是否有 RLS policy 允许 anon SELECT认证通过后再查')
console.error('[supa-check] 5. SUPABASE_PUBLIC_URL 是否仍是 localhost:8000Connect 显示值不代表前端请求地址)')
console.error('[supa-check] error:', JSON.stringify(response.error))
uni.showToast({ title: '认证失败(401),请运维核查 Supabase 配置', icon: 'none', duration: 4000 })
// 401 = 认证不过,暂停后续请求避免日志污染
return false
}
if (status === 404) {
console.error('[supa-check] ⚠ categories 表不存在404')
console.error('[supa-check] 请在 Supabase Studio 确认表名是否为 categoriesschema 是否为 public')
return true // PostgREST 在线,但表名/schema 可能有误
}
console.log('[supa-check] status:', status, '— PostgREST 在线')
return true
} catch (e) {
console.error('[supa-check] ✗ 异常:', e)
uni.showToast({ title: 'Supabase连接失败', icon: 'none', duration: 3000 })
return false
}
},
async loadMemberLevels() {
this.memberLevels = []
try {
console.log('[member-levels] request start, table: ml_member_levels')
const response = await supa
.from('ml_member_levels')
.select('*')
@@ -379,15 +418,22 @@
.order('level_rank', { ascending: true })
.execute()
console.log('[member-levels] response status:', response.status)
if (response.error != null) {
console.error('获取会员等级失败:', response.error)
console.error('[member-levels] query error:', response.error)
console.error('[member-levels] status:', response.status)
const d = response.data as any
if (d != null && typeof d === 'object' && d['raw'] != null) {
console.error('[member-levels] raw preview:', String(d['raw']).substring(0, 200))
}
return
}
const rawData = response.data as any[]
console.log('[member-levels] raw length:', rawData != null ? rawData.length : 0)
if (rawData == null) return
this.memberLevels = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as any
this.memberLevels.push({
@@ -398,93 +444,95 @@
price: ''
} as MemberLevelType)
}
console.log('[member-levels] loaded:', this.memberLevels.length)
// 如果是编辑模式,还需要加载已有的会员价
if (this.isEdit) {
this.loadMemberPrices()
}
} catch (e) {
console.error('获取会员等级异常:', e)
console.error('[member-levels] exception:', e)
}
},
async loadMemberPrices() {
// 严格对齐 categories.sql 真实字段id, name, icon, description, color, created_at
async loadCategories() {
this.categories = []
try {
console.log('[categories] 开始从 categories 表读取分类(严格字段映射)...')
const response = await supa
.from('ml_product_member_prices')
.select('*')
.eq('product_id', this.productId)
.from('categories')
.select('id,name,icon,description,color,created_at')
.limit(100)
.execute()
console.log('[categories] response status:', response.status)
if (response.error != null) {
console.error('获取会员价失败:', response.error)
console.error('[categories] 查询失败:', response.error)
uni.showToast({ title: '分类加载失败', icon: 'none' })
return
}
const rawData = response.data as any[]
if (rawData == null || rawData.length == 0) return
console.log('[categories] raw length:', rawData != null ? rawData.length : 0)
if (rawData == null || rawData.length === 0) {
console.error('[categories] 返回数据为空,请检查:表名/RLS策略/anon key')
uni.showToast({ title: '分类加载失败:数据为空', icon: 'none' })
return
}
// 严格字段映射:仅接受 categories.sql 定义的 6 个字段
// 不再做 category_id/uuid/category_name/title/label 等兼容回退
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as any
const levelId = String(item['level_id'])
const price = String(item['member_price'])
const index = this.memberLevels.findIndex(lv => lv.id === levelId)
if (index >= 0) {
this.memberLevels[index].price = price
}
const catId = item['id'] != null ? String(item['id']) : ''
const catName = item['name'] != null ? String(item['name']) : ''
// id 或 name 缺失则跳过categories.sql 中两者均为 NOT NULL
if (catId === '' || catName === '') continue
this.categories.push({
id: catId,
name: catName,
icon: item['icon'] != null ? String(item['icon']) : '',
description: item['description'] != null ? String(item['description']) : '',
color: item['color'] != null ? String(item['color']) : '',
created_at: item['created_at'] != null ? String(item['created_at']) : ''
} as CategoryType)
}
console.log('[categories] 严格映射完成,共加载:', this.categories.length, '条')
// 编辑模式:分类加载完成后尝试回填
if (this.isEdit && this.product.category_id) {
this.tryCategoryBackfill()
}
} catch (e) {
console.error('获取会员价异常:', e)
console.error('[categories] 获取分类异常:', e)
uni.showToast({ title: '分类加载异常', icon: 'none' })
}
},
async loadCategories() {
try {
const response = await supa
.from('ml_categories')
.select('id, name')
.eq('is_active', true)
.order('sort_order', { ascending: true })
.execute()
if (response.error != null) {
console.error('获取分类失败:', response.error)
}
const rawData = response.data as any[]
if (rawData != null && rawData.length > 0) {
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as any
this.categories.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : ''
} as CategoryType)
}
} else {
// 演示版默认医养分类
this.categories = [
{ id: 'med', name: '医疗服务' },
{ id: 'drug', name: '药品器械' },
{ id: 'care', name: '居家护理' },
{ id: 'life', name: '生活服务' },
{ id: 'health', name: '健康管理' }
] as CategoryType[]
}
} catch (e) {
console.error('获取分类异常:', e)
this.categories = [
{ id: 'med', name: '医疗服务' },
{ id: 'drug', name: '药品器械' },
{ id: 'care', name: '居家护理' },
{ id: 'life', name: '生活服务' },
{ id: 'health', name: '健康管理' }
] as CategoryType[]
// 编辑态分类回填categories 加载完成 AND product.category_id 已就位时调用
tryCategoryBackfill() {
if (!this.product.category_id || this.categories.length === 0) return
const idx = this.categories.findIndex(c => c.id === this.product.category_id)
if (idx >= 0) {
this.categoryIndex = idx
this.selectedCategory = this.categories[idx]
console.log('[categories] 编辑态回填成功:', this.selectedCategory.name)
} else {
console.warn('[categories] 编辑态回填未找到匹配分类, category_id=', this.product.category_id)
}
},
async loadBrands() {
this.brands = []
try {
console.log('[brands] request start, table: ml_brands')
const response = await supa
.from('ml_brands')
.select('id, name, logo_url')
@@ -492,12 +540,21 @@
.order('name', { ascending: true })
.execute()
console.log('[brands] response status:', response.status)
if (response.error != null) {
console.error('获取品牌失败:', response.error)
console.error('[brands] query error:', response.error)
console.error('[brands] status:', response.status)
// 检查是否是非 JSON 响应包裹
const d = response.data as any
if (d != null && typeof d === 'object' && d['raw'] != null) {
console.error('[brands] raw preview:', String(d['raw']).substring(0, 200))
}
return
}
const rawData = response.data as any[]
console.log('[brands] raw length:', rawData != null ? rawData.length : 0)
if (rawData == null) return
for (let i = 0; i < rawData.length; i++) {
@@ -508,8 +565,9 @@
logo_url: item['logo_url'] != null ? String(item['logo_url']) : ''
} as BrandType)
}
console.log('[brands] loaded:', this.brands.length)
} catch (e) {
console.error('获取品牌异常:', e)
console.error('[brands] exception:', e)
}
},
@@ -544,7 +602,6 @@
this.product.name = getStr('name')
this.product.subtitle = getStr('subtitle')
this.product.category_id = getStr('category_id')
this.product.brand_id = getStr('brand_id')
this.product.main_image_url = getStr('main_image_url')
this.product.imageList = this.parseImageUrls(getStr('image_urls'))
this.product.base_price = getStr('base_price')
@@ -562,18 +619,9 @@
this.product.vip_discount_rate = getStr('vip_discount_rate')
this.product.description = getStr('description')
// 尝试回填分类(若此时 categories 已加载则立即生效;若尚未完成则由 loadCategories 结尾再次触发)
if (this.product.category_id) {
this.categoryIndex = this.categories.findIndex(c => c.id === this.product.category_id)
if (this.categoryIndex >= 0) {
this.selectedCategory = this.categories[this.categoryIndex]
}
}
if (this.product.brand_id) {
this.brandIndex = this.brands.findIndex(b => b.id === this.product.brand_id)
if (this.brandIndex >= 0) {
this.selectedBrand = this.brands[this.brandIndex]
}
this.tryCategoryBackfill()
}
} catch (e) {
uni.hideLoading()
@@ -599,13 +647,6 @@
this.product.category_id = this.selectedCategory.id
},
onBrandChange(e: any) {
const index = e.detail.value as number
this.brandIndex = index
this.selectedBrand = this.brands[index]
this.product.brand_id = this.selectedBrand.id
},
chooseMainImage() {
uni.chooseImage({
count: 1,
@@ -706,7 +747,6 @@
name: this.product.name,
subtitle: this.product.subtitle,
category_id: this.product.category_id,
brand_id: this.product.brand_id || null,
main_image_url: finalMainImage,
image_urls: imageUrlsStr,
base_price: this.product.base_price ? parseFloat(this.product.base_price) : 0,
@@ -761,58 +801,6 @@
response = insertResponse
}
// 保存会员价
let targetProductId = this.isEdit ? this.productId : ''
if (response != null && response.data != null) {
const responseData = response.data
if (Array.isArray(responseData)) {
const dataArr = responseData as any[]
if (dataArr.length > 0) {
const firstRow = dataArr[0] as UTSJSONObject
if (firstRow['id'] != null) {
targetProductId = String(firstRow['id'])
}
}
} else if (responseData instanceof UTSJSONObject) {
const dataObj = responseData as UTSJSONObject
if (dataObj['id'] != null) {
targetProductId = String(dataObj['id'])
}
}
}
console.log('最终目标产品ID:', targetProductId)
if (targetProductId && targetProductId !== '' && targetProductId !== 'undefined') {
// 1. 先删除旧的会员价
if (this.isEdit) {
console.log('删除旧会员价:', targetProductId)
await supa.from('ml_product_member_prices').delete().eq('product_id', targetProductId).execute()
}
// 2. 插入新的会员价
for (let i = 0; i < this.memberLevels.length; i++) {
const level = this.memberLevels[i]
if (level.price && level.price > 0) {
const memberPriceData = {
product_id: targetProductId,
level_id: level.id,
member_price: level.price,
created_at: new Date().toISOString()
} as UTSJSONObject
const insertRes = await supa
.from('ml_product_member_prices')
.insert(memberPriceData)
.execute()
if (insertRes.error != null) {
console.error('插入会员价失败', insertRes.error)
}
}
}
}
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {

View File

@@ -143,11 +143,6 @@
</view>
</scroll-view>
<!-- 添加服务按钮 -->
<view class="add-product-btn" @click="addProduct">
<text class="add-icon">+</text>
<text class="add-text">添加服务项目</text>
</view>
</view>
</template>

View File

@@ -181,6 +181,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import MerchantTabBar from '@/components/merchant-tabbar/MerchantTabBar.uvue'
import { USE_MOCK, MOCK_MERCHANT_ID, MOCK_SHOP_INFO, MOCK_TODAY_STATS, MOCK_PENDING_COUNTS, getMockRecentOrders } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
// ---- 复用 index 的类型定义 ----
type ShopInfoType = {
@@ -328,6 +329,10 @@
},
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
if (session != null && session.user != null) {
@@ -343,6 +348,35 @@
// 复用 index 的完整数据加载链
async loadAllData() {
if (USE_MOCK) {
this.shopInfo = {
id: MOCK_SHOP_INFO.id,
merchant_id: MOCK_SHOP_INFO.merchant_id,
shop_name: MOCK_SHOP_INFO.shop_name,
shop_logo: MOCK_SHOP_INFO.shop_logo,
shop_banner: MOCK_SHOP_INFO.shop_banner,
description: MOCK_SHOP_INFO.description,
contact_name: MOCK_SHOP_INFO.contact_name,
contact_phone: MOCK_SHOP_INFO.contact_phone,
rating_avg: MOCK_SHOP_INFO.rating_avg,
total_sales: MOCK_SHOP_INFO.total_sales,
status: MOCK_SHOP_INFO.status
} as ShopInfoType
this.todayStats = {
orders: MOCK_TODAY_STATS.orders,
sales: MOCK_TODAY_STATS.sales,
visitors: MOCK_TODAY_STATS.visitors,
conversion: MOCK_TODAY_STATS.conversion
} as TodayStatsType
this.pendingCounts = {
pending_shipment: MOCK_PENDING_COUNTS.pending_shipment,
refund_requests: MOCK_PENDING_COUNTS.refund_requests,
low_stock: MOCK_PENDING_COUNTS.low_stock,
pending_reviews: MOCK_PENDING_COUNTS.pending_reviews
} as PendingCountsType
this.recentOrders = getMockRecentOrders() as OrderType[]
return
}
await this.loadMerchantData()
await this.loadTodayStats()
await this.loadPendingCounts()

View File

@@ -47,6 +47,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockPromotions } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type PromotionType = {
id: string
@@ -75,21 +76,39 @@
},
onShow() {
this.loadPromotions()
if (this.merchantId !== '') {
this.loadPromotions()
}
},
methods: {
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
this.loadPromotions()
return
}
try {
const session = supa.getSession()
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
} catch (e) {}
this.loadPromotions()
this.loadPromotions()
},
async loadPromotions() {
if (USE_MOCK) {
this.promotions = getMockPromotions(this.currentTab)
this.loading = false
this.refreshing = false
return
}
this.loading = true
try {
const response = await supa
.from('ml_coupon_templates')
.select('*')
.eq('merchant_id', this.merchantId)
.eq('coupon_type', this.currentTab)
.order('created_at', { ascending: false })
.limit(50)
.execute()
@@ -118,7 +137,7 @@
this.promotions = promos
} catch (e) {
console.error('加载失败:', e)
console.error('加载活动失败:', e)
} finally {
this.loading = false
this.refreshing = false

View File

@@ -66,6 +66,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockReviews } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type ReviewType = {
id: string
@@ -100,6 +101,11 @@
},
onLoad() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
this.loadReviews()
return
}
// 同步设置 merchantId不用 async 包裹,避免 generator 内 this 绑定异常
try {
const session = supa.getSession()
@@ -117,6 +123,13 @@
if (!this.merchantId || this.merchantId.split('-').length !== 5) return
if (this.loading) return
this.loading = true
if (USE_MOCK) {
this.reviews = getMockReviews(this.currentFilter) as ReviewType[]
this.hasMore = false
this.loading = false
this.refreshing = false
return
}
try {
let query = supa
.from('ml_product_reviews')

View File

@@ -0,0 +1 @@
INSERT INTO "public"."categories" ("id", "name", "icon", "description", "color", "created_at") VALUES ('allergy', '抗过敏', '🤧', '过敏性疾病用药', '#9C27B0', '2026-01-30 01:48:28.396373+00'), ('child', '儿童用药', '👶', '儿童专用', '#00BCD4', '2026-01-28 09:23:10.344413+00'), ('chronic', '慢性疾病', '🫀', '长期管理', '#795548', '2026-01-28 09:23:10.344413+00'), ('cold', '感冒发烧', '🤧', '解热镇痛', '#2196F3', '2026-01-28 09:23:10.344413+00'), ('cough', '止咳化痰', '😷', '呼吸道疾病用药', '#2196F3', '2026-01-30 01:48:28.396373+00'), ('device', '医疗器械', '🩺', '医疗设备', '#607D8B', '2026-01-28 09:23:10.344413+00'), ('external', '外用药品', '🧴', '外用制剂', '#8BC34A', '2026-01-28 09:23:10.344413+00'), ('health', '健康食品', '🥗', '保健食品', '#FFC107', '2026-01-28 09:23:10.344413+00'), ('pain', '止痛消炎', '💊', '镇痛消炎', '#F44336', '2026-01-28 09:23:10.344413+00'), ('skin', '皮肤用药', '🤕', '皮肤护理', '#9C27B0', '2026-01-28 09:23:10.344413+00'), ('stomach', '肠胃用药', '🤢', '消化系统', '#4CAF50', '2026-01-28 09:23:10.344413+00'), ('vitamin', '维生素', '🍊', '营养补充', '#FF9800', '2026-01-28 09:23:10.344413+00');

View File

@@ -0,0 +1,109 @@
-- =====================================================
-- 在 Supabase Studio > SQL Editor 中执行此文件
-- 解决 ml_brands / ml_member_levels 404 问题
-- 字段名严格对齐前端 product-edit.uvue 代码
-- =====================================================
-- =====================================================
-- 一、品牌表 ml_brands
-- =====================================================
-- 前端 loadBrands() 用到字段id, name, logo_url, is_active
-- 查询条件is_active = true排序name asc
CREATE TABLE IF NOT EXISTS public.ml_brands (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
logo_url TEXT,
description TEXT,
website VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE public.ml_brands IS '品牌表';
-- RLS 策略:登录用户 / 匿名用户均可读
ALTER TABLE public.ml_brands ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "ml_brands anon select" ON public.ml_brands;
CREATE POLICY "ml_brands anon select"
ON public.ml_brands FOR SELECT
TO authenticated, anon
USING (true);
-- 初始化测试数据(可按需修改)
INSERT INTO public.ml_brands (name, logo_url, description, is_active) VALUES
('健民医药', '', '综合医药品牌', true),
('同仁堂', '', '百年老字号中医药', true),
('华润医疗', '', '华润集团旗下医疗品牌', true)
ON CONFLICT DO NOTHING;
-- =====================================================
-- 二、会员等级表 ml_member_levels
-- =====================================================
-- 前端 loadMemberLevels() 用到字段:
-- id, name, level_rank, discount_rate, is_active
-- 查询条件is_active = true排序level_rank asc
CREATE TABLE IF NOT EXISTS public.ml_member_levels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
level_rank INTEGER NOT NULL DEFAULT 0, -- 等级排序(越小越低)
discount_rate DECIMAL(5,4) NOT NULL DEFAULT 1.0000, -- 折扣率0.85 = 85折
min_amount DECIMAL(10,2) DEFAULT 0, -- 升级所需累计消费金额
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE public.ml_member_levels IS '会员等级配置表';
COMMENT ON COLUMN public.ml_member_levels.discount_rate IS '折扣率0.85 表示 85 折1.0 表示无折扣';
COMMENT ON COLUMN public.ml_member_levels.level_rank IS '等级排序值,数值越小等级越低,用于前端排序';
-- RLS 策略:登录用户 / 匿名用户均可读
ALTER TABLE public.ml_member_levels ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "ml_member_levels anon select" ON public.ml_member_levels;
CREATE POLICY "ml_member_levels anon select"
ON public.ml_member_levels FOR SELECT
TO authenticated, anon
USING (true);
-- 初始化 5 档会员等级数据
INSERT INTO public.ml_member_levels (name, level_rank, discount_rate, min_amount, description, is_active) VALUES
('普通会员', 0, 1.0000, 0, '注册即享', true),
('铜牌会员', 1, 0.9800, 500, '累计消费满500元', true),
('银牌会员', 2, 0.9500, 2000, '累计消费满2000元', true),
('金牌会员', 3, 0.9200, 5000, '累计消费满5000元', true),
('钻石会员', 4, 0.8800, 10000, '累计消费满10000元',true)
ON CONFLICT DO NOTHING;
-- =====================================================
-- 三、商品会员价关联表 ml_product_member_prices如不存在则补建
-- =====================================================
-- 前端 loadMemberPrices() 用到字段product_id, level_id, member_price
CREATE TABLE IF NOT EXISTS public.ml_product_member_prices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL,
level_id UUID NOT NULL REFERENCES public.ml_member_levels(id) ON DELETE CASCADE,
member_price DECIMAL(12,2) NOT NULL CHECK (member_price >= 0),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (product_id, level_id)
);
COMMENT ON TABLE public.ml_product_member_prices IS '商品会员等级专属价格';
ALTER TABLE public.ml_product_member_prices ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "ml_product_member_prices anon select" ON public.ml_product_member_prices;
CREATE POLICY "ml_product_member_prices anon select"
ON public.ml_product_member_prices FOR SELECT
TO authenticated, anon
USING (true);

View File

@@ -68,6 +68,7 @@
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import { USE_MOCK, MOCK_MERCHANT_ID, getMockStats, getMockTrendData, MOCK_HOT_PRODUCTS } from '@/pages/mall/merchant/mock/merchant-mock-data.uts'
type ProductType = {
id: string
@@ -116,6 +117,10 @@
methods: {
async initMerchantId() {
if (USE_MOCK) {
this.merchantId = MOCK_MERCHANT_ID
return
}
try {
const session = supa.getSession()
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
@@ -123,6 +128,13 @@
},
async loadStatistics() {
if (USE_MOCK) {
const s = getMockStats(this.dateRange)
this.stats = s
this.trendData = getMockTrendData(this.dateRange)
this.hotProducts = MOCK_HOT_PRODUCTS as ProductType[]
return
}
try {
const response = await supa
.from('ml_orders')

View File

@@ -1,39 +1,275 @@
mp.esm.js:126 TypeError: this.loadReviews is not a function
at Proxy.onShow (reviews.uvue:107)
at callWithErrorHandling (vue.runtime.esm.js:1356)
at callWithAsyncErrorHandling (vue.runtime.esm.js:1363)
at Array.hook.__weh.hook.__weh (vue.runtime.esm.js:2461)
at invokeArrayFns (uni-shared.es.js:469)
at Proxy.callHook (uni.mp.esm.js:944)
at ai.mpOptions.<computed> [as onShow] (uni.mp.esm.js:984)
at ai.<anonymous> (VM16334 WASubContext.js:1)
at ai.s.__callPageLifeTime__ (VM16334 WASubContext.js:1)
at VM16334 WASubContext.js:1(env: Windows,mp,1.06.2504030; lib: 3.14.2)
onError2 @ mp.esm.js:126
[product-edit] onLoad start
mp.esm.js:485 [product-edit] mode: 新增
mp.esm.js:485 [product-edit] productId: (none)
mp.esm.js:485 [SupaConfig] SUPA_URL : http://119.146.131.237:9129
mp.esm.js:485 [SupaConfig] SUPA_KEY : eyJhbGciOi...aavHyVvg
mp.esm.js:485 [supa-check] SUPA_URL: http://119.146.131.237:9129
mp.esm.js:485 [AkSupaQueryBuilder] execute - 表: categories filter: null
mp.esm.js:485 [ak-req] GET http://119.146.131.237:9129/rest/v1/categories?select=*&limit=1
mp.esm.js:485 [ak-req] apikey: eyJhbG...yVvg | Authorization: Bearer eyJhbG...yVvg | auth-mode: anon-key | prefer: count=exact
mp.esm.js:485 [supa-check] categories probe status: 206
mp.esm.js:485 [supa-check] status: 206 — PostgREST 在线
mp.esm.js:485 [categories] 开始从 categories 表读取分类(严格字段映射)...
mp.esm.js:485 [AkSupaQueryBuilder] execute - 表: categories filter: null
mp.esm.js:485 [ak-req] GET http://119.146.131.237:9129/rest/v1/categories?select=id%2Cname%2Cicon%2Cdescription%2Ccolor%2Ccreated_at&limit=100
mp.esm.js:485 [ak-req] apikey: eyJhbG...yVvg | Authorization: Bearer eyJhbG...yVvg | auth-mode: anon-key | prefer: count=exact
mp.esm.js:485 [categories] response status: 200
mp.esm.js:485 [categories] raw length: 12
mp.esm.js:485 [categories] 严格映射完成,共加载: 12 条
mp.esm.js:485 [brands] request start, table: ml_brands
mp.esm.js:485 [AkSupaQueryBuilder] execute - 表: ml_brands filter: is_active=eq.true
mp.esm.js:485 [member-levels] request start, table: ml_member_levels
mp.esm.js:485 [AkSupaQueryBuilder] execute - 表: ml_member_levels filter: is_active=eq.true
mp.esm.js:485 [ak-req] GET http://119.146.131.237:9129/rest/v1/ml_brands?select=id%2C%20name%2C%20logo_url&order=name.asc&is_active=eq.true
mp.esm.js:485 [ak-req] apikey: eyJhbG...yVvg | Authorization: Bearer eyJhbG...yVvg | auth-mode: anon-key | prefer: (none)
mp.esm.js:485 [ak-req] GET http://119.146.131.237:9129/rest/v1/ml_member_levels?select=*&order=level_rank.asc&is_active=eq.true
mp.esm.js:485 [ak-req] apikey: eyJhbG...yVvg | Authorization: Bearer eyJhbG...yVvg | auth-mode: anon-key | prefer: (none)
uni.api.esm.js:1043 GET http://119.146.131.237:9129/rest/v1/ml_brands?select=id%2C%20name%2C%20logo_url&order=name.asc&is_active=eq.true 404 (Not Found)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
(anonymous) @ uni.api.esm.js:1043
invokeApi @ uni.api.esm.js:330
promiseApi @ uni.api.esm.js:890
(anonymous) @ ak-req.uts:214
doOnce @ ak-req.uts:213
_loop$ @ ak-req.uts:312
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
request @ ak-req.uts:148
_callee19$ @ aksupa.uts:1259
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
requestWithAutoRefresh @ aksupa.uts:1258
_callee11$ @ aksupa.uts:1040
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
select @ aksupa.uts:955
_callee$ @ aksupa.uts:377
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
execute @ aksupa.uts:356
_callee7$ @ product-edit.uvue:626
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
loadBrands @ product-edit.uvue:617
_callee$ @ product-edit.uvue:373
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
onLoad @ product-edit.uvue:326
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
hook.__weh.hook.__weh @ vue.runtime.esm.js:2461
invokeArrayFns @ uni-shared.es.js:469
callHook @ uni.mp.esm.js:944
errorHandler @ vue.runtime.esm.js:5331
methods.onLoad @ uni.mp.esm.js:1542
Show 21 more frames
uni.api.esm.js:1043 GET http://119.146.131.237:9129/rest/v1/ml_member_levels?select=*&order=level_rank.asc&is_active=eq.true 404 (Not Found)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
(anonymous) @ uni.api.esm.js:1043
invokeApi @ uni.api.esm.js:330
promiseApi @ uni.api.esm.js:890
(anonymous) @ ak-req.uts:214
doOnce @ ak-req.uts:213
_loop$ @ ak-req.uts:312
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
s @ regeneratorRuntime.js?forceSync=true:1
_ @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
request @ ak-req.uts:148
_callee19$ @ aksupa.uts:1259
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
requestWithAutoRefresh @ aksupa.uts:1258
_callee11$ @ aksupa.uts:1040
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
select @ aksupa.uts:955
_callee$ @ aksupa.uts:377
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
execute @ aksupa.uts:356
_callee4$ @ product-edit.uvue:473
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
loadMemberLevels @ product-edit.uvue:464
_callee$ @ product-edit.uvue:374
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
onLoad @ product-edit.uvue:326
callWithErrorHandling @ vue.runtime.esm.js:1356
handleError @ vue.runtime.esm.js:1396
callWithErrorHandling @ vue.runtime.esm.js:1358
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
hook.__weh.hook.__weh @ vue.runtime.esm.js:2461
invokeArrayFns @ uni-shared.es.js:469
callHook @ uni.mp.esm.js:944
mpOptions.<computed> @ uni.mp.esm.js:984
TypeError: this.loadReviews is not a function
at Proxy._callee$ (reviews.uvue:116)
at s (regeneratorRuntime.js?forceSync=true:1)
at Generator.<anonymous> (regeneratorRuntime.js?forceSync=true:1)
at Generator.next (regeneratorRuntime.js?forceSync=true:1)
at tslib.es6.js:76
at new Promise (<anonymous>)
at Object.__awaiter (tslib.es6.js:72)
at Proxy.initMerchantId (reviews.uvue:111)
at Proxy.onLoad (reviews.uvue:103)
at callWithErrorHandling (vue.runtime.esm.js:1356)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
mp.esm.js:126 {reason: TypeError: this.loadReviews is not a function
at Proxy._callee$ (weapp:///pages/mall/merchant/r…, promise: Promise}(env: Windows,mp,1.06.2504030; lib: 3.14.2)
methods.onLoad @ uni.mp.esm.js:1542
Show 21 more frames
mp.esm.js:485 [brands] response status: 404
mp.esm.js:485 [brands] query error: UniError: 请求失败: 404
at new UniError2 (weapp:///http://127.0.0.1:48893/appservice/common/vendor.js?t=wechat&s=1776040029386&v=c01e6f70ace82f72049bc4519dbb3205:8560:5)
at Object.toUniError (weapp:///http://127.0.0.1:48893/appservice/utils/utils.js?t=wechat&s=1776040029386&v=170a1b511d4b4583a4ff38ba2eb804a7:77:18)
at AkSupa._callee19$ (weapp:///http://127.0.0.1:48893/appservice/components/supadb/aksupa.js?t=wechat&s=1776040029386&v=b1c88035cdf7fd4aab11f1c0cd809cf3:1878:41)
at s (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1588)
at Generator.<anonymous> (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:2925)
at Generator.next (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1951)
at fulfilled (weapp:///http://127.0.0.1:48893/appservice/common/vendor.js?t=wechat&s=1776040029386&v=c01e6f70ace82f72049bc4519dbb3205:9601:24)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
(anonymous) @ mp.esm.js:485
__f__ @ uni.api.esm.js:591
_callee7$ @ product-edit.uvue:631
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
loadBrands @ product-edit.uvue:617
_callee$ @ product-edit.uvue:373
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
onLoad @ product-edit.uvue:326
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
hook.__weh.hook.__weh @ vue.runtime.esm.js:2461
invokeArrayFns @ uni-shared.es.js:469
callHook @ uni.mp.esm.js:944
methods.onLoad @ uni.mp.esm.js:1542
mp.esm.js:485 [brands] status: 404(env: Windows,mp,1.06.2504030; lib: 3.14.2)
(anonymous) @ mp.esm.js:485
__f__ @ uni.api.esm.js:591
_callee7$ @ product-edit.uvue:632
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
loadBrands @ product-edit.uvue:617
_callee$ @ product-edit.uvue:373
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
onLoad @ product-edit.uvue:326
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
hook.__weh.hook.__weh @ vue.runtime.esm.js:2461
invokeArrayFns @ uni-shared.es.js:469
callHook @ uni.mp.esm.js:944
methods.onLoad @ uni.mp.esm.js:1542
mp.esm.js:485 [member-levels] response status: 404
mp.esm.js:485 [member-levels] query error: UniError: 请求失败: 404
at new UniError2 (weapp:///http://127.0.0.1:48893/appservice/common/vendor.js?t=wechat&s=1776040029386&v=c01e6f70ace82f72049bc4519dbb3205:8560:5)
at Object.toUniError (weapp:///http://127.0.0.1:48893/appservice/utils/utils.js?t=wechat&s=1776040029386&v=170a1b511d4b4583a4ff38ba2eb804a7:77:18)
at AkSupa._callee19$ (weapp:///http://127.0.0.1:48893/appservice/components/supadb/aksupa.js?t=wechat&s=1776040029386&v=b1c88035cdf7fd4aab11f1c0cd809cf3:1878:41)
at s (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1588)
at Generator.<anonymous> (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:2925)
at Generator.next (weapp:///http://127.0.0.1:48893/appservice/@babel/runtime/helpers/regeneratorRuntime.js?forceSync=true:1:1951)
at fulfilled (weapp:///http://127.0.0.1:48893/appservice/common/vendor.js?t=wechat&s=1776040029386&v=c01e6f70ace82f72049bc4519dbb3205:9601:24)(env: Windows,mp,1.06.2504030; lib: 3.14.2)
(anonymous) @ mp.esm.js:485
__f__ @ uni.api.esm.js:591
_callee4$ @ product-edit.uvue:478
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
loadMemberLevels @ product-edit.uvue:464
_callee$ @ product-edit.uvue:374
s @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
(anonymous) @ regeneratorRuntime.js?forceSync=true:1
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
fulfilled @ tslib.es6.js:73
Promise.then (async)
step @ tslib.es6.js:75
(anonymous) @ tslib.es6.js:76
__awaiter @ tslib.es6.js:72
onLoad @ product-edit.uvue:326
callWithErrorHandling @ vue.runtime.esm.js:1356
callWithAsyncErrorHandling @ vue.runtime.esm.js:1363
hook.__weh.hook.__weh @ vue.runtime.esm.js:2461
invokeArrayFns @ uni-shared.es.js:469
callHook @ uni.mp.esm.js:944
methods.onLoad @ uni.mp.esm.js:1542
mp.esm.js:485 [member-levels] status: 404(env: Windows,mp,1.06.2504030; lib: 3.14.2)

View File

@@ -30,6 +30,24 @@ function removeAuthStore(key : string) {
uni.removeStorageSync(key);
}
/**
* 安全读取 UTSJSONObject 或普通 JS 对象的字段。
* 微信小程序编译后 res.header / res.data / options.headers 均为普通 JS 对象,
* 直接调用 .getString() / .set() 会抛 "not a function" 异常。
* 此函数统一处理两种情况:先尝试 getString不可用则 fallback 到 []。
*/
function safeStr(obj: any, key: string): string | null {
if (obj == null) return null
try {
if (typeof obj.getString === 'function') {
const v = obj.getString(key)
return v != null ? String(v) : null
}
} catch (e) {}
const v = obj[key]
return (v != null && v !== '') ? String(v) : null
}
let _accessToken : string | null = null;
let _refreshToken : string | null = null;
let _expiresAt : number | null = null;
@@ -130,84 +148,62 @@ export class AkReq {
static async request(options : AkReqOptions, skipRefresh ?: boolean) : Promise<AkReqResponse<any>> {
// 自动刷新 token
if (skipRefresh != true) {
let apikey : string | null = null;
const headersObj = options.headers;
if (headersObj != null && typeof headersObj.getString === 'function') {
apikey = headersObj.getString('apikey');
}
// safeStr 兼容 plain object 和 UTSJSONObject
const apikey = safeStr(options.headers as any, 'apikey');
await this.refreshTokenIfNeeded(apikey);
}
// 构建新的 headers 对象,确保所有字段都被正确传递
const newHeaders = new UTSJSONObject()
// 首先复制原始 headers
if (options.headers != null) {
const originalHeaders = options.headers
if (typeof originalHeaders.getString === 'function') {
// 复制 apikey
const apikeyStr = originalHeaders.getString('apikey')
if (apikeyStr != null) {
newHeaders.set('apikey', apikeyStr)
}
// 复制 Content-Type
const contentType = originalHeaders.getString('Content-Type')
if (contentType != null) {
newHeaders.set('Content-Type', contentType)
}
// 复制 Prefer
const prefer = originalHeaders.getString('Prefer')
if (prefer != null) {
newHeaders.set('Prefer', prefer)
}
// 复制 Authorization如果存在
const auth = originalHeaders.getString('Authorization')
if (auth != null) {
newHeaders.set('Authorization', auth)
}
// 构建请求 headers:用 safeStr 兼容 plain object / UTSJSONObject
// aksupa.uts 创建的 headers 是“{ key: val } as UTSJSONObject” ,只是类型标注,运行时是 plain object
const origHdr = options.headers as any
const origApikey = safeStr(origHdr, 'apikey')
const origCT = safeStr(origHdr, 'Content-Type') ?? safeStr(origHdr, 'content-type')
const origPrefer = safeStr(origHdr, 'Prefer')
const origAuth = safeStr(origHdr, 'Authorization') ?? safeStr(origHdr, 'authorization')
const origRange = safeStr(origHdr, 'Range')
const origRangeUnit = safeStr(origHdr, 'Range-Unit')
// apikey优先用传入的再 fallback 到全局 SUPA_KEY
const finalApikey = origApikey ?? (SUPA_KEY !== '' ? SUPA_KEY : null)
// AuthorizationSupabase Kong 要求所有请求都带 Authorization: Bearer <jwt>
// 匿名态Bearer <anon_key>登录态Bearer <access_token>
const token = this.getToken()
let finalAuth: string | null = origAuth
let authMode: string = 'none'
if (finalAuth == null) {
if (token != null && token !== '') {
// 登录态:用用户 access_token
finalAuth = `Bearer ${token}`
authMode = 'user-token'
} else if (finalApikey != null && finalApikey !== '') {
// 匿名态:用 anon key 作为 BearerSupabase Kong 必须)
finalAuth = `Bearer ${finalApikey}`
authMode = 'anon-key'
}
} else {
authMode = 'pre-set'
}
// 补齐 apikey (如果 headers 中没有,则直接使用 SUPA_KEY 补全)
if (newHeaders.getString('apikey') == null) {
if (SUPA_KEY != null && SUPA_KEY != "") {
newHeaders.set('apikey', SUPA_KEY)
}
}
// 添加/更新 Authorization
const token = this.getToken();
// 检查原始 headers 中是否存在 Authorization
let existAuth: string | null = null
if (options.headers != null) {
const originalHeaders = options.headers
if (typeof originalHeaders.getString === 'function') {
existAuth = originalHeaders.getString('Authorization')
if (existAuth == null) {
existAuth = originalHeaders.getString('authorization')
}
}
}
// 组装最终 headerplain object所有平台均可正确传递
const currentHeaders: Record<string, string> = {}
if (finalApikey != null && finalApikey !== '') currentHeaders['apikey'] = finalApikey
if (finalAuth != null && finalAuth !== '') currentHeaders['Authorization'] = finalAuth
currentHeaders['Content-Type'] = origCT ?? options.contentType ?? 'application/json'
currentHeaders['Accept'] = 'application/json'
if (origPrefer != null) currentHeaders['Prefer'] = origPrefer
if (origRange != null) currentHeaders['Range'] = origRange
if (origRangeUnit != null) currentHeaders['Range-Unit'] = origRangeUnit
if ((token != null && token != "") && (existAuth == null)) {
newHeaders.set('Authorization', `Bearer ${token}`)
}
// 确保 Content-Type 存在
if (newHeaders.getString('Content-Type') == null) {
const contentType = options.contentType ?? 'application/json'
if (contentType != null && contentType != "") {
newHeaders.set('Content-Type', contentType)
}
}
// 添加 Accept
newHeaders.set('Accept', 'application/json')
console.log('[AkReq.request] headers:', JSON.stringify(newHeaders))
const currentHeaders = newHeaders
// 请求发出前诊断日志脱敏前6位+后4位
const dbgKey = finalApikey != null && finalApikey.length > 14
? finalApikey.substring(0, 6) + '...' + finalApikey.substring(finalApikey.length - 4)
: (finalApikey != null ? '(short)' : '(MISSING!)')
const dbgAuth = finalAuth != null && finalAuth.length > 15
? finalAuth.substring(0, 13) + '...' + finalAuth.substring(finalAuth.length - 4)
: (finalAuth != null ? finalAuth : '(MISSING!)')
console.log('[ak-req]', (options.method ?? 'GET'), options.url)
console.log('[ak-req] apikey:', dbgKey, '| Authorization:', dbgAuth, '| auth-mode:', authMode, '| prefer:', origPrefer ?? '(none)')
const timeout = options.timeout ?? 10000;
const maxRetry = Math.max(0, options.retryCount ?? 0);
@@ -234,15 +230,41 @@ export class AkReq {
}
// 兼容 res.data 可能为 string 或 UTSJSONObject 或 UTSArray
// 先读取响应 content-type判断是否应该 JSON.parse
// res.header 在 MP 环境下是普通 JS 对象(无 getString用括号访问
let respContentType = ''
try {
const hdr = res.header as any
if (hdr != null) {
respContentType = hdr['content-type'] ?? hdr['Content-Type'] ?? ''
}
} catch(e) {}
let data : UTSJSONObject | Array<UTSJSONObject> | null;
if (typeof res.data == 'string') {
const strData = res.data as string;
if (strData.length > 0 && /[^\s]/.test(strData)) {
try {
data = JSON.parse(strData) as UTSJSONObject;
} catch (e) {
// 非 JSON 响应(例如纯文本/空响应/数字等),保持原始字符串,避免 JSON.parse 崩溃
data = new UTSJSONObject({ raw: strData });
const looksLikeHtml = strData.trimStart().startsWith('<')
if (looksLikeHtml || (respContentType.indexOf('text/html') >= 0)) {
// 明确是 HTML直接跳过 JSON.parse
const preview = strData.substring(0, 250)
console.error('[ak-req] non-json response (HTML)')
console.error('[ak-req] status:', res.statusCode)
console.error('[ak-req] content-type:', respContentType)
console.error('[ak-req] raw preview:', preview)
data = new UTSJSONObject({ raw: strData })
} else {
try {
data = JSON.parse(strData) as UTSJSONObject;
} catch (e) {
// 非 JSON 响应(纯文本/数字等)
const preview = strData.substring(0, 250)
console.error('[ak-req] json parse failed')
console.error('[ak-req] status:', res.statusCode)
console.error('[ak-req] content-type:', respContentType)
console.error('[ak-req] raw preview:', preview)
data = new UTSJSONObject({ raw: strData });
}
}
} else {
data = null;
@@ -250,12 +272,14 @@ export class AkReq {
} else if (Array.isArray(res.data)) {
data = res.data as UTSJSONObject[];
} else {
const objData = res.data as UTSJSONObject | null;
data = objData;
// MP 下 res.data 可能是 plain JS object平台已预解析 JSON用 safeStr 兼容
const objData = res.data as any;
data = objData as UTSJSONObject;
if (objData != null) {
const accessToken = objData.getString('access_token');
const refreshTokenNew = objData.getString('refresh_token');
const expiresAt = objData.getNumber('expires_at');
const accessToken = safeStr(objData, 'access_token')
const refreshTokenNew = safeStr(objData, 'refresh_token')
const expiresAtRaw = objData['expires_at']
const expiresAt = expiresAtRaw != null ? Number(expiresAtRaw) : null
if (accessToken !== null && refreshTokenNew !== null && expiresAt !== null) {
AkReq.setToken(accessToken, refreshTokenNew, expiresAt);
}
@@ -298,26 +322,47 @@ export class AkReq {
attempt++;
}
const finalRes = lastRes!!;
// 全局处理 401 未授权:在非 refresh 场景下,清理 token。
// 测试模式下不强制跳登录页,避免影响任意跳转调试。
// 全局处理 401 未授权
if ((finalRes.status === 401) && (skipRefresh !== true)) {
uni.$emit('AUTH_SESSION_EXPIRED', { reason: '401' });
// 401 诊断日志:区分"前端 header 问题"vs"服务端 key/实例问题"
const sentApikey = (currentHeaders as any)['apikey']
const sentAuth = (currentHeaders as any)['Authorization']
const dbgSentKey = sentApikey != null && sentApikey.length > 14
? sentApikey.substring(0, 6) + '...' + sentApikey.substring(sentApikey.length - 4)
: (sentApikey != null ? '(short)' : '(MISSING!)')
const dbgSentAuth = sentAuth != null && sentAuth.length > 15
? sentAuth.substring(0, 13) + '...' + sentAuth.substring(sentAuth.length - 4)
: (sentAuth != null ? '(short)' : '(MISSING!)')
console.error('[ak-req] ★ 401 Unauthorized')
console.error('[ak-req] url:', options.url)
console.error('[ak-req] auth-mode: ' + authMode)
console.error('[ak-req] 发送 apikey:', dbgSentKey)
console.error('[ak-req] 发送 Authorization:', dbgSentAuth)
console.error('[ak-req] response body:', JSON.stringify(finalRes.data))
// 前端 header 检查
if (sentApikey == null || sentApikey === '') {
console.error('[ak-req] ✗ 前端问题apikey 未发送,检查 SUPA_KEY 是否已配置')
} else if (sentAuth == null || sentAuth === '') {
console.error('[ak-req] ✗ 前端问题Authorization 未发送(本次修复后不应再出现此情况)')
} else {
console.error('[ak-req] ✓ 前端 header 已正确发送401 来自服务端')
console.error('[ak-req] 请运维核查以下服务端配置:')
console.error('[ak-req] 1. SUPA_KEY 是否属于 9126 这个实例(不同实例 key 不通用)')
console.error('[ak-req] 2. 9126 和 9127 是否为同一套 docker-compose / 同一实例')
console.error('[ak-req] 3. Kong 是否已 reload 到最新 consumer/key 配置')
console.error('[ak-req] 4. Supabase 实例的 ANON_KEY 环境变量与 SUPA_KEY 是否一致')
console.error('[ak-req] 5. categories/ml_brands/ml_member_levels 是否在该实例 public schema 下')
console.error('[ak-req] 6. RLS policy 是否允许 anon role SELECT')
}
uni.$emit('AUTH_SESSION_EXPIRED', { reason: '401' });
try {
this.clearToken();
uni.showToast({ title: '未授权或登录已过期,请重新登录', icon: 'none' });
// IS_TEST_MODE=true 时不 showToast 和跳转,避免干扰调试
if (IS_TEST_MODE !== true) {
uni.showToast({ title: '未授权或登录已过期,请重新登录', icon: 'none' });
}
} catch (e) {}
try {
// 动态读取配置,避免 ak-req 模块与业务工程强耦合
// const cfg = require('@/ak/config.uts') as any
// const isTest = cfg != null ? (cfg.IS_TEST_MODE === true) : false
const isTest = IS_TEST_MODE
// if (!isTest) {
// uni.reLaunch({ url: '/pages/user/login' });
// }
} catch (e) {
// try { uni.reLaunch({ url: '/pages/user/login' }); } catch (e2) {}
}
}
return finalRes;
}

View File

@@ -6,7 +6,10 @@ export { supa }
import type { OrderOptions } from '@/components/supadb/aksupa.uts'
const OLD_URL = '192.168.1.61:18000'
const NEW_URL = '119.146.131.237:9126'
// const NEW_URL = '119.146.131.237:9126'
// 医疗项目 Supabase 实例地址(与 ak/config.uts 中的 SUPA_URL 保持一致,去掉 http:// 前缀)
const NEW_URL = '119.146.131.237:9127'
function fixImageUrl(url: string | null): string {
if (url == null) return ''