增加添加商品功能
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>商品类型:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="radio-group">
|
||||
@@ -35,81 +35,112 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>商品名称:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="input-box">
|
||||
<input class="real-input" value="UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060" />
|
||||
<text class="count">36/80</text>
|
||||
<input class="real-input" v-model="formData.name" placeholder="请输入商品名称" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>单位:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="input-box small">
|
||||
<input class="real-input" value="件" />
|
||||
<text class="count">1/5</text>
|
||||
<input class="real-input" v-model="formData.attributes.unit" placeholder="例: 件" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item multi-line">
|
||||
<view class="label"><text class="required">*</text><text>商品轮播图:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="image-uploader">
|
||||
<view v-for="(img, i) in carouselImages" :key="i" class="img-item">
|
||||
<view v-for="(img, i) in formData.image_urls" :key="i" class="img-item">
|
||||
<image :src="img" mode="aspectFill" />
|
||||
<view class="img-close">×</view>
|
||||
<view v-if="i === 0" class="main-tag">主图</view>
|
||||
<view class="img-close" @click="removeImage(i)">×</view>
|
||||
</view>
|
||||
<view class="upload-btn">
|
||||
<view class="upload-btn" @click="uploadImage" v-if="formData.image_urls.length < 10">
|
||||
<text class="icon">+</text>
|
||||
<text class="upload-txt">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="tip">建议尺寸:800*800,可拖拽改变图片顺序,默认首张图为主图,最多上传10张</text>
|
||||
<text class="tip">建议尺寸:800*800,默认首张图为主图,最多上传10张</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item multi-line">
|
||||
<view class="label"><text>添加视频:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="upload-btn v-btn">
|
||||
<text class="v-icon">📹</text>
|
||||
<view class="image-uploader">
|
||||
<view v-for="(vid, i) in formData.video_urls" :key="i" class="img-item video-item">
|
||||
<video :src="vid" style="width: 100%; height: 100%; border-radius: 4px;" :controls="false"></video>
|
||||
<view class="img-close" @click="removeVideo(i)">×</view>
|
||||
</view>
|
||||
<view class="upload-btn v-btn" @click="uploadVideo" v-if="formData.video_urls.length < 1">
|
||||
<text class="v-icon">📹</text>
|
||||
<text class="upload-txt">上传视频</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="tip">建议时长:9~30秒,视频宽高比16:9</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>商品分类:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="tag-selector">
|
||||
<view v-for="tag in categories" :key="tag" class="tag-item">
|
||||
<text>{{ tag }}</text>
|
||||
<text class="close">×</text>
|
||||
<picker :range="categories" @change="onCategoryChange">
|
||||
<view class="mock-btn-select">
|
||||
<text>{{ categoryName || '请选择分类' }}</text>
|
||||
</view>
|
||||
<text class="add-link">新增分类</text>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>商品售价:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="input-box small">
|
||||
<input class="real-input" type="number" v-model="formData.base_price" placeholder="请输入售价" />
|
||||
<text class="unit">元</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="label"><text>商品标签:</text></view>
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text class="required">*</text><text>商品库存:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="mock-btn-select">选择标签</view>
|
||||
<view class="input-box small">
|
||||
<input class="real-input" type="number" v-model="formData.available_stock" placeholder="请输入库存" />
|
||||
<text class="unit">件</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text>商品标签:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="tag-selector">
|
||||
<view v-for="(tg, index) in formData.tags" :key="index" class="tag-item">
|
||||
<text>{{ tg }}</text>
|
||||
<text class="close" @click="removeTag(index)">×</text>
|
||||
</view>
|
||||
<text class="add-link" @click="addTag">+添加标签</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item align-center">
|
||||
<view class="label"><text>商品状态:</text></view>
|
||||
<view class="input-wrap">
|
||||
<view class="radio-group-simple">
|
||||
<view class="radio-simple on">
|
||||
<view class="radio-simple" :class="{ on: formData.status === 1 }" @click="formData.status = 1">
|
||||
<text class="dot"></text>
|
||||
<text>上架</text>
|
||||
</view>
|
||||
<view class="radio-simple">
|
||||
<view class="radio-simple" :class="{ on: formData.status === 2 }" @click="formData.status = 2">
|
||||
<text class="dot"></text>
|
||||
<text>下架</text>
|
||||
</view>
|
||||
@@ -119,30 +150,279 @@
|
||||
</view>
|
||||
|
||||
<view class="footer-btns">
|
||||
<button class="btn-next">下一步</button>
|
||||
<button class="btn-save">保存</button>
|
||||
<button class="btn-save" @click="saveDraftProduct">草稿箱</button>
|
||||
<button class="btn-next" @click="saveAndNext">下一步</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||
|
||||
const activeStep = ref(0)
|
||||
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
|
||||
|
||||
const carouselImages = ref([
|
||||
'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
'https://img2.baidu.com/it/u=3025255470,3051061730&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
'https://img2.baidu.com/it/u=3775079632,546700868&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
|
||||
])
|
||||
const formData = ref({
|
||||
id: '',
|
||||
merchant_id: '',
|
||||
category_id: '',
|
||||
name: '',
|
||||
base_price: 0,
|
||||
available_stock: 0,
|
||||
total_stock: 0,
|
||||
main_image_url: '',
|
||||
image_urls: [] as string[],
|
||||
video_urls: [] as string[],
|
||||
status: 1, // 1上架 2下架 3草稿 4删除
|
||||
tags: [] as string[],
|
||||
attributes: { unit: '件' } as Record<string, any>,
|
||||
published_at: null as string | null
|
||||
})
|
||||
|
||||
const categories = ref(['生活家居', '运动专区 / 361', '运动专区 / 特步', '运动专区 / 匹克'])
|
||||
const categories = ref(['361度', '特步', '匹克', '生活家居'])
|
||||
const categoryName = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await ensureSupabaseReady()
|
||||
const mId = uni.getStorageSync('merchant_id') as string | null
|
||||
if (!mId) {
|
||||
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
formData.value.merchant_id = mId
|
||||
|
||||
const editId = uni.getStorageSync('edit_product_id') as string | null
|
||||
if (editId) {
|
||||
uni.removeStorageSync('edit_product_id')
|
||||
await fetchProductDetail(editId, mId)
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchProductDetail(id: string, mId: string) {
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.eq('merchant_id', mId)
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
if (data) {
|
||||
formData.value.id = data.id
|
||||
formData.value.name = data.name || ''
|
||||
formData.value.base_price = data.base_price || 0
|
||||
formData.value.available_stock = data.available_stock || 0
|
||||
formData.value.total_stock = data.total_stock || data.available_stock || 0
|
||||
formData.value.status = data.status || 1
|
||||
formData.value.main_image_url = data.main_image_url || ''
|
||||
formData.value.image_urls = data.image_urls || []
|
||||
formData.value.video_urls = data.video_urls || []
|
||||
formData.value.tags = data.tags || []
|
||||
|
||||
if (data.attributes && typeof data.attributes === 'object') {
|
||||
const attrs = data.attributes as Record<string, any>
|
||||
formData.value.attributes.unit = attrs['unit'] || '件'
|
||||
}
|
||||
|
||||
// Try to map category
|
||||
formData.value.category_id = data.category_id || ''
|
||||
categoryName.value = data.category_id ? '已绑定分类' : ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取详情失败', e)
|
||||
uni.showToast({ title: '获取商品信息失败,或者无权限编辑', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function onCategoryChange(e: any) {
|
||||
const v = e.detail.value as number
|
||||
categoryName.value = categories.value[v]
|
||||
// In a real project, this maps to an actual category ID, but for now we use a mock one
|
||||
formData.value.category_id = `cat_${v}`
|
||||
}
|
||||
|
||||
function addTag() {
|
||||
uni.showModal({
|
||||
title: '添加标签',
|
||||
editable: true,
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
formData.value.tags.push(res.content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeTag(index: number) {
|
||||
formData.value.tags.splice(index, 1)
|
||||
}
|
||||
|
||||
function uploadImage() {
|
||||
const maxCount = 10 - formData.value.image_urls.length
|
||||
if (maxCount <= 0) return
|
||||
uni.chooseImage({
|
||||
count: maxCount,
|
||||
success: (res) => {
|
||||
const paths = res.tempFilePaths as string[]
|
||||
formData.value.image_urls.push(...paths)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeImage(index: number) {
|
||||
formData.value.image_urls.splice(index, 1)
|
||||
}
|
||||
|
||||
function uploadVideo() {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (res) => {
|
||||
formData.value.video_urls.push(res.tempFilePath as string)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeVideo(index: number) {
|
||||
formData.value.video_urls.splice(index, 1)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
openRoute('product_productList')
|
||||
}
|
||||
|
||||
// 通用上传方法
|
||||
async function uploadToSupabase(filePath: string): Promise<string> {
|
||||
if (filePath.startsWith('http') && !filePath.startsWith('blob:')) {
|
||||
return filePath
|
||||
}
|
||||
const extMatch = filePath.match(/\.(\w+)$/)
|
||||
const ext = extMatch ? extMatch[1] : 'png'
|
||||
const uuid = Math.random().toString(36).substring(2, 10)
|
||||
const remotePath = `products/${formData.value.merchant_id}_${Date.now()}_${uuid}.${ext}`
|
||||
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
const { data, error } = await supa.storage.from('zhipao').upload(remotePath, filePath, {})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const urlKey = typeof data === 'object' ? (data as any)['Key'] || (data as any)['path'] : ''
|
||||
// fallback logic, generally Supabase uses 'storage/v1/object/public/bucket/' + path
|
||||
if (urlKey) {
|
||||
return `https://ak3.oulog.com/storage/v1/object/public/${urlKey}`
|
||||
}
|
||||
return ''
|
||||
} catch (e: any) {
|
||||
console.error('上传文件失败:', e)
|
||||
throw new Error(e.message || '上传异常')
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAllCurrentFiles() {
|
||||
const newImages = [] as string[]
|
||||
for (const p of formData.value.image_urls) {
|
||||
const u = await uploadToSupabase(p)
|
||||
if(u) newImages.push(u)
|
||||
}
|
||||
formData.value.image_urls = newImages
|
||||
|
||||
const newVideos = [] as string[]
|
||||
for (const p of formData.value.video_urls) {
|
||||
const u = await uploadToSupabase(p)
|
||||
if(u) newVideos.push(u)
|
||||
}
|
||||
formData.value.video_urls = newVideos
|
||||
}
|
||||
|
||||
async function doSaveProduct(targetStatus: number) {
|
||||
if (!formData.value.name) return uni.showToast({ title: '请输入商品名称', icon: 'none' })
|
||||
if (!formData.value.category_id) return uni.showToast({ title: '请选择商品分类', icon: 'none' })
|
||||
if (formData.value.image_urls.length === 0) return uni.showToast({ title: '请至少上传一张商品图', icon: 'none' })
|
||||
|
||||
// 必须获取正确的 merchant_id,并且存在
|
||||
if (!formData.value.merchant_id || formData.value.merchant_id.startsWith('merchant_123')) {
|
||||
const mId = uni.getStorageSync('merchant_id') as string | null
|
||||
if (!mId) {
|
||||
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
formData.value.merchant_id = mId
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
// 1. 上传文件拿到正式 URL
|
||||
await uploadAllCurrentFiles()
|
||||
|
||||
// 2. 组装 Payload
|
||||
// 根据 CRMEB: 1:上架 2:下架 3:草稿 4:删除
|
||||
formData.value.status = targetStatus
|
||||
if (targetStatus === 1) {
|
||||
formData.value.published_at = new Date().toISOString()
|
||||
} else if (targetStatus === 3) {
|
||||
formData.value.published_at = null
|
||||
}
|
||||
|
||||
const payload = {
|
||||
merchant_id: formData.value.merchant_id,
|
||||
name: formData.value.name,
|
||||
category_id: formData.value.category_id || null,
|
||||
base_price: Number(formData.value.base_price),
|
||||
main_image_url: formData.value.image_urls.length > 0 ? formData.value.image_urls[0] : '', // 首图同步
|
||||
image_urls: formData.value.image_urls,
|
||||
video_urls: formData.value.video_urls,
|
||||
available_stock: Number(formData.value.available_stock),
|
||||
total_stock: Number(formData.value.total_stock) || Number(formData.value.available_stock),
|
||||
status: formData.value.status,
|
||||
tags: formData.value.tags,
|
||||
attributes: formData.value.attributes,
|
||||
published_at: formData.value.published_at
|
||||
}
|
||||
|
||||
let result;
|
||||
if (formData.value.id) {
|
||||
// 必须加上 merchant_id 检查,防止越权串数据
|
||||
result = await supa.from('ml_products')
|
||||
.update(payload)
|
||||
.eq('id', formData.value.id)
|
||||
.eq('merchant_id', formData.value.merchant_id)
|
||||
} else {
|
||||
result = await supa.from('ml_products').insert([payload])
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (result && result.error) {
|
||||
console.error('Save Product DB Error:', result.error)
|
||||
uni.showToast({ title: '保存失败: ' + result.error.message, icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
uni.$emit('REFRESH_PRODUCT_LIST')
|
||||
setTimeout(() => goBack(), 1000)
|
||||
}
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
console.error('保存报错:', error)
|
||||
uni.showToast({ title: '发生错误: ' + (error.message || ''), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 草稿箱
|
||||
function saveDraftProduct() {
|
||||
doSaveProduct(3) // 3 表示草稿
|
||||
}
|
||||
|
||||
// 下一步/发布
|
||||
function saveAndNext() {
|
||||
// 这里如果原本要求点击下一步是发布上架,则传入 1
|
||||
doSaveProduct(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -216,15 +496,28 @@ function goBack() {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
&.multi-line {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
padding-top: 8px;
|
||||
margin-right: 20px;
|
||||
line-height: 20px;
|
||||
.required { color: #f5222d; margin-right: 4px; }
|
||||
}
|
||||
|
||||
&.multi-line .label {
|
||||
padding-top: 8px; /* 顶部对齐缓冲 */
|
||||
}
|
||||
|
||||
.input-wrap { flex: 1; }
|
||||
}
|
||||
|
||||
@@ -267,7 +560,7 @@ function goBack() {
|
||||
height: 36px;
|
||||
&.small { width: 150px; }
|
||||
.real-input { flex: 1; font-size: 14px; color: #333; }
|
||||
.count { font-size: 12px; color: #bfbfbf; }
|
||||
.count, .unit { font-size: 14px; color: #666; margin-left: 8px; }
|
||||
}
|
||||
|
||||
.image-uploader {
|
||||
@@ -277,21 +570,37 @@ function goBack() {
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
.img-item {
|
||||
width: 80px; height: 80px; position: relative;
|
||||
width: 100px; height: 100px; position: relative; border-radius: 4px; overflow: visible;
|
||||
border: 1px solid #e8e8e8;
|
||||
background-color: #f9f9f9;
|
||||
image { width: 100%; height: 100%; border-radius: 4px; }
|
||||
.img-close {
|
||||
position: absolute; right: -6px; top: -6px; width: 16px; height: 16px;
|
||||
position: absolute; right: -8px; top: -8px; width: 20px; height: 20px;
|
||||
background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 12px;
|
||||
cursor: pointer; z-index: 2;
|
||||
}
|
||||
.main-tag {
|
||||
position: absolute; left: 0; bottom: 0; background: rgba(24,144,255,0.8);
|
||||
color: #fff; font-size: 12px; padding: 2px 6px; border-top-right-radius: 4px; border-bottom-left-radius: 4px;
|
||||
}
|
||||
}
|
||||
.video-item {
|
||||
width: 150px; height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 80px; height: 80px; border: 1px dashed #d9d9d9; border-radius: 4px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
.icon { font-size: 24px; color: #999; }
|
||||
&.v-btn { width: 64px; height: 64px; margin-bottom: 8px; .v-icon { font-size: 24px; } }
|
||||
width: 100px; height: 100px; border: 1px dashed #d9d9d9; border-radius: 4px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer;
|
||||
background-color: #fafafa;
|
||||
&:hover { border-color: #1890ff; }
|
||||
.icon { font-size: 28px; color: #999; margin-bottom: 4px; }
|
||||
.upload-txt { font-size: 12px; color: #666; }
|
||||
&.v-btn {
|
||||
width: 150px; height: 100px;
|
||||
.v-icon { font-size: 24px; color: #999; margin-bottom: 4px; }
|
||||
}
|
||||
}
|
||||
|
||||
.tip { font-size: 12px; color: #999; }
|
||||
@@ -303,17 +612,17 @@ function goBack() {
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
.tag-item {
|
||||
background: #f5f5f5; border: 1px solid #d9d9d9; padding: 2px 10px; border-radius: 4px;
|
||||
background: #f5f5f5; border: 1px solid #d9d9d9; padding: 4px 10px; border-radius: 4px;
|
||||
display: flex; flex-direction: row; align-items: center; gap: 6px;
|
||||
font-size: 14px; color: #666;
|
||||
.close { color: #999; cursor: pointer; }
|
||||
.close { color: #999; cursor: pointer; font-size: 16px; margin-left: 2px; }
|
||||
}
|
||||
.add-link { font-size: 14px; color: #1890ff; cursor: pointer; }
|
||||
.add-link { font-size: 14px; color: #1890ff; cursor: pointer; padding: 4px 0; }
|
||||
}
|
||||
|
||||
.mock-btn-select {
|
||||
border: 1px solid #d9d9d9; border-radius: 4px; padding: 6px 16px;
|
||||
font-size: 14px; color: #666; display: inline-block;
|
||||
border: 1px solid #d9d9d9; border-radius: 4px; padding: 0 16px; height: 36px;
|
||||
font-size: 14px; color: #333; display: flex; align-items: center; cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-group-simple {
|
||||
@@ -321,7 +630,7 @@ function goBack() {
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
.radio-simple {
|
||||
display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 14px; color: #666;
|
||||
display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 14px; color: #666; cursor: pointer;
|
||||
.dot { width: 14px; height: 14px; border: 1px solid #d9d9d9; border-radius: 50%; position: relative; }
|
||||
&.on {
|
||||
color: #1890ff;
|
||||
@@ -341,7 +650,8 @@ function goBack() {
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding-bottom: 40px;
|
||||
.btn-next { background: #1890ff; color: #fff; border: none; padding: 0 24px; height: 40px; border-radius: 4px; }
|
||||
.btn-save { background: #fff; color: #1890ff; border: 1px solid #1890ff; padding: 0 24px; height: 40px; border-radius: 4px; }
|
||||
.btn-next { background: #1890ff; color: #fff; border: none; padding: 0 24px; height: 40px; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
||||
.btn-save { background: #fff; color: #333; border: 1px solid #d9d9d9; padding: 0 24px; height: 40px; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
||||
.btn-save:hover { color: #1890ff; border-color: #1890ff; }
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
pages/mall/admin/product/product-management/edit_tmp.txt
Normal file
BIN
pages/mall/admin/product/product-management/edit_tmp.txt
Normal file
Binary file not shown.
@@ -162,60 +162,82 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import StatusSwitch from '@/components/StatusSwitch.uvue'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance'
|
||||
|
||||
const total = ref(49)
|
||||
const total = ref(0)
|
||||
const activeStatus = ref('selling')
|
||||
const activeDropdownId = ref<number | null>(null)
|
||||
|
||||
const statusTabs = ref([
|
||||
{ key: 'selling', label: '出售中的商品', count: 49 },
|
||||
{ key: 'warehouse', label: '仓库中的商品', count: 4 },
|
||||
{ key: 'soldout', label: '已经售罄商品', count: 11 },
|
||||
{ key: 'alarm', label: '警戒库存商品', count: 27 },
|
||||
{ key: 'recycle', label: '回收站的商品', count: 176 },
|
||||
{ key: 'selling', label: '出售中的商品', count: 0 },
|
||||
{ key: 'warehouse', label: '仓库中的商品', count: 0 },
|
||||
{ key: 'soldout', label: '已经售罄商品', count: 0 },
|
||||
{ key: 'alarm', label: '警戒库存商品', count: 0 },
|
||||
{ key: 'recycle', label: '回收站的商品', count: 0 },
|
||||
])
|
||||
|
||||
const productList = ref([
|
||||
{
|
||||
id: 963,
|
||||
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
|
||||
activities: ['kj', 'pt'],
|
||||
typeName: '普通商品',
|
||||
price: '0.01',
|
||||
sales: 639,
|
||||
stock: 1602,
|
||||
sort: 9999,
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 108,
|
||||
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
|
||||
name: 'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师师单椅单沙头层牛皮/单椅',
|
||||
activities: ['pt', 'ms'],
|
||||
typeName: '普通商品',
|
||||
price: '7580.00',
|
||||
sales: 14,
|
||||
stock: 16638,
|
||||
sort: 9999,
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水蓝/传奇墨水蓝/白 XL',
|
||||
activities: ['kj', 'pt', 'ms'],
|
||||
typeName: '普通商品',
|
||||
price: '100.00',
|
||||
sales: 841,
|
||||
stock: 2318,
|
||||
sort: 9998,
|
||||
status: 1
|
||||
const productList = ref<any[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
fetchProducts()
|
||||
uni.$on('REFRESH_PRODUCT_LIST', () => {
|
||||
fetchProducts()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('REFRESH_PRODUCT_LIST')
|
||||
})
|
||||
|
||||
// 从 ml_products 表中获取商品数据
|
||||
async function fetchProducts() {
|
||||
await ensureSupabaseReady()
|
||||
|
||||
// 从本地缓存获取 current merchant_id
|
||||
const currentMerchantId = uni.getStorageSync('merchant_id') as string | null
|
||||
|
||||
if (!currentMerchantId) {
|
||||
uni.showToast({ title: '未获取到商家信息,请重新登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
])
|
||||
|
||||
try {
|
||||
const { data, error, count } = await supa
|
||||
.from('ml_products')
|
||||
.select('id, name, main_image_url, base_price, available_stock, status, created_at', { count: 'exact' })
|
||||
.eq('merchant_id', currentMerchantId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (error) {
|
||||
console.error('Fetch products error:', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (data) {
|
||||
productList.value = data.map((item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
image: item.main_image_url || '',
|
||||
name: item.name || '未命名商品',
|
||||
activities: [],
|
||||
typeName: '普通商品',
|
||||
price: item.base_price !== null ? Number(item.base_price).toFixed(2) : '0.00',
|
||||
sales: 0,
|
||||
stock: item.available_stock || 0,
|
||||
sort: 0,
|
||||
status: item.status || 0
|
||||
}
|
||||
})
|
||||
total.value = count || data.length
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取商品列表失败:', err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function getActivityName(tag: string): string {
|
||||
if (tag === 'kj') return '砍价'
|
||||
@@ -225,6 +247,11 @@ function getActivityName(tag: string): string {
|
||||
}
|
||||
|
||||
function goEdit(id: number | null) {
|
||||
if (id !== null) {
|
||||
uni.setStorageSync('edit_product_id', id)
|
||||
} else {
|
||||
uni.removeStorageSync('edit_product_id')
|
||||
}
|
||||
openRoute('product_edit')
|
||||
}
|
||||
|
||||
|
||||
2
pages/mall/admin/sql/ml_products_rows.sql
Normal file
2
pages/mall/admin/sql/ml_products_rows.sql
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user