diff --git a/docs/pb_tbl_product_list.md b/docs/pb_tbl_product_list.md
index d453215..e542694 100644
--- a/docs/pb_tbl_product_list.md
+++ b/docs/pb_tbl_product_list.md
@@ -19,7 +19,7 @@
| `prod_list_icon` | `text` | 否 | 产品图标,保存 `tbl_attachments.attachments_id` |
| `prod_list_description` | `text` | 否 | 产品说明(editor 内容,建议保存 Markdown 或已净化 HTML) |
| `prod_list_feature` | `text` | 否 | 产品特色 |
-| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 对象/数组) |
+| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "name": "属性名", "value": "属性值" } ]`) |
| `prod_list_plantype` | `text` | 否 | 产品方案 |
| `prod_list_category` | `text` | 是 | 产品分类(必填,单选) |
| `prod_list_sort` | `number` | 否 | 排序值(同分类内按升序) |
@@ -49,7 +49,7 @@
- `prod_list_icon` 仅保存附件业务 ID,真实文件统一在 `tbl_attachments`。
- 当前预构建脚本中已将 `listRule` 与 `viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。
-- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交对象/数组,不建议再二次字符串化。
+- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"name":"属性名","value":"属性值"}]`。
- `prod_list_description` 作为 editor 内容字段,建议统一为 Markdown 或净化后的 HTML;若允许 HTML,需在写入前做 XSS 白名单过滤。
- 前端渲染 `prod_list_description` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。
- `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。
diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js
index 92a6d04..2b778d8 100644
--- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js
+++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js
@@ -2,7 +2,7 @@ module.exports = {
NODE_ENV: 'production',
APP_VERSION: '0.1.21',
APP_BASE_URL: 'https://bai-api.blv-oa.com',
- POCKETBASE_API_URL: 'http://127.0.0.1:8090',
+ POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb',
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
WECHAT_APPID: 'wx3bd7a7b19679da7a',
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js
index 621a214..d9ba824 100644
--- a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js
+++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js
@@ -349,24 +349,51 @@ function validateDocumentDeleteBody(e) {
function normalizeProductParameters(value) {
if (value === null || typeof value === 'undefined' || value === '') {
- return {}
+ return []
}
- if (typeof value !== 'object' || Array.isArray(value)) {
- throw createAppError(400, 'prod_list_parameters 必须为对象')
+ const result = []
+ const indexByName = {}
+
+ function upsert(nameValue, rawValue) {
+ const name = String(nameValue || '').trim()
+ if (!name) {
+ return
+ }
+
+ const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ result[existingIndex].value = normalizedValue
+ return
+ }
+
+ indexByName[name] = result.length
+ result.push({
+ name: name,
+ value: normalizedValue,
+ })
+ }
+
+ if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i += 1) {
+ const item = value[i] && typeof value[i] === 'object' ? value[i] : null
+ if (!item) {
+ continue
+ }
+ upsert(item.name || item.key, item.value)
+ }
+ return result
+ }
+
+ if (typeof value !== 'object') {
+ throw createAppError(400, 'prod_list_parameters 必须为数组或对象')
}
- const result = {}
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i += 1) {
- const key = String(keys[i] || '').trim()
- if (!key) {
- continue
- }
-
- const current = value[keys[i]]
- result[key] = current === null || typeof current === 'undefined' ? '' : String(current)
+ upsert(keys[i], value[keys[i]])
}
return result
diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js
index aabac2f..1544735 100644
--- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js
+++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js
@@ -112,28 +112,50 @@ function buildCategoryRankMap(records) {
function normalizeParameters(value) {
if (value === null || typeof value === 'undefined' || value === '') {
- return {}
+ return []
}
- if (typeof value !== 'object' || Array.isArray(value)) {
- throw createAppError(400, 'prod_list_parameters 必须是对象')
+ const result = []
+ const indexByName = {}
+
+ function upsert(nameValue, rawValue) {
+ const name = normalizeText(nameValue)
+ if (!name) {
+ return
+ }
+
+ const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ result[existingIndex].value = normalizedValue
+ return
+ }
+
+ indexByName[name] = result.length
+ result.push({
+ name: name,
+ value: normalizedValue,
+ })
+ }
+
+ if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i += 1) {
+ const item = value[i] && typeof value[i] === 'object' ? value[i] : null
+ if (!item) {
+ continue
+ }
+ upsert(item.name || item.key, item.value)
+ }
+ return result
+ }
+
+ if (typeof value !== 'object') {
+ throw createAppError(400, 'prod_list_parameters 必须是数组或对象')
}
- const result = {}
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i += 1) {
- const key = normalizeText(keys[i])
- if (!key) {
- continue
- }
-
- const current = value[keys[i]]
- if (current === null || typeof current === 'undefined') {
- result[key] = ''
- continue
- }
-
- result[key] = String(current)
+ upsert(keys[i], value[keys[i]])
}
return result
@@ -141,7 +163,7 @@ function normalizeParameters(value) {
function normalizeParametersForOutput(value) {
if (value === null || typeof value === 'undefined' || value === '') {
- return {}
+ return []
}
let source = value
@@ -153,9 +175,10 @@ function normalizeParametersForOutput(value) {
if (raw.indexOf('map[') === 0 && raw.endsWith(']')) {
const body = raw.slice(4, -1).trim()
if (!body) {
- return {}
+ return []
}
- const result = {}
+ const result = []
+ const indexByName = {}
const pairs = body.split(/\s+/)
for (let i = 0; i < pairs.length; i += 1) {
const pair = pairs[i]
@@ -168,32 +191,47 @@ function normalizeParametersForOutput(value) {
continue
}
const val = pair.slice(separatorIndex + 1)
- result[key] = val === null || typeof val === 'undefined' ? '' : String(val)
+ const normalizedValue = val === null || typeof val === 'undefined' ? '' : String(val)
+ const existingIndex = indexByName[key]
+ if (typeof existingIndex === 'number') {
+ result[existingIndex].value = normalizedValue
+ } else {
+ indexByName[key] = result.length
+ result.push({ name: key, value: normalizedValue })
+ }
}
return result
}
- return {}
+ return []
}
}
if (Array.isArray(source)) {
- const mapped = {}
+ const mapped = []
+ const indexByName = {}
for (let i = 0; i < source.length; i += 1) {
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
if (!item) {
continue
}
- const key = normalizeText(item.key)
- if (!key) {
+ const name = normalizeText(item.name || item.key)
+ if (!name) {
continue
}
- mapped[key] = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value)
+ const normalizedValue = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value)
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ mapped[existingIndex].value = normalizedValue
+ } else {
+ indexByName[name] = mapped.length
+ mapped.push({ name: name, value: normalizedValue })
+ }
}
return mapped
}
if (typeof source !== 'object') {
- return {}
+ return []
}
// Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object.
@@ -201,15 +239,23 @@ function normalizeParametersForOutput(value) {
source = JSON.parse(JSON.stringify(source))
} catch (_error) {}
- const result = {}
+ const result = []
+ const indexByName = {}
const keys = Object.keys(source)
for (let i = 0; i < keys.length; i += 1) {
- const key = normalizeText(keys[i])
- if (!key) {
+ const name = normalizeText(keys[i])
+ if (!name) {
continue
}
const current = source[keys[i]]
- result[key] = current === null || typeof current === 'undefined' ? '' : String(current)
+ const normalizedValue = current === null || typeof current === 'undefined' ? '' : String(current)
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ result[existingIndex].value = normalizedValue
+ } else {
+ indexByName[name] = result.length
+ result.push({ name: name, value: normalizedValue })
+ }
}
return result
@@ -252,7 +298,7 @@ function exportProductRecord(record, extra) {
const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : {}
const parametersRaw = record.get('prod_list_parameters')
const parametersFromRaw = normalizeParametersForOutput(parametersRaw)
- const parameters = Object.keys(parametersFromText).length ? parametersFromText : parametersFromRaw
+ const parameters = parametersFromText.length ? parametersFromText : parametersFromRaw
return {
pb_id: record.id,
diff --git a/pocket-base/bai_web_pb_hooks/pages/product-manage.js b/pocket-base/bai_web_pb_hooks/pages/product-manage.js
index 45eb205..d06a88b 100644
--- a/pocket-base/bai_web_pb_hooks/pages/product-manage.js
+++ b/pocket-base/bai_web_pb_hooks/pages/product-manage.js
@@ -778,7 +778,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
paramsBodyEl.innerHTML = state.parameterRows.map(function (row, index) {
return '
'
- + ' | '
+ + ' | '
+ ' | '
+ ' | '
+ '
'
@@ -808,13 +808,13 @@ routerAdd('GET', '/manage/product-manage', function (e) {
const incomingRows = []
const keys = Object.keys(parsed)
for (let i = 0; i < keys.length; i += 1) {
- const key = normalizeText(keys[i])
- if (!key) {
+ const name = normalizeText(keys[i])
+ if (!name) {
continue
}
const value = parsed[keys[i]]
incomingRows.push({
- key: key,
+ name: name,
value: value === null || typeof value === 'undefined' ? '' : String(value),
})
}
@@ -826,12 +826,12 @@ routerAdd('GET', '/manage/product-manage', function (e) {
const existingIndexMap = {}
for (let i = 0; i < state.parameterRows.length; i += 1) {
- const key = normalizeText(state.parameterRows[i].key)
- if (!key) {
+ const name = normalizeText(state.parameterRows[i].name)
+ if (!name) {
continue
}
- if (!Object.prototype.hasOwnProperty.call(existingIndexMap, key)) {
- existingIndexMap[key] = i
+ if (!Object.prototype.hasOwnProperty.call(existingIndexMap, name)) {
+ existingIndexMap[name] = i
}
}
@@ -839,13 +839,13 @@ routerAdd('GET', '/manage/product-manage', function (e) {
let updatedCount = 0
for (let i = 0; i < incomingRows.length; i += 1) {
const row = incomingRows[i]
- const idx = existingIndexMap[row.key]
+ const idx = existingIndexMap[row.name]
if (typeof idx === 'number') {
state.parameterRows[idx].value = row.value
updatedCount += 1
} else {
- state.parameterRows.push({ key: row.key, value: row.value })
- existingIndexMap[row.key] = state.parameterRows.length - 1
+ state.parameterRows.push({ name: row.name, value: row.value })
+ existingIndexMap[row.name] = state.parameterRows.length - 1
addedCount += 1
}
}
@@ -854,22 +854,33 @@ routerAdd('GET', '/manage/product-manage', function (e) {
setStatus('参数增量导入完成:新增 ' + addedCount + ' 项,更新 ' + updatedCount + ' 项,当前共 ' + state.parameterRows.length + ' 项。', 'success')
}
- function collectParameterObject() {
- const result = {}
+ function collectParameterArray() {
+ const result = []
+ const indexByName = {}
for (let i = 0; i < state.parameterRows.length; i += 1) {
- const key = normalizeText(state.parameterRows[i].key)
+ const name = normalizeText(state.parameterRows[i].name)
const value = normalizeText(state.parameterRows[i].value)
- if (!key) {
+ if (!name) {
continue
}
- result[key] = value
+
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ result[existingIndex].value = value
+ } else {
+ indexByName[name] = result.length
+ result.push({
+ name: name,
+ value: value,
+ })
+ }
}
return result
}
- function normalizeParameterObject(value) {
+ function normalizeParameterRows(value) {
if (value === null || typeof value === 'undefined' || value === '') {
- return {}
+ return []
}
let source = value
@@ -877,28 +888,41 @@ routerAdd('GET', '/manage/product-manage', function (e) {
try {
source = JSON.parse(source)
} catch (_error) {
- return {}
+ return []
+ }
+ }
+
+ const rows = []
+ const indexByName = {}
+
+ function upsert(nameValue, rawValue) {
+ const name = normalizeText(nameValue)
+ if (!name) {
+ return
+ }
+ const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
+ const existingIndex = indexByName[name]
+ if (typeof existingIndex === 'number') {
+ rows[existingIndex].value = normalizedValue
+ } else {
+ indexByName[name] = rows.length
+ rows.push({ name: name, value: normalizedValue })
}
}
if (Array.isArray(source)) {
- const mapped = {}
for (let i = 0; i < source.length; i += 1) {
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
if (!item) {
continue
}
- const key = normalizeText(item.key)
- if (!key) {
- continue
- }
- mapped[key] = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value)
+ upsert(item.name || item.key, item.value)
}
- return mapped
+ return rows
}
if (typeof source !== 'object') {
- return {}
+ return []
}
// Some PocketBase/Goja payloads are object-like values; roundtrip makes keys enumerable.
@@ -906,18 +930,12 @@ routerAdd('GET', '/manage/product-manage', function (e) {
source = JSON.parse(JSON.stringify(source))
} catch (_error) {}
- const result = {}
const keys = Object.keys(source)
for (let i = 0; i < keys.length; i += 1) {
- const key = normalizeText(keys[i])
- if (!key) {
- continue
- }
- const current = source[keys[i]]
- result[key] = current === null || typeof current === 'undefined' ? '' : String(current)
+ upsert(keys[i], source[keys[i]])
}
- return result
+ return rows
}
function updateEditorMode() {
@@ -1002,11 +1020,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
state.selections.series = splitPipe(item.prod_list_series)
state.selections.powerSupply = splitPipe(item.prod_list_power_supply)
state.selections.tags = splitPipe(item.prod_list_tags)
- const params = normalizeParameterObject(item.prod_list_parameters)
- state.parameterRows = Object.keys(params).map(function (key) {
- const current = params[key]
- return { key: key, value: current === null || typeof current === 'undefined' ? '' : String(current) }
- })
+ state.parameterRows = normalizeParameterRows(item.prod_list_parameters)
state.currentIconAttachment = item.prod_list_icon_attachment || null
clearPendingIconPreview()
state.removedIconId = ''
@@ -1142,7 +1156,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
prod_list_icon: finalIconId,
prod_list_description: normalizeText(fields.description.value),
prod_list_feature: normalizeText(fields.feature.value),
- prod_list_parameters: collectParameterObject(),
+ prod_list_parameters: collectParameterArray(),
prod_list_plantype: joinPipe(state.selections.plantype),
prod_list_category: joinPipe(state.selections.category),
prod_list_comm_type: joinPipe(state.selections.commType),
@@ -1281,10 +1295,10 @@ routerAdd('GET', '/manage/product-manage', function (e) {
return
}
- if (target.matches('input[data-param-key]')) {
- const index = Number(target.getAttribute('data-param-key'))
+ if (target.matches('input[data-param-name]')) {
+ const index = Number(target.getAttribute('data-param-name'))
if (Number.isInteger(index) && state.parameterRows[index]) {
- state.parameterRows[index].key = target.value
+ state.parameterRows[index].name = target.value
}
return
}
@@ -1345,7 +1359,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
})
document.getElementById('addParamBtn').addEventListener('click', function () {
- state.parameterRows.push({ key: '', value: '' })
+ state.parameterRows.push({ name: '', value: '' })
renderParameterRows()
})
diff --git a/pocket-base/spec/openapi-wx.yaml b/pocket-base/spec/openapi-wx.yaml
index 43c207a..b6ffa77 100644
--- a/pocket-base/spec/openapi-wx.yaml
+++ b/pocket-base/spec/openapi-wx.yaml
@@ -731,7 +731,9 @@ paths:
prod_list_icon: <产品图标附件ID>|
prod_list_description: <产品说明>|
prod_list_feature: <产品特色>|
- prod_list_parameters: <产品参数JSON>|