Files
2025-09-29 21:38:32 +08:00

224 lines
8.9 KiB
Vue
Raw Permalink 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>
<scroll-view scroll-y class="page">
<!-- 顶部标题与操作提示 -->
<view class="hero small">
<text class="title">编辑货品</text>
<text class="sub">完善基础信息与价格</text>
</view>
<view v-if="form.platformStatus==='platform'" class="tip platform">平台推荐货品建议谨慎修改核心字段</view>
<view v-else-if="form.sourceSubmissionId" class="tip custom">此货品源于我的提交审核通过后已入库</view>
<view class="section">
<view class="row">
<text class="label">商品名称</text>
<input v-model.trim="form.name" placeholder="必填" />
</view>
<view class="row">
<text class="label">条形码</text>
<input class="input-long" v-model.trim="form.barcode" placeholder="可扫码或输入" />
<!-- #ifdef MP-WEIXIN -->
<button size="mini" class="picker-btn" @click="chooseAndScanBarcode">图片识码</button>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<button size="mini" class="picker-btn" @click="chooseAndScanBarcode">图片识码</button>
<!-- #endif -->
</view>
<view class="row">
<input v-model.trim="form.brand" placeholder="品牌" />
</view>
<view class="row">
<input v-model.trim="form.model" placeholder="型号" />
</view>
<view class="row">
<input v-model.trim="form.spec" placeholder="规格" />
</view>
<!-- 隐藏产地输入 -->
<!-- 隐藏主单位选择 -->
<view class="row">
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">类别{{ categoryLabel }}</view>
</picker>
</view>
</view>
<!-- 隐藏库存与安全库存输入 -->
<!-- 隐藏价格相关输入 -->
<view class="section">
<text class="label">图片</text>
<ImageUploader v-model="form.images" :formData="{ ownerType: 'product' }" />
</view>
<view class="section">
<text class="label">备注</text>
<textarea v-model.trim="form.remark" placeholder="可选" auto-height />
</view>
<view class="fixed" :style="{ bottom: (keyboardHeight || 0) + 'px' }">
<button class="ghost" @click="save(false)">保存</button>
<button class="primary" @click="save(true)">保存并继续</button>
</view>
</scroll-view>
</template>
<script>
import ImageUploader from '../../components/ImageUploader.vue'
import { get, post, put, upload } from '../../common/http.js'
export default {
components: { ImageUploader },
data() {
return {
id: '',
form: {
name: '', barcode: '', brand: '', model: '', spec: '',
categoryId: '',
images: [], remark: '',
platformStatus: '', sourceSubmissionId: ''
},
categories: [],
keyboardHeight: 0
}
},
onLoad(query) {
this.id = query?.id || ''
this.bootstrap()
this.initKeyboardListener()
},
onUnload() {
this.disposeKeyboardListener()
},
computed: {
categoryNames() { return this.categories.map(c => c.name) },
categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
return c ? c.name : '选择类别'
}
},
methods: {
async bootstrap() {
await Promise.all([this.fetchCategories()])
if (this.id) this.loadDetail()
},
initKeyboardListener() {
try {
this.__keyboardListener = (e) => {
const h = (e && (e.height || e.targetHeight || 0)) || 0
this.keyboardHeight = h
}
uni.onKeyboardHeightChange && uni.onKeyboardHeightChange(this.__keyboardListener)
} catch (_) {}
},
disposeKeyboardListener() {
try {
if (this.__keyboardListener && uni.offKeyboardHeightChange) {
uni.offKeyboardHeightChange(this.__keyboardListener)
}
} catch (_) {}
},
async fetchCategories() {
try {
const res = await get('/api/product-categories')
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
onPickCategory(e) {
const idx = Number(e.detail.value); const c = this.categories[idx]
this.form.categoryId = c ? c.id : ''
},
async chooseAndScanBarcode() {
try {
const chooseRes = await uni.chooseImage({ count: 1, sourceType: ['camera','album'], sizeType: ['compressed'] })
let filePath = chooseRes.tempFilePaths[0]
try {
const comp = await uni.compressImage({ src: filePath, quality: 80 })
filePath = comp.tempFilePath || filePath
} catch (e) {}
const data = await upload('/api/barcode/scan', filePath, {}, 'file')
if (data && data.success && data.barcode) {
this.form.barcode = data.barcode
uni.showToast({ title: '识别成功', icon: 'success', mask: false })
return
}
const msg = (data && (data.message || data.error || data.msg)) || '未识别'
uni.showToast({ title: msg, icon: 'none', mask: false })
} catch (e) {
const msg = (e && e.message) ? String(e.message) : '网络异常或服务不可用'
uni.showToast({ title: msg, icon: 'none', mask: false })
}
},
async loadDetail() {
try {
const data = await get('/api/products/' + this.id)
Object.assign(this.form, {
name: data.name,
barcode: data.barcode, brand: data.brand, model: data.model, spec: data.spec,
categoryId: data.categoryId,
images: (data.images || []).map(i => i.url || i),
remark: data.remark || '',
platformStatus: data.platformStatus || '',
sourceSubmissionId: data.sourceSubmissionId || ''
})
} catch (_) {}
},
validate() {
if (!this.form.name) { uni.showToast({ title: '请填写名称', icon: 'none' }); return false }
return true
},
buildPayload() {
const f = this.form
return {
name: f.name, barcode: f.barcode, brand: f.brand, model: f.model, spec: f.spec,
categoryId: f.categoryId || null,
images: f.images,
remark: f.remark
}
},
async save(goOn) {
try { uni.hideKeyboard && uni.hideKeyboard() } catch (_) {}
if (!this.validate()) return
const payload = this.buildPayload()
try {
if (this.id) await put('/api/products/' + this.id, payload)
else await post('/api/products', payload)
uni.showToast({ title: '保存成功', icon: 'success', mask: false })
if (goOn && !this.id) {
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', categoryId: '', images: [], remark: '', platformStatus: '', sourceSubmissionId: '' }
} else {
setTimeout(() => uni.navigateBack(), 400)
}
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none', mask: false })
}
}
}
}
</script>
<style lang="scss">
.page { background:$uni-bg-color; min-height: 100vh; padding-bottom: 160rpx; box-sizing: border-box; }
.hero.small { margin: 22rpx 24rpx 12rpx; padding: 0 4rpx 18rpx; color: $uni-text-color; border-bottom: 2rpx solid rgba(94,124,174,0.12); }
.hero.small .title { font-size: 34rpx; font-weight: 800; }
.hero.small .sub { display: block; margin-top: 6rpx; color: $uni-text-color-grey; font-size: 24rpx; }
.section { margin: 0 24rpx 28rpx; padding-bottom: 6rpx; border-bottom: 2rpx solid rgba(94,124,174,0.10); }
.section:last-of-type { border-bottom: 0; margin-bottom: 0; }
.section .row:first-child .label { font-weight: 700; color: $uni-text-color; }
.row { display:flex; gap: 8rpx; align-items: center; margin-top: 18rpx; }
.row .input-long { flex: 1.2; }
.row:first-child { margin-top: 0; }
.label { width: 150rpx; color:$uni-text-color-grey; font-size: 26rpx; }
.row input { flex:1; background:#f7f9fc; border-radius: 14rpx; padding: 18rpx 20rpx; color:$uni-text-color; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.picker-btn { background:#ffffff; border: 2rpx solid rgba($uni-color-primary, .45); color:$uni-color-primary; padding: 0 24rpx; border-radius: 999rpx; font-size: 24rpx; }
.picker { padding: 16rpx 22rpx; background:#f7f9fc; border-radius: 14rpx; color:$uni-text-color-grey; margin-left: 8rpx; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.prices input { width: 30%; }
.section textarea { width: 100%; min-height: 160rpx; background:#f7f9fc; border-radius: 14rpx; padding: 20rpx 22rpx; box-sizing: border-box; color:$uni-text-color; border: 0; box-shadow: inset 0 0 0 2rpx rgba(134,155,191,0.06); }
.fixed { position: fixed; left: 0; right: 0; bottom: env(safe-area-inset-bottom); background:#ffffff; padding: 16rpx 16rpx calc(16rpx + constant(safe-area-inset-bottom)) 16rpx; display:flex; gap: 16rpx; box-shadow: 0 -6rpx 18rpx rgba(24,55,105,0.08); z-index: 999; }
.fixed .primary { flex:1; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 18rpx 0; font-weight: 700; }
.fixed .ghost { flex:1; background:#ffffff; color:$uni-color-primary; border: 2rpx solid rgba($uni-color-primary, .45); border-radius: 999rpx; padding: 18rpx 0; }
.tip { margin: 0 30rpx 20rpx; padding: 16rpx 20rpx; border-radius: 16rpx; font-size: 24rpx; }
.tip.platform { background: rgba(45,140,240,0.12); color: #2d8cf0; }
.tip.custom { background: rgba(103,194,58,0.12); color: #67c23a; }
</style>