2
This commit is contained in:
299
frontend/pages/product/submit.vue
Normal file
299
frontend/pages/product/submit.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user