This commit is contained in:
2025-09-27 22:57:59 +08:00
parent 8a458ff0a4
commit ed26244cdb
12585 changed files with 1914308 additions and 3474 deletions

View 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>