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_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
|
||||||
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
|
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> =
|
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] } :
|
N1 extends keyof LocalComponents ? { [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] } :
|
N2 extends keyof LocalComponents ? { [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] } :
|
N3 extends keyof LocalComponents ? { [K in N0]: LocalComponents[N3] } :
|
||||||
Self extends object ? { [K in N0]: Self } :
|
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] } :
|
N1 extends keyof __VLS_GlobalComponents ? { [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] } :
|
N2 extends keyof __VLS_GlobalComponents ? { [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] } :
|
N3 extends keyof __VLS_GlobalComponents ? { [K in N0]: __VLS_GlobalComponents[N3] } :
|
||||||
{};
|
{};
|
||||||
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
|
type __VLS_FunctionalComponentCtx<T, K> = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny<K, {}>
|
||||||
? K extends { __ctx?: infer Ctx } ? NonNullable<Ctx> : never : any
|
? 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<
|
type __VLS_ResolveEmits<
|
||||||
Comp,
|
Comp,
|
||||||
Emits,
|
Emits,
|
||||||
@@ -90,10 +94,16 @@ export {};
|
|||||||
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
|
NormalizedEmits = __VLS_NormalizeEmits<Emits> extends infer E ? string extends keyof E ? {} : E : never,
|
||||||
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
|
> = __VLS_SpreadMerge<NormalizedEmits, TypeEmits>;
|
||||||
type __VLS_ResolveDirectives<T> = {
|
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_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_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): [
|
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
|
||||||
item: T extends number ? number
|
item: T extends number ? number
|
||||||
@@ -115,7 +125,6 @@ export {};
|
|||||||
: T extends (...args: any) => any
|
: T extends (...args: any) => any
|
||||||
? T
|
? T
|
||||||
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
|
: (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):
|
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 new (...args: any) => any ? __VLS_FunctionalComponent<K>
|
||||||
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
|
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
|
||||||
|
|||||||
@@ -64,7 +64,13 @@ async function fetch(){
|
|||||||
}
|
}
|
||||||
function reset(){ q.status='open'; q.kw=''; fetch() }
|
function reset(){ q.status='open'; q.kw=''; fetch() }
|
||||||
function openReply(row: any){ visible.value = true; current.value = row; reply.value='' }
|
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' }
|
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: '知道了' }) }
|
function viewReply(row: any){ ElMessageBox.alert(row.replyContent || '无', '已回复内容', { confirmButtonText: '知道了' }) }
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" style="padding:0;">
|
<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 type="index" width="60" label="#" />
|
||||||
<el-table-column prop="model" label="型号" width="160" />
|
<el-table-column prop="model" label="型号" width="160" />
|
||||||
<el-table-column prop="name" label="名称" min-width="180" />
|
<el-table-column prop="name" label="名称" min-width="180" />
|
||||||
@@ -66,36 +78,8 @@
|
|||||||
|
|
||||||
<el-dialog v-model="detail.visible" title="配件详情" width="720px">
|
<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 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-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>
|
||||||
<el-form-item label="图片">
|
<el-form-item label="图片">
|
||||||
<div class="thumb-list">
|
<div class="thumb-list">
|
||||||
@@ -142,6 +126,8 @@ const loading = ref(false)
|
|||||||
const exporting = 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 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 paramRows = ref<any[]>([])
|
||||||
const jsonPlaceholder = '{"key":"value"}'
|
const jsonPlaceholder = '{"key":"value"}'
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -179,6 +165,9 @@ async function fetchList(){
|
|||||||
const res = await get('/api/admin/parts/submissions', params)
|
const res = await get('/api/admin/parts/submissions', params)
|
||||||
rows.value = Array.isArray(res?.list) ? res.list : []
|
rows.value = Array.isArray(res?.list) ? res.list : []
|
||||||
total.value = Number(res?.total || rows.value.length)
|
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) {
|
} catch (e:any) {
|
||||||
ElMessage.error(e.message || '加载失败')
|
ElMessage.error(e.message || '加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -194,6 +183,33 @@ function reset(){
|
|||||||
}
|
}
|
||||||
function onPage(p:number){ query.page = p; fetchList() }
|
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){
|
async function openDetail(id:number){
|
||||||
try {
|
try {
|
||||||
const data = await get(`/api/admin/parts/submissions/${id}`)
|
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-list { display:flex; flex-wrap:wrap; gap:12px; }
|
||||||
.thumb { width: 120px; display:flex; flex-direction: column; gap: 8px; }
|
.thumb { width: 120px; display:flex; flex-direction: column; gap: 8px; }
|
||||||
.thumb .el-image { width: 120px; height: 120px; border-radius: 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>
|
</style>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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 :model="form" label-width="100px">
|
||||||
<el-form-item label="分类">
|
<el-form-item label="分类">
|
||||||
<el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
|
<el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
|
||||||
@@ -60,20 +60,14 @@
|
|||||||
<el-table-column label="#" width="48">
|
<el-table-column label="#" width="48">
|
||||||
<template #default="{ $index }">{{ $index+1 }}</template>
|
<template #default="{ $index }">{{ $index+1 }}</template>
|
||||||
</el-table-column>
|
</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="名称">
|
<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>
|
||||||
<el-table-column label="类型" width="120">
|
<el-table-column label="类型" width="120">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
|
<el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
|
||||||
<el-option label="string" value="string" />
|
<el-option label="数字" value="number" />
|
||||||
<el-option label="number" value="number" />
|
<el-option label="非数字" value="string" />
|
||||||
<el-option label="boolean" value="boolean" />
|
|
||||||
<el-option label="enum" value="enum" />
|
|
||||||
<el-option label="date" value="date" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -83,15 +77,25 @@
|
|||||||
<el-table-column label="单位" width="120">
|
<el-table-column label="单位" width="120">
|
||||||
<template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
|
<template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
|
||||||
</el-table-column>
|
</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>
|
||||||
<el-table-column label="检索" width="80">
|
<el-table-column label="卡片展示" width="100">
|
||||||
<template #default="{row}"><el-switch v-model="row.searchable" :disabled="!!dlg.id" /></template>
|
<template #default="{row}">
|
||||||
|
<el-switch v-model="row.cardDisplay" :disabled="!!dlg.id" />
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="去重" width="80">
|
<el-table-column label="容差" width="160">
|
||||||
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" :disabled="!!dlg.id" /></template>
|
<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>
|
||||||
|
|
||||||
<el-table-column label="操作" width="90">
|
<el-table-column label="操作" width="90">
|
||||||
<template #default="{ $index }">
|
<template #default="{ $index }">
|
||||||
<el-button v-if="!dlg.id" text type="danger" @click="removeParam($index)">删除</el-button>
|
<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 => {
|
http.get(`/api/admin/part-templates/${row.id}`).then(res => {
|
||||||
const d = res.data
|
const d = res.data
|
||||||
Object.assign(form, { id: d.id, categoryId: d.categoryId, name: d.name, modelRule: d.modelRule, status: d.status,
|
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() {
|
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 removeParam(i:number) { form.params.splice(i,1) }
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
// 先为所有行生成 fieldKey(基于名称拼音首字母),并消重
|
||||||
|
genAllKeys()
|
||||||
// 校验
|
// 校验
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
for (const p of form.params) {
|
for (const p of form.params) {
|
||||||
@@ -160,13 +168,27 @@ function save() {
|
|||||||
if (!key) { ElMessage.warning('参数键不能为空'); return }
|
if (!key) { ElMessage.warning('参数键不能为空'); return }
|
||||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { ElMessage.warning(`参数键不合法: ${key}`); return }
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { ElMessage.warning(`参数键不合法: ${key}`); return }
|
||||||
if (!label) { ElMessage.warning('参数名称不能为空'); 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 }
|
if (seen.has(key)) { ElMessage.warning(`参数键重复: ${key}`); return }
|
||||||
seen.add(key)
|
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,
|
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,
|
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(()=>{
|
http.post('/api/admin/part-templates', payload).then(()=>{
|
||||||
ElMessage.success('创建成功'); dlg.visible=false; load()
|
ElMessage.success('创建成功'); dlg.visible=false; load()
|
||||||
})
|
})
|
||||||
@@ -183,6 +205,51 @@ function doDelete(row:any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(()=>{ loadCategories(); load() })
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -18,11 +18,23 @@
|
|||||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||||
<el-table-column prop="brand" label="品牌" width="120" />
|
<el-table-column prop="brand" label="品牌" width="120" />
|
||||||
<el-table-column prop="model" label="型号" width="160" />
|
<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">
|
<el-table-column label="图片" width="160">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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() }
|
async function save(){ await put(`/api/admin/parts/${form.id}`, form); visible.value=false; await fetch() }
|
||||||
|
|
||||||
onMounted(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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -24,28 +24,15 @@
|
|||||||
<el-table-column label="状态" width="120">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{row}"><el-tag :type="row.status===1?'success':'danger'">{{ row.status===1?'正常':'黑名单' }}</el-tag></template>
|
<template #default="{row}"><el-tag :type="row.status===1?'success':'danger'">{{ row.status===1?'正常':'黑名单' }}</el-tag></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column fixed="right" label="操作" width="240">
|
<el-table-column fixed="right" label="操作" width="160">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
|
||||||
<el-button size="small" @click="toggle(row)">{{ row.status===1?'拉黑':'恢复' }}</el-button>
|
<el-button size="small" @click="toggle(row)">{{ row.status===1?'拉黑':'恢复' }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -55,8 +42,6 @@ import { get, put } from '../../api/http'
|
|||||||
|
|
||||||
const q = reactive({ kw: '' })
|
const q = reactive({ kw: '' })
|
||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
const visible = ref(false)
|
|
||||||
const form = reactive<any>({})
|
|
||||||
|
|
||||||
async function fetch(){
|
async function fetch(){
|
||||||
// 需要后端提供:GET /api/admin/users?kw=...
|
// 需要后端提供:GET /api/admin/users?kw=...
|
||||||
@@ -64,8 +49,6 @@ async function fetch(){
|
|||||||
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
|
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
|
||||||
}
|
}
|
||||||
function reset(){ q.kw=''; fetch() }
|
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){
|
async function toggle(row: any){
|
||||||
const newStatus = row.status===1?0:1
|
const newStatus = row.status===1?0:1
|
||||||
await put(`/api/admin/users/${row.id}`, { status: newStatus })
|
await put(`/api/admin/users/${row.id}`, { status: newStatus })
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="fetch">查询</el-button>
|
<el-button type="primary" @click="fetch">查询</el-button>
|
||||||
<el-button @click="reset">重置</el-button>
|
<el-button @click="reset">重置</el-button>
|
||||||
<el-button type="success" @click="openEdit()">新增VIP</el-button>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,9 +18,6 @@
|
|||||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||||
<el-table-column prop="name" label="姓名" width="140" />
|
<el-table-column prop="name" label="姓名" width="140" />
|
||||||
<el-table-column prop="phone" label="手机号" width="160" />
|
<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 prop="expireAt" label="到期时间" width="180" />
|
||||||
<el-table-column label="状态" width="120">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
@@ -30,23 +26,22 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column fixed="right" label="操作" width="220">
|
<el-table-column fixed="right" label="操作" width="220">
|
||||||
<template #default="{row}">
|
<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>
|
<el-button size="small" @click="toggle(row)">{{ row.status===1?'停用':'启用' }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="visible" :title="editing?'编辑VIP':'新增VIP'" width="520">
|
<el-dialog v-model="expireVisible" title="设置到期时间" width="420">
|
||||||
<el-form :model="form" label-width="96px">
|
<el-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="到期时间">
|
||||||
<el-form-item label="是否VIP"><el-switch v-model="form.isVip" :active-value="1" :inactive-value="0" /></el-form-item>
|
<el-date-picker v-model="expireAtVal" type="datetime" placeholder="选择到期时间" />
|
||||||
<el-form-item label="到期时间"><el-date-picker v-model="form.expireAt" type="datetime" placeholder="选择到期时间" /></el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible=false">取消</el-button>
|
<el-button @click="expireVisible=false">取消</el-button>
|
||||||
<el-button type="primary" @click="save">保存</el-button>
|
<el-button type="primary" @click="saveExpire">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -56,13 +51,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { get, post, put } from '../../api/http'
|
import { get, put } from '../../api/http'
|
||||||
|
|
||||||
const q = reactive({ phone: '' })
|
const q = reactive({ phone: '' })
|
||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
const visible = ref(false)
|
|
||||||
const editing = ref(false)
|
const expireVisible = ref(false)
|
||||||
const form = reactive({ id: 0, userId: 0, isVip: 1, expireAt: '', status: 1 })
|
const expireAtVal = ref<any>('')
|
||||||
|
const expireRow = ref<any>(null)
|
||||||
|
|
||||||
async function fetch(){
|
async function fetch(){
|
||||||
// 需要后端提供:GET /api/admin/vips?phone=... (或 /api/users + /api/vips 关联)
|
// 需要后端提供: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:[])
|
rows.value = Array.isArray(data?.list) ? data.list : (Array.isArray(data)?data:[])
|
||||||
}
|
}
|
||||||
function reset(){ q.phone=''; fetch() }
|
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){
|
async function toggle(row: any){
|
||||||
await put(`/api/admin/vips/${row.id}`, { ...row, status: row.status===1?0:1 })
|
await put(`/api/admin/vips/${row.id}`, { ...row, status: row.status===1?0:1 })
|
||||||
await fetch()
|
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)
|
onMounted(fetch)
|
||||||
@@ -98,3 +93,4 @@ onMounted(fetch)
|
|||||||
</style>
|
</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()));
|
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);
|
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")
|
@PutMapping("/{id}/resolve")
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ public class AdminPartController {
|
|||||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||||
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
int offset = Math.max(0, page - 1) * Math.max(1, size);
|
||||||
StringBuilder sql = new StringBuilder(
|
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 " +
|
"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");
|
"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<>();
|
List<Object> ps = new ArrayList<>();
|
||||||
if (shopId != null) { sql.append(" AND p.shop_id=?"); ps.add(shopId); }
|
if (shopId != null) { sql.append(" AND p.shop_id=?"); ps.add(shopId); }
|
||||||
if (kw != null && !kw.isBlank()) {
|
if (kw != null && !kw.isBlank()) {
|
||||||
@@ -50,6 +51,10 @@ public class AdminPartController {
|
|||||||
m.put("brand", rs.getString("brand"));
|
m.put("brand", rs.getString("brand"));
|
||||||
m.put("model", rs.getString("model"));
|
m.put("model", rs.getString("model"));
|
||||||
m.put("spec", rs.getString("spec"));
|
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;
|
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>>(){});
|
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("enumOptions", enums);
|
||||||
pm.put("searchable", p.getSearchable());
|
pm.put("searchable", p.getSearchable());
|
||||||
pm.put("dedupeParticipate", p.getDedupeParticipate());
|
// 不再暴露 dedupeParticipate
|
||||||
pm.put("sortOrder", p.getSortOrder());
|
pm.put("sortOrder", p.getSortOrder());
|
||||||
ps.add(pm);
|
ps.add(pm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.example.demo.product.dto;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class PartTemplateDtos {
|
public class PartTemplateDtos {
|
||||||
|
|
||||||
@@ -13,8 +12,11 @@ public class PartTemplateDtos {
|
|||||||
public boolean required;
|
public boolean required;
|
||||||
public String unit; // 自定义单位文本
|
public String unit; // 自定义单位文本
|
||||||
public List<String> enumOptions; // type=enum 时可用
|
public List<String> enumOptions; // type=enum 时可用
|
||||||
public boolean searchable;
|
public boolean searchable; // 默认参与搜索;前端不再展示开关
|
||||||
public boolean dedupeParticipate;
|
public boolean fuzzySearchable; // 仅 type=number 生效
|
||||||
|
public java.math.BigDecimal fuzzyTolerance; // 可空=使用默认
|
||||||
|
public boolean cardDisplay; // 是否在用户端货品卡片展示
|
||||||
|
// public boolean dedupeParticipate; // 已废弃,后端忽略
|
||||||
public int sortOrder;
|
public int sortOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class ProductDtos {
|
|||||||
public BigDecimal retailPrice; // from product_prices
|
public BigDecimal retailPrice; // from product_prices
|
||||||
public String cover; // first image url
|
public String cover; // first image url
|
||||||
public Boolean deleted; // derived from deleted_at
|
public Boolean deleted; // derived from deleted_at
|
||||||
|
public java.util.Map<String, String> cardParams; // 货品卡片展示的参数(最多4个,label->value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProductDetail {
|
public static class ProductDetail {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.demo.product.entity;
|
package com.example.demo.product.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -36,7 +37,16 @@ public class PartTemplateParam {
|
|||||||
private Boolean searchable;
|
private Boolean searchable;
|
||||||
|
|
||||||
@Column(name = "dedupe_participate", nullable = false)
|
@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)
|
@Column(name = "sort_order", nullable = false)
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
@@ -64,8 +74,14 @@ public class PartTemplateParam {
|
|||||||
public void setEnumOptionsJson(String enumOptionsJson) { this.enumOptionsJson = enumOptionsJson; }
|
public void setEnumOptionsJson(String enumOptionsJson) { this.enumOptionsJson = enumOptionsJson; }
|
||||||
public Boolean getSearchable() { return searchable; }
|
public Boolean getSearchable() { return searchable; }
|
||||||
public void setSearchable(Boolean searchable) { this.searchable = searchable; }
|
public void setSearchable(Boolean searchable) { this.searchable = searchable; }
|
||||||
public Boolean getDedupeParticipate() { return dedupeParticipate; }
|
public Boolean getDedupeParticipate() { return dedupeParticipate == null ? Boolean.FALSE : dedupeParticipate; }
|
||||||
public void setDedupeParticipate(Boolean dedupeParticipate) { this.dedupeParticipate = 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 Integer getSortOrder() { return sortOrder; }
|
||||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
|||||||
@@ -164,8 +164,13 @@ public class PartTemplateService {
|
|||||||
p.setRequired(def.required);
|
p.setRequired(def.required);
|
||||||
p.setUnit(def.unit);
|
p.setUnit(def.unit);
|
||||||
p.setEnumOptionsJson(def.enumOptions == null ? null : JsonUtils.toJson(def.enumOptions));
|
p.setEnumOptionsJson(def.enumOptions == null ? null : JsonUtils.toJson(def.enumOptions));
|
||||||
p.setSearchable(def.searchable);
|
// 搜索默认参与:若前端未传,置为 true
|
||||||
p.setDedupeParticipate(def.dedupeParticipate);
|
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.setSortOrder(def.sortOrder == 0 ? idx : def.sortOrder);
|
||||||
p.setCreatedAt(now);
|
p.setCreatedAt(now);
|
||||||
p.setUpdatedAt(now);
|
p.setUpdatedAt(now);
|
||||||
@@ -185,7 +190,10 @@ public class PartTemplateService {
|
|||||||
d.unit = p.getUnit();
|
d.unit = p.getUnit();
|
||||||
d.enumOptions = JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>(){});
|
d.enumOptions = JsonUtils.fromJson(p.getEnumOptionsJson(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>(){});
|
||||||
d.searchable = Boolean.TRUE.equals(p.getSearchable());
|
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();
|
d.sortOrder = p.getSortOrder() == null ? 0 : p.getSortOrder();
|
||||||
out.add(d);
|
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))) {
|
if (!("string".equals(type) || "number".equals(type) || "boolean".equals(type) || "enum".equals(type) || "date".equals(type))) {
|
||||||
throw new IllegalArgumentException("不支持的参数类型: " + 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);
|
if (keys.contains(key)) throw new IllegalArgumentException("参数键重复: " + key);
|
||||||
keys.add(key);
|
keys.add(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,20 @@ public class ProductService {
|
|||||||
private final InventoryRepository inventoryRepository;
|
private final InventoryRepository inventoryRepository;
|
||||||
private final ProductImageRepository imageRepository;
|
private final ProductImageRepository imageRepository;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final com.example.demo.common.SearchFuzzyProperties fuzzyProps;
|
||||||
|
|
||||||
public ProductService(ProductRepository productRepository,
|
public ProductService(ProductRepository productRepository,
|
||||||
ProductPriceRepository priceRepository,
|
ProductPriceRepository priceRepository,
|
||||||
InventoryRepository inventoryRepository,
|
InventoryRepository inventoryRepository,
|
||||||
ProductImageRepository imageRepository,
|
ProductImageRepository imageRepository,
|
||||||
JdbcTemplate jdbcTemplate) {
|
JdbcTemplate jdbcTemplate,
|
||||||
|
com.example.demo.common.SearchFuzzyProperties fuzzyProps) {
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.priceRepository = priceRepository;
|
this.priceRepository = priceRepository;
|
||||||
this.inventoryRepository = inventoryRepository;
|
this.inventoryRepository = inventoryRepository;
|
||||||
this.imageRepository = imageRepository;
|
this.imageRepository = imageRepository;
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.fuzzyProps = fuzzyProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -121,10 +124,7 @@ public class ProductService {
|
|||||||
product.setAttributesJson(submission.getAttributesJson());
|
product.setAttributesJson(submission.getAttributesJson());
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (submission.getDedupeKey() != null && !submission.getDedupeKey().isBlank()) {
|
// 忽略 dedupeKey
|
||||||
product.setDedupeKey(submission.getDedupeKey());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
product.setUpdatedAt(LocalDateTime.now());
|
product.setUpdatedAt(LocalDateTime.now());
|
||||||
productRepository.save(product);
|
productRepository.save(product);
|
||||||
@@ -149,15 +149,46 @@ public class ProductService {
|
|||||||
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
|
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 (templateId != null) { sql.append(" AND p.template_id=?"); ps.add(templateId); }
|
||||||
if (paramFilters != null && !paramFilters.isEmpty()) {
|
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()) {
|
for (java.util.Map.Entry<String,String> ent : paramFilters.entrySet()) {
|
||||||
String key = ent.getKey(); String val = ent.getValue();
|
String key = ent.getKey(); String val = ent.getValue();
|
||||||
if (key == null || key.isBlank() || val == null || val.isBlank()) continue;
|
if (key == null || key.isBlank() || val == null || val.isBlank()) continue;
|
||||||
// 精确匹配参数值:将 JSON 值解包后与入参做等值比较,避免 LIKE 导致的误匹配
|
|
||||||
|
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, ?)) = ?");
|
sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
|
||||||
ps.add("$." + key);
|
ps.add("$." + key);
|
||||||
ps.add(val.trim());
|
ps.add(val.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
|
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
|
||||||
ps.add(size); ps.add(page * size);
|
ps.add(size); ps.add(page * size);
|
||||||
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
|
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
|
||||||
@@ -173,11 +204,49 @@ public class ProductService {
|
|||||||
it.retailPrice = rp;
|
it.retailPrice = rp;
|
||||||
it.cover = rs.getString("cover");
|
it.cover = rs.getString("cover");
|
||||||
it.deleted = rs.getBoolean("deleted");
|
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;
|
return it;
|
||||||
}, ps.toArray());
|
}, ps.toArray());
|
||||||
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
|
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) {
|
public Optional<ProductDtos.ProductDetail> findDetail(Long id, boolean includeDeleted) {
|
||||||
Optional<Product> op = productRepository.findById(id);
|
Optional<Product> op = productRepository.findById(id);
|
||||||
if (op.isEmpty()) return Optional.empty();
|
if (op.isEmpty()) return Optional.empty();
|
||||||
@@ -235,7 +304,7 @@ public class ProductService {
|
|||||||
p.setOrigin(emptyToNull(req.origin));
|
p.setOrigin(emptyToNull(req.origin));
|
||||||
p.setCategoryId(req.categoryId);
|
p.setCategoryId(req.categoryId);
|
||||||
// 单位字段已移除
|
// 单位字段已移除
|
||||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
// 忽略 dedupeKey
|
||||||
p.setSafeMin(req.safeMin);
|
p.setSafeMin(req.safeMin);
|
||||||
p.setSafeMax(req.safeMax);
|
p.setSafeMax(req.safeMax);
|
||||||
p.setDescription(emptyToNull(req.remark));
|
p.setDescription(emptyToNull(req.remark));
|
||||||
@@ -290,7 +359,7 @@ public class ProductService {
|
|||||||
p.setOrigin(emptyToNull(req.origin));
|
p.setOrigin(emptyToNull(req.origin));
|
||||||
p.setCategoryId(req.categoryId);
|
p.setCategoryId(req.categoryId);
|
||||||
// 单位字段已移除
|
// 单位字段已移除
|
||||||
p.setDedupeKey(emptyToNull(req.dedupeKey));
|
// 忽略 dedupeKey
|
||||||
p.setSafeMin(req.safeMin);
|
p.setSafeMin(req.safeMin);
|
||||||
p.setSafeMax(req.safeMax);
|
p.setSafeMax(req.safeMax);
|
||||||
p.setDescription(emptyToNull(req.remark));
|
p.setDescription(emptyToNull(req.remark));
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class ProductSubmissionService {
|
|||||||
submission.setBarcode(req.barcode);
|
submission.setBarcode(req.barcode);
|
||||||
submission.setSafeMin(req.safeMin);
|
submission.setSafeMin(req.safeMin);
|
||||||
submission.setSafeMax(req.safeMax);
|
submission.setSafeMax(req.safeMax);
|
||||||
submission.setDedupeKey(buildDedupeKey(req.templateId, req.name, req.model, req.brand, req.parameters));
|
// 按“前端隐藏 + 后端忽略”方案:不再计算/使用 dedupeKey(兼容历史字段保留)
|
||||||
submission.setStatus(ProductSubmission.Status.pending);
|
submission.setStatus(ProductSubmission.Status.pending);
|
||||||
submission.setCreatedAt(LocalDateTime.now());
|
submission.setCreatedAt(LocalDateTime.now());
|
||||||
submission.setUpdatedAt(LocalDateTime.now());
|
submission.setUpdatedAt(LocalDateTime.now());
|
||||||
@@ -223,9 +223,7 @@ public class ProductSubmissionService {
|
|||||||
if (req.barcode != null) submission.setBarcode(req.barcode);
|
if (req.barcode != null) submission.setBarcode(req.barcode);
|
||||||
if (req.safeMin != null) submission.setSafeMin(req.safeMin);
|
if (req.safeMin != null) submission.setSafeMin(req.safeMin);
|
||||||
if (req.safeMax != null) submission.setSafeMax(req.safeMax);
|
if (req.safeMax != null) submission.setSafeMax(req.safeMax);
|
||||||
// 参数或基础字段变更后重算去重键
|
// 不再重算 dedupeKey(忽略去重键)
|
||||||
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));
|
|
||||||
submission.setUpdatedAt(LocalDateTime.now());
|
submission.setUpdatedAt(LocalDateTime.now());
|
||||||
submissionRepository.save(submission);
|
submissionRepository.save(submission);
|
||||||
}
|
}
|
||||||
@@ -472,7 +470,7 @@ public class ProductSubmissionService {
|
|||||||
payload.origin = submission.getOrigin();
|
payload.origin = submission.getOrigin();
|
||||||
payload.categoryId = submission.getCategoryId();
|
payload.categoryId = submission.getCategoryId();
|
||||||
// 单位字段已移除
|
// 单位字段已移除
|
||||||
payload.dedupeKey = submission.getDedupeKey();
|
// 不再透传 dedupeKey
|
||||||
payload.safeMin = submission.getSafeMin();
|
payload.safeMin = submission.getSafeMin();
|
||||||
payload.safeMax = submission.getSafeMax();
|
payload.safeMax = submission.getSafeMax();
|
||||||
payload.remark = submission.getRemarkText();
|
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
|
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.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.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: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 | | 单位(文本) |
|
| unit | VARCHAR(32) | YES | | 单位(文本) |
|
||||||
| enum_options | JSON | YES | | 枚举项(type=enum) |
|
| enum_options | JSON | YES | | 枚举项(type=enum) |
|
||||||
| searchable | TINYINT(1) | NOT NULL | 0 | 参与检索 |
|
| 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 | 参与去重键 |
|
| dedupe_participate | TINYINT(1) | NOT NULL | 0 | 参与去重键 |
|
||||||
| sort_order | INT | NOT NULL | 0 | |
|
| sort_order | INT | NOT NULL | 0 | |
|
||||||
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ paths:
|
|||||||
/api/admin/parts:
|
/api/admin/parts:
|
||||||
get:
|
get:
|
||||||
summary: 管理端-用户配件列表(✅ Fully Implemented)
|
summary: 管理端-用户配件列表(✅ Fully Implemented)
|
||||||
description: 返回商品列表;支持 shopId/kw 分页查询。
|
description: 返回商品列表;支持 shopId/kw 分页查询。自 0.2.1 起附带模板信息与模板参数JSON。
|
||||||
parameters:
|
parameters:
|
||||||
- in: header
|
- in: header
|
||||||
name: X-Admin-Id
|
name: X-Admin-Id
|
||||||
@@ -270,7 +270,10 @@ paths:
|
|||||||
name: { type: string }
|
name: { type: string }
|
||||||
brand: { type: string }
|
brand: { type: string }
|
||||||
model: { 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:
|
images:
|
||||||
type: array
|
type: array
|
||||||
items: { type: string, description: '图片URL,可能为绝对或以 / 开头的相对路径' }
|
items: { type: string, description: '图片URL,可能为绝对或以 / 开头的相对路径' }
|
||||||
@@ -455,8 +458,8 @@ paths:
|
|||||||
createdAt: { type: string, format: date-time }
|
createdAt: { type: string, format: date-time }
|
||||||
/api/admin/consults/{id}/reply:
|
/api/admin/consults/{id}/reply:
|
||||||
post:
|
post:
|
||||||
summary: 管理端-回复咨询(✅ Fully Implemented,仅允许一次回复)
|
summary: 管理端-回复咨询(✅ Fully Implemented,回复后自动标记已解决)
|
||||||
description: 当该咨询已存在回复时返回 409。
|
description: 当该咨询已存在回复时返回 409。自 0.2.1 起:成功回复后,该咨询状态将自动更新为 resolved。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -467,7 +470,14 @@ paths:
|
|||||||
content: { type: string }
|
content: { type: string }
|
||||||
required: [content]
|
required: [content]
|
||||||
responses:
|
responses:
|
||||||
'200': { description: 成功 }
|
'200':
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status: { type: string, enum: [open, resolved, closed] }
|
||||||
'409': { description: 该咨询已被回复 }
|
'409': { description: 该咨询已被回复 }
|
||||||
/api/admin/consults/{id}/resolve:
|
/api/admin/consults/{id}/resolve:
|
||||||
put:
|
put:
|
||||||
@@ -1173,7 +1183,7 @@ paths:
|
|||||||
/api/products:
|
/api/products:
|
||||||
get:
|
get:
|
||||||
summary: 商品搜索(✅ Fully Implemented)
|
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:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
name: kw
|
name: kw
|
||||||
@@ -2546,6 +2556,8 @@ paths:
|
|||||||
unit: { type: string, nullable: true }
|
unit: { type: string, nullable: true }
|
||||||
enumOptions: { type: array, items: { type: string }, nullable: true }
|
enumOptions: { type: array, items: { type: string }, nullable: true }
|
||||||
searchable: { type: boolean }
|
searchable: { type: boolean }
|
||||||
|
fuzzySearchable: { type: boolean, description: '仅 type=number 生效' }
|
||||||
|
fuzzyTolerance: { type: number, nullable: true, description: '容差;为空使用平台默认' }
|
||||||
dedupeParticipate: { type: boolean }
|
dedupeParticipate: { type: boolean }
|
||||||
sortOrder: { type: integer }
|
sortOrder: { type: integer }
|
||||||
responses: { '200': { description: 成功 } }
|
responses: { '200': { description: 成功 } }
|
||||||
@@ -2597,6 +2609,8 @@ paths:
|
|||||||
unit: { type: string, nullable: true }
|
unit: { type: string, nullable: true }
|
||||||
enumOptions: { type: array, items: { type: string }, nullable: true }
|
enumOptions: { type: array, items: { type: string }, nullable: true }
|
||||||
searchable: { type: boolean }
|
searchable: { type: boolean }
|
||||||
|
fuzzySearchable: { type: boolean, description: '仅 type=number 生效' }
|
||||||
|
fuzzyTolerance: { type: number, nullable: true, description: '容差;为空使用平台默认' }
|
||||||
dedupeParticipate: { type: boolean }
|
dedupeParticipate: { type: boolean }
|
||||||
sortOrder: { type: integer }
|
sortOrder: { type: integer }
|
||||||
responses: { '200': { description: 成功 } }
|
responses: { '200': { description: 成功 } }
|
||||||
|
|||||||
@@ -54,7 +54,9 @@
|
|||||||
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
|
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</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>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
@@ -235,6 +237,8 @@ export default {
|
|||||||
.tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; }
|
.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; }
|
.tag-deleted { font-size: 22rpx; color:#fff; background:#909399; padding: 4rpx 10rpx; border-radius: 8rpx; }
|
||||||
.meta { color:$uni-text-color-grey; font-size: 24rpx; }
|
.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; }
|
.price { margin-left: 20rpx; color:$uni-color-primary; }
|
||||||
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; }
|
.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); }
|
.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; }
|
.block-title { font-size: 28rpx; font-weight: 600; color: #2d3a4a; margin-bottom: 12rpx; }
|
||||||
.placeholder { font-size: 26rpx; color: #7a8899; }
|
.placeholder { font-size: 26rpx; color: #7a8899; }
|
||||||
.params { display: flex; flex-direction: column; gap: 12rpx; }
|
.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-key { color: #7a8899; }
|
||||||
|
.param-val { text-align: left; }
|
||||||
.images { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; }
|
.images { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; }
|
||||||
.image { width: 100%; height: 200rpx; border-radius: 16rpx; background: #f0f2f5; }
|
.image { width: 100%; height: 200rpx; border-radius: 16rpx; background: #f0f2f5; }
|
||||||
.footer { display: flex; justify-content: flex-end; gap: 20rpx; }
|
.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
|
1
|
||||||
/* TEXT */
|
/* 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"]);
|
], 8, ["onClick"]);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -136,6 +136,19 @@
|
|||||||
color: #444;
|
color: #444;
|
||||||
font-size: 0.75rem;
|
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 {
|
.price {
|
||||||
margin-left: 0.625rem;
|
margin-left: 0.625rem;
|
||||||
color: #4C8DFF;
|
color: #4C8DFF;
|
||||||
|
|||||||
@@ -92,13 +92,18 @@
|
|||||||
}
|
}
|
||||||
.param {
|
.param {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: #2d3a4a;
|
color: #2d3a4a;
|
||||||
}
|
}
|
||||||
.param-key {
|
.param-key {
|
||||||
color: #7a8899;
|
color: #7a8899;
|
||||||
}
|
}
|
||||||
|
.param-val {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.images {
|
.images {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
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 || "-"),
|
g: common_vendor.t(it.brand || "-"),
|
||||||
h: common_vendor.t(it.model || ""),
|
h: common_vendor.t(it.model || ""),
|
||||||
i: common_vendor.t(it.spec || ""),
|
i: common_vendor.t(it.spec || ""),
|
||||||
j: it.id,
|
j: it.cardParams && Object.keys(it.cardParams).length
|
||||||
k: common_vendor.o(($event) => $options.openDetail(it.id), it.id)
|
}, 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;
|
color: #444;
|
||||||
font-size: 24rpx;
|
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 {
|
.price {
|
||||||
margin-left: 20rpx;
|
margin-left: 20rpx;
|
||||||
color: #4C8DFF;
|
color: #4C8DFF;
|
||||||
|
|||||||
@@ -92,13 +92,18 @@
|
|||||||
}
|
}
|
||||||
.param {
|
.param {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #2d3a4a;
|
color: #2d3a4a;
|
||||||
}
|
}
|
||||||
.param-key {
|
.param-key {
|
||||||
color: #7a8899;
|
color: #7a8899;
|
||||||
}
|
}
|
||||||
|
.param-val {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.images {
|
.images {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
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