300 lines
11 KiB
Vue
300 lines
11 KiB
Vue
<template>
|
||
<scroll-view scroll-y class="page">
|
||
<view class="hero">
|
||
<text class="title">提交配件</text>
|
||
<text class="desc">填写型号、名称、参数与图片,提交后进入待审核状态</text>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="row required">
|
||
<text class="label">型号</text>
|
||
<input v-model.trim="form.model" placeholder="请输入型号(必填)" />
|
||
</view>
|
||
<view class="row">
|
||
<text class="label">品牌</text>
|
||
<input v-model.trim="form.brand" placeholder="品牌/厂商" />
|
||
</view>
|
||
|
||
<view class="row">
|
||
<text class="label">条码</text>
|
||
<input v-model.trim="form.barcode" placeholder="可选,建议扫码录入" />
|
||
<button size="mini" class="picker-btn" @click="scanBarcode">识码</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
|
||
<view class="row">
|
||
<text class="label">类别</text>
|
||
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
|
||
<view class="picker">{{ categoryLabel }}</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="row">
|
||
<text class="label">模板</text>
|
||
</view>
|
||
<view class="row">
|
||
<picker mode="selector" :range="templateNames" @change="onPickTemplate">
|
||
<view class="picker">{{ templateLabel }}</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="row">
|
||
<text class="label">图片</text>
|
||
</view>
|
||
<ImageUploader v-model="form.images" :max="9" :formData="{ ownerType: 'submission' }" />
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="row">
|
||
<text class="label">备注</text>
|
||
</view>
|
||
<textarea class="textarea" v-model.trim="form.remark" placeholder="选填:补充说明" />
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="row">
|
||
<text class="label">安全库存</text>
|
||
</view>
|
||
<view class="row triple">
|
||
<input type="number" v-model.number="form.safeMin" placeholder="下限" />
|
||
<input type="number" v-model.number="form.safeMax" placeholder="上限" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="fixed">
|
||
<button class="primary" :loading="submitting" @click="submit">提交审核</button>
|
||
<button class="primary" style="margin-top:16rpx;background:#7aa9ff" :loading="checking" @click="checkModel">查重</button>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script>
|
||
import ImageUploader from '../../components/ImageUploader.vue'
|
||
import { get, post, upload } from '../../common/http.js'
|
||
|
||
export default {
|
||
components: { ImageUploader },
|
||
data() {
|
||
return {
|
||
form: {
|
||
model: '',
|
||
|
||
brand: '',
|
||
|
||
barcode: '',
|
||
|
||
categoryId: '',
|
||
templateId: '',
|
||
parameters: {},
|
||
images: [],
|
||
remark: '',
|
||
safeMin: null,
|
||
safeMax: null
|
||
},
|
||
templates: [],
|
||
paramValues: {},
|
||
checking: false,
|
||
parameterText: '',
|
||
|
||
categories: [],
|
||
submitting: false,
|
||
paramPlaceholder: '可输入 JSON,如 {"颜色":"黑","材质":"钢"}'
|
||
}
|
||
},
|
||
computed: {
|
||
categoryNames() { return this.categories.map(c => c.name) },
|
||
templateNames() { return this.templates.map(t => t.name) },
|
||
categoryLabel() {
|
||
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
|
||
return c ? c.name : '选择类别'
|
||
},
|
||
selectedTemplate() {
|
||
return this.templates.find(t => String(t.id) === String(this.form.templateId))
|
||
},
|
||
templateLabel() {
|
||
const t = this.selectedTemplate
|
||
return t ? `${t.name}` : '选择模板'
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.bootstrap()
|
||
if (options && options.prefill) {
|
||
try {
|
||
const data = JSON.parse(decodeURIComponent(options.prefill))
|
||
Object.assign(this.form, {
|
||
model: data.model || '',
|
||
brand: data.brand || '',
|
||
barcode: data.barcode || '',
|
||
categoryId: data.categoryId || '',
|
||
remark: data.remark || ''
|
||
})
|
||
if (data.parameters && typeof data.parameters === 'object') {
|
||
this.parameterText = JSON.stringify(data.parameters, null, 2)
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
},
|
||
methods: {
|
||
async bootstrap() {
|
||
await Promise.all([ this.fetchCategories()])
|
||
await this.fetchTemplates()
|
||
},
|
||
async fetchCategories() {
|
||
try {
|
||
const res = await get('/api/product-categories')
|
||
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||
} catch (_) {
|
||
this.categories = []
|
||
}
|
||
},
|
||
async fetchTemplates() {
|
||
try {
|
||
const res = await get('/api/product-templates', this.form.categoryId ? { categoryId: this.form.categoryId } : {})
|
||
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||
this.templates = list
|
||
} catch (_) { this.templates = [] }
|
||
},
|
||
onPickCategory(e) {
|
||
const idx = Number(e.detail.value)
|
||
const c = this.categories[idx]
|
||
this.form.categoryId = c ? c.id : ''
|
||
this.fetchTemplates()
|
||
},
|
||
onPickTemplate(e) {
|
||
const idx = Number(e.detail.value)
|
||
const t = this.templates[idx]
|
||
this.form.templateId = t ? t.id : ''
|
||
this.paramValues = {}
|
||
},
|
||
onPickEnum(p, e) {
|
||
const idx = Number(e.detail.value)
|
||
const arr = p.enumOptions || []
|
||
this.paramValues[p.fieldKey] = arr[idx]
|
||
},
|
||
async scanBarcode() {
|
||
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 (_) {}
|
||
const data = await upload('/api/barcode/scan', filePath, {}, 'file')
|
||
const barcode = data?.barcode || data?.data?.barcode
|
||
if (barcode) {
|
||
this.form.barcode = barcode
|
||
uni.showToast({ title: '识别成功', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '未识别到条码', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
const msg = e?.message || '识码失败'
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
}
|
||
},
|
||
async checkModel() {
|
||
if (!this.form.model) return uni.showToast({ title: '请填写型号', icon: 'none' })
|
||
try {
|
||
this.checking = true
|
||
const res = await post('/api/products/submissions/check-model', { templateId: this.form.templateId, model: this.form.model })
|
||
if (res && res.available) {
|
||
uni.showToast({ title: '可用,无重复', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '已存在相同型号提交', icon: 'none' })
|
||
}
|
||
} catch (e) {
|
||
const msg = e?.message || '校验失败'
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
} finally { this.checking = false }
|
||
},
|
||
async submit() {
|
||
if (this.submitting) return
|
||
if (!this.form.model) {
|
||
return uni.showToast({ title: '请填写型号', icon: 'none' })
|
||
}
|
||
let paramsObj = null
|
||
if (this.parameterText) {
|
||
try {
|
||
paramsObj = JSON.parse(this.parameterText)
|
||
} catch (e) {
|
||
return uni.showToast({ title: '参数 JSON 不合法', icon: 'none' })
|
||
}
|
||
}
|
||
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
|
||
return uni.showToast({ title: '安全库存区间不合法', icon: 'none' })
|
||
}
|
||
// 模板必填校验与参数整理
|
||
let paramsForSubmit = paramsObj
|
||
if (this.selectedTemplate) {
|
||
// 校验必填
|
||
for (const p of (this.selectedTemplate.params || [])) {
|
||
if (p.required && (this.paramValues[p.fieldKey] === undefined || this.paramValues[p.fieldKey] === null || this.paramValues[p.fieldKey] === '')) {
|
||
return uni.showToast({ title: `请填写 ${p.fieldLabel}`, icon: 'none' })
|
||
}
|
||
}
|
||
// 类型规范化
|
||
const shaped = {}
|
||
for (const p of (this.selectedTemplate.params || [])) {
|
||
let v = this.paramValues[p.fieldKey]
|
||
if (p.type === 'number' && v !== undefined && v !== null && v !== '') v = Number(v)
|
||
if (p.type === 'boolean') v = !!v
|
||
shaped[p.fieldKey] = v
|
||
}
|
||
paramsForSubmit = shaped
|
||
}
|
||
|
||
const payload = {
|
||
model: this.form.model,
|
||
brand: this.form.brand,
|
||
barcode: this.form.barcode,
|
||
categoryId: this.form.categoryId || null,
|
||
templateId: this.form.templateId || null,
|
||
parameters: paramsForSubmit,
|
||
images: this.form.images,
|
||
remark: this.form.remark,
|
||
safeMin: this.form.safeMin,
|
||
safeMax: this.form.safeMax
|
||
}
|
||
this.submitting = true
|
||
try {
|
||
await post('/api/products/submissions', payload)
|
||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.redirectTo({ url: '/pages/product/submissions' })
|
||
}, 400)
|
||
} catch (e) {
|
||
const msg = e?.message || '提交失败'
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.page { padding: 24rpx 24rpx 120rpx; background: #f6f7fb; }
|
||
.hero { padding: 24rpx; background: linear-gradient(135deg, #4c8dff, #6ab7ff); border-radius: 20rpx; color: #fff; margin-bottom: 24rpx; }
|
||
.title { font-size: 36rpx; font-weight: 700; }
|
||
.desc { font-size: 26rpx; margin-top: 8rpx; opacity: 0.9; }
|
||
.section { background: #fff; border-radius: 16rpx; padding: 20rpx 22rpx; margin-bottom: 24rpx; box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.04); }
|
||
.row { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 0; border-bottom: 1rpx solid #f1f2f5; }
|
||
.row:last-child { border-bottom: none; }
|
||
.row.required .label::after { content: '*'; color: #ff5b5b; margin-left: 6rpx; }
|
||
.label { width: 130rpx; font-size: 28rpx; color: #2d3a4a; }
|
||
input { flex: 1; background: #f8f9fb; border-radius: 12rpx; padding: 16rpx 18rpx; font-size: 28rpx; color: #222; }
|
||
.textarea { width: 100%; min-height: 160rpx; background: #f8f9fb; border-radius: 12rpx; padding: 18rpx; font-size: 28rpx; color: #222; }
|
||
.picker { flex: 1; background: #f8f9fb; border-radius: 12rpx; padding: 18rpx; font-size: 28rpx; color: #222; }
|
||
.picker-btn { background: #4c8dff; color: #fff; border-radius: 999rpx; padding: 10rpx 22rpx; }
|
||
.triple input { flex: 1; }
|
||
.fixed { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 24rpx 40rpx; background: rgba(255,255,255,0.96); box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.08); }
|
||
.primary { width: 100%; height: 88rpx; border-radius: 999rpx; background: #4c8dff; color: #fff; font-size: 32rpx; font-weight: 600; }
|
||
</style>
|