consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题
This commit is contained in:
715
pages/mall/merchant/product-edit.uvue
Normal file
715
pages/mall/merchant/product-edit.uvue
Normal file
@@ -0,0 +1,715 @@
|
||||
<!-- 商家端 - 商品编辑页面 -->
|
||||
<template>
|
||||
<view class="product-edit-page">
|
||||
<!-- 商品基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品名称 *</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.name"
|
||||
placeholder="请输入商品名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品副标题</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.subtitle"
|
||||
placeholder="请输入商品副标题"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品分类 *</text>
|
||||
<picker
|
||||
class="picker"
|
||||
:range="categories"
|
||||
range-key="name"
|
||||
:value="categoryIndex"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ selectedCategory?.name || '请选择分类' }}
|
||||
</view>
|
||||
</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>
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品图片</view>
|
||||
|
||||
<view class="image-section">
|
||||
<text class="label">主图 *</text>
|
||||
<view class="image-grid">
|
||||
<view class="image-item main-image" @click="chooseMainImage">
|
||||
<image v-if="product.main_image_url" :src="product.main_image_url" mode="aspectFill" class="preview-image"/>
|
||||
<view v-else class="add-image">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="image-section">
|
||||
<text class="label">轮播图</text>
|
||||
<view class="image-grid">
|
||||
<view
|
||||
v-for="(img, index) in product.imageList"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image :src="img" mode="aspectFill" class="preview-image"/>
|
||||
<view class="delete-btn" @click="removeImage(index)">×</view>
|
||||
</view>
|
||||
<view class="image-item add-image" @click="chooseImages" v-if="product.imageList.length < 5">
|
||||
+
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 价格库存 -->
|
||||
<view class="section">
|
||||
<view class="section-title">价格库存</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">销售价 *</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.base_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">市场价</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.market_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">成本价</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.cost_price"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">总库存 *</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="product.total_stock"
|
||||
placeholder="0"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">库存预警</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="product.warning_stock"
|
||||
placeholder="库存低于此值提醒"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品属性 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品属性</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品单位</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="product.unit"
|
||||
placeholder="如: 件, 盒, 箱"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">热卖商品</text>
|
||||
<switch
|
||||
:checked="product.is_hot"
|
||||
@change="product.is_hot = !product.is_hot"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">新品上架</text>
|
||||
<switch
|
||||
:checked="product.is_new"
|
||||
@change="product.is_new = !product.is_new"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="switch-item">
|
||||
<text class="label">推荐商品</text>
|
||||
<switch
|
||||
:checked="product.is_featured"
|
||||
@change="product.is_featured = !product.is_featured"
|
||||
color="#007AFF"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品详情 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品详情</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">商品描述</text>
|
||||
<textarea
|
||||
class="textarea"
|
||||
v-model="product.description"
|
||||
placeholder="请输入商品详细描述"
|
||||
:maxlength="2000"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-bar">
|
||||
<view class="submit-btn primary" @click="saveProduct">
|
||||
{{ isEdit ? '保存修改' : '发布商品' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CategoryType = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type BrandType = {
|
||||
id: string
|
||||
name: string
|
||||
logo_url: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
productId: '',
|
||||
isEdit: false,
|
||||
categories: [] as CategoryType[],
|
||||
categoryIndex: -1,
|
||||
selectedCategory: null as CategoryType | null,
|
||||
brands: [] as BrandType[],
|
||||
brandIndex: -1,
|
||||
selectedBrand: null as BrandType | null,
|
||||
product: {
|
||||
name: '',
|
||||
subtitle: '',
|
||||
category_id: '',
|
||||
brand_id: '',
|
||||
main_image_url: '',
|
||||
imageList: [] as string[],
|
||||
base_price: '',
|
||||
market_price: '',
|
||||
cost_price: '',
|
||||
total_stock: '',
|
||||
warning_stock: '10',
|
||||
unit: '件',
|
||||
is_hot: false,
|
||||
is_new: false,
|
||||
is_featured: false,
|
||||
description: ''
|
||||
},
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
const productId = options.productId as string
|
||||
if (productId) {
|
||||
this.productId = productId
|
||||
this.isEdit = true
|
||||
this.loadProductDetail(productId)
|
||||
}
|
||||
this.initMerchantId()
|
||||
this.loadCategories()
|
||||
this.loadBrands()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.categories.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || ''
|
||||
} as CategoryType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取分类异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadBrands() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_brands')
|
||||
.select('id, name, logo_url')
|
||||
.eq('is_active', true)
|
||||
.order('name', { ascending: true })
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取品牌失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.brands.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
logo_url: item.getString('logo_url') || ''
|
||||
} as BrandType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取品牌异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadProductDetail(productId: string) {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('id', productId)
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取商品详情失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as UTSJSONObject
|
||||
if (rawData == null) return
|
||||
|
||||
this.product = {
|
||||
name: rawData.getString('name') || '',
|
||||
subtitle: rawData.getString('subtitle') || '',
|
||||
category_id: rawData.getString('category_id') || '',
|
||||
brand_id: rawData.getString('brand_id') || '',
|
||||
main_image_url: rawData.getString('main_image_url') || '',
|
||||
imageList: this.parseImageUrls(rawData.getString('image_urls')),
|
||||
base_price: rawData.getString('base_price') || '',
|
||||
market_price: rawData.getString('market_price') || '',
|
||||
cost_price: rawData.getString('cost_price') || '',
|
||||
total_stock: rawData.getString('total_stock') || '',
|
||||
warning_stock: rawData.getString('warning_stock') || '10',
|
||||
unit: rawData.getString('unit') || '件',
|
||||
is_hot: rawData.getBoolean('is_hot') || false,
|
||||
is_new: rawData.getBoolean('is_new') || false,
|
||||
is_featured: rawData.getBoolean('is_featured') || false,
|
||||
description: rawData.getString('description') || ''
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取商品详情异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
parseImageUrls(urlsStr: string): string[] {
|
||||
if (!urlsStr) return []
|
||||
try {
|
||||
const arr = JSON.parse(urlsStr)
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
onCategoryChange(e: any) {
|
||||
const index = e.detail.value as number
|
||||
this.categoryIndex = index
|
||||
this.selectedCategory = this.categories[index]
|
||||
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,
|
||||
sizeType: ['compressed'],
|
||||
success: (res) => {
|
||||
this.product.main_image_url = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
chooseImages() {
|
||||
const remainCount = 5 - this.product.imageList.length
|
||||
uni.chooseImage({
|
||||
count: remainCount,
|
||||
sizeType: ['compressed'],
|
||||
success: (res) => {
|
||||
this.product.imageList = [...this.product.imageList, ...res.tempFilePaths]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
removeImage(index: number) {
|
||||
this.product.imageList.splice(index, 1)
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
const imageUrlsStr = JSON.stringify(this.product.imageList)
|
||||
|
||||
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: this.product.main_image_url,
|
||||
image_urls: imageUrlsStr,
|
||||
base_price: parseFloat(this.product.base_price),
|
||||
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),
|
||||
warning_stock: parseInt(this.product.warning_stock) || 10,
|
||||
unit: this.product.unit,
|
||||
is_hot: this.product.is_hot,
|
||||
is_new: this.product.is_new,
|
||||
is_featured: this.product.is_featured,
|
||||
description: this.product.description,
|
||||
status: 1,
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
let response
|
||||
if (this.isEdit) {
|
||||
response = await supa
|
||||
.from('ml_products')
|
||||
.update(productData)
|
||||
.eq('id', this.productId)
|
||||
.execute()
|
||||
} else {
|
||||
productData['created_at'] = new Date().toISOString()
|
||||
response = await supa
|
||||
.from('ml_products')
|
||||
.insert(productData)
|
||||
.execute()
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('保存商品失败:', response.error)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('保存商品异常:', e)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.product-edit-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.price-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.price-input .unit {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.price-input .input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.switch-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60rpx;
|
||||
color: #999;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 5rpx;
|
||||
right: 5rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
|
||||
.submit-btn.primary {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user