diff --git a/common/config/global.js b/common/config/global.js
index 5729efd..b8937d4 100644
--- a/common/config/global.js
+++ b/common/config/global.js
@@ -9,6 +9,9 @@ export default {
timeout: 10000,
tokenKey: 'jcss_token'
},
+ // 多租户:后端开启 urbanops.tenant.enable=true 后,未登录请求必须携带 tenant-id 请求头
+ // 取值 = system_tenant.id(集团租户号,与 sql/mysql/多租户-03-租户套餐与账号迁移.sql 的 @tenant_id 保持一致)
+ tenantId: '1',
router: {
loginPath: '/pages/login/index',
tabBarList: [
diff --git a/common/utils/request.js b/common/utils/request.js
index 8e95448..6918e8f 100644
--- a/common/utils/request.js
+++ b/common/utils/request.js
@@ -22,6 +22,10 @@ const request = (options) => {
if (token) {
opts.header['Authorization'] = `Bearer ${token}`;
+ } else if (globalConfig.tenantId) {
+ // 多租户:未登录时后端无法从 token 推导租户,必须显式携带 tenant-id(登录、刷新令牌等接口)
+ // 已登录请求由后端按 token 取租户,故不硬编码此头,避免将来多租户下跨租户 403
+ opts.header['tenant-id'] = String(globalConfig.tenantId);
}
opts.url = globalConfig.api.baseUrl + opts.url;
diff --git a/components/grouped-picker/grouped-picker.vue b/components/grouped-picker/grouped-picker.vue
new file mode 100644
index 0000000..5bd5d63
--- /dev/null
+++ b/components/grouped-picker/grouped-picker.vue
@@ -0,0 +1,517 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ side.label }}
+ {{ side.count }}
+
+
+
+
+
+
+ {{ group.label }}
+
+
+ ✓
+
+
+ {{ row.label }}
+
+ {{ breadcrumb(row) }}
+
+
+
+ {{ unit }}
+
+
+
+ 暂无数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages-sub/daily/maintain-manage/add-record.vue b/pages-sub/daily/maintain-manage/add-record.vue
index aa11786..56fade7 100644
--- a/pages-sub/daily/maintain-manage/add-record.vue
+++ b/pages-sub/daily/maintain-manage/add-record.vue
@@ -17,6 +17,14 @@
+
+
+
+
+
+
@@ -120,6 +128,12 @@
+
+
@@ -178,6 +192,20 @@ export default {
fieldCode: '', // 当前操作的字段
options: [] as any[],
fieldItem: null // 保存当前点击的field对象,用于后面查找label
+ },
+ // 分层选项选择器配置(选项带 path 的 3/6 字段用,其余字段仍走上面的 actionSheet / 内联复选框)
+ groupPicker: {
+ show: false,
+ fieldCode: '',
+ fieldItem: null as any,
+ title: '',
+ options: [] as any[],
+ multiple: false,
+ maxCount: Infinity,
+ needQuantity: false,
+ value: '' as any,
+ quantities: {} as any,
+ unit: ''
}
}
},
@@ -224,10 +252,14 @@ export default {
},
/**
* 点击表单项时按类型决定弹出哪个选择器:
- * 3 枚举(单选) / 9 关联记录 → 底部 actionSheet;11 地理位置 → 微信地图选点
+ * 分层选项(带 path 的 3/6) → 分组弹层;3 枚举(单选) / 9 关联记录 → 底部 actionSheet;11 地理位置 → 微信地图选点
* 其余类型(多选、图片、附件、可输入的链接)不在这里处理,由控件自身响应
*/
openPickerIfNeeded(item: any) {
+ // 分层选项优先:它的只读入口是靠本方法打开弹层的(模板里 3/6 的原有分支已被 isGroupedField 挡在前面)
+ if (this.isGroupedField(item)) {
+ return this.openGroupedPicker(item)
+ }
// 条件与模板互补:模板用 multiple === 1 渲染复选框组,这里就必须用 !== 1 才打开弹窗,
// 否则 multiple 为 NULL/0 的存量单选枚举会既不渲染复选框、又点不开弹窗
if (item.fieldType === 3 && item.multiple !== 1) {
@@ -241,6 +273,51 @@ export default {
}
},
/**
+ * 是否为「分层选项」字段:3枚举/6多选,且选项带了后端下发的 path。
+ * path 是纯增量字段,老字段(静态/字典/物料/数据表/接口/无 gN_ 别名的 SQL)没有它 → 走原路径,行为不变
+ */
+ isGroupedField(item: any) {
+ if (!item || (item.fieldType !== 3 && item.fieldType !== 6)) {
+ return false
+ }
+ return (item.selectOptions || []).some((opt: any) => Array.isArray(opt.path) && opt.path.length > 0)
+ },
+ /**
+ * 打开分层选项弹层。单选/多选判定与模板同一口径:
+ * 6 → 多选;3 且 multiple===1 → 多选(提交按逗号拼接);3 其余 → 单选
+ */
+ openGroupedPicker(item: any) {
+ const code = item.fieldCode
+ const multiple = item.fieldType === 6 || item.multiple === 1
+ this.groupPicker.fieldCode = code
+ this.groupPicker.fieldItem = item
+ this.groupPicker.title = item.fieldName
+ this.groupPicker.options = item.selectOptions || []
+ this.groupPicker.multiple = multiple
+ this.groupPicker.maxCount = this.maxCountOf(item, Infinity)
+ // 只有 6 多选需要每项数量;3 枚举没有这层语义
+ this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item)
+ this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '')
+ this.groupPicker.quantities = Object.assign({}, this.qtyMap[code] || {})
+ this.groupPicker.unit = item.unit || ''
+ this.groupPicker.show = true
+ },
+ /**
+ * 分层弹层确定回调。写入位置与原有两条路径完全一致(inspectForm/labelMap/qtyMap),
+ * 因此 buildDynamicFields() 一行都不用改
+ */
+ onGroupedConfirm(payload: any) {
+ const code = this.groupPicker.fieldCode
+ this.inspectForm[code] = payload.values
+ this.labelMap[code] = payload.labelText
+ if (this.groupPicker.needQuantity) {
+ // 合并而不是替换:保留 loadDynamicFields 预建的选项键,避免非分层路径读到 undefined
+ this.qtyMap[code] = Object.assign({}, this.qtyMap[code] || {}, payload.quantities || {})
+ }
+ this.groupPicker.show = false
+ this.$refs.inspectFormRef.validateField(code)
+ },
+ /**
* 打开底部选择弹窗 actionSheet
*/
openActionSheet(item: any) {
@@ -343,7 +420,7 @@ export default {
// 由后端按 optionSource 分发。类型 3/6/9 需要选项,其余类型不需要
if (field.fieldType === 3 || field.fieldType === 6 || field.fieldType === 9) {
if (field.optionSource === 1 && Array.isArray(field.options)) {
- field.selectOptions = field.options.map(opt => ({ label: opt.label, value: String(opt.value) }))
+ field.selectOptions = field.options.map(opt => ({ label: opt.label, value: String(opt.value), path: opt.path }))
} else if (field.optionSource === 3) {
// 物料来源:调 materials(比 options 多返回 currentNum 库存余量/unit),供选择时显示库存
const matRes = await getMaterialsByCode({
@@ -365,7 +442,8 @@ export default {
workTypeId,
fieldCode: field.fieldCode
})
- field.selectOptions = (optRes || []).map(opt => ({ label: opt.label, value: String(opt.value) }))
+ // path 后端仅在 SQL 来源按 gN_key/gN_label 别名产出;透传后由 isGroupedField 判定是否走分层弹层
+ field.selectOptions = (optRes || []).map(opt => ({ label: opt.label, value: String(opt.value), path: opt.path }))
}
}
// 数量收集初始化(必须在 selectOptions 装载之后):