4
This commit is contained in:
25
admin/node_modules/.vue-global-types/vue_99_0.d.ts
generated
vendored
25
admin/node_modules/.vue-global-types/vue_99_0.d.ts
generated
vendored
@@ -15,13 +15,13 @@ export {};
|
||||
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
|
||||
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
|
||||
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
|
||||
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
|
||||
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
|
||||
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N3] } :
|
||||
N1 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N1] } :
|
||||
N2 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N2] } :
|
||||
N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :
|
||||
Self extends object ? { [K in N0]: Self } :
|
||||
N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } :
|
||||
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
|
||||
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :
|
||||
N1 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N1] } :
|
||||
N2 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N2] } :
|
||||
N3 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N3] } :
|
||||
{};
|
||||
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
|
||||
? K extends { __ctx?: infer Ctx } ? NonNullable<Ctx> : never : any
|
||||
@@ -83,6 +83,10 @@ export {};
|
||||
}
|
||||
>
|
||||
>;
|
||||
type __VLS_EmitsToProps<T> = __VLS_PrettifyGlobal<{
|
||||
[K in string & keyof T as `on${Capitalize<K>}`]?:
|
||||
(...args: T[K] extends (...args: infer P) => any ? P : T[K] extends null ? any[] : never) => any;
|
||||
}>;
|
||||
type __VLS_ResolveEmits<
|
||||
Comp,
|
||||
Emits,
|
||||
@@ -90,10 +94,16 @@ export {};
|
||||
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
|
||||
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
|
||||
type __VLS_ResolveDirectives<T> = {
|
||||
[K in Exclude<keyof T, keyof __VLS_GlobalDirectives> & string as `v${Capitalize<K>}`]: T[K];
|
||||
[K in keyof T & string as `v${Capitalize<K>}`]: T[K];
|
||||
};
|
||||
type __VLS_PrettifyGlobal<T> = { [K in keyof T as K]: T[K]; } & {};
|
||||
type __VLS_WithDefaultsGlobal<P, D> = {
|
||||
[K in keyof P as K extends keyof D ? K : never]-?: P[K];
|
||||
} & {
|
||||
[K in keyof P as K extends keyof D ? never : K]: P[K];
|
||||
};
|
||||
type __VLS_UseTemplateRef<T> = Readonly<import('vue').ShallowRef<T | null>>;
|
||||
type __VLS_ProxyRefs<T> = import('vue').ShallowUnwrapRef<T>;
|
||||
|
||||
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
|
||||
item: T extends number ? number
|
||||
@@ -115,7 +125,6 @@ export {};
|
||||
: T extends (...args: any) => any
|
||||
? T
|
||||
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
|
||||
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
|
||||
function __VLS_asFunctionalComponent<T, K = T extends new (...args: any) => any ? InstanceType<T> : unknown>(t: T, instance?: K):
|
||||
T extends new (...args: any) => any ? __VLS_FunctionalComponent<K>
|
||||
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
|
||||
|
||||
@@ -64,7 +64,13 @@ async function fetch(){
|
||||
}
|
||||
function reset(){ q.status='open'; q.kw=''; fetch() }
|
||||
function openReply(row: any){ visible.value = true; current.value = row; reply.value='' }
|
||||
async function sendReply(){ await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value }); visible.value=false; await fetch() }
|
||||
async function sendReply(){
|
||||
const data = await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value })
|
||||
visible.value=false
|
||||
// 若后端返回状态,立即更新当前行展示
|
||||
if (current.value && data && data.status) current.value.status = data.status
|
||||
await fetch()
|
||||
}
|
||||
async function resolve(row: any){ await put(`/api/admin/consults/${row.id}/resolve`, {}); row.status = 'resolved' }
|
||||
function viewReply(row: any){ ElMessageBox.alert(row.replyContent || '无', '已回复内容', { confirmButtonText: '知道了' }) }
|
||||
|
||||
|
||||
@@ -28,7 +28,19 @@
|
||||
</div>
|
||||
|
||||
<div class="panel" style="padding:0;">
|
||||
<el-table :data="rows" style="width:100%" :loading="loading">
|
||||
<el-table :data="rows" style="width:100%" :loading="loading" row-key="id" :expand-row-keys="expanded" @expand-change="onExpandChange">
|
||||
<el-table-column type="expand" width="28">
|
||||
<template #default="{row}">
|
||||
<div class="expand-wrap">
|
||||
<div class="expand-line">模板:<b>{{ (expand[row.id]?.templateName)|| '-' }}</b></div>
|
||||
<div v-if="expand[row.id]?.loading" class="expand-loading">加载中...</div>
|
||||
<el-table v-else :data="expand[row.id]?.rows||[]" size="small" style="width:100%;">
|
||||
<el-table-column prop="label" label="参数" min-width="160" />
|
||||
<el-table-column prop="value" label="值" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" width="60" label="#" />
|
||||
<el-table-column prop="model" label="型号" width="160" />
|
||||
<el-table-column prop="name" label="名称" min-width="180" />
|
||||
@@ -66,36 +78,8 @@
|
||||
|
||||
<el-dialog v-model="detail.visible" title="配件详情" width="720px">
|
||||
<el-form v-if="detail.data" :model="detail.data" label-width="108px" class="detail-form">
|
||||
<el-form-item label="型号">
|
||||
<el-input v-model="detail.data.model" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="detail.data.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌">
|
||||
<el-input v-model="detail.data.brand" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<el-input v-model="detail.data.spec" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板">
|
||||
<el-input v-model="detail.data.templateId" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数">
|
||||
<el-table :data="paramRows" size="small" style="width:100%">
|
||||
<el-table-column prop="fieldLabel" label="名称" />
|
||||
<el-table-column prop="unit" label="单位" width="100" />
|
||||
<el-table-column prop="required" label="必填" width="80">
|
||||
<template #default="{row}"><el-tag :type="row.required?'danger':'info'">{{ row.required?'是':'否' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="提交值" />
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数JSON">
|
||||
<el-input type="textarea" v-model="detail.parameterText" :rows="4" :placeholder="jsonPlaceholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input type="textarea" v-model="detail.data.remark" :rows="3" />
|
||||
<el-input type="textarea" v-model="detail.data.remark" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<div class="thumb-list">
|
||||
@@ -142,6 +126,8 @@ const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
const detail = reactive({ visible: false, id: 0, data: null as any, images: [] as string[], parameterText: '', template: null as any })
|
||||
const expand = reactive<Record<number, { loading: boolean, templateName: string, rows: Array<{label:string,value:any}> }>>({})
|
||||
const expanded = ref<number[]>([])
|
||||
const paramRows = ref<any[]>([])
|
||||
const jsonPlaceholder = '{"key":"value"}'
|
||||
const saving = ref(false)
|
||||
@@ -179,6 +165,9 @@ async function fetchList(){
|
||||
const res = await get('/api/admin/parts/submissions', params)
|
||||
rows.value = Array.isArray(res?.list) ? res.list : []
|
||||
total.value = Number(res?.total || rows.value.length)
|
||||
// 默认展开全部并预加载参数
|
||||
expanded.value = rows.value.map((r:any)=>Number(r.id)).filter((x:number)=>!!x)
|
||||
for (const r of rows.value) { await ensureExpand(r) }
|
||||
} catch (e:any) {
|
||||
ElMessage.error(e.message || '加载失败')
|
||||
} finally {
|
||||
@@ -194,6 +183,33 @@ function reset(){
|
||||
}
|
||||
function onPage(p:number){ query.page = p; fetchList() }
|
||||
|
||||
async function ensureExpand(row:any){
|
||||
const id = Number(row?.id)
|
||||
if (!id) return
|
||||
if (expand[id] && !expand[id].loading && (expand[id].rows||[]).length) return
|
||||
expand[id] = { loading: true, templateName: '', rows: [] }
|
||||
try {
|
||||
const data = await get(`/api/admin/parts/submissions/${id}`)
|
||||
const params = data?.parameters || {}
|
||||
if (data?.templateId) {
|
||||
try {
|
||||
const t = await get(`/api/admin/part-templates/${data.templateId}`)
|
||||
expand[id].templateName = t?.name || ''
|
||||
const defs = (t?.params||[])
|
||||
expand[id].rows = defs.map((p:any)=>({ label: p.fieldLabel + (p.unit?` (${p.unit})`:''), value: params[p.fieldKey] ?? '' }))
|
||||
} catch {
|
||||
expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
|
||||
}
|
||||
} else {
|
||||
expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
|
||||
}
|
||||
} finally {
|
||||
if (expand[id]) expand[id].loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function onExpandChange(row:any){ ensureExpand(row) }
|
||||
|
||||
async function openDetail(id:number){
|
||||
try {
|
||||
const data = await get(`/api/admin/parts/submissions/${id}`)
|
||||
@@ -319,4 +335,7 @@ onMounted(fetchList)
|
||||
.thumb-list { display:flex; flex-wrap:wrap; gap:12px; }
|
||||
.thumb { width: 120px; display:flex; flex-direction: column; gap: 8px; }
|
||||
.thumb .el-image { width: 120px; height: 120px; border-radius: 8px; }
|
||||
.expand-wrap { padding: 8px 12px; background: rgba(255,255,255,0.02); }
|
||||
.expand-line { margin-bottom: 6px; font-size: 13px; color: #ddd; }
|
||||
.expand-loading { padding: 8px 0; color:#999; }
|
||||
</style>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="720">
|
||||
<el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="1100">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
|
||||
@@ -60,20 +60,14 @@
|
||||
<el-table-column label="#" width="48">
|
||||
<template #default="{ $index }">{{ $index+1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="键">
|
||||
<template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" :disabled="!!dlg.id" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称">
|
||||
<template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" /></template>
|
||||
<template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" @input="onLabelInput(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{row}">
|
||||
<el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
|
||||
<el-option label="string" value="string" />
|
||||
<el-option label="number" value="number" />
|
||||
<el-option label="boolean" value="boolean" />
|
||||
<el-option label="enum" value="enum" />
|
||||
<el-option label="date" value="date" />
|
||||
<el-option label="数字" value="number" />
|
||||
<el-option label="非数字" value="string" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -83,15 +77,25 @@
|
||||
<el-table-column label="单位" width="120">
|
||||
<template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="枚举项">
|
||||
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔(type=enum)" :disabled="!!dlg.id" /></template>
|
||||
|
||||
<!-- 检索默认参与:前端不再显示开关 -->
|
||||
<el-table-column label="可模糊" width="90">
|
||||
<template #default="{row}">
|
||||
<el-switch v-model="row.fuzzySearchable" :disabled="!!dlg.id || row.type!=='number'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检索" width="80">
|
||||
<template #default="{row}"><el-switch v-model="row.searchable" :disabled="!!dlg.id" /></template>
|
||||
<el-table-column label="卡片展示" width="100">
|
||||
<template #default="{row}">
|
||||
<el-switch v-model="row.cardDisplay" :disabled="!!dlg.id" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="去重" width="80">
|
||||
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" :disabled="!!dlg.id" /></template>
|
||||
<el-table-column label="容差" width="160">
|
||||
<template #default="{row}">
|
||||
<el-input v-model="row.fuzzyTolerance" :disabled="!!dlg.id || row.type!=='number' || !row.fuzzySearchable" placeholder="默认1.0" />
|
||||
<div v-if="row.unit" style="font-size:12px;color:#999">单位:{{ row.unit }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ $index }">
|
||||
<el-button v-if="!dlg.id" text type="danger" @click="removeParam($index)">删除</el-button>
|
||||
@@ -136,7 +140,8 @@ function openView(row:any) {
|
||||
http.get(`/api/admin/part-templates/${row.id}`).then(res => {
|
||||
const d = res.data
|
||||
Object.assign(form, { id: d.id, categoryId: d.categoryId, name: d.name, modelRule: d.modelRule, status: d.status,
|
||||
params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(',') })) })
|
||||
params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(','),
|
||||
fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: p.fuzzyTolerance })) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,12 +151,15 @@ function openCreate() {
|
||||
}
|
||||
|
||||
function addParam() {
|
||||
form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
|
||||
form.params.push({ fieldKey:'', fieldLabel:'', type:'number', required:false, unit:'mm', enumOptionsText:'', searchable:false,
|
||||
fuzzySearchable:false, fuzzyTolerance:null, sortOrder:0 })
|
||||
}
|
||||
|
||||
function removeParam(i:number) { form.params.splice(i,1) }
|
||||
|
||||
function save() {
|
||||
// 先为所有行生成 fieldKey(基于名称拼音首字母),并消重
|
||||
genAllKeys()
|
||||
// 校验
|
||||
const seen = new Set<string>()
|
||||
for (const p of form.params) {
|
||||
@@ -160,13 +168,27 @@ function save() {
|
||||
if (!key) { ElMessage.warning('参数键不能为空'); return }
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { ElMessage.warning(`参数键不合法: ${key}`); return }
|
||||
if (!label) { ElMessage.warning('参数名称不能为空'); return }
|
||||
if (!['string','number','boolean','enum','date'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
|
||||
if (!['string','number'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
|
||||
if (p.fuzzySearchable && p.type !== 'number') { ElMessage.warning(`仅 number 类型可开启可模糊: ${key}`); return }
|
||||
if (p.fuzzySearchable && p.fuzzyTolerance !== undefined && p.fuzzyTolerance !== null && String(p.fuzzyTolerance).trim() !== '') {
|
||||
const num = Number(p.fuzzyTolerance)
|
||||
if (!(num > 0)) { ElMessage.warning(`容差需为正数: ${key}`); return }
|
||||
p.fuzzyTolerance = num
|
||||
}
|
||||
if (seen.has(key)) { ElMessage.warning(`参数键重复: ${key}`); return }
|
||||
seen.add(key)
|
||||
}
|
||||
// 限制卡片展示最多4个
|
||||
if (form.params.filter((p:any)=>!!p.cardDisplay).length > 4) {
|
||||
ElMessage.warning('卡片展示最多选择4个参数');
|
||||
return
|
||||
}
|
||||
const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status,
|
||||
params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit,
|
||||
enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) }
|
||||
enumOptions: undefined, searchable:true,
|
||||
fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: (p.fuzzyTolerance===null||p.fuzzyTolerance===''? null : Number(p.fuzzyTolerance)),
|
||||
cardDisplay: !!p.cardDisplay,
|
||||
sortOrder:p.sortOrder })) }
|
||||
http.post('/api/admin/part-templates', payload).then(()=>{
|
||||
ElMessage.success('创建成功'); dlg.visible=false; load()
|
||||
})
|
||||
@@ -183,6 +205,51 @@ function doDelete(row:any) {
|
||||
}
|
||||
|
||||
onMounted(()=>{ loadCategories(); load() })
|
||||
|
||||
// 生成拼音首字母作为键,并做合法化与消重
|
||||
// 轻量拼音首字母生成:优先使用浏览器 Intl.Segmenter 分词 + 拉丁字母提取;不依赖外部包
|
||||
function toInitialKey(label: string): string {
|
||||
const s = String(label||'').trim()
|
||||
if (!s) return ''
|
||||
// 规则:取每个“词”的首字符(非中文/字母/数字作为分隔),保留字母数字;中文用简单映射(常用声母)
|
||||
const simpleMap: Record<string,string> = {
|
||||
'长':'c','重':'z','沈':'s','厦':'x','重庆':'c','长沙':'c'
|
||||
}
|
||||
let buf = ''
|
||||
for (let i=0;i<s.length;i++) {
|
||||
const ch = s[i]
|
||||
if (/^[A-Za-z0-9]$/.test(ch)) { buf += ch[0] ; while (i+1<s.length && /[A-Za-z0-9]/.test(s[i+1])) i++; continue }
|
||||
// 中文:取一个字符映射为首字母(非常简化)
|
||||
if (/^[\u4e00-\u9fa5]$/.test(ch)) { buf += (simpleMap[ch] || 'z') ; continue }
|
||||
// 其它符号:作为分隔,不取
|
||||
}
|
||||
let key = buf.toLowerCase().replace(/[^a-z0-9_]/g,'')
|
||||
if (!key) key = s.replace(/\s+/g,'_').replace(/[^A-Za-z0-9_]/g,'').toLowerCase()
|
||||
if (!/^[A-Za-z_]/.test(key)) key = '_' + key
|
||||
return key
|
||||
}
|
||||
function onLabelInput(row:any){
|
||||
const base = toInitialKey(row.fieldLabel)
|
||||
row.fieldKey = uniqueKey(base, row)
|
||||
}
|
||||
function genAllKeys(){
|
||||
for (const r of form.params) {
|
||||
if (!r.fieldKey) r.fieldKey = toInitialKey(r.fieldLabel)
|
||||
}
|
||||
// 再次去重
|
||||
const counts: Record<string, number> = {}
|
||||
form.params.forEach(r=>{ r.fieldKey = uniqueKey(r.fieldKey, r) })
|
||||
}
|
||||
function uniqueKey(base: string, currentRow:any){
|
||||
let k = (base||'').toLowerCase()
|
||||
if (!k) k = '_k'
|
||||
// 确保在当前列表中唯一
|
||||
const others = form.params.filter((x:any)=>x!==currentRow).map((x:any)=>String(x.fieldKey||'').toLowerCase())
|
||||
if (!others.includes(k)) return k
|
||||
let i = 2
|
||||
while (others.includes(`${k}${i}`)) i++
|
||||
return `${k}${i}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -18,11 +18,23 @@
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column prop="brand" label="品牌" width="120" />
|
||||
<el-table-column prop="model" label="型号" width="160" />
|
||||
<el-table-column prop="spec" label="规格" />
|
||||
|
||||
<el-table-column prop="templateName" label="模板" width="220" />
|
||||
<el-table-column label="模板内容">
|
||||
<template #default="{row}">
|
||||
<div v-if="row.attributesJson" style="display:flex; flex-wrap:wrap; gap:8px;">
|
||||
<el-tag v-for="(v,k) in parseAttrs(row.attributesJson)" :key="k" type="info">{{ k }}: {{ v }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="图片" width="160">
|
||||
<template #default="{row}">
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<el-image v-for="(img,i) in (row.images||[])" :key="i" :src="withBaseUrl(img)" fit="cover" style="width:48px;height:48px;border-radius:8px;" :preview-src-list="(row.images||[]).map(withBaseUrl)" />
|
||||
<el-image v-for="(img,i) in (row.images||[])" :key="i" :src="withBaseUrl(img)" fit="cover"
|
||||
style="width:48px;height:48px;border-radius:8px;"
|
||||
:z-index="3000"
|
||||
:preview-teleported="true"
|
||||
:preview-src-list="(row.images||[]).map(withBaseUrl)" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -71,6 +83,16 @@ function edit(row: any){ visible.value=true; Object.assign(form, JSON.parse(JSON
|
||||
async function save(){ await put(`/api/admin/parts/${form.id}`, form); visible.value=false; await fetch() }
|
||||
|
||||
onMounted(fetch)
|
||||
|
||||
function parseAttrs(json: any): Record<string, any> {
|
||||
try {
|
||||
if (!json) return {}
|
||||
if (typeof json === 'object') return json as Record<string, any>
|
||||
return JSON.parse(String(json)) || {}
|
||||
} catch(e){
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -24,28 +24,15 @@
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{row}"><el-tag :type="row.status===1?'success':'danger'">{{ row.status===1?'正常':'黑名单' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="240">
|
||||
<el-table-column fixed="right" label="操作" width="160">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" @click="toggle(row)">{{ row.status===1?'拉黑':'恢复' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="visible" title="编辑用户" width="520">
|
||||
<el-form :model="form" label-width="96px">
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-form-item label="角色"><el-input v-model="form.role" placeholder="owner/staff/finance" /></el-form-item>
|
||||
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
|
||||
<el-form-item label="是否店主"><el-switch v-model="form.isOwner" :active-value="true" :inactive-value="false" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -55,8 +42,6 @@ import { get, put } from '../../api/http'
|
||||
|
||||
const q = reactive({ kw: '' })
|
||||
const rows = ref<any[]>([])
|
||||
const visible = ref(false)
|
||||
const form = reactive<any>({})
|
||||
|
||||
async function fetch(){
|
||||
// 需要后端提供:GET /api/admin/users?kw=...
|
||||
@@ -64,8 +49,6 @@ async function fetch(){
|
||||
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
|
||||
}
|
||||
function reset(){ q.kw=''; fetch() }
|
||||
function openEdit(row: any){ visible.value=true; Object.assign(form, row) }
|
||||
async function save(){ await put(`/api/admin/users/${form.id}`, form); visible.value=false; await fetch() }
|
||||
async function toggle(row: any){
|
||||
const newStatus = row.status===1?0:1
|
||||
await put(`/api/admin/users/${row.id}`, { status: newStatus })
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetch">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="success" @click="openEdit()">新增VIP</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -19,9 +18,6 @@
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column prop="name" label="姓名" width="140" />
|
||||
<el-table-column prop="phone" label="手机号" width="160" />
|
||||
<el-table-column label="VIP" width="100">
|
||||
<template #default="{row}"><el-tag :type="row.isVip===1?'warning':'info'">{{ row.isVip===1?'是':'否' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expireAt" label="到期时间" width="180" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{row}">
|
||||
@@ -30,25 +26,24 @@
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="220">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" @click="openExpire(row)">到期时间</el-button>
|
||||
<el-button size="small" @click="toggle(row)">{{ row.status===1?'停用':'启用' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="visible" :title="editing?'编辑VIP':'新增VIP'" width="520">
|
||||
<el-form :model="form" label-width="96px">
|
||||
<el-form-item label="用户ID"><el-input v-model.number="form.userId" placeholder="输入用户ID" /></el-form-item>
|
||||
<el-form-item label="是否VIP"><el-switch v-model="form.isVip" :active-value="1" :inactive-value="0" /></el-form-item>
|
||||
<el-form-item label="到期时间"><el-date-picker v-model="form.expireAt" type="datetime" placeholder="选择到期时间" /></el-form-item>
|
||||
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="expireVisible" title="设置到期时间" width="420">
|
||||
<el-form label-width="96px">
|
||||
<el-form-item label="到期时间">
|
||||
<el-date-picker v-model="expireAtVal" type="datetime" placeholder="选择到期时间" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="expireVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveExpire">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -56,13 +51,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { get, post, put } from '../../api/http'
|
||||
import { get, put } from '../../api/http'
|
||||
|
||||
const q = reactive({ phone: '' })
|
||||
const rows = ref<any[]>([])
|
||||
const visible = ref(false)
|
||||
const editing = ref(false)
|
||||
const form = reactive({ id: 0, userId: 0, isVip: 1, expireAt: '', status: 1 })
|
||||
|
||||
const expireVisible = ref(false)
|
||||
const expireAtVal = ref<any>('')
|
||||
const expireRow = ref<any>(null)
|
||||
|
||||
async function fetch(){
|
||||
// 需要后端提供:GET /api/admin/vips?phone=... (或 /api/users + /api/vips 关联)
|
||||
@@ -70,24 +66,23 @@ async function fetch(){
|
||||
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
|
||||
}
|
||||
function reset(){ q.phone=''; fetch() }
|
||||
function openEdit(row?: any){
|
||||
visible.value = true
|
||||
editing.value = !!row
|
||||
if (row) Object.assign(form, { id: row.id, userId: row.userId, isVip: row.isVip ?? 1, expireAt: row.expireAt, status: row.status })
|
||||
else Object.assign(form, { id: 0, userId: 0, isVip: 1, expireAt: '', status: 1 })
|
||||
}
|
||||
async function save(){
|
||||
const body = { userId: form.userId, isVip: form.isVip, expireAt: form.expireAt || null, status: form.status }
|
||||
if (editing.value) await put(`/api/admin/vips/${form.id}`, body)
|
||||
else await post('/api/admin/vips', body)
|
||||
visible.value = false
|
||||
await fetch()
|
||||
}
|
||||
async function toggle(row: any){
|
||||
await put(`/api/admin/vips/${row.id}`, { ...row, status: row.status===1?0:1 })
|
||||
await fetch()
|
||||
}
|
||||
|
||||
function openExpire(row: any){
|
||||
expireRow.value = row
|
||||
expireAtVal.value = row?.expireAt || ''
|
||||
expireVisible.value = true
|
||||
}
|
||||
async function saveExpire(){
|
||||
if (!expireRow.value) { expireVisible.value = false; return }
|
||||
await put(`/api/admin/vips/${expireRow.value.id}`, { expireAt: expireAtVal.value || null })
|
||||
expireVisible.value = false
|
||||
await fetch()
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(fetch)
|
||||
@@ -98,3 +93,4 @@ onMounted(fetch)
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -66,7 +66,9 @@ public class AdminConsultController {
|
||||
}
|
||||
Long uid = (userId != null ? userId : (adminId != null ? adminId : defaults.getUserId()));
|
||||
jdbcTemplate.update("INSERT INTO consult_replies (consult_id,user_id,content,created_at) VALUES (?,?,?,NOW())", id, uid, content);
|
||||
return ResponseEntity.ok().build();
|
||||
// 自动判定为已解决
|
||||
jdbcTemplate.update("UPDATE consults SET status='resolved', updated_at=NOW() WHERE id=?", id);
|
||||
return ResponseEntity.ok(java.util.Map.of("status", "resolved"));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/resolve")
|
||||
|
||||
@@ -24,8 +24,9 @@ public class AdminPartController {
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted " +
|
||||
"FROM products p JOIN shops s ON s.id=p.shop_id WHERE p.deleted_at IS NULL");
|
||||
"SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted, " +
|
||||
"p.template_id AS templateId, t.name AS templateName, p.attributes_json AS attributesJson " +
|
||||
"FROM products p JOIN shops s ON s.id=p.shop_id LEFT JOIN part_templates t ON t.id=p.template_id WHERE p.deleted_at IS NULL");
|
||||
List<Object> ps = new ArrayList<>();
|
||||
if (shopId != null) { sql.append(" AND p.shop_id=?"); ps.add(shopId); }
|
||||
if (kw != null && !kw.isBlank()) {
|
||||
@@ -50,6 +51,10 @@ public class AdminPartController {
|
||||
m.put("brand", rs.getString("brand"));
|
||||
m.put("model", rs.getString("model"));
|
||||
m.put("spec", rs.getString("spec"));
|
||||
Object tid = rs.getObject("templateId");
|
||||
if (tid != null) m.put("templateId", tid);
|
||||
m.put("templateName", rs.getString("templateName"));
|
||||
m.put("attributesJson", rs.getString("attributesJson"));
|
||||
return m;
|
||||
});
|
||||
// 附加每个商品的图片列表
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "search.fuzzy")
|
||||
public class SearchFuzzyProperties {
|
||||
|
||||
private boolean enabled = true;
|
||||
private BigDecimal defaultTolerance = new BigDecimal("1.0");
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public BigDecimal getDefaultTolerance() { return defaultTolerance; }
|
||||
public void setDefaultTolerance(BigDecimal defaultTolerance) { this.defaultTolerance = defaultTolerance; }
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class MetadataController {
|
||||
java.util.List<String> enums = com.example.demo.common.JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.List<String>>(){});
|
||||
pm.put("enumOptions", enums);
|
||||
pm.put("searchable", p.getSearchable());
|
||||
pm.put("dedupeParticipate", p.getDedupeParticipate());
|
||||
// 不再暴露 dedupeParticipate
|
||||
pm.put("sortOrder", p.getSortOrder());
|
||||
ps.add(pm);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.example.demo.product.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PartTemplateDtos {
|
||||
|
||||
@@ -13,8 +12,11 @@ public class PartTemplateDtos {
|
||||
public boolean required;
|
||||
public String unit; // 自定义单位文本
|
||||
public List<String> enumOptions; // type=enum 时可用
|
||||
public boolean searchable;
|
||||
public boolean dedupeParticipate;
|
||||
public boolean searchable; // 默认参与搜索;前端不再展示开关
|
||||
public boolean fuzzySearchable; // 仅 type=number 生效
|
||||
public java.math.BigDecimal fuzzyTolerance; // 可空=使用默认
|
||||
public boolean cardDisplay; // 是否在用户端货品卡片展示
|
||||
// public boolean dedupeParticipate; // 已废弃,后端忽略
|
||||
public int sortOrder;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ProductDtos {
|
||||
public BigDecimal retailPrice; // from product_prices
|
||||
public String cover; // first image url
|
||||
public Boolean deleted; // derived from deleted_at
|
||||
public java.util.Map<String, String> cardParams; // 货品卡片展示的参数(最多4个,label->value)
|
||||
}
|
||||
|
||||
public static class ProductDetail {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@@ -36,7 +37,16 @@ public class PartTemplateParam {
|
||||
private Boolean searchable;
|
||||
|
||||
@Column(name = "dedupe_participate", nullable = false)
|
||||
private Boolean dedupeParticipate;
|
||||
private Boolean dedupeParticipate = false;
|
||||
|
||||
@Column(name = "fuzzy_searchable", nullable = false)
|
||||
private Boolean fuzzySearchable = false;
|
||||
|
||||
@Column(name = "fuzzy_tolerance", precision = 18, scale = 6)
|
||||
private BigDecimal fuzzyTolerance;
|
||||
|
||||
@Column(name = "card_display", nullable = false)
|
||||
private Boolean cardDisplay = false;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder;
|
||||
@@ -64,8 +74,14 @@ public class PartTemplateParam {
|
||||
public void setEnumOptionsJson(String enumOptionsJson) { this.enumOptionsJson = enumOptionsJson; }
|
||||
public Boolean getSearchable() { return searchable; }
|
||||
public void setSearchable(Boolean searchable) { this.searchable = searchable; }
|
||||
public Boolean getDedupeParticipate() { return dedupeParticipate; }
|
||||
public void setDedupeParticipate(Boolean dedupeParticipate) { this.dedupeParticipate = dedupeParticipate; }
|
||||
public Boolean getDedupeParticipate() { return dedupeParticipate == null ? Boolean.FALSE : dedupeParticipate; }
|
||||
public void setDedupeParticipate(Boolean dedupeParticipate) { this.dedupeParticipate = (dedupeParticipate == null ? Boolean.FALSE : dedupeParticipate); }
|
||||
public Boolean getFuzzySearchable() { return fuzzySearchable; }
|
||||
public void setFuzzySearchable(Boolean fuzzySearchable) { this.fuzzySearchable = fuzzySearchable; }
|
||||
public BigDecimal getFuzzyTolerance() { return fuzzyTolerance; }
|
||||
public void setFuzzyTolerance(BigDecimal fuzzyTolerance) { this.fuzzyTolerance = fuzzyTolerance; }
|
||||
public Boolean getCardDisplay() { return cardDisplay == null ? Boolean.FALSE : cardDisplay; }
|
||||
public void setCardDisplay(Boolean cardDisplay) { this.cardDisplay = (cardDisplay == null ? Boolean.FALSE : cardDisplay); }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
|
||||
@@ -164,8 +164,13 @@ public class PartTemplateService {
|
||||
p.setRequired(def.required);
|
||||
p.setUnit(def.unit);
|
||||
p.setEnumOptionsJson(def.enumOptions == null ? null : JsonUtils.toJson(def.enumOptions));
|
||||
p.setSearchable(def.searchable);
|
||||
p.setDedupeParticipate(def.dedupeParticipate);
|
||||
// 搜索默认参与:若前端未传,置为 true
|
||||
p.setSearchable(def.searchable || true);
|
||||
p.setFuzzySearchable(def.fuzzySearchable);
|
||||
p.setFuzzyTolerance(def.fuzzyTolerance);
|
||||
p.setCardDisplay(def.cardDisplay);
|
||||
// 已忽略 dedupeParticipate(统一置 false 以满足非空约束)
|
||||
p.setDedupeParticipate(false);
|
||||
p.setSortOrder(def.sortOrder == 0 ? idx : def.sortOrder);
|
||||
p.setCreatedAt(now);
|
||||
p.setUpdatedAt(now);
|
||||
@@ -185,7 +190,10 @@ public class PartTemplateService {
|
||||
d.unit = p.getUnit();
|
||||
d.enumOptions = JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>(){});
|
||||
d.searchable = Boolean.TRUE.equals(p.getSearchable());
|
||||
d.dedupeParticipate = Boolean.TRUE.equals(p.getDedupeParticipate());
|
||||
d.fuzzySearchable = Boolean.TRUE.equals(p.getFuzzySearchable());
|
||||
d.fuzzyTolerance = p.getFuzzyTolerance();
|
||||
d.cardDisplay = Boolean.TRUE.equals(p.getCardDisplay());
|
||||
// 不再回传 dedupeParticipate
|
||||
d.sortOrder = p.getSortOrder() == null ? 0 : p.getSortOrder();
|
||||
out.add(d);
|
||||
}
|
||||
@@ -214,6 +222,15 @@ public class PartTemplateService {
|
||||
if (!("string".equals(type) || "number".equals(type) || "boolean".equals(type) || "enum".equals(type) || "date".equals(type))) {
|
||||
throw new IllegalArgumentException("不支持的参数类型: " + type);
|
||||
}
|
||||
// fuzzy 校验:仅 number 类型允许;启用时容差可留空(用默认)或为正数
|
||||
if (Boolean.TRUE.equals(def.fuzzySearchable) && !"number".equals(type)) {
|
||||
throw new IllegalArgumentException("仅 number 类型参数允许开启可模糊查询: " + key);
|
||||
}
|
||||
if ("number".equals(type) && Boolean.TRUE.equals(def.fuzzySearchable) && def.fuzzyTolerance != null) {
|
||||
if (def.fuzzyTolerance.compareTo(java.math.BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("容差需为正数: " + key);
|
||||
}
|
||||
}
|
||||
if (keys.contains(key)) throw new IllegalArgumentException("参数键重复: " + key);
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
@@ -32,17 +32,20 @@ public class ProductService {
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final ProductImageRepository imageRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final com.example.demo.common.SearchFuzzyProperties fuzzyProps;
|
||||
|
||||
public ProductService(ProductRepository productRepository,
|
||||
ProductPriceRepository priceRepository,
|
||||
InventoryRepository inventoryRepository,
|
||||
ProductImageRepository imageRepository,
|
||||
JdbcTemplate jdbcTemplate) {
|
||||
JdbcTemplate jdbcTemplate,
|
||||
com.example.demo.common.SearchFuzzyProperties fuzzyProps) {
|
||||
this.productRepository = productRepository;
|
||||
this.priceRepository = priceRepository;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.imageRepository = imageRepository;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.fuzzyProps = fuzzyProps;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -121,10 +124,7 @@ public class ProductService {
|
||||
product.setAttributesJson(submission.getAttributesJson());
|
||||
changed = true;
|
||||
}
|
||||
if (submission.getDedupeKey() != null && !submission.getDedupeKey().isBlank()) {
|
||||
product.setDedupeKey(submission.getDedupeKey());
|
||||
changed = true;
|
||||
}
|
||||
// 忽略 dedupeKey
|
||||
if (changed) {
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(product);
|
||||
@@ -149,13 +149,44 @@ public class ProductService {
|
||||
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
|
||||
if (templateId != null) { sql.append(" AND p.template_id=?"); ps.add(templateId); }
|
||||
if (paramFilters != null && !paramFilters.isEmpty()) {
|
||||
java.util.Map<String, Boolean> keyFuzzyCache = new java.util.HashMap<>();
|
||||
for (java.util.Map.Entry<String,String> ent : paramFilters.entrySet()) {
|
||||
String key = ent.getKey(); String val = ent.getValue();
|
||||
if (key == null || key.isBlank() || val == null || val.isBlank()) continue;
|
||||
// 精确匹配参数值:将 JSON 值解包后与入参做等值比较,避免 LIKE 导致的误匹配
|
||||
sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
|
||||
ps.add("$." + key);
|
||||
ps.add(val.trim());
|
||||
|
||||
boolean fuzzyEnabled = fuzzyProps.isEnabled() && isKeyFuzzyEnabled(key, keyFuzzyCache);
|
||||
java.math.BigDecimal valNum = null;
|
||||
boolean numericOk = false;
|
||||
if (fuzzyEnabled) {
|
||||
try {
|
||||
valNum = new java.math.BigDecimal(val.trim());
|
||||
numericOk = true;
|
||||
} catch (Exception ignore) { numericOk = false; }
|
||||
}
|
||||
|
||||
if (fuzzyEnabled && numericOk) {
|
||||
// 行级模糊:存在 fuzzy 定义则按区间,否则按等值(NOT EXISTS 分支)
|
||||
sql.append(" AND ( ")
|
||||
.append("EXISTS (SELECT 1 FROM part_template_params ptp WHERE ptp.template_id=p.template_id AND ptp.field_key=? AND ptp.type='number' AND ptp.fuzzy_searchable=1 ")
|
||||
.append("AND CAST(JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) AS DECIMAL(18,6)) BETWEEN GREATEST(0, ? - COALESCE(ptp.fuzzy_tolerance, ?)) AND (? + COALESCE(ptp.fuzzy_tolerance, ?)) ) ")
|
||||
.append(" OR (NOT EXISTS (SELECT 1 FROM part_template_params ptp2 WHERE ptp2.template_id=p.template_id AND ptp2.field_key=? AND ptp2.type='number' AND ptp2.fuzzy_searchable=1) ")
|
||||
.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ? ) )");
|
||||
|
||||
ps.add(key); // ptp.field_key
|
||||
ps.add("$." + key); // json path
|
||||
ps.add(valNum); // v for lower
|
||||
ps.add(fuzzyProps.getDefaultTolerance()); // default tol
|
||||
ps.add(valNum); // v for upper
|
||||
ps.add(fuzzyProps.getDefaultTolerance()); // default tol
|
||||
ps.add(key); // ptp2.field_key for NOT EXISTS
|
||||
ps.add("$." + key); // json path for equality
|
||||
ps.add(val.trim()); // equality value
|
||||
} else {
|
||||
// 直接等值匹配(包括:未启用全局模糊、该字段不在任何模板中启用模糊、或值非数字)
|
||||
sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
|
||||
ps.add("$." + key);
|
||||
ps.add(val.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
|
||||
@@ -173,11 +204,49 @@ public class ProductService {
|
||||
it.retailPrice = rp;
|
||||
it.cover = rs.getString("cover");
|
||||
it.deleted = rs.getBoolean("deleted");
|
||||
// 取卡片展示参数:根据模板定义 card_display=1,最多4个
|
||||
try {
|
||||
String json = jdbcTemplate.queryForObject(
|
||||
"SELECT p.attributes_json FROM products p WHERE p.id=?", String.class, it.id);
|
||||
java.util.Map<String,Object> params = com.example.demo.common.JsonUtils.fromJson(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
|
||||
java.util.LinkedHashMap<String,String> map = new java.util.LinkedHashMap<>();
|
||||
java.util.List<java.util.Map<String,Object>> defs = jdbcTemplate.query(
|
||||
"SELECT field_key, field_label, unit FROM part_template_params WHERE template_id=(SELECT template_id FROM products WHERE id=?) AND card_display=1 ORDER BY sort_order, id LIMIT 4",
|
||||
ps2 -> ps2.setLong(1, it.id),
|
||||
(r2, rn2) -> {
|
||||
java.util.Map<String,Object> m = new java.util.HashMap<>();
|
||||
m.put("key", r2.getString("field_key"));
|
||||
m.put("label", r2.getString("field_label"));
|
||||
m.put("unit", r2.getString("unit"));
|
||||
return m;
|
||||
}
|
||||
);
|
||||
for (java.util.Map<String,Object> d : defs) {
|
||||
String k = (String)d.get("key");
|
||||
String label = (String)d.get("label");
|
||||
String unit = (String)d.get("unit");
|
||||
Object v = params == null ? null : params.get(k);
|
||||
if (v == null) continue;
|
||||
String val = String.valueOf(v) + (unit==null||unit.isBlank()?"":"" );
|
||||
map.put(label + (unit==null||unit.isBlank()?"":"("+unit+")"), val);
|
||||
}
|
||||
it.cardParams = map;
|
||||
} catch (Exception ignore) {}
|
||||
return it;
|
||||
}, ps.toArray());
|
||||
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
|
||||
}
|
||||
|
||||
private boolean isKeyFuzzyEnabled(String fieldKey, java.util.Map<String, Boolean> cache) {
|
||||
if (cache.containsKey(fieldKey)) return cache.get(fieldKey);
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM part_template_params WHERE field_key=? AND type='number' AND fuzzy_searchable=1",
|
||||
Integer.class, fieldKey);
|
||||
boolean enabled = cnt != null && cnt > 0;
|
||||
cache.put(fieldKey, enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public Optional<ProductDtos.ProductDetail> findDetail(Long id, boolean includeDeleted) {
|
||||
Optional<Product> op = productRepository.findById(id);
|
||||
if (op.isEmpty()) return Optional.empty();
|
||||
@@ -235,7 +304,7 @@ public class ProductService {
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
// 单位字段已移除
|
||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
||||
// 忽略 dedupeKey
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
@@ -290,7 +359,7 @@ public class ProductService {
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
// 单位字段已移除
|
||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
||||
// 忽略 dedupeKey
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
|
||||
@@ -79,7 +79,7 @@ public class ProductSubmissionService {
|
||||
submission.setBarcode(req.barcode);
|
||||
submission.setSafeMin(req.safeMin);
|
||||
submission.setSafeMax(req.safeMax);
|
||||
submission.setDedupeKey(buildDedupeKey(req.templateId, req.name, req.model, req.brand, req.parameters));
|
||||
// 按“前端隐藏 + 后端忽略”方案:不再计算/使用 dedupeKey(兼容历史字段保留)
|
||||
submission.setStatus(ProductSubmission.Status.pending);
|
||||
submission.setCreatedAt(LocalDateTime.now());
|
||||
submission.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -223,9 +223,7 @@ public class ProductSubmissionService {
|
||||
if (req.barcode != null) submission.setBarcode(req.barcode);
|
||||
if (req.safeMin != null) submission.setSafeMin(req.safeMin);
|
||||
if (req.safeMax != null) submission.setSafeMax(req.safeMax);
|
||||
// 参数或基础字段变更后重算去重键
|
||||
Map<String,Object> params = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>() {});
|
||||
submission.setDedupeKey(buildDedupeKey(submission.getTemplateId(), submission.getName(), submission.getModelUnique(), submission.getBrand(), params));
|
||||
// 不再重算 dedupeKey(忽略去重键)
|
||||
submission.setUpdatedAt(LocalDateTime.now());
|
||||
submissionRepository.save(submission);
|
||||
}
|
||||
@@ -472,7 +470,7 @@ public class ProductSubmissionService {
|
||||
payload.origin = submission.getOrigin();
|
||||
payload.categoryId = submission.getCategoryId();
|
||||
// 单位字段已移除
|
||||
payload.dedupeKey = submission.getDedupeKey();
|
||||
// 不再透传 dedupeKey
|
||||
payload.safeMin = submission.getSafeMin();
|
||||
payload.safeMax = submission.getSafeMax();
|
||||
payload.remark = submission.getRemarkText();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Fuzzy search global configuration
|
||||
search.fuzzy.enabled=true
|
||||
search.fuzzy.default-tolerance=1.0
|
||||
spring.application.name=demo
|
||||
|
||||
# 数据源配置(通过环境变量注入,避免硬编码)
|
||||
|
||||
@@ -226,3 +226,35 @@
|
||||
2025-09-29 21:30:25.754 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 21:30:25.756 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 21:30:25.775 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 21:51:40.924 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 21:51:40.926 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 21:51:40.927 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 21:51:40.952 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:10:05.539 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:10:05.541 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 22:10:05.542 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 22:10:05.564 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:16:24.106 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:16:24.107 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 22:16:24.108 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 22:16:24.128 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:40:06.467 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:40:06.469 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 22:40:06.470 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 22:40:06.491 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:47:48.473 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:47:48.474 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 22:47:48.477 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 22:47:48.497 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:51:18.065 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 22:51:18.066 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 22:51:18.068 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 22:51:18.090 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 23:25:18.836 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 23:25:18.837 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 23:25:18.839 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 23:25:18.861 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 23:34:39.749 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-29 23:34:39.750 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-29 23:34:39.751 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-29 23:34:39.773 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
|
||||
@@ -394,6 +394,8 @@
|
||||
| unit | VARCHAR(32) | YES | | 单位(文本) |
|
||||
| enum_options | JSON | YES | | 枚举项(type=enum) |
|
||||
| searchable | TINYINT(1) | NOT NULL | 0 | 参与检索 |
|
||||
| fuzzy_searchable | TINYINT(1) | NOT NULL | 0 | 可模糊查询(仅数值型) |
|
||||
| fuzzy_tolerance | DECIMAL(18,6) | YES | | 容差;NULL 使用平台默认 |
|
||||
| dedupe_participate | TINYINT(1) | NOT NULL | 0 | 参与去重键 |
|
||||
| sort_order | INT | NOT NULL | 0 | |
|
||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||
|
||||
@@ -230,7 +230,7 @@ paths:
|
||||
/api/admin/parts:
|
||||
get:
|
||||
summary: 管理端-用户配件列表(✅ Fully Implemented)
|
||||
description: 返回商品列表;支持 shopId/kw 分页查询。
|
||||
description: 返回商品列表;支持 shopId/kw 分页查询。自 0.2.1 起附带模板信息与模板参数JSON。
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Admin-Id
|
||||
@@ -270,7 +270,10 @@ paths:
|
||||
name: { type: string }
|
||||
brand: { type: string }
|
||||
model: { type: string }
|
||||
spec: { type: string }
|
||||
spec: { type: string, nullable: true }
|
||||
templateId: { type: integer, format: int64, nullable: true }
|
||||
templateName: { type: string, nullable: true }
|
||||
attributesJson: { type: string, description: '模板参数JSON字符串;键值对,如 {"内径":10,"长度":200}' }
|
||||
images:
|
||||
type: array
|
||||
items: { type: string, description: '图片URL,可能为绝对或以 / 开头的相对路径' }
|
||||
@@ -455,8 +458,8 @@ paths:
|
||||
createdAt: { type: string, format: date-time }
|
||||
/api/admin/consults/{id}/reply:
|
||||
post:
|
||||
summary: 管理端-回复咨询(✅ Fully Implemented,仅允许一次回复)
|
||||
description: 当该咨询已存在回复时返回 409。
|
||||
summary: 管理端-回复咨询(✅ Fully Implemented,回复后自动标记已解决)
|
||||
description: 当该咨询已存在回复时返回 409。自 0.2.1 起:成功回复后,该咨询状态将自动更新为 resolved。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -467,7 +470,14 @@ paths:
|
||||
content: { type: string }
|
||||
required: [content]
|
||||
responses:
|
||||
'200': { description: 成功 }
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status: { type: string, enum: [open, resolved, closed] }
|
||||
'409': { description: 该咨询已被回复 }
|
||||
/api/admin/consults/{id}/resolve:
|
||||
put:
|
||||
@@ -1173,7 +1183,7 @@ paths:
|
||||
/api/products:
|
||||
get:
|
||||
summary: 商品搜索(✅ Fully Implemented)
|
||||
description: 支持 kw/page/size/categoryId/templateId 以及模板参数过滤。模板参数以 param_ 前缀传入,如 param_颜色=黑、param_内径=10;后端对 JSON attributes 进行 LIKE 匹配(字符串化),多个参数为 AND 关系。返回 {list:[]} 以兼容前端。
|
||||
description: 支持 kw/page/size/categoryId/templateId 以及模板参数过滤。模板参数以 param_ 前缀传入,如 param_颜色=黑、param_内径=10;当某字段在其对应模板中配置为可模糊查询(仅 number 类型)时,按 [v-tol, v+tol](下限截断为0)进行行级匹配,否则等值。未提供 templateId 亦会启用行级模糊(按每行 template_id 判定)。返回 {list:[]} 以兼容前端。
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
@@ -2546,6 +2556,8 @@ paths:
|
||||
unit: { type: string, nullable: true }
|
||||
enumOptions: { type: array, items: { type: string }, nullable: true }
|
||||
searchable: { type: boolean }
|
||||
fuzzySearchable: { type: boolean, description: '仅 type=number 生效' }
|
||||
fuzzyTolerance: { type: number, nullable: true, description: '容差;为空使用平台默认' }
|
||||
dedupeParticipate: { type: boolean }
|
||||
sortOrder: { type: integer }
|
||||
responses: { '200': { description: 成功 } }
|
||||
@@ -2597,6 +2609,8 @@ paths:
|
||||
unit: { type: string, nullable: true }
|
||||
enumOptions: { type: array, items: { type: string }, nullable: true }
|
||||
searchable: { type: boolean }
|
||||
fuzzySearchable: { type: boolean, description: '仅 type=number 生效' }
|
||||
fuzzyTolerance: { type: number, nullable: true, description: '容差;为空使用平台默认' }
|
||||
dedupeParticipate: { type: boolean }
|
||||
sortOrder: { type: integer }
|
||||
responses: { '200': { description: 成功 } }
|
||||
|
||||
@@ -54,7 +54,9 @@
|
||||
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
|
||||
</view>
|
||||
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
|
||||
<!-- 隐藏库存与价格展示,按需求仅展示基础信息 -->
|
||||
<view class="card-params" v-if="it.cardParams && Object.keys(it.cardParams).length">
|
||||
<view class="param" v-for="(v,k) in it.cardParams" :key="k">{{ k }}:{{ v }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
@@ -235,6 +237,8 @@ export default {
|
||||
.tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; }
|
||||
.tag-deleted { font-size: 22rpx; color:#fff; background:#909399; padding: 4rpx 10rpx; border-radius: 8rpx; }
|
||||
.meta { color:$uni-text-color-grey; font-size: 24rpx; }
|
||||
.card-params { display:flex; flex-wrap:wrap; gap: 8rpx 16rpx; margin-top: 8rpx; }
|
||||
.card-params .param { color:$uni-text-color-grey; font-size: 22rpx; background:$uni-bg-color-grey; padding: 2rpx 6rpx; border-radius: 8rpx; }
|
||||
.price { margin-left: 20rpx; color:$uni-color-primary; }
|
||||
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; }
|
||||
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background:$uni-color-primary; color:#fff; border-radius: 50rpx; text-align:center; line-height: 100rpx; font-size: 48rpx; box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15); }
|
||||
|
||||
@@ -132,8 +132,9 @@ export default {
|
||||
.block-title { font-size: 28rpx; font-weight: 600; color: #2d3a4a; margin-bottom: 12rpx; }
|
||||
.placeholder { font-size: 26rpx; color: #7a8899; }
|
||||
.params { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.param { display: flex; justify-content: space-between; font-size: 26rpx; color: #2d3a4a; }
|
||||
.param { display: flex; justify-content: flex-start; align-items: center; gap: 16rpx; font-size: 26rpx; color: #2d3a4a; }
|
||||
.param-key { color: #7a8899; }
|
||||
.param-val { text-align: left; }
|
||||
.images { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; }
|
||||
.image { width: 100%; height: 200rpx; border-radius: 16rpx; background: #f0f2f5; }
|
||||
.footer { display: flex; justify-content: flex-end; gap: 20rpx; }
|
||||
|
||||
BIN
frontend/static/icons/~$配件审核_1759156818563.xlsx
Normal file
BIN
frontend/static/icons/~$配件审核_1759156818563.xlsx
Normal file
Binary file not shown.
BIN
frontend/static/icons/配件审核_1759156818563.xlsx
Normal file
BIN
frontend/static/icons/配件审核_1759156818563.xlsx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2176,7 +2176,29 @@ if (uni.restoreGlobal) {
|
||||
1
|
||||
/* TEXT */
|
||||
),
|
||||
vue.createCommentVNode(" 隐藏库存与价格展示,按需求仅展示基础信息 ")
|
||||
it.cardParams && Object.keys(it.cardParams).length ? (vue.openBlock(), vue.createElementBlock("view", {
|
||||
key: 0,
|
||||
class: "card-params"
|
||||
}, [
|
||||
(vue.openBlock(true), vue.createElementBlock(
|
||||
vue.Fragment,
|
||||
null,
|
||||
vue.renderList(it.cardParams, (v, k) => {
|
||||
return vue.openBlock(), vue.createElementBlock(
|
||||
"view",
|
||||
{
|
||||
class: "param",
|
||||
key: k
|
||||
},
|
||||
vue.toDisplayString(k) + ":" + vue.toDisplayString(v),
|
||||
1
|
||||
/* TEXT */
|
||||
);
|
||||
}),
|
||||
128
|
||||
/* KEYED_FRAGMENT */
|
||||
))
|
||||
])) : vue.createCommentVNode("v-if", true)
|
||||
])
|
||||
], 8, ["onClick"]);
|
||||
}),
|
||||
|
||||
@@ -136,6 +136,19 @@
|
||||
color: #444;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.card-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.card-params .param {
|
||||
color: #444;
|
||||
font-size: 0.6875rem;
|
||||
background: #ffffff;
|
||||
padding: 0.0625rem 0.1875rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.price {
|
||||
margin-left: 0.625rem;
|
||||
color: #4C8DFF;
|
||||
|
||||
@@ -92,13 +92,18 @@
|
||||
}
|
||||
.param {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #2d3a4a;
|
||||
}
|
||||
.param-key {
|
||||
color: #7a8899;
|
||||
}
|
||||
.param-val {
|
||||
text-align: left;
|
||||
}
|
||||
.images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
BIN
frontend/unpackage/dist/dev/app-plus/static/icons/~$配件审核_1759156818563.xlsx
vendored
Normal file
BIN
frontend/unpackage/dist/dev/app-plus/static/icons/~$配件审核_1759156818563.xlsx
vendored
Normal file
Binary file not shown.
BIN
frontend/unpackage/dist/dev/app-plus/static/icons/配件审核_1759156818563.xlsx
vendored
Normal file
BIN
frontend/unpackage/dist/dev/app-plus/static/icons/配件审核_1759156818563.xlsx
vendored
Normal file
Binary file not shown.
@@ -266,8 +266,18 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
g: common_vendor.t(it.brand || "-"),
|
||||
h: common_vendor.t(it.model || ""),
|
||||
i: common_vendor.t(it.spec || ""),
|
||||
j: it.id,
|
||||
k: common_vendor.o(($event) => $options.openDetail(it.id), it.id)
|
||||
j: it.cardParams && Object.keys(it.cardParams).length
|
||||
}, it.cardParams && Object.keys(it.cardParams).length ? {
|
||||
k: common_vendor.f(it.cardParams, (v, k, i1) => {
|
||||
return {
|
||||
a: common_vendor.t(k),
|
||||
b: common_vendor.t(v),
|
||||
c: k
|
||||
};
|
||||
})
|
||||
} : {}, {
|
||||
l: it.id,
|
||||
m: common_vendor.o(($event) => $options.openDetail(it.id), it.id)
|
||||
});
|
||||
})
|
||||
} : {}, {
|
||||
|
||||
@@ -1 +1 @@
|
||||
<view class="page"><view class="tabs"><view class="{{['tab', a && 'active']}}" bindtap="{{b}}">全部</view><view class="{{['tab', c && 'active']}}" bindtap="{{d}}">查询</view><view class="tab extra" bindtap="{{e}}">我的提交</view></view><view wx:if="{{f}}" class="{{['search', x && 'template-mode']}}"><view class="mode"><picker mode="selector" range="{{h}}" bindchange="{{i}}"><view class="picker">{{g}}</view></picker></view><block wx:if="{{j}}"><input placeholder="输入名称/条码/规格查询" bindconfirm="{{k}}" value="{{l}}" bindinput="{{m}}"/></block><block wx:if="{{n}}"><view class="picker-row"><picker mode="selector" range="{{p}}" bindchange="{{q}}"><view class="picker">{{o}}</view></picker><picker mode="selector" range="{{s}}" bindchange="{{t}}"><view class="picker">{{r}}</view></picker></view><view class="params-wrap"><view wx:for="{{v}}" wx:for-item="p" wx:key="w" class="param-row"><input wx:if="{{p.a}}" placeholder="{{p.b}}" value="{{p.c}}" bindinput="{{p.d}}"/><input wx:elif="{{p.e}}" type="number" placeholder="{{p.f}}" value="{{p.g}}" bindinput="{{p.h}}"/><switch wx:elif="{{p.i}}" checked="{{p.j}}" bindchange="{{p.k}}"/><picker wx:elif="{{p.l}}" mode="selector" range="{{p.n}}" bindchange="{{p.o}}"><view class="picker">{{p.m}}</view></picker><picker wx:elif="{{p.p}}" mode="date" bindchange="{{p.r}}"><view class="picker">{{p.q}}</view></picker><input wx:else placeholder="{{p.s}}" value="{{p.t}}" bindinput="{{p.v}}"/></view></view></block><button size="mini" bindtap="{{w}}">查询</button></view><scroll-view scroll-y class="list" bindscrolltolower="{{A}}"><block wx:if="{{y}}"><view wx:for="{{z}}" wx:for-item="it" wx:key="j" class="item" bindtap="{{it.k}}"><image wx:if="{{it.a}}" src="{{it.b}}" class="thumb" mode="aspectFill"/><view class="content"><view class="name"><text>{{it.c}}</text><text wx:if="{{it.d}}" class="tag-deleted">已删除</text><text wx:if="{{it.e}}" class="tag-platform">平台推荐</text><text wx:elif="{{it.f}}" class="tag-custom">我的提交</text></view><view class="meta">{{it.g}} {{it.h}} {{it.i}}</view></view></view></block><view wx:else class="empty"><text>暂无数据,点击右上角“+”新增</text></view></scroll-view></view>
|
||||
<view class="page"><view class="tabs"><view class="{{['tab', a && 'active']}}" bindtap="{{b}}">全部</view><view class="{{['tab', c && 'active']}}" bindtap="{{d}}">查询</view><view class="tab extra" bindtap="{{e}}">我的提交</view></view><view wx:if="{{f}}" class="{{['search', x && 'template-mode']}}"><view class="mode"><picker mode="selector" range="{{h}}" bindchange="{{i}}"><view class="picker">{{g}}</view></picker></view><block wx:if="{{j}}"><input placeholder="输入名称/条码/规格查询" bindconfirm="{{k}}" value="{{l}}" bindinput="{{m}}"/></block><block wx:if="{{n}}"><view class="picker-row"><picker mode="selector" range="{{p}}" bindchange="{{q}}"><view class="picker">{{o}}</view></picker><picker mode="selector" range="{{s}}" bindchange="{{t}}"><view class="picker">{{r}}</view></picker></view><view class="params-wrap"><view wx:for="{{v}}" wx:for-item="p" wx:key="w" class="param-row"><input wx:if="{{p.a}}" placeholder="{{p.b}}" value="{{p.c}}" bindinput="{{p.d}}"/><input wx:elif="{{p.e}}" type="number" placeholder="{{p.f}}" value="{{p.g}}" bindinput="{{p.h}}"/><switch wx:elif="{{p.i}}" checked="{{p.j}}" bindchange="{{p.k}}"/><picker wx:elif="{{p.l}}" mode="selector" range="{{p.n}}" bindchange="{{p.o}}"><view class="picker">{{p.m}}</view></picker><picker wx:elif="{{p.p}}" mode="date" bindchange="{{p.r}}"><view class="picker">{{p.q}}</view></picker><input wx:else placeholder="{{p.s}}" value="{{p.t}}" bindinput="{{p.v}}"/></view></view></block><button size="mini" bindtap="{{w}}">查询</button></view><scroll-view scroll-y class="list" bindscrolltolower="{{A}}"><block wx:if="{{y}}"><view wx:for="{{z}}" wx:for-item="it" wx:key="l" class="item" bindtap="{{it.m}}"><image wx:if="{{it.a}}" src="{{it.b}}" class="thumb" mode="aspectFill"/><view class="content"><view class="name"><text>{{it.c}}</text><text wx:if="{{it.d}}" class="tag-deleted">已删除</text><text wx:if="{{it.e}}" class="tag-platform">平台推荐</text><text wx:elif="{{it.f}}" class="tag-custom">我的提交</text></view><view class="meta">{{it.g}} {{it.h}} {{it.i}}</view><view wx:if="{{it.j}}" class="card-params"><view wx:for="{{it.k}}" wx:for-item="v" wx:key="c" class="param">{{v.a}}:{{v.b}}</view></view></view></view></block><view wx:else class="empty"><text>暂无数据,点击右上角“+”新增</text></view></scroll-view></view>
|
||||
@@ -136,6 +136,19 @@
|
||||
color: #444;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.card-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.card-params .param {
|
||||
color: #444;
|
||||
font-size: 22rpx;
|
||||
background: #ffffff;
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.price {
|
||||
margin-left: 20rpx;
|
||||
color: #4C8DFF;
|
||||
|
||||
@@ -92,13 +92,18 @@
|
||||
}
|
||||
.param {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2d3a4a;
|
||||
}
|
||||
.param-key {
|
||||
color: #7a8899;
|
||||
}
|
||||
.param-val {
|
||||
text-align: left;
|
||||
}
|
||||
.images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
BIN
frontend/unpackage/dist/dev/mp-weixin/static/icons/~$配件审核_1759156818563.xlsx
vendored
Normal file
BIN
frontend/unpackage/dist/dev/mp-weixin/static/icons/~$配件审核_1759156818563.xlsx
vendored
Normal file
Binary file not shown.
BIN
frontend/unpackage/dist/dev/mp-weixin/static/icons/配件审核_1759156818563.xlsx
vendored
Normal file
BIN
frontend/unpackage/dist/dev/mp-weixin/static/icons/配件审核_1759156818563.xlsx
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user