= { [K in keyof T as K]: T[K]; } & {};
+ type __VLS_WithDefaultsGlobal = {
+ [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 = Readonly>;
+ type __VLS_ProxyRefs = import('vue').ShallowUnwrapRef;
function __VLS_getVForSourceType>(source: T): [
item: T extends number ? number
@@ -115,7 +125,6 @@ export {};
: T extends (...args: any) => any
? T
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
- function __VLS_makeOptional(t: T): { [K in keyof T]?: T[K] };
function __VLS_asFunctionalComponent any ? InstanceType : unknown>(t: T, instance?: K):
T extends new (...args: any) => any ? __VLS_FunctionalComponent
: T extends () => any ? (props: {}, ctx?: any) => ReturnType
diff --git a/admin/src/views/consult/ConsultList.vue b/admin/src/views/consult/ConsultList.vue
index ab3ce5d..a7bac88 100644
--- a/admin/src/views/consult/ConsultList.vue
+++ b/admin/src/views/consult/ConsultList.vue
@@ -64,7 +64,13 @@ async function fetch(){
}
function reset(){ q.status='open'; q.kw=''; fetch() }
function openReply(row: any){ visible.value = true; current.value = row; reply.value='' }
-async function sendReply(){ await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value }); visible.value=false; await fetch() }
+async function sendReply(){
+ const data = await post(`/api/admin/consults/${current.value.id}/reply`, { content: reply.value })
+ visible.value=false
+ // 若后端返回状态,立即更新当前行展示
+ if (current.value && data && data.status) current.value.status = data.status
+ await fetch()
+}
async function resolve(row: any){ await put(`/api/admin/consults/${row.id}/resolve`, {}); row.status = 'resolved' }
function viewReply(row: any){ ElMessageBox.alert(row.replyContent || '无', '已回复内容', { confirmButtonText: '知道了' }) }
diff --git a/admin/src/views/parts/Submissions.vue b/admin/src/views/parts/Submissions.vue
index 9f762e6..c3c5ae9 100644
--- a/admin/src/views/parts/Submissions.vue
+++ b/admin/src/views/parts/Submissions.vue
@@ -28,7 +28,19 @@
-
+
+
+
+
+
模板:{{ (expand[row.id]?.templateName)|| '-' }}
+
加载中...
+
+
+
+
+
+
+
@@ -66,36 +78,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.required?'是':'否' }}
-
-
-
-
-
-
-
-
+
@@ -142,6 +126,8 @@ const loading = ref(false)
const exporting = ref(false)
const detail = reactive({ visible: false, id: 0, data: null as any, images: [] as string[], parameterText: '', template: null as any })
+const expand = reactive
}>>({})
+const expanded = ref([])
const paramRows = ref([])
const jsonPlaceholder = '{"key":"value"}'
const saving = ref(false)
@@ -179,6 +165,9 @@ async function fetchList(){
const res = await get('/api/admin/parts/submissions', params)
rows.value = Array.isArray(res?.list) ? res.list : []
total.value = Number(res?.total || rows.value.length)
+ // 默认展开全部并预加载参数
+ expanded.value = rows.value.map((r:any)=>Number(r.id)).filter((x:number)=>!!x)
+ for (const r of rows.value) { await ensureExpand(r) }
} catch (e:any) {
ElMessage.error(e.message || '加载失败')
} finally {
@@ -194,6 +183,33 @@ function reset(){
}
function onPage(p:number){ query.page = p; fetchList() }
+async function ensureExpand(row:any){
+ const id = Number(row?.id)
+ if (!id) return
+ if (expand[id] && !expand[id].loading && (expand[id].rows||[]).length) return
+ expand[id] = { loading: true, templateName: '', rows: [] }
+ try {
+ const data = await get(`/api/admin/parts/submissions/${id}`)
+ const params = data?.parameters || {}
+ if (data?.templateId) {
+ try {
+ const t = await get(`/api/admin/part-templates/${data.templateId}`)
+ expand[id].templateName = t?.name || ''
+ const defs = (t?.params||[])
+ expand[id].rows = defs.map((p:any)=>({ label: p.fieldLabel + (p.unit?` (${p.unit})`:''), value: params[p.fieldKey] ?? '' }))
+ } catch {
+ expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
+ }
+ } else {
+ expand[id].rows = Object.keys(params).map(k=>({ label: k, value: params[k] }))
+ }
+ } finally {
+ if (expand[id]) expand[id].loading = false
+ }
+}
+
+function onExpandChange(row:any){ ensureExpand(row) }
+
async function openDetail(id:number){
try {
const data = await get(`/api/admin/parts/submissions/${id}`)
@@ -319,4 +335,7 @@ onMounted(fetchList)
.thumb-list { display:flex; flex-wrap:wrap; gap:12px; }
.thumb { width: 120px; display:flex; flex-direction: column; gap: 8px; }
.thumb .el-image { width: 120px; height: 120px; border-radius: 8px; }
+.expand-wrap { padding: 8px 12px; background: rgba(255,255,255,0.02); }
+.expand-line { margin-bottom: 6px; font-size: 13px; color: #ddd; }
+.expand-loading { padding: 8px 0; color:#999; }
diff --git a/admin/src/views/parts/Templates.vue b/admin/src/views/parts/Templates.vue
index c62768e..3de0346 100644
--- a/admin/src/views/parts/Templates.vue
+++ b/admin/src/views/parts/Templates.vue
@@ -36,7 +36,7 @@
-
+
@@ -60,20 +60,14 @@
{{ $index+1 }}
-
-
-
-
+
-
-
-
-
-
+
+
@@ -83,15 +77,25 @@
-
-
+
+
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+ 单位:{{ row.unit }}
+
+
删除
@@ -136,7 +140,8 @@ function openView(row:any) {
http.get(`/api/admin/part-templates/${row.id}`).then(res => {
const d = res.data
Object.assign(form, { id: d.id, categoryId: d.categoryId, name: d.name, modelRule: d.modelRule, status: d.status,
- params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(',') })) })
+ params: (d.params||[]).map((p:any)=>({ ...p, enumOptionsText: (p.enumOptions||[]).join(','),
+ fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: p.fuzzyTolerance })) })
})
}
@@ -146,12 +151,15 @@ function openCreate() {
}
function addParam() {
- form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
+ form.params.push({ fieldKey:'', fieldLabel:'', type:'number', required:false, unit:'mm', enumOptionsText:'', searchable:false,
+ fuzzySearchable:false, fuzzyTolerance:null, sortOrder:0 })
}
function removeParam(i:number) { form.params.splice(i,1) }
function save() {
+ // 先为所有行生成 fieldKey(基于名称拼音首字母),并消重
+ genAllKeys()
// 校验
const seen = new Set()
for (const p of form.params) {
@@ -160,13 +168,27 @@ function save() {
if (!key) { ElMessage.warning('参数键不能为空'); return }
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { ElMessage.warning(`参数键不合法: ${key}`); return }
if (!label) { ElMessage.warning('参数名称不能为空'); return }
- if (!['string','number','boolean','enum','date'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
+ if (!['string','number'].includes(p.type)) { ElMessage.warning(`不支持的类型: ${p.type}`); return }
+ if (p.fuzzySearchable && p.type !== 'number') { ElMessage.warning(`仅 number 类型可开启可模糊: ${key}`); return }
+ if (p.fuzzySearchable && p.fuzzyTolerance !== undefined && p.fuzzyTolerance !== null && String(p.fuzzyTolerance).trim() !== '') {
+ const num = Number(p.fuzzyTolerance)
+ if (!(num > 0)) { ElMessage.warning(`容差需为正数: ${key}`); return }
+ p.fuzzyTolerance = num
+ }
if (seen.has(key)) { ElMessage.warning(`参数键重复: ${key}`); return }
seen.add(key)
}
+ // 限制卡片展示最多4个
+ if (form.params.filter((p:any)=>!!p.cardDisplay).length > 4) {
+ ElMessage.warning('卡片展示最多选择4个参数');
+ return
+ }
const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status,
params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit,
- enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) }
+ enumOptions: undefined, searchable:true,
+ fuzzySearchable: !!p.fuzzySearchable, fuzzyTolerance: (p.fuzzyTolerance===null||p.fuzzyTolerance===''? null : Number(p.fuzzyTolerance)),
+ cardDisplay: !!p.cardDisplay,
+ sortOrder:p.sortOrder })) }
http.post('/api/admin/part-templates', payload).then(()=>{
ElMessage.success('创建成功'); dlg.visible=false; load()
})
@@ -183,6 +205,51 @@ function doDelete(row:any) {
}
onMounted(()=>{ loadCategories(); load() })
+
+// 生成拼音首字母作为键,并做合法化与消重
+// 轻量拼音首字母生成:优先使用浏览器 Intl.Segmenter 分词 + 拉丁字母提取;不依赖外部包
+function toInitialKey(label: string): string {
+ const s = String(label||'').trim()
+ if (!s) return ''
+ // 规则:取每个“词”的首字符(非中文/字母/数字作为分隔),保留字母数字;中文用简单映射(常用声母)
+ const simpleMap: Record = {
+ '长':'c','重':'z','沈':'s','厦':'x','重庆':'c','长沙':'c'
+ }
+ let buf = ''
+ for (let i=0;i = {}
+ 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}`
+}
+
diff --git a/backend/data/attachments/2025/09/29/0cd43d7ba26180d89bc4c5e68f3c3aefc57559a0e6466348af851aafca61cf78.jpg b/backend/data/attachments/2025/09/29/0cd43d7ba26180d89bc4c5e68f3c3aefc57559a0e6466348af851aafca61cf78.jpg
new file mode 100644
index 0000000..8a83c50
Binary files /dev/null and b/backend/data/attachments/2025/09/29/0cd43d7ba26180d89bc4c5e68f3c3aefc57559a0e6466348af851aafca61cf78.jpg differ
diff --git a/backend/src/main/java/com/example/demo/admin/AdminConsultController.java b/backend/src/main/java/com/example/demo/admin/AdminConsultController.java
index d8cf2b5..5364d96 100644
--- a/backend/src/main/java/com/example/demo/admin/AdminConsultController.java
+++ b/backend/src/main/java/com/example/demo/admin/AdminConsultController.java
@@ -66,7 +66,9 @@ public class AdminConsultController {
}
Long uid = (userId != null ? userId : (adminId != null ? adminId : defaults.getUserId()));
jdbcTemplate.update("INSERT INTO consult_replies (consult_id,user_id,content,created_at) VALUES (?,?,?,NOW())", id, uid, content);
- return ResponseEntity.ok().build();
+ // 自动判定为已解决
+ jdbcTemplate.update("UPDATE consults SET status='resolved', updated_at=NOW() WHERE id=?", id);
+ return ResponseEntity.ok(java.util.Map.of("status", "resolved"));
}
@PutMapping("/{id}/resolve")
diff --git a/backend/src/main/java/com/example/demo/admin/AdminPartController.java b/backend/src/main/java/com/example/demo/admin/AdminPartController.java
index 9ad5ed4..6803922 100644
--- a/backend/src/main/java/com/example/demo/admin/AdminPartController.java
+++ b/backend/src/main/java/com/example/demo/admin/AdminPartController.java
@@ -24,8 +24,9 @@ public class AdminPartController {
@RequestParam(name = "size", defaultValue = "20") int size) {
int offset = Math.max(0, page - 1) * Math.max(1, size);
StringBuilder sql = new StringBuilder(
- "SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted " +
- "FROM products p JOIN shops s ON s.id=p.shop_id WHERE p.deleted_at IS NULL");
+ "SELECT p.id,p.user_id AS userId,p.shop_id AS shopId,s.name AS shopName,p.name,p.brand,p.model,p.spec,p.is_blacklisted, " +
+ "p.template_id AS templateId, t.name AS templateName, p.attributes_json AS attributesJson " +
+ "FROM products p JOIN shops s ON s.id=p.shop_id LEFT JOIN part_templates t ON t.id=p.template_id WHERE p.deleted_at IS NULL");
List