Files
medical-mall/pages/mall/merchant/product-edit.uvue
2026-04-13 11:32:31 +08:00

1013 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 机构端 - 服务/商品编辑页面 -->
<template>
<view class="product-edit-page">
<!-- #ifdef MP-WEIXIN -->
<view style="padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: flex-end; border-bottom: 1rpx solid #eeeeee; box-sizing: border-box; height: calc(88rpx + var(--status-bar-height));">
<view style="display: flex; flex-direction: row; align-items: center; padding: 0 30rpx; height: 88rpx;" @click="uni.navigateBack()">
<text style="font-size: 44rpx; color: #333333; line-height: 1; margin-right: 6rpx;"></text>
<text style="font-size: 28rpx; color: #333333;">返回</text>
</view>
</view>
<!-- #endif -->
<!-- 服务基本信息 -->
<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>
<!-- 商品图片 -->
<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>
<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">关怀价折扣率</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>
<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="#09C39D"
/>
</view>
<view class="switch-item">
<text class="label">新增服务</text>
<switch
:checked="product.is_new"
@change="product.is_new = !product.is_new"
color="#09C39D"
/>
</view>
<view class="switch-item">
<text class="label">推荐服务</text>
<switch
:checked="product.is_featured"
@change="product.is_featured = !product.is_featured"
color="#09C39D"
/>
</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'
import { SUPA_URL, logSupaConfig } from '@/ak/config.uts'
// 严格对齐 categories.sql 真实字段,禁止自行添加或兼容不存在的字段
type CategoryType = {
id: string
name: string
icon: string
description: string
color: string
created_at: string
}
export default {
data() {
return {
productId: '',
isEdit: false,
categories: [] as CategoryType[],
categoryIndex: -1,
selectedCategory: null as CategoryType | null,
product: {
name: '',
subtitle: '',
category_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,
is_vip_discount: true,
vip_discount_rate: '',
description: ''
},
merchantId: ''
}
},
async onLoad(options: any) {
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) {}
}
}
// ── 启动诊断日志 ──
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: '编辑服务' })
} else {
uni.setNavigationBarTitle({ title: '发布服务' })
}
this.initMerchantId()
// ── 先探活,通过后再加载业务数据 ──
const endpointOk = await this.checkSupabaseEndpoint()
if (!endpointOk) {
console.error('[product-edit] Supabase 端点不可用,跳过业务数据加载')
return
}
await this.loadCategories()
if (this.isEdit && productId !== '') {
this.loadProductDetail(productId)
}
},
methods: {
async initMerchantId() {
try {
const session = supa.getSession()
if (session != null && session.user != null) {
this.merchantId = (session.user as any)['id'] != null ? String((session.user as any)['id']) : ''
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
}
} catch (e) {
console.error('获取商户ID失败:', e)
}
},
/**
* 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('*')
.eq('is_active', true)
.order('level_rank', { ascending: true })
.execute()
console.log('[member-levels] response status:', response.status)
if (response.error != null) {
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
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)
}
console.log('[member-levels] loaded:', this.memberLevels.length)
// 如果是编辑模式,还需要加载已有的会员价
if (this.isEdit) {
this.loadMemberPrices()
}
} catch (e) {
console.error('[member-levels] exception:', e)
}
},
// 严格对齐 categories.sql 真实字段id, name, icon, description, color, created_at
async loadCategories() {
this.categories = []
try {
console.log('[categories] 开始从 categories 表读取分类(严格字段映射)...')
const response = await supa
.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('[categories] 查询失败:', response.error)
uni.showToast({ title: '分类加载失败', icon: 'none' })
return
}
const rawData = response.data as any[]
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 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('[categories] 获取分类异常:', e)
uni.showToast({ title: '分类加载异常', icon: 'none' })
}
},
// 编辑态分类回填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')
.eq('is_active', true)
.order('name', { ascending: true })
.execute()
console.log('[brands] response status:', response.status)
if (response.error != null) {
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++) {
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']) : ''
} as BrandType)
}
console.log('[brands] loaded:', this.brands.length)
} catch (e) {
console.error('[brands] exception:', e)
}
},
async loadProductDetail(productId: string) {
try {
uni.showLoading({ title: '加载商品中...' })
const response = await supa
.from('ml_products')
.select('*')
.eq('id', productId)
.single()
.execute()
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.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')
// 尝试回填分类(若此时 categories 已加载则立即生效;若尚未完成则由 loadCategories 结尾再次触发)
if (this.product.category_id) {
this.tryCategoryBackfill()
}
} catch (e) {
uni.hideLoading()
console.error('获取商品详情异常:', e)
uni.showToast({ title: '加载异常: ' + String(e), icon: 'none', duration: 3000 })
}
},
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
},
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 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,
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
}
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' })
}
}
}
}
</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;
padding-left: 16rpx;
border-left: 6rpx solid #09C39D;
}
.section-desc {
font-size: 24rpx;
color: #999;
margin-top: -20rpx;
margin-bottom: 30rpx;
}
.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;
flex-direction: row;
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;
flex-direction: row;
align-items: center;
}
.picker-value {
font-size: 28rpx;
color: #333;
}
.switch-item {
display: flex;
flex-direction: row;
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-direction: row;
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;
flex-direction: row;
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;
flex-direction: row;
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, #A6F1E4 0%, #69DFC2 100%);
color: #fff;
}
</style>