feat: 更新产品参数处理逻辑,支持数组格式;修改相关 API 文档;添加产品参数迁移脚本
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -778,7 +778,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
|
||||
|
||||
paramsBodyEl.innerHTML = state.parameterRows.map(function (row, index) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="属性名"><input data-param-key="' + index + '" value="' + escapeHtml(row.key) + '" placeholder="属性名" /></td>'
|
||||
+ '<td data-label="属性名"><input data-param-name="' + index + '" value="' + escapeHtml(row.name) + '" placeholder="属性名" /></td>'
|
||||
+ '<td data-label="属性值"><input data-param-value="' + index + '" value="' + escapeHtml(row.value) + '" placeholder="属性值" /></td>'
|
||||
+ '<td data-label="操作"><button class="btn btn-light" type="button" data-param-remove="' + index + '">删除</button></td>'
|
||||
+ '</tr>'
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -731,7 +731,9 @@ paths:
|
||||
prod_list_icon: <产品图标附件ID>|<string>
|
||||
prod_list_description: <产品说明>|<string>
|
||||
prod_list_feature: <产品特色>|<string>
|
||||
prod_list_parameters: <产品参数JSON>|<object>
|
||||
prod_list_parameters:
|
||||
- name: <属性名>|<string>
|
||||
value: <属性值>|<string>
|
||||
prod_list_plantype: <产品方案>|<string>
|
||||
prod_list_category: <产品分类>|<string>
|
||||
prod_list_sort: <排序值>|<number>
|
||||
@@ -1628,11 +1630,20 @@ components:
|
||||
description: 产品特色
|
||||
example: <产品特色>|<string>
|
||||
prod_list_parameters:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: 产品参数(JSON 对象/数组)
|
||||
type: array
|
||||
description: 产品参数数组,每项包含 name/value
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: <属性名>|<string>
|
||||
value:
|
||||
type: string
|
||||
example: <属性值>|<string>
|
||||
example:
|
||||
key: <参数值>|<string>
|
||||
- name: <属性名>|<string>
|
||||
value: <属性值>|<string>
|
||||
prod_list_plantype:
|
||||
type: string
|
||||
description: 产品方案
|
||||
@@ -1694,7 +1705,8 @@ components:
|
||||
prod_list_description: <产品说明>|<string>
|
||||
prod_list_feature: <产品特色>|<string>
|
||||
prod_list_parameters:
|
||||
key: <参数值>|<string>
|
||||
- name: <属性名>|<string>
|
||||
value: <属性值>|<string>
|
||||
prod_list_plantype: <产品方案>|<string>
|
||||
prod_list_category: <产品分类>|<string>
|
||||
prod_list_sort: <排序值>|<number>
|
||||
@@ -1756,7 +1768,8 @@ components:
|
||||
prod_list_description: <产品说明>|<string>
|
||||
prod_list_feature: <产品特色>|<string>
|
||||
prod_list_parameters:
|
||||
key: <参数值>|<string>
|
||||
- name: <属性名>|<string>
|
||||
value: <属性值>|<string>
|
||||
prod_list_plantype: <产品方案>|<string>
|
||||
prod_list_category: <产品分类>|<string>
|
||||
prod_list_sort: <排序值>|<number>
|
||||
|
||||
Reference in New Issue
Block a user