合并merchant文件

This commit is contained in:
2026-03-20 15:43:33 +08:00
parent 29f588a2b2
commit 620ae742df
12 changed files with 3477 additions and 0 deletions

View File

@@ -1,4 +1,8 @@
<<<<<<< HEAD
<!-- 商家端 - 商品编辑页面 -->
=======
 <!-- 商家端 - 商品编辑页面 (已修复缓存) -->
>>>>>>> local-backup-root-cyj
<template>
<view class="product-edit-page">
<!-- 商品基本信息 -->
@@ -131,8 +135,28 @@
</view>
</view>
<<<<<<< HEAD
<view class="form-item">
<text class="label">总库存 *</text>
=======
<view class="form-item">
<text class="label">VIP独立折扣</text>
<switch :checked="product.is_vip_discount" @change="e => { product.is_vip_discount = e.detail.value as boolean }" />
</view>
<view class="form-item" v-if="product.is_vip_discount">
<text class="label">VIP折扣率</text>
<input
class="input"
type="digit"
v-model="product.vip_discount_rate"
placeholder="如 0.85 代表 85 折(空代表采用全局)"
/>
</view>
<view class="form-item">
<text class="label">总库存 *</text>
>>>>>>> local-backup-root-cyj
<input
class="input"
type="number"
@@ -152,6 +176,28 @@
</view>
</view>
<<<<<<< HEAD
=======
<!-- 会员阶梯价 -->
<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>
>>>>>>> local-backup-root-cyj
<!-- 商品属性 -->
<view class="section">
<view class="section-title">商品属性</view>
@@ -231,6 +277,17 @@
logo_url: string
}
<<<<<<< HEAD
=======
type MemberLevelType = {
id: string
name: string
level_rank: number
discount_rate: number
price: string // 绑定输入框用
}
>>>>>>> local-backup-root-cyj
export default {
data() {
return {
@@ -242,6 +299,10 @@
brands: [] as BrandType[],
brandIndex: -1,
selectedBrand: null as BrandType | null,
<<<<<<< HEAD
=======
memberLevels: [] as MemberLevelType[],
>>>>>>> local-backup-root-cyj
product: {
name: '',
subtitle: '',
@@ -255,9 +316,17 @@
total_stock: '',
warning_stock: '10',
unit: '件',
<<<<<<< HEAD
is_hot: false,
is_new: false,
is_featured: false,
=======
is_hot: false,
is_new: false,
is_featured: false,
is_vip_discount: true,
vip_discount_rate: '',
>>>>>>> local-backup-root-cyj
description: ''
},
merchantId: ''
@@ -265,15 +334,52 @@
},
onLoad(options: any) {
<<<<<<< HEAD
const productId = options.productId as string
if (productId) {
this.productId = productId
this.isEdit = true
this.loadProductDetail(productId)
=======
let productId = ''
if (options) {
const keys = Object.keys(options as object)
for (let i = 0; i < keys.length; i++) {
if (keys[i] === 'productId') {
productId = String((options as Record<string, any>)[keys[i]])
}
}
if (!productId && options['productId']) {
productId = String(options['productId'])
}
// 兼容某些平台
if (!productId) {
try {
const optsStr = JSON.stringify(options)
const optsObj = JSON.parse(optsStr) as Record<string, any>
if (optsObj['productId']) {
productId = String(optsObj['productId'])
}
} catch(e) {}
}
}
if (productId && productId !== '') {
this.productId = productId
this.isEdit = true
uni.setNavigationBarTitle({ title: '编辑商品' })
this.loadProductDetail(productId)
} else {
uni.setNavigationBarTitle({ title: '添加商品' })
>>>>>>> local-backup-root-cyj
}
this.initMerchantId()
this.loadCategories()
this.loadBrands()
<<<<<<< HEAD
=======
this.loadMemberLevels()
>>>>>>> local-backup-root-cyj
},
methods: {
@@ -281,7 +387,11 @@
try {
const session = supa.getSession()
if (session != null && session.user != null) {
<<<<<<< HEAD
this.merchantId = session.user.getString('id') || ''
=======
this.merchantId = (session.user as any)['id'] != null ? String((session.user as any)['id']) : ''
>>>>>>> local-backup-root-cyj
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
@@ -291,6 +401,78 @@
}
},
<<<<<<< HEAD
=======
async loadMemberLevels() {
try {
const response = await supa
.from('ml_member_levels')
.select('*')
.eq('is_active', true)
.order('level_rank', { ascending: true })
.execute()
if (response.error != null) {
console.error('获取会员等级失败:', response.error)
return
}
const rawData = response.data as any[]
if (rawData == null) return
this.memberLevels = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as any
this.memberLevels.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : '',
level_rank: item['level_rank'] != null ? parseInt(String(item['level_rank'])) : 0,
discount_rate: item['discount_rate'] != null ? parseFloat(String(item['discount_rate'])) : 1.0,
price: ''
} as MemberLevelType)
}
// 如果是编辑模式,还需要加载已有的会员价
if (this.isEdit) {
this.loadMemberPrices()
}
} catch (e) {
console.error('获取会员等级异常:', e)
}
},
async loadMemberPrices() {
try {
const response = await supa
.from('ml_product_member_prices')
.select('*')
.eq('product_id', this.productId)
.execute()
if (response.error != null) {
console.error('获取会员价失败:', response.error)
return
}
const rawData = response.data as any[]
if (rawData == null || rawData.length == 0) return
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
}
}
} catch (e) {
console.error('获取会员价异常:', e)
}
},
>>>>>>> local-backup-root-cyj
async loadCategories() {
try {
const response = await supa
@@ -309,10 +491,17 @@
if (rawData == null) return
for (let i = 0; i < rawData.length; i++) {
<<<<<<< HEAD
const item = rawData[i] as UTSJSONObject
this.categories.push({
id: item.getString('id') || '',
name: item.getString('name') || ''
=======
const item = rawData[i] as any
this.categories.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : ''
>>>>>>> local-backup-root-cyj
} as CategoryType)
}
} catch (e) {
@@ -338,11 +527,19 @@
if (rawData == null) return
for (let i = 0; i < rawData.length; i++) {
<<<<<<< HEAD
const item = rawData[i] as UTSJSONObject
this.brands.push({
id: item.getString('id') || '',
name: item.getString('name') || '',
logo_url: item.getString('logo_url') || ''
=======
const item = rawData[i] as any
this.brands.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : '',
logo_url: item['logo_url'] != null ? String(item['logo_url']) : ''
>>>>>>> local-backup-root-cyj
} as BrandType)
}
} catch (e) {
@@ -352,6 +549,10 @@
async loadProductDetail(productId: string) {
try {
<<<<<<< HEAD
=======
uni.showLoading({ title: '加载商品中...' })
>>>>>>> local-backup-root-cyj
const response = await supa
.from('ml_products')
.select('*')
@@ -359,6 +560,7 @@
.single()
.execute()
<<<<<<< HEAD
if (response.error != null) {
console.error('获取商品详情失败:', response.error)
return
@@ -386,6 +588,47 @@
description: rawData.getString('description') || ''
}
=======
uni.hideLoading()
if (response.error != null) {
console.error('获取详情失败:', response.error)
uni.showToast({ title: '没有找到该商品', icon: 'none' })
return
}
let rawData = response.data as any
if (rawData == null) return
// 防止Supabase某些版本把single()仍返回数组的坑
if (Array.isArray(rawData) && rawData.length > 0) {
rawData = rawData[0]
}
const getStr = (key: string): string => { try { return rawData[key] != null ? String(rawData[key]) : '' } catch(e){ return '' } }
const getBool = (key: string): boolean => { try { return rawData[key] === true || rawData[key] === 'true' } catch(e){ return false } }
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')
this.product.market_price = getStr('market_price')
this.product.cost_price = getStr('cost_price')
this.product.total_stock = getStr('total_stock')
this.product.warning_stock = getStr('warning_stock') || '10'
this.product.unit = getStr('unit') || '件'
this.product.is_hot = getBool('is_hot')
this.product.is_new = getBool('is_new')
this.product.is_featured = getBool('is_featured')
const _isVip = rawData['is_vip_discount']
this.product.is_vip_discount = _isVip == null ? true : getBool('is_vip_discount')
this.product.vip_discount_rate = getStr('vip_discount_rate')
this.product.description = getStr('description')
>>>>>>> local-backup-root-cyj
if (this.product.category_id) {
this.categoryIndex = this.categories.findIndex(c => c.id === this.product.category_id)
if (this.categoryIndex >= 0) {
@@ -400,7 +643,13 @@
}
}
} catch (e) {
<<<<<<< HEAD
console.error('获取商品详情异常:', e)
=======
uni.hideLoading()
console.error('获取商品详情异常:', e)
uni.showToast({ title: '加载异常: ' + String(e), icon: 'none', duration: 3000 })
>>>>>>> local-backup-root-cyj
}
},
@@ -453,6 +702,7 @@
this.product.imageList.splice(index, 1)
},
<<<<<<< HEAD
async saveProduct() {
if (!this.product.name) {
uni.showToast({ title: '请输入商品名称', icon: 'none' })
@@ -533,6 +783,203 @@
}
}
}
=======
async uploadImageToSupa(localPath: string): Promise<string> {
if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
return localPath
}
let ext = '.jpg'
const dotIndex = localPath.lastIndexOf('.')
if (dotIndex > -1) {
ext = localPath.substring(dotIndex).toLowerCase()
}
const uuid = Date.now().toString() + '_' + Math.floor(Math.random() * 1000)
const remotePath = `products/${this.merchantId}_${uuid}${ext}`
try {
const uploadResult = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
if (uploadResult.error != null) {
console.error('上传图片失败:', uploadResult.error)
return localPath
}
return supa.storage.getPublicUrl('zhipao', remotePath)
} catch (e) {
console.error('上传图片异常:', e)
return localPath
}
},
async saveProduct() {
if (!this.product.name) {
uni.showToast({ title: '请输入商品名称', icon: 'none' })
return
}
if (!this.product.category_id) {
uni.showToast({ title: '请选择商品分类', icon: 'none' })
return
}
if (!this.product.base_price) {
uni.showToast({ title: '请输入销售价', icon: 'none' })
return
}
if (!this.product.total_stock) {
uni.showToast({ title: '请输入总库存', icon: 'none' })
return
}
if (this.product.is_vip_discount && this.product.vip_discount_rate !== '') {
const rate = parseFloat(this.product.vip_discount_rate)
if (isNaN(rate) || rate <= 0 || rate > 1) {
uni.showToast({ title: 'VIP折扣率需在0~1之间', icon: 'none' })
return
}
}
uni.showLoading({ title: '保存中...' })
try {
let finalMainImage = this.product.main_image_url
if (finalMainImage != '') {
finalMainImage = await this.uploadImageToSupa(finalMainImage)
}
const finalImageList = [] as string[]
for (let i = 0; i < this.product.imageList.length; i++) {
const img = await this.uploadImageToSupa(this.product.imageList[i])
finalImageList.push(img)
}
const imageUrlsStr = JSON.stringify(finalImageList)
const productData = {
merchant_id: this.merchantId,
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,
market_price: this.product.market_price ? parseFloat(this.product.market_price) : null,
cost_price: this.product.cost_price ? parseFloat(this.product.cost_price) : null,
total_stock: parseInt(this.product.total_stock),
available_stock: parseInt(this.product.total_stock),
is_hot: this.product.is_hot,
is_new: this.product.is_new,
is_featured: this.product.is_featured,
is_vip_discount: this.product.is_vip_discount,
vip_discount_rate: this.product.vip_discount_rate ? parseFloat(this.product.vip_discount_rate) : null,
description: this.product.description,
status: 1,
updated_at: new Date().toISOString()
} as UTSJSONObject
let response : any = null
if (this.isEdit) {
const updateData = {} as UTSJSONObject
const keys = UTSJSONObject.keys(productData)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key != 'status') {
updateData[key] = productData[key]
}
}
console.log('执行产品更新, ID:', this.productId)
const updateResponse = await supa
.from('ml_products')
.update(updateData)
.eq('id', this.productId)
.execute()
if (updateResponse.error != null) {
throw new Error('产品更新失败: ' + String(updateResponse.error!.message))
}
response = updateResponse
} else {
productData['created_at'] = new Date().toISOString()
productData['product_code'] = 'P' + Date.now().toString()
console.log('执行新产品插入')
const insertResponse = await supa
.from('ml_products')
.insert(productData)
.execute()
if (insertResponse.error != null) {
throw new Error('产品发布失败: ' + String(insertResponse.error!.message))
}
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(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
uni.hideLoading()
console.error('保存商品异常:', e)
uni.showToast({ title: '保存异常: ' + String(e), icon: 'none' })
}
}
}
}
>>>>>>> local-backup-root-cyj
</script>
<style>
@@ -557,6 +1004,16 @@
border-bottom: 1rpx solid #f5f5f5;
}
<<<<<<< HEAD
=======
.section-desc {
font-size: 24rpx;
color: #999;
margin-top: -20rpx;
margin-bottom: 30rpx;
}
>>>>>>> local-backup-root-cyj
.form-item {
margin-bottom: 30rpx;
}
@@ -713,3 +1170,9 @@
color: #fff;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj