Files
2025-10-08 19:15:20 +08:00

349 lines
13 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="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>
<input v-model.trim="form.externalCode" placeholder="内部/外部编号(可选)" />
</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" v-if="selectedTemplate">
<view class="row">
<text class="label">参数</text>
</view>
<view class="param-list">
<view class="row" v-for="(p,idx) in (selectedTemplate.params||[])" :key="p.fieldKey">
<text class="label">
{{ p.fieldLabel }}<text v-if="p.unit">{{ p.unit }}</text><text v-if="p.required" style="color:#ff5b5b">*</text>
</text>
<block v-if="p.type==='string'">
<input v-model.trim="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
<block v-else-if="p.type==='number'">
<input type="number" v-model.number="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
<block v-else-if="p.type==='boolean'">
<switch :checked="!!paramValues[p.fieldKey]" @change="e => (paramValues[p.fieldKey]=e.detail.value)" />
</block>
<block v-else-if="p.type==='enum'">
<picker mode="selector" :range="p.enumOptions||[]" @change="onPickEnum(p, $event)">
<view class="picker">{{ displayEnum(p) }}</view>
</picker>
</block>
<block v-else-if="p.type==='date'">
<picker mode="date" @change="onPickDate(p, $event)">
<view class="picker">{{ paramValues[p.fieldKey] || ('选择' + p.fieldLabel) }}</view>
</picker>
</block>
<block v-else>
<input v-model.trim="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
</view>
</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: '',
externalCode: '',
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 = {}
},
onPickDate(p, e) {
this.paramValues[p.fieldKey] = e?.detail?.value || ''
},
onPickEnum(p, e) {
const idx = Number(e.detail.value)
const arr = p.enumOptions || []
this.paramValues[p.fieldKey] = arr[idx]
},
displayEnum(p) {
const v = this.paramValues[p.fieldKey]
return (v === undefined || v === null || v === '') ? ('选择' + p.fieldLabel) : String(v)
},
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,
externalCode: this.form.externalCode || null,
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 {
height: 100vh;
padding: 16rpx 24rpx 120rpx;
background: #f6f7fb;
box-sizing: border-box;
}
.section { background: #fff; border-radius: 16rpx; padding: 20rpx 22rpx; margin-bottom: 16rpx; 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>