添加mock数据
This commit is contained in:
@@ -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/非 JSON:SUPA_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:8000(Connect 显示值不代表前端请求地址)')
|
||||
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 确认表名是否为 categories,schema 是否为 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(() => {
|
||||
|
||||
Reference in New Issue
Block a user