添加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

@@ -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(() => {