liyu-pc-base 1.0.921 → 1.0.923

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/{FieldRender-BOg7nm5z.js → FieldRender-DnOLSXTl.js} +1 -1
  2. package/dist/{FieldRender-BOg7nm5z.js.map → FieldRender-DnOLSXTl.js.map} +1 -1
  3. package/dist/{FieldRender2-_MtcDkI5.js → FieldRender2-CvxGe238.js} +2 -2
  4. package/dist/{FieldRender2-_MtcDkI5.js.map → FieldRender2-CvxGe238.js.map} +1 -1
  5. package/dist/{FormRender3-BtY9d91e.js → FormRender3-CAxOQgue.js} +1 -1
  6. package/dist/{FormRender3-BtY9d91e.js.map → FormRender3-CAxOQgue.js.map} +1 -1
  7. package/dist/{RemoteSelectPro-CcWTS4f6.js → RemoteSelectPro-CcL_vU1K.js} +1 -1
  8. package/dist/{RemoteSelectPro-CcWTS4f6.js.map → RemoteSelectPro-CcL_vU1K.js.map} +1 -1
  9. package/dist/{SearchRender-ByarFel1.js → SearchRender-Dr_qnvZM.js} +1 -1
  10. package/dist/{SearchRender-ByarFel1.js.map → SearchRender-Dr_qnvZM.js.map} +1 -1
  11. package/dist/components/index.js +7 -7
  12. package/dist/{dbViewConfig-RGSyUx3F.js → dbViewConfig-BF0QZ_mT.js} +1 -1
  13. package/dist/{dbViewConfig-RGSyUx3F.js.map → dbViewConfig-BF0QZ_mT.js.map} +1 -1
  14. package/dist/{detailTablePro-CqcVbn8O.js → detailTablePro-DxgfGYN3.js} +1 -1
  15. package/dist/{detailTablePro-CqcVbn8O.js.map → detailTablePro-DxgfGYN3.js.map} +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/liyu-pc-base.css +1 -1
  18. package/dist/{tableModal-DSKuV-hn.js → tableModal-DZE5wntG.js} +1 -1
  19. package/dist/{tableModal-DSKuV-hn.js.map → tableModal-DZE5wntG.js.map} +1 -1
  20. package/dist/{tablePageHost-B_BOlBNA.js → tablePageHost-hE_1hQv4.js} +82 -44
  21. package/dist/{tablePageHost-B_BOlBNA.js.map → tablePageHost-hE_1hQv4.js.map} +1 -1
  22. package/dist/{tablePro-IAcxxAGI.js → tablePro-t2yDX9oW.js} +16 -11
  23. package/dist/{tablePro-IAcxxAGI.js.map → tablePro-t2yDX9oW.js.map} +1 -1
  24. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"dbViewConfig-RGSyUx3F.js","names":[],"sources":["../src/components/dbView/configSteps/helpers.ts","../src/components/dbView/configSteps/StepBasicConfig.vue","../src/components/dbView/configSteps/StepBasicConfig.vue","../src/components/dbView/configSteps/constants.ts","../src/components/dbView/configSteps/StepDisplayConfig.vue","../src/components/dbView/configSteps/StepDisplayConfig.vue","../src/components/dbView/ColumnMapColorModal.vue","../src/components/dbView/ColumnMapColorModal.vue","../src/components/dbView/searchComponentSetting/constants.ts","../src/components/dbView/searchComponentSetting/BasicConfig.vue","../src/components/dbView/searchComponentSetting/BasicConfig.vue","../src/components/dbView/searchComponentSetting/OptionKeysConfig.vue","../src/components/dbView/searchComponentSetting/OptionKeysConfig.vue","../src/components/dbView/searchComponentSetting/OptionListConfig.vue","../src/components/dbView/searchComponentSetting/OptionListConfig.vue","../src/components/dbView/searchComponentSetting/PostUrlField.vue","../src/components/dbView/searchComponentSetting/PostUrlField.vue","../src/components/dbView/searchComponentSetting/QueryParamsConfig.vue","../src/components/dbView/searchComponentSetting/QueryParamsConfig.vue","../src/components/dbView/searchComponentSetting/SelectionModeConfig.vue","../src/components/dbView/searchComponentSetting/SelectionModeConfig.vue","../src/components/dbView/searchComponentSetting/ShowSearchConfig.vue","../src/components/dbView/searchComponentSetting/ShowSearchConfig.vue","../src/components/dbView/searchComponentSetting/DataSourcePanel.vue","../src/components/dbView/searchComponentSetting/DataSourcePanel.vue","../src/components/dbView/searchComponentSetting/DateFormatConfig.vue","../src/components/dbView/searchComponentSetting/DateFormatConfig.vue","../src/components/dbView/searchComponentSetting/settings/DateSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/DateSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/FlatSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/FlatSelectSetting.vue","../src/components/dbView/searchComponentSetting/NumberRangeConfig.vue","../src/components/dbView/searchComponentSetting/NumberRangeConfig.vue","../src/components/dbView/searchComponentSetting/TextAddonConfig.vue","../src/components/dbView/searchComponentSetting/TextAddonConfig.vue","../src/components/dbView/searchComponentSetting/settings/NumberInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/NumberInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/RemoteSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/RemoteSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/TextInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/TextInputSetting.vue","../src/components/dbView/searchComponentSetting/TreeDataConfig.vue","../src/components/dbView/searchComponentSetting/TreeDataConfig.vue","../src/components/dbView/searchComponentSetting/settings/TreeSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/TreeSelectSetting.vue","../src/components/dbView/ColumnSearchComponentModal.vue","../src/components/dbView/ColumnSearchComponentModal.vue","../src/components/dbView/configSteps/StepColumnConfig.vue","../src/components/dbView/configSteps/StepColumnConfig.vue","../src/components/dbView/configSteps/StepMenu.vue","../src/components/dbView/configSteps/StepMenu.vue","../src/components/dbView/configSteps/viewMeta.ts","../src/components/dbView/dbViewConfig.vue","../src/components/dbView/dbViewConfig.vue"],"sourcesContent":["/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分(`dbView/configSteps/`)时抽出的\n 跨步骤共用小工具。只放**两个以上步骤都会用**的纯函数,只被单步用到的就近放在该组件里。 */\n\n/* 兼容 getApiData 可能的返回结构(直接对象/数组 或 {data:...})\n ⚠️ v29.23 的血泪教训:自建接口的响应负载键**不要叫 `data`**——宿主 request 层会直接回吐\n result 负载,此处以 `res.data` 判信封,负载键恰好也叫 data 时行数组会被二次解包。 */\nexport const extractData = (res: any) => {\n if (res && typeof res === 'object' && !Array.isArray(res) && res.data !== undefined)\n return res.data;\n return res;\n};\n\n/** 一律归一成数组(拿不到就返回空数组,调用方不用再判) */\nexport const toList = (res: any): any[] => {\n const d = extractData(res);\n return Array.isArray(d) ? d : [];\n};\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图名称\"\n name=\"name\"\n :rules=\"rules.name\"\n >\n <a-input\n v-model:value=\"model.name\"\n placeholder=\"请填写视图名称\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图标识\"\n name=\"flag\"\n :rules=\"rules.flag\"\n extra=\"全库唯一;仅英文大小写字母和数字(禁下划线/横杠),用于 URL 与菜单\"\n >\n <a-input\n v-model:value=\"model.flag\"\n placeholder=\"如 salesOrderList\"\n allow-clear\n >\n <template\n v-if=\"form_type === 'create'\"\n #suffix\n >\n <a-button\n type=\"link\"\n size=\"small\"\n style=\"padding: 0\"\n :loading=\"generatingFlag\"\n @click=\"genFlag\"\n >\n 换一个\n </a-button>\n </template>\n </a-input>\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"数据源\"\n name=\"data_source_id\"\n :rules=\"rules.data_source_id\"\n >\n <a-select\n v-model:value=\"model.data_source_id\"\n placeholder=\"请选择数据源\"\n show-search\n option-filter-prop=\"label\"\n :options=\"dataSourceOptions\"\n :loading=\"dsLoading\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <a-form-item\n label=\"查询SQL\"\n name=\"sql\"\n :rules=\"rules.sql\"\n extra=\"只读查询;可用模板变量 {{org_id}}、{{person_id}}(jinja2 渲染)\"\n >\n <a-textarea\n v-model:value=\"model.sql\"\n :rows=\"8\"\n placeholder=\"SELECT id, name FROM t_order WHERE org_id = {{org_id}}\"\n />\n </a-form-item>\n <div class=\"sql-toolbar\">\n <!-- ★ v29.23(用户定案):「探测列」按钮移除——列探测统一收敛到列配置步骤的\n 「列探测刷新」(基于此处 SQL+数据源实时探测并合并配置) -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"previewing\"\n @click=\"runPreview\"\n >\n 预览(前 100 行)\n </a-button>\n <span class=\"meta-tip\">先选数据源并填写 SQL;预览走配置期接口,不落库</span>\n </div>\n <a-alert\n v-if=\"sqlError\"\n type=\"error\"\n show-icon\n :message=\"sqlError\"\n class=\"sql-alert\"\n />\n <div\n v-if=\"previewRows.length\"\n class=\"preview-block\"\n >\n <div class=\"meta-tip\">预览结果:共 {{ previewTotal }} 行(最多回传 100 行)</div>\n <a-table\n :columns=\"previewCols\"\n :data-source=\"previewRows\"\n row-key=\"__i\"\n size=\"small\"\n :pagination=\"false\"\n :scroll=\"{ x: 640, y: 220 }\"\n />\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { extractData, toList } from './helpers';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「基础配置」步(Step 1)。\n 本步持有:视图名称 / 视图标识(flag,含「换一个」) / 数据源 / 查询SQL + 预览结果。\n\n 两条拆分约定(四个步骤组件一致):\n ① `model` 仍由外壳持有并原样传进来(**对象引用共享**:外壳 `model.value` 与这里\n `props.model` 是同一个对象)→ 模板里照旧写 `model.xxx`,v-model 直接改属性即可。\n 外壳会整个换掉对象(编辑态 `initForEdit` 用 `model.value = {...}` 重建),\n 故此处用 `computed(() => props.model)` 取用,才能跟着换到新对象(持有裸引用会失联)。\n eslint 的 `vue/no-mutating-props` 不会误报(模板写的是 `model.xxx`,不是 `props.model.xxx`)。\n ② 校验用的 `rules` 只服务本步这四个字段 → 就近定义在这里;`a-form` 仍是外壳的,\n 本组件的 `a-form-item` 在它内部 —— provide/inject 走组件树,外壳 `validateFields` 照样找得到。 */\n\ninterface Props {\n model: any;\n record: any;\n formType: string;\n /** 预览错误由外壳持有:第 3 步「列探测刷新」开始前也要清它(StepColumnConfig 会 emit probeStart) */\n sqlError: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['update:sqlError']);\n\nconst model = computed(() => props.model);\nconst form_type = computed(() => props.formType);\n\n/* 预览错误:写成「可写 computed」代理到外壳,本组件内部照旧 `sqlError.value = ''` 赋值,\n 不必在每处写 emit(原实现的读写点一个没动)。 */\nconst sqlError = computed<string>({\n get: () => props.sqlError,\n set: (v: string) => emit('update:sqlError', v),\n});\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\nconst rules: any = {\n name: [{ required: true, message: '请填写视图名称' }],\n flag: [\n { required: true, message: '请填写视图标识' },\n { pattern: /^[A-Za-z0-9]+$/, message: '仅英文大小写字母和数字,禁下划线/横杠' },\n ],\n data_source_id: [{ required: true, message: '请选择数据源' }],\n sql: [{ required: true, message: '请填写查询SQL' }],\n};\n\n/* ═══ 下拉数据源 ═══ */\nconst dataSourceOptions = ref<any[]>([]);\nconst dsLoading = ref(false);\n\nconst loadDataSources = async () => {\n dsLoading.value = true;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, is_pagination: false },\n `${props.record.model_app}/query_db_data_source_list`,\n );\n dataSourceOptions.value = toList(res).map((it: any) => ({\n value: it.id,\n label: it.name,\n }));\n } catch (e) {\n dataSourceOptions.value = [];\n } finally {\n dsLoading.value = false;\n }\n};\n\n/* ★ v29.12(用户定案):新建视图时自动调 gen_view_flag 生成唯一默认 flag(用户仍可手改/换一个) */\nconst generatingFlag = ref(false);\nconst genFlag = async () => {\n generatingFlag.value = true;\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id },\n `${props.record.model_app}/gen_view_flag`,\n );\n const payload = extractData(res);\n if (payload?.flag) {\n model.value.flag = payload.flag;\n } else {\n message.warning((res && res.message) || '生成视图标识失败,请手动填写');\n }\n } catch (e: any) {\n message.warning(e?.message || '生成视图标识失败,请手动填写');\n } finally {\n generatingFlag.value = false;\n }\n};\n\n/* ═══ SQL 配置期验证:预览(前 100 行)═══\n ★ v29.23:probeCols / runProbe(探测列按钮)移除——探测统一收敛到列配置步骤的\n 列探测刷新(runProbeRefresh)。\n ★ v29.23 预览恒空修复:后端响应键 `data` → `rows`——宿主 request 层直接回吐\n result 负载,extractData 以 res.data 判信封,负载键恰叫 data 时行数组被误当\n 信封二次解包 → payload 变数组 → 恒「结果为空」。 */\nconst previewing = ref(false);\nconst previewRows = ref<any[]>([]);\nconst previewCols = ref<any[]>([]);\nconst previewTotal = ref(0);\n\n/* SQL / 数据源变化后旧结果即失效,直接清掉避免误导 */\nwatch(\n () => [model.value.sql, model.value.data_source_id],\n () => {\n sqlError.value = '';\n previewRows.value = [];\n previewCols.value = [];\n previewTotal.value = 0;\n },\n);\n\nconst runPreview = async () => {\n if (!model.value.data_source_id || !model.value.sql) return;\n previewing.value = true;\n sqlError.value = '';\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/preview_sql`,\n );\n const payload = extractData(res);\n const rows: any[] = Array.isArray(payload?.rows) ? payload.rows : [];\n if (res && res.success === false) {\n sqlError.value = res.message || 'SQL 执行失败';\n previewRows.value = [];\n } else if (!rows.length) {\n sqlError.value = 'SQL 执行成功,但结果为空(0 行)';\n previewRows.value = [];\n previewTotal.value = 0;\n } else {\n previewRows.value = rows.map((r: any, i: number) => ({ ...r, __i: i }));\n previewCols.value = Object.keys(rows[0] || {}).map(k => ({\n title: k,\n dataIndex: k,\n key: k,\n width: 160,\n ellipsis: true,\n }));\n previewTotal.value = payload?.total ?? rows.length;\n }\n } catch (e: any) {\n sqlError.value = e?.message || 'SQL 预览失败';\n previewRows.value = [];\n } finally {\n previewing.value = false;\n }\n};\n\nonMounted(() => {\n loadDataSources();\n /* ★ v29.12:新建流程自动生成唯一默认 flag(原先在外壳 onMounted 的 create 分支里,\n 现随本步骤下沉;外壳的 formType 在 setup 顶层就已初始化,此处读到的一定是正确值) */\n if (props.formType !== 'edit') genFlag();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.sql-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n .meta-tip {\n margin-top: 0;\n }\n}\n.sql-alert {\n margin-bottom: 8px;\n}\n.preview-block {\n margin-bottom: 8px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图名称\"\n name=\"name\"\n :rules=\"rules.name\"\n >\n <a-input\n v-model:value=\"model.name\"\n placeholder=\"请填写视图名称\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图标识\"\n name=\"flag\"\n :rules=\"rules.flag\"\n extra=\"全库唯一;仅英文大小写字母和数字(禁下划线/横杠),用于 URL 与菜单\"\n >\n <a-input\n v-model:value=\"model.flag\"\n placeholder=\"如 salesOrderList\"\n allow-clear\n >\n <template\n v-if=\"form_type === 'create'\"\n #suffix\n >\n <a-button\n type=\"link\"\n size=\"small\"\n style=\"padding: 0\"\n :loading=\"generatingFlag\"\n @click=\"genFlag\"\n >\n 换一个\n </a-button>\n </template>\n </a-input>\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"数据源\"\n name=\"data_source_id\"\n :rules=\"rules.data_source_id\"\n >\n <a-select\n v-model:value=\"model.data_source_id\"\n placeholder=\"请选择数据源\"\n show-search\n option-filter-prop=\"label\"\n :options=\"dataSourceOptions\"\n :loading=\"dsLoading\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <a-form-item\n label=\"查询SQL\"\n name=\"sql\"\n :rules=\"rules.sql\"\n extra=\"只读查询;可用模板变量 {{org_id}}、{{person_id}}(jinja2 渲染)\"\n >\n <a-textarea\n v-model:value=\"model.sql\"\n :rows=\"8\"\n placeholder=\"SELECT id, name FROM t_order WHERE org_id = {{org_id}}\"\n />\n </a-form-item>\n <div class=\"sql-toolbar\">\n <!-- ★ v29.23(用户定案):「探测列」按钮移除——列探测统一收敛到列配置步骤的\n 「列探测刷新」(基于此处 SQL+数据源实时探测并合并配置) -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"previewing\"\n @click=\"runPreview\"\n >\n 预览(前 100 行)\n </a-button>\n <span class=\"meta-tip\">先选数据源并填写 SQL;预览走配置期接口,不落库</span>\n </div>\n <a-alert\n v-if=\"sqlError\"\n type=\"error\"\n show-icon\n :message=\"sqlError\"\n class=\"sql-alert\"\n />\n <div\n v-if=\"previewRows.length\"\n class=\"preview-block\"\n >\n <div class=\"meta-tip\">预览结果:共 {{ previewTotal }} 行(最多回传 100 行)</div>\n <a-table\n :columns=\"previewCols\"\n :data-source=\"previewRows\"\n row-key=\"__i\"\n size=\"small\"\n :pagination=\"false\"\n :scroll=\"{ x: 640, y: 220 }\"\n />\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { extractData, toList } from './helpers';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「基础配置」步(Step 1)。\n 本步持有:视图名称 / 视图标识(flag,含「换一个」) / 数据源 / 查询SQL + 预览结果。\n\n 两条拆分约定(四个步骤组件一致):\n ① `model` 仍由外壳持有并原样传进来(**对象引用共享**:外壳 `model.value` 与这里\n `props.model` 是同一个对象)→ 模板里照旧写 `model.xxx`,v-model 直接改属性即可。\n 外壳会整个换掉对象(编辑态 `initForEdit` 用 `model.value = {...}` 重建),\n 故此处用 `computed(() => props.model)` 取用,才能跟着换到新对象(持有裸引用会失联)。\n eslint 的 `vue/no-mutating-props` 不会误报(模板写的是 `model.xxx`,不是 `props.model.xxx`)。\n ② 校验用的 `rules` 只服务本步这四个字段 → 就近定义在这里;`a-form` 仍是外壳的,\n 本组件的 `a-form-item` 在它内部 —— provide/inject 走组件树,外壳 `validateFields` 照样找得到。 */\n\ninterface Props {\n model: any;\n record: any;\n formType: string;\n /** 预览错误由外壳持有:第 3 步「列探测刷新」开始前也要清它(StepColumnConfig 会 emit probeStart) */\n sqlError: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['update:sqlError']);\n\nconst model = computed(() => props.model);\nconst form_type = computed(() => props.formType);\n\n/* 预览错误:写成「可写 computed」代理到外壳,本组件内部照旧 `sqlError.value = ''` 赋值,\n 不必在每处写 emit(原实现的读写点一个没动)。 */\nconst sqlError = computed<string>({\n get: () => props.sqlError,\n set: (v: string) => emit('update:sqlError', v),\n});\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\nconst rules: any = {\n name: [{ required: true, message: '请填写视图名称' }],\n flag: [\n { required: true, message: '请填写视图标识' },\n { pattern: /^[A-Za-z0-9]+$/, message: '仅英文大小写字母和数字,禁下划线/横杠' },\n ],\n data_source_id: [{ required: true, message: '请选择数据源' }],\n sql: [{ required: true, message: '请填写查询SQL' }],\n};\n\n/* ═══ 下拉数据源 ═══ */\nconst dataSourceOptions = ref<any[]>([]);\nconst dsLoading = ref(false);\n\nconst loadDataSources = async () => {\n dsLoading.value = true;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, is_pagination: false },\n `${props.record.model_app}/query_db_data_source_list`,\n );\n dataSourceOptions.value = toList(res).map((it: any) => ({\n value: it.id,\n label: it.name,\n }));\n } catch (e) {\n dataSourceOptions.value = [];\n } finally {\n dsLoading.value = false;\n }\n};\n\n/* ★ v29.12(用户定案):新建视图时自动调 gen_view_flag 生成唯一默认 flag(用户仍可手改/换一个) */\nconst generatingFlag = ref(false);\nconst genFlag = async () => {\n generatingFlag.value = true;\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id },\n `${props.record.model_app}/gen_view_flag`,\n );\n const payload = extractData(res);\n if (payload?.flag) {\n model.value.flag = payload.flag;\n } else {\n message.warning((res && res.message) || '生成视图标识失败,请手动填写');\n }\n } catch (e: any) {\n message.warning(e?.message || '生成视图标识失败,请手动填写');\n } finally {\n generatingFlag.value = false;\n }\n};\n\n/* ═══ SQL 配置期验证:预览(前 100 行)═══\n ★ v29.23:probeCols / runProbe(探测列按钮)移除——探测统一收敛到列配置步骤的\n 列探测刷新(runProbeRefresh)。\n ★ v29.23 预览恒空修复:后端响应键 `data` → `rows`——宿主 request 层直接回吐\n result 负载,extractData 以 res.data 判信封,负载键恰叫 data 时行数组被误当\n 信封二次解包 → payload 变数组 → 恒「结果为空」。 */\nconst previewing = ref(false);\nconst previewRows = ref<any[]>([]);\nconst previewCols = ref<any[]>([]);\nconst previewTotal = ref(0);\n\n/* SQL / 数据源变化后旧结果即失效,直接清掉避免误导 */\nwatch(\n () => [model.value.sql, model.value.data_source_id],\n () => {\n sqlError.value = '';\n previewRows.value = [];\n previewCols.value = [];\n previewTotal.value = 0;\n },\n);\n\nconst runPreview = async () => {\n if (!model.value.data_source_id || !model.value.sql) return;\n previewing.value = true;\n sqlError.value = '';\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/preview_sql`,\n );\n const payload = extractData(res);\n const rows: any[] = Array.isArray(payload?.rows) ? payload.rows : [];\n if (res && res.success === false) {\n sqlError.value = res.message || 'SQL 执行失败';\n previewRows.value = [];\n } else if (!rows.length) {\n sqlError.value = 'SQL 执行成功,但结果为空(0 行)';\n previewRows.value = [];\n previewTotal.value = 0;\n } else {\n previewRows.value = rows.map((r: any, i: number) => ({ ...r, __i: i }));\n previewCols.value = Object.keys(rows[0] || {}).map(k => ({\n title: k,\n dataIndex: k,\n key: k,\n width: 160,\n ellipsis: true,\n }));\n previewTotal.value = payload?.total ?? rows.length;\n }\n } catch (e: any) {\n sqlError.value = e?.message || 'SQL 预览失败';\n previewRows.value = [];\n } finally {\n previewing.value = false;\n }\n};\n\nonMounted(() => {\n loadDataSources();\n /* ★ v29.12:新建流程自动生成唯一默认 flag(原先在外壳 onMounted 的 create 分支里,\n 现随本步骤下沉;外壳的 formType 在 setup 顶层就已初始化,此处读到的一定是正确值) */\n if (props.formType !== 'edit') genFlag();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.sql-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n .meta-tip {\n margin-top: 0;\n }\n}\n.sql-alert {\n margin-bottom: 8px;\n}\n.preview-block {\n margin-bottom: 8px;\n}\n</style>\n","/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分的「数据字典」集中地\n (与 `dbView/searchComponentSetting/constants.ts` 同一约定:常量留这里,逻辑留组件)。\n 注意:`COL_TABLE_WIDTH` 由 `COL_COLUMNS` 求和得出,两者必须同文件,改列宽才不会漂。 */\n\n/* ═══ 展示配置(Step 3)═══ */\n\nexport const VIEW_TYPE_OPTIONS = [\n { value: 'normal', label: '标准视图' },\n { value: 'pivot', label: '透视表' },\n { value: 'pivot_charts', label: '透视图' },\n];\n\n/* ═══ 列配置(Step 4)═══ */\n\n/* cell_component 可选项(对照 liyu-pc-lib/src/components/cellComponent/ 实存组件,\n 挑通用项;金额等特殊语义可后续按需扩充)。\n ★ v29.38(用户定案):label 一律不带原始英文(原来写作「中文(datetime)」)——\n `value` 本身就是要落的英文枚举值,界面不再重复展示;若日后要按英文搜下拉,\n 给该 `a-select` 加 `:filter-option=\"(v, o) => String(o.value).includes(v) || String(o.label).includes(v)\"`。 */\nexport const CELL_COMPONENT_OPTIONS = [\n { value: 'datetime', label: '日期时间' },\n { value: 'datetime_hh_mm', label: '日期时间到分' },\n { value: 'time', label: '时间' },\n { value: 'date', label: '日期' },\n { value: 'date_year', label: '年份' },\n { value: 'month', label: '月份' },\n { value: 'boolean', label: '布尔' },\n { value: 'tag', label: '标签' },\n { value: 'tag_list', label: '标签列表' },\n { value: 'json', label: 'JSON' },\n { value: 'json_tag', label: 'JSON标签' },\n { value: 'money', label: '金额' },\n { value: 'to_fixed_0', label: '整数' },\n { value: 'to_fixed_2', label: '两位小数' },\n { value: 'to_fixed_4', label: '四位小数' },\n { value: 'percent', label: '百分比' },\n { value: 'tooltip', label: '悬浮提示' },\n { value: 'image', label: '图片' },\n { value: 'image_url', label: '图片链接' },\n { value: 'link', label: '链接' },\n { value: 'url_out', label: '外链' },\n { value: 'obj', label: '对象' },\n { value: 'list', label: '列表' },\n { value: 'color', label: '颜色块' },\n { value: 'bolder', label: '加粗' },\n { value: 'click_copy', label: '点击复制' },\n];\n\n/* 探测类型 → class_name / cell_component(对齐后端 sync_columns 的映射)\n ★ v29.23.1:后端 probe_columns 改真实行推断(_py_type_name),type 为\n int/float/bool/datetime/date/str 六类真实类型名,str 显式映射(probe 值优先于已有值)。 */\nexport const PROBE_CLASS_NAME_MAP: Record<string, string> = {\n int: 'int',\n float: 'float',\n bool: 'bool',\n datetime: 'datetime',\n date: 'date',\n str: 'str',\n};\nexport const PROBE_CELL_MAP: Record<string, string> = {\n datetime: 'datetime',\n date: 'date',\n bool: 'tag',\n json: 'json',\n};\n\n/* s-table 行高:本表每格都是 small 尺寸输入控件(24px),36 留出上下呼吸空间 */\nexport const COL_ROW_HEIGHT = 36;\n\n/* 列配置表列定义。\n antdv ColumnsType 对 fixed 等字段是字面量类型,这里用 any[] 放宽避免字面量拓宽报错。\n ★ v29.16(用户定案):去掉列宽(width)配置;新增 可拖宽(resizable)/汇总(summary)/\n 可排序(is_sortable);content_map+color_map 合并为「转换/颜色」一列(模态框联合配置);\n 新增「搜索组件」列(模态框配置高级搜索组件对象)。 */\nexport const COL_COLUMNS: any[] = [\n { title: '顺序', key: 'column_order', width: 80, fixed: 'left' },\n {\n title: '列名',\n dataIndex: 'data_index',\n key: 'data_index',\n width: 140,\n fixed: 'left',\n },\n { title: '列展示名', key: 'title', width: 130, fixed: 'left' },\n // ★ v29.40:原「唯一主键」→「主键」(主键改为可多选,措辞不能再带「唯一」);\n // 文案变短,列宽 85 → 70(COL_TABLE_WIDTH 由本数组求和派生,改宽自动同步)\n { title: '主键', key: 'is_primary_key', width: 70 },\n { title: '类型', dataIndex: 'class_name', key: 'class_name', width: 80 },\n\n { title: '查询名', key: 'query_title', width: 120 },\n { title: '单元格组件', key: 'cell_component', width: 150 },\n { title: '显示', key: 'checked', width: 55 },\n { title: '可拖宽', key: 'resizable', width: 65 },\n { title: '合计', key: 'summary', width: 55 },\n { title: '可排序', key: 'is_sortable', width: 65 },\n { title: '基础搜索', key: 'is_base_query', width: 75 },\n { title: '默认搜索', key: 'is_default_query', width: 75 },\n { title: '分组', key: 'is_group_query', width: 55 },\n { title: '默认排序', key: 'default_sort', width: 200 },\n { title: '转换/颜色', key: 'map_color', width: 90 },\n { title: '搜索组件', key: 'adv_search', width: 90 },\n { title: '操作', key: 'action', width: 60, fixed: 'right' },\n];\n\n/* ★ v29.34:s-table 的横向滚动宽度 = 表格总宽,直接由各列显式 width 求和得出\n (原来是手写的 `:scroll=\"{ x: 1685 }\"`,改列宽就会漂;现在改 width 自动跟随)。 */\nexport const COL_TABLE_WIDTH = COL_COLUMNS.reduce((sum: number, c: any) => sum + (c.width || 0), 0);\n\n/* ═══ 步骤导航(外壳用)═══ */\n\n/* 前进/跳步时只校验「当前步骤」的必填项(其余步骤的错误不可见,全量校验会让用户困惑)。\n ★ v29.23:菜单四项拆出 → 4 步。\n ★ v29.45(用户定案):「菜单与权限」挪到末位并改名「菜单」→ 现为\n 基础配置 / 展示配置 / 列配置 / 菜单。\n ⚠️ 本数组是**按步骤位置**排的:改步骤顺序时,这里的顺序与上面每个注释的措辞必须一起改,\n 否则会「校验了另一步的字段」还看不出错。 */\nexport const STEP_FIELDS: string[][] = [\n ['name', 'flag', 'data_source_id', 'sql'], // 第 1 步 基础配置\n [], // 第 2 步 展示配置(无必填项)\n [], // 第 3 步 列配置(随视图一起保存)\n [], // 第 4 步 菜单(无必填项:父级留空 = 顶级菜单;两项由 save_view 同步到菜单行)\n];\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"启用的视图类型\"\n name=\"enable_view_types\"\n extra=\"不选 = 全部启用\"\n >\n <a-select\n v-model:value=\"model.enable_view_types\"\n mode=\"multiple\"\n allow-clear\n placeholder=\"不选 = 全部启用\"\n :options=\"viewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"默认视图类型\"\n name=\"default_view_type\"\n extra=\"仅能在已启用的视图类型中选择;未启用任何类型时不限\"\n >\n <a-select\n v-model:value=\"model.default_view_type\"\n allow-clear\n placeholder=\"默认标准视图\"\n :options=\"defaultViewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"启用分页\">\n <a-switch\n v-model:checked=\"model.is_pagination\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数\"\n name=\"pageSize\"\n >\n <a-input-number\n v-model:value=\"model.pageSize\"\n style=\"width: 100%\"\n :min=\"1\"\n placeholder=\"默认 200\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数选项\"\n name=\"page_size_options\"\n >\n <a-select\n v-model:value=\"model.page_size_options\"\n mode=\"tags\"\n :open=\"false\"\n placeholder=\"如 10、20、50、100\"\n :token-separators=\"[',', ' ']\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from 'vue';\nimport { VIEW_TYPE_OPTIONS } from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「展示配置」步(★ v29.45 起为 Step 2)。\n 本步持有 view_meta 白名单里回传给前端的 5 个键(写在共享 model 上)。\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.13:org_filter 已废除(组织过滤只靠 SQL 里手写 {{org_id}}),不再提供配置项。\n 拆分约定同 Step 1:`model` 由外壳持有、对象引用共享,这里取 computed 后直接改属性。\n `view_meta` 的拆包/合并(applyViewMeta / buildViewMeta)已提为纯函数放 `./viewMeta.ts`。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\nconst viewTypeOptions = VIEW_TYPE_OPTIONS;\n\n/* ★ v29.18:default_view_type 仅能在 enable_view_types 勾选的值中选择;\n enable_view_types 为空 = 全部启用,default_view_type 不受限 */\nconst defaultViewTypeOptions = computed(() => {\n const enabled = model.value.enable_view_types;\n return Array.isArray(enabled) && enabled.length\n ? viewTypeOptions.filter(o => enabled.includes(o.value))\n : viewTypeOptions;\n});\n\nwatch(\n () => model.value.enable_view_types,\n enabled => {\n if (\n Array.isArray(enabled) &&\n enabled.length &&\n model.value.default_view_type &&\n !enabled.includes(model.value.default_view_type)\n ) {\n model.value.default_view_type = undefined; // 启用范围变更后默认类型失效 → 清空\n }\n },\n);\n</script>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"启用的视图类型\"\n name=\"enable_view_types\"\n extra=\"不选 = 全部启用\"\n >\n <a-select\n v-model:value=\"model.enable_view_types\"\n mode=\"multiple\"\n allow-clear\n placeholder=\"不选 = 全部启用\"\n :options=\"viewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"默认视图类型\"\n name=\"default_view_type\"\n extra=\"仅能在已启用的视图类型中选择;未启用任何类型时不限\"\n >\n <a-select\n v-model:value=\"model.default_view_type\"\n allow-clear\n placeholder=\"默认标准视图\"\n :options=\"defaultViewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"启用分页\">\n <a-switch\n v-model:checked=\"model.is_pagination\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数\"\n name=\"pageSize\"\n >\n <a-input-number\n v-model:value=\"model.pageSize\"\n style=\"width: 100%\"\n :min=\"1\"\n placeholder=\"默认 200\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数选项\"\n name=\"page_size_options\"\n >\n <a-select\n v-model:value=\"model.page_size_options\"\n mode=\"tags\"\n :open=\"false\"\n placeholder=\"如 10、20、50、100\"\n :token-separators=\"[',', ' ']\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from 'vue';\nimport { VIEW_TYPE_OPTIONS } from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「展示配置」步(★ v29.45 起为 Step 2)。\n 本步持有 view_meta 白名单里回传给前端的 5 个键(写在共享 model 上)。\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.13:org_filter 已废除(组织过滤只靠 SQL 里手写 {{org_id}}),不再提供配置项。\n 拆分约定同 Step 1:`model` 由外壳持有、对象引用共享,这里取 computed 后直接改属性。\n `view_meta` 的拆包/合并(applyViewMeta / buildViewMeta)已提为纯函数放 `./viewMeta.ts`。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\nconst viewTypeOptions = VIEW_TYPE_OPTIONS;\n\n/* ★ v29.18:default_view_type 仅能在 enable_view_types 勾选的值中选择;\n enable_view_types 为空 = 全部启用,default_view_type 不受限 */\nconst defaultViewTypeOptions = computed(() => {\n const enabled = model.value.enable_view_types;\n return Array.isArray(enabled) && enabled.length\n ? viewTypeOptions.filter(o => enabled.includes(o.value))\n : viewTypeOptions;\n});\n\nwatch(\n () => model.value.enable_view_types,\n enabled => {\n if (\n Array.isArray(enabled) &&\n enabled.length &&\n model.value.default_view_type &&\n !enabled.includes(model.value.default_view_type)\n ) {\n model.value.default_view_type = undefined; // 启用范围变更后默认类型失效 → 清空\n }\n },\n);\n</script>\n","<template>\n <a-modal\n :title=\"`转换与颜色 —— ${record?.title || record?.data_index || ''}`\"\n width=\"760px\"\n :open=\"open\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <div class=\"map-tip\">\n 原值 → 显示文本 与 原值 → 颜色 在此一体化配置:一行 =\n 一条「原值」的转换与颜色,落库时拆回两个键。 最右「预览」按运行时标签组件的真实样式渲染 ——\n 内置色是浅底彩字、 自定义色是实心彩底白字,两者观感不同属正常。\n </div>\n <a-table\n :data-source=\"rows\"\n :columns=\"columns\"\n row-key=\"__k\"\n size=\"small\"\n :pagination=\"false\"\n >\n <template #bodyCell=\"{ column, record: row }\">\n <template v-if=\"column.key === 'value'\">\n <a-input\n v-model:value=\"row.value\"\n size=\"small\"\n placeholder=\"原值\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'label'\">\n <a-input\n v-model:value=\"row.label\"\n size=\"small\"\n placeholder=\"显示文本\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'color'\">\n <div class=\"color-cell\">\n <!-- ★ v29.23(用户定案):内置色板直接存颜色名字符串(pink/red/…,不转十六进制);\n 自定义走颜色选择器(选择器产出 #十六进制),不再让用户手输。\n ★ v29.23.2:a-color-picker 在 antdv 4.2.6 中不存在(es 目录无 color-picker,\n 模板里写它是未知组件被静默渲染为空——「自定义颜色没出现」的根因),\n 改用原生 input[type=color](产出 #rrggbb,同尺寸色块形态)。 -->\n <div class=\"swatches\">\n <span\n v-for=\"c in PRESET_COLORS\"\n :key=\"c\"\n class=\"swatch\"\n :class=\"{ active: row.color === c }\"\n :style=\"{ background: PRESET_HEX[c] }\"\n :title=\"c\"\n @click=\"row.color = c\"\n />\n <span class=\"custom-color-label\">自定义:</span>\n <input\n type=\"color\"\n class=\"custom-color-input\"\n :class=\"{ active: isCustomColor(row) }\"\n :value=\"row.color && row.color.startsWith('#') ? row.color : '#1677ff'\"\n :title=\"isCustomColor(row) ? row.color : '自定义颜色'\"\n @input=\"onCustomColor(row, $event)\"\n />\n </div>\n </div>\n </template>\n <template v-else-if=\"column.key === 'preview'\">\n <!-- ★ v29.39(用户定案):a-tag 实时预览 —— 把「原值 + 显示文本 + 颜色」按运行时\n cell_tag.vue 的语义渲染出来,配色的观感当场可见(色名浅底彩字 / 十六进制实心彩底白字)。\n ⚠️ 文案与颜色必须与 liyu-pc-lib/src/components/cellComponent/cell_tag.vue 逐条对齐,\n 见脚本里 previewText / previewColor 的注释;这里显示什么,真机就长什么样。 -->\n <div class=\"preview-cell\">\n <a-tag\n v-if=\"previewText(row)\"\n :color=\"previewColor(row)\"\n >\n {{ previewText(row) }}\n </a-tag>\n <span\n v-else\n class=\"preview-empty\"\n >\n —\n </span>\n </div>\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n @click=\"removeRow(row.__k)\"\n >\n 删除\n </a-button>\n </template>\n </template>\n </a-table>\n <a-button\n size=\"small\"\n type=\"dashed\"\n block\n style=\"margin-top: 8px\"\n @click=\"addRow\"\n >\n + 添加一条转换\n </a-button>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, ref, watch } from 'vue';\n\n/* ★ v29.16(用户定案):content_map(原值→显示文本)与 color_map(原值→颜色)不再割裂为\n 两个配置入口,而是一张「转换与颜色」联合表 —— 一行 = {原值, 显示文本, 颜色},\n 确定时按原值拆回 content_map / color_map 两个键写回行记录(运行时契约不变)。 */\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(colRows 中的一项,直接读写其 content_map / color_map)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst columns: any[] = [\n { title: '原值', key: 'value', width: '18%' },\n { title: '显示文本', key: 'label', width: '18%' },\n { title: '颜色', key: 'color', width: '36%' },\n { title: '预览', key: 'preview', width: '20%' },\n { title: '', key: 'action', width: '8%' },\n];\n\n/* ★ v29.23(用户定案):内置色板直接存 antd 预设色**名字符串**(red/orange/green/cyan/\n blue/purple,运行时 tag/色块组件按名字渲染),不转十六进制;PRESET_HEX 仅用于色板展示。\n 自定义 = 颜色选择器(a-color-picker),产出 #十六进制字符串。\n ⚠️ `pink` 已从色板移除:历史数据里存的 `pink` 运行时仍能正常渲染(antd 侧自带该预设色),\n 但配置弹窗里**既不会点亮色块、也不会点亮取色器**(`pink` 不以 `#` 开头,又不在 PRESET_COLORS 里)\n —— 这个「0 个亮」是已知退化,已由技能里的 preview-vs-runtime-check.mjs 断言钉住。 */\nconst PRESET_COLORS = ['red', 'orange', 'green', 'cyan', 'blue', 'purple'];\nconst PRESET_HEX: Record<string, string> = {\n red: '#f5222d',\n orange: '#fa8c16',\n green: '#52c41a',\n cyan: '#13c2c2',\n blue: '#1677ff',\n purple: '#722ed1',\n};\n\n/* 自定义取色:原生 input[type=color] 事件值恒为 #rrggbb 字符串(v29.23.2,替代不存在的 a-color-picker) */\nconst onCustomColor = (row: any, e: Event) => {\n row.color = (e.target as HTMLInputElement).value;\n};\n\n/* ★ v29.39(用户定案):自定义颜色也要有「已选中」高亮,与内置色块的 .active 同款。\n 判定取「值是 # 开头的十六进制」= 取色器的产出形态 ——\n ① 与取色器 :value 的取值判定同源,不会出现「高亮了但取色器显示的并不是这个色」;\n ② 也与运行时一致:cell_tag.vue 把该值直接交给 a-tag 的 color,\n antd 内部按「是否预设色名」分两路渲染(色名 → 浅底彩字 / 十六进制 → 实心彩底白字),\n 所以「# 开头」正是「走了自定义分支」的准确刻画。 */\nconst isCustomColor = (row: any) =>\n String(row.color || '')\n .trim()\n .startsWith('#');\n\nlet seq = 0;\nconst rows = ref<any[]>([]);\nconst buildRow = (value = '', label = '', color = '') => ({ __k: ++seq, value, label, color });\n\n/* 每次打开时从行记录重建本地编辑态(content_map/color_map → 联合行) */\nwatch(\n () => props.open,\n val => {\n if (!val) return;\n seq = 0;\n const cm = props.record?.content_map || {};\n const colors = props.record?.color_map || {};\n const keys = Array.from(new Set([...Object.keys(cm || {}), ...Object.keys(colors || {})]));\n rows.value = keys.length\n ? keys.map(k => buildRow(String(k), cm?.[k] ?? '', colors?.[k] ?? ''))\n : [buildRow()];\n },\n);\n\nconst addRow = () => rows.value.push(buildRow());\nconst removeRow = (k: number) => {\n rows.value = rows.value.filter(r => r.__k !== k);\n};\n\n/* ★ v29.39:a-tag 预览 —— 与运行时 cell_tag.vue 一一对应,保证「所见即真机所得」。\n 运行时(liyu-pc-lib/src/components/cellComponent/cell_tag.vue)的算法:\n tagText = contentMap[text] || contentMap['other'] || text (text = 单元格原始值)\n tagColor = colorMap ? (colorMap[text] || colorMap['other']) : undefined\n 其中 `other` 是约定的兜底键:任何未命中的原始值都落到它。\n ⚠️ 外层 `v-if=\"cmpObj.column.contentMap\"` 看似是「没配转换就不渲染」,实际不成立 ——\n 后端 columns_config() 产出的是 `c.content_map or {}`(空 map → 空对象,truthy),\n 所以「只配颜色」的列同样会渲染标签,文字退回原始值。预览照此实现,不另加限制。 */\n\n/* 兜底行 = 原值填 `other` 的那一行(可能不存在)。\n 取**最后一条**与 handleOk 的落库口径一致(重复原值时保留最后一次),否则预览会和真机不符。 */\nconst otherRow = computed(() => {\n const hits = rows.value.filter(r => (r.value || '').trim() === 'other');\n return hits.length ? hits[hits.length - 1] : undefined;\n});\n\n/* 预览文案:本行显示文本 → `other` 兜底显示文本 → 原始值 */\nconst previewText = (row: any) => {\n const v = (row.value || '').trim();\n if (!v) return ''; // 无原值 → 该行谈不上渲染\n const label = (row.label || '').trim() || (otherRow.value?.label || '').trim();\n return label || v;\n};\n\n/* 预览颜色:本行颜色 → `other` 兜底颜色;都没有 → undefined(a-tag 默认样式,与运行时一致) */\nconst previewColor = (row: any) => {\n if (!(row.value || '').trim()) return undefined;\n const color = (row.color || '').trim() || (otherRow.value?.color || '').trim();\n return color || undefined;\n};\n\nconst handleOk = () => {\n const content_map: Record<string, string> = {};\n const color_map: Record<string, string> = {};\n for (const r of rows.value) {\n const v = (r.value || '').trim();\n if (!v) continue; // 空原值行直接忽略\n if (v in content_map) {\n message.warning(`原值「${v}」重复,已保留最后一次配置`);\n }\n const label = (r.label || '').trim();\n const color = (r.color || '').trim();\n if (label) content_map[v] = label;\n if (color) color_map[v] = color;\n }\n props.record.content_map = Object.keys(content_map).length ? content_map : null;\n props.record.color_map = Object.keys(color_map).length ? color_map : null;\n /* ★ v29.23(用户定案):确定时若有配置项 → 单元格组件自动置为 tag(仅自动改,\n 用户随后可手动改成其他组件,不强制) */\n if (props.record.content_map || props.record.color_map) {\n props.record.cell_component = 'tag';\n }\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.map-tip {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n line-height: 1.5;\n}\n.color-cell {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n/* ★ v29.39:a-tag 预览列。窄列里必须收掉 a-tag 自带的 margin-inline-end(antd 默认 8px),\n 否则标签会被挤出单元格;显示文本过长按省略号截断,不换行、不撑高行。 */\n.preview-cell {\n display: flex;\n align-items: center;\n overflow: hidden;\n :deep(.ant-tag) {\n margin-inline-end: 0;\n max-width: 100%;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n}\n.preview-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.25);\n}\n.swatches {\n display: flex;\n gap: 4px;\n flex: none;\n}\n.swatch {\n width: 16px;\n height: 16px;\n border-radius: 3px;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.15);\n}\n/* 选中态高亮:内置色块与自定义取色器**同口径**(v29.39 用户定案:配了自定义色也要高亮,\n 否则用户看不出当前行到底选的是色板还是取色器)。放在两者之后,保证覆盖各自的基础样式。 */\n.swatch.active,\n.custom-color-input.active {\n outline: 2px solid #1677ff;\n outline-offset: 1px;\n}\n.custom-color-label {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 16px;\n}\n/* 原生取色输入:整成与色块同尺寸的小方块(点击弹系统取色器) */\n.custom-color-input {\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n &::-webkit-color-swatch-wrapper {\n padding: 0;\n }\n &::-webkit-color-swatch {\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 3px;\n }\n}\n</style>\n","<template>\n <a-modal\n :title=\"`转换与颜色 —— ${record?.title || record?.data_index || ''}`\"\n width=\"760px\"\n :open=\"open\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <div class=\"map-tip\">\n 原值 → 显示文本 与 原值 → 颜色 在此一体化配置:一行 =\n 一条「原值」的转换与颜色,落库时拆回两个键。 最右「预览」按运行时标签组件的真实样式渲染 ——\n 内置色是浅底彩字、 自定义色是实心彩底白字,两者观感不同属正常。\n </div>\n <a-table\n :data-source=\"rows\"\n :columns=\"columns\"\n row-key=\"__k\"\n size=\"small\"\n :pagination=\"false\"\n >\n <template #bodyCell=\"{ column, record: row }\">\n <template v-if=\"column.key === 'value'\">\n <a-input\n v-model:value=\"row.value\"\n size=\"small\"\n placeholder=\"原值\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'label'\">\n <a-input\n v-model:value=\"row.label\"\n size=\"small\"\n placeholder=\"显示文本\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'color'\">\n <div class=\"color-cell\">\n <!-- ★ v29.23(用户定案):内置色板直接存颜色名字符串(pink/red/…,不转十六进制);\n 自定义走颜色选择器(选择器产出 #十六进制),不再让用户手输。\n ★ v29.23.2:a-color-picker 在 antdv 4.2.6 中不存在(es 目录无 color-picker,\n 模板里写它是未知组件被静默渲染为空——「自定义颜色没出现」的根因),\n 改用原生 input[type=color](产出 #rrggbb,同尺寸色块形态)。 -->\n <div class=\"swatches\">\n <span\n v-for=\"c in PRESET_COLORS\"\n :key=\"c\"\n class=\"swatch\"\n :class=\"{ active: row.color === c }\"\n :style=\"{ background: PRESET_HEX[c] }\"\n :title=\"c\"\n @click=\"row.color = c\"\n />\n <span class=\"custom-color-label\">自定义:</span>\n <input\n type=\"color\"\n class=\"custom-color-input\"\n :class=\"{ active: isCustomColor(row) }\"\n :value=\"row.color && row.color.startsWith('#') ? row.color : '#1677ff'\"\n :title=\"isCustomColor(row) ? row.color : '自定义颜色'\"\n @input=\"onCustomColor(row, $event)\"\n />\n </div>\n </div>\n </template>\n <template v-else-if=\"column.key === 'preview'\">\n <!-- ★ v29.39(用户定案):a-tag 实时预览 —— 把「原值 + 显示文本 + 颜色」按运行时\n cell_tag.vue 的语义渲染出来,配色的观感当场可见(色名浅底彩字 / 十六进制实心彩底白字)。\n ⚠️ 文案与颜色必须与 liyu-pc-lib/src/components/cellComponent/cell_tag.vue 逐条对齐,\n 见脚本里 previewText / previewColor 的注释;这里显示什么,真机就长什么样。 -->\n <div class=\"preview-cell\">\n <a-tag\n v-if=\"previewText(row)\"\n :color=\"previewColor(row)\"\n >\n {{ previewText(row) }}\n </a-tag>\n <span\n v-else\n class=\"preview-empty\"\n >\n —\n </span>\n </div>\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n @click=\"removeRow(row.__k)\"\n >\n 删除\n </a-button>\n </template>\n </template>\n </a-table>\n <a-button\n size=\"small\"\n type=\"dashed\"\n block\n style=\"margin-top: 8px\"\n @click=\"addRow\"\n >\n + 添加一条转换\n </a-button>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, ref, watch } from 'vue';\n\n/* ★ v29.16(用户定案):content_map(原值→显示文本)与 color_map(原值→颜色)不再割裂为\n 两个配置入口,而是一张「转换与颜色」联合表 —— 一行 = {原值, 显示文本, 颜色},\n 确定时按原值拆回 content_map / color_map 两个键写回行记录(运行时契约不变)。 */\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(colRows 中的一项,直接读写其 content_map / color_map)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst columns: any[] = [\n { title: '原值', key: 'value', width: '18%' },\n { title: '显示文本', key: 'label', width: '18%' },\n { title: '颜色', key: 'color', width: '36%' },\n { title: '预览', key: 'preview', width: '20%' },\n { title: '', key: 'action', width: '8%' },\n];\n\n/* ★ v29.23(用户定案):内置色板直接存 antd 预设色**名字符串**(red/orange/green/cyan/\n blue/purple,运行时 tag/色块组件按名字渲染),不转十六进制;PRESET_HEX 仅用于色板展示。\n 自定义 = 颜色选择器(a-color-picker),产出 #十六进制字符串。\n ⚠️ `pink` 已从色板移除:历史数据里存的 `pink` 运行时仍能正常渲染(antd 侧自带该预设色),\n 但配置弹窗里**既不会点亮色块、也不会点亮取色器**(`pink` 不以 `#` 开头,又不在 PRESET_COLORS 里)\n —— 这个「0 个亮」是已知退化,已由技能里的 preview-vs-runtime-check.mjs 断言钉住。 */\nconst PRESET_COLORS = ['red', 'orange', 'green', 'cyan', 'blue', 'purple'];\nconst PRESET_HEX: Record<string, string> = {\n red: '#f5222d',\n orange: '#fa8c16',\n green: '#52c41a',\n cyan: '#13c2c2',\n blue: '#1677ff',\n purple: '#722ed1',\n};\n\n/* 自定义取色:原生 input[type=color] 事件值恒为 #rrggbb 字符串(v29.23.2,替代不存在的 a-color-picker) */\nconst onCustomColor = (row: any, e: Event) => {\n row.color = (e.target as HTMLInputElement).value;\n};\n\n/* ★ v29.39(用户定案):自定义颜色也要有「已选中」高亮,与内置色块的 .active 同款。\n 判定取「值是 # 开头的十六进制」= 取色器的产出形态 ——\n ① 与取色器 :value 的取值判定同源,不会出现「高亮了但取色器显示的并不是这个色」;\n ② 也与运行时一致:cell_tag.vue 把该值直接交给 a-tag 的 color,\n antd 内部按「是否预设色名」分两路渲染(色名 → 浅底彩字 / 十六进制 → 实心彩底白字),\n 所以「# 开头」正是「走了自定义分支」的准确刻画。 */\nconst isCustomColor = (row: any) =>\n String(row.color || '')\n .trim()\n .startsWith('#');\n\nlet seq = 0;\nconst rows = ref<any[]>([]);\nconst buildRow = (value = '', label = '', color = '') => ({ __k: ++seq, value, label, color });\n\n/* 每次打开时从行记录重建本地编辑态(content_map/color_map → 联合行) */\nwatch(\n () => props.open,\n val => {\n if (!val) return;\n seq = 0;\n const cm = props.record?.content_map || {};\n const colors = props.record?.color_map || {};\n const keys = Array.from(new Set([...Object.keys(cm || {}), ...Object.keys(colors || {})]));\n rows.value = keys.length\n ? keys.map(k => buildRow(String(k), cm?.[k] ?? '', colors?.[k] ?? ''))\n : [buildRow()];\n },\n);\n\nconst addRow = () => rows.value.push(buildRow());\nconst removeRow = (k: number) => {\n rows.value = rows.value.filter(r => r.__k !== k);\n};\n\n/* ★ v29.39:a-tag 预览 —— 与运行时 cell_tag.vue 一一对应,保证「所见即真机所得」。\n 运行时(liyu-pc-lib/src/components/cellComponent/cell_tag.vue)的算法:\n tagText = contentMap[text] || contentMap['other'] || text (text = 单元格原始值)\n tagColor = colorMap ? (colorMap[text] || colorMap['other']) : undefined\n 其中 `other` 是约定的兜底键:任何未命中的原始值都落到它。\n ⚠️ 外层 `v-if=\"cmpObj.column.contentMap\"` 看似是「没配转换就不渲染」,实际不成立 ——\n 后端 columns_config() 产出的是 `c.content_map or {}`(空 map → 空对象,truthy),\n 所以「只配颜色」的列同样会渲染标签,文字退回原始值。预览照此实现,不另加限制。 */\n\n/* 兜底行 = 原值填 `other` 的那一行(可能不存在)。\n 取**最后一条**与 handleOk 的落库口径一致(重复原值时保留最后一次),否则预览会和真机不符。 */\nconst otherRow = computed(() => {\n const hits = rows.value.filter(r => (r.value || '').trim() === 'other');\n return hits.length ? hits[hits.length - 1] : undefined;\n});\n\n/* 预览文案:本行显示文本 → `other` 兜底显示文本 → 原始值 */\nconst previewText = (row: any) => {\n const v = (row.value || '').trim();\n if (!v) return ''; // 无原值 → 该行谈不上渲染\n const label = (row.label || '').trim() || (otherRow.value?.label || '').trim();\n return label || v;\n};\n\n/* 预览颜色:本行颜色 → `other` 兜底颜色;都没有 → undefined(a-tag 默认样式,与运行时一致) */\nconst previewColor = (row: any) => {\n if (!(row.value || '').trim()) return undefined;\n const color = (row.color || '').trim() || (otherRow.value?.color || '').trim();\n return color || undefined;\n};\n\nconst handleOk = () => {\n const content_map: Record<string, string> = {};\n const color_map: Record<string, string> = {};\n for (const r of rows.value) {\n const v = (r.value || '').trim();\n if (!v) continue; // 空原值行直接忽略\n if (v in content_map) {\n message.warning(`原值「${v}」重复,已保留最后一次配置`);\n }\n const label = (r.label || '').trim();\n const color = (r.color || '').trim();\n if (label) content_map[v] = label;\n if (color) color_map[v] = color;\n }\n props.record.content_map = Object.keys(content_map).length ? content_map : null;\n props.record.color_map = Object.keys(color_map).length ? color_map : null;\n /* ★ v29.23(用户定案):确定时若有配置项 → 单元格组件自动置为 tag(仅自动改,\n 用户随后可手动改成其他组件,不强制) */\n if (props.record.content_map || props.record.color_map) {\n props.record.cell_component = 'tag';\n }\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.map-tip {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n line-height: 1.5;\n}\n.color-cell {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n/* ★ v29.39:a-tag 预览列。窄列里必须收掉 a-tag 自带的 margin-inline-end(antd 默认 8px),\n 否则标签会被挤出单元格;显示文本过长按省略号截断,不换行、不撑高行。 */\n.preview-cell {\n display: flex;\n align-items: center;\n overflow: hidden;\n :deep(.ant-tag) {\n margin-inline-end: 0;\n max-width: 100%;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n}\n.preview-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.25);\n}\n.swatches {\n display: flex;\n gap: 4px;\n flex: none;\n}\n.swatch {\n width: 16px;\n height: 16px;\n border-radius: 3px;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.15);\n}\n/* 选中态高亮:内置色块与自定义取色器**同口径**(v29.39 用户定案:配了自定义色也要高亮,\n 否则用户看不出当前行到底选的是色板还是取色器)。放在两者之后,保证覆盖各自的基础样式。 */\n.swatch.active,\n.custom-color-input.active {\n outline: 2px solid #1677ff;\n outline-offset: 1px;\n}\n.custom-color-label {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 16px;\n}\n/* 原生取色输入:整成与色块同尺寸的小方块(点击弹系统取色器) */\n.custom-color-input {\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n &::-webkit-color-swatch-wrapper {\n padding: 0;\n }\n &::-webkit-color-swatch {\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 3px;\n }\n}\n</style>\n","import { ConditionOperatorMap } from 'liyu-pc-base/api/typing';\n\n/* ★ v29.20:ColumnSearchComponentModal 拆分产物。\n 所有类型分组 / 类型默认值 / 运算符集集中于此(port 自 FormDesignModal/data.ts + defaultConfig,\n 裁剪到搜索支持的范围),供各基础配置组件与分类型 Setting 组件共用。 */\n\n/* ───────── 栅格约定(★ v29.31,改布局前先读) ─────────\n 容器 = 右侧抽屉,宽 920px(ColumnSearchComponentModal 的 SEARCH_DRAWER_WIDTH);\n 表单标签列固定 190px(`:label-col=\"{ style: { width: '190px' } }\"`)。\n\n 于是每一列留给控件的宽度 ≈ 列宽 − 190px:\n · span=24 → 888 − 190 = 698px (整行独占:长文本 / 表格 / 提示)\n · span=12 → 444 − 190 = 254px (**每行 2 个控件**,本目录的默认密度)\n · span=8 → 296 − 190 = 106px ← 已废弃:106px 装不下日期格式串/数值框,会被挤成两行\n · span=6 / span=4 → 更窄,同样废弃\n ⇒ **约定:一行最多 2 个控件(span=12),整行独占才用 24。** 新增配置项照此选 span;\n 若调整 SEARCH_DRAWER_WIDTH 或标签列宽度,**回到这里重算一次**(本目录所有 :span 都是 12/24)。 */\n\n/* ───────── 运算符(port 自 FieldSetting 的查询条件配置) ───────── */\nexport const OPERATOR: Record<string, string> = ConditionOperatorMap as any;\nexport const TEXT_OPS = ['eq', 'icontains', 'istartswith', 'iendswith', 'not_eq', 'not_blank_not_null', 'contains', 'startswith', 'endswith', 'noticontains', 'notistartswith', 'notiendswith', 'notcontains', 'notstartswith', 'notendswith'];\nexport const NUMBER_OPS = ['between', 'gt', 'lt', 'eq', 'gte', 'lte', 'notbetween', 'not_eq', 'not_blank_not_null'];\nexport const SELECT_OPS = ['in', 'notin'];\nexport const DATE_OPS = ['between', 'eq', 'gt', 'lt', 'gte', 'lte', 'notbetween'];\nexport const SEARCH_CLASS_NAME = ['int', 'str', 'bool', 'datetime'];\nexport const FORMAT_TYPE = ['input', 'remote', 'choices'];\n/* antdv select option 的 value 类型声明不含 boolean,这里放宽(布尔固定查询值需要 true/false) */\nexport const BOOL_OPTIONS: any[] = [\n { value: true, label: 'true' },\n { value: false, label: 'false' },\n];\n\n/* ───────── 组件类型分组 ───────── */\n/** 本地扁平选项组件(options 数组) */\nexport const FLAT_OPTION_TYPES = ['select', 'radio', 'checkbox'];\n/** ★ v29.29:本地静态选项类(选项列表在「数据源」页配置)——原版 FieldSetting 的\n * 「选项列表」编辑器覆盖范围就是这三类;groupselect 走 a-select 的 :options 直传\n * (不认 valueKey/labelKey),同样需要本地 options,故一并纳入。 */\nexport const LOCAL_OPTION_TYPES = ['select', 'radio', 'checkbox', 'groupselect'];\n/** FlatSelectSetting 覆盖的类型(FLAT_OPTION_TYPES + groupselect) */\nexport const FLAT_SETTING_TYPES = ['select', 'radio', 'checkbox', 'groupselect'];\n/** 用 mode(default/multiple/tags) 表达单选多选的类型 */\nexport const MODE_TYPES = ['select', 'remoteselectpro', 'remotecomplete', 'remotetext'];\n/** 用 multiple(bool) 表达单选多选的类型 */\nexport const MULTIPLE_TYPES = ['treeselect', 'cascader', 'remotetreeselectpro', 'remotecascaderpro'];\n/** 选择类(运算符取 in/notin 的判定范围) */\nexport const SELECT_LIKE_TYPES = [...FLAT_OPTION_TYPES, 'treeselect', 'cascader', 'groupselect', 'remotefilterselect', ...MODE_TYPES, ...MULTIPLE_TYPES];\n/** 支持 maxlength / 前后缀标签的类型 */\nexport const TEXT_WITH_ADDON_TYPES = ['input', 'number'];\n/** 数值范围配置的类型 */\nexport const NUMBER_TYPES = ['number', 'money'];\n/** 日期时间类 */\nexport const DATE_TYPES = ['datepicker', 'monthpicker', 'monthrangepicker', 'timepicker', 'timerangepicker', 'rangepicker'];\n/** placeholder 为 [开始, 结束] 区间双输入的类型 */\nexport const RANGE_PLACEHOLDER_TYPES = ['rangepicker', 'timerangepicker', 'monthrangepicker'];\n/** 远程数据源类(数据源页适用) */\nexport const REMOTE_TYPES = ['remoteselectpro', 'remotecomplete', 'remotetreeselectpro', 'remotecascaderpro', 'remotetext'];\n\n/* ───────── 可选组件类型(SmallTableSerarchField 支持且适合搜索的子集,按 defaultConfig 分组) ───────── */\nexport const typeOptions = [\n {\n label: '输入类',\n options: [\n { value: 'input', label: '单行文本框' },\n { value: 'number', label: '数字' },\n { value: 'money', label: '金额' },\n { value: 'remotetext', label: '远程文本' },\n { value: 'remotecomplete', label: '自动补全文本框' },\n ],\n },\n {\n label: '选择类',\n options: [\n { value: 'select', label: '下拉选框' },\n { value: 'radio', label: '单选框' },\n { value: 'checkbox', label: '多选框' },\n { value: 'treeselect', label: '树形下拉' },\n { value: 'cascader', label: '级联下拉' },\n { value: 'remoteselectpro', label: '远程下拉选框' },\n { value: 'remotetreeselectpro', label: '远程树形下拉' },\n { value: 'remotecascaderpro', label: '远程级联下拉' },\n { value: 'remotefilterselect', label: '远程筛选下拉' },\n { value: 'groupselect', label: '分组下拉' },\n ],\n },\n {\n label: '日期/时间',\n options: [\n { value: 'datepicker', label: '日期时间选择器' },\n { value: 'rangepicker', label: '日期时间区间' },\n { value: 'monthpicker', label: '月份选择器' },\n { value: 'monthrangepicker', label: '月份区间' },\n { value: 'timepicker', label: '时间选择器' },\n { value: 'timerangepicker', label: '时间区间' },\n ],\n },\n];\n\n/* ───────── 本地静态选项契约(v29.29) ───────── */\n/** 选项是否有效:value 必须有真值语义(0 / false 合法,null / undefined / '' 视为未填)。\n * 供「选项列表」编辑与保存校验共用——空值选项在 a-select 里无法选中,落库即脏配置。 */\nexport const hasOptionValue = (o: any) =>\n !!o && o.value !== null && o.value !== undefined && o.value !== '';\n\n/* ───────── 各类型默认属性(port 自 defaultConfig.ts;搜索场景 select 默认多选) ───────── */\nexport const DS_DEFAULTS: Record<string, any> = {\n labelKeyType: 1,\n labelKeyList: [],\n labelSpace: '-',\n valueKey: 'id',\n labelKey: 'name',\n queryType: 'advanced',\n searchConditions: [[{ searchType: 3, field_name: 'is_active', operator: 'eq', value: [true], format: 'choices', class_name: 'bool' }]],\n simpleSearchConditions: [],\n};\nexport const REMOTE_BASE = { show_search: true, postUrl: '', ...DS_DEFAULTS };\nexport const TYPE_DEFAULTS: Record<string, any> = {\n input: { type: 'input', label: '单行文本框', inputType: 'text', maxlength: 100, allowClear: true, placeholder: '' },\n number: { type: 'number', label: '数字', inputType: 'number', precision: null, step: 1, allowClear: true, placeholder: '' },\n money: { type: 'money', label: '金额', precision: 2, allowClear: true, placeholder: '' },\n remotetext: { type: 'remotetext', label: '远程文本', mode: 'default', ...REMOTE_BASE },\n remotecomplete: { type: 'remotecomplete', label: '自动补全文本框', mode: 'default', ...REMOTE_BASE },\n /* ★ v29.29:本地静态选项类【不放 valueKey/labelKey】——选项对象统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 的唯一形状:`util/field_config.py` 产出 `options:[{label,value}]`\n 且不写这两个键;后端 `_adv_search_item` 由 content_map 自动推断下拉时同样只写 `{value,label}`)。\n 运行时 `SmallTableSerarchField.vue` 读的是 `option[valueKey||'value']` / `option[labelKey||'label']`,\n groupselect 更是把 options 直接丢给 a-select 的 `:options`(不认键)——所以一旦残留\n `labelKey:'name'`,`{value,label}` 的选项会整列渲染成空白。 */\n select: { type: 'select', label: '下拉选框', mode: 'multiple', show_search: false, options: [], allowClear: true, placeholder: '' },\n radio: { type: 'radio', label: '单选框', options: [] },\n checkbox: { type: 'checkbox', label: '多选框', options: [] },\n treeselect: { type: 'treeselect', label: '树形下拉选框', multiple: false, show_search: false, valueKey: 'value', labelKey: 'name', options: [], allowClear: true, placeholder: '' },\n cascader: { type: 'cascader', label: '级联下拉选框', multiple: false, show_search: false, valueKey: 'value', labelKey: 'name', options: [], allowClear: true, placeholder: '' },\n remotefilterselect: { type: 'remotefilterselect', label: '远程筛选下拉', ...REMOTE_BASE },\n groupselect: { type: 'groupselect', label: '分组下拉', show_search: false, options: [], allowClear: true, placeholder: '' },\n remoteselectpro: { type: 'remoteselectpro', label: '远程下拉选框', mode: 'default', ...REMOTE_BASE },\n remotetreeselectpro: { type: 'remotetreeselectpro', label: '远程树形下拉框', multiple: false, ...REMOTE_BASE },\n remotecascaderpro: { type: 'remotecascaderpro', label: '远程级联下拉框', multiple: false, show_search: false, ...REMOTE_BASE },\n datepicker: { type: 'datepicker', label: '日期时间选择器', showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', allowClear: true, placeholder: '' },\n rangepicker: { type: 'rangepicker', label: '日期时间区间', showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', placeholder: ['开始时间', '结束时间'], allowClear: true },\n monthpicker: { type: 'monthpicker', label: '月份选择器', showTime: false, format: 'YYYY-MM', valueFormat: 'YYYY-MM-DD', allowClear: true, placeholder: '' },\n monthrangepicker: { type: 'monthrangepicker', label: '月份区间', picker: 'month', showTime: false, format: 'YYYY-MM', valueFormat: 'YYYY-MM-DD', placeholder: ['开始月份', '结束月份'], allowClear: true },\n timepicker: { type: 'timepicker', label: '时间选择器', format: 'HH:mm:ss', valueFormat: 'HH:mm:ss', allowClear: true, placeholder: '' },\n timerangepicker: { type: 'timerangepicker', label: '时间区间', format: 'HH:mm:ss', valueFormat: 'HH:mm:ss', placeholder: ['开始时间', '结束时间'], allowClear: true },\n};\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item label=\"组件类型\">\n <a-select\n :value=\"config.type\"\n placeholder=\"选择搜索组件\"\n show-search\n option-filter-prop=\"label\"\n :options=\"typeOptions\"\n @change=\"onTypeChange\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"名称\">\n <a-input\n v-model:value=\"config.label\"\n allow-clear\n :placeholder=\"record?.title || '默认=列中文名'\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"可清空\">\n <a-switch\n v-model:checked=\"config.allowClear\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"禁用\">\n <a-switch\n v-model:checked=\"config.disabled\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"isRangeType\"\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-space-compact\n block\n style=\"width: 100%\"\n >\n <a-input\n v-model:value=\"config.placeholder[0]\"\n placeholder=\"开始\"\n :style=\"{ width: '50%' }\"\n />\n <a-input\n v-model:value=\"config.placeholder[1]\"\n placeholder=\"结束\"\n :style=\"{ width: '50%' }\"\n />\n </a-space-compact>\n </a-form-item>\n </a-col>\n <a-col\n v-else\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-input\n v-model:value=\"config.placeholder\"\n allow-clear\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\n/* 基础属性(所有组件类型共享):类型选择 + label + 可清空/禁用 + placeholder(区间类型双输入)。\n 类型切换交给父组件处理(需要合并类型默认值)。 */\nimport { computed } from 'vue';\nimport { RANGE_PLACEHOLDER_TYPES, typeOptions } from './constants';\n\nconst props = defineProps<{\n config: any;\n record: any; // 列配置行(取列中文名作 label 默认值)\n}>();\nconst emit = defineEmits<{ (e: 'type-change', type: string): void }>();\n\nconst isRangeType = computed(() => RANGE_PLACEHOLDER_TYPES.includes(props.config?.type));\nconst onTypeChange = (type: any) => emit('type-change', type);\n</script>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item label=\"组件类型\">\n <a-select\n :value=\"config.type\"\n placeholder=\"选择搜索组件\"\n show-search\n option-filter-prop=\"label\"\n :options=\"typeOptions\"\n @change=\"onTypeChange\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"名称\">\n <a-input\n v-model:value=\"config.label\"\n allow-clear\n :placeholder=\"record?.title || '默认=列中文名'\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"可清空\">\n <a-switch\n v-model:checked=\"config.allowClear\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"禁用\">\n <a-switch\n v-model:checked=\"config.disabled\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"isRangeType\"\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-space-compact\n block\n style=\"width: 100%\"\n >\n <a-input\n v-model:value=\"config.placeholder[0]\"\n placeholder=\"开始\"\n :style=\"{ width: '50%' }\"\n />\n <a-input\n v-model:value=\"config.placeholder[1]\"\n placeholder=\"结束\"\n :style=\"{ width: '50%' }\"\n />\n </a-space-compact>\n </a-form-item>\n </a-col>\n <a-col\n v-else\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-input\n v-model:value=\"config.placeholder\"\n allow-clear\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\n/* 基础属性(所有组件类型共享):类型选择 + label + 可清空/禁用 + placeholder(区间类型双输入)。\n 类型切换交给父组件处理(需要合并类型默认值)。 */\nimport { computed } from 'vue';\nimport { RANGE_PLACEHOLDER_TYPES, typeOptions } from './constants';\n\nconst props = defineProps<{\n config: any;\n record: any; // 列配置行(取列中文名作 label 默认值)\n}>();\nconst emit = defineEmits<{ (e: 'type-change', type: string): void }>();\n\nconst isRangeType = computed(() => RANGE_PLACEHOLDER_TYPES.includes(props.config?.type));\nconst onTypeChange = (type: any) => emit('type-change', type);\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"数值字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.valueKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.valueKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入数值字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"!remote || config.labelKeyType === 1\"\n :span=\"12\"\n >\n <a-form-item label=\"显示字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.labelKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.labelKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入显示字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值字段/显示字段配置(共享):\n 本地选项组件(select/radio/checkbox/groupselect/treeselect/cascader)用 a-input;\n 远程组件用 a-auto-complete(候选来自数据源字段列表,labelKeyType=2 时 labelKey 由 labelKeyList 编辑)。 */\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n config: any;\n remote?: boolean;\n options?: any[]; // 远程字段候选(value/label)\n loading?: boolean;\n}>();\n\nconst remote = computed(() => !!props.remote);\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"数值字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.valueKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.valueKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入数值字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"!remote || config.labelKeyType === 1\"\n :span=\"12\"\n >\n <a-form-item label=\"显示字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.labelKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.labelKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入显示字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值字段/显示字段配置(共享):\n 本地选项组件(select/radio/checkbox/groupselect/treeselect/cascader)用 a-input;\n 远程组件用 a-auto-complete(候选来自数据源字段列表,labelKeyType=2 时 labelKey 由 labelKeyList 编辑)。 */\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n config: any;\n remote?: boolean;\n options?: any[]; // 远程字段候选(value/label)\n loading?: boolean;\n}>();\n\nconst remote = computed(() => !!props.remote);\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n</script>\n","<template>\n <!-- ★ v29.29:本地静态选项列表(select/radio/checkbox/groupselect)——原版\n FormDesignModal/FieldSetting.vue「数据源」页的「选项列表」移植(原版仅含这三类)。\n 落库形状对齐 lybase ADVANCED_SEARCH + choices 契约:`config.options = [{value, label}]`\n (`util/field_config.py` 由 Django choices/BooleanField 产出的就是这一形状,且不写 valueKey/labelKey;\n 运行时 `SmallTableSerarchField.vue` 按 `option[valueKey||'value'] / option[labelKey||'label']` 取值)。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>选项列表</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRow\"\n >\n + 添加选项\n </a-button>\n <a-button\n v-if=\"importable\"\n size=\"small\"\n @click=\"importFromContentMap\"\n >\n 从「转换/颜色」导入\n </a-button>\n <span class=\"opt-tip\">值类型按该列在 SQL 中的实际类型选择(数值列勿用字符,Oracle 下会直接比较失败)。</span>\n </div>\n <div\n v-if=\"!rows.length\"\n class=\"opt-empty\"\n >\n 暂无选项 —— 下拉选框/单选框/多选框至少需要一个选项才能使用。\n </div>\n <div\n v-for=\"(row, i) in rows\"\n :key=\"i\"\n class=\"row-line\"\n >\n <a-select\n v-model:value=\"row.valueType\"\n class=\"w-type\"\n :options=\"VALUE_TYPE_OPTIONS\"\n @change=\"onValueTypeChange(row)\"\n />\n <a-select\n v-if=\"row.valueType === 'bool'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input-number\n v-else-if=\"row.valueType === 'int'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n />\n <a-input\n v-else\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n allow-clear\n />\n <a-input\n v-model:value=\"row.label\"\n class=\"w-label\"\n placeholder=\"显示文本\"\n allow-clear\n />\n <a-button\n type=\"link\"\n danger\n @click=\"rows.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 选项列表编辑器:本地维护 rows(含 valueType,便于按类型选输入控件),\n 任何编辑立即回写 `config.options = [{value, label}]`(实时镜像 JSON 页的同源语义)。\n 与 TreeDataConfig 同款「config 引用变化 → 重新初始化」做法:外壳切类型/JSON 页回写会整体替换 config。 */\nimport { nextTick, ref, watch } from 'vue';\nimport { BOOL_OPTIONS } from './constants';\n\nconst props = defineProps<{\n config: any;\n /** 列配置行:用 class_name 定新选项的值类型默认值、用 content_map 做「导入」 */\n record?: any;\n}>();\n\nconst VALUE_TYPE_OPTIONS = [\n { value: 'int', label: '数字' },\n { value: 'str', label: '字符' },\n { value: 'bool', label: '布尔' },\n];\n\nconst rows = ref<any[]>([]);\n/* 回写自身触发的同步重入守卫:写回 config.options 会命中下面的 watch,靠「内容相等就跳过」收敛 */\nlet syncing = false;\n\nconst inferValueType = (v: any) =>\n typeof v === 'boolean' ? 'bool' : typeof v === 'number' ? 'int' : 'str';\n\n/** SQL 列类型 → 默认值类型(class_name 由列探测写入) */\nconst defaultValueType = () => {\n const cls = String(props.record?.class_name || '').toLowerCase();\n if (cls === 'int' || cls === 'float' || cls === 'decimal' || cls === 'number') return 'int';\n if (cls === 'bool' || cls === 'boolean') return 'bool';\n return 'str';\n};\n\nconst syncFromConfig = () => {\n const opts = Array.isArray(props.config?.options) ? props.config.options : [];\n const next = opts.map((o: any) => ({\n valueType: inferValueType(o?.value),\n value: o?.value ?? null,\n // 兼容历史上按创建/编辑表单形状写的 {value, name}(发布前的老配置)\n label: o?.label ?? o?.name ?? '',\n }));\n if (JSON.stringify(next) === JSON.stringify(rows.value)) return; // 无变化 → 不动,避免回写循环\n syncing = true;\n rows.value = next;\n nextTick(() => {\n syncing = false;\n });\n};\nwatch(() => props.config?.options, syncFromConfig, { immediate: true });\n\n/* 值类型切换:把已填的值按新类型归一(否则 a-select 拿 true 去比对字符串 'true' 会不匹配) */\nconst coerceValue = (row: any) => {\n const v = row.value;\n if (row.valueType === 'bool') {\n if (v === true || v === 'true' || v === 1 || v === '1') return true;\n if (v === false || v === 'false' || v === 0 || v === '0') return false;\n return null;\n }\n if (row.valueType === 'int') {\n if (v === null || v === undefined || v === '') return null;\n const n = Number(v);\n return Number.isNaN(n) ? null : n;\n }\n return v === null || v === undefined ? '' : String(v);\n};\nconst onValueTypeChange = (row: any) => {\n row.value = coerceValue(row);\n};\n\nconst writeBack = () => {\n if (syncing) return;\n props.config.options = rows.value.map((r: any) => ({\n value: coerceValue(r),\n label: r.label == null ? '' : String(r.label),\n }));\n};\nwatch(rows, writeBack, { deep: true });\n\nconst addRow = () => {\n rows.value.push({ valueType: defaultValueType(), value: null, label: '' });\n};\n\n/* ───────── 从「转换/颜色」(content_map) 导入 ─────────\n ★ 必要性:后端 `_adv_search_item` 只在【没有人工搜索组件】时才用 content_map 自动推断\n `type='select' + options`。一旦人工把组件配成下拉,自动推断即让位 → 手工配置的下拉会\n 覆盖掉原本可用的自动选项。这里给一键回填,避免「配了转换/颜色 + 手工下拉」后选项为空。 */\nconst importable = ref(false);\nwatch(\n () => props.record?.content_map,\n (cm) => {\n importable.value = !!cm && typeof cm === 'object' && !Array.isArray(cm) && Object.keys(cm).length > 0;\n },\n { immediate: true },\n);\n\nconst importFromContentMap = () => {\n const cm = props.record?.content_map;\n if (!cm || typeof cm !== 'object') return;\n const vt = defaultValueType();\n const exist = new Set(rows.value.map((r: any) => String(coerceValue(r))));\n Object.keys(cm).forEach((k) => {\n const raw = vt === 'int' ? Number(k) : vt === 'bool' ? k === 'true' || k === '1' : k;\n const value = vt === 'int' && Number.isNaN(raw as number) ? k : raw;\n if (exist.has(String(value))) return;\n exist.add(String(value));\n rows.value.push({ valueType: vt, value, label: cm[k] ? String(cm[k]) : k });\n });\n};\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.opt-tip {\n font-size: 12px;\n font-weight: 400;\n color: rgba(0, 0, 0, 0.45);\n}\n.opt-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.w-type {\n flex: none;\n width: 100px;\n}\n.w-val {\n flex: 1 1 0;\n min-width: 140px;\n}\n.w-label {\n flex: 1 1 0;\n min-width: 180px;\n}\n</style>\n","<template>\n <!-- ★ v29.29:本地静态选项列表(select/radio/checkbox/groupselect)——原版\n FormDesignModal/FieldSetting.vue「数据源」页的「选项列表」移植(原版仅含这三类)。\n 落库形状对齐 lybase ADVANCED_SEARCH + choices 契约:`config.options = [{value, label}]`\n (`util/field_config.py` 由 Django choices/BooleanField 产出的就是这一形状,且不写 valueKey/labelKey;\n 运行时 `SmallTableSerarchField.vue` 按 `option[valueKey||'value'] / option[labelKey||'label']` 取值)。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>选项列表</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRow\"\n >\n + 添加选项\n </a-button>\n <a-button\n v-if=\"importable\"\n size=\"small\"\n @click=\"importFromContentMap\"\n >\n 从「转换/颜色」导入\n </a-button>\n <span class=\"opt-tip\">值类型按该列在 SQL 中的实际类型选择(数值列勿用字符,Oracle 下会直接比较失败)。</span>\n </div>\n <div\n v-if=\"!rows.length\"\n class=\"opt-empty\"\n >\n 暂无选项 —— 下拉选框/单选框/多选框至少需要一个选项才能使用。\n </div>\n <div\n v-for=\"(row, i) in rows\"\n :key=\"i\"\n class=\"row-line\"\n >\n <a-select\n v-model:value=\"row.valueType\"\n class=\"w-type\"\n :options=\"VALUE_TYPE_OPTIONS\"\n @change=\"onValueTypeChange(row)\"\n />\n <a-select\n v-if=\"row.valueType === 'bool'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input-number\n v-else-if=\"row.valueType === 'int'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n />\n <a-input\n v-else\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n allow-clear\n />\n <a-input\n v-model:value=\"row.label\"\n class=\"w-label\"\n placeholder=\"显示文本\"\n allow-clear\n />\n <a-button\n type=\"link\"\n danger\n @click=\"rows.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 选项列表编辑器:本地维护 rows(含 valueType,便于按类型选输入控件),\n 任何编辑立即回写 `config.options = [{value, label}]`(实时镜像 JSON 页的同源语义)。\n 与 TreeDataConfig 同款「config 引用变化 → 重新初始化」做法:外壳切类型/JSON 页回写会整体替换 config。 */\nimport { nextTick, ref, watch } from 'vue';\nimport { BOOL_OPTIONS } from './constants';\n\nconst props = defineProps<{\n config: any;\n /** 列配置行:用 class_name 定新选项的值类型默认值、用 content_map 做「导入」 */\n record?: any;\n}>();\n\nconst VALUE_TYPE_OPTIONS = [\n { value: 'int', label: '数字' },\n { value: 'str', label: '字符' },\n { value: 'bool', label: '布尔' },\n];\n\nconst rows = ref<any[]>([]);\n/* 回写自身触发的同步重入守卫:写回 config.options 会命中下面的 watch,靠「内容相等就跳过」收敛 */\nlet syncing = false;\n\nconst inferValueType = (v: any) =>\n typeof v === 'boolean' ? 'bool' : typeof v === 'number' ? 'int' : 'str';\n\n/** SQL 列类型 → 默认值类型(class_name 由列探测写入) */\nconst defaultValueType = () => {\n const cls = String(props.record?.class_name || '').toLowerCase();\n if (cls === 'int' || cls === 'float' || cls === 'decimal' || cls === 'number') return 'int';\n if (cls === 'bool' || cls === 'boolean') return 'bool';\n return 'str';\n};\n\nconst syncFromConfig = () => {\n const opts = Array.isArray(props.config?.options) ? props.config.options : [];\n const next = opts.map((o: any) => ({\n valueType: inferValueType(o?.value),\n value: o?.value ?? null,\n // 兼容历史上按创建/编辑表单形状写的 {value, name}(发布前的老配置)\n label: o?.label ?? o?.name ?? '',\n }));\n if (JSON.stringify(next) === JSON.stringify(rows.value)) return; // 无变化 → 不动,避免回写循环\n syncing = true;\n rows.value = next;\n nextTick(() => {\n syncing = false;\n });\n};\nwatch(() => props.config?.options, syncFromConfig, { immediate: true });\n\n/* 值类型切换:把已填的值按新类型归一(否则 a-select 拿 true 去比对字符串 'true' 会不匹配) */\nconst coerceValue = (row: any) => {\n const v = row.value;\n if (row.valueType === 'bool') {\n if (v === true || v === 'true' || v === 1 || v === '1') return true;\n if (v === false || v === 'false' || v === 0 || v === '0') return false;\n return null;\n }\n if (row.valueType === 'int') {\n if (v === null || v === undefined || v === '') return null;\n const n = Number(v);\n return Number.isNaN(n) ? null : n;\n }\n return v === null || v === undefined ? '' : String(v);\n};\nconst onValueTypeChange = (row: any) => {\n row.value = coerceValue(row);\n};\n\nconst writeBack = () => {\n if (syncing) return;\n props.config.options = rows.value.map((r: any) => ({\n value: coerceValue(r),\n label: r.label == null ? '' : String(r.label),\n }));\n};\nwatch(rows, writeBack, { deep: true });\n\nconst addRow = () => {\n rows.value.push({ valueType: defaultValueType(), value: null, label: '' });\n};\n\n/* ───────── 从「转换/颜色」(content_map) 导入 ─────────\n ★ 必要性:后端 `_adv_search_item` 只在【没有人工搜索组件】时才用 content_map 自动推断\n `type='select' + options`。一旦人工把组件配成下拉,自动推断即让位 → 手工配置的下拉会\n 覆盖掉原本可用的自动选项。这里给一键回填,避免「配了转换/颜色 + 手工下拉」后选项为空。 */\nconst importable = ref(false);\nwatch(\n () => props.record?.content_map,\n (cm) => {\n importable.value = !!cm && typeof cm === 'object' && !Array.isArray(cm) && Object.keys(cm).length > 0;\n },\n { immediate: true },\n);\n\nconst importFromContentMap = () => {\n const cm = props.record?.content_map;\n if (!cm || typeof cm !== 'object') return;\n const vt = defaultValueType();\n const exist = new Set(rows.value.map((r: any) => String(coerceValue(r))));\n Object.keys(cm).forEach((k) => {\n const raw = vt === 'int' ? Number(k) : vt === 'bool' ? k === 'true' || k === '1' : k;\n const value = vt === 'int' && Number.isNaN(raw as number) ? k : raw;\n if (exist.has(String(value))) return;\n exist.add(String(value));\n rows.value.push({ valueType: vt, value, label: cm[k] ? String(cm[k]) : k });\n });\n};\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.opt-tip {\n font-size: 12px;\n font-weight: 400;\n color: rgba(0, 0, 0, 0.45);\n}\n.opt-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.w-type {\n flex: none;\n width: 100px;\n}\n.w-val {\n flex: 1 1 0;\n min-width: 140px;\n}\n.w-label {\n flex: 1 1 0;\n min-width: 180px;\n}\n</style>\n","<template>\n <a-auto-complete\n :value=\"value\"\n :loading=\"loading\"\n style=\"width: 100%\"\n :options=\"options\"\n @update:value=\"handleUpdate\"\n @search=\"handleSearch\"\n >\n <template #option=\"{ value: v, label: l }\">\n <div class=\"opt-line opt-label\">{{ l }}</div>\n <div class=\"opt-line opt-value\">{{ v }}</div>\n </template>\n <template #default>\n <a-input-search\n :placeholder=\"placeholder\"\n :value=\"value\"\n size=\"middle\"\n @search=\"handleSearch\"\n >\n <template #suffix>\n <LoadingOutlined v-if=\"loading\" />\n <span\n v-else\n :title=\"findItem(value)?.label\"\n class=\"post-url-suffix\"\n >\n {{ findItem(value)?.label }}\n </span>\n </template>\n </a-input-search>\n </template>\n </a-auto-complete>\n</template>\n\n<script setup lang=\"ts\">\n/* 数据源(postUrl)选择字段(port 自 FormDesignModal/BtnConfig/columnNameField.vue):\n - a-auto-complete 变体:选项双行显示(第一行=数据源名称,第二行=URL);\n - 输入框为 a-input-search,suffix 实时显示当前选中数据源的名称(非裸 URL);\n - @search 输入本地过滤 value/label,同时 emit('search') 供父级做 >100 条时的远程过滤;\n - 选中项变化时 emit('select', item)(item 含 data 原始数据)供父级自动回填。 */\nimport { LoadingOutlined } from '@ant-design/icons-vue';\nimport { computed, ref } from 'vue';\n\nconst props = defineProps<{\n value: string;\n itemList: any[]; // [{ value: post_url, label: name, data: item }]\n placeholder?: string;\n loading?: boolean;\n}>();\nconst emit = defineEmits<{\n (e: 'update:value', value: string): void;\n (e: 'select', item: any): void;\n (e: 'search', str: string): void;\n}>();\n\n/* ★ v29.22.1:options 由 itemList + 搜索词实时 computed 派生(此前是独立 ref,只在 @search 时\n 重建——itemList 被父级远程搜索替换后 options 停在旧子集,且初始为空数组首开无选项)。 */\nconst searchText = ref('');\nconst lastSelected = ref<any>(null);\nconst findItem = (v: any) => props.itemList.find((item: any) => item.value === v) as any;\n\nconst options = computed(() =>\n props.itemList\n .filter(\n (item: any) =>\n !searchText.value ||\n item.value.includes(searchText.value) ||\n (item.label || '').includes(searchText.value),\n )\n .map((item: any) => ({ value: item.value, label: item.label })),\n);\n\nconst handleSearch = (str: any) => {\n searchText.value = str || '';\n emit('search', str);\n};\n\nconst handleUpdate = (v: any) => {\n emit('update:value', v);\n const currentSelected = findItem(v);\n if (currentSelected && currentSelected.value !== lastSelected.value) {\n emit('select', currentSelected);\n lastSelected.value = currentSelected.value;\n /* 选中后重置本地过滤词:重新打开下拉时展示完整列表(而非停在关键字子集) */\n searchText.value = '';\n }\n};\n</script>\n\n<style scoped lang=\"less\">\n.opt-line {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n &.opt-label {\n font-size: 14px;\n color: #2f2f2f;\n }\n &.opt-value {\n font-size: 12px;\n color: #999;\n }\n}\n.post-url-suffix {\n font-size: 12px;\n color: #999;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n","<template>\n <a-auto-complete\n :value=\"value\"\n :loading=\"loading\"\n style=\"width: 100%\"\n :options=\"options\"\n @update:value=\"handleUpdate\"\n @search=\"handleSearch\"\n >\n <template #option=\"{ value: v, label: l }\">\n <div class=\"opt-line opt-label\">{{ l }}</div>\n <div class=\"opt-line opt-value\">{{ v }}</div>\n </template>\n <template #default>\n <a-input-search\n :placeholder=\"placeholder\"\n :value=\"value\"\n size=\"middle\"\n @search=\"handleSearch\"\n >\n <template #suffix>\n <LoadingOutlined v-if=\"loading\" />\n <span\n v-else\n :title=\"findItem(value)?.label\"\n class=\"post-url-suffix\"\n >\n {{ findItem(value)?.label }}\n </span>\n </template>\n </a-input-search>\n </template>\n </a-auto-complete>\n</template>\n\n<script setup lang=\"ts\">\n/* 数据源(postUrl)选择字段(port 自 FormDesignModal/BtnConfig/columnNameField.vue):\n - a-auto-complete 变体:选项双行显示(第一行=数据源名称,第二行=URL);\n - 输入框为 a-input-search,suffix 实时显示当前选中数据源的名称(非裸 URL);\n - @search 输入本地过滤 value/label,同时 emit('search') 供父级做 >100 条时的远程过滤;\n - 选中项变化时 emit('select', item)(item 含 data 原始数据)供父级自动回填。 */\nimport { LoadingOutlined } from '@ant-design/icons-vue';\nimport { computed, ref } from 'vue';\n\nconst props = defineProps<{\n value: string;\n itemList: any[]; // [{ value: post_url, label: name, data: item }]\n placeholder?: string;\n loading?: boolean;\n}>();\nconst emit = defineEmits<{\n (e: 'update:value', value: string): void;\n (e: 'select', item: any): void;\n (e: 'search', str: string): void;\n}>();\n\n/* ★ v29.22.1:options 由 itemList + 搜索词实时 computed 派生(此前是独立 ref,只在 @search 时\n 重建——itemList 被父级远程搜索替换后 options 停在旧子集,且初始为空数组首开无选项)。 */\nconst searchText = ref('');\nconst lastSelected = ref<any>(null);\nconst findItem = (v: any) => props.itemList.find((item: any) => item.value === v) as any;\n\nconst options = computed(() =>\n props.itemList\n .filter(\n (item: any) =>\n !searchText.value ||\n item.value.includes(searchText.value) ||\n (item.label || '').includes(searchText.value),\n )\n .map((item: any) => ({ value: item.value, label: item.label })),\n);\n\nconst handleSearch = (str: any) => {\n searchText.value = str || '';\n emit('search', str);\n};\n\nconst handleUpdate = (v: any) => {\n emit('update:value', v);\n const currentSelected = findItem(v);\n if (currentSelected && currentSelected.value !== lastSelected.value) {\n emit('select', currentSelected);\n lastSelected.value = currentSelected.value;\n /* 选中后重置本地过滤词:重新打开下拉时展示完整列表(而非停在关键字子集) */\n searchText.value = '';\n }\n};\n</script>\n\n<style scoped lang=\"less\">\n.opt-line {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n &.opt-label {\n font-size: 14px;\n color: #2f2f2f;\n }\n &.opt-value {\n font-size: 12px;\n color: #999;\n }\n}\n.post-url-suffix {\n font-size: 12px;\n color: #999;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n","<template>\n <!-- 查询参数(queryType):高级=条件分组编辑器(监听/固定/上下文 + 运算符 + 固定值);\n 简单=JSON value 条目。port 自 FieldSetting 查询条件配置。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>查询参数</span>\n <a-radio-group\n v-model:value=\"config.queryType\"\n size=\"small\"\n >\n <a-radio-button value=\"simple\">简单查询</a-radio-button>\n <a-radio-button value=\"advanced\">高级查询</a-radio-button>\n </a-radio-group>\n <a-button\n v-if=\"config.queryType === 'advanced'\"\n type=\"primary\"\n size=\"small\"\n @click=\"addSearchConditions\"\n >\n + 添加查询条件分组\n </a-button>\n <a-button\n v-else\n type=\"primary\"\n size=\"small\"\n @click=\"addSimpleSearchItem\"\n >\n + 添加查询条件\n </a-button>\n </div>\n </a-col>\n\n <!-- 高级查询:条件分组编辑器 -->\n <a-col\n v-if=\"config.queryType === 'advanced'\"\n :span=\"24\"\n >\n <div\n v-for=\"(group, gi) in config.searchConditions || []\"\n :key=\"gi\"\n class=\"cond-group\"\n >\n <div class=\"group-header\">\n <span class=\"group-title\">查询条件组 {{ gi + 1 }}</span>\n <a-space>\n <a-button\n size=\"small\"\n type=\"link\"\n @click=\"addSearchItem(gi)\"\n >\n + 添加查询条件\n </a-button>\n <a-button\n size=\"small\"\n type=\"link\"\n danger\n @click=\"config.searchConditions.splice(gi, 1)\"\n >\n 删除分组\n </a-button>\n </a-space>\n </div>\n <div\n v-for=\"(item, ii) in group\"\n :key=\"ii\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ ii + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 160px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"group.splice(ii, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div class=\"cond-row\">\n <a-select\n v-model:value=\"item.format\"\n style=\"width: 110px\"\n :options=\"FORMAT_TYPE.map((f) => ({ value: f, label: f }))\"\n />\n <a-select\n v-model:value=\"item.operator\"\n style=\"width: 190px\"\n placeholder=\"判断条件\"\n :options=\"operatorOptions\"\n @change=\"ensureValueShape(item)\"\n />\n <a-select\n v-model:value=\"item.class_name\"\n style=\"width: 110px\"\n placeholder=\"数值类型\"\n :options=\"SEARCH_CLASS_NAME.map((c) => ({ value: c, label: c }))\"\n @change=\"item.value = []\"\n />\n <!-- 固定查询才可填值 -->\n <template v-if=\"item.searchType === 3\">\n <template v-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'str'\">\n <a-input\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'int'\">\n <a-input-number\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input-number\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'datetime'\">\n <a-range-picker\n v-model:value=\"item.value\"\n show-time\n style=\"width: 260px\"\n />\n </template>\n <a-select\n v-else-if=\"item.class_name === 'bool'\"\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input\n v-else-if=\"item.operator === 'not_blank_not_null'\"\n value=\"\"\n style=\"width: 150px\"\n disabled\n placeholder=\"(无值)\"\n />\n <a-input\n v-else\n v-model:value=\"item.value[0]\"\n style=\"width: 150px\"\n placeholder=\"值\"\n allow-clear\n />\n </template>\n </div>\n </div>\n </div>\n </a-col>\n\n <!-- 简单查询 -->\n <a-col\n v-if=\"config.queryType === 'simple'\"\n :span=\"24\"\n >\n <div class=\"cond-group\">\n <div\n v-for=\"(item, si) in config.simpleSearchConditions || []\"\n :key=\"si\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ si + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 170px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"config.simpleSearchConditions.splice(si, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div\n v-if=\"item.searchType === 3\"\n class=\"cond-row\"\n >\n <a-textarea\n :value=\"typeof item.value === 'object' ? JSON.stringify(item.value) : item.value\"\n placeholder=\"请输入值(对象/数组/字符串)\"\n :auto-size=\"{ minRows: 2, maxRows: 6 }\"\n style=\"flex: 1\"\n @input=\"(e: any) => handleTextareaInput(e, item)\"\n />\n <a-tag\n v-if=\"item.value !== '' && item.value != null\"\n color=\"blue\"\n >\n {{ getDataType(item.value) }}\n </a-tag>\n </div>\n </div>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport {\n BOOL_OPTIONS,\n DATE_OPS,\n DATE_TYPES,\n FORMAT_TYPE,\n NUMBER_OPS,\n NUMBER_TYPES,\n OPERATOR,\n SEARCH_CLASS_NAME,\n SELECT_LIKE_TYPES,\n SELECT_OPS,\n TEXT_OPS,\n} from './constants';\n\nconst props = defineProps<{\n config: any;\n fieldOptions: any[]; // 数据源字段候选\n fieldOptionsLoading?: boolean;\n}>();\n\nconst SEARCH_TYPE_OPTIONS = [\n { value: 1, label: '监听查询' },\n { value: 3, label: '固定查询' },\n { value: 4, label: '上下文' },\n];\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* 运算符候选按组件类型过滤 */\nconst operatorOptions = computed(() => {\n const t = props.config?.type;\n let ops = TEXT_OPS;\n if (NUMBER_TYPES.includes(t)) ops = NUMBER_OPS;\n else if (DATE_TYPES.includes(t)) ops = DATE_OPS;\n else if (SELECT_LIKE_TYPES.includes(t)) ops = SELECT_OPS;\n return Object.keys(OPERATOR)\n .filter((k) => ops.includes(k))\n .map((k) => ({ value: k, label: `${OPERATOR[k]}(${k})` }));\n});\n\nconst addSearchConditions = () => {\n if (!Array.isArray(props.config.searchConditions)) props.config.searchConditions = [];\n props.config.searchConditions.push([{ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] }]);\n};\nconst addSearchItem = (gi: number) => {\n props.config.searchConditions[gi].push({ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] });\n};\n/* operator/class_name 变化时保证 value 形状(between 需两位) */\nconst ensureValueShape = (item: any) => {\n if (item.operator === 'between' || item.operator === 'notbetween') {\n const v0 = Array.isArray(item.value) ? item.value[0] : item.value;\n const v1 = Array.isArray(item.value) ? item.value[1] : undefined;\n item.value = [v0 ?? null, v1 ?? null];\n } else if (!Array.isArray(item.value)) {\n item.value = [item.value];\n } else if (item.value.length > 1) {\n item.value = [item.value[0]];\n }\n};\n\nconst addSimpleSearchItem = () => {\n if (!Array.isArray(props.config.simpleSearchConditions)) props.config.simpleSearchConditions = [];\n props.config.simpleSearchConditions.push({ searchType: 3, field_name: '', value: '' });\n};\nconst handleTextareaInput = (e: any, item: any) => {\n try {\n item.value = JSON.parse(e.target.value);\n } catch {\n item.value = e.target.value;\n }\n};\nconst getDataType = (value: any) => (typeof value === 'object' ? 'json' : typeof value);\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.cond-group {\n border: 1px solid rgba(0, 0, 0, 0.08);\n border-radius: 6px;\n padding: 8px;\n margin-bottom: 8px;\n}\n.group-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n .group-title {\n font-weight: 500;\n }\n}\n.cond-item {\n padding: 4px 0;\n border-top: 1px dashed rgba(0, 0, 0, 0.06);\n}\n.cond-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 4px 0;\n flex-wrap: wrap;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n.range-sep {\n text-align: center;\n}\n</style>\n","<template>\n <!-- 查询参数(queryType):高级=条件分组编辑器(监听/固定/上下文 + 运算符 + 固定值);\n 简单=JSON value 条目。port 自 FieldSetting 查询条件配置。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>查询参数</span>\n <a-radio-group\n v-model:value=\"config.queryType\"\n size=\"small\"\n >\n <a-radio-button value=\"simple\">简单查询</a-radio-button>\n <a-radio-button value=\"advanced\">高级查询</a-radio-button>\n </a-radio-group>\n <a-button\n v-if=\"config.queryType === 'advanced'\"\n type=\"primary\"\n size=\"small\"\n @click=\"addSearchConditions\"\n >\n + 添加查询条件分组\n </a-button>\n <a-button\n v-else\n type=\"primary\"\n size=\"small\"\n @click=\"addSimpleSearchItem\"\n >\n + 添加查询条件\n </a-button>\n </div>\n </a-col>\n\n <!-- 高级查询:条件分组编辑器 -->\n <a-col\n v-if=\"config.queryType === 'advanced'\"\n :span=\"24\"\n >\n <div\n v-for=\"(group, gi) in config.searchConditions || []\"\n :key=\"gi\"\n class=\"cond-group\"\n >\n <div class=\"group-header\">\n <span class=\"group-title\">查询条件组 {{ gi + 1 }}</span>\n <a-space>\n <a-button\n size=\"small\"\n type=\"link\"\n @click=\"addSearchItem(gi)\"\n >\n + 添加查询条件\n </a-button>\n <a-button\n size=\"small\"\n type=\"link\"\n danger\n @click=\"config.searchConditions.splice(gi, 1)\"\n >\n 删除分组\n </a-button>\n </a-space>\n </div>\n <div\n v-for=\"(item, ii) in group\"\n :key=\"ii\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ ii + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 160px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"group.splice(ii, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div class=\"cond-row\">\n <a-select\n v-model:value=\"item.format\"\n style=\"width: 110px\"\n :options=\"FORMAT_TYPE.map((f) => ({ value: f, label: f }))\"\n />\n <a-select\n v-model:value=\"item.operator\"\n style=\"width: 190px\"\n placeholder=\"判断条件\"\n :options=\"operatorOptions\"\n @change=\"ensureValueShape(item)\"\n />\n <a-select\n v-model:value=\"item.class_name\"\n style=\"width: 110px\"\n placeholder=\"数值类型\"\n :options=\"SEARCH_CLASS_NAME.map((c) => ({ value: c, label: c }))\"\n @change=\"item.value = []\"\n />\n <!-- 固定查询才可填值 -->\n <template v-if=\"item.searchType === 3\">\n <template v-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'str'\">\n <a-input\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'int'\">\n <a-input-number\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input-number\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'datetime'\">\n <a-range-picker\n v-model:value=\"item.value\"\n show-time\n style=\"width: 260px\"\n />\n </template>\n <a-select\n v-else-if=\"item.class_name === 'bool'\"\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input\n v-else-if=\"item.operator === 'not_blank_not_null'\"\n value=\"\"\n style=\"width: 150px\"\n disabled\n placeholder=\"(无值)\"\n />\n <a-input\n v-else\n v-model:value=\"item.value[0]\"\n style=\"width: 150px\"\n placeholder=\"值\"\n allow-clear\n />\n </template>\n </div>\n </div>\n </div>\n </a-col>\n\n <!-- 简单查询 -->\n <a-col\n v-if=\"config.queryType === 'simple'\"\n :span=\"24\"\n >\n <div class=\"cond-group\">\n <div\n v-for=\"(item, si) in config.simpleSearchConditions || []\"\n :key=\"si\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ si + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 170px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"config.simpleSearchConditions.splice(si, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div\n v-if=\"item.searchType === 3\"\n class=\"cond-row\"\n >\n <a-textarea\n :value=\"typeof item.value === 'object' ? JSON.stringify(item.value) : item.value\"\n placeholder=\"请输入值(对象/数组/字符串)\"\n :auto-size=\"{ minRows: 2, maxRows: 6 }\"\n style=\"flex: 1\"\n @input=\"(e: any) => handleTextareaInput(e, item)\"\n />\n <a-tag\n v-if=\"item.value !== '' && item.value != null\"\n color=\"blue\"\n >\n {{ getDataType(item.value) }}\n </a-tag>\n </div>\n </div>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport {\n BOOL_OPTIONS,\n DATE_OPS,\n DATE_TYPES,\n FORMAT_TYPE,\n NUMBER_OPS,\n NUMBER_TYPES,\n OPERATOR,\n SEARCH_CLASS_NAME,\n SELECT_LIKE_TYPES,\n SELECT_OPS,\n TEXT_OPS,\n} from './constants';\n\nconst props = defineProps<{\n config: any;\n fieldOptions: any[]; // 数据源字段候选\n fieldOptionsLoading?: boolean;\n}>();\n\nconst SEARCH_TYPE_OPTIONS = [\n { value: 1, label: '监听查询' },\n { value: 3, label: '固定查询' },\n { value: 4, label: '上下文' },\n];\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* 运算符候选按组件类型过滤 */\nconst operatorOptions = computed(() => {\n const t = props.config?.type;\n let ops = TEXT_OPS;\n if (NUMBER_TYPES.includes(t)) ops = NUMBER_OPS;\n else if (DATE_TYPES.includes(t)) ops = DATE_OPS;\n else if (SELECT_LIKE_TYPES.includes(t)) ops = SELECT_OPS;\n return Object.keys(OPERATOR)\n .filter((k) => ops.includes(k))\n .map((k) => ({ value: k, label: `${OPERATOR[k]}(${k})` }));\n});\n\nconst addSearchConditions = () => {\n if (!Array.isArray(props.config.searchConditions)) props.config.searchConditions = [];\n props.config.searchConditions.push([{ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] }]);\n};\nconst addSearchItem = (gi: number) => {\n props.config.searchConditions[gi].push({ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] });\n};\n/* operator/class_name 变化时保证 value 形状(between 需两位) */\nconst ensureValueShape = (item: any) => {\n if (item.operator === 'between' || item.operator === 'notbetween') {\n const v0 = Array.isArray(item.value) ? item.value[0] : item.value;\n const v1 = Array.isArray(item.value) ? item.value[1] : undefined;\n item.value = [v0 ?? null, v1 ?? null];\n } else if (!Array.isArray(item.value)) {\n item.value = [item.value];\n } else if (item.value.length > 1) {\n item.value = [item.value[0]];\n }\n};\n\nconst addSimpleSearchItem = () => {\n if (!Array.isArray(props.config.simpleSearchConditions)) props.config.simpleSearchConditions = [];\n props.config.simpleSearchConditions.push({ searchType: 3, field_name: '', value: '' });\n};\nconst handleTextareaInput = (e: any, item: any) => {\n try {\n item.value = JSON.parse(e.target.value);\n } catch {\n item.value = e.target.value;\n }\n};\nconst getDataType = (value: any) => (typeof value === 'object' ? 'json' : typeof value);\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.cond-group {\n border: 1px solid rgba(0, 0, 0, 0.08);\n border-radius: 6px;\n padding: 8px;\n margin-bottom: 8px;\n}\n.group-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n .group-title {\n font-weight: 500;\n }\n}\n.cond-item {\n padding: 4px 0;\n border-top: 1px dashed rgba(0, 0, 0, 0.06);\n}\n.cond-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 4px 0;\n flex-wrap: wrap;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n.range-sep {\n text-align: center;\n}\n</style>\n","<template>\n <a-col :span=\"12\">\n <!-- 两边 label 文案一致(原来靠 `(mode)` / `(multiple)` 区分,v29.38 去英文后合并为静态 label) -->\n <a-form-item label=\"单选多选\">\n <a-radio-group\n v-if=\"field === 'mode'\"\n v-model:value=\"config.mode\"\n >\n <a-radio value=\"default\">单选</a-radio>\n <a-radio value=\"multiple\">多选</a-radio>\n <a-radio value=\"tags\">标签</a-radio>\n </a-radio-group>\n <a-radio-group\n v-else\n v-model:value=\"config.multiple\"\n >\n <a-radio :value=\"false\">单选</a-radio>\n <a-radio :value=\"true\">多选</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 单选多选配置(共享):field='mode' 走 default/multiple/tags 三态,\n field='multiple' 走布尔二态(树/级联类组件)。 */\ndefineProps<{\n config: any;\n field: 'mode' | 'multiple';\n}>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <!-- 两边 label 文案一致(原来靠 `(mode)` / `(multiple)` 区分,v29.38 去英文后合并为静态 label) -->\n <a-form-item label=\"单选多选\">\n <a-radio-group\n v-if=\"field === 'mode'\"\n v-model:value=\"config.mode\"\n >\n <a-radio value=\"default\">单选</a-radio>\n <a-radio value=\"multiple\">多选</a-radio>\n <a-radio value=\"tags\">标签</a-radio>\n </a-radio-group>\n <a-radio-group\n v-else\n v-model:value=\"config.multiple\"\n >\n <a-radio :value=\"false\">单选</a-radio>\n <a-radio :value=\"true\">多选</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 单选多选配置(共享):field='mode' 走 default/multiple/tags 三态,\n field='multiple' 走布尔二态(树/级联类组件)。 */\ndefineProps<{\n config: any;\n field: 'mode' | 'multiple';\n}>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"可否搜索\">\n <a-radio-group v-model:value=\"config.show_search\">\n <a-radio :value=\"false\">否</a-radio>\n <a-radio :value=\"true\">是</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 可否搜索配置(共享)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"可否搜索\">\n <a-radio-group v-model:value=\"config.show_search\">\n <a-radio :value=\"false\">否</a-radio>\n <a-radio :value=\"true\">是</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 可否搜索配置(共享)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 数据源页:远程五类(接口/字段映射/查询参数/全局切换/初始参数)\n + ★ v29.29 本地静态选项类(select/radio/checkbox/groupselect)的「选项列表」。\n ★ v29.32:原「保存/加载/复制/粘贴 配置」四按钮(后端 /sys/create_data_source_config +\n /sys/query_data_source_config_list,及 localStorage 剪贴板)已按用户要求整体删除。 -->\n <a-row :gutter=\"16\">\n <!-- ═══ 远程五类:接口 + 字段映射 + 查询参数 + 全局切换 + 初始参数 ═══ -->\n <template v-if=\"isRemote\">\n <a-col :span=\"24\">\n <a-form-item label=\"数据源\">\n <PostUrlField\n v-model:value=\"config.postUrl\"\n :item-list=\"postUrlOptions\"\n :loading=\"postUrlLoading\"\n placeholder=\"请输入数据源\"\n @select=\"onPostUrlSelect\"\n @search=\"handlePostUrlSearch\"\n />\n </a-form-item>\n </a-col>\n\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n\n <a-col :span=\"12\">\n <a-form-item label=\"显示类型\">\n <a-radio-group v-model:value=\"config.labelKeyType\">\n <a-radio :value=\"1\">单字段</a-radio>\n <a-radio :value=\"2\">多字段</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n <OptionKeysConfig\n remote\n :config=\"config\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n />\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"12\"\n >\n <a-form-item label=\"间隔符号\">\n <a-input\n v-model:value=\"config.labelSpace\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"24\"\n >\n <a-form-item label=\"显示列表\">\n <div\n v-for=\"(_, i) in config.labelKeyList || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.labelKeyList[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入显示字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.labelKeyList.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('labelKeyList')\"\n >\n + 添加显示字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"扩展查询字段\">\n <div\n v-for=\"(_, i) in config.extendedFields || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.extendedFields[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入扩展查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.extendedFields.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('extendedFields')\"\n >\n + 添加扩展查询字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"默认值配置\">\n <a-select\n v-model:value=\"config.defaultValueType\"\n style=\"width: 200px\"\n placeholder=\"未指定\"\n :options=\"[\n { value: '', label: '未指定' },\n { value: 'first', label: '第一条数据' },\n { value: 'global', label: '全局值' },\n ]\"\n />\n <template v-if=\"config.defaultValueType === 'global'\">\n <a-input\n v-model:value=\"config.globalDefaultKey\"\n placeholder=\"键\"\n style=\"width: 200px; margin-left: 8px\"\n />\n <a-input\n v-model:value=\"config.globalDefaultField\"\n placeholder=\"字段名\"\n style=\"width: 260px; margin-left: 8px\"\n />\n </template>\n </a-form-item>\n </a-col>\n\n <QueryParamsConfig\n :config=\"config\"\n :field-options=\"paramsOptions\"\n :field-options-loading=\"paramsOptionsLoading\"\n />\n\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>全局切换配置</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRequiredCondition\"\n >\n + 添加全局切换\n </a-button>\n </div>\n <div\n v-for=\"(item, ri) in config.required_condition || []\"\n :key=\"ri\"\n class=\"row-line\"\n >\n <a-input\n v-model:value=\"item.key\"\n placeholder=\"键\"\n style=\"width: 260px\"\n />\n <a-input\n v-model:value=\"item.field_name\"\n placeholder=\"字段名\"\n style=\"width: 360px\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.required_condition.splice(ri, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"初始参数:JSON\">\n <a-textarea\n v-model:value=\"initModelText\"\n :rows=\"3\"\n placeholder='{\"is_active\": true}'\n />\n </a-form-item>\n </a-col>\n </template>\n\n <!-- ═══ 本地静态选项类(select/radio/checkbox/groupselect):选项列表\n (用独立 v-if 而非 v-else-if:与上面的远程分支靠类型互斥,不依赖相邻分支匹配) ═══ -->\n <OptionListConfig\n v-if=\"isLocalOption\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { debounce } from 'lodash';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { LOCAL_OPTION_TYPES, MODE_TYPES, MULTIPLE_TYPES, REMOTE_TYPES } from './constants';\nimport OptionKeysConfig from './OptionKeysConfig.vue';\nimport OptionListConfig from './OptionListConfig.vue';\nimport PostUrlField from './PostUrlField.vue';\nimport QueryParamsConfig from './QueryParamsConfig.vue';\nimport SelectionModeConfig from './SelectionModeConfig.vue';\nimport ShowSearchConfig from './ShowSearchConfig.vue';\n\n/* 数据源配置页:postUrl 搜索(/sys/base_query_url_list)、选源自动回填、字段候选\n (/{app}/obtain_{model_name}_column_data)、labelKeyList/extendedFields、defaultValueType、\n 查询参数(QueryParamsConfig)、required_condition、init_model。\n ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)也进本页——原版 FieldSetting 的\n 「选项列表」就在「数据源」页;本页对它们只渲染选项列表,远程字段/接口那半块整体不渲染。 */\nconst props = defineProps<{\n config: any;\n /** 列配置行(透传 OptionListConfig:class_name 定值类型默认值、content_map 供导入) */\n record?: any;\n}>();\nconst initModelText = defineModel<string>('initModelText', { default: '' });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(props.config?.type));\nconst isLocalOption = computed(() => LOCAL_OPTION_TYPES.includes(props.config?.type));\n\nconst userStore = useUserStore();\nconst current_org = (userStore as any).currentOrg;\n\n/* ───────── 数据源搜索(/sys/base_query_url_list) ───────── */\nconst postUrlOptions = ref<any[]>([]);\nconst postUrlLoading = ref(false);\nlet postUrlTotal = 0;\n/* ★ v29.22.2:响应序号守卫——debounce 只合并 500ms 内的输入,跨窗口的响应仍可能乱序\n (先发的请求后返回会把 itemList 反向覆盖成旧关键字子集 → 下拉空/越搜越少),只认最新。 */\nlet postUrlSeq = 0;\n\nconst getPostUrlData = debounce((obj: any) => {\n postUrlLoading.value = true;\n const seq = ++postUrlSeq;\n getApiData(obj, '/sys/base_query_url_list')\n .then((res: any) => {\n if (seq !== postUrlSeq) return; // 过期响应丢弃\n postUrlOptions.value = [];\n if (res && res.data && res.data.length > 0) {\n res.data.forEach((item: any) => {\n if (props.config.postUrl === item.post_url) {\n getParamsOptions(item.app, item.model_name);\n }\n postUrlOptions.value.push({ value: item.post_url, label: item.name, data: item });\n });\n }\n /* ★ v29.22.1:postUrlTotal 恒存「全量总数」——带 query_param 的远程过滤结果的 total\n 是过滤后总数,不能覆盖(否则降到 ≤100 后后续输入不再触发远程搜索,列表永久收缩)。 */\n if (!obj.query_param) {\n postUrlTotal = res.total ?? postUrlOptions.value.length;\n }\n postUrlLoading.value = false;\n })\n .catch(() => {\n if (seq === postUrlSeq) {\n postUrlLoading.value = false;\n }\n });\n}, 500);\n\nonMounted(() => {\n /* ★ v29.29:本地选项类不拉数据源列表(/sys/base_query_url_list 只服务远程五类,白跑一次请求) */\n if (isRemote.value) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type),\n });\n }\n});\n\n/* 远程类型互切时本组件不重挂载,按新类型(是否树形)重拉数据源列表(对齐原版 onTypeChange 行为) */\nwatch(\n () => props.config.type,\n t => {\n if (t && REMOTE_TYPES.includes(t)) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(t),\n });\n }\n },\n);\n\nconst handlePostUrlSearch = (str: string) => {\n const is_tree = ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type);\n /* ★ v29.22.1:清空输入 → 重拉全量列表(此前清空不恢复,itemList 停在旧关键字的远程子集) */\n if (!str || !str.trim()) {\n getPostUrlData({ org_id: current_org?.id, is_tree });\n return;\n }\n if (postUrlTotal > 100) {\n getPostUrlData({ org_id: current_org?.id, is_tree, query_param: str });\n }\n};\n\n/* 选中数据源 → 自动回填(port 自 FieldSetting.selectPostUrl;item = {value, label, data}) */\nconst onPostUrlSelect = (item: any) => {\n const data = item?.data;\n if (!data) return;\n const parameters = data.parameters;\n if (parameters) {\n props.config.labelKeyType = 1;\n props.config.labelKey = parameters.label_key;\n props.config.valueKey = parameters.value_key;\n props.config.queryType = 'simple';\n props.config.simpleSearchConditions = [];\n (parameters.search_fields || []).forEach((key: any) => {\n props.config.simpleSearchConditions.push({\n searchType: 1,\n field_name: key,\n select_name: key,\n });\n });\n }\n getParamsOptions(data.app, data.model_name);\n};\n\n/* 字段候选(/{app}/obtain_{model_name}_column_data,扁平化带前缀) */\nconst paramsOptions = ref<any[]>([]);\nconst paramsOptionsLoading = ref(false);\nconst processParamsOptions = (res: any[], out: any[], prefix = '') => {\n prefix = prefix.replace('ID', '');\n if (prefix) prefix = `${prefix}-`;\n for (const item of res) {\n out.push({ value: item.name, label: `${prefix}${item.verbose_name}` });\n if (item.children) processParamsOptions(item.children, out, `${prefix}${item.verbose_name}`);\n }\n};\nconst getParamsOptions = async (app: string, model_name: string) => {\n if (app && model_name) {\n paramsOptionsLoading.value = true;\n try {\n const res = await getApiData({}, `/${app}/obtain_${model_name}_column_data`, false, {\n skip_error: true,\n });\n if (!res) {\n paramsOptions.value = [];\n } else {\n const out: any[] = [];\n processParamsOptions(res, out);\n paramsOptions.value = out;\n }\n } finally {\n paramsOptionsLoading.value = false;\n }\n }\n};\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* ───────── 列表键增删(labelKeyList / extendedFields) ───────── */\nconst addListKey = (key: 'labelKeyList' | 'extendedFields') => {\n if (!Array.isArray(props.config[key])) props.config[key] = [];\n props.config[key].push('');\n};\n\n/* ───────── 全局切换配置 ───────── */\nconst addRequiredCondition = () => {\n if (!Array.isArray(props.config.required_condition)) props.config.required_condition = [];\n props.config.required_condition.push({ key: '', field_name: '' });\n};\n\n/* ★ v29.32:原「数据源配置 保存 / 加载 / 复制 / 粘贴」整块(CLIP_KEY 剪贴板、saveModal、loadModal、\n pickDsConfig 及 6 个 handle* 函数,依赖 /sys/create_data_source_config 与\n /sys/query_data_source_config_list)已按用户要求删除。 */\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n</style>\n","<template>\n <!-- 数据源页:远程五类(接口/字段映射/查询参数/全局切换/初始参数)\n + ★ v29.29 本地静态选项类(select/radio/checkbox/groupselect)的「选项列表」。\n ★ v29.32:原「保存/加载/复制/粘贴 配置」四按钮(后端 /sys/create_data_source_config +\n /sys/query_data_source_config_list,及 localStorage 剪贴板)已按用户要求整体删除。 -->\n <a-row :gutter=\"16\">\n <!-- ═══ 远程五类:接口 + 字段映射 + 查询参数 + 全局切换 + 初始参数 ═══ -->\n <template v-if=\"isRemote\">\n <a-col :span=\"24\">\n <a-form-item label=\"数据源\">\n <PostUrlField\n v-model:value=\"config.postUrl\"\n :item-list=\"postUrlOptions\"\n :loading=\"postUrlLoading\"\n placeholder=\"请输入数据源\"\n @select=\"onPostUrlSelect\"\n @search=\"handlePostUrlSearch\"\n />\n </a-form-item>\n </a-col>\n\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n\n <a-col :span=\"12\">\n <a-form-item label=\"显示类型\">\n <a-radio-group v-model:value=\"config.labelKeyType\">\n <a-radio :value=\"1\">单字段</a-radio>\n <a-radio :value=\"2\">多字段</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n <OptionKeysConfig\n remote\n :config=\"config\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n />\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"12\"\n >\n <a-form-item label=\"间隔符号\">\n <a-input\n v-model:value=\"config.labelSpace\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"24\"\n >\n <a-form-item label=\"显示列表\">\n <div\n v-for=\"(_, i) in config.labelKeyList || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.labelKeyList[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入显示字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.labelKeyList.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('labelKeyList')\"\n >\n + 添加显示字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"扩展查询字段\">\n <div\n v-for=\"(_, i) in config.extendedFields || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.extendedFields[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入扩展查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.extendedFields.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('extendedFields')\"\n >\n + 添加扩展查询字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"默认值配置\">\n <a-select\n v-model:value=\"config.defaultValueType\"\n style=\"width: 200px\"\n placeholder=\"未指定\"\n :options=\"[\n { value: '', label: '未指定' },\n { value: 'first', label: '第一条数据' },\n { value: 'global', label: '全局值' },\n ]\"\n />\n <template v-if=\"config.defaultValueType === 'global'\">\n <a-input\n v-model:value=\"config.globalDefaultKey\"\n placeholder=\"键\"\n style=\"width: 200px; margin-left: 8px\"\n />\n <a-input\n v-model:value=\"config.globalDefaultField\"\n placeholder=\"字段名\"\n style=\"width: 260px; margin-left: 8px\"\n />\n </template>\n </a-form-item>\n </a-col>\n\n <QueryParamsConfig\n :config=\"config\"\n :field-options=\"paramsOptions\"\n :field-options-loading=\"paramsOptionsLoading\"\n />\n\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>全局切换配置</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRequiredCondition\"\n >\n + 添加全局切换\n </a-button>\n </div>\n <div\n v-for=\"(item, ri) in config.required_condition || []\"\n :key=\"ri\"\n class=\"row-line\"\n >\n <a-input\n v-model:value=\"item.key\"\n placeholder=\"键\"\n style=\"width: 260px\"\n />\n <a-input\n v-model:value=\"item.field_name\"\n placeholder=\"字段名\"\n style=\"width: 360px\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.required_condition.splice(ri, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"初始参数:JSON\">\n <a-textarea\n v-model:value=\"initModelText\"\n :rows=\"3\"\n placeholder='{\"is_active\": true}'\n />\n </a-form-item>\n </a-col>\n </template>\n\n <!-- ═══ 本地静态选项类(select/radio/checkbox/groupselect):选项列表\n (用独立 v-if 而非 v-else-if:与上面的远程分支靠类型互斥,不依赖相邻分支匹配) ═══ -->\n <OptionListConfig\n v-if=\"isLocalOption\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { debounce } from 'lodash';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { LOCAL_OPTION_TYPES, MODE_TYPES, MULTIPLE_TYPES, REMOTE_TYPES } from './constants';\nimport OptionKeysConfig from './OptionKeysConfig.vue';\nimport OptionListConfig from './OptionListConfig.vue';\nimport PostUrlField from './PostUrlField.vue';\nimport QueryParamsConfig from './QueryParamsConfig.vue';\nimport SelectionModeConfig from './SelectionModeConfig.vue';\nimport ShowSearchConfig from './ShowSearchConfig.vue';\n\n/* 数据源配置页:postUrl 搜索(/sys/base_query_url_list)、选源自动回填、字段候选\n (/{app}/obtain_{model_name}_column_data)、labelKeyList/extendedFields、defaultValueType、\n 查询参数(QueryParamsConfig)、required_condition、init_model。\n ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)也进本页——原版 FieldSetting 的\n 「选项列表」就在「数据源」页;本页对它们只渲染选项列表,远程字段/接口那半块整体不渲染。 */\nconst props = defineProps<{\n config: any;\n /** 列配置行(透传 OptionListConfig:class_name 定值类型默认值、content_map 供导入) */\n record?: any;\n}>();\nconst initModelText = defineModel<string>('initModelText', { default: '' });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(props.config?.type));\nconst isLocalOption = computed(() => LOCAL_OPTION_TYPES.includes(props.config?.type));\n\nconst userStore = useUserStore();\nconst current_org = (userStore as any).currentOrg;\n\n/* ───────── 数据源搜索(/sys/base_query_url_list) ───────── */\nconst postUrlOptions = ref<any[]>([]);\nconst postUrlLoading = ref(false);\nlet postUrlTotal = 0;\n/* ★ v29.22.2:响应序号守卫——debounce 只合并 500ms 内的输入,跨窗口的响应仍可能乱序\n (先发的请求后返回会把 itemList 反向覆盖成旧关键字子集 → 下拉空/越搜越少),只认最新。 */\nlet postUrlSeq = 0;\n\nconst getPostUrlData = debounce((obj: any) => {\n postUrlLoading.value = true;\n const seq = ++postUrlSeq;\n getApiData(obj, '/sys/base_query_url_list')\n .then((res: any) => {\n if (seq !== postUrlSeq) return; // 过期响应丢弃\n postUrlOptions.value = [];\n if (res && res.data && res.data.length > 0) {\n res.data.forEach((item: any) => {\n if (props.config.postUrl === item.post_url) {\n getParamsOptions(item.app, item.model_name);\n }\n postUrlOptions.value.push({ value: item.post_url, label: item.name, data: item });\n });\n }\n /* ★ v29.22.1:postUrlTotal 恒存「全量总数」——带 query_param 的远程过滤结果的 total\n 是过滤后总数,不能覆盖(否则降到 ≤100 后后续输入不再触发远程搜索,列表永久收缩)。 */\n if (!obj.query_param) {\n postUrlTotal = res.total ?? postUrlOptions.value.length;\n }\n postUrlLoading.value = false;\n })\n .catch(() => {\n if (seq === postUrlSeq) {\n postUrlLoading.value = false;\n }\n });\n}, 500);\n\nonMounted(() => {\n /* ★ v29.29:本地选项类不拉数据源列表(/sys/base_query_url_list 只服务远程五类,白跑一次请求) */\n if (isRemote.value) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type),\n });\n }\n});\n\n/* 远程类型互切时本组件不重挂载,按新类型(是否树形)重拉数据源列表(对齐原版 onTypeChange 行为) */\nwatch(\n () => props.config.type,\n t => {\n if (t && REMOTE_TYPES.includes(t)) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(t),\n });\n }\n },\n);\n\nconst handlePostUrlSearch = (str: string) => {\n const is_tree = ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type);\n /* ★ v29.22.1:清空输入 → 重拉全量列表(此前清空不恢复,itemList 停在旧关键字的远程子集) */\n if (!str || !str.trim()) {\n getPostUrlData({ org_id: current_org?.id, is_tree });\n return;\n }\n if (postUrlTotal > 100) {\n getPostUrlData({ org_id: current_org?.id, is_tree, query_param: str });\n }\n};\n\n/* 选中数据源 → 自动回填(port 自 FieldSetting.selectPostUrl;item = {value, label, data}) */\nconst onPostUrlSelect = (item: any) => {\n const data = item?.data;\n if (!data) return;\n const parameters = data.parameters;\n if (parameters) {\n props.config.labelKeyType = 1;\n props.config.labelKey = parameters.label_key;\n props.config.valueKey = parameters.value_key;\n props.config.queryType = 'simple';\n props.config.simpleSearchConditions = [];\n (parameters.search_fields || []).forEach((key: any) => {\n props.config.simpleSearchConditions.push({\n searchType: 1,\n field_name: key,\n select_name: key,\n });\n });\n }\n getParamsOptions(data.app, data.model_name);\n};\n\n/* 字段候选(/{app}/obtain_{model_name}_column_data,扁平化带前缀) */\nconst paramsOptions = ref<any[]>([]);\nconst paramsOptionsLoading = ref(false);\nconst processParamsOptions = (res: any[], out: any[], prefix = '') => {\n prefix = prefix.replace('ID', '');\n if (prefix) prefix = `${prefix}-`;\n for (const item of res) {\n out.push({ value: item.name, label: `${prefix}${item.verbose_name}` });\n if (item.children) processParamsOptions(item.children, out, `${prefix}${item.verbose_name}`);\n }\n};\nconst getParamsOptions = async (app: string, model_name: string) => {\n if (app && model_name) {\n paramsOptionsLoading.value = true;\n try {\n const res = await getApiData({}, `/${app}/obtain_${model_name}_column_data`, false, {\n skip_error: true,\n });\n if (!res) {\n paramsOptions.value = [];\n } else {\n const out: any[] = [];\n processParamsOptions(res, out);\n paramsOptions.value = out;\n }\n } finally {\n paramsOptionsLoading.value = false;\n }\n }\n};\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* ───────── 列表键增删(labelKeyList / extendedFields) ───────── */\nconst addListKey = (key: 'labelKeyList' | 'extendedFields') => {\n if (!Array.isArray(props.config[key])) props.config[key] = [];\n props.config[key].push('');\n};\n\n/* ───────── 全局切换配置 ───────── */\nconst addRequiredCondition = () => {\n if (!Array.isArray(props.config.required_condition)) props.config.required_condition = [];\n props.config.required_condition.push({ key: '', field_name: '' });\n};\n\n/* ★ v29.32:原「数据源配置 保存 / 加载 / 复制 / 粘贴」整块(CLIP_KEY 剪贴板、saveModal、loadModal、\n pickDsConfig 及 6 个 handle* 函数,依赖 /sys/create_data_source_config 与\n /sys/query_data_source_config_list)已按用户要求删除。 */\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n</style>\n","<template>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"选择粒度\">\n <a-select\n v-model:value=\"config.picker\"\n allow-clear\n placeholder=\"默认日\"\n :options=\"[\n { value: 'date', label: '日' },\n { value: 'week', label: '周' },\n { value: 'month', label: '月' },\n { value: 'quarter', label: '季' },\n { value: 'year', label: '年' },\n ]\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"显示时间\">\n <a-switch\n v-model:checked=\"config.showTime\"\n checked-children=\"有\"\n un-checked-children=\"无\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"时间格式\">\n <a-input\n v-model:value=\"config.format\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"传值格式\">\n <a-input\n v-model:value=\"config.valueFormat\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 日期时间类配置(共享):picker/showTime(仅 datepicker)+ format/valueFormat。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"选择粒度\">\n <a-select\n v-model:value=\"config.picker\"\n allow-clear\n placeholder=\"默认日\"\n :options=\"[\n { value: 'date', label: '日' },\n { value: 'week', label: '周' },\n { value: 'month', label: '月' },\n { value: 'quarter', label: '季' },\n { value: 'year', label: '年' },\n ]\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"显示时间\">\n <a-switch\n v-model:checked=\"config.showTime\"\n checked-children=\"有\"\n un-checked-children=\"无\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"时间格式\">\n <a-input\n v-model:value=\"config.format\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"传值格式\">\n <a-input\n v-model:value=\"config.valueFormat\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 日期时间类配置(共享):picker/showTime(仅 datepicker)+ format/valueFormat。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 日期时间类(datepicker/rangepicker/monthpicker/monthrangepicker/timepicker/timerangepicker):\n picker/showTime/format/valueFormat -->\n <a-row :gutter=\"16\">\n <DateFormatConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport DateFormatConfig from '../DateFormatConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 日期时间类(datepicker/rangepicker/monthpicker/monthrangepicker/timepicker/timerangepicker):\n picker/showTime/format/valueFormat -->\n <a-row :gutter=\"16\">\n <DateFormatConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport DateFormatConfig from '../DateFormatConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- select/radio/checkbox/groupselect 本地静态选项类:mode(仅 select)+ show_search;\n ★ v29.29:选项列表(数值/显示文本)在「数据源」页维护——选项统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 契约),故不再暴露 valueKey/labelKey\n (残留 labelKey:'name' 会让选项渲染成空白,详见 constants.ts 说明)。 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"flat-hint\">\n 本地静态选项(数值 / 显示文本)在「数据源」页的「选项列表」中维护;选项对象为\n <code>{ value, label }</code>。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.flat-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- select/radio/checkbox/groupselect 本地静态选项类:mode(仅 select)+ show_search;\n ★ v29.29:选项列表(数值/显示文本)在「数据源」页维护——选项统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 契约),故不再暴露 valueKey/labelKey\n (残留 labelKey:'name' 会让选项渲染成空白,详见 constants.ts 说明)。 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"flat-hint\">\n 本地静态选项(数值 / 显示文本)在「数据源」页的「选项列表」中维护;选项对象为\n <code>{ value, label }</code>。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.flat-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n margin-top: 4px;\n}\n</style>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最小值\">\n <a-input\n v-model:value=\"config.min\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"最大值\">\n <a-input\n v-model:value=\"config.max\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"数值精度\">\n <a-input\n v-model:value=\"config.precision\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"步进数值\">\n <a-input\n v-model:value=\"config.step\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值范围配置(共享):min/max/precision/step(number/money)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最小值\">\n <a-input\n v-model:value=\"config.min\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"最大值\">\n <a-input\n v-model:value=\"config.max\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"数值精度\">\n <a-input\n v-model:value=\"config.precision\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"步进数值\">\n <a-input\n v-model:value=\"config.step\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值范围配置(共享):min/max/precision/step(number/money)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最大字符\">\n <a-input-number\n v-model:value=\"config.maxlength\"\n style=\"width: 100%\"\n :min=\"1\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"前缀标签\">\n <a-input\n v-model:value=\"config.addonBefore\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"后缀标签\">\n <a-input\n v-model:value=\"config.addonAfter\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 文本类输入增强(共享):maxlength + 前后缀标签(input/number)。\n 图标型前后缀(addonBeforeIcon/addonAfterIcon)可在「JSON配置」tab 补。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最大字符\">\n <a-input-number\n v-model:value=\"config.maxlength\"\n style=\"width: 100%\"\n :min=\"1\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"前缀标签\">\n <a-input\n v-model:value=\"config.addonBefore\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"后缀标签\">\n <a-input\n v-model:value=\"config.addonAfter\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 文本类输入增强(共享):maxlength + 前后缀标签(input/number)。\n 图标型前后缀(addonBeforeIcon/addonAfterIcon)可在「JSON配置」tab 补。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- number/money 数字类:maxlength/前后缀(仅 number)+ min/max/precision/step -->\n <a-row :gutter=\"16\">\n <TextAddonConfig\n v-if=\"TEXT_WITH_ADDON_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <NumberRangeConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { TEXT_WITH_ADDON_TYPES } from '../constants';\nimport NumberRangeConfig from '../NumberRangeConfig.vue';\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- number/money 数字类:maxlength/前后缀(仅 number)+ min/max/precision/step -->\n <a-row :gutter=\"16\">\n <TextAddonConfig\n v-if=\"TEXT_WITH_ADDON_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <NumberRangeConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { TEXT_WITH_ADDON_TYPES } from '../constants';\nimport NumberRangeConfig from '../NumberRangeConfig.vue';\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 远程五类(remoteselectpro/remotecomplete/remotetext/remotetreeselectpro/remotecascaderpro):\n mode|multiple + show_search;接口/字段/查询条件等在「数据源」页配置 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"remote-hint\">\n 远程类组件的数据源接口、显示字段、查询参数(监听/固定/上下文)、全局切换等在「数据源」页配置。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES, MULTIPLE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.remote-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- 远程五类(remoteselectpro/remotecomplete/remotetext/remotetreeselectpro/remotecascaderpro):\n mode|multiple + show_search;接口/字段/查询条件等在「数据源」页配置 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"remote-hint\">\n 远程类组件的数据源接口、显示字段、查询参数(监听/固定/上下文)、全局切换等在「数据源」页配置。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES, MULTIPLE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.remote-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- input 单行文本框:maxlength + 前后缀标签 -->\n <a-row :gutter=\"16\">\n <TextAddonConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- input 单行文本框:maxlength + 前后缀标签 -->\n <a-row :gutter=\"16\">\n <TextAddonConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"24\">\n <a-form-item label=\"树形数据\">\n <a-textarea\n v-model:value=\"text\"\n :rows=\"6\"\n placeholder='[{\"value\":\"1\",\"label\":\"父级1\",\"children\":[{\"value\":\"1-1\",\"label\":\"子级1-1\"}]}]'\n />\n <div\n v-if=\"error\"\n class=\"json-error\"\n >\n {{ error }}\n </div>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 树形本地数据配置(共享,treeselect/cascader 专属):JSON 文本编辑 + 实时校验写回 config.options。\n 外壳类型切换会整体替换 config 对象(treeselect↔cascader 共用本组件不重挂载),\n 因此 watch config 引用变化时重新从 config.options 初始化(对齐原版 syncTreeDataText 行为)。 */\nimport { ref, watch } from 'vue';\n\nconst props = defineProps<{ config: any }>();\n\nconst text = ref('');\nconst error = ref('');\n\nconst initFromConfig = () => {\n error.value = '';\n const opts = props.config?.options;\n text.value = Array.isArray(opts) ? JSON.stringify(opts, null, 2) : '';\n};\n\nwatch(() => props.config, initFromConfig, { immediate: true });\n\nwatch(text, (txt) => {\n if (!txt || !txt.trim()) {\n error.value = '';\n props.config.options = [];\n return;\n }\n try {\n const parsed = JSON.parse(txt);\n if (!Array.isArray(parsed)) throw new Error('必须是数组');\n error.value = '';\n props.config.options = parsed;\n } catch (e: any) {\n error.value = 'JSON 无效:' + (e?.message || '解析失败');\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.json-error {\n color: #f5222d;\n font-size: 12px;\n}\n</style>\n","<template>\n <a-col :span=\"24\">\n <a-form-item label=\"树形数据\">\n <a-textarea\n v-model:value=\"text\"\n :rows=\"6\"\n placeholder='[{\"value\":\"1\",\"label\":\"父级1\",\"children\":[{\"value\":\"1-1\",\"label\":\"子级1-1\"}]}]'\n />\n <div\n v-if=\"error\"\n class=\"json-error\"\n >\n {{ error }}\n </div>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 树形本地数据配置(共享,treeselect/cascader 专属):JSON 文本编辑 + 实时校验写回 config.options。\n 外壳类型切换会整体替换 config 对象(treeselect↔cascader 共用本组件不重挂载),\n 因此 watch config 引用变化时重新从 config.options 初始化(对齐原版 syncTreeDataText 行为)。 */\nimport { ref, watch } from 'vue';\n\nconst props = defineProps<{ config: any }>();\n\nconst text = ref('');\nconst error = ref('');\n\nconst initFromConfig = () => {\n error.value = '';\n const opts = props.config?.options;\n text.value = Array.isArray(opts) ? JSON.stringify(opts, null, 2) : '';\n};\n\nwatch(() => props.config, initFromConfig, { immediate: true });\n\nwatch(text, (txt) => {\n if (!txt || !txt.trim()) {\n error.value = '';\n props.config.options = [];\n return;\n }\n try {\n const parsed = JSON.parse(txt);\n if (!Array.isArray(parsed)) throw new Error('必须是数组');\n error.value = '';\n props.config.options = parsed;\n } catch (e: any) {\n error.value = 'JSON 无效:' + (e?.message || '解析失败');\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.json-error {\n color: #f5222d;\n font-size: 12px;\n}\n</style>\n","<template>\n <!-- treeselect/cascader 本地树/级联类:multiple + show_search + 树形数据 JSON -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <TreeDataConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\nimport TreeDataConfig from '../TreeDataConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- treeselect/cascader 本地树/级联类:multiple + show_search + 树形数据 JSON -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <TreeDataConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\nimport TreeDataConfig from '../TreeDataConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- ★ v29.31:容器由「全屏弹窗」改为「右侧抽屉」——配置面板不遮全屏、可与父级视图配置弹窗对照;\n 内容布局与交互不变(内部滚动、底部操作区固定不随滚动),见文件底部 .csc-drawer 全局样式。 -->\n <a-drawer\n :title=\"`高级搜索组件 —— ${record?.title || record?.data_index || ''}`\"\n placement=\"right\"\n :width=\"SEARCH_DRAWER_WIDTH\"\n root-class-name=\"csc-drawer\"\n :open=\"open\"\n destroy-on-close\n @close=\"handleCancel\"\n >\n <!-- 统一布局骨架:纵向 flex(tip / tabs / footer 各占固定或弹性高度),\n 纵向滚动只发生在 tabs 的 content-holder,Tab 标签栏与底部操作区固定不随滚动。\n v29.21:基础+高级合并为「属性配置」单页;「其他属性(JSON)」升级为「JSON配置」tab(CodeMirror 编辑整体 JSON) -->\n <a-form\n class=\"csc-body\"\n :label-col=\"{ style: { width: '190px' } }\"\n >\n <div class=\"adv-tip\">\n 「属性配置」= 基础 + 高级属性(按组件类型联动);远程类组件在「数据源」页配置接口、显示字段、查询条件,\n 本地静态选项类(下拉选框/单选框/多选框/分组下拉)在「数据源」页配置「选项列表」;\n 「JSON配置」可直接编辑整体配置对象(与表单双向同步,保存前校验合法性)。\n 运算符不在此配置——后端按组件类型自动补默认集。\n </div>\n\n <a-tabs\n v-model:activeKey=\"activeTab\"\n class=\"csc-tabs\"\n type=\"card\"\n size=\"small\"\n >\n <!-- ═══════════════ 属性配置(基础 + 高级合并单页) ═══════════════ -->\n <a-tab-pane\n key=\"props\"\n tab=\"属性配置\"\n >\n <BasicConfig\n :config=\"config\"\n :record=\"record\"\n @type-change=\"onTypeChange\"\n />\n <template v-if=\"config.type\">\n <a-divider\n class=\"props-divider\"\n orientation=\"left\"\n plain\n >\n 高级属性\n </a-divider>\n <TextInputSetting\n v-if=\"config.type === 'input'\"\n :config=\"config\"\n />\n <NumberInputSetting\n v-else-if=\"NUMBER_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <FlatSelectSetting\n v-else-if=\"FLAT_SETTING_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <TreeSelectSetting\n v-else-if=\"['treeselect', 'cascader'].includes(config.type)\"\n :config=\"config\"\n />\n <DateSelectSetting\n v-else-if=\"DATE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <RemoteSelectSetting\n v-else-if=\"REMOTE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n </template>\n </a-tab-pane>\n\n <!-- ═══════════════ 数据源(远程类调接口 / 本地静态选项类配选项列表,共享 DataSourcePanel) ═══════════════ -->\n <a-tab-pane\n key=\"ds\"\n tab=\"数据源\"\n :disabled=\"!dsTabEnabled\"\n >\n <DataSourcePanel\n v-if=\"dsTabEnabled\"\n v-model:init-model-text=\"initModelText\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-tab-pane>\n\n <!-- ═══════════════ JSON 配置(CodeMirror 编辑整体配置对象) ═══════════════ -->\n <a-tab-pane\n key=\"json\"\n tab=\"JSON配置\"\n :disabled=\"!config.type\"\n >\n <div class=\"csc-json-pane\">\n <div class=\"json-toolbar\">\n <a-alert\n v-if=\"jsonError\"\n type=\"error\"\n show-icon\n :message=\"jsonError\"\n banner\n />\n <span\n v-else\n class=\"json-tip\"\n >整体配置对象,与「属性配置」表单实时双向同步;保存时以此为最终值。</span>\n <a-button\n size=\"small\"\n :disabled=\"!!jsonError\"\n @click=\"handleFormatJson\"\n >\n 格式化\n </a-button>\n </div>\n <div class=\"json-editor-wrap\">\n <CodeEditor\n ref=\"jsonEditorRef\"\n v-model:editorValue=\"jsonText\"\n mode=\"javascript\"\n :options=\"{ lineNumbers: true, indentUnit: 2, lineWrapping: false }\"\n />\n </div>\n </div>\n </a-tab-pane>\n </a-tabs>\n\n <div\n v-if=\"config.type\"\n class=\"adv-footer\"\n >\n <a-button\n size=\"small\"\n danger\n @click=\"handleClear\"\n >\n 清除配置(恢复按类型自动推导)\n </a-button>\n </div>\n </a-form>\n\n <!-- 抽屉无 cancel-text/ok-text 属性(那是 a-modal 的),操作区用 footer 插槽 -->\n <template #footer>\n <div class=\"csc-drawer-footer\">\n <a-button @click=\"handleCancel\">\n 取消\n </a-button>\n <a-button\n type=\"primary\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </div>\n </template>\n </a-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { cloneDeep } from 'lodash';\nimport { computed, nextTick, ref, watch } from 'vue';\nimport CodeEditor from 'liyu-pc-base/components/code/CodeEditor.vue';\nimport {\n DATE_TYPES,\n FLAT_SETTING_TYPES,\n LOCAL_OPTION_TYPES,\n NUMBER_TYPES,\n RANGE_PLACEHOLDER_TYPES,\n REMOTE_TYPES,\n TYPE_DEFAULTS,\n hasOptionValue,\n} from './searchComponentSetting/constants';\nimport BasicConfig from './searchComponentSetting/BasicConfig.vue';\nimport DataSourcePanel from './searchComponentSetting/DataSourcePanel.vue';\nimport DateSelectSetting from './searchComponentSetting/settings/DateSelectSetting.vue';\nimport FlatSelectSetting from './searchComponentSetting/settings/FlatSelectSetting.vue';\nimport NumberInputSetting from './searchComponentSetting/settings/NumberInputSetting.vue';\nimport RemoteSelectSetting from './searchComponentSetting/settings/RemoteSelectSetting.vue';\nimport TextInputSetting from './searchComponentSetting/settings/TextInputSetting.vue';\nimport TreeSelectSetting from './searchComponentSetting/settings/TreeSelectSetting.vue';\n\n/* ★ v29.19 完整移植 FieldSetting.vue 搜索组件配置;v29.20 拆分 + 全屏弹窗;v29.31 改为右侧抽屉:\n - 外壳只负责容器编排(tab 分发 / 类型切换合并默认值 / 确定保存与 JSON 合并清理);\n - 基础属性 = BasicConfig(共享);高级属性按组件类型分发到 settings/*(组合共享基础配置块);\n - 数据源页 = DataSourcePanel(远程五类专属,含查询参数编辑器 QueryParamsConfig)。\n 产出物 = 完整组件配置对象(JSONField advanced_search_component),\n get_page_config 编译时整体并入高级搜索组件对象(field_name 由列锁定)。 */\n\n/* 抽屉宽度:内容含 190px 的标签列 + 远程数据源(URL/字段映射/查询参数) + CodeMirror JSON 编辑器,\n 过窄会把控件挤成多行。父级「视图配置」弹窗为 1020px,这里取略窄以保证「抽屉」观感;\n 要调宽度只改这一处。 */\nconst SEARCH_DRAWER_WIDTH = 920;\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(读写其 advanced_search_component)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(config.value?.type));\n/* ★ v29.29:「数据源」tab 的启用范围 = 远程五类(接口/字段映射) + 本地静态选项类(选项列表)。\n 此前只看 isRemote,导致 select/radio/checkbox 的「选项列表」(原版 FieldSetting 就挂在数据源页)\n 既没有入口也进不去那一页 → 选框类组件配不出选项。 */\nconst dsTabEnabled = computed(\n () => isRemote.value || LOCAL_OPTION_TYPES.includes(config.value?.type),\n);\n\n/* ───────── 状态 ───────── */\nconst activeTab = ref('props');\nconst config = ref<any>({});\nconst initModelText = ref('');\n/* JSON 配置 tab:jsonDirty=编辑器文本领先于 config(未同步/语法错误);jsonError=当前文本非法 */\nconst jsonText = ref('');\nconst jsonDirty = ref(false);\nconst jsonError = ref('');\nconst jsonEditorRef = ref<any>();\n\n/* ───────── 打开时初始化 ───────── */\nwatch(\n () => props.open,\n (val) => {\n if (!val) return;\n activeTab.value = 'props';\n const saved = props.record?.advanced_search_component;\n /* config 恒为对象(v29.18 null 崩溃修复沿用);保存过的配置 + 类型默认值合并(保存值优先) */\n if (saved && typeof saved === 'object' && saved.type) {\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[saved.type] || {}), ...cloneDeep(saved) });\n config.value.type = saved.type;\n } else {\n config.value = {};\n }\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonText.value = '';\n jsonDirty.value = false;\n jsonError.value = '';\n },\n);\n\n/* ───────── JSON 配置 tab:配置实时镜像 + 编辑文本回写 ───────── */\n/* 序列化快照:config + 数据源页未保存的 init_model(initModelText 独立文本态,保存时才合并),\n 保证 JSON 页看到/保存的语义与 handleOk 一致 */\nconst buildSnapshot = () => {\n const snapshot = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n snapshot.init_model = JSON.parse(initModelText.value);\n } catch {\n /* 数据源页文本非法:不并入,由其自身保存校验拦截 */\n }\n } else {\n delete snapshot.init_model;\n }\n return snapshot;\n};\n\nwatch(activeTab, (tab, oldTab) => {\n /* 离开 JSON 页时仍有语法错误:提示未同步(是否丢弃取决于期间是否改过表单,见下方镜像守卫) */\n if (oldTab === 'json' && jsonError.value) {\n message.warning('JSON 配置存在语法错误,未同步到表单');\n }\n /* 进入时兜底刷新:覆盖「只改了 initModelText 而 config 未变」(镜像 watch 不会触发)的情况 */\n if (tab === 'json' && !jsonDirty.value) {\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n jsonError.value = '';\n }\n if (tab === 'json') {\n /* CodeMirror 可能在 pane 隐藏期间收到过 setValue,重新显示后主动 refresh,防止渲染滞后 */\n nextTick(() => jsonEditorRef.value?.handleCodeMirrorRefresh?.());\n }\n});\n\n/* ★ 配置实时镜像:任何 config 变化(表单/数据源页/类型切换)立即刷新 JSON 文本并清除脏态,\n 不再依赖「进入 tab」时机。说明:\n - config 变化只可能来自非 JSON 页(JSON 页内无法编辑表单),无条件覆盖即「表单为准」,\n 非法草稿随表单编辑作废(离开时已有 warning 提示);\n - JSON 页内合法回写也会触发本 watch,但镜像文本与编辑器现有文本一致,自然收敛不循环。 */\nwatch(\n config,\n () => {\n if (config.value?.type) {\n jsonDirty.value = false;\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n }\n },\n { deep: true },\n);\n\nwatch(jsonText, (text) => {\n if (!text.trim()) {\n jsonError.value = '';\n return;\n }\n try {\n const parsed = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.type) {\n jsonError.value = '必须是包含 type 字符串键的 JSON 对象(如 {\"type\":\"input\", ...}),未同步到表单';\n return;\n }\n /* 与打开初始化/类型切换同一语义:JSON 为准,缺键补类型默认(cloneDeep 防共享 TYPE_DEFAULTS 引用),\n 再归一 placeholder 形状 */\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[parsed.type] || {}), ...parsed });\n /* init_model 在 DataSourcePanel 有独立文本态,保持一致避免保存时被旧值覆盖 */\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonError.value = '';\n jsonDirty.value = false;\n } catch (e: any) {\n jsonError.value = `JSON 语法错误:${e?.message || e}(未同步到表单)`;\n jsonDirty.value = true;\n }\n});\n\nconst handleFormatJson = () => {\n try {\n jsonText.value = JSON.stringify(JSON.parse(jsonText.value), null, 2);\n } catch {\n /* 语法错误时按钮已禁用,防御性兜底 */\n }\n};\n\n/* ───────── placeholder 形状归一(区间类=数组双输入,其余=字符串) ─────────\n 覆盖三个 config 入口:打开初始化 / 类型切换(keep 跨类保留)/ JSON 回写(直改 type 或缺键),\n 否则 BasicConfig 区间分支绑定 config.placeholder[0] 会在 undefined/字符串上崩溃或赋值无效 */\nconst normalizePlaceholder = (cfg: any) => {\n if (!cfg?.type) return cfg;\n const isRange = RANGE_PLACEHOLDER_TYPES.includes(cfg.type);\n if (isRange) {\n if (!Array.isArray(cfg.placeholder)) {\n cfg.placeholder =\n typeof cfg.placeholder === 'string' && cfg.placeholder\n ? [cfg.placeholder, cfg.placeholder]\n : cloneDeep(TYPE_DEFAULTS[cfg.type]?.placeholder) || ['开始', '结束'];\n }\n } else if (Array.isArray(cfg.placeholder)) {\n cfg.placeholder = cfg.placeholder.filter(Boolean)[0] || '';\n } else if (typeof cfg.placeholder !== 'string') {\n cfg.placeholder = TYPE_DEFAULTS[cfg.type]?.placeholder ?? '';\n }\n return cfg;\n};\n\n/* ───────── 类型切换 ───────── */\nconst onTypeChange = (type: string) => {\n if (!type) {\n config.value = {};\n return;\n }\n const prev = config.value || {};\n // 切类型:以该类型默认属性为基础,保留通用已填项(label/placeholder/allowClear/disabled)\n const keep: any = {};\n if (prev.label) keep.label = prev.label;\n if (prev.disabled != null) keep.disabled = prev.disabled;\n if (RANGE_PLACEHOLDER_TYPES.includes(prev.type)) {\n if (Array.isArray(prev.placeholder)) keep.placeholder = prev.placeholder;\n } else if (typeof prev.placeholder === 'string') {\n keep.placeholder = prev.placeholder;\n }\n if (prev.allowClear != null) keep.allowClear = prev.allowClear;\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[type] || { type }), ...keep });\n /* 类型变了,JSON tab 下次进入需按新配置重新序列化 */\n jsonDirty.value = false;\n};\n\n/* ───────── 确定保存 ───────── */\nconst handleClear = () => {\n props.record.advanced_search_component = null;\n config.value = {};\n open.value = false;\n};\n\nconst handleOk = () => {\n if (!config.value?.type) {\n // 未选类型 = 不配置(保留原值不动)\n open.value = false;\n return;\n }\n if (jsonError.value) {\n message.warning('JSON 配置存在语法错误,请修正后再保存');\n return;\n }\n const finalConfig = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n finalConfig.init_model = JSON.parse(initModelText.value.trim());\n } catch {\n message.warning('初始参数不是合法 JSON,请修正后再保存');\n return;\n }\n } else {\n delete finalConfig.init_model;\n }\n /* ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)\n ① 选项对象必须是 lybase choices 契约 `{value, label}`,且不能带 valueKey/labelKey ——\n 运行时按 `option[labelKey||'label']` 取显示文本,残留 `labelKey:'name'` 会整列渲染成空白;\n ② 至少一个有效选项:这类组件没有别的 options 来源(远程接口那套对它们不适用),\n 空选项落库 = 一个永远选不出东西的下拉框,属静默坏配置,这里直接拦住。 */\n if (LOCAL_OPTION_TYPES.includes(finalConfig.type)) {\n delete finalConfig.valueKey;\n delete finalConfig.labelKey;\n /* 只丢「整行空白」的项;手写在 JSON 里的**嵌套分组节点**(antdv `{label, options:[...]}`,\n 逐项直传 groupselect)自身没有 value,必须原样保留,否则一次保存就被清空。 */\n const opts = (Array.isArray(finalConfig.options) ? finalConfig.options : []).filter(\n (o: any) => hasOptionValue(o) || (Array.isArray(o?.options) && o.options.length > 0),\n );\n if (opts.length === 0) {\n message.warning('下拉选框/单选框/多选框至少需要一个选项,请在「数据源」页的「选项列表」中添加');\n return;\n }\n finalConfig.options = opts;\n }\n // 清理空壳键:空数组/空串不落库\n ['labelKeyList', 'extendedFields', 'searchConditions', 'simpleSearchConditions', 'required_condition', 'options'].forEach((k) => {\n if (Array.isArray(finalConfig[k]) && finalConfig[k].length === 0) delete finalConfig[k];\n });\n if (finalConfig.defaultValueType === '') delete finalConfig.defaultValueType;\n props.record.advanced_search_component = finalConfig;\n jsonDirty.value = false;\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.csc-body {\n /* 抽屉 body 与改造前的弹窗 body 一样是「确定高度的 flex item」,flex:1 + height:100% 双保险撑满剩余高度 */\n flex: 1;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.adv-tip {\n flex: none;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 12px;\n line-height: 1.5;\n}\n.csc-tabs {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 滚动只发生在内容区:Tab 标签栏固定 */\n :deep(.ant-tabs-nav) {\n flex: none;\n margin-bottom: 12px;\n }\n :deep(.ant-tabs-content-holder) {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 抵消 a-row gutter 的 -8px 负 margin,消除多余横向滚动条 */\n padding: 0 8px;\n }\n /* 让 tabpane 拿到确定高度,JSON 编辑器才能撑满内容区 */\n :deep(.ant-tabs-content),\n :deep(.ant-tabs-tabpane) {\n height: 100%;\n }\n}\n.props-divider {\n margin: 8px 0 12px;\n}\n.csc-json-pane {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.json-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 8px;\n min-width: 0;\n}\n.json-toolbar .ant-alert {\n flex: 1;\n min-width: 0;\n}\n.json-tip {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.json-editor-wrap {\n flex: 1;\n min-height: 0;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n overflow: hidden;\n}\n/* CodeEditor 全局样式固定 300px 高,这里撑满 JSON 页剩余高度 */\n.json-editor-wrap :deep(.CodeMirror) {\n height: 100% !important;\n font-size: 13px !important;\n}\n.adv-footer {\n flex: none;\n margin-top: 8px;\n}\n/* 抽屉底部操作区(footer 插槽内容,是本文档的元素 → scoped 可命中) */\n.csc-drawer-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n</style>\n\n<!-- ★ v29.31:抽屉是 teleport 到 body 的,`root-class-name` 挂在**抽屉自己的根节点**上,\n 本组件根元素不是它的祖先 → scoped 的 :deep() 选不中,只能写全局样式(用 .csc-drawer 限定作用域)。\n ⚠️ antdv 4.2.6 的 a-drawer **没有 `wrap-class-name`**(只有 `root-class-name`/`root-style`),\n 别照搬 a-modal 的 wrap-class-name 写法 —— 会被当成普通 attr 落到别处,样式静默失效。\n ⚠️ 选择器**刻意写成三段** `.csc-drawer.ant-drawer .ant-drawer-body`(特异度 0,3,0):\n antd 的 `.ant-drawer-body` 规则经 cssinjs 生成后是 `:where(.css-hash).ant-drawer .ant-drawer-body`(0,2,0),\n 两段式 `.csc-drawer .ant-drawer-body`(同为 0,2,0)会与它**打平**,胜负交给样式注入顺序 —— 不稳。\n (`.csc-drawer` 与 `.ant-drawer` 同在抽屉根节点上:vc-drawer 把 rootClassName 并进根节点的 class 列表。) -->\n<style lang=\"less\">\n.csc-drawer.ant-drawer {\n /* 内部滚动:滚动只发生在 tabs 的 content-holder(见上方 scoped 块),\n 抽屉标题栏与底部操作区固定不随滚动 —— 与改造前的全屏弹窗行为一致 */\n .ant-drawer-body {\n display: flex;\n flex-direction: column;\n padding: 16px;\n overflow: hidden;\n }\n .ant-drawer-footer {\n flex: none;\n }\n}\n</style>\n","<template>\n <!-- ★ v29.31:容器由「全屏弹窗」改为「右侧抽屉」——配置面板不遮全屏、可与父级视图配置弹窗对照;\n 内容布局与交互不变(内部滚动、底部操作区固定不随滚动),见文件底部 .csc-drawer 全局样式。 -->\n <a-drawer\n :title=\"`高级搜索组件 —— ${record?.title || record?.data_index || ''}`\"\n placement=\"right\"\n :width=\"SEARCH_DRAWER_WIDTH\"\n root-class-name=\"csc-drawer\"\n :open=\"open\"\n destroy-on-close\n @close=\"handleCancel\"\n >\n <!-- 统一布局骨架:纵向 flex(tip / tabs / footer 各占固定或弹性高度),\n 纵向滚动只发生在 tabs 的 content-holder,Tab 标签栏与底部操作区固定不随滚动。\n v29.21:基础+高级合并为「属性配置」单页;「其他属性(JSON)」升级为「JSON配置」tab(CodeMirror 编辑整体 JSON) -->\n <a-form\n class=\"csc-body\"\n :label-col=\"{ style: { width: '190px' } }\"\n >\n <div class=\"adv-tip\">\n 「属性配置」= 基础 + 高级属性(按组件类型联动);远程类组件在「数据源」页配置接口、显示字段、查询条件,\n 本地静态选项类(下拉选框/单选框/多选框/分组下拉)在「数据源」页配置「选项列表」;\n 「JSON配置」可直接编辑整体配置对象(与表单双向同步,保存前校验合法性)。\n 运算符不在此配置——后端按组件类型自动补默认集。\n </div>\n\n <a-tabs\n v-model:activeKey=\"activeTab\"\n class=\"csc-tabs\"\n type=\"card\"\n size=\"small\"\n >\n <!-- ═══════════════ 属性配置(基础 + 高级合并单页) ═══════════════ -->\n <a-tab-pane\n key=\"props\"\n tab=\"属性配置\"\n >\n <BasicConfig\n :config=\"config\"\n :record=\"record\"\n @type-change=\"onTypeChange\"\n />\n <template v-if=\"config.type\">\n <a-divider\n class=\"props-divider\"\n orientation=\"left\"\n plain\n >\n 高级属性\n </a-divider>\n <TextInputSetting\n v-if=\"config.type === 'input'\"\n :config=\"config\"\n />\n <NumberInputSetting\n v-else-if=\"NUMBER_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <FlatSelectSetting\n v-else-if=\"FLAT_SETTING_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <TreeSelectSetting\n v-else-if=\"['treeselect', 'cascader'].includes(config.type)\"\n :config=\"config\"\n />\n <DateSelectSetting\n v-else-if=\"DATE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <RemoteSelectSetting\n v-else-if=\"REMOTE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n </template>\n </a-tab-pane>\n\n <!-- ═══════════════ 数据源(远程类调接口 / 本地静态选项类配选项列表,共享 DataSourcePanel) ═══════════════ -->\n <a-tab-pane\n key=\"ds\"\n tab=\"数据源\"\n :disabled=\"!dsTabEnabled\"\n >\n <DataSourcePanel\n v-if=\"dsTabEnabled\"\n v-model:init-model-text=\"initModelText\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-tab-pane>\n\n <!-- ═══════════════ JSON 配置(CodeMirror 编辑整体配置对象) ═══════════════ -->\n <a-tab-pane\n key=\"json\"\n tab=\"JSON配置\"\n :disabled=\"!config.type\"\n >\n <div class=\"csc-json-pane\">\n <div class=\"json-toolbar\">\n <a-alert\n v-if=\"jsonError\"\n type=\"error\"\n show-icon\n :message=\"jsonError\"\n banner\n />\n <span\n v-else\n class=\"json-tip\"\n >整体配置对象,与「属性配置」表单实时双向同步;保存时以此为最终值。</span>\n <a-button\n size=\"small\"\n :disabled=\"!!jsonError\"\n @click=\"handleFormatJson\"\n >\n 格式化\n </a-button>\n </div>\n <div class=\"json-editor-wrap\">\n <CodeEditor\n ref=\"jsonEditorRef\"\n v-model:editorValue=\"jsonText\"\n mode=\"javascript\"\n :options=\"{ lineNumbers: true, indentUnit: 2, lineWrapping: false }\"\n />\n </div>\n </div>\n </a-tab-pane>\n </a-tabs>\n\n <div\n v-if=\"config.type\"\n class=\"adv-footer\"\n >\n <a-button\n size=\"small\"\n danger\n @click=\"handleClear\"\n >\n 清除配置(恢复按类型自动推导)\n </a-button>\n </div>\n </a-form>\n\n <!-- 抽屉无 cancel-text/ok-text 属性(那是 a-modal 的),操作区用 footer 插槽 -->\n <template #footer>\n <div class=\"csc-drawer-footer\">\n <a-button @click=\"handleCancel\">\n 取消\n </a-button>\n <a-button\n type=\"primary\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </div>\n </template>\n </a-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { cloneDeep } from 'lodash';\nimport { computed, nextTick, ref, watch } from 'vue';\nimport CodeEditor from 'liyu-pc-base/components/code/CodeEditor.vue';\nimport {\n DATE_TYPES,\n FLAT_SETTING_TYPES,\n LOCAL_OPTION_TYPES,\n NUMBER_TYPES,\n RANGE_PLACEHOLDER_TYPES,\n REMOTE_TYPES,\n TYPE_DEFAULTS,\n hasOptionValue,\n} from './searchComponentSetting/constants';\nimport BasicConfig from './searchComponentSetting/BasicConfig.vue';\nimport DataSourcePanel from './searchComponentSetting/DataSourcePanel.vue';\nimport DateSelectSetting from './searchComponentSetting/settings/DateSelectSetting.vue';\nimport FlatSelectSetting from './searchComponentSetting/settings/FlatSelectSetting.vue';\nimport NumberInputSetting from './searchComponentSetting/settings/NumberInputSetting.vue';\nimport RemoteSelectSetting from './searchComponentSetting/settings/RemoteSelectSetting.vue';\nimport TextInputSetting from './searchComponentSetting/settings/TextInputSetting.vue';\nimport TreeSelectSetting from './searchComponentSetting/settings/TreeSelectSetting.vue';\n\n/* ★ v29.19 完整移植 FieldSetting.vue 搜索组件配置;v29.20 拆分 + 全屏弹窗;v29.31 改为右侧抽屉:\n - 外壳只负责容器编排(tab 分发 / 类型切换合并默认值 / 确定保存与 JSON 合并清理);\n - 基础属性 = BasicConfig(共享);高级属性按组件类型分发到 settings/*(组合共享基础配置块);\n - 数据源页 = DataSourcePanel(远程五类专属,含查询参数编辑器 QueryParamsConfig)。\n 产出物 = 完整组件配置对象(JSONField advanced_search_component),\n get_page_config 编译时整体并入高级搜索组件对象(field_name 由列锁定)。 */\n\n/* 抽屉宽度:内容含 190px 的标签列 + 远程数据源(URL/字段映射/查询参数) + CodeMirror JSON 编辑器,\n 过窄会把控件挤成多行。父级「视图配置」弹窗为 1020px,这里取略窄以保证「抽屉」观感;\n 要调宽度只改这一处。 */\nconst SEARCH_DRAWER_WIDTH = 920;\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(读写其 advanced_search_component)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(config.value?.type));\n/* ★ v29.29:「数据源」tab 的启用范围 = 远程五类(接口/字段映射) + 本地静态选项类(选项列表)。\n 此前只看 isRemote,导致 select/radio/checkbox 的「选项列表」(原版 FieldSetting 就挂在数据源页)\n 既没有入口也进不去那一页 → 选框类组件配不出选项。 */\nconst dsTabEnabled = computed(\n () => isRemote.value || LOCAL_OPTION_TYPES.includes(config.value?.type),\n);\n\n/* ───────── 状态 ───────── */\nconst activeTab = ref('props');\nconst config = ref<any>({});\nconst initModelText = ref('');\n/* JSON 配置 tab:jsonDirty=编辑器文本领先于 config(未同步/语法错误);jsonError=当前文本非法 */\nconst jsonText = ref('');\nconst jsonDirty = ref(false);\nconst jsonError = ref('');\nconst jsonEditorRef = ref<any>();\n\n/* ───────── 打开时初始化 ───────── */\nwatch(\n () => props.open,\n (val) => {\n if (!val) return;\n activeTab.value = 'props';\n const saved = props.record?.advanced_search_component;\n /* config 恒为对象(v29.18 null 崩溃修复沿用);保存过的配置 + 类型默认值合并(保存值优先) */\n if (saved && typeof saved === 'object' && saved.type) {\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[saved.type] || {}), ...cloneDeep(saved) });\n config.value.type = saved.type;\n } else {\n config.value = {};\n }\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonText.value = '';\n jsonDirty.value = false;\n jsonError.value = '';\n },\n);\n\n/* ───────── JSON 配置 tab:配置实时镜像 + 编辑文本回写 ───────── */\n/* 序列化快照:config + 数据源页未保存的 init_model(initModelText 独立文本态,保存时才合并),\n 保证 JSON 页看到/保存的语义与 handleOk 一致 */\nconst buildSnapshot = () => {\n const snapshot = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n snapshot.init_model = JSON.parse(initModelText.value);\n } catch {\n /* 数据源页文本非法:不并入,由其自身保存校验拦截 */\n }\n } else {\n delete snapshot.init_model;\n }\n return snapshot;\n};\n\nwatch(activeTab, (tab, oldTab) => {\n /* 离开 JSON 页时仍有语法错误:提示未同步(是否丢弃取决于期间是否改过表单,见下方镜像守卫) */\n if (oldTab === 'json' && jsonError.value) {\n message.warning('JSON 配置存在语法错误,未同步到表单');\n }\n /* 进入时兜底刷新:覆盖「只改了 initModelText 而 config 未变」(镜像 watch 不会触发)的情况 */\n if (tab === 'json' && !jsonDirty.value) {\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n jsonError.value = '';\n }\n if (tab === 'json') {\n /* CodeMirror 可能在 pane 隐藏期间收到过 setValue,重新显示后主动 refresh,防止渲染滞后 */\n nextTick(() => jsonEditorRef.value?.handleCodeMirrorRefresh?.());\n }\n});\n\n/* ★ 配置实时镜像:任何 config 变化(表单/数据源页/类型切换)立即刷新 JSON 文本并清除脏态,\n 不再依赖「进入 tab」时机。说明:\n - config 变化只可能来自非 JSON 页(JSON 页内无法编辑表单),无条件覆盖即「表单为准」,\n 非法草稿随表单编辑作废(离开时已有 warning 提示);\n - JSON 页内合法回写也会触发本 watch,但镜像文本与编辑器现有文本一致,自然收敛不循环。 */\nwatch(\n config,\n () => {\n if (config.value?.type) {\n jsonDirty.value = false;\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n }\n },\n { deep: true },\n);\n\nwatch(jsonText, (text) => {\n if (!text.trim()) {\n jsonError.value = '';\n return;\n }\n try {\n const parsed = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.type) {\n jsonError.value = '必须是包含 type 字符串键的 JSON 对象(如 {\"type\":\"input\", ...}),未同步到表单';\n return;\n }\n /* 与打开初始化/类型切换同一语义:JSON 为准,缺键补类型默认(cloneDeep 防共享 TYPE_DEFAULTS 引用),\n 再归一 placeholder 形状 */\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[parsed.type] || {}), ...parsed });\n /* init_model 在 DataSourcePanel 有独立文本态,保持一致避免保存时被旧值覆盖 */\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonError.value = '';\n jsonDirty.value = false;\n } catch (e: any) {\n jsonError.value = `JSON 语法错误:${e?.message || e}(未同步到表单)`;\n jsonDirty.value = true;\n }\n});\n\nconst handleFormatJson = () => {\n try {\n jsonText.value = JSON.stringify(JSON.parse(jsonText.value), null, 2);\n } catch {\n /* 语法错误时按钮已禁用,防御性兜底 */\n }\n};\n\n/* ───────── placeholder 形状归一(区间类=数组双输入,其余=字符串) ─────────\n 覆盖三个 config 入口:打开初始化 / 类型切换(keep 跨类保留)/ JSON 回写(直改 type 或缺键),\n 否则 BasicConfig 区间分支绑定 config.placeholder[0] 会在 undefined/字符串上崩溃或赋值无效 */\nconst normalizePlaceholder = (cfg: any) => {\n if (!cfg?.type) return cfg;\n const isRange = RANGE_PLACEHOLDER_TYPES.includes(cfg.type);\n if (isRange) {\n if (!Array.isArray(cfg.placeholder)) {\n cfg.placeholder =\n typeof cfg.placeholder === 'string' && cfg.placeholder\n ? [cfg.placeholder, cfg.placeholder]\n : cloneDeep(TYPE_DEFAULTS[cfg.type]?.placeholder) || ['开始', '结束'];\n }\n } else if (Array.isArray(cfg.placeholder)) {\n cfg.placeholder = cfg.placeholder.filter(Boolean)[0] || '';\n } else if (typeof cfg.placeholder !== 'string') {\n cfg.placeholder = TYPE_DEFAULTS[cfg.type]?.placeholder ?? '';\n }\n return cfg;\n};\n\n/* ───────── 类型切换 ───────── */\nconst onTypeChange = (type: string) => {\n if (!type) {\n config.value = {};\n return;\n }\n const prev = config.value || {};\n // 切类型:以该类型默认属性为基础,保留通用已填项(label/placeholder/allowClear/disabled)\n const keep: any = {};\n if (prev.label) keep.label = prev.label;\n if (prev.disabled != null) keep.disabled = prev.disabled;\n if (RANGE_PLACEHOLDER_TYPES.includes(prev.type)) {\n if (Array.isArray(prev.placeholder)) keep.placeholder = prev.placeholder;\n } else if (typeof prev.placeholder === 'string') {\n keep.placeholder = prev.placeholder;\n }\n if (prev.allowClear != null) keep.allowClear = prev.allowClear;\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[type] || { type }), ...keep });\n /* 类型变了,JSON tab 下次进入需按新配置重新序列化 */\n jsonDirty.value = false;\n};\n\n/* ───────── 确定保存 ───────── */\nconst handleClear = () => {\n props.record.advanced_search_component = null;\n config.value = {};\n open.value = false;\n};\n\nconst handleOk = () => {\n if (!config.value?.type) {\n // 未选类型 = 不配置(保留原值不动)\n open.value = false;\n return;\n }\n if (jsonError.value) {\n message.warning('JSON 配置存在语法错误,请修正后再保存');\n return;\n }\n const finalConfig = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n finalConfig.init_model = JSON.parse(initModelText.value.trim());\n } catch {\n message.warning('初始参数不是合法 JSON,请修正后再保存');\n return;\n }\n } else {\n delete finalConfig.init_model;\n }\n /* ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)\n ① 选项对象必须是 lybase choices 契约 `{value, label}`,且不能带 valueKey/labelKey ——\n 运行时按 `option[labelKey||'label']` 取显示文本,残留 `labelKey:'name'` 会整列渲染成空白;\n ② 至少一个有效选项:这类组件没有别的 options 来源(远程接口那套对它们不适用),\n 空选项落库 = 一个永远选不出东西的下拉框,属静默坏配置,这里直接拦住。 */\n if (LOCAL_OPTION_TYPES.includes(finalConfig.type)) {\n delete finalConfig.valueKey;\n delete finalConfig.labelKey;\n /* 只丢「整行空白」的项;手写在 JSON 里的**嵌套分组节点**(antdv `{label, options:[...]}`,\n 逐项直传 groupselect)自身没有 value,必须原样保留,否则一次保存就被清空。 */\n const opts = (Array.isArray(finalConfig.options) ? finalConfig.options : []).filter(\n (o: any) => hasOptionValue(o) || (Array.isArray(o?.options) && o.options.length > 0),\n );\n if (opts.length === 0) {\n message.warning('下拉选框/单选框/多选框至少需要一个选项,请在「数据源」页的「选项列表」中添加');\n return;\n }\n finalConfig.options = opts;\n }\n // 清理空壳键:空数组/空串不落库\n ['labelKeyList', 'extendedFields', 'searchConditions', 'simpleSearchConditions', 'required_condition', 'options'].forEach((k) => {\n if (Array.isArray(finalConfig[k]) && finalConfig[k].length === 0) delete finalConfig[k];\n });\n if (finalConfig.defaultValueType === '') delete finalConfig.defaultValueType;\n props.record.advanced_search_component = finalConfig;\n jsonDirty.value = false;\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.csc-body {\n /* 抽屉 body 与改造前的弹窗 body 一样是「确定高度的 flex item」,flex:1 + height:100% 双保险撑满剩余高度 */\n flex: 1;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.adv-tip {\n flex: none;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 12px;\n line-height: 1.5;\n}\n.csc-tabs {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 滚动只发生在内容区:Tab 标签栏固定 */\n :deep(.ant-tabs-nav) {\n flex: none;\n margin-bottom: 12px;\n }\n :deep(.ant-tabs-content-holder) {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 抵消 a-row gutter 的 -8px 负 margin,消除多余横向滚动条 */\n padding: 0 8px;\n }\n /* 让 tabpane 拿到确定高度,JSON 编辑器才能撑满内容区 */\n :deep(.ant-tabs-content),\n :deep(.ant-tabs-tabpane) {\n height: 100%;\n }\n}\n.props-divider {\n margin: 8px 0 12px;\n}\n.csc-json-pane {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.json-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 8px;\n min-width: 0;\n}\n.json-toolbar .ant-alert {\n flex: 1;\n min-width: 0;\n}\n.json-tip {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.json-editor-wrap {\n flex: 1;\n min-height: 0;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n overflow: hidden;\n}\n/* CodeEditor 全局样式固定 300px 高,这里撑满 JSON 页剩余高度 */\n.json-editor-wrap :deep(.CodeMirror) {\n height: 100% !important;\n font-size: 13px !important;\n}\n.adv-footer {\n flex: none;\n margin-top: 8px;\n}\n/* 抽屉底部操作区(footer 插槽内容,是本文档的元素 → scoped 可命中) */\n.csc-drawer-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n</style>\n\n<!-- ★ v29.31:抽屉是 teleport 到 body 的,`root-class-name` 挂在**抽屉自己的根节点**上,\n 本组件根元素不是它的祖先 → scoped 的 :deep() 选不中,只能写全局样式(用 .csc-drawer 限定作用域)。\n ⚠️ antdv 4.2.6 的 a-drawer **没有 `wrap-class-name`**(只有 `root-class-name`/`root-style`),\n 别照搬 a-modal 的 wrap-class-name 写法 —— 会被当成普通 attr 落到别处,样式静默失效。\n ⚠️ 选择器**刻意写成三段** `.csc-drawer.ant-drawer .ant-drawer-body`(特异度 0,3,0):\n antd 的 `.ant-drawer-body` 规则经 cssinjs 生成后是 `:where(.css-hash).ant-drawer .ant-drawer-body`(0,2,0),\n 两段式 `.csc-drawer .ant-drawer-body`(同为 0,2,0)会与它**打平**,胜负交给样式注入顺序 —— 不稳。\n (`.csc-drawer` 与 `.ant-drawer` 同在抽屉根节点上:vc-drawer 把 rootClassName 并进根节点的 class 列表。) -->\n<style lang=\"less\">\n.csc-drawer.ant-drawer {\n /* 内部滚动:滚动只发生在 tabs 的 content-holder(见上方 scoped 块),\n 抽屉标题栏与底部操作区固定不随滚动 —— 与改造前的全屏弹窗行为一致 */\n .ant-drawer-body {\n display: flex;\n flex-direction: column;\n padding: 16px;\n overflow: hidden;\n }\n .ant-drawer-footer {\n flex: none;\n }\n}\n</style>\n","<template>\n <div class=\"col-toolbar\">\n <!-- ★ v29.23(用户定案):刷新/保存配置按钮移除,改为「列探测刷新」——\n 基于第 1 步表单的 SQL+数据源实时探测(不读库不写库),结果按 data_index\n 合并进当前配置:已有列保留全部人工配置、新列补默认行,绝不清空。 -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"probing\"\n @click=\"runProbeRefresh\"\n >\n 列探测刷新\n </a-button>\n <span class=\"meta-tip\">\n 基于基础配置的 SQL + 数据源实时探测列(不落库);已有列的人工配置合并保留,SQL\n 新增列自动补默认行。视图与列配置在点「确定」时同时保存。主键<strong>可多选</strong>:\n 勾 1 列时表格行键就是该列,勾 0 列或多列时改用列表接口自动生成的稳定行键\n (多列时由各主键值拼接而成),新建时探测到\n <code>id</code>\n 列会自动选中(可改)。\n </span>\n </div>\n\n <!-- ★ v29.34(用户定案):列配置表由 a-table 换成 s-table(@surely-vue/table,\n 宿主 app 的 main.ts 全局注册 `s-table`,与 tablePro / tableConfig 的\n columnsConfig.vue 同源)。换来三点 a-table 给不了的:① 固定表头 + 表体虚拟滚动\n (18 列的长清单不卡);② 列宽只认显式 width,不再被均分挤压;\n ③ 高度由容器实测下发(colTableHeight),父级不再出纵向滚动条。\n ⚠️ 对照坑:s-table 的「表体可视高度」是 `:height`(不是 scroll.y);\n `rangeSelection` 默认 'single'(拖选单元格),本表整行都是输入控件 → 显式关掉,\n 避免与输入框内的文字选择打架。 -->\n <div\n v-if=\"colTableMounted\"\n ref=\"colTableWrapRef\"\n class=\"col-table-wrapper\"\n >\n <s-table\n :columns=\"COL_COLUMNS\"\n :data-source=\"colRows\"\n row-key=\"data_index\"\n size=\"small\"\n :rowHeight=\"COL_ROW_HEIGHT\"\n :height=\"colTableHeight\"\n :scroll=\"{ x: COL_TABLE_WIDTH }\"\n :pagination=\"false\"\n :range-selection=\"false\"\n :loading=\"colLoading\"\n >\n <!-- ★ v29.37(用户报障「表格里的数据改完不会立马更新」):**所有读写都必须经\n `recordIndexs` 落到原始数据源 `colRows` 上**,禁止直接用插槽给的 `record`。\n s-table 的作者约定:它为了虚拟滚动/树形/编辑态的高性能,内部对数据做了处理,\n 插槽下发的 `record` 不属于响应式数据源;`recordIndexs: number[]` 才是\n 「按 rowKey 反查原始数据源」的索引路径(源码里 BodyCell 收到的就是\n `getIndexsByKey(rowKey)`;其内部 EditInput 写回也是这么定位 dataSource 的)。\n 非树形数据恒为 `[i]`,故一律写 `colRows[recordIndexs[0]].xxx`。\n\n 「立马更新」这条链是怎么闭合的(改完实测确认,别想当然):\n ① `colRows[...]` 是**响应式读取**,依赖挂在**本组件的渲染 effect** 上\n → 改 `colRows[i].xxx` 会触发本组件重渲染;\n ② 本组件重渲染 → s-table 的 `:scroll=\"{ x: COL_TABLE_WIDTH }\"` 是**内联对象字面量**,\n 每次 render 都是新对象,且它在编译产物的 dynamicProps 里 → Vue 的\n `shouldUpdateComponent` 走 `PROPS(8)` 分支**逐键比较**(runtime-core 源码),\n `nextProps.scroll !== prevProps.scroll` 恒成立 → **s-table 必然重渲染**\n → 插槽重新求值 → 单元格立即刷新。\n ⚠️ 反过来,用 `record.xxx` 时表达式里没有本组件的响应式依赖,改它不产生任何渲染\n 副作用(症状:勾了「默认排序」同行不展开、`已配 N 项` 不跟手、主键复选框点了没反应)。\n ⚠️ **别把 `:scroll` 之类的对象/数组 props 提成常量或 computed**:一旦它们的引用稳定,\n 上面第 ② 步就断了,s-table 会被 `shouldUpdateComponent` 跳过、整表不再刷新。\n\n ⚠️ 因此插槽解构里**刻意不接 `record`** —— 留一条 `record.xxx` 都会被 vue-tsc 报\n 「Cannot find name 'record'」,当作编译期护栏,防止回归。\n ⚠️ 前提:`row-key` 必须唯一(这里用 `data_index`,探测侧已按它去重、后端 unique_together 兜底);\n rowKey 一旦重复,`getIndexsByKey` 会返回第一个匹配项 → 改 A 行会改到 B 行。 -->\n <template #bodyCell=\"{ column, recordIndexs }\">\n <template v-if=\"column.key === 'column_order'\">\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].column_order\"\n size=\"small\"\n style=\"width: 50px\"\n :min=\"0\"\n />\n </template>\n <template v-else-if=\"column.key === 'is_primary_key'\">\n <a-checkbox\n :checked=\"!!colRows[recordIndexs[0]].is_primary_key\"\n @change=\"onPrimaryKeyChange(colRows[recordIndexs[0]], $event)\"\n />\n </template>\n <template v-else-if=\"column.key === 'title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'query_title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].query_title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'cell_component'\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].cell_component\"\n size=\"small\"\n style=\"width: 130px\"\n allow-clear\n show-search\n option-filter-prop=\"label\"\n placeholder=\"默认渲染\"\n :options=\"CELL_COMPONENT_OPTIONS\"\n />\n </template>\n <template v-else-if=\"column.key === 'checked'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].checked\" />\n </template>\n <template v-else-if=\"column.key === 'resizable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].resizable\" />\n </template>\n <template v-else-if=\"column.key === 'summary'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].summary\" />\n </template>\n <template v-else-if=\"column.key === 'is_sortable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_sortable\" />\n </template>\n <!-- ★ v29.18:默认排序三配置合一单元格——开启后出现 升/降序 + 排序顺序 -->\n <template v-else-if=\"column.key === 'default_sort'\">\n <div class=\"default-sort-cell\">\n <a-checkbox\n v-model:checked=\"colRows[recordIndexs[0]].is_default_sort\"\n @change=\"onDefaultSortToggle(colRows[recordIndexs[0]])\"\n />\n <template v-if=\"colRows[recordIndexs[0]].is_default_sort\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].default_sort_type\"\n size=\"small\"\n style=\"width: 68px\"\n :options=\"[\n { value: 'asc', label: '升序' },\n { value: 'desc', label: '降序' },\n ]\"\n />\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].default_sort_order\"\n size=\"small\"\n style=\"width: 60px\"\n :min=\"1\"\n placeholder=\"顺序\"\n />\n </template>\n </div>\n </template>\n <template v-else-if=\"column.key === 'map_color'\">\n <!-- ⚠️ 传**原始行**(不是插槽 record):模态框内部直接写 `props.record.content_map`\n / `color_map`,必须落在表格认可的原始数据源上(含 `record.cell_component = 'tag'`) -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openMapColorModal(colRows[recordIndexs[0]])\"\n >\n {{\n mapCount(colRows[recordIndexs[0]])\n ? `已配 ${mapCount(colRows[recordIndexs[0]])} 项`\n : '配置'\n }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'adv_search'\">\n <!-- ⚠️ 同上:模态框直接写 `props.record.advanced_search_component` -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openAdvSearchModal(colRows[recordIndexs[0]])\"\n >\n {{ colRows[recordIndexs[0]].advanced_search_component?.type || '配置' }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'is_base_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_base_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_default_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_default_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_group_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_group_query\" />\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-popconfirm\n title=\"确认删除该列配置?\"\n @confirm=\"removeColumn(colRows[recordIndexs[0]])\"\n >\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n >\n 删除\n </a-button>\n </a-popconfirm>\n </template>\n </template>\n </s-table>\n </div>\n\n <!-- ★ v29.16:转换/颜色 联合配置模态框 + 高级搜索组件配置模态框 -->\n <ColumnMapColorModal\n v-model:open=\"mapColorOpen\"\n :record=\"mapColorRecord!\"\n />\n <ColumnSearchComponentModal\n v-model:open=\"advSearchOpen\"\n :record=\"advSearchRecord!\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { useResizeObserver } from '@vueuse/core';\nimport { computed, nextTick, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport ColumnMapColorModal from 'liyu-pc-base/components/dbView/ColumnMapColorModal.vue';\nimport ColumnSearchComponentModal from 'liyu-pc-base/components/dbView/ColumnSearchComponentModal.vue';\nimport { extractData, toList } from './helpers';\nimport {\n CELL_COMPONENT_OPTIONS,\n COL_COLUMNS,\n COL_ROW_HEIGHT,\n COL_TABLE_WIDTH,\n PROBE_CELL_MAP,\n PROBE_CLASS_NAME_MAP,\n} from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「列配置」步(★ v29.45 起为 Step 3,§3.1.4 字段配置子表)。\n 本步持有:列行数据 / 探测 / 主键多选(v29.40) / 删除 / 默认排序开关 / 两个行内模态框,\n 以及 s-table 的「容器实测高度」链路(v29.34)。\n\n 拆分带来的三处接口(原先是同一个 setup 里的自由变量,现在必须显式打通):\n ① **进入本步的时机**:原来是外壳 `goNext`/`onStepChange` 里读 `colRows.length` 决定要不要自动探测,\n 现在下沉为 `watch(() => props.active)` —— 外壳传 `active`(`currentStep === 3`)即可,\n 顺带把 v29.34 的 `colTableMounted` 门控也挂到同一个 watch 上(挂载 + 兜底测量 + 自动探测一次做完)。\n ② **提交载荷**:外壳 `handleOk` 需要 columns / deleted_data_indexes → 由 `defineExpose` 提供\n `buildColumnsPayload` / `getDeletedIndexes` / `resetDeletedIndexes`,外壳用 ref 调用。\n ③ **清预览错误**:`runProbeRefresh` 原先顺手清 `sqlError`(第 1 步可能残留的预览报错)\n → 改为 `emit('probeStart')`,由外壳清(`sqlError` 仍是外壳持有的共享状态)。\n\n ⚠️ 本组件是**多根(fragment)模板**:三个根节点会原样插进外壳的\n `<div class=\"step-pane step-pane--col\">` 里,不额外产生 DOM 层 —— 这是刻意的,\n 多包一层 div 就会让 `.col-table-wrapper` 的 `flex: 1` 失去参照(父级不再是那个定高 flex column),\n 整条自适应高度链路会从第一环断掉。代价:外壳不能给本组件传 class / v-show(fragment 无法继承),\n 显隐由外壳那层 div 的 v-show 负责。 */\n\ninterface Props {\n /** 外壳告知「当前就是本步」(= currentStep === 3):用于挂载表格与进入时自动探测 */\n active: boolean;\n model: any;\n record: any;\n formType: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['probeStart']);\n\nconst model = computed(() => props.model);\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* ═══ 列配置子表(§3.1.4) ═══\n ★ v29.23:数据来源两路——编辑态预取走 DB(loadColumns);探测刷新走 probe_columns\n (基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并:\n 已有列保留全部人工配置(class_name/sql_type 属类型信息,随探测更新)、\n 新列补默认行;探测里已消失的列保留在末尾(不清除现有配置,可手动删行)。 */\nconst colRows = ref<any[]>([]);\nconst colLoading = ref(false);\n/* v29.11:本次保存要删除的 data_index(删除仅移除列配置行,不改 SQL) */\nconst deletedDataIndexes = ref<string[]>([]);\n\n/* ═══ ★ v29.34(用户定案):列配置表换 s-table + 容器自适应高度 ═══\n 高度算法照抄 `tablePro/tableConfig/columnsConfig.vue` 的 `tableHeight`:\n **量「装表格的那层容器」,把它原样交给 s-table 当整体高度**。\n 目标不变量(只有一条):**整个本步(列配置)里只能有 s-table 自己的那根滚动条**——\n 表体纵向滚动由 s-table 负责,横向由 `scroll.x` 负责,面板(`.step-pane.step-pane--col`)既不滚也不溢。\n 推论:`colTableHeight` 必须等于「面板高 − 工具条高 − 间距」,多 1px 就会把面板撑出\n 第二根滚动条,少 1px 表格底部就留白。所以链路必须整段闭合,断任何一环都拿不到这个差:\n ① `.step-pane.step-pane--col`(在外壳,作用在外壳那层 div 上):定高(继承 .step-pane 的\n `calc(80vh - 150px)`)+ flex column + `overflow: hidden`(兜住亚像素误差);\n ② `.col-table-wrapper`(本组件):`flex: 1; min-height: 0`,**且只包表格、不包工具条**——\n 它的高度由 flex 解析出来(不是靠内容撑),正好等于「剩余高度」;\n 这也是能安全观测自己的原因:高度不依赖表格内容,不存在「观测→改高→再观测」循环;\n ③ `:height=\"colTableHeight\"`:s-table 的整体高度(表头 + 表体,s-table 用 `height`\n 表达 scroll.y)。留白方向偏安全:用整数 `clientHeight` 截断,宁可差不到 1px 也不外溢。\n 实测来源两路互补:\n - `useResizeObserver`:容器几何变化(切步骤 / 弹窗尺寸变化 / 字体缩放)就重下发;\n - 首次进入本步时先挂载(见下方 watch(active))、再 `nextTick` 显式量一次做兜底,\n 且 `if (h > 0)` 守卫保证「隐藏期间不清掉上一次的有效高度」。 */\nconst colTableWrapRef = ref<HTMLElement | null>(null);\nconst colTableHeight = ref(320); // 兜底值:RO 首帧 / 隐藏期间报不出有效高度时的可用高度\nconst measureColTable = () => {\n const h = colTableWrapRef.value?.clientHeight || 0;\n if (h > 0) colTableHeight.value = h;\n};\nuseResizeObserver(colTableWrapRef, entries => {\n const h = entries[0]?.contentRect?.height || 0;\n if (h > 0) colTableHeight.value = h;\n});\n\n/* s-table 的**挂载时机**:必须等本步真正完成布局之后再挂。\n 为什么不能直接写 `v-if=\"active\"`:`v-show` 的 `display` 是 Vue 的\n vShow 指令在 **updated 钩子**里写的,而指令钩子走 post-flush(整个 patch 结束之后)——\n 于是「把表格插进 DOM」的那一刻,父级还是 display:none,s-table 首帧量到 0 宽 0 高。\n 用 `nextTick` 把挂载推到 flush 之后即可(此时 display 已生效)。\n 挂载后就不再卸载:来回切步骤由外壳的 v-show 隐藏,保留表格内部的横向滚动位置。 */\nconst colTableMounted = ref(false);\nwatch(\n () => props.active,\n act => {\n if (!act) return;\n colTableMounted.value = true;\n nextTick(measureColTable); // 挂载后立刻量一次;此后由 ResizeObserver 接管\n /* ★ v29.23:进入本步且当前无列(新建流程)→ 按基础配置的 SQL+数据源自动探测推断\n (原先写在外壳的 goNext / onStepChange 里,随本步一起下沉) */\n if (!colRows.value.length) runProbeRefresh();\n },\n);\n\n/* ★ v29.26(用户定案):新建流程自动探测列后,若存在 `id` 列(不分大小写)则自动选它为主键。\n `pkAutoLocked` = 自动选择只做一次、且一旦用户手动动过主键就不再干预\n (否则用户取消勾选后再点「列探测刷新」又被自动勾回来)。 */\nconst pkAutoLocked = ref(false);\n\n/* 在探测出的列集合里自动指定主键:命中 `id` 列且当前一列主键都没有时生效。\n ★ v29.40:主键可多选后本函数**只勾 id 那一列**(不再整列重置)—— 前置守卫已保证\n 「当前一列主键都没有」,所以效果与原先的「互斥式整列重置」等价,但语义更直白。 */\nconst autoPickPrimaryKey = (rows: any[]) => {\n if (pkAutoLocked.value || props.formType === 'edit') return;\n if (rows.some((r: any) => !!r.is_primary_key)) return; // 已有主键(如探测保留的配置)不覆盖\n const idRow = rows.find(\n (r: any) =>\n String(r.data_index || '')\n .trim()\n .toLowerCase() === 'id',\n );\n if (!idRow) return;\n idRow.is_primary_key = true;\n pkAutoLocked.value = true;\n};\n\n/* ★ v29.24 引入 / ★ v29.40(用户定案)改为**可多选**:纯 toggle,不再「勾一个自动清其他」。\n 语义与后端 rowKey 派生对齐(db_view/utils.resolve_row_key):\n · 勾 0 列 → rowKey 落 '_id',值 = 整行内容 hash(隐含主键)\n · 勾 1 列 → rowKey = 该列名\n · 勾 ≥2 列 → rowKey 落 '_id',值 = 各主键列值用 '|' 连接\n 后端保存侧原先的「最多一列 True」兜底已一并删除(is_primary_key 本来就逐行落库)。 */\nconst onPrimaryKeyChange = (record: any, e: any) => {\n pkAutoLocked.value = true; // ★ v29.26:用户手动改过 → 后续探测刷新不再自动指定\n record.is_primary_key = !!e?.target?.checked;\n};\n\nconst buildDefaultColumn = (name: string, type: string, order: number) => ({\n data_index: name,\n title: name,\n query_title: undefined,\n column_order: order,\n width: 150,\n checked: true,\n ellipsis: true,\n resizable: true,\n summary: false,\n is_primary_key: false, // ★ v29.24:默认不指定主键(人工勾选;一列都不勾走隐含 _id)\n // ★ v29.23.2(用户定案):新建时 可排序/基础搜索/默认搜索 默认全部勾选\n is_sortable: true,\n is_default_sort: false,\n default_sort_type: null,\n default_sort_order: null,\n class_name: PROBE_CLASS_NAME_MAP[type] || 'str',\n sql_type: type || '',\n cell_component: PROBE_CELL_MAP[type] || '',\n is_base_query: true,\n is_default_query: true,\n is_group_query: false,\n});\n\nconst probing = ref(false);\n\nconst runProbeRefresh = async () => {\n if (!model.value.data_source_id || !model.value.sql) {\n message.warning('请先在「基础配置」步骤选择数据源并填写 SQL');\n return;\n }\n probing.value = true;\n /* ★ v29.35:原先是 `sqlError.value = ''`(清掉第 1 步可能残留的预览报错)——\n sqlError 归外壳持有(第 1 步渲染它),这里改成事件通知,行为不变 */\n emit('probeStart');\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/probe_columns`,\n );\n const payload = extractData(res);\n\n const cols: any[] = payload?.columns || [];\n if (!cols.length) {\n message.warning('探测到 0 列,请检查 SQL 输出');\n return;\n }\n /* 按 data_index 合并:命中行保留配置只更新类型信息;新列追加默认行;已消失列保留 */\n const byIdx = new Map<string, any>(colRows.value.map((r: any) => [r.data_index, r]));\n const next: any[] = [];\n const seen = new Set<string>();\n cols.forEach((c: any, i: number) => {\n if (seen.has(c.name)) return; // SQL 输出重名列兜底(unique_together 会炸)\n seen.add(c.name);\n const exist = byIdx.get(c.name);\n if (exist) {\n exist.class_name = PROBE_CLASS_NAME_MAP[c.type] || exist.class_name || 'str';\n exist.sql_type = c.type || exist.sql_type || '';\n next.push(exist);\n } else {\n next.push(buildDefaultColumn(c.name, c.type || '', i + 1));\n }\n });\n colRows.value.forEach((r: any) => {\n if (!seen.has(r.data_index)) next.push(r); // 探测里没有的现有列:原样保留(不清除配置)\n });\n // ★ v29.26:新建流程探测后若存在 `id` 列,自动选为主键(用户手动改过则不干预)\n autoPickPrimaryKey(next);\n colRows.value = next;\n } finally {\n probing.value = false;\n }\n};\n\n/* ★ v29.18:默认排序开关——开启时默认升序 + 排序顺序=1(用户可再改) */\nconst onDefaultSortToggle = (record: any) => {\n if (record.is_default_sort) {\n record.default_sort_type = record.default_sort_type || 'asc';\n record.default_sort_order = record.default_sort_order || 1;\n }\n};\n\n/* 编辑态预取已保存的列配置(★ v29.23:仅作底数,列探测刷新可在此基础上实时合并) */\nconst loadColumns = async () => {\n if (props.formType !== 'edit' || props.record?.id == null) return;\n colLoading.value = true;\n try {\n /* ★ v29.8.1(用户定案):读取走 lybase 通用 query_db_view_column_list,\n filter_fields 契约按 view_id 过滤;不传 sort_fields → 按 Meta.ordering(column_order)排序 */\n const res = await getApiData(\n {\n org_id: current_org?.id,\n filter_fields: ['view_id'],\n view_id: props.record.id,\n is_pagination: false,\n },\n `${props.record.model_app}/query_db_view_column_list`,\n );\n colRows.value = toList(res);\n deletedDataIndexes.value = []; // 刷新 = 放弃本地未保存的删改\n } catch (e) {\n colRows.value = [];\n } finally {\n colLoading.value = false;\n }\n};\n\nconst removeColumn = (record: any) => {\n colRows.value = colRows.value.filter((r: any) => r.data_index !== record.data_index);\n deletedDataIndexes.value.push(record.data_index);\n};\n\n/* ═══ ★ v29.16:列配置行内两个模态框 ═══ */\n/* 转换/颜色(content_map + color_map 联合配置,用户定案:不割裂为两个入口) */\nconst mapColorOpen = ref(false);\nconst mapColorRecord = ref<any>(null);\nconst openMapColorModal = (record: any) => {\n mapColorRecord.value = record;\n mapColorOpen.value = true;\n};\nconst mapCount = (record: any) => {\n const keys = new Set([\n ...Object.keys(record?.content_map || {}),\n ...Object.keys(record?.color_map || {}),\n ]);\n return keys.size;\n};\n/* 高级搜索组件(完整组件配置对象,JSONField 落库) */\nconst advSearchOpen = ref(false);\nconst advSearchRecord = ref<any>(null);\nconst openAdvSearchModal = (record: any) => {\n advSearchRecord.value = record;\n advSearchOpen.value = true;\n};\n\n/* ═══ ★ v29.23:列配置随视图一起保存(save_view 同时落两表) ═══\n 此前 doSaveColumns/saveColumns 独立按钮与连带保存已移除——外壳 handleOk 统一打包\n 主表字段 + columns + deleted_data_indexes 提交 save_view(新建/编辑共用),\n 故这里把两个「取值口」expose 给外壳。 */\nconst buildColumnsPayload = () =>\n colRows.value.map((r: any) => ({\n data_index: r.data_index,\n column_order: r.column_order,\n title: r.title,\n query_title: r.query_title,\n checked: r.checked,\n ellipsis: r.ellipsis,\n resizable: r.resizable,\n summary: r.summary,\n is_sortable: !!r.is_sortable, // ★ v29.16:sorter(JSON) → is_sortable(Bool)\n is_default_sort: !!r.is_default_sort, // ★ v29.18:默认排序三字段\n default_sort_type: r.is_default_sort ? r.default_sort_type || 'asc' : null,\n default_sort_order: r.is_default_sort ? r.default_sort_order ?? null : null,\n cell_component: r.cell_component,\n is_base_query: r.is_base_query,\n is_default_query: r.is_default_query,\n is_group_query: r.is_group_query,\n content_map: r.content_map ?? null, // ★ v29.16:转换/颜色 模态框产出\n color_map: r.color_map ?? null,\n advanced_search_component: r.advanced_search_component ?? null, // ★ v29.16:组件配置对象\n // ★ v29.23.1:类型信息随保存落库——后端 _upsert_columns 白名单已放行;\n // 漏传会导致编辑态探测出的类型保存即丢(保存后再次编辑类型恒空)\n class_name: r.class_name || '',\n sql_type: r.sql_type || '',\n // ★ v29.24:主键标记。★ v29.40:可多选 —— 逐行原样照传,后端不再做「最多一列」收敛\n is_primary_key: !!r.is_primary_key,\n }));\n\ndefineExpose({\n /** 提交用:列配置载荷(外壳 handleOk 取) */\n buildColumnsPayload,\n /** 提交用:本次要删除的 data_index(外壳 handleOk 取) */\n getDeletedIndexes: () => [...deletedDataIndexes.value],\n /** 保存成功后:清空待删除列表(原先由外壳直接 `deletedDataIndexes.value = []` 完成) */\n resetDeletedIndexes: () => {\n deletedDataIndexes.value = [];\n },\n});\n\nonMounted(() => {\n /* 编辑态预取列配置(原先在外壳 onMounted 的 edit 分支里调;本组件的 mounted 早于外壳,\n 但 `formType` / `record.id` 都由外壳在 setup 顶层就绪,读到的值一定是对的) */\n if (props.formType === 'edit') loadColumns();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.col-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n}\n/* ★ v29.34:s-table 的高度来源容器——flex:1 撑满工具条之外的空间;min-height:0 让\n 它能在内容更高时被压缩(否则 flex 子项默认 min-height:auto,会顶破父级定高)。 */\n.col-table-wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n/* ★ v29.18:默认排序单元格——开关 + 升降序 + 顺序 横排 */\n.default-sort-cell {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n</style>\n","<template>\n <div class=\"col-toolbar\">\n <!-- ★ v29.23(用户定案):刷新/保存配置按钮移除,改为「列探测刷新」——\n 基于第 1 步表单的 SQL+数据源实时探测(不读库不写库),结果按 data_index\n 合并进当前配置:已有列保留全部人工配置、新列补默认行,绝不清空。 -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"probing\"\n @click=\"runProbeRefresh\"\n >\n 列探测刷新\n </a-button>\n <span class=\"meta-tip\">\n 基于基础配置的 SQL + 数据源实时探测列(不落库);已有列的人工配置合并保留,SQL\n 新增列自动补默认行。视图与列配置在点「确定」时同时保存。主键<strong>可多选</strong>:\n 勾 1 列时表格行键就是该列,勾 0 列或多列时改用列表接口自动生成的稳定行键\n (多列时由各主键值拼接而成),新建时探测到\n <code>id</code>\n 列会自动选中(可改)。\n </span>\n </div>\n\n <!-- ★ v29.34(用户定案):列配置表由 a-table 换成 s-table(@surely-vue/table,\n 宿主 app 的 main.ts 全局注册 `s-table`,与 tablePro / tableConfig 的\n columnsConfig.vue 同源)。换来三点 a-table 给不了的:① 固定表头 + 表体虚拟滚动\n (18 列的长清单不卡);② 列宽只认显式 width,不再被均分挤压;\n ③ 高度由容器实测下发(colTableHeight),父级不再出纵向滚动条。\n ⚠️ 对照坑:s-table 的「表体可视高度」是 `:height`(不是 scroll.y);\n `rangeSelection` 默认 'single'(拖选单元格),本表整行都是输入控件 → 显式关掉,\n 避免与输入框内的文字选择打架。 -->\n <div\n v-if=\"colTableMounted\"\n ref=\"colTableWrapRef\"\n class=\"col-table-wrapper\"\n >\n <s-table\n :columns=\"COL_COLUMNS\"\n :data-source=\"colRows\"\n row-key=\"data_index\"\n size=\"small\"\n :rowHeight=\"COL_ROW_HEIGHT\"\n :height=\"colTableHeight\"\n :scroll=\"{ x: COL_TABLE_WIDTH }\"\n :pagination=\"false\"\n :range-selection=\"false\"\n :loading=\"colLoading\"\n >\n <!-- ★ v29.37(用户报障「表格里的数据改完不会立马更新」):**所有读写都必须经\n `recordIndexs` 落到原始数据源 `colRows` 上**,禁止直接用插槽给的 `record`。\n s-table 的作者约定:它为了虚拟滚动/树形/编辑态的高性能,内部对数据做了处理,\n 插槽下发的 `record` 不属于响应式数据源;`recordIndexs: number[]` 才是\n 「按 rowKey 反查原始数据源」的索引路径(源码里 BodyCell 收到的就是\n `getIndexsByKey(rowKey)`;其内部 EditInput 写回也是这么定位 dataSource 的)。\n 非树形数据恒为 `[i]`,故一律写 `colRows[recordIndexs[0]].xxx`。\n\n 「立马更新」这条链是怎么闭合的(改完实测确认,别想当然):\n ① `colRows[...]` 是**响应式读取**,依赖挂在**本组件的渲染 effect** 上\n → 改 `colRows[i].xxx` 会触发本组件重渲染;\n ② 本组件重渲染 → s-table 的 `:scroll=\"{ x: COL_TABLE_WIDTH }\"` 是**内联对象字面量**,\n 每次 render 都是新对象,且它在编译产物的 dynamicProps 里 → Vue 的\n `shouldUpdateComponent` 走 `PROPS(8)` 分支**逐键比较**(runtime-core 源码),\n `nextProps.scroll !== prevProps.scroll` 恒成立 → **s-table 必然重渲染**\n → 插槽重新求值 → 单元格立即刷新。\n ⚠️ 反过来,用 `record.xxx` 时表达式里没有本组件的响应式依赖,改它不产生任何渲染\n 副作用(症状:勾了「默认排序」同行不展开、`已配 N 项` 不跟手、主键复选框点了没反应)。\n ⚠️ **别把 `:scroll` 之类的对象/数组 props 提成常量或 computed**:一旦它们的引用稳定,\n 上面第 ② 步就断了,s-table 会被 `shouldUpdateComponent` 跳过、整表不再刷新。\n\n ⚠️ 因此插槽解构里**刻意不接 `record`** —— 留一条 `record.xxx` 都会被 vue-tsc 报\n 「Cannot find name 'record'」,当作编译期护栏,防止回归。\n ⚠️ 前提:`row-key` 必须唯一(这里用 `data_index`,探测侧已按它去重、后端 unique_together 兜底);\n rowKey 一旦重复,`getIndexsByKey` 会返回第一个匹配项 → 改 A 行会改到 B 行。 -->\n <template #bodyCell=\"{ column, recordIndexs }\">\n <template v-if=\"column.key === 'column_order'\">\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].column_order\"\n size=\"small\"\n style=\"width: 50px\"\n :min=\"0\"\n />\n </template>\n <template v-else-if=\"column.key === 'is_primary_key'\">\n <a-checkbox\n :checked=\"!!colRows[recordIndexs[0]].is_primary_key\"\n @change=\"onPrimaryKeyChange(colRows[recordIndexs[0]], $event)\"\n />\n </template>\n <template v-else-if=\"column.key === 'title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'query_title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].query_title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'cell_component'\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].cell_component\"\n size=\"small\"\n style=\"width: 130px\"\n allow-clear\n show-search\n option-filter-prop=\"label\"\n placeholder=\"默认渲染\"\n :options=\"CELL_COMPONENT_OPTIONS\"\n />\n </template>\n <template v-else-if=\"column.key === 'checked'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].checked\" />\n </template>\n <template v-else-if=\"column.key === 'resizable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].resizable\" />\n </template>\n <template v-else-if=\"column.key === 'summary'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].summary\" />\n </template>\n <template v-else-if=\"column.key === 'is_sortable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_sortable\" />\n </template>\n <!-- ★ v29.18:默认排序三配置合一单元格——开启后出现 升/降序 + 排序顺序 -->\n <template v-else-if=\"column.key === 'default_sort'\">\n <div class=\"default-sort-cell\">\n <a-checkbox\n v-model:checked=\"colRows[recordIndexs[0]].is_default_sort\"\n @change=\"onDefaultSortToggle(colRows[recordIndexs[0]])\"\n />\n <template v-if=\"colRows[recordIndexs[0]].is_default_sort\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].default_sort_type\"\n size=\"small\"\n style=\"width: 68px\"\n :options=\"[\n { value: 'asc', label: '升序' },\n { value: 'desc', label: '降序' },\n ]\"\n />\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].default_sort_order\"\n size=\"small\"\n style=\"width: 60px\"\n :min=\"1\"\n placeholder=\"顺序\"\n />\n </template>\n </div>\n </template>\n <template v-else-if=\"column.key === 'map_color'\">\n <!-- ⚠️ 传**原始行**(不是插槽 record):模态框内部直接写 `props.record.content_map`\n / `color_map`,必须落在表格认可的原始数据源上(含 `record.cell_component = 'tag'`) -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openMapColorModal(colRows[recordIndexs[0]])\"\n >\n {{\n mapCount(colRows[recordIndexs[0]])\n ? `已配 ${mapCount(colRows[recordIndexs[0]])} 项`\n : '配置'\n }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'adv_search'\">\n <!-- ⚠️ 同上:模态框直接写 `props.record.advanced_search_component` -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openAdvSearchModal(colRows[recordIndexs[0]])\"\n >\n {{ colRows[recordIndexs[0]].advanced_search_component?.type || '配置' }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'is_base_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_base_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_default_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_default_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_group_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_group_query\" />\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-popconfirm\n title=\"确认删除该列配置?\"\n @confirm=\"removeColumn(colRows[recordIndexs[0]])\"\n >\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n >\n 删除\n </a-button>\n </a-popconfirm>\n </template>\n </template>\n </s-table>\n </div>\n\n <!-- ★ v29.16:转换/颜色 联合配置模态框 + 高级搜索组件配置模态框 -->\n <ColumnMapColorModal\n v-model:open=\"mapColorOpen\"\n :record=\"mapColorRecord!\"\n />\n <ColumnSearchComponentModal\n v-model:open=\"advSearchOpen\"\n :record=\"advSearchRecord!\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { useResizeObserver } from '@vueuse/core';\nimport { computed, nextTick, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport ColumnMapColorModal from 'liyu-pc-base/components/dbView/ColumnMapColorModal.vue';\nimport ColumnSearchComponentModal from 'liyu-pc-base/components/dbView/ColumnSearchComponentModal.vue';\nimport { extractData, toList } from './helpers';\nimport {\n CELL_COMPONENT_OPTIONS,\n COL_COLUMNS,\n COL_ROW_HEIGHT,\n COL_TABLE_WIDTH,\n PROBE_CELL_MAP,\n PROBE_CLASS_NAME_MAP,\n} from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「列配置」步(★ v29.45 起为 Step 3,§3.1.4 字段配置子表)。\n 本步持有:列行数据 / 探测 / 主键多选(v29.40) / 删除 / 默认排序开关 / 两个行内模态框,\n 以及 s-table 的「容器实测高度」链路(v29.34)。\n\n 拆分带来的三处接口(原先是同一个 setup 里的自由变量,现在必须显式打通):\n ① **进入本步的时机**:原来是外壳 `goNext`/`onStepChange` 里读 `colRows.length` 决定要不要自动探测,\n 现在下沉为 `watch(() => props.active)` —— 外壳传 `active`(`currentStep === 3`)即可,\n 顺带把 v29.34 的 `colTableMounted` 门控也挂到同一个 watch 上(挂载 + 兜底测量 + 自动探测一次做完)。\n ② **提交载荷**:外壳 `handleOk` 需要 columns / deleted_data_indexes → 由 `defineExpose` 提供\n `buildColumnsPayload` / `getDeletedIndexes` / `resetDeletedIndexes`,外壳用 ref 调用。\n ③ **清预览错误**:`runProbeRefresh` 原先顺手清 `sqlError`(第 1 步可能残留的预览报错)\n → 改为 `emit('probeStart')`,由外壳清(`sqlError` 仍是外壳持有的共享状态)。\n\n ⚠️ 本组件是**多根(fragment)模板**:三个根节点会原样插进外壳的\n `<div class=\"step-pane step-pane--col\">` 里,不额外产生 DOM 层 —— 这是刻意的,\n 多包一层 div 就会让 `.col-table-wrapper` 的 `flex: 1` 失去参照(父级不再是那个定高 flex column),\n 整条自适应高度链路会从第一环断掉。代价:外壳不能给本组件传 class / v-show(fragment 无法继承),\n 显隐由外壳那层 div 的 v-show 负责。 */\n\ninterface Props {\n /** 外壳告知「当前就是本步」(= currentStep === 3):用于挂载表格与进入时自动探测 */\n active: boolean;\n model: any;\n record: any;\n formType: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['probeStart']);\n\nconst model = computed(() => props.model);\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* ═══ 列配置子表(§3.1.4) ═══\n ★ v29.23:数据来源两路——编辑态预取走 DB(loadColumns);探测刷新走 probe_columns\n (基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并:\n 已有列保留全部人工配置(class_name/sql_type 属类型信息,随探测更新)、\n 新列补默认行;探测里已消失的列保留在末尾(不清除现有配置,可手动删行)。 */\nconst colRows = ref<any[]>([]);\nconst colLoading = ref(false);\n/* v29.11:本次保存要删除的 data_index(删除仅移除列配置行,不改 SQL) */\nconst deletedDataIndexes = ref<string[]>([]);\n\n/* ═══ ★ v29.34(用户定案):列配置表换 s-table + 容器自适应高度 ═══\n 高度算法照抄 `tablePro/tableConfig/columnsConfig.vue` 的 `tableHeight`:\n **量「装表格的那层容器」,把它原样交给 s-table 当整体高度**。\n 目标不变量(只有一条):**整个本步(列配置)里只能有 s-table 自己的那根滚动条**——\n 表体纵向滚动由 s-table 负责,横向由 `scroll.x` 负责,面板(`.step-pane.step-pane--col`)既不滚也不溢。\n 推论:`colTableHeight` 必须等于「面板高 − 工具条高 − 间距」,多 1px 就会把面板撑出\n 第二根滚动条,少 1px 表格底部就留白。所以链路必须整段闭合,断任何一环都拿不到这个差:\n ① `.step-pane.step-pane--col`(在外壳,作用在外壳那层 div 上):定高(继承 .step-pane 的\n `calc(80vh - 150px)`)+ flex column + `overflow: hidden`(兜住亚像素误差);\n ② `.col-table-wrapper`(本组件):`flex: 1; min-height: 0`,**且只包表格、不包工具条**——\n 它的高度由 flex 解析出来(不是靠内容撑),正好等于「剩余高度」;\n 这也是能安全观测自己的原因:高度不依赖表格内容,不存在「观测→改高→再观测」循环;\n ③ `:height=\"colTableHeight\"`:s-table 的整体高度(表头 + 表体,s-table 用 `height`\n 表达 scroll.y)。留白方向偏安全:用整数 `clientHeight` 截断,宁可差不到 1px 也不外溢。\n 实测来源两路互补:\n - `useResizeObserver`:容器几何变化(切步骤 / 弹窗尺寸变化 / 字体缩放)就重下发;\n - 首次进入本步时先挂载(见下方 watch(active))、再 `nextTick` 显式量一次做兜底,\n 且 `if (h > 0)` 守卫保证「隐藏期间不清掉上一次的有效高度」。 */\nconst colTableWrapRef = ref<HTMLElement | null>(null);\nconst colTableHeight = ref(320); // 兜底值:RO 首帧 / 隐藏期间报不出有效高度时的可用高度\nconst measureColTable = () => {\n const h = colTableWrapRef.value?.clientHeight || 0;\n if (h > 0) colTableHeight.value = h;\n};\nuseResizeObserver(colTableWrapRef, entries => {\n const h = entries[0]?.contentRect?.height || 0;\n if (h > 0) colTableHeight.value = h;\n});\n\n/* s-table 的**挂载时机**:必须等本步真正完成布局之后再挂。\n 为什么不能直接写 `v-if=\"active\"`:`v-show` 的 `display` 是 Vue 的\n vShow 指令在 **updated 钩子**里写的,而指令钩子走 post-flush(整个 patch 结束之后)——\n 于是「把表格插进 DOM」的那一刻,父级还是 display:none,s-table 首帧量到 0 宽 0 高。\n 用 `nextTick` 把挂载推到 flush 之后即可(此时 display 已生效)。\n 挂载后就不再卸载:来回切步骤由外壳的 v-show 隐藏,保留表格内部的横向滚动位置。 */\nconst colTableMounted = ref(false);\nwatch(\n () => props.active,\n act => {\n if (!act) return;\n colTableMounted.value = true;\n nextTick(measureColTable); // 挂载后立刻量一次;此后由 ResizeObserver 接管\n /* ★ v29.23:进入本步且当前无列(新建流程)→ 按基础配置的 SQL+数据源自动探测推断\n (原先写在外壳的 goNext / onStepChange 里,随本步一起下沉) */\n if (!colRows.value.length) runProbeRefresh();\n },\n);\n\n/* ★ v29.26(用户定案):新建流程自动探测列后,若存在 `id` 列(不分大小写)则自动选它为主键。\n `pkAutoLocked` = 自动选择只做一次、且一旦用户手动动过主键就不再干预\n (否则用户取消勾选后再点「列探测刷新」又被自动勾回来)。 */\nconst pkAutoLocked = ref(false);\n\n/* 在探测出的列集合里自动指定主键:命中 `id` 列且当前一列主键都没有时生效。\n ★ v29.40:主键可多选后本函数**只勾 id 那一列**(不再整列重置)—— 前置守卫已保证\n 「当前一列主键都没有」,所以效果与原先的「互斥式整列重置」等价,但语义更直白。 */\nconst autoPickPrimaryKey = (rows: any[]) => {\n if (pkAutoLocked.value || props.formType === 'edit') return;\n if (rows.some((r: any) => !!r.is_primary_key)) return; // 已有主键(如探测保留的配置)不覆盖\n const idRow = rows.find(\n (r: any) =>\n String(r.data_index || '')\n .trim()\n .toLowerCase() === 'id',\n );\n if (!idRow) return;\n idRow.is_primary_key = true;\n pkAutoLocked.value = true;\n};\n\n/* ★ v29.24 引入 / ★ v29.40(用户定案)改为**可多选**:纯 toggle,不再「勾一个自动清其他」。\n 语义与后端 rowKey 派生对齐(db_view/utils.resolve_row_key):\n · 勾 0 列 → rowKey 落 '_id',值 = 整行内容 hash(隐含主键)\n · 勾 1 列 → rowKey = 该列名\n · 勾 ≥2 列 → rowKey 落 '_id',值 = 各主键列值用 '|' 连接\n 后端保存侧原先的「最多一列 True」兜底已一并删除(is_primary_key 本来就逐行落库)。 */\nconst onPrimaryKeyChange = (record: any, e: any) => {\n pkAutoLocked.value = true; // ★ v29.26:用户手动改过 → 后续探测刷新不再自动指定\n record.is_primary_key = !!e?.target?.checked;\n};\n\nconst buildDefaultColumn = (name: string, type: string, order: number) => ({\n data_index: name,\n title: name,\n query_title: undefined,\n column_order: order,\n width: 150,\n checked: true,\n ellipsis: true,\n resizable: true,\n summary: false,\n is_primary_key: false, // ★ v29.24:默认不指定主键(人工勾选;一列都不勾走隐含 _id)\n // ★ v29.23.2(用户定案):新建时 可排序/基础搜索/默认搜索 默认全部勾选\n is_sortable: true,\n is_default_sort: false,\n default_sort_type: null,\n default_sort_order: null,\n class_name: PROBE_CLASS_NAME_MAP[type] || 'str',\n sql_type: type || '',\n cell_component: PROBE_CELL_MAP[type] || '',\n is_base_query: true,\n is_default_query: true,\n is_group_query: false,\n});\n\nconst probing = ref(false);\n\nconst runProbeRefresh = async () => {\n if (!model.value.data_source_id || !model.value.sql) {\n message.warning('请先在「基础配置」步骤选择数据源并填写 SQL');\n return;\n }\n probing.value = true;\n /* ★ v29.35:原先是 `sqlError.value = ''`(清掉第 1 步可能残留的预览报错)——\n sqlError 归外壳持有(第 1 步渲染它),这里改成事件通知,行为不变 */\n emit('probeStart');\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/probe_columns`,\n );\n const payload = extractData(res);\n\n const cols: any[] = payload?.columns || [];\n if (!cols.length) {\n message.warning('探测到 0 列,请检查 SQL 输出');\n return;\n }\n /* 按 data_index 合并:命中行保留配置只更新类型信息;新列追加默认行;已消失列保留 */\n const byIdx = new Map<string, any>(colRows.value.map((r: any) => [r.data_index, r]));\n const next: any[] = [];\n const seen = new Set<string>();\n cols.forEach((c: any, i: number) => {\n if (seen.has(c.name)) return; // SQL 输出重名列兜底(unique_together 会炸)\n seen.add(c.name);\n const exist = byIdx.get(c.name);\n if (exist) {\n exist.class_name = PROBE_CLASS_NAME_MAP[c.type] || exist.class_name || 'str';\n exist.sql_type = c.type || exist.sql_type || '';\n next.push(exist);\n } else {\n next.push(buildDefaultColumn(c.name, c.type || '', i + 1));\n }\n });\n colRows.value.forEach((r: any) => {\n if (!seen.has(r.data_index)) next.push(r); // 探测里没有的现有列:原样保留(不清除配置)\n });\n // ★ v29.26:新建流程探测后若存在 `id` 列,自动选为主键(用户手动改过则不干预)\n autoPickPrimaryKey(next);\n colRows.value = next;\n } finally {\n probing.value = false;\n }\n};\n\n/* ★ v29.18:默认排序开关——开启时默认升序 + 排序顺序=1(用户可再改) */\nconst onDefaultSortToggle = (record: any) => {\n if (record.is_default_sort) {\n record.default_sort_type = record.default_sort_type || 'asc';\n record.default_sort_order = record.default_sort_order || 1;\n }\n};\n\n/* 编辑态预取已保存的列配置(★ v29.23:仅作底数,列探测刷新可在此基础上实时合并) */\nconst loadColumns = async () => {\n if (props.formType !== 'edit' || props.record?.id == null) return;\n colLoading.value = true;\n try {\n /* ★ v29.8.1(用户定案):读取走 lybase 通用 query_db_view_column_list,\n filter_fields 契约按 view_id 过滤;不传 sort_fields → 按 Meta.ordering(column_order)排序 */\n const res = await getApiData(\n {\n org_id: current_org?.id,\n filter_fields: ['view_id'],\n view_id: props.record.id,\n is_pagination: false,\n },\n `${props.record.model_app}/query_db_view_column_list`,\n );\n colRows.value = toList(res);\n deletedDataIndexes.value = []; // 刷新 = 放弃本地未保存的删改\n } catch (e) {\n colRows.value = [];\n } finally {\n colLoading.value = false;\n }\n};\n\nconst removeColumn = (record: any) => {\n colRows.value = colRows.value.filter((r: any) => r.data_index !== record.data_index);\n deletedDataIndexes.value.push(record.data_index);\n};\n\n/* ═══ ★ v29.16:列配置行内两个模态框 ═══ */\n/* 转换/颜色(content_map + color_map 联合配置,用户定案:不割裂为两个入口) */\nconst mapColorOpen = ref(false);\nconst mapColorRecord = ref<any>(null);\nconst openMapColorModal = (record: any) => {\n mapColorRecord.value = record;\n mapColorOpen.value = true;\n};\nconst mapCount = (record: any) => {\n const keys = new Set([\n ...Object.keys(record?.content_map || {}),\n ...Object.keys(record?.color_map || {}),\n ]);\n return keys.size;\n};\n/* 高级搜索组件(完整组件配置对象,JSONField 落库) */\nconst advSearchOpen = ref(false);\nconst advSearchRecord = ref<any>(null);\nconst openAdvSearchModal = (record: any) => {\n advSearchRecord.value = record;\n advSearchOpen.value = true;\n};\n\n/* ═══ ★ v29.23:列配置随视图一起保存(save_view 同时落两表) ═══\n 此前 doSaveColumns/saveColumns 独立按钮与连带保存已移除——外壳 handleOk 统一打包\n 主表字段 + columns + deleted_data_indexes 提交 save_view(新建/编辑共用),\n 故这里把两个「取值口」expose 给外壳。 */\nconst buildColumnsPayload = () =>\n colRows.value.map((r: any) => ({\n data_index: r.data_index,\n column_order: r.column_order,\n title: r.title,\n query_title: r.query_title,\n checked: r.checked,\n ellipsis: r.ellipsis,\n resizable: r.resizable,\n summary: r.summary,\n is_sortable: !!r.is_sortable, // ★ v29.16:sorter(JSON) → is_sortable(Bool)\n is_default_sort: !!r.is_default_sort, // ★ v29.18:默认排序三字段\n default_sort_type: r.is_default_sort ? r.default_sort_type || 'asc' : null,\n default_sort_order: r.is_default_sort ? r.default_sort_order ?? null : null,\n cell_component: r.cell_component,\n is_base_query: r.is_base_query,\n is_default_query: r.is_default_query,\n is_group_query: r.is_group_query,\n content_map: r.content_map ?? null, // ★ v29.16:转换/颜色 模态框产出\n color_map: r.color_map ?? null,\n advanced_search_component: r.advanced_search_component ?? null, // ★ v29.16:组件配置对象\n // ★ v29.23.1:类型信息随保存落库——后端 _upsert_columns 白名单已放行;\n // 漏传会导致编辑态探测出的类型保存即丢(保存后再次编辑类型恒空)\n class_name: r.class_name || '',\n sql_type: r.sql_type || '',\n // ★ v29.24:主键标记。★ v29.40:可多选 —— 逐行原样照传,后端不再做「最多一列」收敛\n is_primary_key: !!r.is_primary_key,\n }));\n\ndefineExpose({\n /** 提交用:列配置载荷(外壳 handleOk 取) */\n buildColumnsPayload,\n /** 提交用:本次要删除的 data_index(外壳 handleOk 取) */\n getDeletedIndexes: () => [...deletedDataIndexes.value],\n /** 保存成功后:清空待删除列表(原先由外壳直接 `deletedDataIndexes.value = []` 完成) */\n resetDeletedIndexes: () => {\n deletedDataIndexes.value = [];\n },\n});\n\nonMounted(() => {\n /* 编辑态预取列配置(原先在外壳 onMounted 的 edit 分支里调;本组件的 mounted 早于外壳,\n 但 `formType` / `record.id` 都由外壳在 setup 顶层就绪,读到的值一定是对的) */\n if (props.formType === 'edit') loadColumns();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.col-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n}\n/* ★ v29.34:s-table 的高度来源容器——flex:1 撑满工具条之外的空间;min-height:0 让\n 它能在内容更高时被压缩(否则 flex 子项默认 min-height:auto,会顶破父级定高)。 */\n.col-table-wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n/* ★ v29.18:默认排序单元格——开关 + 升降序 + 顺序 横排 */\n.default-sort-cell {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"父级路由\"\n name=\"parent_menu_id\"\n extra=\"选择父级菜单;留空表示顶级菜单。保存时会直接写到该视图对应的菜单行\"\n >\n <!-- ★ v29.45(用户定案):父级从「state 字符串」改为 **Menu.id**。\n 旧版写 `parent_menu_flag`(存父菜单的 state 原值),需要在保存时把 state 解析成 id;\n 现在 `save_view` 直接写 `Menu.parent_id`,不再需要转换,\n 取值口径与 menuConfig.vue 的父级菜单选择器也统一成 id。 -->\n <RemoteTreeSelectPro\n v-model:value=\"model.parent_menu_id\"\n :form-model=\"model\"\n placeholder=\"请选择父级菜单\"\n value-key=\"id\"\n label-key=\"name\"\n :field=\"{\n placeholder_i18n: '请选择父级菜单',\n name: 'parent_menu_id',\n allowClear: true,\n valueKey: 'id',\n labelKey: 'name',\n placeholder: '请选择父级菜单',\n postUrl: '/permission/query_menu_list__tree',\n span: 12,\n }\"\n :allow-clear=\"true\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"隶属项目\"\n name=\"menu_project\"\n extra=\"该视图菜单归属的模块/项目;留空 = 通用(所有项目可见)。同样同步到菜单行\"\n >\n <!-- ★ v29.13:照 menuConfig 的 menu_project,postUrl 取语言项目表(值域 = 各宿主项目 flag) -->\n <RemoteSelectPro\n v-model:value=\"model.menu_project\"\n :form-model=\"model\"\n value-key=\"project\"\n label-key=\"name\"\n placeholder=\"请选择模块\"\n :field=\"menuProjectField\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <div class=\"step-menu-tip\">\n 本步两项在保存时<b>直接写入该视图对应的菜单行</b>(菜单不存在则自动新建);\n 菜单的<b>可见角色</b>请在「菜单管理」里配该菜单的角色授权 —— SQL 视图自身已不再保存角色信息。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport RemoteTreeSelectPro from 'liyu-pc-base/components/FormRender/RemoteTreeSelectPro/RemoteTreeSelectPro.vue';\nimport RemoteSelectPro from 'liyu-pc-base/components/FormRender/RemoteSelectPro/RemoteSelectPro.vue';\n\n/* ★ v29.45(用户定案):原「菜单与权限」步(Step 2)挪到**最后一步**并改名「菜单」,\n 同时裁掉两项:\n · 菜单索引(sort) → 改由后端在**新建菜单时**取 `max(sort) + 1`,视图侧不再配置\n · 可见角色(roles) → 改由「菜单的角色授权」承载,`DbView` 不再有角色概念\n 只留「父级路由」与「隶属项目」,两者由 `save_view` 同步到该视图对应的菜单行。\n\n 拆分约定同其它步骤:`model` 由外壳持有、按对象引用共享,这里用 computed 取用后直接改属性。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\n/* ★ v29.13:menu_project 的 RemoteSelectPro 字段配置(照 menuConfig 同款;\n Field 接口必填属性多、模板内联对象字面量会被 TS 判缺属性,这里用 any 放宽) */\nconst menuProjectField: any = {\n name: 'menu_project',\n show_search: true,\n mode: 'default',\n postUrl: '/i18n/query_language_project_list',\n};\n</script>\n\n<style scoped lang=\"less\">\n/* 说明文案:与各 a-form-item 的 extra 同色(次要文字),整行铺开 */\n.step-menu-tip {\n color: rgba(0, 0, 0, 0.45);\n font-size: 12px;\n line-height: 20px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"父级路由\"\n name=\"parent_menu_id\"\n extra=\"选择父级菜单;留空表示顶级菜单。保存时会直接写到该视图对应的菜单行\"\n >\n <!-- ★ v29.45(用户定案):父级从「state 字符串」改为 **Menu.id**。\n 旧版写 `parent_menu_flag`(存父菜单的 state 原值),需要在保存时把 state 解析成 id;\n 现在 `save_view` 直接写 `Menu.parent_id`,不再需要转换,\n 取值口径与 menuConfig.vue 的父级菜单选择器也统一成 id。 -->\n <RemoteTreeSelectPro\n v-model:value=\"model.parent_menu_id\"\n :form-model=\"model\"\n placeholder=\"请选择父级菜单\"\n value-key=\"id\"\n label-key=\"name\"\n :field=\"{\n placeholder_i18n: '请选择父级菜单',\n name: 'parent_menu_id',\n allowClear: true,\n valueKey: 'id',\n labelKey: 'name',\n placeholder: '请选择父级菜单',\n postUrl: '/permission/query_menu_list__tree',\n span: 12,\n }\"\n :allow-clear=\"true\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"隶属项目\"\n name=\"menu_project\"\n extra=\"该视图菜单归属的模块/项目;留空 = 通用(所有项目可见)。同样同步到菜单行\"\n >\n <!-- ★ v29.13:照 menuConfig 的 menu_project,postUrl 取语言项目表(值域 = 各宿主项目 flag) -->\n <RemoteSelectPro\n v-model:value=\"model.menu_project\"\n :form-model=\"model\"\n value-key=\"project\"\n label-key=\"name\"\n placeholder=\"请选择模块\"\n :field=\"menuProjectField\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <div class=\"step-menu-tip\">\n 本步两项在保存时<b>直接写入该视图对应的菜单行</b>(菜单不存在则自动新建);\n 菜单的<b>可见角色</b>请在「菜单管理」里配该菜单的角色授权 —— SQL 视图自身已不再保存角色信息。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport RemoteTreeSelectPro from 'liyu-pc-base/components/FormRender/RemoteTreeSelectPro/RemoteTreeSelectPro.vue';\nimport RemoteSelectPro from 'liyu-pc-base/components/FormRender/RemoteSelectPro/RemoteSelectPro.vue';\n\n/* ★ v29.45(用户定案):原「菜单与权限」步(Step 2)挪到**最后一步**并改名「菜单」,\n 同时裁掉两项:\n · 菜单索引(sort) → 改由后端在**新建菜单时**取 `max(sort) + 1`,视图侧不再配置\n · 可见角色(roles) → 改由「菜单的角色授权」承载,`DbView` 不再有角色概念\n 只留「父级路由」与「隶属项目」,两者由 `save_view` 同步到该视图对应的菜单行。\n\n 拆分约定同其它步骤:`model` 由外壳持有、按对象引用共享,这里用 computed 取用后直接改属性。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\n/* ★ v29.13:menu_project 的 RemoteSelectPro 字段配置(照 menuConfig 同款;\n Field 接口必填属性多、模板内联对象字面量会被 TS 判缺属性,这里用 any 放宽) */\nconst menuProjectField: any = {\n name: 'menu_project',\n show_search: true,\n mode: 'default',\n postUrl: '/i18n/query_language_project_list',\n};\n</script>\n\n<style scoped lang=\"less\">\n/* 说明文案:与各 a-form-item 的 extra 同色(次要文字),整行铺开 */\n.step-menu-tip {\n color: rgba(0, 0, 0, 0.45);\n font-size: 12px;\n line-height: 20px;\n}\n</style>\n","/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分时,把 `view_meta` 的两端读写\n 提成**纯函数**——它横跨「展示配置」步(表单里的 5 个键)与外壳(详情加载 / 提交),\n 放在任一侧都会造成反向依赖,提成不持有状态的纯函数最干净。\n\n ★ v29.18:order_json(迁到列配置默认排序)/ max_page_size / pivot_table_max_records(机制废除)\n 从 META_KEYS 移除——编辑态加载时存量值落入 preserved,原样保留,不再回写表单。 */\n\nconst META_KEYS = [\n 'default_view_type',\n 'enable_view_types',\n 'is_pagination',\n 'pageSize',\n 'page_size_options',\n];\n\n/**\n * 详情加载:把 view_meta 拆成「表单消费的 5 个键」与「原样保留的其余键」。\n * 表单键**就地写进 model**(对象引用共享,外壳与各步骤看到的是同一个对象),\n * 其余键返回给调用方持有,提交时原样合并回去(btn_list、advanced_base_conditions 等不会丢)。\n */\nexport const applyViewMeta = (model: any, meta: Record<string, any>): Record<string, any> => {\n const preserved: Record<string, any> = {};\n Object.keys(meta || {}).forEach((k) => {\n if (!META_KEYS.includes(k)) preserved[k] = meta[k];\n });\n // ★ v29.13:org_filter 已废除(不再消费),存量键落入 preserved 原样保留\n model.default_view_type = meta.default_view_type ?? undefined;\n model.enable_view_types = Array.isArray(meta.enable_view_types) ? meta.enable_view_types : [];\n model.is_pagination = meta.is_pagination ?? true; // ★ v29.18:默认开启(与页面实际行为一致)\n model.pageSize = meta.pageSize ?? undefined;\n model.page_size_options = Array.isArray(meta.page_size_options) ? meta.page_size_options : [];\n return preserved;\n};\n\n/**\n * 提交:结构化值非空才写回,避免把「未配置」变成 null 脏数据。\n * ★ v29.13:org_filter 不再写回(引擎无消费点;存量键留在 preserved 里原样保留)。\n */\nexport const buildViewMeta = (model: any, preservedMeta: Record<string, any>) => {\n const meta: any = { ...preservedMeta };\n if (model.default_view_type) meta.default_view_type = model.default_view_type;\n else delete meta.default_view_type;\n if (model.enable_view_types && model.enable_view_types.length)\n meta.enable_view_types = model.enable_view_types;\n else delete meta.enable_view_types;\n /* ★ v29.18:default_view_type 必须在 enable_view_types 范围内(空 = 不限),\n 不在范围内的配置视为无效,落库前剔除 */\n if (\n Array.isArray(model.enable_view_types) &&\n model.enable_view_types.length &&\n meta.default_view_type &&\n !model.enable_view_types.includes(meta.default_view_type)\n ) {\n delete meta.default_view_type;\n }\n if (typeof model.is_pagination === 'boolean') meta.is_pagination = model.is_pagination;\n else meta.is_pagination = true; // ★ v29.18:默认开启\n if (model.pageSize != null) meta.pageSize = model.pageSize;\n else delete meta.pageSize;\n if (model.page_size_options && model.page_size_options.length)\n meta.page_size_options = model.page_size_options;\n else delete meta.page_size_options;\n return meta;\n};\n","<template>\n <a-modal\n :title=\"title\"\n width=\"1020px\"\n :open=\"open\"\n :bodyStyle=\"{ maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', padding: '0 24px' }\"\n :confirm-loading=\"loading\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <a-steps\n :current=\"currentStep\"\n size=\"small\"\n class=\"config-steps\"\n @change=\"onStepChange\"\n >\n <a-step title=\"基础配置\" />\n <a-step title=\"展示配置\" />\n <a-step title=\"列配置\" />\n <a-step title=\"菜单\" />\n </a-steps>\n\n <a-form\n ref=\"formRef\"\n :model=\"model\"\n layout=\"vertical\"\n >\n <!-- ═══════════ Step 1 基础配置 ═══════════ -->\n <div\n v-show=\"currentStep === 0\"\n class=\"step-pane\"\n >\n <StepBasicConfig\n v-model:sql-error=\"sqlError\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n />\n </div>\n\n <!-- ═══════════ Step 2 展示配置(view_meta 白名单透传给前端)\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.45:原第 2 步「菜单与权限」挪到末位 → 本步上移到第 2 步 ═══════════ -->\n <!-- ★ v29.13:org_filter 配置已废除(用户定案:组织过滤只靠 SQL 里手写 {{org_id}},\n 引擎不做任何特殊处理),此处不再提供配置项 -->\n <div\n v-show=\"currentStep === 1\"\n class=\"step-pane\"\n >\n <StepDisplayConfig :model=\"model\" />\n </div>\n\n <!-- ═══════════ Step 3 列配置(§3.1.4 字段配置子表:列探测刷新 → 人工覆盖)\n ★ v29.34:本步额外挂 `step-pane--col`,把「工具条 + 表格」改成 flex 纵向布局,\n 让 s-table 吃掉剩余高度(详见样式区注释),父级不再出纵向滚动条。\n ★ v29.35:这层 div 是**高度链路的第 ① 环**,必须留着——StepColumnConfig 是\n 多根(fragment)组件,它内部 `.col-toolbar` / `.col-table-wrapper` 会原样插进这里,\n 成为本 div(flex column)的直接子元素;多包一层 DOM 就会让 wrapper 的 `flex: 1`\n 失去参照。显隐也由本 div 的 v-show 负责(fragment 组件不继承 v-show / class)。\n ★ v29.45:本步由第 4 步上移到第 3 步(菜单步挪到末位)—— 显隐由 `colActive`\n 统一表达,故这次换位只需改 `colActive` 的判据,不必动这里。 ═══════════ -->\n <div\n v-show=\"colActive\"\n class=\"step-pane step-pane--col\"\n >\n <StepColumnConfig\n ref=\"colStepRef\"\n :active=\"colActive\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n @probe-start=\"onProbeStart\"\n />\n </div>\n\n <!-- ═══════════ Step 4 菜单(★ v29.45:原第 2 步「菜单与权限」挪到**末位**并改名「菜单」)\n 裁掉两项:菜单索引(sort,改由后端在新建菜单时取 max+1)、可见角色(roles,改由\n 「菜单的角色授权」承载)。只留父级路由 + 隶属项目,保存时由 save_view 同步到\n 该视图对应的菜单行(不存在则新建)。 ═══════════ -->\n <div\n v-show=\"currentStep === 3\"\n class=\"step-pane\"\n >\n <StepMenu :model=\"model\" />\n </div>\n </a-form>\n\n <template #footer>\n <a-button\n v-if=\"currentStep > 0\"\n @click=\"currentStep -= 1\"\n >\n 上一步\n </a-button>\n <a-button\n v-if=\"currentStep < 3\"\n type=\"primary\"\n ghost\n @click=\"goNext\"\n >\n 下一步\n </a-button>\n <a-button @click=\"handleCancel\">取消</a-button>\n <a-button\n type=\"primary\"\n :loading=\"loading\"\n :disabled=\"form_type !== 'edit' && currentStep < 3\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </template>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref } from 'vue';\nimport { event_obj } from 'liyu-pc-base/init-plugin';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport StepBasicConfig from 'liyu-pc-base/components/dbView/configSteps/StepBasicConfig.vue';\nimport StepDisplayConfig from 'liyu-pc-base/components/dbView/configSteps/StepDisplayConfig.vue';\nimport StepColumnConfig from 'liyu-pc-base/components/dbView/configSteps/StepColumnConfig.vue';\nimport StepMenu from 'liyu-pc-base/components/dbView/configSteps/StepMenu.vue';\nimport { STEP_FIELDS } from './configSteps/constants';\nimport { extractData } from './configSteps/helpers';\nimport { applyViewMeta, buildViewMeta } from './configSteps/viewMeta';\n\n/* ★ v29.7:db_view 的「视图配置」新增/编辑弹窗组件。\n ★ v29.8:a-tabs 改 a-steps(用户定案),并新增「列配置」步骤(§3.1.4 字段配置子表)。\n ★ v29.23(用户定案批量调整):① 「菜单与权限」独立成步(sort / parent_menu_flag /\n menu_project / roles 四项从基础配置移出)——步骤为 基础配置/菜单与权限/展示配置/列配置;\n ② 「探测列」按钮移除,列探测统一收敛到列配置步骤的「列探测刷新」(probe_columns,\n 基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并当前配置不清空;\n ③ 新建流程进入列配置步骤自动探测推断列;\n ④ 预览恒空修复(响应键 data→rows,见 runPreview 注释);\n ⑤ **保存统一走 save_view**:主表字段 + columns + deleted_data_indexes 一次提交,\n 事务内两表同落(新建/编辑共用),不再 create_db_view/update_db_view + save_columns\n 两段式;「刷新」「保存列配置」按钮随之移除。\n view_meta 不再是裸 JSON 文本框,按「实际可配置的键」拆步骤——\n ★ v29.13:父菜单 RemoteTreeSelectPro(valueKey=state)/ menu_project / order→sort /\n org_filter 废除;★ v29.18:order_json 迁列配置默认排序、max_page_size /\n pivot_table_max_records 废除。\n 未在表单展开的其余 view_meta 键(btn_list、advanced_base_conditions 等)\n 编辑时原样保留(preservedMeta 合并回提交值),不会丢失。\n\n ★ v29.35(用户定案):**按步骤拆组件**——本文件只留「外壳」:弹窗 + 步骤条 + a-form 容器\n + footer + 共享 model + 详情加载 / 提交编排。四个步骤落到 `dbView/configSteps/`:\n · StepBasicConfig.vue 基础配置(名称 / flag / 数据源 / SQL + 预览)\n · StepDisplayConfig.vue 展示配置(view_meta 白名单 5 键)\n · StepColumnConfig.vue 列配置(s-table 自适应高度 + 探测 + 两个行内模态框)\n · StepMenu.vue 菜单(父级路由 parent_menu_id / 隶属项目 menu_project)\n 外加三个纯逻辑文件:`constants.ts`(数据字典)、`helpers.ts`(extractData/toList)、\n `viewMeta.ts`(view_meta 拆包与合并,纯函数)。\n\n ★ v29.45(用户定案,菜单归属重构):步骤**顺序调整 + 裁剪** ——\n 0 基础配置 / 1 展示配置 / 2 列配置 / **3 菜单**(原第 2 步「菜单与权限」挪到末位并改名);\n 该步裁掉「菜单索引(sort)」与「可见角色(roles)」,只留「父级路由 + 隶属项目」;\n `DbView` 的 `parent_menu_flag` / `menu_project` / `sort` / `roles` 四个字段**已删除**,\n 菜单信息改由 `save_view` 同步到主服务的 `liyu_permission_menu`(详见\n `docs/design/07-menu-integration.md`)。连带把「父级路由」的取值从 **Menu.state**\n 改成 **Menu.id**(`model.parent_menu_id`,直接写 `Menu.parent_id`)。\n\n 拆分后的三条约定(改这些地方请一起看):\n ① **model 仍是唯一数据源**,由外壳持有、按对象引用传给各步骤(模板里照旧 `model.xxx`);\n 外壳整体重建对象(initForEdit 的 `model.value = {...}`)时,各步骤的 `computed(() => props.model)`\n 会跟着换到新对象(持有裸引用会失联)。\n ② **校验仍归外壳**:`rules` 就近定义在 StepBasicConfig(那 4 个字段只属于它),\n 但 `a-form` / `formRef.validate*` / `STEP_FIELDS` 都在外壳 —— a-form-item 在外壳的\n `<a-form>` 组件树内,provide/inject 照常生效。\n ③ **跨步骤的两个口子**:`sqlError`(第 1 步渲染、第 3 步探测前清)仍由外壳持有,\n 第 3 步用 `probeStart` 事件通知;列配置的提交载荷(columns / deleted_data_indexes)\n 由 StepColumnConfig 用 `defineExpose` 提供,外壳通过 `colStepRef` 取。 */\n\ninterface Props {\n title: string;\n record: any;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['cancel', 'ok']);\n\nconst formRef = ref<any>(null);\nconst loading = ref(false);\nconst open = ref(true);\n/* ★ v29.35:改为 setup 顶层初始化(原先在 onMounted 里才赋值)——四个步骤组件都会在自己的\n onMounted 里读 formType 决定加载行为,而子组件的 mounted 早于父组件的 mounted,\n 顶层初始化才能保证它们读到的一定是正确值。差异仅「挂载前就是正确的值」,无其他影响;\n onMounted 里那句同步赋值保留,兼容 record 被替换的情形。 */\nconst form_type = ref(props.record?.form_type || 'create');\nconst currentStep = ref(0);\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* 列配置步的进入标志:既控制那层 div 的显隐,也作为 `active` 传给 StepColumnConfig\n (它要在本步真正显示出来之后才挂 s-table 并自动探测,见该组件 watch(active) 的说明)。\n ★ v29.45:本步由第 4 步上移到**第 3 步**(菜单步挪到末位)→ 判据 3 改 2。\n ⚠️ 模板里那层 `v-show=\"colActive\"` 与本行是**同一个来源**,换位只改这一处;\n 但 `constants.ts` 的 `STEP_FIELDS` 是按步骤**位置**排的数组,注释必须同步。 */\nconst colActive = computed(() => currentStep.value === 2);\n\n/* ★ v29.35:第 1 步的预览错误由外壳持有——第 3 步「列探测刷新」开始前也要清它\n (原先两处是同一作用域的自由变量,拆开后第 3 步改为 emit('probeStart') 通知) */\nconst sqlError = ref('');\nconst onProbeStart = () => {\n sqlError.value = '';\n};\n\n/* ★ v29.35:第 3 步的提交取值口(columns / deleted_data_indexes 都下沉到子组件了) */\nconst colStepRef = ref<any>(null);\n\n/* ★ v29.35:这份 model 是四个步骤**共享**的表单载体(对象引用传给各步骤,它们直接改属性)。\n 字段按步骤分组:基础配置(name/flag/data_source_id/sql)、展示配置(default_view_type/\n enable_view_types/is_pagination/pageSize/page_size_options)、菜单(parent_menu_id/menu_project)。\n ★ v29.45:删 `sort` / `role_ids`(字段已从 DbView 删除);加 `parent_menu_id`\n (父级菜单的 **Menu.id**,替换原 `parent_menu_flag` 的 state 语义)。 */\nconst model = ref<any>({\n name: undefined,\n flag: undefined,\n data_source_id: undefined,\n parent_menu_id: undefined,\n menu_project: undefined,\n sql: undefined,\n // view_meta 结构化键(★ v29.18:order_json/max_page_size/pivot_table_max_records 废除——\n // order_json 迁到列配置 DbViewColumn 默认排序三字段,后两个机制删除)\n default_view_type: undefined,\n enable_view_types: [],\n is_pagination: true,\n pageSize: undefined,\n page_size_options: [],\n});\n\n/* ═══ 步骤导航:前进先校验当前步骤,后退/原地自由 ═══\n ★ v29.14(用户定案):**编辑态**允许点步骤条直达任意步骤——前提是当前步骤\n 校验通过(不通过则停留,错误由表单项显示);最终保存时 handleOk 仍会全量校验。\n 新建态维持「只许回退」。\n ★ v29.18:步骤收敛为 3 步;★ v29.23:菜单四项拆出 → 4 步\n (基础配置 / 菜单与权限 / 展示配置 / 列配置)。\n ★ v29.45(用户定案):「菜单与权限」**挪到末位**并改名「菜单」→ 现为\n (基础配置 / 展示配置 / 列配置 / 菜单),仍 4 步。\n ⚠️ footer 的 `currentStep < 3` 与「新建态须走到最后一步」的判据**都不用改**(仍是 4 步、末位仍是 3)。\n ★ v29.35:进入列配置步骤时「若无列则自动探测」已下沉到 StepColumnConfig 的 watch(active),\n 这里不再关心列数据。 */\nconst stepFields = STEP_FIELDS;\nconst goNext = async () => {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤,错误提示已由表单项显示\n }\n }\n if (currentStep.value < 3) {\n currentStep.value += 1;\n }\n};\nconst onStepChange = async (target: number) => {\n if (target <= currentStep.value) {\n currentStep.value = target; // 后退/原地自由\n return;\n }\n if (form_type.value !== 'edit') return; // 新建态:不可跳步前进\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤\n }\n }\n currentStep.value = target;\n};\n\n/* ═══ view_meta:保留键由外壳持有 ═══\n ★ v29.35:拆步骤后,view_meta 的「拆包/合并」提成纯函数放 `./configSteps/viewMeta.ts`\n (它横跨外壳与「展示配置」步,放在任一侧都会造成反向依赖)。\n 这里只留外壳需要的那一半:表单不消费的键。 */\nlet preservedMeta: Record<string, any> = {};\n\n/* ═══ 详情 / 提交 ═══ */\nconst loadDetail = async (id: any) => {\n const res = await getApiData(\n { db_view_id: id, org_id: current_org?.id },\n `${props.record.model_app}/get_db_view`,\n );\n return extractData(res) || {};\n};\n\nconst buildPayload = () => {\n const m = model.value;\n return {\n name: m.name,\n flag: m.flag,\n data_source_id: m.data_source_id,\n // ★ v29.45(用户定案):`parent_menu_flag`(旧字段已删) → `parent_menu_id`(Menu.id)。\n // null = 顶级菜单。后端据此直接写 `Menu.parent_id`,不再做 state→id 解析。\n parent_menu_id: m.parent_menu_id || null,\n menu_project: m.menu_project || null, // ★ v29.13:菜单隶属项目(区分模块)→ 写到菜单行\n sql: m.sql,\n view_meta: buildViewMeta(m, preservedMeta),\n };\n};\n\nconst handleOk = async () => {\n try {\n /* ★ v29.17:编辑态「确定」任意步骤可点——只校验当前步骤的表单项(其余步骤的\n 错误不可见,全量校验会让用户困惑);新建态仍须走到最后一步,维持全量校验兜底。 */\n if (form_type.value === 'edit') {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) await formRef.value?.validateFields(fields);\n } else {\n await formRef.value.validate();\n }\n /* ★ v29.23(用户定案):视图主表与列配置【同时保存】——统一走 save_view\n (事务内两表同落,新建/编辑共用),不再分 create_db_view + save_columns 两次请求。\n ★ v29.35:columns / deleted_data_indexes 改由 StepColumnConfig 提供(ref 调 expose 的方法)。 */\n const payload: any = {\n ...buildPayload(),\n columns: colStepRef.value?.buildColumnsPayload?.() ?? [],\n deleted_data_indexes: colStepRef.value?.getDeletedIndexes?.() ?? [],\n };\n if (form_type.value === 'edit') {\n payload.db_view_id = props.record.id;\n }\n loading.value = true;\n await getApiData(\n { ...payload, org_id: current_org?.id },\n `${props.record.model_app}/save_view`,\n );\n message.success('操作成功');\n colStepRef.value?.resetDeletedIndexes?.();\n event_obj()?.emit('refresh');\n emit('ok');\n } catch (error) {\n // 校验失败或接口异常:保持弹窗打开\n } finally {\n loading.value = false;\n }\n};\nconst handleCancel = () => emit('cancel');\n\n/* ★ v29.45(用户定案):菜单两项**不再来自 DbView**(`parent_menu_flag` / `menu_project`\n 两个字段已从模型删除),改从「该视图对应的菜单行」读 —— `/db_view/get_db_view_menu`。\n ⚠️ 必须在 `loadDetail()` 与下面那次整体赋值**之后**再回填:`initForEdit` 会用\n `model.value = {...}` 重建对象,顺序反了这两项会被覆盖掉。\n ⚠️ 走 `skip_error: true` + try/catch:菜单回显失败**不该阻塞编辑**(留空即可,\n 保存时后端会按新值写/建菜单)。 */\nconst loadMenuInfo = async (flag: any) => {\n if (!flag) return;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, flag },\n `${props.record.model_app}/get_db_view_menu`,\n false,\n { skip_error: true },\n );\n const data = extractData(res) || {};\n if (!data.exists) return; // 视图还没保存过菜单 / 菜单已被删除 → 两项留空\n model.value.parent_menu_id = data.parent_id ?? undefined;\n model.value.menu_project = data.menu_project || undefined;\n } catch (e) {\n // 保持留空\n }\n};\n\nconst initForEdit = async () => {\n const id = props.record?.id;\n const record = (id != null ? await loadDetail(id) : props.record) || {};\n model.value = {\n ...model.value,\n name: record.name || undefined,\n flag: record.flag || undefined,\n data_source_id: record.data_source_id ?? undefined,\n sql: record.sql || undefined,\n };\n // ★ v29.35:拆包结果写回共享 model,其余键留在 preservedMeta(提交时原样合并)\n preservedMeta = applyViewMeta(model.value, record.view_meta || {});\n // ★ v29.45:菜单两项(父级 / 隶属项目)最后回填(必须在上面两次赋值之后)\n await loadMenuInfo(model.value.flag);\n};\n\nonMounted(async () => {\n if (props.record?.form_type) form_type.value = props.record.form_type;\n /* ★ v29.35:数据源下拉 / 角色列表 / 列配置预取 / 新建自动生成 flag 都已随各自步骤下沉,\n 各自的 mounted 里自行加载;外壳只剩「编辑态拉详情」这一步。 */\n if (form_type.value === 'edit') {\n await initForEdit();\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.config-steps {\n padding: 12px 0 4px;\n}\n/* ★ v29.17.2:步骤内容区固定高度(每步内部滚动),切换步骤时弹窗总高不跳变 */\n.step-pane {\n height: calc(80vh - 150px);\n min-height: 360px;\n overflow-y: auto;\n overflow-x: hidden; /* ★ gutter 负 margin / 表格 min-width 等横向溢出不外溢 */\n padding-right: 4px;\n}\n/* ★ v29.34:列配置步专用——工具条固定在上、表格吃满剩余高度,**整个面板只留 s-table 一根滚动条**。\n ① 必须把基础 `.step-pane` 的 `overflow-y: auto` 收掉:否则出现的是「外层滚动条」,\n 表头会跟着内容滚走,`flex: 1` 也就没有「剩余高度」可解析,自动高度链路从这一环断。\n 纵向交给 s-table 表体、横向交给 `scroll.x`。\n ② 选择器刻意写成**复合** `.step-pane.step-pane--col`(0,2,0)而不是 `.step-pane--col`(0,1,0):\n 后者与 `.step-pane` 特异度打平,`overflow` 能否压住 `overflow-y: auto` 就**只取决于两个样式块的\n 先后顺序**——将来有人调整顺序就会静默退回 auto(多出一根滚动条、表格高度失去意义)。\n 与抽屉那次 `.csc-drawer.ant-drawer .ant-drawer-body` 是同一类坑,写成复合/三段式即与顺序无关。\n ★ v29.35:本 div 的直接子元素就是 StepColumnConfig 内部的 `.col-toolbar` 与\n `.col-table-wrapper`(fragment 组件不产生额外 DOM),故 `flex: 1` 的参照仍是它。 */\n.step-pane.step-pane--col {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n</style>\n","<template>\n <a-modal\n :title=\"title\"\n width=\"1020px\"\n :open=\"open\"\n :bodyStyle=\"{ maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', padding: '0 24px' }\"\n :confirm-loading=\"loading\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <a-steps\n :current=\"currentStep\"\n size=\"small\"\n class=\"config-steps\"\n @change=\"onStepChange\"\n >\n <a-step title=\"基础配置\" />\n <a-step title=\"展示配置\" />\n <a-step title=\"列配置\" />\n <a-step title=\"菜单\" />\n </a-steps>\n\n <a-form\n ref=\"formRef\"\n :model=\"model\"\n layout=\"vertical\"\n >\n <!-- ═══════════ Step 1 基础配置 ═══════════ -->\n <div\n v-show=\"currentStep === 0\"\n class=\"step-pane\"\n >\n <StepBasicConfig\n v-model:sql-error=\"sqlError\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n />\n </div>\n\n <!-- ═══════════ Step 2 展示配置(view_meta 白名单透传给前端)\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.45:原第 2 步「菜单与权限」挪到末位 → 本步上移到第 2 步 ═══════════ -->\n <!-- ★ v29.13:org_filter 配置已废除(用户定案:组织过滤只靠 SQL 里手写 {{org_id}},\n 引擎不做任何特殊处理),此处不再提供配置项 -->\n <div\n v-show=\"currentStep === 1\"\n class=\"step-pane\"\n >\n <StepDisplayConfig :model=\"model\" />\n </div>\n\n <!-- ═══════════ Step 3 列配置(§3.1.4 字段配置子表:列探测刷新 → 人工覆盖)\n ★ v29.34:本步额外挂 `step-pane--col`,把「工具条 + 表格」改成 flex 纵向布局,\n 让 s-table 吃掉剩余高度(详见样式区注释),父级不再出纵向滚动条。\n ★ v29.35:这层 div 是**高度链路的第 ① 环**,必须留着——StepColumnConfig 是\n 多根(fragment)组件,它内部 `.col-toolbar` / `.col-table-wrapper` 会原样插进这里,\n 成为本 div(flex column)的直接子元素;多包一层 DOM 就会让 wrapper 的 `flex: 1`\n 失去参照。显隐也由本 div 的 v-show 负责(fragment 组件不继承 v-show / class)。\n ★ v29.45:本步由第 4 步上移到第 3 步(菜单步挪到末位)—— 显隐由 `colActive`\n 统一表达,故这次换位只需改 `colActive` 的判据,不必动这里。 ═══════════ -->\n <div\n v-show=\"colActive\"\n class=\"step-pane step-pane--col\"\n >\n <StepColumnConfig\n ref=\"colStepRef\"\n :active=\"colActive\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n @probe-start=\"onProbeStart\"\n />\n </div>\n\n <!-- ═══════════ Step 4 菜单(★ v29.45:原第 2 步「菜单与权限」挪到**末位**并改名「菜单」)\n 裁掉两项:菜单索引(sort,改由后端在新建菜单时取 max+1)、可见角色(roles,改由\n 「菜单的角色授权」承载)。只留父级路由 + 隶属项目,保存时由 save_view 同步到\n 该视图对应的菜单行(不存在则新建)。 ═══════════ -->\n <div\n v-show=\"currentStep === 3\"\n class=\"step-pane\"\n >\n <StepMenu :model=\"model\" />\n </div>\n </a-form>\n\n <template #footer>\n <a-button\n v-if=\"currentStep > 0\"\n @click=\"currentStep -= 1\"\n >\n 上一步\n </a-button>\n <a-button\n v-if=\"currentStep < 3\"\n type=\"primary\"\n ghost\n @click=\"goNext\"\n >\n 下一步\n </a-button>\n <a-button @click=\"handleCancel\">取消</a-button>\n <a-button\n type=\"primary\"\n :loading=\"loading\"\n :disabled=\"form_type !== 'edit' && currentStep < 3\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </template>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref } from 'vue';\nimport { event_obj } from 'liyu-pc-base/init-plugin';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport StepBasicConfig from 'liyu-pc-base/components/dbView/configSteps/StepBasicConfig.vue';\nimport StepDisplayConfig from 'liyu-pc-base/components/dbView/configSteps/StepDisplayConfig.vue';\nimport StepColumnConfig from 'liyu-pc-base/components/dbView/configSteps/StepColumnConfig.vue';\nimport StepMenu from 'liyu-pc-base/components/dbView/configSteps/StepMenu.vue';\nimport { STEP_FIELDS } from './configSteps/constants';\nimport { extractData } from './configSteps/helpers';\nimport { applyViewMeta, buildViewMeta } from './configSteps/viewMeta';\n\n/* ★ v29.7:db_view 的「视图配置」新增/编辑弹窗组件。\n ★ v29.8:a-tabs 改 a-steps(用户定案),并新增「列配置」步骤(§3.1.4 字段配置子表)。\n ★ v29.23(用户定案批量调整):① 「菜单与权限」独立成步(sort / parent_menu_flag /\n menu_project / roles 四项从基础配置移出)——步骤为 基础配置/菜单与权限/展示配置/列配置;\n ② 「探测列」按钮移除,列探测统一收敛到列配置步骤的「列探测刷新」(probe_columns,\n 基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并当前配置不清空;\n ③ 新建流程进入列配置步骤自动探测推断列;\n ④ 预览恒空修复(响应键 data→rows,见 runPreview 注释);\n ⑤ **保存统一走 save_view**:主表字段 + columns + deleted_data_indexes 一次提交,\n 事务内两表同落(新建/编辑共用),不再 create_db_view/update_db_view + save_columns\n 两段式;「刷新」「保存列配置」按钮随之移除。\n view_meta 不再是裸 JSON 文本框,按「实际可配置的键」拆步骤——\n ★ v29.13:父菜单 RemoteTreeSelectPro(valueKey=state)/ menu_project / order→sort /\n org_filter 废除;★ v29.18:order_json 迁列配置默认排序、max_page_size /\n pivot_table_max_records 废除。\n 未在表单展开的其余 view_meta 键(btn_list、advanced_base_conditions 等)\n 编辑时原样保留(preservedMeta 合并回提交值),不会丢失。\n\n ★ v29.35(用户定案):**按步骤拆组件**——本文件只留「外壳」:弹窗 + 步骤条 + a-form 容器\n + footer + 共享 model + 详情加载 / 提交编排。四个步骤落到 `dbView/configSteps/`:\n · StepBasicConfig.vue 基础配置(名称 / flag / 数据源 / SQL + 预览)\n · StepDisplayConfig.vue 展示配置(view_meta 白名单 5 键)\n · StepColumnConfig.vue 列配置(s-table 自适应高度 + 探测 + 两个行内模态框)\n · StepMenu.vue 菜单(父级路由 parent_menu_id / 隶属项目 menu_project)\n 外加三个纯逻辑文件:`constants.ts`(数据字典)、`helpers.ts`(extractData/toList)、\n `viewMeta.ts`(view_meta 拆包与合并,纯函数)。\n\n ★ v29.45(用户定案,菜单归属重构):步骤**顺序调整 + 裁剪** ——\n 0 基础配置 / 1 展示配置 / 2 列配置 / **3 菜单**(原第 2 步「菜单与权限」挪到末位并改名);\n 该步裁掉「菜单索引(sort)」与「可见角色(roles)」,只留「父级路由 + 隶属项目」;\n `DbView` 的 `parent_menu_flag` / `menu_project` / `sort` / `roles` 四个字段**已删除**,\n 菜单信息改由 `save_view` 同步到主服务的 `liyu_permission_menu`(详见\n `docs/design/07-menu-integration.md`)。连带把「父级路由」的取值从 **Menu.state**\n 改成 **Menu.id**(`model.parent_menu_id`,直接写 `Menu.parent_id`)。\n\n 拆分后的三条约定(改这些地方请一起看):\n ① **model 仍是唯一数据源**,由外壳持有、按对象引用传给各步骤(模板里照旧 `model.xxx`);\n 外壳整体重建对象(initForEdit 的 `model.value = {...}`)时,各步骤的 `computed(() => props.model)`\n 会跟着换到新对象(持有裸引用会失联)。\n ② **校验仍归外壳**:`rules` 就近定义在 StepBasicConfig(那 4 个字段只属于它),\n 但 `a-form` / `formRef.validate*` / `STEP_FIELDS` 都在外壳 —— a-form-item 在外壳的\n `<a-form>` 组件树内,provide/inject 照常生效。\n ③ **跨步骤的两个口子**:`sqlError`(第 1 步渲染、第 3 步探测前清)仍由外壳持有,\n 第 3 步用 `probeStart` 事件通知;列配置的提交载荷(columns / deleted_data_indexes)\n 由 StepColumnConfig 用 `defineExpose` 提供,外壳通过 `colStepRef` 取。 */\n\ninterface Props {\n title: string;\n record: any;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['cancel', 'ok']);\n\nconst formRef = ref<any>(null);\nconst loading = ref(false);\nconst open = ref(true);\n/* ★ v29.35:改为 setup 顶层初始化(原先在 onMounted 里才赋值)——四个步骤组件都会在自己的\n onMounted 里读 formType 决定加载行为,而子组件的 mounted 早于父组件的 mounted,\n 顶层初始化才能保证它们读到的一定是正确值。差异仅「挂载前就是正确的值」,无其他影响;\n onMounted 里那句同步赋值保留,兼容 record 被替换的情形。 */\nconst form_type = ref(props.record?.form_type || 'create');\nconst currentStep = ref(0);\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* 列配置步的进入标志:既控制那层 div 的显隐,也作为 `active` 传给 StepColumnConfig\n (它要在本步真正显示出来之后才挂 s-table 并自动探测,见该组件 watch(active) 的说明)。\n ★ v29.45:本步由第 4 步上移到**第 3 步**(菜单步挪到末位)→ 判据 3 改 2。\n ⚠️ 模板里那层 `v-show=\"colActive\"` 与本行是**同一个来源**,换位只改这一处;\n 但 `constants.ts` 的 `STEP_FIELDS` 是按步骤**位置**排的数组,注释必须同步。 */\nconst colActive = computed(() => currentStep.value === 2);\n\n/* ★ v29.35:第 1 步的预览错误由外壳持有——第 3 步「列探测刷新」开始前也要清它\n (原先两处是同一作用域的自由变量,拆开后第 3 步改为 emit('probeStart') 通知) */\nconst sqlError = ref('');\nconst onProbeStart = () => {\n sqlError.value = '';\n};\n\n/* ★ v29.35:第 3 步的提交取值口(columns / deleted_data_indexes 都下沉到子组件了) */\nconst colStepRef = ref<any>(null);\n\n/* ★ v29.35:这份 model 是四个步骤**共享**的表单载体(对象引用传给各步骤,它们直接改属性)。\n 字段按步骤分组:基础配置(name/flag/data_source_id/sql)、展示配置(default_view_type/\n enable_view_types/is_pagination/pageSize/page_size_options)、菜单(parent_menu_id/menu_project)。\n ★ v29.45:删 `sort` / `role_ids`(字段已从 DbView 删除);加 `parent_menu_id`\n (父级菜单的 **Menu.id**,替换原 `parent_menu_flag` 的 state 语义)。 */\nconst model = ref<any>({\n name: undefined,\n flag: undefined,\n data_source_id: undefined,\n parent_menu_id: undefined,\n menu_project: undefined,\n sql: undefined,\n // view_meta 结构化键(★ v29.18:order_json/max_page_size/pivot_table_max_records 废除——\n // order_json 迁到列配置 DbViewColumn 默认排序三字段,后两个机制删除)\n default_view_type: undefined,\n enable_view_types: [],\n is_pagination: true,\n pageSize: undefined,\n page_size_options: [],\n});\n\n/* ═══ 步骤导航:前进先校验当前步骤,后退/原地自由 ═══\n ★ v29.14(用户定案):**编辑态**允许点步骤条直达任意步骤——前提是当前步骤\n 校验通过(不通过则停留,错误由表单项显示);最终保存时 handleOk 仍会全量校验。\n 新建态维持「只许回退」。\n ★ v29.18:步骤收敛为 3 步;★ v29.23:菜单四项拆出 → 4 步\n (基础配置 / 菜单与权限 / 展示配置 / 列配置)。\n ★ v29.45(用户定案):「菜单与权限」**挪到末位**并改名「菜单」→ 现为\n (基础配置 / 展示配置 / 列配置 / 菜单),仍 4 步。\n ⚠️ footer 的 `currentStep < 3` 与「新建态须走到最后一步」的判据**都不用改**(仍是 4 步、末位仍是 3)。\n ★ v29.35:进入列配置步骤时「若无列则自动探测」已下沉到 StepColumnConfig 的 watch(active),\n 这里不再关心列数据。 */\nconst stepFields = STEP_FIELDS;\nconst goNext = async () => {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤,错误提示已由表单项显示\n }\n }\n if (currentStep.value < 3) {\n currentStep.value += 1;\n }\n};\nconst onStepChange = async (target: number) => {\n if (target <= currentStep.value) {\n currentStep.value = target; // 后退/原地自由\n return;\n }\n if (form_type.value !== 'edit') return; // 新建态:不可跳步前进\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤\n }\n }\n currentStep.value = target;\n};\n\n/* ═══ view_meta:保留键由外壳持有 ═══\n ★ v29.35:拆步骤后,view_meta 的「拆包/合并」提成纯函数放 `./configSteps/viewMeta.ts`\n (它横跨外壳与「展示配置」步,放在任一侧都会造成反向依赖)。\n 这里只留外壳需要的那一半:表单不消费的键。 */\nlet preservedMeta: Record<string, any> = {};\n\n/* ═══ 详情 / 提交 ═══ */\nconst loadDetail = async (id: any) => {\n const res = await getApiData(\n { db_view_id: id, org_id: current_org?.id },\n `${props.record.model_app}/get_db_view`,\n );\n return extractData(res) || {};\n};\n\nconst buildPayload = () => {\n const m = model.value;\n return {\n name: m.name,\n flag: m.flag,\n data_source_id: m.data_source_id,\n // ★ v29.45(用户定案):`parent_menu_flag`(旧字段已删) → `parent_menu_id`(Menu.id)。\n // null = 顶级菜单。后端据此直接写 `Menu.parent_id`,不再做 state→id 解析。\n parent_menu_id: m.parent_menu_id || null,\n menu_project: m.menu_project || null, // ★ v29.13:菜单隶属项目(区分模块)→ 写到菜单行\n sql: m.sql,\n view_meta: buildViewMeta(m, preservedMeta),\n };\n};\n\nconst handleOk = async () => {\n try {\n /* ★ v29.17:编辑态「确定」任意步骤可点——只校验当前步骤的表单项(其余步骤的\n 错误不可见,全量校验会让用户困惑);新建态仍须走到最后一步,维持全量校验兜底。 */\n if (form_type.value === 'edit') {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) await formRef.value?.validateFields(fields);\n } else {\n await formRef.value.validate();\n }\n /* ★ v29.23(用户定案):视图主表与列配置【同时保存】——统一走 save_view\n (事务内两表同落,新建/编辑共用),不再分 create_db_view + save_columns 两次请求。\n ★ v29.35:columns / deleted_data_indexes 改由 StepColumnConfig 提供(ref 调 expose 的方法)。 */\n const payload: any = {\n ...buildPayload(),\n columns: colStepRef.value?.buildColumnsPayload?.() ?? [],\n deleted_data_indexes: colStepRef.value?.getDeletedIndexes?.() ?? [],\n };\n if (form_type.value === 'edit') {\n payload.db_view_id = props.record.id;\n }\n loading.value = true;\n await getApiData(\n { ...payload, org_id: current_org?.id },\n `${props.record.model_app}/save_view`,\n );\n message.success('操作成功');\n colStepRef.value?.resetDeletedIndexes?.();\n event_obj()?.emit('refresh');\n emit('ok');\n } catch (error) {\n // 校验失败或接口异常:保持弹窗打开\n } finally {\n loading.value = false;\n }\n};\nconst handleCancel = () => emit('cancel');\n\n/* ★ v29.45(用户定案):菜单两项**不再来自 DbView**(`parent_menu_flag` / `menu_project`\n 两个字段已从模型删除),改从「该视图对应的菜单行」读 —— `/db_view/get_db_view_menu`。\n ⚠️ 必须在 `loadDetail()` 与下面那次整体赋值**之后**再回填:`initForEdit` 会用\n `model.value = {...}` 重建对象,顺序反了这两项会被覆盖掉。\n ⚠️ 走 `skip_error: true` + try/catch:菜单回显失败**不该阻塞编辑**(留空即可,\n 保存时后端会按新值写/建菜单)。 */\nconst loadMenuInfo = async (flag: any) => {\n if (!flag) return;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, flag },\n `${props.record.model_app}/get_db_view_menu`,\n false,\n { skip_error: true },\n );\n const data = extractData(res) || {};\n if (!data.exists) return; // 视图还没保存过菜单 / 菜单已被删除 → 两项留空\n model.value.parent_menu_id = data.parent_id ?? undefined;\n model.value.menu_project = data.menu_project || undefined;\n } catch (e) {\n // 保持留空\n }\n};\n\nconst initForEdit = async () => {\n const id = props.record?.id;\n const record = (id != null ? await loadDetail(id) : props.record) || {};\n model.value = {\n ...model.value,\n name: record.name || undefined,\n flag: record.flag || undefined,\n data_source_id: record.data_source_id ?? undefined,\n sql: record.sql || undefined,\n };\n // ★ v29.35:拆包结果写回共享 model,其余键留在 preservedMeta(提交时原样合并)\n preservedMeta = applyViewMeta(model.value, record.view_meta || {});\n // ★ v29.45:菜单两项(父级 / 隶属项目)最后回填(必须在上面两次赋值之后)\n await loadMenuInfo(model.value.flag);\n};\n\nonMounted(async () => {\n if (props.record?.form_type) form_type.value = props.record.form_type;\n /* ★ v29.35:数据源下拉 / 角色列表 / 列配置预取 / 新建自动生成 flag 都已随各自步骤下沉,\n 各自的 mounted 里自行加载;外壳只剩「编辑态拉详情」这一步。 */\n if (form_type.value === 'edit') {\n await initForEdit();\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.config-steps {\n padding: 12px 0 4px;\n}\n/* ★ v29.17.2:步骤内容区固定高度(每步内部滚动),切换步骤时弹窗总高不跳变 */\n.step-pane {\n height: calc(80vh - 150px);\n min-height: 360px;\n overflow-y: auto;\n overflow-x: hidden; /* ★ gutter 负 margin / 表格 min-width 等横向溢出不外溢 */\n padding-right: 4px;\n}\n/* ★ v29.34:列配置步专用——工具条固定在上、表格吃满剩余高度,**整个面板只留 s-table 一根滚动条**。\n ① 必须把基础 `.step-pane` 的 `overflow-y: auto` 收掉:否则出现的是「外层滚动条」,\n 表头会跟着内容滚走,`flex: 1` 也就没有「剩余高度」可解析,自动高度链路从这一环断。\n 纵向交给 s-table 表体、横向交给 `scroll.x`。\n ② 选择器刻意写成**复合** `.step-pane.step-pane--col`(0,2,0)而不是 `.step-pane--col`(0,1,0):\n 后者与 `.step-pane` 特异度打平,`overflow` 能否压住 `overflow-y: auto` 就**只取决于两个样式块的\n 先后顺序**——将来有人调整顺序就会静默退回 auto(多出一根滚动条、表格高度失去意义)。\n 与抽屉那次 `.csc-drawer.ant-drawer .ant-drawer-body` 是同一类坑,写成复合/三段式即与顺序无关。\n ★ v29.35:本 div 的直接子元素就是 StepColumnConfig 内部的 `.col-toolbar` 与\n `.col-table-wrapper`(fragment 组件不产生额外 DOM),故 `flex: 1` 的参照仍是它。 */\n.step-pane.step-pane--col {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"dbViewConfig-BF0QZ_mT.js","names":[],"sources":["../src/components/dbView/configSteps/helpers.ts","../src/components/dbView/configSteps/StepBasicConfig.vue","../src/components/dbView/configSteps/StepBasicConfig.vue","../src/components/dbView/configSteps/constants.ts","../src/components/dbView/configSteps/StepDisplayConfig.vue","../src/components/dbView/configSteps/StepDisplayConfig.vue","../src/components/dbView/ColumnMapColorModal.vue","../src/components/dbView/ColumnMapColorModal.vue","../src/components/dbView/searchComponentSetting/constants.ts","../src/components/dbView/searchComponentSetting/BasicConfig.vue","../src/components/dbView/searchComponentSetting/BasicConfig.vue","../src/components/dbView/searchComponentSetting/OptionKeysConfig.vue","../src/components/dbView/searchComponentSetting/OptionKeysConfig.vue","../src/components/dbView/searchComponentSetting/OptionListConfig.vue","../src/components/dbView/searchComponentSetting/OptionListConfig.vue","../src/components/dbView/searchComponentSetting/PostUrlField.vue","../src/components/dbView/searchComponentSetting/PostUrlField.vue","../src/components/dbView/searchComponentSetting/QueryParamsConfig.vue","../src/components/dbView/searchComponentSetting/QueryParamsConfig.vue","../src/components/dbView/searchComponentSetting/SelectionModeConfig.vue","../src/components/dbView/searchComponentSetting/SelectionModeConfig.vue","../src/components/dbView/searchComponentSetting/ShowSearchConfig.vue","../src/components/dbView/searchComponentSetting/ShowSearchConfig.vue","../src/components/dbView/searchComponentSetting/DataSourcePanel.vue","../src/components/dbView/searchComponentSetting/DataSourcePanel.vue","../src/components/dbView/searchComponentSetting/DateFormatConfig.vue","../src/components/dbView/searchComponentSetting/DateFormatConfig.vue","../src/components/dbView/searchComponentSetting/settings/DateSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/DateSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/FlatSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/FlatSelectSetting.vue","../src/components/dbView/searchComponentSetting/NumberRangeConfig.vue","../src/components/dbView/searchComponentSetting/NumberRangeConfig.vue","../src/components/dbView/searchComponentSetting/TextAddonConfig.vue","../src/components/dbView/searchComponentSetting/TextAddonConfig.vue","../src/components/dbView/searchComponentSetting/settings/NumberInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/NumberInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/RemoteSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/RemoteSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/TextInputSetting.vue","../src/components/dbView/searchComponentSetting/settings/TextInputSetting.vue","../src/components/dbView/searchComponentSetting/TreeDataConfig.vue","../src/components/dbView/searchComponentSetting/TreeDataConfig.vue","../src/components/dbView/searchComponentSetting/settings/TreeSelectSetting.vue","../src/components/dbView/searchComponentSetting/settings/TreeSelectSetting.vue","../src/components/dbView/ColumnSearchComponentModal.vue","../src/components/dbView/ColumnSearchComponentModal.vue","../src/components/dbView/configSteps/StepColumnConfig.vue","../src/components/dbView/configSteps/StepColumnConfig.vue","../src/components/dbView/configSteps/StepMenu.vue","../src/components/dbView/configSteps/StepMenu.vue","../src/components/dbView/configSteps/viewMeta.ts","../src/components/dbView/dbViewConfig.vue","../src/components/dbView/dbViewConfig.vue"],"sourcesContent":["/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分(`dbView/configSteps/`)时抽出的\n 跨步骤共用小工具。只放**两个以上步骤都会用**的纯函数,只被单步用到的就近放在该组件里。 */\n\n/* 兼容 getApiData 可能的返回结构(直接对象/数组 或 {data:...})\n ⚠️ v29.23 的血泪教训:自建接口的响应负载键**不要叫 `data`**——宿主 request 层会直接回吐\n result 负载,此处以 `res.data` 判信封,负载键恰好也叫 data 时行数组会被二次解包。 */\nexport const extractData = (res: any) => {\n if (res && typeof res === 'object' && !Array.isArray(res) && res.data !== undefined)\n return res.data;\n return res;\n};\n\n/** 一律归一成数组(拿不到就返回空数组,调用方不用再判) */\nexport const toList = (res: any): any[] => {\n const d = extractData(res);\n return Array.isArray(d) ? d : [];\n};\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图名称\"\n name=\"name\"\n :rules=\"rules.name\"\n >\n <a-input\n v-model:value=\"model.name\"\n placeholder=\"请填写视图名称\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图标识\"\n name=\"flag\"\n :rules=\"rules.flag\"\n extra=\"全库唯一;仅英文大小写字母和数字(禁下划线/横杠),用于 URL 与菜单\"\n >\n <a-input\n v-model:value=\"model.flag\"\n placeholder=\"如 salesOrderList\"\n allow-clear\n >\n <template\n v-if=\"form_type === 'create'\"\n #suffix\n >\n <a-button\n type=\"link\"\n size=\"small\"\n style=\"padding: 0\"\n :loading=\"generatingFlag\"\n @click=\"genFlag\"\n >\n 换一个\n </a-button>\n </template>\n </a-input>\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"数据源\"\n name=\"data_source_id\"\n :rules=\"rules.data_source_id\"\n >\n <a-select\n v-model:value=\"model.data_source_id\"\n placeholder=\"请选择数据源\"\n show-search\n option-filter-prop=\"label\"\n :options=\"dataSourceOptions\"\n :loading=\"dsLoading\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <a-form-item\n label=\"查询SQL\"\n name=\"sql\"\n :rules=\"rules.sql\"\n extra=\"只读查询;可用模板变量 {{org_id}}、{{person_id}}(jinja2 渲染)\"\n >\n <a-textarea\n v-model:value=\"model.sql\"\n :rows=\"8\"\n placeholder=\"SELECT id, name FROM t_order WHERE org_id = {{org_id}}\"\n />\n </a-form-item>\n <div class=\"sql-toolbar\">\n <!-- ★ v29.23(用户定案):「探测列」按钮移除——列探测统一收敛到列配置步骤的\n 「列探测刷新」(基于此处 SQL+数据源实时探测并合并配置) -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"previewing\"\n @click=\"runPreview\"\n >\n 预览(前 100 行)\n </a-button>\n <span class=\"meta-tip\">先选数据源并填写 SQL;预览走配置期接口,不落库</span>\n </div>\n <a-alert\n v-if=\"sqlError\"\n type=\"error\"\n show-icon\n :message=\"sqlError\"\n class=\"sql-alert\"\n />\n <div\n v-if=\"previewRows.length\"\n class=\"preview-block\"\n >\n <div class=\"meta-tip\">预览结果:共 {{ previewTotal }} 行(最多回传 100 行)</div>\n <a-table\n :columns=\"previewCols\"\n :data-source=\"previewRows\"\n row-key=\"__i\"\n size=\"small\"\n :pagination=\"false\"\n :scroll=\"{ x: 640, y: 220 }\"\n />\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { extractData, toList } from './helpers';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「基础配置」步(Step 1)。\n 本步持有:视图名称 / 视图标识(flag,含「换一个」) / 数据源 / 查询SQL + 预览结果。\n\n 两条拆分约定(四个步骤组件一致):\n ① `model` 仍由外壳持有并原样传进来(**对象引用共享**:外壳 `model.value` 与这里\n `props.model` 是同一个对象)→ 模板里照旧写 `model.xxx`,v-model 直接改属性即可。\n 外壳会整个换掉对象(编辑态 `initForEdit` 用 `model.value = {...}` 重建),\n 故此处用 `computed(() => props.model)` 取用,才能跟着换到新对象(持有裸引用会失联)。\n eslint 的 `vue/no-mutating-props` 不会误报(模板写的是 `model.xxx`,不是 `props.model.xxx`)。\n ② 校验用的 `rules` 只服务本步这四个字段 → 就近定义在这里;`a-form` 仍是外壳的,\n 本组件的 `a-form-item` 在它内部 —— provide/inject 走组件树,外壳 `validateFields` 照样找得到。 */\n\ninterface Props {\n model: any;\n record: any;\n formType: string;\n /** 预览错误由外壳持有:第 3 步「列探测刷新」开始前也要清它(StepColumnConfig 会 emit probeStart) */\n sqlError: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['update:sqlError']);\n\nconst model = computed(() => props.model);\nconst form_type = computed(() => props.formType);\n\n/* 预览错误:写成「可写 computed」代理到外壳,本组件内部照旧 `sqlError.value = ''` 赋值,\n 不必在每处写 emit(原实现的读写点一个没动)。 */\nconst sqlError = computed<string>({\n get: () => props.sqlError,\n set: (v: string) => emit('update:sqlError', v),\n});\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\nconst rules: any = {\n name: [{ required: true, message: '请填写视图名称' }],\n flag: [\n { required: true, message: '请填写视图标识' },\n { pattern: /^[A-Za-z0-9]+$/, message: '仅英文大小写字母和数字,禁下划线/横杠' },\n ],\n data_source_id: [{ required: true, message: '请选择数据源' }],\n sql: [{ required: true, message: '请填写查询SQL' }],\n};\n\n/* ═══ 下拉数据源 ═══ */\nconst dataSourceOptions = ref<any[]>([]);\nconst dsLoading = ref(false);\n\nconst loadDataSources = async () => {\n dsLoading.value = true;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, is_pagination: false },\n `${props.record.model_app}/query_db_data_source_list`,\n );\n dataSourceOptions.value = toList(res).map((it: any) => ({\n value: it.id,\n label: it.name,\n }));\n } catch (e) {\n dataSourceOptions.value = [];\n } finally {\n dsLoading.value = false;\n }\n};\n\n/* ★ v29.12(用户定案):新建视图时自动调 gen_view_flag 生成唯一默认 flag(用户仍可手改/换一个) */\nconst generatingFlag = ref(false);\nconst genFlag = async () => {\n generatingFlag.value = true;\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id },\n `${props.record.model_app}/gen_view_flag`,\n );\n const payload = extractData(res);\n if (payload?.flag) {\n model.value.flag = payload.flag;\n } else {\n message.warning((res && res.message) || '生成视图标识失败,请手动填写');\n }\n } catch (e: any) {\n message.warning(e?.message || '生成视图标识失败,请手动填写');\n } finally {\n generatingFlag.value = false;\n }\n};\n\n/* ═══ SQL 配置期验证:预览(前 100 行)═══\n ★ v29.23:probeCols / runProbe(探测列按钮)移除——探测统一收敛到列配置步骤的\n 列探测刷新(runProbeRefresh)。\n ★ v29.23 预览恒空修复:后端响应键 `data` → `rows`——宿主 request 层直接回吐\n result 负载,extractData 以 res.data 判信封,负载键恰叫 data 时行数组被误当\n 信封二次解包 → payload 变数组 → 恒「结果为空」。 */\nconst previewing = ref(false);\nconst previewRows = ref<any[]>([]);\nconst previewCols = ref<any[]>([]);\nconst previewTotal = ref(0);\n\n/* SQL / 数据源变化后旧结果即失效,直接清掉避免误导 */\nwatch(\n () => [model.value.sql, model.value.data_source_id],\n () => {\n sqlError.value = '';\n previewRows.value = [];\n previewCols.value = [];\n previewTotal.value = 0;\n },\n);\n\nconst runPreview = async () => {\n if (!model.value.data_source_id || !model.value.sql) return;\n previewing.value = true;\n sqlError.value = '';\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/preview_sql`,\n );\n const payload = extractData(res);\n const rows: any[] = Array.isArray(payload?.rows) ? payload.rows : [];\n if (res && res.success === false) {\n sqlError.value = res.message || 'SQL 执行失败';\n previewRows.value = [];\n } else if (!rows.length) {\n sqlError.value = 'SQL 执行成功,但结果为空(0 行)';\n previewRows.value = [];\n previewTotal.value = 0;\n } else {\n previewRows.value = rows.map((r: any, i: number) => ({ ...r, __i: i }));\n previewCols.value = Object.keys(rows[0] || {}).map(k => ({\n title: k,\n dataIndex: k,\n key: k,\n width: 160,\n ellipsis: true,\n }));\n previewTotal.value = payload?.total ?? rows.length;\n }\n } catch (e: any) {\n sqlError.value = e?.message || 'SQL 预览失败';\n previewRows.value = [];\n } finally {\n previewing.value = false;\n }\n};\n\nonMounted(() => {\n loadDataSources();\n /* ★ v29.12:新建流程自动生成唯一默认 flag(原先在外壳 onMounted 的 create 分支里,\n 现随本步骤下沉;外壳的 formType 在 setup 顶层就已初始化,此处读到的一定是正确值) */\n if (props.formType !== 'edit') genFlag();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.sql-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n .meta-tip {\n margin-top: 0;\n }\n}\n.sql-alert {\n margin-bottom: 8px;\n}\n.preview-block {\n margin-bottom: 8px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图名称\"\n name=\"name\"\n :rules=\"rules.name\"\n >\n <a-input\n v-model:value=\"model.name\"\n placeholder=\"请填写视图名称\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"视图标识\"\n name=\"flag\"\n :rules=\"rules.flag\"\n extra=\"全库唯一;仅英文大小写字母和数字(禁下划线/横杠),用于 URL 与菜单\"\n >\n <a-input\n v-model:value=\"model.flag\"\n placeholder=\"如 salesOrderList\"\n allow-clear\n >\n <template\n v-if=\"form_type === 'create'\"\n #suffix\n >\n <a-button\n type=\"link\"\n size=\"small\"\n style=\"padding: 0\"\n :loading=\"generatingFlag\"\n @click=\"genFlag\"\n >\n 换一个\n </a-button>\n </template>\n </a-input>\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"数据源\"\n name=\"data_source_id\"\n :rules=\"rules.data_source_id\"\n >\n <a-select\n v-model:value=\"model.data_source_id\"\n placeholder=\"请选择数据源\"\n show-search\n option-filter-prop=\"label\"\n :options=\"dataSourceOptions\"\n :loading=\"dsLoading\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <a-form-item\n label=\"查询SQL\"\n name=\"sql\"\n :rules=\"rules.sql\"\n extra=\"只读查询;可用模板变量 {{org_id}}、{{person_id}}(jinja2 渲染)\"\n >\n <a-textarea\n v-model:value=\"model.sql\"\n :rows=\"8\"\n placeholder=\"SELECT id, name FROM t_order WHERE org_id = {{org_id}}\"\n />\n </a-form-item>\n <div class=\"sql-toolbar\">\n <!-- ★ v29.23(用户定案):「探测列」按钮移除——列探测统一收敛到列配置步骤的\n 「列探测刷新」(基于此处 SQL+数据源实时探测并合并配置) -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"previewing\"\n @click=\"runPreview\"\n >\n 预览(前 100 行)\n </a-button>\n <span class=\"meta-tip\">先选数据源并填写 SQL;预览走配置期接口,不落库</span>\n </div>\n <a-alert\n v-if=\"sqlError\"\n type=\"error\"\n show-icon\n :message=\"sqlError\"\n class=\"sql-alert\"\n />\n <div\n v-if=\"previewRows.length\"\n class=\"preview-block\"\n >\n <div class=\"meta-tip\">预览结果:共 {{ previewTotal }} 行(最多回传 100 行)</div>\n <a-table\n :columns=\"previewCols\"\n :data-source=\"previewRows\"\n row-key=\"__i\"\n size=\"small\"\n :pagination=\"false\"\n :scroll=\"{ x: 640, y: 220 }\"\n />\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { extractData, toList } from './helpers';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「基础配置」步(Step 1)。\n 本步持有:视图名称 / 视图标识(flag,含「换一个」) / 数据源 / 查询SQL + 预览结果。\n\n 两条拆分约定(四个步骤组件一致):\n ① `model` 仍由外壳持有并原样传进来(**对象引用共享**:外壳 `model.value` 与这里\n `props.model` 是同一个对象)→ 模板里照旧写 `model.xxx`,v-model 直接改属性即可。\n 外壳会整个换掉对象(编辑态 `initForEdit` 用 `model.value = {...}` 重建),\n 故此处用 `computed(() => props.model)` 取用,才能跟着换到新对象(持有裸引用会失联)。\n eslint 的 `vue/no-mutating-props` 不会误报(模板写的是 `model.xxx`,不是 `props.model.xxx`)。\n ② 校验用的 `rules` 只服务本步这四个字段 → 就近定义在这里;`a-form` 仍是外壳的,\n 本组件的 `a-form-item` 在它内部 —— provide/inject 走组件树,外壳 `validateFields` 照样找得到。 */\n\ninterface Props {\n model: any;\n record: any;\n formType: string;\n /** 预览错误由外壳持有:第 3 步「列探测刷新」开始前也要清它(StepColumnConfig 会 emit probeStart) */\n sqlError: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['update:sqlError']);\n\nconst model = computed(() => props.model);\nconst form_type = computed(() => props.formType);\n\n/* 预览错误:写成「可写 computed」代理到外壳,本组件内部照旧 `sqlError.value = ''` 赋值,\n 不必在每处写 emit(原实现的读写点一个没动)。 */\nconst sqlError = computed<string>({\n get: () => props.sqlError,\n set: (v: string) => emit('update:sqlError', v),\n});\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\nconst rules: any = {\n name: [{ required: true, message: '请填写视图名称' }],\n flag: [\n { required: true, message: '请填写视图标识' },\n { pattern: /^[A-Za-z0-9]+$/, message: '仅英文大小写字母和数字,禁下划线/横杠' },\n ],\n data_source_id: [{ required: true, message: '请选择数据源' }],\n sql: [{ required: true, message: '请填写查询SQL' }],\n};\n\n/* ═══ 下拉数据源 ═══ */\nconst dataSourceOptions = ref<any[]>([]);\nconst dsLoading = ref(false);\n\nconst loadDataSources = async () => {\n dsLoading.value = true;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, is_pagination: false },\n `${props.record.model_app}/query_db_data_source_list`,\n );\n dataSourceOptions.value = toList(res).map((it: any) => ({\n value: it.id,\n label: it.name,\n }));\n } catch (e) {\n dataSourceOptions.value = [];\n } finally {\n dsLoading.value = false;\n }\n};\n\n/* ★ v29.12(用户定案):新建视图时自动调 gen_view_flag 生成唯一默认 flag(用户仍可手改/换一个) */\nconst generatingFlag = ref(false);\nconst genFlag = async () => {\n generatingFlag.value = true;\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id },\n `${props.record.model_app}/gen_view_flag`,\n );\n const payload = extractData(res);\n if (payload?.flag) {\n model.value.flag = payload.flag;\n } else {\n message.warning((res && res.message) || '生成视图标识失败,请手动填写');\n }\n } catch (e: any) {\n message.warning(e?.message || '生成视图标识失败,请手动填写');\n } finally {\n generatingFlag.value = false;\n }\n};\n\n/* ═══ SQL 配置期验证:预览(前 100 行)═══\n ★ v29.23:probeCols / runProbe(探测列按钮)移除——探测统一收敛到列配置步骤的\n 列探测刷新(runProbeRefresh)。\n ★ v29.23 预览恒空修复:后端响应键 `data` → `rows`——宿主 request 层直接回吐\n result 负载,extractData 以 res.data 判信封,负载键恰叫 data 时行数组被误当\n 信封二次解包 → payload 变数组 → 恒「结果为空」。 */\nconst previewing = ref(false);\nconst previewRows = ref<any[]>([]);\nconst previewCols = ref<any[]>([]);\nconst previewTotal = ref(0);\n\n/* SQL / 数据源变化后旧结果即失效,直接清掉避免误导 */\nwatch(\n () => [model.value.sql, model.value.data_source_id],\n () => {\n sqlError.value = '';\n previewRows.value = [];\n previewCols.value = [];\n previewTotal.value = 0;\n },\n);\n\nconst runPreview = async () => {\n if (!model.value.data_source_id || !model.value.sql) return;\n previewing.value = true;\n sqlError.value = '';\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/preview_sql`,\n );\n const payload = extractData(res);\n const rows: any[] = Array.isArray(payload?.rows) ? payload.rows : [];\n if (res && res.success === false) {\n sqlError.value = res.message || 'SQL 执行失败';\n previewRows.value = [];\n } else if (!rows.length) {\n sqlError.value = 'SQL 执行成功,但结果为空(0 行)';\n previewRows.value = [];\n previewTotal.value = 0;\n } else {\n previewRows.value = rows.map((r: any, i: number) => ({ ...r, __i: i }));\n previewCols.value = Object.keys(rows[0] || {}).map(k => ({\n title: k,\n dataIndex: k,\n key: k,\n width: 160,\n ellipsis: true,\n }));\n previewTotal.value = payload?.total ?? rows.length;\n }\n } catch (e: any) {\n sqlError.value = e?.message || 'SQL 预览失败';\n previewRows.value = [];\n } finally {\n previewing.value = false;\n }\n};\n\nonMounted(() => {\n loadDataSources();\n /* ★ v29.12:新建流程自动生成唯一默认 flag(原先在外壳 onMounted 的 create 分支里,\n 现随本步骤下沉;外壳的 formType 在 setup 顶层就已初始化,此处读到的一定是正确值) */\n if (props.formType !== 'edit') genFlag();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.sql-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n .meta-tip {\n margin-top: 0;\n }\n}\n.sql-alert {\n margin-bottom: 8px;\n}\n.preview-block {\n margin-bottom: 8px;\n}\n</style>\n","/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分的「数据字典」集中地\n (与 `dbView/searchComponentSetting/constants.ts` 同一约定:常量留这里,逻辑留组件)。\n 注意:`COL_TABLE_WIDTH` 由 `COL_COLUMNS` 求和得出,两者必须同文件,改列宽才不会漂。 */\n\n/* ═══ 展示配置(Step 3)═══ */\n\nexport const VIEW_TYPE_OPTIONS = [\n { value: 'normal', label: '标准视图' },\n { value: 'pivot', label: '透视表' },\n { value: 'pivot_charts', label: '透视图' },\n];\n\n/* ═══ 列配置(Step 4)═══ */\n\n/* cell_component 可选项(对照 liyu-pc-lib/src/components/cellComponent/ 实存组件,\n 挑通用项;金额等特殊语义可后续按需扩充)。\n ★ v29.38(用户定案):label 一律不带原始英文(原来写作「中文(datetime)」)——\n `value` 本身就是要落的英文枚举值,界面不再重复展示;若日后要按英文搜下拉,\n 给该 `a-select` 加 `:filter-option=\"(v, o) => String(o.value).includes(v) || String(o.label).includes(v)\"`。 */\nexport const CELL_COMPONENT_OPTIONS = [\n { value: 'datetime', label: '日期时间' },\n { value: 'datetime_hh_mm', label: '日期时间到分' },\n { value: 'time', label: '时间' },\n { value: 'date', label: '日期' },\n { value: 'date_year', label: '年份' },\n { value: 'month', label: '月份' },\n { value: 'boolean', label: '布尔' },\n { value: 'tag', label: '标签' },\n { value: 'tag_list', label: '标签列表' },\n { value: 'json', label: 'JSON' },\n { value: 'json_tag', label: 'JSON标签' },\n { value: 'money', label: '金额' },\n { value: 'to_fixed_0', label: '整数' },\n { value: 'to_fixed_2', label: '两位小数' },\n { value: 'to_fixed_4', label: '四位小数' },\n { value: 'percent', label: '百分比' },\n { value: 'tooltip', label: '悬浮提示' },\n { value: 'image', label: '图片' },\n { value: 'image_url', label: '图片链接' },\n { value: 'link', label: '链接' },\n { value: 'url_out', label: '外链' },\n { value: 'obj', label: '对象' },\n { value: 'list', label: '列表' },\n { value: 'color', label: '颜色块' },\n { value: 'bolder', label: '加粗' },\n { value: 'click_copy', label: '点击复制' },\n];\n\n/* 探测类型 → class_name / cell_component(对齐后端 sync_columns 的映射)\n ★ v29.23.1:后端 probe_columns 改真实行推断(_py_type_name),type 为\n int/float/bool/datetime/date/str 六类真实类型名,str 显式映射(probe 值优先于已有值)。 */\nexport const PROBE_CLASS_NAME_MAP: Record<string, string> = {\n int: 'int',\n float: 'float',\n bool: 'bool',\n datetime: 'datetime',\n date: 'date',\n str: 'str',\n};\nexport const PROBE_CELL_MAP: Record<string, string> = {\n datetime: 'datetime',\n date: 'date',\n bool: 'tag',\n json: 'json',\n};\n\n/* s-table 行高:本表每格都是 small 尺寸输入控件(24px),36 留出上下呼吸空间 */\nexport const COL_ROW_HEIGHT = 36;\n\n/* 列配置表列定义。\n antdv ColumnsType 对 fixed 等字段是字面量类型,这里用 any[] 放宽避免字面量拓宽报错。\n ★ v29.16(用户定案):去掉列宽(width)配置;新增 可拖宽(resizable)/汇总(summary)/\n 可排序(is_sortable);content_map+color_map 合并为「转换/颜色」一列(模态框联合配置);\n 新增「搜索组件」列(模态框配置高级搜索组件对象)。 */\nexport const COL_COLUMNS: any[] = [\n { title: '顺序', key: 'column_order', width: 80, fixed: 'left' },\n {\n title: '列名',\n dataIndex: 'data_index',\n key: 'data_index',\n width: 140,\n fixed: 'left',\n },\n { title: '列展示名', key: 'title', width: 130, fixed: 'left' },\n // ★ v29.40:原「唯一主键」→「主键」(主键改为可多选,措辞不能再带「唯一」);\n // 文案变短,列宽 85 → 70(COL_TABLE_WIDTH 由本数组求和派生,改宽自动同步)\n { title: '主键', key: 'is_primary_key', width: 70 },\n { title: '类型', dataIndex: 'class_name', key: 'class_name', width: 80 },\n\n { title: '查询名', key: 'query_title', width: 120 },\n { title: '单元格组件', key: 'cell_component', width: 150 },\n { title: '显示', key: 'checked', width: 55 },\n { title: '可拖宽', key: 'resizable', width: 65 },\n { title: '合计', key: 'summary', width: 55 },\n { title: '可排序', key: 'is_sortable', width: 65 },\n { title: '基础搜索', key: 'is_base_query', width: 75 },\n { title: '默认搜索', key: 'is_default_query', width: 75 },\n { title: '分组', key: 'is_group_query', width: 55 },\n { title: '默认排序', key: 'default_sort', width: 200 },\n { title: '转换/颜色', key: 'map_color', width: 90 },\n { title: '搜索组件', key: 'adv_search', width: 90 },\n { title: '操作', key: 'action', width: 60, fixed: 'right' },\n];\n\n/* ★ v29.34:s-table 的横向滚动宽度 = 表格总宽,直接由各列显式 width 求和得出\n (原来是手写的 `:scroll=\"{ x: 1685 }\"`,改列宽就会漂;现在改 width 自动跟随)。 */\nexport const COL_TABLE_WIDTH = COL_COLUMNS.reduce((sum: number, c: any) => sum + (c.width || 0), 0);\n\n/* ═══ 步骤导航(外壳用)═══ */\n\n/* 前进/跳步时只校验「当前步骤」的必填项(其余步骤的错误不可见,全量校验会让用户困惑)。\n ★ v29.23:菜单四项拆出 → 4 步。\n ★ v29.45(用户定案):「菜单与权限」挪到末位并改名「菜单」→ 现为\n 基础配置 / 展示配置 / 列配置 / 菜单。\n ⚠️ 本数组是**按步骤位置**排的:改步骤顺序时,这里的顺序与上面每个注释的措辞必须一起改,\n 否则会「校验了另一步的字段」还看不出错。 */\nexport const STEP_FIELDS: string[][] = [\n ['name', 'flag', 'data_source_id', 'sql'], // 第 1 步 基础配置\n [], // 第 2 步 展示配置(无必填项)\n [], // 第 3 步 列配置(随视图一起保存)\n [], // 第 4 步 菜单(无必填项:父级留空 = 顶级菜单;两项由 save_view 同步到菜单行)\n];\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"启用的视图类型\"\n name=\"enable_view_types\"\n extra=\"不选 = 全部启用\"\n >\n <a-select\n v-model:value=\"model.enable_view_types\"\n mode=\"multiple\"\n allow-clear\n placeholder=\"不选 = 全部启用\"\n :options=\"viewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"默认视图类型\"\n name=\"default_view_type\"\n extra=\"仅能在已启用的视图类型中选择;未启用任何类型时不限\"\n >\n <a-select\n v-model:value=\"model.default_view_type\"\n allow-clear\n placeholder=\"默认标准视图\"\n :options=\"defaultViewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"启用分页\">\n <a-switch\n v-model:checked=\"model.is_pagination\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数\"\n name=\"pageSize\"\n >\n <a-input-number\n v-model:value=\"model.pageSize\"\n style=\"width: 100%\"\n :min=\"1\"\n placeholder=\"默认 200\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数选项\"\n name=\"page_size_options\"\n >\n <a-select\n v-model:value=\"model.page_size_options\"\n mode=\"tags\"\n :open=\"false\"\n placeholder=\"如 10、20、50、100\"\n :token-separators=\"[',', ' ']\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from 'vue';\nimport { VIEW_TYPE_OPTIONS } from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「展示配置」步(★ v29.45 起为 Step 2)。\n 本步持有 view_meta 白名单里回传给前端的 5 个键(写在共享 model 上)。\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.13:org_filter 已废除(组织过滤只靠 SQL 里手写 {{org_id}}),不再提供配置项。\n 拆分约定同 Step 1:`model` 由外壳持有、对象引用共享,这里取 computed 后直接改属性。\n `view_meta` 的拆包/合并(applyViewMeta / buildViewMeta)已提为纯函数放 `./viewMeta.ts`。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\nconst viewTypeOptions = VIEW_TYPE_OPTIONS;\n\n/* ★ v29.18:default_view_type 仅能在 enable_view_types 勾选的值中选择;\n enable_view_types 为空 = 全部启用,default_view_type 不受限 */\nconst defaultViewTypeOptions = computed(() => {\n const enabled = model.value.enable_view_types;\n return Array.isArray(enabled) && enabled.length\n ? viewTypeOptions.filter(o => enabled.includes(o.value))\n : viewTypeOptions;\n});\n\nwatch(\n () => model.value.enable_view_types,\n enabled => {\n if (\n Array.isArray(enabled) &&\n enabled.length &&\n model.value.default_view_type &&\n !enabled.includes(model.value.default_view_type)\n ) {\n model.value.default_view_type = undefined; // 启用范围变更后默认类型失效 → 清空\n }\n },\n);\n</script>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"启用的视图类型\"\n name=\"enable_view_types\"\n extra=\"不选 = 全部启用\"\n >\n <a-select\n v-model:value=\"model.enable_view_types\"\n mode=\"multiple\"\n allow-clear\n placeholder=\"不选 = 全部启用\"\n :options=\"viewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"默认视图类型\"\n name=\"default_view_type\"\n extra=\"仅能在已启用的视图类型中选择;未启用任何类型时不限\"\n >\n <a-select\n v-model:value=\"model.default_view_type\"\n allow-clear\n placeholder=\"默认标准视图\"\n :options=\"defaultViewTypeOptions\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"启用分页\">\n <a-switch\n v-model:checked=\"model.is_pagination\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数\"\n name=\"pageSize\"\n >\n <a-input-number\n v-model:value=\"model.pageSize\"\n style=\"width: 100%\"\n :min=\"1\"\n placeholder=\"默认 200\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"每页条数选项\"\n name=\"page_size_options\"\n >\n <a-select\n v-model:value=\"model.page_size_options\"\n mode=\"tags\"\n :open=\"false\"\n placeholder=\"如 10、20、50、100\"\n :token-separators=\"[',', ' ']\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from 'vue';\nimport { VIEW_TYPE_OPTIONS } from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「展示配置」步(★ v29.45 起为 Step 2)。\n 本步持有 view_meta 白名单里回传给前端的 5 个键(写在共享 model 上)。\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.13:org_filter 已废除(组织过滤只靠 SQL 里手写 {{org_id}}),不再提供配置项。\n 拆分约定同 Step 1:`model` 由外壳持有、对象引用共享,这里取 computed 后直接改属性。\n `view_meta` 的拆包/合并(applyViewMeta / buildViewMeta)已提为纯函数放 `./viewMeta.ts`。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\nconst viewTypeOptions = VIEW_TYPE_OPTIONS;\n\n/* ★ v29.18:default_view_type 仅能在 enable_view_types 勾选的值中选择;\n enable_view_types 为空 = 全部启用,default_view_type 不受限 */\nconst defaultViewTypeOptions = computed(() => {\n const enabled = model.value.enable_view_types;\n return Array.isArray(enabled) && enabled.length\n ? viewTypeOptions.filter(o => enabled.includes(o.value))\n : viewTypeOptions;\n});\n\nwatch(\n () => model.value.enable_view_types,\n enabled => {\n if (\n Array.isArray(enabled) &&\n enabled.length &&\n model.value.default_view_type &&\n !enabled.includes(model.value.default_view_type)\n ) {\n model.value.default_view_type = undefined; // 启用范围变更后默认类型失效 → 清空\n }\n },\n);\n</script>\n","<template>\n <a-modal\n :title=\"`转换与颜色 —— ${record?.title || record?.data_index || ''}`\"\n width=\"760px\"\n :open=\"open\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <div class=\"map-tip\">\n 原值 → 显示文本 与 原值 → 颜色 在此一体化配置:一行 =\n 一条「原值」的转换与颜色,落库时拆回两个键。 最右「预览」按运行时标签组件的真实样式渲染 ——\n 内置色是浅底彩字、 自定义色是实心彩底白字,两者观感不同属正常。\n </div>\n <a-table\n :data-source=\"rows\"\n :columns=\"columns\"\n row-key=\"__k\"\n size=\"small\"\n :pagination=\"false\"\n >\n <template #bodyCell=\"{ column, record: row }\">\n <template v-if=\"column.key === 'value'\">\n <a-input\n v-model:value=\"row.value\"\n size=\"small\"\n placeholder=\"原值\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'label'\">\n <a-input\n v-model:value=\"row.label\"\n size=\"small\"\n placeholder=\"显示文本\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'color'\">\n <div class=\"color-cell\">\n <!-- ★ v29.23(用户定案):内置色板直接存颜色名字符串(pink/red/…,不转十六进制);\n 自定义走颜色选择器(选择器产出 #十六进制),不再让用户手输。\n ★ v29.23.2:a-color-picker 在 antdv 4.2.6 中不存在(es 目录无 color-picker,\n 模板里写它是未知组件被静默渲染为空——「自定义颜色没出现」的根因),\n 改用原生 input[type=color](产出 #rrggbb,同尺寸色块形态)。 -->\n <div class=\"swatches\">\n <span\n v-for=\"c in PRESET_COLORS\"\n :key=\"c\"\n class=\"swatch\"\n :class=\"{ active: row.color === c }\"\n :style=\"{ background: PRESET_HEX[c] }\"\n :title=\"c\"\n @click=\"row.color = c\"\n />\n <span class=\"custom-color-label\">自定义:</span>\n <input\n type=\"color\"\n class=\"custom-color-input\"\n :class=\"{ active: isCustomColor(row) }\"\n :value=\"row.color && row.color.startsWith('#') ? row.color : '#1677ff'\"\n :title=\"isCustomColor(row) ? row.color : '自定义颜色'\"\n @input=\"onCustomColor(row, $event)\"\n />\n </div>\n </div>\n </template>\n <template v-else-if=\"column.key === 'preview'\">\n <!-- ★ v29.39(用户定案):a-tag 实时预览 —— 把「原值 + 显示文本 + 颜色」按运行时\n cell_tag.vue 的语义渲染出来,配色的观感当场可见(色名浅底彩字 / 十六进制实心彩底白字)。\n ⚠️ 文案与颜色必须与 liyu-pc-lib/src/components/cellComponent/cell_tag.vue 逐条对齐,\n 见脚本里 previewText / previewColor 的注释;这里显示什么,真机就长什么样。 -->\n <div class=\"preview-cell\">\n <a-tag\n v-if=\"previewText(row)\"\n :color=\"previewColor(row)\"\n >\n {{ previewText(row) }}\n </a-tag>\n <span\n v-else\n class=\"preview-empty\"\n >\n —\n </span>\n </div>\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n @click=\"removeRow(row.__k)\"\n >\n 删除\n </a-button>\n </template>\n </template>\n </a-table>\n <a-button\n size=\"small\"\n type=\"dashed\"\n block\n style=\"margin-top: 8px\"\n @click=\"addRow\"\n >\n + 添加一条转换\n </a-button>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, ref, watch } from 'vue';\n\n/* ★ v29.16(用户定案):content_map(原值→显示文本)与 color_map(原值→颜色)不再割裂为\n 两个配置入口,而是一张「转换与颜色」联合表 —— 一行 = {原值, 显示文本, 颜色},\n 确定时按原值拆回 content_map / color_map 两个键写回行记录(运行时契约不变)。 */\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(colRows 中的一项,直接读写其 content_map / color_map)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst columns: any[] = [\n { title: '原值', key: 'value', width: '18%' },\n { title: '显示文本', key: 'label', width: '18%' },\n { title: '颜色', key: 'color', width: '36%' },\n { title: '预览', key: 'preview', width: '20%' },\n { title: '', key: 'action', width: '8%' },\n];\n\n/* ★ v29.23(用户定案):内置色板直接存 antd 预设色**名字符串**(red/orange/green/cyan/\n blue/purple,运行时 tag/色块组件按名字渲染),不转十六进制;PRESET_HEX 仅用于色板展示。\n 自定义 = 颜色选择器(a-color-picker),产出 #十六进制字符串。\n ⚠️ `pink` 已从色板移除:历史数据里存的 `pink` 运行时仍能正常渲染(antd 侧自带该预设色),\n 但配置弹窗里**既不会点亮色块、也不会点亮取色器**(`pink` 不以 `#` 开头,又不在 PRESET_COLORS 里)\n —— 这个「0 个亮」是已知退化,已由技能里的 preview-vs-runtime-check.mjs 断言钉住。 */\nconst PRESET_COLORS = ['red', 'orange', 'green', 'cyan', 'blue', 'purple'];\nconst PRESET_HEX: Record<string, string> = {\n red: '#f5222d',\n orange: '#fa8c16',\n green: '#52c41a',\n cyan: '#13c2c2',\n blue: '#1677ff',\n purple: '#722ed1',\n};\n\n/* 自定义取色:原生 input[type=color] 事件值恒为 #rrggbb 字符串(v29.23.2,替代不存在的 a-color-picker) */\nconst onCustomColor = (row: any, e: Event) => {\n row.color = (e.target as HTMLInputElement).value;\n};\n\n/* ★ v29.39(用户定案):自定义颜色也要有「已选中」高亮,与内置色块的 .active 同款。\n 判定取「值是 # 开头的十六进制」= 取色器的产出形态 ——\n ① 与取色器 :value 的取值判定同源,不会出现「高亮了但取色器显示的并不是这个色」;\n ② 也与运行时一致:cell_tag.vue 把该值直接交给 a-tag 的 color,\n antd 内部按「是否预设色名」分两路渲染(色名 → 浅底彩字 / 十六进制 → 实心彩底白字),\n 所以「# 开头」正是「走了自定义分支」的准确刻画。 */\nconst isCustomColor = (row: any) =>\n String(row.color || '')\n .trim()\n .startsWith('#');\n\nlet seq = 0;\nconst rows = ref<any[]>([]);\nconst buildRow = (value = '', label = '', color = '') => ({ __k: ++seq, value, label, color });\n\n/* 每次打开时从行记录重建本地编辑态(content_map/color_map → 联合行) */\nwatch(\n () => props.open,\n val => {\n if (!val) return;\n seq = 0;\n const cm = props.record?.content_map || {};\n const colors = props.record?.color_map || {};\n const keys = Array.from(new Set([...Object.keys(cm || {}), ...Object.keys(colors || {})]));\n rows.value = keys.length\n ? keys.map(k => buildRow(String(k), cm?.[k] ?? '', colors?.[k] ?? ''))\n : [buildRow()];\n },\n);\n\nconst addRow = () => rows.value.push(buildRow());\nconst removeRow = (k: number) => {\n rows.value = rows.value.filter(r => r.__k !== k);\n};\n\n/* ★ v29.39:a-tag 预览 —— 与运行时 cell_tag.vue 一一对应,保证「所见即真机所得」。\n 运行时(liyu-pc-lib/src/components/cellComponent/cell_tag.vue)的算法:\n tagText = contentMap[text] || contentMap['other'] || text (text = 单元格原始值)\n tagColor = colorMap ? (colorMap[text] || colorMap['other']) : undefined\n 其中 `other` 是约定的兜底键:任何未命中的原始值都落到它。\n ⚠️ 外层 `v-if=\"cmpObj.column.contentMap\"` 看似是「没配转换就不渲染」,实际不成立 ——\n 后端 columns_config() 产出的是 `c.content_map or {}`(空 map → 空对象,truthy),\n 所以「只配颜色」的列同样会渲染标签,文字退回原始值。预览照此实现,不另加限制。 */\n\n/* 兜底行 = 原值填 `other` 的那一行(可能不存在)。\n 取**最后一条**与 handleOk 的落库口径一致(重复原值时保留最后一次),否则预览会和真机不符。 */\nconst otherRow = computed(() => {\n const hits = rows.value.filter(r => (r.value || '').trim() === 'other');\n return hits.length ? hits[hits.length - 1] : undefined;\n});\n\n/* 预览文案:本行显示文本 → `other` 兜底显示文本 → 原始值 */\nconst previewText = (row: any) => {\n const v = (row.value || '').trim();\n if (!v) return ''; // 无原值 → 该行谈不上渲染\n const label = (row.label || '').trim() || (otherRow.value?.label || '').trim();\n return label || v;\n};\n\n/* 预览颜色:本行颜色 → `other` 兜底颜色;都没有 → undefined(a-tag 默认样式,与运行时一致) */\nconst previewColor = (row: any) => {\n if (!(row.value || '').trim()) return undefined;\n const color = (row.color || '').trim() || (otherRow.value?.color || '').trim();\n return color || undefined;\n};\n\nconst handleOk = () => {\n const content_map: Record<string, string> = {};\n const color_map: Record<string, string> = {};\n for (const r of rows.value) {\n const v = (r.value || '').trim();\n if (!v) continue; // 空原值行直接忽略\n if (v in content_map) {\n message.warning(`原值「${v}」重复,已保留最后一次配置`);\n }\n const label = (r.label || '').trim();\n const color = (r.color || '').trim();\n if (label) content_map[v] = label;\n if (color) color_map[v] = color;\n }\n props.record.content_map = Object.keys(content_map).length ? content_map : null;\n props.record.color_map = Object.keys(color_map).length ? color_map : null;\n /* ★ v29.23(用户定案):确定时若有配置项 → 单元格组件自动置为 tag(仅自动改,\n 用户随后可手动改成其他组件,不强制) */\n if (props.record.content_map || props.record.color_map) {\n props.record.cell_component = 'tag';\n }\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.map-tip {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n line-height: 1.5;\n}\n.color-cell {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n/* ★ v29.39:a-tag 预览列。窄列里必须收掉 a-tag 自带的 margin-inline-end(antd 默认 8px),\n 否则标签会被挤出单元格;显示文本过长按省略号截断,不换行、不撑高行。 */\n.preview-cell {\n display: flex;\n align-items: center;\n overflow: hidden;\n :deep(.ant-tag) {\n margin-inline-end: 0;\n max-width: 100%;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n}\n.preview-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.25);\n}\n.swatches {\n display: flex;\n gap: 4px;\n flex: none;\n}\n.swatch {\n width: 16px;\n height: 16px;\n border-radius: 3px;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.15);\n}\n/* 选中态高亮:内置色块与自定义取色器**同口径**(v29.39 用户定案:配了自定义色也要高亮,\n 否则用户看不出当前行到底选的是色板还是取色器)。放在两者之后,保证覆盖各自的基础样式。 */\n.swatch.active,\n.custom-color-input.active {\n outline: 2px solid #1677ff;\n outline-offset: 1px;\n}\n.custom-color-label {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 16px;\n}\n/* 原生取色输入:整成与色块同尺寸的小方块(点击弹系统取色器) */\n.custom-color-input {\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n &::-webkit-color-swatch-wrapper {\n padding: 0;\n }\n &::-webkit-color-swatch {\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 3px;\n }\n}\n</style>\n","<template>\n <a-modal\n :title=\"`转换与颜色 —— ${record?.title || record?.data_index || ''}`\"\n width=\"760px\"\n :open=\"open\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <div class=\"map-tip\">\n 原值 → 显示文本 与 原值 → 颜色 在此一体化配置:一行 =\n 一条「原值」的转换与颜色,落库时拆回两个键。 最右「预览」按运行时标签组件的真实样式渲染 ——\n 内置色是浅底彩字、 自定义色是实心彩底白字,两者观感不同属正常。\n </div>\n <a-table\n :data-source=\"rows\"\n :columns=\"columns\"\n row-key=\"__k\"\n size=\"small\"\n :pagination=\"false\"\n >\n <template #bodyCell=\"{ column, record: row }\">\n <template v-if=\"column.key === 'value'\">\n <a-input\n v-model:value=\"row.value\"\n size=\"small\"\n placeholder=\"原值\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'label'\">\n <a-input\n v-model:value=\"row.label\"\n size=\"small\"\n placeholder=\"显示文本\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'color'\">\n <div class=\"color-cell\">\n <!-- ★ v29.23(用户定案):内置色板直接存颜色名字符串(pink/red/…,不转十六进制);\n 自定义走颜色选择器(选择器产出 #十六进制),不再让用户手输。\n ★ v29.23.2:a-color-picker 在 antdv 4.2.6 中不存在(es 目录无 color-picker,\n 模板里写它是未知组件被静默渲染为空——「自定义颜色没出现」的根因),\n 改用原生 input[type=color](产出 #rrggbb,同尺寸色块形态)。 -->\n <div class=\"swatches\">\n <span\n v-for=\"c in PRESET_COLORS\"\n :key=\"c\"\n class=\"swatch\"\n :class=\"{ active: row.color === c }\"\n :style=\"{ background: PRESET_HEX[c] }\"\n :title=\"c\"\n @click=\"row.color = c\"\n />\n <span class=\"custom-color-label\">自定义:</span>\n <input\n type=\"color\"\n class=\"custom-color-input\"\n :class=\"{ active: isCustomColor(row) }\"\n :value=\"row.color && row.color.startsWith('#') ? row.color : '#1677ff'\"\n :title=\"isCustomColor(row) ? row.color : '自定义颜色'\"\n @input=\"onCustomColor(row, $event)\"\n />\n </div>\n </div>\n </template>\n <template v-else-if=\"column.key === 'preview'\">\n <!-- ★ v29.39(用户定案):a-tag 实时预览 —— 把「原值 + 显示文本 + 颜色」按运行时\n cell_tag.vue 的语义渲染出来,配色的观感当场可见(色名浅底彩字 / 十六进制实心彩底白字)。\n ⚠️ 文案与颜色必须与 liyu-pc-lib/src/components/cellComponent/cell_tag.vue 逐条对齐,\n 见脚本里 previewText / previewColor 的注释;这里显示什么,真机就长什么样。 -->\n <div class=\"preview-cell\">\n <a-tag\n v-if=\"previewText(row)\"\n :color=\"previewColor(row)\"\n >\n {{ previewText(row) }}\n </a-tag>\n <span\n v-else\n class=\"preview-empty\"\n >\n —\n </span>\n </div>\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n @click=\"removeRow(row.__k)\"\n >\n 删除\n </a-button>\n </template>\n </template>\n </a-table>\n <a-button\n size=\"small\"\n type=\"dashed\"\n block\n style=\"margin-top: 8px\"\n @click=\"addRow\"\n >\n + 添加一条转换\n </a-button>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, ref, watch } from 'vue';\n\n/* ★ v29.16(用户定案):content_map(原值→显示文本)与 color_map(原值→颜色)不再割裂为\n 两个配置入口,而是一张「转换与颜色」联合表 —— 一行 = {原值, 显示文本, 颜色},\n 确定时按原值拆回 content_map / color_map 两个键写回行记录(运行时契约不变)。 */\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(colRows 中的一项,直接读写其 content_map / color_map)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst columns: any[] = [\n { title: '原值', key: 'value', width: '18%' },\n { title: '显示文本', key: 'label', width: '18%' },\n { title: '颜色', key: 'color', width: '36%' },\n { title: '预览', key: 'preview', width: '20%' },\n { title: '', key: 'action', width: '8%' },\n];\n\n/* ★ v29.23(用户定案):内置色板直接存 antd 预设色**名字符串**(red/orange/green/cyan/\n blue/purple,运行时 tag/色块组件按名字渲染),不转十六进制;PRESET_HEX 仅用于色板展示。\n 自定义 = 颜色选择器(a-color-picker),产出 #十六进制字符串。\n ⚠️ `pink` 已从色板移除:历史数据里存的 `pink` 运行时仍能正常渲染(antd 侧自带该预设色),\n 但配置弹窗里**既不会点亮色块、也不会点亮取色器**(`pink` 不以 `#` 开头,又不在 PRESET_COLORS 里)\n —— 这个「0 个亮」是已知退化,已由技能里的 preview-vs-runtime-check.mjs 断言钉住。 */\nconst PRESET_COLORS = ['red', 'orange', 'green', 'cyan', 'blue', 'purple'];\nconst PRESET_HEX: Record<string, string> = {\n red: '#f5222d',\n orange: '#fa8c16',\n green: '#52c41a',\n cyan: '#13c2c2',\n blue: '#1677ff',\n purple: '#722ed1',\n};\n\n/* 自定义取色:原生 input[type=color] 事件值恒为 #rrggbb 字符串(v29.23.2,替代不存在的 a-color-picker) */\nconst onCustomColor = (row: any, e: Event) => {\n row.color = (e.target as HTMLInputElement).value;\n};\n\n/* ★ v29.39(用户定案):自定义颜色也要有「已选中」高亮,与内置色块的 .active 同款。\n 判定取「值是 # 开头的十六进制」= 取色器的产出形态 ——\n ① 与取色器 :value 的取值判定同源,不会出现「高亮了但取色器显示的并不是这个色」;\n ② 也与运行时一致:cell_tag.vue 把该值直接交给 a-tag 的 color,\n antd 内部按「是否预设色名」分两路渲染(色名 → 浅底彩字 / 十六进制 → 实心彩底白字),\n 所以「# 开头」正是「走了自定义分支」的准确刻画。 */\nconst isCustomColor = (row: any) =>\n String(row.color || '')\n .trim()\n .startsWith('#');\n\nlet seq = 0;\nconst rows = ref<any[]>([]);\nconst buildRow = (value = '', label = '', color = '') => ({ __k: ++seq, value, label, color });\n\n/* 每次打开时从行记录重建本地编辑态(content_map/color_map → 联合行) */\nwatch(\n () => props.open,\n val => {\n if (!val) return;\n seq = 0;\n const cm = props.record?.content_map || {};\n const colors = props.record?.color_map || {};\n const keys = Array.from(new Set([...Object.keys(cm || {}), ...Object.keys(colors || {})]));\n rows.value = keys.length\n ? keys.map(k => buildRow(String(k), cm?.[k] ?? '', colors?.[k] ?? ''))\n : [buildRow()];\n },\n);\n\nconst addRow = () => rows.value.push(buildRow());\nconst removeRow = (k: number) => {\n rows.value = rows.value.filter(r => r.__k !== k);\n};\n\n/* ★ v29.39:a-tag 预览 —— 与运行时 cell_tag.vue 一一对应,保证「所见即真机所得」。\n 运行时(liyu-pc-lib/src/components/cellComponent/cell_tag.vue)的算法:\n tagText = contentMap[text] || contentMap['other'] || text (text = 单元格原始值)\n tagColor = colorMap ? (colorMap[text] || colorMap['other']) : undefined\n 其中 `other` 是约定的兜底键:任何未命中的原始值都落到它。\n ⚠️ 外层 `v-if=\"cmpObj.column.contentMap\"` 看似是「没配转换就不渲染」,实际不成立 ——\n 后端 columns_config() 产出的是 `c.content_map or {}`(空 map → 空对象,truthy),\n 所以「只配颜色」的列同样会渲染标签,文字退回原始值。预览照此实现,不另加限制。 */\n\n/* 兜底行 = 原值填 `other` 的那一行(可能不存在)。\n 取**最后一条**与 handleOk 的落库口径一致(重复原值时保留最后一次),否则预览会和真机不符。 */\nconst otherRow = computed(() => {\n const hits = rows.value.filter(r => (r.value || '').trim() === 'other');\n return hits.length ? hits[hits.length - 1] : undefined;\n});\n\n/* 预览文案:本行显示文本 → `other` 兜底显示文本 → 原始值 */\nconst previewText = (row: any) => {\n const v = (row.value || '').trim();\n if (!v) return ''; // 无原值 → 该行谈不上渲染\n const label = (row.label || '').trim() || (otherRow.value?.label || '').trim();\n return label || v;\n};\n\n/* 预览颜色:本行颜色 → `other` 兜底颜色;都没有 → undefined(a-tag 默认样式,与运行时一致) */\nconst previewColor = (row: any) => {\n if (!(row.value || '').trim()) return undefined;\n const color = (row.color || '').trim() || (otherRow.value?.color || '').trim();\n return color || undefined;\n};\n\nconst handleOk = () => {\n const content_map: Record<string, string> = {};\n const color_map: Record<string, string> = {};\n for (const r of rows.value) {\n const v = (r.value || '').trim();\n if (!v) continue; // 空原值行直接忽略\n if (v in content_map) {\n message.warning(`原值「${v}」重复,已保留最后一次配置`);\n }\n const label = (r.label || '').trim();\n const color = (r.color || '').trim();\n if (label) content_map[v] = label;\n if (color) color_map[v] = color;\n }\n props.record.content_map = Object.keys(content_map).length ? content_map : null;\n props.record.color_map = Object.keys(color_map).length ? color_map : null;\n /* ★ v29.23(用户定案):确定时若有配置项 → 单元格组件自动置为 tag(仅自动改,\n 用户随后可手动改成其他组件,不强制) */\n if (props.record.content_map || props.record.color_map) {\n props.record.cell_component = 'tag';\n }\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.map-tip {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n line-height: 1.5;\n}\n.color-cell {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n/* ★ v29.39:a-tag 预览列。窄列里必须收掉 a-tag 自带的 margin-inline-end(antd 默认 8px),\n 否则标签会被挤出单元格;显示文本过长按省略号截断,不换行、不撑高行。 */\n.preview-cell {\n display: flex;\n align-items: center;\n overflow: hidden;\n :deep(.ant-tag) {\n margin-inline-end: 0;\n max-width: 100%;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n}\n.preview-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.25);\n}\n.swatches {\n display: flex;\n gap: 4px;\n flex: none;\n}\n.swatch {\n width: 16px;\n height: 16px;\n border-radius: 3px;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.15);\n}\n/* 选中态高亮:内置色块与自定义取色器**同口径**(v29.39 用户定案:配了自定义色也要高亮,\n 否则用户看不出当前行到底选的是色板还是取色器)。放在两者之后,保证覆盖各自的基础样式。 */\n.swatch.active,\n.custom-color-input.active {\n outline: 2px solid #1677ff;\n outline-offset: 1px;\n}\n.custom-color-label {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 16px;\n}\n/* 原生取色输入:整成与色块同尺寸的小方块(点击弹系统取色器) */\n.custom-color-input {\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n &::-webkit-color-swatch-wrapper {\n padding: 0;\n }\n &::-webkit-color-swatch {\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 3px;\n }\n}\n</style>\n","import { ConditionOperatorMap } from 'liyu-pc-base/api/typing';\n\n/* ★ v29.20:ColumnSearchComponentModal 拆分产物。\n 所有类型分组 / 类型默认值 / 运算符集集中于此(port 自 FormDesignModal/data.ts + defaultConfig,\n 裁剪到搜索支持的范围),供各基础配置组件与分类型 Setting 组件共用。 */\n\n/* ───────── 栅格约定(★ v29.31,改布局前先读) ─────────\n 容器 = 右侧抽屉,宽 920px(ColumnSearchComponentModal 的 SEARCH_DRAWER_WIDTH);\n 表单标签列固定 190px(`:label-col=\"{ style: { width: '190px' } }\"`)。\n\n 于是每一列留给控件的宽度 ≈ 列宽 − 190px:\n · span=24 → 888 − 190 = 698px (整行独占:长文本 / 表格 / 提示)\n · span=12 → 444 − 190 = 254px (**每行 2 个控件**,本目录的默认密度)\n · span=8 → 296 − 190 = 106px ← 已废弃:106px 装不下日期格式串/数值框,会被挤成两行\n · span=6 / span=4 → 更窄,同样废弃\n ⇒ **约定:一行最多 2 个控件(span=12),整行独占才用 24。** 新增配置项照此选 span;\n 若调整 SEARCH_DRAWER_WIDTH 或标签列宽度,**回到这里重算一次**(本目录所有 :span 都是 12/24)。 */\n\n/* ───────── 运算符(port 自 FieldSetting 的查询条件配置) ───────── */\nexport const OPERATOR: Record<string, string> = ConditionOperatorMap as any;\nexport const TEXT_OPS = ['eq', 'icontains', 'istartswith', 'iendswith', 'not_eq', 'not_blank_not_null', 'contains', 'startswith', 'endswith', 'noticontains', 'notistartswith', 'notiendswith', 'notcontains', 'notstartswith', 'notendswith'];\nexport const NUMBER_OPS = ['between', 'gt', 'lt', 'eq', 'gte', 'lte', 'notbetween', 'not_eq', 'not_blank_not_null'];\nexport const SELECT_OPS = ['in', 'notin'];\nexport const DATE_OPS = ['between', 'eq', 'gt', 'lt', 'gte', 'lte', 'notbetween'];\nexport const SEARCH_CLASS_NAME = ['int', 'str', 'bool', 'datetime'];\nexport const FORMAT_TYPE = ['input', 'remote', 'choices'];\n/* antdv select option 的 value 类型声明不含 boolean,这里放宽(布尔固定查询值需要 true/false) */\nexport const BOOL_OPTIONS: any[] = [\n { value: true, label: 'true' },\n { value: false, label: 'false' },\n];\n\n/* ───────── 组件类型分组 ───────── */\n/** 本地扁平选项组件(options 数组) */\nexport const FLAT_OPTION_TYPES = ['select', 'radio', 'checkbox'];\n/** ★ v29.29:本地静态选项类(选项列表在「数据源」页配置)——原版 FieldSetting 的\n * 「选项列表」编辑器覆盖范围就是这三类;groupselect 走 a-select 的 :options 直传\n * (不认 valueKey/labelKey),同样需要本地 options,故一并纳入。 */\nexport const LOCAL_OPTION_TYPES = ['select', 'radio', 'checkbox', 'groupselect'];\n/** FlatSelectSetting 覆盖的类型(FLAT_OPTION_TYPES + groupselect) */\nexport const FLAT_SETTING_TYPES = ['select', 'radio', 'checkbox', 'groupselect'];\n/** 用 mode(default/multiple/tags) 表达单选多选的类型 */\nexport const MODE_TYPES = ['select', 'remoteselectpro', 'remotecomplete', 'remotetext'];\n/** 用 multiple(bool) 表达单选多选的类型 */\nexport const MULTIPLE_TYPES = ['treeselect', 'cascader', 'remotetreeselectpro', 'remotecascaderpro'];\n/** 选择类(运算符取 in/notin 的判定范围) */\nexport const SELECT_LIKE_TYPES = [...FLAT_OPTION_TYPES, 'treeselect', 'cascader', 'groupselect', 'remotefilterselect', ...MODE_TYPES, ...MULTIPLE_TYPES];\n/** 支持 maxlength / 前后缀标签的类型 */\nexport const TEXT_WITH_ADDON_TYPES = ['input', 'number'];\n/** 数值范围配置的类型 */\nexport const NUMBER_TYPES = ['number', 'money'];\n/** 日期时间类 */\nexport const DATE_TYPES = ['datepicker', 'monthpicker', 'monthrangepicker', 'timepicker', 'timerangepicker', 'rangepicker'];\n/** placeholder 为 [开始, 结束] 区间双输入的类型 */\nexport const RANGE_PLACEHOLDER_TYPES = ['rangepicker', 'timerangepicker', 'monthrangepicker'];\n/** 远程数据源类(数据源页适用) */\nexport const REMOTE_TYPES = ['remoteselectpro', 'remotecomplete', 'remotetreeselectpro', 'remotecascaderpro', 'remotetext'];\n\n/* ───────── 可选组件类型(SmallTableSerarchField 支持且适合搜索的子集,按 defaultConfig 分组) ───────── */\nexport const typeOptions = [\n {\n label: '输入类',\n options: [\n { value: 'input', label: '单行文本框' },\n { value: 'number', label: '数字' },\n { value: 'money', label: '金额' },\n { value: 'remotetext', label: '远程文本' },\n { value: 'remotecomplete', label: '自动补全文本框' },\n ],\n },\n {\n label: '选择类',\n options: [\n { value: 'select', label: '下拉选框' },\n { value: 'radio', label: '单选框' },\n { value: 'checkbox', label: '多选框' },\n { value: 'treeselect', label: '树形下拉' },\n { value: 'cascader', label: '级联下拉' },\n { value: 'remoteselectpro', label: '远程下拉选框' },\n { value: 'remotetreeselectpro', label: '远程树形下拉' },\n { value: 'remotecascaderpro', label: '远程级联下拉' },\n { value: 'remotefilterselect', label: '远程筛选下拉' },\n { value: 'groupselect', label: '分组下拉' },\n ],\n },\n {\n label: '日期/时间',\n options: [\n { value: 'datepicker', label: '日期时间选择器' },\n { value: 'rangepicker', label: '日期时间区间' },\n { value: 'monthpicker', label: '月份选择器' },\n { value: 'monthrangepicker', label: '月份区间' },\n { value: 'timepicker', label: '时间选择器' },\n { value: 'timerangepicker', label: '时间区间' },\n ],\n },\n];\n\n/* ───────── 本地静态选项契约(v29.29) ───────── */\n/** 选项是否有效:value 必须有真值语义(0 / false 合法,null / undefined / '' 视为未填)。\n * 供「选项列表」编辑与保存校验共用——空值选项在 a-select 里无法选中,落库即脏配置。 */\nexport const hasOptionValue = (o: any) =>\n !!o && o.value !== null && o.value !== undefined && o.value !== '';\n\n/* ───────── 各类型默认属性(port 自 defaultConfig.ts;搜索场景 select 默认多选) ───────── */\nexport const DS_DEFAULTS: Record<string, any> = {\n labelKeyType: 1,\n labelKeyList: [],\n labelSpace: '-',\n valueKey: 'id',\n labelKey: 'name',\n queryType: 'advanced',\n searchConditions: [[{ searchType: 3, field_name: 'is_active', operator: 'eq', value: [true], format: 'choices', class_name: 'bool' }]],\n simpleSearchConditions: [],\n};\nexport const REMOTE_BASE = { show_search: true, postUrl: '', ...DS_DEFAULTS };\nexport const TYPE_DEFAULTS: Record<string, any> = {\n input: { type: 'input', label: '单行文本框', inputType: 'text', maxlength: 100, allowClear: true, placeholder: '' },\n number: { type: 'number', label: '数字', inputType: 'number', precision: null, step: 1, allowClear: true, placeholder: '' },\n money: { type: 'money', label: '金额', precision: 2, allowClear: true, placeholder: '' },\n remotetext: { type: 'remotetext', label: '远程文本', mode: 'default', ...REMOTE_BASE },\n remotecomplete: { type: 'remotecomplete', label: '自动补全文本框', mode: 'default', ...REMOTE_BASE },\n /* ★ v29.29:本地静态选项类【不放 valueKey/labelKey】——选项对象统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 的唯一形状:`util/field_config.py` 产出 `options:[{label,value}]`\n 且不写这两个键;后端 `_adv_search_item` 由 content_map 自动推断下拉时同样只写 `{value,label}`)。\n 运行时 `SmallTableSerarchField.vue` 读的是 `option[valueKey||'value']` / `option[labelKey||'label']`,\n groupselect 更是把 options 直接丢给 a-select 的 `:options`(不认键)——所以一旦残留\n `labelKey:'name'`,`{value,label}` 的选项会整列渲染成空白。 */\n select: { type: 'select', label: '下拉选框', mode: 'multiple', show_search: false, options: [], allowClear: true, placeholder: '' },\n radio: { type: 'radio', label: '单选框', options: [] },\n checkbox: { type: 'checkbox', label: '多选框', options: [] },\n treeselect: { type: 'treeselect', label: '树形下拉选框', multiple: false, show_search: false, valueKey: 'value', labelKey: 'name', options: [], allowClear: true, placeholder: '' },\n cascader: { type: 'cascader', label: '级联下拉选框', multiple: false, show_search: false, valueKey: 'value', labelKey: 'name', options: [], allowClear: true, placeholder: '' },\n remotefilterselect: { type: 'remotefilterselect', label: '远程筛选下拉', ...REMOTE_BASE },\n groupselect: { type: 'groupselect', label: '分组下拉', show_search: false, options: [], allowClear: true, placeholder: '' },\n remoteselectpro: { type: 'remoteselectpro', label: '远程下拉选框', mode: 'default', ...REMOTE_BASE },\n remotetreeselectpro: { type: 'remotetreeselectpro', label: '远程树形下拉框', multiple: false, ...REMOTE_BASE },\n remotecascaderpro: { type: 'remotecascaderpro', label: '远程级联下拉框', multiple: false, show_search: false, ...REMOTE_BASE },\n datepicker: { type: 'datepicker', label: '日期时间选择器', showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', allowClear: true, placeholder: '' },\n rangepicker: { type: 'rangepicker', label: '日期时间区间', showTime: true, format: 'YYYY-MM-DD HH:mm:ss', valueFormat: 'YYYY-MM-DD HH:mm:ss', placeholder: ['开始时间', '结束时间'], allowClear: true },\n monthpicker: { type: 'monthpicker', label: '月份选择器', showTime: false, format: 'YYYY-MM', valueFormat: 'YYYY-MM-DD', allowClear: true, placeholder: '' },\n monthrangepicker: { type: 'monthrangepicker', label: '月份区间', picker: 'month', showTime: false, format: 'YYYY-MM', valueFormat: 'YYYY-MM-DD', placeholder: ['开始月份', '结束月份'], allowClear: true },\n timepicker: { type: 'timepicker', label: '时间选择器', format: 'HH:mm:ss', valueFormat: 'HH:mm:ss', allowClear: true, placeholder: '' },\n timerangepicker: { type: 'timerangepicker', label: '时间区间', format: 'HH:mm:ss', valueFormat: 'HH:mm:ss', placeholder: ['开始时间', '结束时间'], allowClear: true },\n};\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item label=\"组件类型\">\n <a-select\n :value=\"config.type\"\n placeholder=\"选择搜索组件\"\n show-search\n option-filter-prop=\"label\"\n :options=\"typeOptions\"\n @change=\"onTypeChange\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"名称\">\n <a-input\n v-model:value=\"config.label\"\n allow-clear\n :placeholder=\"record?.title || '默认=列中文名'\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"可清空\">\n <a-switch\n v-model:checked=\"config.allowClear\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"禁用\">\n <a-switch\n v-model:checked=\"config.disabled\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"isRangeType\"\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-space-compact\n block\n style=\"width: 100%\"\n >\n <a-input\n v-model:value=\"config.placeholder[0]\"\n placeholder=\"开始\"\n :style=\"{ width: '50%' }\"\n />\n <a-input\n v-model:value=\"config.placeholder[1]\"\n placeholder=\"结束\"\n :style=\"{ width: '50%' }\"\n />\n </a-space-compact>\n </a-form-item>\n </a-col>\n <a-col\n v-else\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-input\n v-model:value=\"config.placeholder\"\n allow-clear\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\n/* 基础属性(所有组件类型共享):类型选择 + label + 可清空/禁用 + placeholder(区间类型双输入)。\n 类型切换交给父组件处理(需要合并类型默认值)。 */\nimport { computed } from 'vue';\nimport { RANGE_PLACEHOLDER_TYPES, typeOptions } from './constants';\n\nconst props = defineProps<{\n config: any;\n record: any; // 列配置行(取列中文名作 label 默认值)\n}>();\nconst emit = defineEmits<{ (e: 'type-change', type: string): void }>();\n\nconst isRangeType = computed(() => RANGE_PLACEHOLDER_TYPES.includes(props.config?.type));\nconst onTypeChange = (type: any) => emit('type-change', type);\n</script>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item label=\"组件类型\">\n <a-select\n :value=\"config.type\"\n placeholder=\"选择搜索组件\"\n show-search\n option-filter-prop=\"label\"\n :options=\"typeOptions\"\n @change=\"onTypeChange\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"名称\">\n <a-input\n v-model:value=\"config.label\"\n allow-clear\n :placeholder=\"record?.title || '默认=列中文名'\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"可清空\">\n <a-switch\n v-model:checked=\"config.allowClear\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"禁用\">\n <a-switch\n v-model:checked=\"config.disabled\"\n checked-children=\"开\"\n un-checked-children=\"关\"\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"isRangeType\"\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-space-compact\n block\n style=\"width: 100%\"\n >\n <a-input\n v-model:value=\"config.placeholder[0]\"\n placeholder=\"开始\"\n :style=\"{ width: '50%' }\"\n />\n <a-input\n v-model:value=\"config.placeholder[1]\"\n placeholder=\"结束\"\n :style=\"{ width: '50%' }\"\n />\n </a-space-compact>\n </a-form-item>\n </a-col>\n <a-col\n v-else\n :span=\"24\"\n >\n <a-form-item label=\"提示语\">\n <a-input\n v-model:value=\"config.placeholder\"\n allow-clear\n :disabled=\"!config.type\"\n />\n </a-form-item>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\n/* 基础属性(所有组件类型共享):类型选择 + label + 可清空/禁用 + placeholder(区间类型双输入)。\n 类型切换交给父组件处理(需要合并类型默认值)。 */\nimport { computed } from 'vue';\nimport { RANGE_PLACEHOLDER_TYPES, typeOptions } from './constants';\n\nconst props = defineProps<{\n config: any;\n record: any; // 列配置行(取列中文名作 label 默认值)\n}>();\nconst emit = defineEmits<{ (e: 'type-change', type: string): void }>();\n\nconst isRangeType = computed(() => RANGE_PLACEHOLDER_TYPES.includes(props.config?.type));\nconst onTypeChange = (type: any) => emit('type-change', type);\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"数值字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.valueKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.valueKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入数值字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"!remote || config.labelKeyType === 1\"\n :span=\"12\"\n >\n <a-form-item label=\"显示字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.labelKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.labelKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入显示字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值字段/显示字段配置(共享):\n 本地选项组件(select/radio/checkbox/groupselect/treeselect/cascader)用 a-input;\n 远程组件用 a-auto-complete(候选来自数据源字段列表,labelKeyType=2 时 labelKey 由 labelKeyList 编辑)。 */\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n config: any;\n remote?: boolean;\n options?: any[]; // 远程字段候选(value/label)\n loading?: boolean;\n}>();\n\nconst remote = computed(() => !!props.remote);\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"数值字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.valueKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.valueKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入数值字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"!remote || config.labelKeyType === 1\"\n :span=\"12\"\n >\n <a-form-item label=\"显示字段\">\n <a-input\n v-if=\"!remote\"\n v-model:value=\"config.labelKey\"\n allow-clear\n />\n <a-auto-complete\n v-else\n v-model:value=\"config.labelKey\"\n :options=\"options\"\n :loading=\"loading\"\n placeholder=\"请输入显示字段\"\n allow-clear\n :filter-option=\"filterOptionContains\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值字段/显示字段配置(共享):\n 本地选项组件(select/radio/checkbox/groupselect/treeselect/cascader)用 a-input;\n 远程组件用 a-auto-complete(候选来自数据源字段列表,labelKeyType=2 时 labelKey 由 labelKeyList 编辑)。 */\nimport { computed } from 'vue';\n\nconst props = defineProps<{\n config: any;\n remote?: boolean;\n options?: any[]; // 远程字段候选(value/label)\n loading?: boolean;\n}>();\n\nconst remote = computed(() => !!props.remote);\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n</script>\n","<template>\n <!-- ★ v29.29:本地静态选项列表(select/radio/checkbox/groupselect)——原版\n FormDesignModal/FieldSetting.vue「数据源」页的「选项列表」移植(原版仅含这三类)。\n 落库形状对齐 lybase ADVANCED_SEARCH + choices 契约:`config.options = [{value, label}]`\n (`util/field_config.py` 由 Django choices/BooleanField 产出的就是这一形状,且不写 valueKey/labelKey;\n 运行时 `SmallTableSerarchField.vue` 按 `option[valueKey||'value'] / option[labelKey||'label']` 取值)。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>选项列表</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRow\"\n >\n + 添加选项\n </a-button>\n <a-button\n v-if=\"importable\"\n size=\"small\"\n @click=\"importFromContentMap\"\n >\n 从「转换/颜色」导入\n </a-button>\n <span class=\"opt-tip\">值类型按该列在 SQL 中的实际类型选择(数值列勿用字符,Oracle 下会直接比较失败)。</span>\n </div>\n <div\n v-if=\"!rows.length\"\n class=\"opt-empty\"\n >\n 暂无选项 —— 下拉选框/单选框/多选框至少需要一个选项才能使用。\n </div>\n <div\n v-for=\"(row, i) in rows\"\n :key=\"i\"\n class=\"row-line\"\n >\n <a-select\n v-model:value=\"row.valueType\"\n class=\"w-type\"\n :options=\"VALUE_TYPE_OPTIONS\"\n @change=\"onValueTypeChange(row)\"\n />\n <a-select\n v-if=\"row.valueType === 'bool'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input-number\n v-else-if=\"row.valueType === 'int'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n />\n <a-input\n v-else\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n allow-clear\n />\n <a-input\n v-model:value=\"row.label\"\n class=\"w-label\"\n placeholder=\"显示文本\"\n allow-clear\n />\n <a-button\n type=\"link\"\n danger\n @click=\"rows.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 选项列表编辑器:本地维护 rows(含 valueType,便于按类型选输入控件),\n 任何编辑立即回写 `config.options = [{value, label}]`(实时镜像 JSON 页的同源语义)。\n 与 TreeDataConfig 同款「config 引用变化 → 重新初始化」做法:外壳切类型/JSON 页回写会整体替换 config。 */\nimport { nextTick, ref, watch } from 'vue';\nimport { BOOL_OPTIONS } from './constants';\n\nconst props = defineProps<{\n config: any;\n /** 列配置行:用 class_name 定新选项的值类型默认值、用 content_map 做「导入」 */\n record?: any;\n}>();\n\nconst VALUE_TYPE_OPTIONS = [\n { value: 'int', label: '数字' },\n { value: 'str', label: '字符' },\n { value: 'bool', label: '布尔' },\n];\n\nconst rows = ref<any[]>([]);\n/* 回写自身触发的同步重入守卫:写回 config.options 会命中下面的 watch,靠「内容相等就跳过」收敛 */\nlet syncing = false;\n\nconst inferValueType = (v: any) =>\n typeof v === 'boolean' ? 'bool' : typeof v === 'number' ? 'int' : 'str';\n\n/** SQL 列类型 → 默认值类型(class_name 由列探测写入) */\nconst defaultValueType = () => {\n const cls = String(props.record?.class_name || '').toLowerCase();\n if (cls === 'int' || cls === 'float' || cls === 'decimal' || cls === 'number') return 'int';\n if (cls === 'bool' || cls === 'boolean') return 'bool';\n return 'str';\n};\n\nconst syncFromConfig = () => {\n const opts = Array.isArray(props.config?.options) ? props.config.options : [];\n const next = opts.map((o: any) => ({\n valueType: inferValueType(o?.value),\n value: o?.value ?? null,\n // 兼容历史上按创建/编辑表单形状写的 {value, name}(发布前的老配置)\n label: o?.label ?? o?.name ?? '',\n }));\n if (JSON.stringify(next) === JSON.stringify(rows.value)) return; // 无变化 → 不动,避免回写循环\n syncing = true;\n rows.value = next;\n nextTick(() => {\n syncing = false;\n });\n};\nwatch(() => props.config?.options, syncFromConfig, { immediate: true });\n\n/* 值类型切换:把已填的值按新类型归一(否则 a-select 拿 true 去比对字符串 'true' 会不匹配) */\nconst coerceValue = (row: any) => {\n const v = row.value;\n if (row.valueType === 'bool') {\n if (v === true || v === 'true' || v === 1 || v === '1') return true;\n if (v === false || v === 'false' || v === 0 || v === '0') return false;\n return null;\n }\n if (row.valueType === 'int') {\n if (v === null || v === undefined || v === '') return null;\n const n = Number(v);\n return Number.isNaN(n) ? null : n;\n }\n return v === null || v === undefined ? '' : String(v);\n};\nconst onValueTypeChange = (row: any) => {\n row.value = coerceValue(row);\n};\n\nconst writeBack = () => {\n if (syncing) return;\n props.config.options = rows.value.map((r: any) => ({\n value: coerceValue(r),\n label: r.label == null ? '' : String(r.label),\n }));\n};\nwatch(rows, writeBack, { deep: true });\n\nconst addRow = () => {\n rows.value.push({ valueType: defaultValueType(), value: null, label: '' });\n};\n\n/* ───────── 从「转换/颜色」(content_map) 导入 ─────────\n ★ 必要性:后端 `_adv_search_item` 只在【没有人工搜索组件】时才用 content_map 自动推断\n `type='select' + options`。一旦人工把组件配成下拉,自动推断即让位 → 手工配置的下拉会\n 覆盖掉原本可用的自动选项。这里给一键回填,避免「配了转换/颜色 + 手工下拉」后选项为空。 */\nconst importable = ref(false);\nwatch(\n () => props.record?.content_map,\n (cm) => {\n importable.value = !!cm && typeof cm === 'object' && !Array.isArray(cm) && Object.keys(cm).length > 0;\n },\n { immediate: true },\n);\n\nconst importFromContentMap = () => {\n const cm = props.record?.content_map;\n if (!cm || typeof cm !== 'object') return;\n const vt = defaultValueType();\n const exist = new Set(rows.value.map((r: any) => String(coerceValue(r))));\n Object.keys(cm).forEach((k) => {\n const raw = vt === 'int' ? Number(k) : vt === 'bool' ? k === 'true' || k === '1' : k;\n const value = vt === 'int' && Number.isNaN(raw as number) ? k : raw;\n if (exist.has(String(value))) return;\n exist.add(String(value));\n rows.value.push({ valueType: vt, value, label: cm[k] ? String(cm[k]) : k });\n });\n};\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.opt-tip {\n font-size: 12px;\n font-weight: 400;\n color: rgba(0, 0, 0, 0.45);\n}\n.opt-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.w-type {\n flex: none;\n width: 100px;\n}\n.w-val {\n flex: 1 1 0;\n min-width: 140px;\n}\n.w-label {\n flex: 1 1 0;\n min-width: 180px;\n}\n</style>\n","<template>\n <!-- ★ v29.29:本地静态选项列表(select/radio/checkbox/groupselect)——原版\n FormDesignModal/FieldSetting.vue「数据源」页的「选项列表」移植(原版仅含这三类)。\n 落库形状对齐 lybase ADVANCED_SEARCH + choices 契约:`config.options = [{value, label}]`\n (`util/field_config.py` 由 Django choices/BooleanField 产出的就是这一形状,且不写 valueKey/labelKey;\n 运行时 `SmallTableSerarchField.vue` 按 `option[valueKey||'value'] / option[labelKey||'label']` 取值)。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>选项列表</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRow\"\n >\n + 添加选项\n </a-button>\n <a-button\n v-if=\"importable\"\n size=\"small\"\n @click=\"importFromContentMap\"\n >\n 从「转换/颜色」导入\n </a-button>\n <span class=\"opt-tip\">值类型按该列在 SQL 中的实际类型选择(数值列勿用字符,Oracle 下会直接比较失败)。</span>\n </div>\n <div\n v-if=\"!rows.length\"\n class=\"opt-empty\"\n >\n 暂无选项 —— 下拉选框/单选框/多选框至少需要一个选项才能使用。\n </div>\n <div\n v-for=\"(row, i) in rows\"\n :key=\"i\"\n class=\"row-line\"\n >\n <a-select\n v-model:value=\"row.valueType\"\n class=\"w-type\"\n :options=\"VALUE_TYPE_OPTIONS\"\n @change=\"onValueTypeChange(row)\"\n />\n <a-select\n v-if=\"row.valueType === 'bool'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input-number\n v-else-if=\"row.valueType === 'int'\"\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n />\n <a-input\n v-else\n v-model:value=\"row.value\"\n class=\"w-val\"\n placeholder=\"值\"\n allow-clear\n />\n <a-input\n v-model:value=\"row.label\"\n class=\"w-label\"\n placeholder=\"显示文本\"\n allow-clear\n />\n <a-button\n type=\"link\"\n danger\n @click=\"rows.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 选项列表编辑器:本地维护 rows(含 valueType,便于按类型选输入控件),\n 任何编辑立即回写 `config.options = [{value, label}]`(实时镜像 JSON 页的同源语义)。\n 与 TreeDataConfig 同款「config 引用变化 → 重新初始化」做法:外壳切类型/JSON 页回写会整体替换 config。 */\nimport { nextTick, ref, watch } from 'vue';\nimport { BOOL_OPTIONS } from './constants';\n\nconst props = defineProps<{\n config: any;\n /** 列配置行:用 class_name 定新选项的值类型默认值、用 content_map 做「导入」 */\n record?: any;\n}>();\n\nconst VALUE_TYPE_OPTIONS = [\n { value: 'int', label: '数字' },\n { value: 'str', label: '字符' },\n { value: 'bool', label: '布尔' },\n];\n\nconst rows = ref<any[]>([]);\n/* 回写自身触发的同步重入守卫:写回 config.options 会命中下面的 watch,靠「内容相等就跳过」收敛 */\nlet syncing = false;\n\nconst inferValueType = (v: any) =>\n typeof v === 'boolean' ? 'bool' : typeof v === 'number' ? 'int' : 'str';\n\n/** SQL 列类型 → 默认值类型(class_name 由列探测写入) */\nconst defaultValueType = () => {\n const cls = String(props.record?.class_name || '').toLowerCase();\n if (cls === 'int' || cls === 'float' || cls === 'decimal' || cls === 'number') return 'int';\n if (cls === 'bool' || cls === 'boolean') return 'bool';\n return 'str';\n};\n\nconst syncFromConfig = () => {\n const opts = Array.isArray(props.config?.options) ? props.config.options : [];\n const next = opts.map((o: any) => ({\n valueType: inferValueType(o?.value),\n value: o?.value ?? null,\n // 兼容历史上按创建/编辑表单形状写的 {value, name}(发布前的老配置)\n label: o?.label ?? o?.name ?? '',\n }));\n if (JSON.stringify(next) === JSON.stringify(rows.value)) return; // 无变化 → 不动,避免回写循环\n syncing = true;\n rows.value = next;\n nextTick(() => {\n syncing = false;\n });\n};\nwatch(() => props.config?.options, syncFromConfig, { immediate: true });\n\n/* 值类型切换:把已填的值按新类型归一(否则 a-select 拿 true 去比对字符串 'true' 会不匹配) */\nconst coerceValue = (row: any) => {\n const v = row.value;\n if (row.valueType === 'bool') {\n if (v === true || v === 'true' || v === 1 || v === '1') return true;\n if (v === false || v === 'false' || v === 0 || v === '0') return false;\n return null;\n }\n if (row.valueType === 'int') {\n if (v === null || v === undefined || v === '') return null;\n const n = Number(v);\n return Number.isNaN(n) ? null : n;\n }\n return v === null || v === undefined ? '' : String(v);\n};\nconst onValueTypeChange = (row: any) => {\n row.value = coerceValue(row);\n};\n\nconst writeBack = () => {\n if (syncing) return;\n props.config.options = rows.value.map((r: any) => ({\n value: coerceValue(r),\n label: r.label == null ? '' : String(r.label),\n }));\n};\nwatch(rows, writeBack, { deep: true });\n\nconst addRow = () => {\n rows.value.push({ valueType: defaultValueType(), value: null, label: '' });\n};\n\n/* ───────── 从「转换/颜色」(content_map) 导入 ─────────\n ★ 必要性:后端 `_adv_search_item` 只在【没有人工搜索组件】时才用 content_map 自动推断\n `type='select' + options`。一旦人工把组件配成下拉,自动推断即让位 → 手工配置的下拉会\n 覆盖掉原本可用的自动选项。这里给一键回填,避免「配了转换/颜色 + 手工下拉」后选项为空。 */\nconst importable = ref(false);\nwatch(\n () => props.record?.content_map,\n (cm) => {\n importable.value = !!cm && typeof cm === 'object' && !Array.isArray(cm) && Object.keys(cm).length > 0;\n },\n { immediate: true },\n);\n\nconst importFromContentMap = () => {\n const cm = props.record?.content_map;\n if (!cm || typeof cm !== 'object') return;\n const vt = defaultValueType();\n const exist = new Set(rows.value.map((r: any) => String(coerceValue(r))));\n Object.keys(cm).forEach((k) => {\n const raw = vt === 'int' ? Number(k) : vt === 'bool' ? k === 'true' || k === '1' : k;\n const value = vt === 'int' && Number.isNaN(raw as number) ? k : raw;\n if (exist.has(String(value))) return;\n exist.add(String(value));\n rows.value.push({ valueType: vt, value, label: cm[k] ? String(cm[k]) : k });\n });\n};\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.opt-tip {\n font-size: 12px;\n font-weight: 400;\n color: rgba(0, 0, 0, 0.45);\n}\n.opt-empty {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 8px;\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.w-type {\n flex: none;\n width: 100px;\n}\n.w-val {\n flex: 1 1 0;\n min-width: 140px;\n}\n.w-label {\n flex: 1 1 0;\n min-width: 180px;\n}\n</style>\n","<template>\n <a-auto-complete\n :value=\"value\"\n :loading=\"loading\"\n style=\"width: 100%\"\n :options=\"options\"\n @update:value=\"handleUpdate\"\n @search=\"handleSearch\"\n >\n <template #option=\"{ value: v, label: l }\">\n <div class=\"opt-line opt-label\">{{ l }}</div>\n <div class=\"opt-line opt-value\">{{ v }}</div>\n </template>\n <template #default>\n <a-input-search\n :placeholder=\"placeholder\"\n :value=\"value\"\n size=\"middle\"\n @search=\"handleSearch\"\n >\n <template #suffix>\n <LoadingOutlined v-if=\"loading\" />\n <span\n v-else\n :title=\"findItem(value)?.label\"\n class=\"post-url-suffix\"\n >\n {{ findItem(value)?.label }}\n </span>\n </template>\n </a-input-search>\n </template>\n </a-auto-complete>\n</template>\n\n<script setup lang=\"ts\">\n/* 数据源(postUrl)选择字段(port 自 FormDesignModal/BtnConfig/columnNameField.vue):\n - a-auto-complete 变体:选项双行显示(第一行=数据源名称,第二行=URL);\n - 输入框为 a-input-search,suffix 实时显示当前选中数据源的名称(非裸 URL);\n - @search 输入本地过滤 value/label,同时 emit('search') 供父级做 >100 条时的远程过滤;\n - 选中项变化时 emit('select', item)(item 含 data 原始数据)供父级自动回填。 */\nimport { LoadingOutlined } from '@ant-design/icons-vue';\nimport { computed, ref } from 'vue';\n\nconst props = defineProps<{\n value: string;\n itemList: any[]; // [{ value: post_url, label: name, data: item }]\n placeholder?: string;\n loading?: boolean;\n}>();\nconst emit = defineEmits<{\n (e: 'update:value', value: string): void;\n (e: 'select', item: any): void;\n (e: 'search', str: string): void;\n}>();\n\n/* ★ v29.22.1:options 由 itemList + 搜索词实时 computed 派生(此前是独立 ref,只在 @search 时\n 重建——itemList 被父级远程搜索替换后 options 停在旧子集,且初始为空数组首开无选项)。 */\nconst searchText = ref('');\nconst lastSelected = ref<any>(null);\nconst findItem = (v: any) => props.itemList.find((item: any) => item.value === v) as any;\n\nconst options = computed(() =>\n props.itemList\n .filter(\n (item: any) =>\n !searchText.value ||\n item.value.includes(searchText.value) ||\n (item.label || '').includes(searchText.value),\n )\n .map((item: any) => ({ value: item.value, label: item.label })),\n);\n\nconst handleSearch = (str: any) => {\n searchText.value = str || '';\n emit('search', str);\n};\n\nconst handleUpdate = (v: any) => {\n emit('update:value', v);\n const currentSelected = findItem(v);\n if (currentSelected && currentSelected.value !== lastSelected.value) {\n emit('select', currentSelected);\n lastSelected.value = currentSelected.value;\n /* 选中后重置本地过滤词:重新打开下拉时展示完整列表(而非停在关键字子集) */\n searchText.value = '';\n }\n};\n</script>\n\n<style scoped lang=\"less\">\n.opt-line {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n &.opt-label {\n font-size: 14px;\n color: #2f2f2f;\n }\n &.opt-value {\n font-size: 12px;\n color: #999;\n }\n}\n.post-url-suffix {\n font-size: 12px;\n color: #999;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n","<template>\n <a-auto-complete\n :value=\"value\"\n :loading=\"loading\"\n style=\"width: 100%\"\n :options=\"options\"\n @update:value=\"handleUpdate\"\n @search=\"handleSearch\"\n >\n <template #option=\"{ value: v, label: l }\">\n <div class=\"opt-line opt-label\">{{ l }}</div>\n <div class=\"opt-line opt-value\">{{ v }}</div>\n </template>\n <template #default>\n <a-input-search\n :placeholder=\"placeholder\"\n :value=\"value\"\n size=\"middle\"\n @search=\"handleSearch\"\n >\n <template #suffix>\n <LoadingOutlined v-if=\"loading\" />\n <span\n v-else\n :title=\"findItem(value)?.label\"\n class=\"post-url-suffix\"\n >\n {{ findItem(value)?.label }}\n </span>\n </template>\n </a-input-search>\n </template>\n </a-auto-complete>\n</template>\n\n<script setup lang=\"ts\">\n/* 数据源(postUrl)选择字段(port 自 FormDesignModal/BtnConfig/columnNameField.vue):\n - a-auto-complete 变体:选项双行显示(第一行=数据源名称,第二行=URL);\n - 输入框为 a-input-search,suffix 实时显示当前选中数据源的名称(非裸 URL);\n - @search 输入本地过滤 value/label,同时 emit('search') 供父级做 >100 条时的远程过滤;\n - 选中项变化时 emit('select', item)(item 含 data 原始数据)供父级自动回填。 */\nimport { LoadingOutlined } from '@ant-design/icons-vue';\nimport { computed, ref } from 'vue';\n\nconst props = defineProps<{\n value: string;\n itemList: any[]; // [{ value: post_url, label: name, data: item }]\n placeholder?: string;\n loading?: boolean;\n}>();\nconst emit = defineEmits<{\n (e: 'update:value', value: string): void;\n (e: 'select', item: any): void;\n (e: 'search', str: string): void;\n}>();\n\n/* ★ v29.22.1:options 由 itemList + 搜索词实时 computed 派生(此前是独立 ref,只在 @search 时\n 重建——itemList 被父级远程搜索替换后 options 停在旧子集,且初始为空数组首开无选项)。 */\nconst searchText = ref('');\nconst lastSelected = ref<any>(null);\nconst findItem = (v: any) => props.itemList.find((item: any) => item.value === v) as any;\n\nconst options = computed(() =>\n props.itemList\n .filter(\n (item: any) =>\n !searchText.value ||\n item.value.includes(searchText.value) ||\n (item.label || '').includes(searchText.value),\n )\n .map((item: any) => ({ value: item.value, label: item.label })),\n);\n\nconst handleSearch = (str: any) => {\n searchText.value = str || '';\n emit('search', str);\n};\n\nconst handleUpdate = (v: any) => {\n emit('update:value', v);\n const currentSelected = findItem(v);\n if (currentSelected && currentSelected.value !== lastSelected.value) {\n emit('select', currentSelected);\n lastSelected.value = currentSelected.value;\n /* 选中后重置本地过滤词:重新打开下拉时展示完整列表(而非停在关键字子集) */\n searchText.value = '';\n }\n};\n</script>\n\n<style scoped lang=\"less\">\n.opt-line {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n &.opt-label {\n font-size: 14px;\n color: #2f2f2f;\n }\n &.opt-value {\n font-size: 12px;\n color: #999;\n }\n}\n.post-url-suffix {\n font-size: 12px;\n color: #999;\n max-width: 150px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n","<template>\n <!-- 查询参数(queryType):高级=条件分组编辑器(监听/固定/上下文 + 运算符 + 固定值);\n 简单=JSON value 条目。port 自 FieldSetting 查询条件配置。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>查询参数</span>\n <a-radio-group\n v-model:value=\"config.queryType\"\n size=\"small\"\n >\n <a-radio-button value=\"simple\">简单查询</a-radio-button>\n <a-radio-button value=\"advanced\">高级查询</a-radio-button>\n </a-radio-group>\n <a-button\n v-if=\"config.queryType === 'advanced'\"\n type=\"primary\"\n size=\"small\"\n @click=\"addSearchConditions\"\n >\n + 添加查询条件分组\n </a-button>\n <a-button\n v-else\n type=\"primary\"\n size=\"small\"\n @click=\"addSimpleSearchItem\"\n >\n + 添加查询条件\n </a-button>\n </div>\n </a-col>\n\n <!-- 高级查询:条件分组编辑器 -->\n <a-col\n v-if=\"config.queryType === 'advanced'\"\n :span=\"24\"\n >\n <div\n v-for=\"(group, gi) in config.searchConditions || []\"\n :key=\"gi\"\n class=\"cond-group\"\n >\n <div class=\"group-header\">\n <span class=\"group-title\">查询条件组 {{ gi + 1 }}</span>\n <a-space>\n <a-button\n size=\"small\"\n type=\"link\"\n @click=\"addSearchItem(gi)\"\n >\n + 添加查询条件\n </a-button>\n <a-button\n size=\"small\"\n type=\"link\"\n danger\n @click=\"config.searchConditions.splice(gi, 1)\"\n >\n 删除分组\n </a-button>\n </a-space>\n </div>\n <div\n v-for=\"(item, ii) in group\"\n :key=\"ii\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ ii + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 160px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"group.splice(ii, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div class=\"cond-row\">\n <a-select\n v-model:value=\"item.format\"\n style=\"width: 110px\"\n :options=\"FORMAT_TYPE.map((f) => ({ value: f, label: f }))\"\n />\n <a-select\n v-model:value=\"item.operator\"\n style=\"width: 190px\"\n placeholder=\"判断条件\"\n :options=\"operatorOptions\"\n @change=\"ensureValueShape(item)\"\n />\n <a-select\n v-model:value=\"item.class_name\"\n style=\"width: 110px\"\n placeholder=\"数值类型\"\n :options=\"SEARCH_CLASS_NAME.map((c) => ({ value: c, label: c }))\"\n @change=\"item.value = []\"\n />\n <!-- 固定查询才可填值 -->\n <template v-if=\"item.searchType === 3\">\n <template v-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'str'\">\n <a-input\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'int'\">\n <a-input-number\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input-number\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'datetime'\">\n <a-range-picker\n v-model:value=\"item.value\"\n show-time\n style=\"width: 260px\"\n />\n </template>\n <a-select\n v-else-if=\"item.class_name === 'bool'\"\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input\n v-else-if=\"item.operator === 'not_blank_not_null'\"\n value=\"\"\n style=\"width: 150px\"\n disabled\n placeholder=\"(无值)\"\n />\n <a-input\n v-else\n v-model:value=\"item.value[0]\"\n style=\"width: 150px\"\n placeholder=\"值\"\n allow-clear\n />\n </template>\n </div>\n </div>\n </div>\n </a-col>\n\n <!-- 简单查询 -->\n <a-col\n v-if=\"config.queryType === 'simple'\"\n :span=\"24\"\n >\n <div class=\"cond-group\">\n <div\n v-for=\"(item, si) in config.simpleSearchConditions || []\"\n :key=\"si\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ si + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 170px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"config.simpleSearchConditions.splice(si, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div\n v-if=\"item.searchType === 3\"\n class=\"cond-row\"\n >\n <a-textarea\n :value=\"typeof item.value === 'object' ? JSON.stringify(item.value) : item.value\"\n placeholder=\"请输入值(对象/数组/字符串)\"\n :auto-size=\"{ minRows: 2, maxRows: 6 }\"\n style=\"flex: 1\"\n @input=\"(e: any) => handleTextareaInput(e, item)\"\n />\n <a-tag\n v-if=\"item.value !== '' && item.value != null\"\n color=\"blue\"\n >\n {{ getDataType(item.value) }}\n </a-tag>\n </div>\n </div>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport {\n BOOL_OPTIONS,\n DATE_OPS,\n DATE_TYPES,\n FORMAT_TYPE,\n NUMBER_OPS,\n NUMBER_TYPES,\n OPERATOR,\n SEARCH_CLASS_NAME,\n SELECT_LIKE_TYPES,\n SELECT_OPS,\n TEXT_OPS,\n} from './constants';\n\nconst props = defineProps<{\n config: any;\n fieldOptions: any[]; // 数据源字段候选\n fieldOptionsLoading?: boolean;\n}>();\n\nconst SEARCH_TYPE_OPTIONS = [\n { value: 1, label: '监听查询' },\n { value: 3, label: '固定查询' },\n { value: 4, label: '上下文' },\n];\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* 运算符候选按组件类型过滤 */\nconst operatorOptions = computed(() => {\n const t = props.config?.type;\n let ops = TEXT_OPS;\n if (NUMBER_TYPES.includes(t)) ops = NUMBER_OPS;\n else if (DATE_TYPES.includes(t)) ops = DATE_OPS;\n else if (SELECT_LIKE_TYPES.includes(t)) ops = SELECT_OPS;\n return Object.keys(OPERATOR)\n .filter((k) => ops.includes(k))\n .map((k) => ({ value: k, label: `${OPERATOR[k]}(${k})` }));\n});\n\nconst addSearchConditions = () => {\n if (!Array.isArray(props.config.searchConditions)) props.config.searchConditions = [];\n props.config.searchConditions.push([{ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] }]);\n};\nconst addSearchItem = (gi: number) => {\n props.config.searchConditions[gi].push({ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] });\n};\n/* operator/class_name 变化时保证 value 形状(between 需两位) */\nconst ensureValueShape = (item: any) => {\n if (item.operator === 'between' || item.operator === 'notbetween') {\n const v0 = Array.isArray(item.value) ? item.value[0] : item.value;\n const v1 = Array.isArray(item.value) ? item.value[1] : undefined;\n item.value = [v0 ?? null, v1 ?? null];\n } else if (!Array.isArray(item.value)) {\n item.value = [item.value];\n } else if (item.value.length > 1) {\n item.value = [item.value[0]];\n }\n};\n\nconst addSimpleSearchItem = () => {\n if (!Array.isArray(props.config.simpleSearchConditions)) props.config.simpleSearchConditions = [];\n props.config.simpleSearchConditions.push({ searchType: 3, field_name: '', value: '' });\n};\nconst handleTextareaInput = (e: any, item: any) => {\n try {\n item.value = JSON.parse(e.target.value);\n } catch {\n item.value = e.target.value;\n }\n};\nconst getDataType = (value: any) => (typeof value === 'object' ? 'json' : typeof value);\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.cond-group {\n border: 1px solid rgba(0, 0, 0, 0.08);\n border-radius: 6px;\n padding: 8px;\n margin-bottom: 8px;\n}\n.group-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n .group-title {\n font-weight: 500;\n }\n}\n.cond-item {\n padding: 4px 0;\n border-top: 1px dashed rgba(0, 0, 0, 0.06);\n}\n.cond-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 4px 0;\n flex-wrap: wrap;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n.range-sep {\n text-align: center;\n}\n</style>\n","<template>\n <!-- 查询参数(queryType):高级=条件分组编辑器(监听/固定/上下文 + 运算符 + 固定值);\n 简单=JSON value 条目。port 自 FieldSetting 查询条件配置。 -->\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>查询参数</span>\n <a-radio-group\n v-model:value=\"config.queryType\"\n size=\"small\"\n >\n <a-radio-button value=\"simple\">简单查询</a-radio-button>\n <a-radio-button value=\"advanced\">高级查询</a-radio-button>\n </a-radio-group>\n <a-button\n v-if=\"config.queryType === 'advanced'\"\n type=\"primary\"\n size=\"small\"\n @click=\"addSearchConditions\"\n >\n + 添加查询条件分组\n </a-button>\n <a-button\n v-else\n type=\"primary\"\n size=\"small\"\n @click=\"addSimpleSearchItem\"\n >\n + 添加查询条件\n </a-button>\n </div>\n </a-col>\n\n <!-- 高级查询:条件分组编辑器 -->\n <a-col\n v-if=\"config.queryType === 'advanced'\"\n :span=\"24\"\n >\n <div\n v-for=\"(group, gi) in config.searchConditions || []\"\n :key=\"gi\"\n class=\"cond-group\"\n >\n <div class=\"group-header\">\n <span class=\"group-title\">查询条件组 {{ gi + 1 }}</span>\n <a-space>\n <a-button\n size=\"small\"\n type=\"link\"\n @click=\"addSearchItem(gi)\"\n >\n + 添加查询条件\n </a-button>\n <a-button\n size=\"small\"\n type=\"link\"\n danger\n @click=\"config.searchConditions.splice(gi, 1)\"\n >\n 删除分组\n </a-button>\n </a-space>\n </div>\n <div\n v-for=\"(item, ii) in group\"\n :key=\"ii\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ ii + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 160px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"group.splice(ii, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div class=\"cond-row\">\n <a-select\n v-model:value=\"item.format\"\n style=\"width: 110px\"\n :options=\"FORMAT_TYPE.map((f) => ({ value: f, label: f }))\"\n />\n <a-select\n v-model:value=\"item.operator\"\n style=\"width: 190px\"\n placeholder=\"判断条件\"\n :options=\"operatorOptions\"\n @change=\"ensureValueShape(item)\"\n />\n <a-select\n v-model:value=\"item.class_name\"\n style=\"width: 110px\"\n placeholder=\"数值类型\"\n :options=\"SEARCH_CLASS_NAME.map((c) => ({ value: c, label: c }))\"\n @change=\"item.value = []\"\n />\n <!-- 固定查询才可填值 -->\n <template v-if=\"item.searchType === 3\">\n <template v-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'str'\">\n <a-input\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'int'\">\n <a-input-number\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n placeholder=\"起\"\n />\n <span class=\"range-sep\">_</span>\n <a-input-number\n v-model:value=\"item.value[1]\"\n style=\"width: 110px\"\n placeholder=\"止\"\n />\n </template>\n <template v-else-if=\"(item.operator === 'between' || item.operator === 'notbetween') && item.class_name === 'datetime'\">\n <a-range-picker\n v-model:value=\"item.value\"\n show-time\n style=\"width: 260px\"\n />\n </template>\n <a-select\n v-else-if=\"item.class_name === 'bool'\"\n v-model:value=\"item.value[0]\"\n style=\"width: 110px\"\n :options=\"BOOL_OPTIONS\"\n />\n <a-input\n v-else-if=\"item.operator === 'not_blank_not_null'\"\n value=\"\"\n style=\"width: 150px\"\n disabled\n placeholder=\"(无值)\"\n />\n <a-input\n v-else\n v-model:value=\"item.value[0]\"\n style=\"width: 150px\"\n placeholder=\"值\"\n allow-clear\n />\n </template>\n </div>\n </div>\n </div>\n </a-col>\n\n <!-- 简单查询 -->\n <a-col\n v-if=\"config.queryType === 'simple'\"\n :span=\"24\"\n >\n <div class=\"cond-group\">\n <div\n v-for=\"(item, si) in config.simpleSearchConditions || []\"\n :key=\"si\"\n class=\"cond-item\"\n >\n <div class=\"cond-row\">\n <span class=\"row-no\">({{ si + 1 }})</span>\n <a-select\n v-model:value=\"item.searchType\"\n style=\"width: 130px\"\n placeholder=\"查询类型\"\n :options=\"SEARCH_TYPE_OPTIONS\"\n />\n <a-input\n v-if=\"item.searchType === 1 || item.searchType === 4\"\n v-model:value=\"item.select_name\"\n style=\"width: 170px\"\n :placeholder=\"item.searchType === 1 ? '监听字段' : '上下文键'\"\n />\n <a-auto-complete\n v-model:value=\"item.field_name\"\n :options=\"fieldOptions\"\n :loading=\"fieldOptionsLoading\"\n style=\"width: 180px\"\n placeholder=\"查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"text\"\n danger\n @click=\"config.simpleSearchConditions.splice(si, 1)\"\n >\n ✕\n </a-button>\n </div>\n <div\n v-if=\"item.searchType === 3\"\n class=\"cond-row\"\n >\n <a-textarea\n :value=\"typeof item.value === 'object' ? JSON.stringify(item.value) : item.value\"\n placeholder=\"请输入值(对象/数组/字符串)\"\n :auto-size=\"{ minRows: 2, maxRows: 6 }\"\n style=\"flex: 1\"\n @input=\"(e: any) => handleTextareaInput(e, item)\"\n />\n <a-tag\n v-if=\"item.value !== '' && item.value != null\"\n color=\"blue\"\n >\n {{ getDataType(item.value) }}\n </a-tag>\n </div>\n </div>\n </div>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport {\n BOOL_OPTIONS,\n DATE_OPS,\n DATE_TYPES,\n FORMAT_TYPE,\n NUMBER_OPS,\n NUMBER_TYPES,\n OPERATOR,\n SEARCH_CLASS_NAME,\n SELECT_LIKE_TYPES,\n SELECT_OPS,\n TEXT_OPS,\n} from './constants';\n\nconst props = defineProps<{\n config: any;\n fieldOptions: any[]; // 数据源字段候选\n fieldOptionsLoading?: boolean;\n}>();\n\nconst SEARCH_TYPE_OPTIONS = [\n { value: 1, label: '监听查询' },\n { value: 3, label: '固定查询' },\n { value: 4, label: '上下文' },\n];\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* 运算符候选按组件类型过滤 */\nconst operatorOptions = computed(() => {\n const t = props.config?.type;\n let ops = TEXT_OPS;\n if (NUMBER_TYPES.includes(t)) ops = NUMBER_OPS;\n else if (DATE_TYPES.includes(t)) ops = DATE_OPS;\n else if (SELECT_LIKE_TYPES.includes(t)) ops = SELECT_OPS;\n return Object.keys(OPERATOR)\n .filter((k) => ops.includes(k))\n .map((k) => ({ value: k, label: `${OPERATOR[k]}(${k})` }));\n});\n\nconst addSearchConditions = () => {\n if (!Array.isArray(props.config.searchConditions)) props.config.searchConditions = [];\n props.config.searchConditions.push([{ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] }]);\n};\nconst addSearchItem = (gi: number) => {\n props.config.searchConditions[gi].push({ searchType: 3, field_name: '', operator: 'eq', format: 'input', class_name: 'str', value: [] });\n};\n/* operator/class_name 变化时保证 value 形状(between 需两位) */\nconst ensureValueShape = (item: any) => {\n if (item.operator === 'between' || item.operator === 'notbetween') {\n const v0 = Array.isArray(item.value) ? item.value[0] : item.value;\n const v1 = Array.isArray(item.value) ? item.value[1] : undefined;\n item.value = [v0 ?? null, v1 ?? null];\n } else if (!Array.isArray(item.value)) {\n item.value = [item.value];\n } else if (item.value.length > 1) {\n item.value = [item.value[0]];\n }\n};\n\nconst addSimpleSearchItem = () => {\n if (!Array.isArray(props.config.simpleSearchConditions)) props.config.simpleSearchConditions = [];\n props.config.simpleSearchConditions.push({ searchType: 3, field_name: '', value: '' });\n};\nconst handleTextareaInput = (e: any, item: any) => {\n try {\n item.value = JSON.parse(e.target.value);\n } catch {\n item.value = e.target.value;\n }\n};\nconst getDataType = (value: any) => (typeof value === 'object' ? 'json' : typeof value);\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.cond-group {\n border: 1px solid rgba(0, 0, 0, 0.08);\n border-radius: 6px;\n padding: 8px;\n margin-bottom: 8px;\n}\n.group-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n .group-title {\n font-weight: 500;\n }\n}\n.cond-item {\n padding: 4px 0;\n border-top: 1px dashed rgba(0, 0, 0, 0.06);\n}\n.cond-row {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 4px 0;\n flex-wrap: wrap;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n.range-sep {\n text-align: center;\n}\n</style>\n","<template>\n <a-col :span=\"12\">\n <!-- 两边 label 文案一致(原来靠 `(mode)` / `(multiple)` 区分,v29.38 去英文后合并为静态 label) -->\n <a-form-item label=\"单选多选\">\n <a-radio-group\n v-if=\"field === 'mode'\"\n v-model:value=\"config.mode\"\n >\n <a-radio value=\"default\">单选</a-radio>\n <a-radio value=\"multiple\">多选</a-radio>\n <a-radio value=\"tags\">标签</a-radio>\n </a-radio-group>\n <a-radio-group\n v-else\n v-model:value=\"config.multiple\"\n >\n <a-radio :value=\"false\">单选</a-radio>\n <a-radio :value=\"true\">多选</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 单选多选配置(共享):field='mode' 走 default/multiple/tags 三态,\n field='multiple' 走布尔二态(树/级联类组件)。 */\ndefineProps<{\n config: any;\n field: 'mode' | 'multiple';\n}>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <!-- 两边 label 文案一致(原来靠 `(mode)` / `(multiple)` 区分,v29.38 去英文后合并为静态 label) -->\n <a-form-item label=\"单选多选\">\n <a-radio-group\n v-if=\"field === 'mode'\"\n v-model:value=\"config.mode\"\n >\n <a-radio value=\"default\">单选</a-radio>\n <a-radio value=\"multiple\">多选</a-radio>\n <a-radio value=\"tags\">标签</a-radio>\n </a-radio-group>\n <a-radio-group\n v-else\n v-model:value=\"config.multiple\"\n >\n <a-radio :value=\"false\">单选</a-radio>\n <a-radio :value=\"true\">多选</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 单选多选配置(共享):field='mode' 走 default/multiple/tags 三态,\n field='multiple' 走布尔二态(树/级联类组件)。 */\ndefineProps<{\n config: any;\n field: 'mode' | 'multiple';\n}>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"可否搜索\">\n <a-radio-group v-model:value=\"config.show_search\">\n <a-radio :value=\"false\">否</a-radio>\n <a-radio :value=\"true\">是</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 可否搜索配置(共享)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"可否搜索\">\n <a-radio-group v-model:value=\"config.show_search\">\n <a-radio :value=\"false\">否</a-radio>\n <a-radio :value=\"true\">是</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 可否搜索配置(共享)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 数据源页:远程五类(接口/字段映射/查询参数/全局切换/初始参数)\n + ★ v29.29 本地静态选项类(select/radio/checkbox/groupselect)的「选项列表」。\n ★ v29.32:原「保存/加载/复制/粘贴 配置」四按钮(后端 /sys/create_data_source_config +\n /sys/query_data_source_config_list,及 localStorage 剪贴板)已按用户要求整体删除。 -->\n <a-row :gutter=\"16\">\n <!-- ═══ 远程五类:接口 + 字段映射 + 查询参数 + 全局切换 + 初始参数 ═══ -->\n <template v-if=\"isRemote\">\n <a-col :span=\"24\">\n <a-form-item label=\"数据源\">\n <PostUrlField\n v-model:value=\"config.postUrl\"\n :item-list=\"postUrlOptions\"\n :loading=\"postUrlLoading\"\n placeholder=\"请输入数据源\"\n @select=\"onPostUrlSelect\"\n @search=\"handlePostUrlSearch\"\n />\n </a-form-item>\n </a-col>\n\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n\n <a-col :span=\"12\">\n <a-form-item label=\"显示类型\">\n <a-radio-group v-model:value=\"config.labelKeyType\">\n <a-radio :value=\"1\">单字段</a-radio>\n <a-radio :value=\"2\">多字段</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n <OptionKeysConfig\n remote\n :config=\"config\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n />\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"12\"\n >\n <a-form-item label=\"间隔符号\">\n <a-input\n v-model:value=\"config.labelSpace\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"24\"\n >\n <a-form-item label=\"显示列表\">\n <div\n v-for=\"(_, i) in config.labelKeyList || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.labelKeyList[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入显示字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.labelKeyList.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('labelKeyList')\"\n >\n + 添加显示字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"扩展查询字段\">\n <div\n v-for=\"(_, i) in config.extendedFields || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.extendedFields[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入扩展查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.extendedFields.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('extendedFields')\"\n >\n + 添加扩展查询字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"默认值配置\">\n <a-select\n v-model:value=\"config.defaultValueType\"\n style=\"width: 200px\"\n placeholder=\"未指定\"\n :options=\"[\n { value: '', label: '未指定' },\n { value: 'first', label: '第一条数据' },\n { value: 'global', label: '全局值' },\n ]\"\n />\n <template v-if=\"config.defaultValueType === 'global'\">\n <a-input\n v-model:value=\"config.globalDefaultKey\"\n placeholder=\"键\"\n style=\"width: 200px; margin-left: 8px\"\n />\n <a-input\n v-model:value=\"config.globalDefaultField\"\n placeholder=\"字段名\"\n style=\"width: 260px; margin-left: 8px\"\n />\n </template>\n </a-form-item>\n </a-col>\n\n <QueryParamsConfig\n :config=\"config\"\n :field-options=\"paramsOptions\"\n :field-options-loading=\"paramsOptionsLoading\"\n />\n\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>全局切换配置</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRequiredCondition\"\n >\n + 添加全局切换\n </a-button>\n </div>\n <div\n v-for=\"(item, ri) in config.required_condition || []\"\n :key=\"ri\"\n class=\"row-line\"\n >\n <a-input\n v-model:value=\"item.key\"\n placeholder=\"键\"\n style=\"width: 260px\"\n />\n <a-input\n v-model:value=\"item.field_name\"\n placeholder=\"字段名\"\n style=\"width: 360px\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.required_condition.splice(ri, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"初始参数:JSON\">\n <a-textarea\n v-model:value=\"initModelText\"\n :rows=\"3\"\n placeholder='{\"is_active\": true}'\n />\n </a-form-item>\n </a-col>\n </template>\n\n <!-- ═══ 本地静态选项类(select/radio/checkbox/groupselect):选项列表\n (用独立 v-if 而非 v-else-if:与上面的远程分支靠类型互斥,不依赖相邻分支匹配) ═══ -->\n <OptionListConfig\n v-if=\"isLocalOption\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { debounce } from 'lodash';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { LOCAL_OPTION_TYPES, MODE_TYPES, MULTIPLE_TYPES, REMOTE_TYPES } from './constants';\nimport OptionKeysConfig from './OptionKeysConfig.vue';\nimport OptionListConfig from './OptionListConfig.vue';\nimport PostUrlField from './PostUrlField.vue';\nimport QueryParamsConfig from './QueryParamsConfig.vue';\nimport SelectionModeConfig from './SelectionModeConfig.vue';\nimport ShowSearchConfig from './ShowSearchConfig.vue';\n\n/* 数据源配置页:postUrl 搜索(/sys/base_query_url_list)、选源自动回填、字段候选\n (/{app}/obtain_{model_name}_column_data)、labelKeyList/extendedFields、defaultValueType、\n 查询参数(QueryParamsConfig)、required_condition、init_model。\n ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)也进本页——原版 FieldSetting 的\n 「选项列表」就在「数据源」页;本页对它们只渲染选项列表,远程字段/接口那半块整体不渲染。 */\nconst props = defineProps<{\n config: any;\n /** 列配置行(透传 OptionListConfig:class_name 定值类型默认值、content_map 供导入) */\n record?: any;\n}>();\nconst initModelText = defineModel<string>('initModelText', { default: '' });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(props.config?.type));\nconst isLocalOption = computed(() => LOCAL_OPTION_TYPES.includes(props.config?.type));\n\nconst userStore = useUserStore();\nconst current_org = (userStore as any).currentOrg;\n\n/* ───────── 数据源搜索(/sys/base_query_url_list) ───────── */\nconst postUrlOptions = ref<any[]>([]);\nconst postUrlLoading = ref(false);\nlet postUrlTotal = 0;\n/* ★ v29.22.2:响应序号守卫——debounce 只合并 500ms 内的输入,跨窗口的响应仍可能乱序\n (先发的请求后返回会把 itemList 反向覆盖成旧关键字子集 → 下拉空/越搜越少),只认最新。 */\nlet postUrlSeq = 0;\n\nconst getPostUrlData = debounce((obj: any) => {\n postUrlLoading.value = true;\n const seq = ++postUrlSeq;\n getApiData(obj, '/sys/base_query_url_list')\n .then((res: any) => {\n if (seq !== postUrlSeq) return; // 过期响应丢弃\n postUrlOptions.value = [];\n if (res && res.data && res.data.length > 0) {\n res.data.forEach((item: any) => {\n if (props.config.postUrl === item.post_url) {\n getParamsOptions(item.app, item.model_name);\n }\n postUrlOptions.value.push({ value: item.post_url, label: item.name, data: item });\n });\n }\n /* ★ v29.22.1:postUrlTotal 恒存「全量总数」——带 query_param 的远程过滤结果的 total\n 是过滤后总数,不能覆盖(否则降到 ≤100 后后续输入不再触发远程搜索,列表永久收缩)。 */\n if (!obj.query_param) {\n postUrlTotal = res.total ?? postUrlOptions.value.length;\n }\n postUrlLoading.value = false;\n })\n .catch(() => {\n if (seq === postUrlSeq) {\n postUrlLoading.value = false;\n }\n });\n}, 500);\n\nonMounted(() => {\n /* ★ v29.29:本地选项类不拉数据源列表(/sys/base_query_url_list 只服务远程五类,白跑一次请求) */\n if (isRemote.value) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type),\n });\n }\n});\n\n/* 远程类型互切时本组件不重挂载,按新类型(是否树形)重拉数据源列表(对齐原版 onTypeChange 行为) */\nwatch(\n () => props.config.type,\n t => {\n if (t && REMOTE_TYPES.includes(t)) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(t),\n });\n }\n },\n);\n\nconst handlePostUrlSearch = (str: string) => {\n const is_tree = ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type);\n /* ★ v29.22.1:清空输入 → 重拉全量列表(此前清空不恢复,itemList 停在旧关键字的远程子集) */\n if (!str || !str.trim()) {\n getPostUrlData({ org_id: current_org?.id, is_tree });\n return;\n }\n if (postUrlTotal > 100) {\n getPostUrlData({ org_id: current_org?.id, is_tree, query_param: str });\n }\n};\n\n/* 选中数据源 → 自动回填(port 自 FieldSetting.selectPostUrl;item = {value, label, data}) */\nconst onPostUrlSelect = (item: any) => {\n const data = item?.data;\n if (!data) return;\n const parameters = data.parameters;\n if (parameters) {\n props.config.labelKeyType = 1;\n props.config.labelKey = parameters.label_key;\n props.config.valueKey = parameters.value_key;\n props.config.queryType = 'simple';\n props.config.simpleSearchConditions = [];\n (parameters.search_fields || []).forEach((key: any) => {\n props.config.simpleSearchConditions.push({\n searchType: 1,\n field_name: key,\n select_name: key,\n });\n });\n }\n getParamsOptions(data.app, data.model_name);\n};\n\n/* 字段候选(/{app}/obtain_{model_name}_column_data,扁平化带前缀) */\nconst paramsOptions = ref<any[]>([]);\nconst paramsOptionsLoading = ref(false);\nconst processParamsOptions = (res: any[], out: any[], prefix = '') => {\n prefix = prefix.replace('ID', '');\n if (prefix) prefix = `${prefix}-`;\n for (const item of res) {\n out.push({ value: item.name, label: `${prefix}${item.verbose_name}` });\n if (item.children) processParamsOptions(item.children, out, `${prefix}${item.verbose_name}`);\n }\n};\nconst getParamsOptions = async (app: string, model_name: string) => {\n if (app && model_name) {\n paramsOptionsLoading.value = true;\n try {\n const res = await getApiData({}, `/${app}/obtain_${model_name}_column_data`, false, {\n skip_error: true,\n });\n if (!res) {\n paramsOptions.value = [];\n } else {\n const out: any[] = [];\n processParamsOptions(res, out);\n paramsOptions.value = out;\n }\n } finally {\n paramsOptionsLoading.value = false;\n }\n }\n};\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* ───────── 列表键增删(labelKeyList / extendedFields) ───────── */\nconst addListKey = (key: 'labelKeyList' | 'extendedFields') => {\n if (!Array.isArray(props.config[key])) props.config[key] = [];\n props.config[key].push('');\n};\n\n/* ───────── 全局切换配置 ───────── */\nconst addRequiredCondition = () => {\n if (!Array.isArray(props.config.required_condition)) props.config.required_condition = [];\n props.config.required_condition.push({ key: '', field_name: '' });\n};\n\n/* ★ v29.32:原「数据源配置 保存 / 加载 / 复制 / 粘贴」整块(CLIP_KEY 剪贴板、saveModal、loadModal、\n pickDsConfig 及 6 个 handle* 函数,依赖 /sys/create_data_source_config 与\n /sys/query_data_source_config_list)已按用户要求删除。 */\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n</style>\n","<template>\n <!-- 数据源页:远程五类(接口/字段映射/查询参数/全局切换/初始参数)\n + ★ v29.29 本地静态选项类(select/radio/checkbox/groupselect)的「选项列表」。\n ★ v29.32:原「保存/加载/复制/粘贴 配置」四按钮(后端 /sys/create_data_source_config +\n /sys/query_data_source_config_list,及 localStorage 剪贴板)已按用户要求整体删除。 -->\n <a-row :gutter=\"16\">\n <!-- ═══ 远程五类:接口 + 字段映射 + 查询参数 + 全局切换 + 初始参数 ═══ -->\n <template v-if=\"isRemote\">\n <a-col :span=\"24\">\n <a-form-item label=\"数据源\">\n <PostUrlField\n v-model:value=\"config.postUrl\"\n :item-list=\"postUrlOptions\"\n :loading=\"postUrlLoading\"\n placeholder=\"请输入数据源\"\n @select=\"onPostUrlSelect\"\n @search=\"handlePostUrlSearch\"\n />\n </a-form-item>\n </a-col>\n\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n\n <a-col :span=\"12\">\n <a-form-item label=\"显示类型\">\n <a-radio-group v-model:value=\"config.labelKeyType\">\n <a-radio :value=\"1\">单字段</a-radio>\n <a-radio :value=\"2\">多字段</a-radio>\n </a-radio-group>\n </a-form-item>\n </a-col>\n <OptionKeysConfig\n remote\n :config=\"config\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n />\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"12\"\n >\n <a-form-item label=\"间隔符号\">\n <a-input\n v-model:value=\"config.labelSpace\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.labelKeyType === 2\"\n :span=\"24\"\n >\n <a-form-item label=\"显示列表\">\n <div\n v-for=\"(_, i) in config.labelKeyList || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.labelKeyList[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入显示字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.labelKeyList.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('labelKeyList')\"\n >\n + 添加显示字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"扩展查询字段\">\n <div\n v-for=\"(_, i) in config.extendedFields || []\"\n :key=\"i\"\n class=\"row-line\"\n >\n <span class=\"row-no\">({{ i + 1 }})</span>\n <a-auto-complete\n v-model:value=\"config.extendedFields[i]\"\n :options=\"paramsOptions\"\n :loading=\"paramsOptionsLoading\"\n placeholder=\"请输入扩展查询字段\"\n :filter-option=\"filterOptionContains\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.extendedFields.splice(i, 1)\"\n >\n 删除\n </a-button>\n </div>\n <a-button\n size=\"small\"\n type=\"dashed\"\n @click=\"addListKey('extendedFields')\"\n >\n + 添加扩展查询字段\n </a-button>\n </a-form-item>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"默认值配置\">\n <a-select\n v-model:value=\"config.defaultValueType\"\n style=\"width: 200px\"\n placeholder=\"未指定\"\n :options=\"[\n { value: '', label: '未指定' },\n { value: 'first', label: '第一条数据' },\n { value: 'global', label: '全局值' },\n ]\"\n />\n <template v-if=\"config.defaultValueType === 'global'\">\n <a-input\n v-model:value=\"config.globalDefaultKey\"\n placeholder=\"键\"\n style=\"width: 200px; margin-left: 8px\"\n />\n <a-input\n v-model:value=\"config.globalDefaultField\"\n placeholder=\"字段名\"\n style=\"width: 260px; margin-left: 8px\"\n />\n </template>\n </a-form-item>\n </a-col>\n\n <QueryParamsConfig\n :config=\"config\"\n :field-options=\"paramsOptions\"\n :field-options-loading=\"paramsOptionsLoading\"\n />\n\n <a-col :span=\"24\">\n <div class=\"section-title\">\n <span>全局切换配置</span>\n <a-button\n type=\"primary\"\n size=\"small\"\n @click=\"addRequiredCondition\"\n >\n + 添加全局切换\n </a-button>\n </div>\n <div\n v-for=\"(item, ri) in config.required_condition || []\"\n :key=\"ri\"\n class=\"row-line\"\n >\n <a-input\n v-model:value=\"item.key\"\n placeholder=\"键\"\n style=\"width: 260px\"\n />\n <a-input\n v-model:value=\"item.field_name\"\n placeholder=\"字段名\"\n style=\"width: 360px\"\n />\n <a-button\n type=\"link\"\n danger\n @click=\"config.required_condition.splice(ri, 1)\"\n >\n 删除\n </a-button>\n </div>\n </a-col>\n\n <a-col :span=\"24\">\n <a-form-item label=\"初始参数:JSON\">\n <a-textarea\n v-model:value=\"initModelText\"\n :rows=\"3\"\n placeholder='{\"is_active\": true}'\n />\n </a-form-item>\n </a-col>\n </template>\n\n <!-- ═══ 本地静态选项类(select/radio/checkbox/groupselect):选项列表\n (用独立 v-if 而非 v-else-if:与上面的远程分支靠类型互斥,不依赖相邻分支匹配) ═══ -->\n <OptionListConfig\n v-if=\"isLocalOption\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport { debounce } from 'lodash';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { LOCAL_OPTION_TYPES, MODE_TYPES, MULTIPLE_TYPES, REMOTE_TYPES } from './constants';\nimport OptionKeysConfig from './OptionKeysConfig.vue';\nimport OptionListConfig from './OptionListConfig.vue';\nimport PostUrlField from './PostUrlField.vue';\nimport QueryParamsConfig from './QueryParamsConfig.vue';\nimport SelectionModeConfig from './SelectionModeConfig.vue';\nimport ShowSearchConfig from './ShowSearchConfig.vue';\n\n/* 数据源配置页:postUrl 搜索(/sys/base_query_url_list)、选源自动回填、字段候选\n (/{app}/obtain_{model_name}_column_data)、labelKeyList/extendedFields、defaultValueType、\n 查询参数(QueryParamsConfig)、required_condition、init_model。\n ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)也进本页——原版 FieldSetting 的\n 「选项列表」就在「数据源」页;本页对它们只渲染选项列表,远程字段/接口那半块整体不渲染。 */\nconst props = defineProps<{\n config: any;\n /** 列配置行(透传 OptionListConfig:class_name 定值类型默认值、content_map 供导入) */\n record?: any;\n}>();\nconst initModelText = defineModel<string>('initModelText', { default: '' });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(props.config?.type));\nconst isLocalOption = computed(() => LOCAL_OPTION_TYPES.includes(props.config?.type));\n\nconst userStore = useUserStore();\nconst current_org = (userStore as any).currentOrg;\n\n/* ───────── 数据源搜索(/sys/base_query_url_list) ───────── */\nconst postUrlOptions = ref<any[]>([]);\nconst postUrlLoading = ref(false);\nlet postUrlTotal = 0;\n/* ★ v29.22.2:响应序号守卫——debounce 只合并 500ms 内的输入,跨窗口的响应仍可能乱序\n (先发的请求后返回会把 itemList 反向覆盖成旧关键字子集 → 下拉空/越搜越少),只认最新。 */\nlet postUrlSeq = 0;\n\nconst getPostUrlData = debounce((obj: any) => {\n postUrlLoading.value = true;\n const seq = ++postUrlSeq;\n getApiData(obj, '/sys/base_query_url_list')\n .then((res: any) => {\n if (seq !== postUrlSeq) return; // 过期响应丢弃\n postUrlOptions.value = [];\n if (res && res.data && res.data.length > 0) {\n res.data.forEach((item: any) => {\n if (props.config.postUrl === item.post_url) {\n getParamsOptions(item.app, item.model_name);\n }\n postUrlOptions.value.push({ value: item.post_url, label: item.name, data: item });\n });\n }\n /* ★ v29.22.1:postUrlTotal 恒存「全量总数」——带 query_param 的远程过滤结果的 total\n 是过滤后总数,不能覆盖(否则降到 ≤100 后后续输入不再触发远程搜索,列表永久收缩)。 */\n if (!obj.query_param) {\n postUrlTotal = res.total ?? postUrlOptions.value.length;\n }\n postUrlLoading.value = false;\n })\n .catch(() => {\n if (seq === postUrlSeq) {\n postUrlLoading.value = false;\n }\n });\n}, 500);\n\nonMounted(() => {\n /* ★ v29.29:本地选项类不拉数据源列表(/sys/base_query_url_list 只服务远程五类,白跑一次请求) */\n if (isRemote.value) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type),\n });\n }\n});\n\n/* 远程类型互切时本组件不重挂载,按新类型(是否树形)重拉数据源列表(对齐原版 onTypeChange 行为) */\nwatch(\n () => props.config.type,\n t => {\n if (t && REMOTE_TYPES.includes(t)) {\n getPostUrlData({\n org_id: current_org?.id,\n is_tree: ['remotetreeselectpro', 'remotecascaderpro'].includes(t),\n });\n }\n },\n);\n\nconst handlePostUrlSearch = (str: string) => {\n const is_tree = ['remotetreeselectpro', 'remotecascaderpro'].includes(props.config.type);\n /* ★ v29.22.1:清空输入 → 重拉全量列表(此前清空不恢复,itemList 停在旧关键字的远程子集) */\n if (!str || !str.trim()) {\n getPostUrlData({ org_id: current_org?.id, is_tree });\n return;\n }\n if (postUrlTotal > 100) {\n getPostUrlData({ org_id: current_org?.id, is_tree, query_param: str });\n }\n};\n\n/* 选中数据源 → 自动回填(port 自 FieldSetting.selectPostUrl;item = {value, label, data}) */\nconst onPostUrlSelect = (item: any) => {\n const data = item?.data;\n if (!data) return;\n const parameters = data.parameters;\n if (parameters) {\n props.config.labelKeyType = 1;\n props.config.labelKey = parameters.label_key;\n props.config.valueKey = parameters.value_key;\n props.config.queryType = 'simple';\n props.config.simpleSearchConditions = [];\n (parameters.search_fields || []).forEach((key: any) => {\n props.config.simpleSearchConditions.push({\n searchType: 1,\n field_name: key,\n select_name: key,\n });\n });\n }\n getParamsOptions(data.app, data.model_name);\n};\n\n/* 字段候选(/{app}/obtain_{model_name}_column_data,扁平化带前缀) */\nconst paramsOptions = ref<any[]>([]);\nconst paramsOptionsLoading = ref(false);\nconst processParamsOptions = (res: any[], out: any[], prefix = '') => {\n prefix = prefix.replace('ID', '');\n if (prefix) prefix = `${prefix}-`;\n for (const item of res) {\n out.push({ value: item.name, label: `${prefix}${item.verbose_name}` });\n if (item.children) processParamsOptions(item.children, out, `${prefix}${item.verbose_name}`);\n }\n};\nconst getParamsOptions = async (app: string, model_name: string) => {\n if (app && model_name) {\n paramsOptionsLoading.value = true;\n try {\n const res = await getApiData({}, `/${app}/obtain_${model_name}_column_data`, false, {\n skip_error: true,\n });\n if (!res) {\n paramsOptions.value = [];\n } else {\n const out: any[] = [];\n processParamsOptions(res, out);\n paramsOptions.value = out;\n }\n } finally {\n paramsOptionsLoading.value = false;\n }\n }\n};\n\nconst filterOptionContains = (input: string, option: any) =>\n !input || (option?.label ?? '').toLowerCase().includes(input.toLowerCase());\n\n/* ───────── 列表键增删(labelKeyList / extendedFields) ───────── */\nconst addListKey = (key: 'labelKeyList' | 'extendedFields') => {\n if (!Array.isArray(props.config[key])) props.config[key] = [];\n props.config[key].push('');\n};\n\n/* ───────── 全局切换配置 ───────── */\nconst addRequiredCondition = () => {\n if (!Array.isArray(props.config.required_condition)) props.config.required_condition = [];\n props.config.required_condition.push({ key: '', field_name: '' });\n};\n\n/* ★ v29.32:原「数据源配置 保存 / 加载 / 复制 / 粘贴」整块(CLIP_KEY 剪贴板、saveModal、loadModal、\n pickDsConfig 及 6 个 handle* 函数,依赖 /sys/create_data_source_config 与\n /sys/query_data_source_config_list)已按用户要求删除。 */\n</script>\n\n<style scoped lang=\"less\">\n.section-title {\n display: flex;\n align-items: center;\n gap: 12px;\n font-weight: 500;\n margin: 8px 0;\n padding-top: 8px;\n border-top: 1px dashed rgba(0, 0, 0, 0.1);\n}\n.row-line {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 4px;\n}\n.row-no {\n color: rgba(0, 0, 0, 0.45);\n flex: none;\n width: 28px;\n}\n</style>\n","<template>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"选择粒度\">\n <a-select\n v-model:value=\"config.picker\"\n allow-clear\n placeholder=\"默认日\"\n :options=\"[\n { value: 'date', label: '日' },\n { value: 'week', label: '周' },\n { value: 'month', label: '月' },\n { value: 'quarter', label: '季' },\n { value: 'year', label: '年' },\n ]\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"显示时间\">\n <a-switch\n v-model:checked=\"config.showTime\"\n checked-children=\"有\"\n un-checked-children=\"无\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"时间格式\">\n <a-input\n v-model:value=\"config.format\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"传值格式\">\n <a-input\n v-model:value=\"config.valueFormat\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 日期时间类配置(共享):picker/showTime(仅 datepicker)+ format/valueFormat。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"选择粒度\">\n <a-select\n v-model:value=\"config.picker\"\n allow-clear\n placeholder=\"默认日\"\n :options=\"[\n { value: 'date', label: '日' },\n { value: 'week', label: '周' },\n { value: 'month', label: '月' },\n { value: 'quarter', label: '季' },\n { value: 'year', label: '年' },\n ]\"\n />\n </a-form-item>\n </a-col>\n <a-col\n v-if=\"config.type === 'datepicker'\"\n :span=\"12\"\n >\n <a-form-item label=\"显示时间\">\n <a-switch\n v-model:checked=\"config.showTime\"\n checked-children=\"有\"\n un-checked-children=\"无\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"时间格式\">\n <a-input\n v-model:value=\"config.format\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"传值格式\">\n <a-input\n v-model:value=\"config.valueFormat\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 日期时间类配置(共享):picker/showTime(仅 datepicker)+ format/valueFormat。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 日期时间类(datepicker/rangepicker/monthpicker/monthrangepicker/timepicker/timerangepicker):\n picker/showTime/format/valueFormat -->\n <a-row :gutter=\"16\">\n <DateFormatConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport DateFormatConfig from '../DateFormatConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 日期时间类(datepicker/rangepicker/monthpicker/monthrangepicker/timepicker/timerangepicker):\n picker/showTime/format/valueFormat -->\n <a-row :gutter=\"16\">\n <DateFormatConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport DateFormatConfig from '../DateFormatConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- select/radio/checkbox/groupselect 本地静态选项类:mode(仅 select)+ show_search;\n ★ v29.29:选项列表(数值/显示文本)在「数据源」页维护——选项统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 契约),故不再暴露 valueKey/labelKey\n (残留 labelKey:'name' 会让选项渲染成空白,详见 constants.ts 说明)。 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"flat-hint\">\n 本地静态选项(数值 / 显示文本)在「数据源」页的「选项列表」中维护;选项对象为\n <code>{ value, label }</code>。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.flat-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- select/radio/checkbox/groupselect 本地静态选项类:mode(仅 select)+ show_search;\n ★ v29.29:选项列表(数值/显示文本)在「数据源」页维护——选项统一为 `{value, label}`\n (lybase ADVANCED_SEARCH + choices 契约),故不再暴露 valueKey/labelKey\n (残留 labelKey:'name' 会让选项渲染成空白,详见 constants.ts 说明)。 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"flat-hint\">\n 本地静态选项(数值 / 显示文本)在「数据源」页的「选项列表」中维护;选项对象为\n <code>{ value, label }</code>。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.flat-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n margin-top: 4px;\n}\n</style>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最小值\">\n <a-input\n v-model:value=\"config.min\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"最大值\">\n <a-input\n v-model:value=\"config.max\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"数值精度\">\n <a-input\n v-model:value=\"config.precision\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"步进数值\">\n <a-input\n v-model:value=\"config.step\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值范围配置(共享):min/max/precision/step(number/money)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最小值\">\n <a-input\n v-model:value=\"config.min\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"最大值\">\n <a-input\n v-model:value=\"config.max\"\n placeholder=\"不限制\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"数值精度\">\n <a-input\n v-model:value=\"config.precision\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"步进数值\">\n <a-input\n v-model:value=\"config.step\"\n type=\"number\"\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 数值范围配置(共享):min/max/precision/step(number/money)。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最大字符\">\n <a-input-number\n v-model:value=\"config.maxlength\"\n style=\"width: 100%\"\n :min=\"1\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"前缀标签\">\n <a-input\n v-model:value=\"config.addonBefore\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"后缀标签\">\n <a-input\n v-model:value=\"config.addonAfter\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 文本类输入增强(共享):maxlength + 前后缀标签(input/number)。\n 图标型前后缀(addonBeforeIcon/addonAfterIcon)可在「JSON配置」tab 补。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"12\">\n <a-form-item label=\"最大字符\">\n <a-input-number\n v-model:value=\"config.maxlength\"\n style=\"width: 100%\"\n :min=\"1\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"前缀标签\">\n <a-input\n v-model:value=\"config.addonBefore\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item label=\"后缀标签\">\n <a-input\n v-model:value=\"config.addonAfter\"\n allow-clear\n />\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 文本类输入增强(共享):maxlength + 前后缀标签(input/number)。\n 图标型前后缀(addonBeforeIcon/addonAfterIcon)可在「JSON配置」tab 补。 */\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- number/money 数字类:maxlength/前后缀(仅 number)+ min/max/precision/step -->\n <a-row :gutter=\"16\">\n <TextAddonConfig\n v-if=\"TEXT_WITH_ADDON_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <NumberRangeConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { TEXT_WITH_ADDON_TYPES } from '../constants';\nimport NumberRangeConfig from '../NumberRangeConfig.vue';\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- number/money 数字类:maxlength/前后缀(仅 number)+ min/max/precision/step -->\n <a-row :gutter=\"16\">\n <TextAddonConfig\n v-if=\"TEXT_WITH_ADDON_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <NumberRangeConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { TEXT_WITH_ADDON_TYPES } from '../constants';\nimport NumberRangeConfig from '../NumberRangeConfig.vue';\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- 远程五类(remoteselectpro/remotecomplete/remotetext/remotetreeselectpro/remotecascaderpro):\n mode|multiple + show_search;接口/字段/查询条件等在「数据源」页配置 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"remote-hint\">\n 远程类组件的数据源接口、显示字段、查询参数(监听/固定/上下文)、全局切换等在「数据源」页配置。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES, MULTIPLE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.remote-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- 远程五类(remoteselectpro/remotecomplete/remotetext/remotetreeselectpro/remotecascaderpro):\n mode|multiple + show_search;接口/字段/查询条件等在「数据源」页配置 -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n v-if=\"MODE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"mode\"\n />\n <SelectionModeConfig\n v-else-if=\"MULTIPLE_TYPES.includes(config.type)\"\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <a-col :span=\"24\">\n <div class=\"remote-hint\">\n 远程类组件的数据源接口、显示字段、查询参数(监听/固定/上下文)、全局切换等在「数据源」页配置。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { MODE_TYPES, MULTIPLE_TYPES } from '../constants';\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n\n<style scoped lang=\"less\">\n.remote-hint {\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-top: 4px;\n}\n</style>\n","<template>\n <!-- input 单行文本框:maxlength + 前后缀标签 -->\n <a-row :gutter=\"16\">\n <TextAddonConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- input 单行文本框:maxlength + 前后缀标签 -->\n <a-row :gutter=\"16\">\n <TextAddonConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport TextAddonConfig from '../TextAddonConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <a-col :span=\"24\">\n <a-form-item label=\"树形数据\">\n <a-textarea\n v-model:value=\"text\"\n :rows=\"6\"\n placeholder='[{\"value\":\"1\",\"label\":\"父级1\",\"children\":[{\"value\":\"1-1\",\"label\":\"子级1-1\"}]}]'\n />\n <div\n v-if=\"error\"\n class=\"json-error\"\n >\n {{ error }}\n </div>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 树形本地数据配置(共享,treeselect/cascader 专属):JSON 文本编辑 + 实时校验写回 config.options。\n 外壳类型切换会整体替换 config 对象(treeselect↔cascader 共用本组件不重挂载),\n 因此 watch config 引用变化时重新从 config.options 初始化(对齐原版 syncTreeDataText 行为)。 */\nimport { ref, watch } from 'vue';\n\nconst props = defineProps<{ config: any }>();\n\nconst text = ref('');\nconst error = ref('');\n\nconst initFromConfig = () => {\n error.value = '';\n const opts = props.config?.options;\n text.value = Array.isArray(opts) ? JSON.stringify(opts, null, 2) : '';\n};\n\nwatch(() => props.config, initFromConfig, { immediate: true });\n\nwatch(text, (txt) => {\n if (!txt || !txt.trim()) {\n error.value = '';\n props.config.options = [];\n return;\n }\n try {\n const parsed = JSON.parse(txt);\n if (!Array.isArray(parsed)) throw new Error('必须是数组');\n error.value = '';\n props.config.options = parsed;\n } catch (e: any) {\n error.value = 'JSON 无效:' + (e?.message || '解析失败');\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.json-error {\n color: #f5222d;\n font-size: 12px;\n}\n</style>\n","<template>\n <a-col :span=\"24\">\n <a-form-item label=\"树形数据\">\n <a-textarea\n v-model:value=\"text\"\n :rows=\"6\"\n placeholder='[{\"value\":\"1\",\"label\":\"父级1\",\"children\":[{\"value\":\"1-1\",\"label\":\"子级1-1\"}]}]'\n />\n <div\n v-if=\"error\"\n class=\"json-error\"\n >\n {{ error }}\n </div>\n </a-form-item>\n </a-col>\n</template>\n\n<script setup lang=\"ts\">\n/* 树形本地数据配置(共享,treeselect/cascader 专属):JSON 文本编辑 + 实时校验写回 config.options。\n 外壳类型切换会整体替换 config 对象(treeselect↔cascader 共用本组件不重挂载),\n 因此 watch config 引用变化时重新从 config.options 初始化(对齐原版 syncTreeDataText 行为)。 */\nimport { ref, watch } from 'vue';\n\nconst props = defineProps<{ config: any }>();\n\nconst text = ref('');\nconst error = ref('');\n\nconst initFromConfig = () => {\n error.value = '';\n const opts = props.config?.options;\n text.value = Array.isArray(opts) ? JSON.stringify(opts, null, 2) : '';\n};\n\nwatch(() => props.config, initFromConfig, { immediate: true });\n\nwatch(text, (txt) => {\n if (!txt || !txt.trim()) {\n error.value = '';\n props.config.options = [];\n return;\n }\n try {\n const parsed = JSON.parse(txt);\n if (!Array.isArray(parsed)) throw new Error('必须是数组');\n error.value = '';\n props.config.options = parsed;\n } catch (e: any) {\n error.value = 'JSON 无效:' + (e?.message || '解析失败');\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.json-error {\n color: #f5222d;\n font-size: 12px;\n}\n</style>\n","<template>\n <!-- treeselect/cascader 本地树/级联类:multiple + show_search + 树形数据 JSON -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <TreeDataConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\nimport TreeDataConfig from '../TreeDataConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- treeselect/cascader 本地树/级联类:multiple + show_search + 树形数据 JSON -->\n <a-row :gutter=\"16\">\n <SelectionModeConfig\n :config=\"config\"\n field=\"multiple\"\n />\n <ShowSearchConfig :config=\"config\" />\n <TreeDataConfig :config=\"config\" />\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport SelectionModeConfig from '../SelectionModeConfig.vue';\nimport ShowSearchConfig from '../ShowSearchConfig.vue';\nimport TreeDataConfig from '../TreeDataConfig.vue';\n\ndefineProps<{ config: any }>();\n</script>\n","<template>\n <!-- ★ v29.31:容器由「全屏弹窗」改为「右侧抽屉」——配置面板不遮全屏、可与父级视图配置弹窗对照;\n 内容布局与交互不变(内部滚动、底部操作区固定不随滚动),见文件底部 .csc-drawer 全局样式。 -->\n <a-drawer\n :title=\"`高级搜索组件 —— ${record?.title || record?.data_index || ''}`\"\n placement=\"right\"\n :width=\"SEARCH_DRAWER_WIDTH\"\n root-class-name=\"csc-drawer\"\n :open=\"open\"\n destroy-on-close\n @close=\"handleCancel\"\n >\n <!-- 统一布局骨架:纵向 flex(tip / tabs / footer 各占固定或弹性高度),\n 纵向滚动只发生在 tabs 的 content-holder,Tab 标签栏与底部操作区固定不随滚动。\n v29.21:基础+高级合并为「属性配置」单页;「其他属性(JSON)」升级为「JSON配置」tab(CodeMirror 编辑整体 JSON) -->\n <a-form\n class=\"csc-body\"\n :label-col=\"{ style: { width: '190px' } }\"\n >\n <div class=\"adv-tip\">\n 「属性配置」= 基础 + 高级属性(按组件类型联动);远程类组件在「数据源」页配置接口、显示字段、查询条件,\n 本地静态选项类(下拉选框/单选框/多选框/分组下拉)在「数据源」页配置「选项列表」;\n 「JSON配置」可直接编辑整体配置对象(与表单双向同步,保存前校验合法性)。\n 运算符不在此配置——后端按组件类型自动补默认集。\n </div>\n\n <a-tabs\n v-model:activeKey=\"activeTab\"\n class=\"csc-tabs\"\n type=\"card\"\n size=\"small\"\n >\n <!-- ═══════════════ 属性配置(基础 + 高级合并单页) ═══════════════ -->\n <a-tab-pane\n key=\"props\"\n tab=\"属性配置\"\n >\n <BasicConfig\n :config=\"config\"\n :record=\"record\"\n @type-change=\"onTypeChange\"\n />\n <template v-if=\"config.type\">\n <a-divider\n class=\"props-divider\"\n orientation=\"left\"\n plain\n >\n 高级属性\n </a-divider>\n <TextInputSetting\n v-if=\"config.type === 'input'\"\n :config=\"config\"\n />\n <NumberInputSetting\n v-else-if=\"NUMBER_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <FlatSelectSetting\n v-else-if=\"FLAT_SETTING_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <TreeSelectSetting\n v-else-if=\"['treeselect', 'cascader'].includes(config.type)\"\n :config=\"config\"\n />\n <DateSelectSetting\n v-else-if=\"DATE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <RemoteSelectSetting\n v-else-if=\"REMOTE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n </template>\n </a-tab-pane>\n\n <!-- ═══════════════ 数据源(远程类调接口 / 本地静态选项类配选项列表,共享 DataSourcePanel) ═══════════════ -->\n <a-tab-pane\n key=\"ds\"\n tab=\"数据源\"\n :disabled=\"!dsTabEnabled\"\n >\n <DataSourcePanel\n v-if=\"dsTabEnabled\"\n v-model:init-model-text=\"initModelText\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-tab-pane>\n\n <!-- ═══════════════ JSON 配置(CodeMirror 编辑整体配置对象) ═══════════════ -->\n <a-tab-pane\n key=\"json\"\n tab=\"JSON配置\"\n :disabled=\"!config.type\"\n >\n <div class=\"csc-json-pane\">\n <div class=\"json-toolbar\">\n <a-alert\n v-if=\"jsonError\"\n type=\"error\"\n show-icon\n :message=\"jsonError\"\n banner\n />\n <span\n v-else\n class=\"json-tip\"\n >整体配置对象,与「属性配置」表单实时双向同步;保存时以此为最终值。</span>\n <a-button\n size=\"small\"\n :disabled=\"!!jsonError\"\n @click=\"handleFormatJson\"\n >\n 格式化\n </a-button>\n </div>\n <div class=\"json-editor-wrap\">\n <CodeEditor\n ref=\"jsonEditorRef\"\n v-model:editorValue=\"jsonText\"\n mode=\"javascript\"\n :options=\"{ lineNumbers: true, indentUnit: 2, lineWrapping: false }\"\n />\n </div>\n </div>\n </a-tab-pane>\n </a-tabs>\n\n <div\n v-if=\"config.type\"\n class=\"adv-footer\"\n >\n <a-button\n size=\"small\"\n danger\n @click=\"handleClear\"\n >\n 清除配置(恢复按类型自动推导)\n </a-button>\n </div>\n </a-form>\n\n <!-- 抽屉无 cancel-text/ok-text 属性(那是 a-modal 的),操作区用 footer 插槽 -->\n <template #footer>\n <div class=\"csc-drawer-footer\">\n <a-button @click=\"handleCancel\">\n 取消\n </a-button>\n <a-button\n type=\"primary\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </div>\n </template>\n </a-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { cloneDeep } from 'lodash';\nimport { computed, nextTick, ref, watch } from 'vue';\nimport CodeEditor from 'liyu-pc-base/components/code/CodeEditor.vue';\nimport {\n DATE_TYPES,\n FLAT_SETTING_TYPES,\n LOCAL_OPTION_TYPES,\n NUMBER_TYPES,\n RANGE_PLACEHOLDER_TYPES,\n REMOTE_TYPES,\n TYPE_DEFAULTS,\n hasOptionValue,\n} from './searchComponentSetting/constants';\nimport BasicConfig from './searchComponentSetting/BasicConfig.vue';\nimport DataSourcePanel from './searchComponentSetting/DataSourcePanel.vue';\nimport DateSelectSetting from './searchComponentSetting/settings/DateSelectSetting.vue';\nimport FlatSelectSetting from './searchComponentSetting/settings/FlatSelectSetting.vue';\nimport NumberInputSetting from './searchComponentSetting/settings/NumberInputSetting.vue';\nimport RemoteSelectSetting from './searchComponentSetting/settings/RemoteSelectSetting.vue';\nimport TextInputSetting from './searchComponentSetting/settings/TextInputSetting.vue';\nimport TreeSelectSetting from './searchComponentSetting/settings/TreeSelectSetting.vue';\n\n/* ★ v29.19 完整移植 FieldSetting.vue 搜索组件配置;v29.20 拆分 + 全屏弹窗;v29.31 改为右侧抽屉:\n - 外壳只负责容器编排(tab 分发 / 类型切换合并默认值 / 确定保存与 JSON 合并清理);\n - 基础属性 = BasicConfig(共享);高级属性按组件类型分发到 settings/*(组合共享基础配置块);\n - 数据源页 = DataSourcePanel(远程五类专属,含查询参数编辑器 QueryParamsConfig)。\n 产出物 = 完整组件配置对象(JSONField advanced_search_component),\n get_page_config 编译时整体并入高级搜索组件对象(field_name 由列锁定)。 */\n\n/* 抽屉宽度:内容含 190px 的标签列 + 远程数据源(URL/字段映射/查询参数) + CodeMirror JSON 编辑器,\n 过窄会把控件挤成多行。父级「视图配置」弹窗为 1020px,这里取略窄以保证「抽屉」观感;\n 要调宽度只改这一处。 */\nconst SEARCH_DRAWER_WIDTH = 920;\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(读写其 advanced_search_component)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(config.value?.type));\n/* ★ v29.29:「数据源」tab 的启用范围 = 远程五类(接口/字段映射) + 本地静态选项类(选项列表)。\n 此前只看 isRemote,导致 select/radio/checkbox 的「选项列表」(原版 FieldSetting 就挂在数据源页)\n 既没有入口也进不去那一页 → 选框类组件配不出选项。 */\nconst dsTabEnabled = computed(\n () => isRemote.value || LOCAL_OPTION_TYPES.includes(config.value?.type),\n);\n\n/* ───────── 状态 ───────── */\nconst activeTab = ref('props');\nconst config = ref<any>({});\nconst initModelText = ref('');\n/* JSON 配置 tab:jsonDirty=编辑器文本领先于 config(未同步/语法错误);jsonError=当前文本非法 */\nconst jsonText = ref('');\nconst jsonDirty = ref(false);\nconst jsonError = ref('');\nconst jsonEditorRef = ref<any>();\n\n/* ───────── 打开时初始化 ───────── */\nwatch(\n () => props.open,\n (val) => {\n if (!val) return;\n activeTab.value = 'props';\n const saved = props.record?.advanced_search_component;\n /* config 恒为对象(v29.18 null 崩溃修复沿用);保存过的配置 + 类型默认值合并(保存值优先) */\n if (saved && typeof saved === 'object' && saved.type) {\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[saved.type] || {}), ...cloneDeep(saved) });\n config.value.type = saved.type;\n } else {\n config.value = {};\n }\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonText.value = '';\n jsonDirty.value = false;\n jsonError.value = '';\n },\n);\n\n/* ───────── JSON 配置 tab:配置实时镜像 + 编辑文本回写 ───────── */\n/* 序列化快照:config + 数据源页未保存的 init_model(initModelText 独立文本态,保存时才合并),\n 保证 JSON 页看到/保存的语义与 handleOk 一致 */\nconst buildSnapshot = () => {\n const snapshot = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n snapshot.init_model = JSON.parse(initModelText.value);\n } catch {\n /* 数据源页文本非法:不并入,由其自身保存校验拦截 */\n }\n } else {\n delete snapshot.init_model;\n }\n return snapshot;\n};\n\nwatch(activeTab, (tab, oldTab) => {\n /* 离开 JSON 页时仍有语法错误:提示未同步(是否丢弃取决于期间是否改过表单,见下方镜像守卫) */\n if (oldTab === 'json' && jsonError.value) {\n message.warning('JSON 配置存在语法错误,未同步到表单');\n }\n /* 进入时兜底刷新:覆盖「只改了 initModelText 而 config 未变」(镜像 watch 不会触发)的情况 */\n if (tab === 'json' && !jsonDirty.value) {\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n jsonError.value = '';\n }\n if (tab === 'json') {\n /* CodeMirror 可能在 pane 隐藏期间收到过 setValue,重新显示后主动 refresh,防止渲染滞后 */\n nextTick(() => jsonEditorRef.value?.handleCodeMirrorRefresh?.());\n }\n});\n\n/* ★ 配置实时镜像:任何 config 变化(表单/数据源页/类型切换)立即刷新 JSON 文本并清除脏态,\n 不再依赖「进入 tab」时机。说明:\n - config 变化只可能来自非 JSON 页(JSON 页内无法编辑表单),无条件覆盖即「表单为准」,\n 非法草稿随表单编辑作废(离开时已有 warning 提示);\n - JSON 页内合法回写也会触发本 watch,但镜像文本与编辑器现有文本一致,自然收敛不循环。 */\nwatch(\n config,\n () => {\n if (config.value?.type) {\n jsonDirty.value = false;\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n }\n },\n { deep: true },\n);\n\nwatch(jsonText, (text) => {\n if (!text.trim()) {\n jsonError.value = '';\n return;\n }\n try {\n const parsed = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.type) {\n jsonError.value = '必须是包含 type 字符串键的 JSON 对象(如 {\"type\":\"input\", ...}),未同步到表单';\n return;\n }\n /* 与打开初始化/类型切换同一语义:JSON 为准,缺键补类型默认(cloneDeep 防共享 TYPE_DEFAULTS 引用),\n 再归一 placeholder 形状 */\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[parsed.type] || {}), ...parsed });\n /* init_model 在 DataSourcePanel 有独立文本态,保持一致避免保存时被旧值覆盖 */\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonError.value = '';\n jsonDirty.value = false;\n } catch (e: any) {\n jsonError.value = `JSON 语法错误:${e?.message || e}(未同步到表单)`;\n jsonDirty.value = true;\n }\n});\n\nconst handleFormatJson = () => {\n try {\n jsonText.value = JSON.stringify(JSON.parse(jsonText.value), null, 2);\n } catch {\n /* 语法错误时按钮已禁用,防御性兜底 */\n }\n};\n\n/* ───────── placeholder 形状归一(区间类=数组双输入,其余=字符串) ─────────\n 覆盖三个 config 入口:打开初始化 / 类型切换(keep 跨类保留)/ JSON 回写(直改 type 或缺键),\n 否则 BasicConfig 区间分支绑定 config.placeholder[0] 会在 undefined/字符串上崩溃或赋值无效 */\nconst normalizePlaceholder = (cfg: any) => {\n if (!cfg?.type) return cfg;\n const isRange = RANGE_PLACEHOLDER_TYPES.includes(cfg.type);\n if (isRange) {\n if (!Array.isArray(cfg.placeholder)) {\n cfg.placeholder =\n typeof cfg.placeholder === 'string' && cfg.placeholder\n ? [cfg.placeholder, cfg.placeholder]\n : cloneDeep(TYPE_DEFAULTS[cfg.type]?.placeholder) || ['开始', '结束'];\n }\n } else if (Array.isArray(cfg.placeholder)) {\n cfg.placeholder = cfg.placeholder.filter(Boolean)[0] || '';\n } else if (typeof cfg.placeholder !== 'string') {\n cfg.placeholder = TYPE_DEFAULTS[cfg.type]?.placeholder ?? '';\n }\n return cfg;\n};\n\n/* ───────── 类型切换 ───────── */\nconst onTypeChange = (type: string) => {\n if (!type) {\n config.value = {};\n return;\n }\n const prev = config.value || {};\n // 切类型:以该类型默认属性为基础,保留通用已填项(label/placeholder/allowClear/disabled)\n const keep: any = {};\n if (prev.label) keep.label = prev.label;\n if (prev.disabled != null) keep.disabled = prev.disabled;\n if (RANGE_PLACEHOLDER_TYPES.includes(prev.type)) {\n if (Array.isArray(prev.placeholder)) keep.placeholder = prev.placeholder;\n } else if (typeof prev.placeholder === 'string') {\n keep.placeholder = prev.placeholder;\n }\n if (prev.allowClear != null) keep.allowClear = prev.allowClear;\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[type] || { type }), ...keep });\n /* 类型变了,JSON tab 下次进入需按新配置重新序列化 */\n jsonDirty.value = false;\n};\n\n/* ───────── 确定保存 ───────── */\nconst handleClear = () => {\n props.record.advanced_search_component = null;\n config.value = {};\n open.value = false;\n};\n\nconst handleOk = () => {\n if (!config.value?.type) {\n // 未选类型 = 不配置(保留原值不动)\n open.value = false;\n return;\n }\n if (jsonError.value) {\n message.warning('JSON 配置存在语法错误,请修正后再保存');\n return;\n }\n const finalConfig = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n finalConfig.init_model = JSON.parse(initModelText.value.trim());\n } catch {\n message.warning('初始参数不是合法 JSON,请修正后再保存');\n return;\n }\n } else {\n delete finalConfig.init_model;\n }\n /* ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)\n ① 选项对象必须是 lybase choices 契约 `{value, label}`,且不能带 valueKey/labelKey ——\n 运行时按 `option[labelKey||'label']` 取显示文本,残留 `labelKey:'name'` 会整列渲染成空白;\n ② 至少一个有效选项:这类组件没有别的 options 来源(远程接口那套对它们不适用),\n 空选项落库 = 一个永远选不出东西的下拉框,属静默坏配置,这里直接拦住。 */\n if (LOCAL_OPTION_TYPES.includes(finalConfig.type)) {\n delete finalConfig.valueKey;\n delete finalConfig.labelKey;\n /* 只丢「整行空白」的项;手写在 JSON 里的**嵌套分组节点**(antdv `{label, options:[...]}`,\n 逐项直传 groupselect)自身没有 value,必须原样保留,否则一次保存就被清空。 */\n const opts = (Array.isArray(finalConfig.options) ? finalConfig.options : []).filter(\n (o: any) => hasOptionValue(o) || (Array.isArray(o?.options) && o.options.length > 0),\n );\n if (opts.length === 0) {\n message.warning('下拉选框/单选框/多选框至少需要一个选项,请在「数据源」页的「选项列表」中添加');\n return;\n }\n finalConfig.options = opts;\n }\n // 清理空壳键:空数组/空串不落库\n ['labelKeyList', 'extendedFields', 'searchConditions', 'simpleSearchConditions', 'required_condition', 'options'].forEach((k) => {\n if (Array.isArray(finalConfig[k]) && finalConfig[k].length === 0) delete finalConfig[k];\n });\n if (finalConfig.defaultValueType === '') delete finalConfig.defaultValueType;\n props.record.advanced_search_component = finalConfig;\n jsonDirty.value = false;\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.csc-body {\n /* 抽屉 body 与改造前的弹窗 body 一样是「确定高度的 flex item」,flex:1 + height:100% 双保险撑满剩余高度 */\n flex: 1;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.adv-tip {\n flex: none;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 12px;\n line-height: 1.5;\n}\n.csc-tabs {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 滚动只发生在内容区:Tab 标签栏固定 */\n :deep(.ant-tabs-nav) {\n flex: none;\n margin-bottom: 12px;\n }\n :deep(.ant-tabs-content-holder) {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 抵消 a-row gutter 的 -8px 负 margin,消除多余横向滚动条 */\n padding: 0 8px;\n }\n /* 让 tabpane 拿到确定高度,JSON 编辑器才能撑满内容区 */\n :deep(.ant-tabs-content),\n :deep(.ant-tabs-tabpane) {\n height: 100%;\n }\n}\n.props-divider {\n margin: 8px 0 12px;\n}\n.csc-json-pane {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.json-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 8px;\n min-width: 0;\n}\n.json-toolbar .ant-alert {\n flex: 1;\n min-width: 0;\n}\n.json-tip {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.json-editor-wrap {\n flex: 1;\n min-height: 0;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n overflow: hidden;\n}\n/* CodeEditor 全局样式固定 300px 高,这里撑满 JSON 页剩余高度 */\n.json-editor-wrap :deep(.CodeMirror) {\n height: 100% !important;\n font-size: 13px !important;\n}\n.adv-footer {\n flex: none;\n margin-top: 8px;\n}\n/* 抽屉底部操作区(footer 插槽内容,是本文档的元素 → scoped 可命中) */\n.csc-drawer-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n</style>\n\n<!-- ★ v29.31:抽屉是 teleport 到 body 的,`root-class-name` 挂在**抽屉自己的根节点**上,\n 本组件根元素不是它的祖先 → scoped 的 :deep() 选不中,只能写全局样式(用 .csc-drawer 限定作用域)。\n ⚠️ antdv 4.2.6 的 a-drawer **没有 `wrap-class-name`**(只有 `root-class-name`/`root-style`),\n 别照搬 a-modal 的 wrap-class-name 写法 —— 会被当成普通 attr 落到别处,样式静默失效。\n ⚠️ 选择器**刻意写成三段** `.csc-drawer.ant-drawer .ant-drawer-body`(特异度 0,3,0):\n antd 的 `.ant-drawer-body` 规则经 cssinjs 生成后是 `:where(.css-hash).ant-drawer .ant-drawer-body`(0,2,0),\n 两段式 `.csc-drawer .ant-drawer-body`(同为 0,2,0)会与它**打平**,胜负交给样式注入顺序 —— 不稳。\n (`.csc-drawer` 与 `.ant-drawer` 同在抽屉根节点上:vc-drawer 把 rootClassName 并进根节点的 class 列表。) -->\n<style lang=\"less\">\n.csc-drawer.ant-drawer {\n /* 内部滚动:滚动只发生在 tabs 的 content-holder(见上方 scoped 块),\n 抽屉标题栏与底部操作区固定不随滚动 —— 与改造前的全屏弹窗行为一致 */\n .ant-drawer-body {\n display: flex;\n flex-direction: column;\n padding: 16px;\n overflow: hidden;\n }\n .ant-drawer-footer {\n flex: none;\n }\n}\n</style>\n","<template>\n <!-- ★ v29.31:容器由「全屏弹窗」改为「右侧抽屉」——配置面板不遮全屏、可与父级视图配置弹窗对照;\n 内容布局与交互不变(内部滚动、底部操作区固定不随滚动),见文件底部 .csc-drawer 全局样式。 -->\n <a-drawer\n :title=\"`高级搜索组件 —— ${record?.title || record?.data_index || ''}`\"\n placement=\"right\"\n :width=\"SEARCH_DRAWER_WIDTH\"\n root-class-name=\"csc-drawer\"\n :open=\"open\"\n destroy-on-close\n @close=\"handleCancel\"\n >\n <!-- 统一布局骨架:纵向 flex(tip / tabs / footer 各占固定或弹性高度),\n 纵向滚动只发生在 tabs 的 content-holder,Tab 标签栏与底部操作区固定不随滚动。\n v29.21:基础+高级合并为「属性配置」单页;「其他属性(JSON)」升级为「JSON配置」tab(CodeMirror 编辑整体 JSON) -->\n <a-form\n class=\"csc-body\"\n :label-col=\"{ style: { width: '190px' } }\"\n >\n <div class=\"adv-tip\">\n 「属性配置」= 基础 + 高级属性(按组件类型联动);远程类组件在「数据源」页配置接口、显示字段、查询条件,\n 本地静态选项类(下拉选框/单选框/多选框/分组下拉)在「数据源」页配置「选项列表」;\n 「JSON配置」可直接编辑整体配置对象(与表单双向同步,保存前校验合法性)。\n 运算符不在此配置——后端按组件类型自动补默认集。\n </div>\n\n <a-tabs\n v-model:activeKey=\"activeTab\"\n class=\"csc-tabs\"\n type=\"card\"\n size=\"small\"\n >\n <!-- ═══════════════ 属性配置(基础 + 高级合并单页) ═══════════════ -->\n <a-tab-pane\n key=\"props\"\n tab=\"属性配置\"\n >\n <BasicConfig\n :config=\"config\"\n :record=\"record\"\n @type-change=\"onTypeChange\"\n />\n <template v-if=\"config.type\">\n <a-divider\n class=\"props-divider\"\n orientation=\"left\"\n plain\n >\n 高级属性\n </a-divider>\n <TextInputSetting\n v-if=\"config.type === 'input'\"\n :config=\"config\"\n />\n <NumberInputSetting\n v-else-if=\"NUMBER_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <FlatSelectSetting\n v-else-if=\"FLAT_SETTING_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <TreeSelectSetting\n v-else-if=\"['treeselect', 'cascader'].includes(config.type)\"\n :config=\"config\"\n />\n <DateSelectSetting\n v-else-if=\"DATE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n <RemoteSelectSetting\n v-else-if=\"REMOTE_TYPES.includes(config.type)\"\n :config=\"config\"\n />\n </template>\n </a-tab-pane>\n\n <!-- ═══════════════ 数据源(远程类调接口 / 本地静态选项类配选项列表,共享 DataSourcePanel) ═══════════════ -->\n <a-tab-pane\n key=\"ds\"\n tab=\"数据源\"\n :disabled=\"!dsTabEnabled\"\n >\n <DataSourcePanel\n v-if=\"dsTabEnabled\"\n v-model:init-model-text=\"initModelText\"\n :config=\"config\"\n :record=\"record\"\n />\n </a-tab-pane>\n\n <!-- ═══════════════ JSON 配置(CodeMirror 编辑整体配置对象) ═══════════════ -->\n <a-tab-pane\n key=\"json\"\n tab=\"JSON配置\"\n :disabled=\"!config.type\"\n >\n <div class=\"csc-json-pane\">\n <div class=\"json-toolbar\">\n <a-alert\n v-if=\"jsonError\"\n type=\"error\"\n show-icon\n :message=\"jsonError\"\n banner\n />\n <span\n v-else\n class=\"json-tip\"\n >整体配置对象,与「属性配置」表单实时双向同步;保存时以此为最终值。</span>\n <a-button\n size=\"small\"\n :disabled=\"!!jsonError\"\n @click=\"handleFormatJson\"\n >\n 格式化\n </a-button>\n </div>\n <div class=\"json-editor-wrap\">\n <CodeEditor\n ref=\"jsonEditorRef\"\n v-model:editorValue=\"jsonText\"\n mode=\"javascript\"\n :options=\"{ lineNumbers: true, indentUnit: 2, lineWrapping: false }\"\n />\n </div>\n </div>\n </a-tab-pane>\n </a-tabs>\n\n <div\n v-if=\"config.type\"\n class=\"adv-footer\"\n >\n <a-button\n size=\"small\"\n danger\n @click=\"handleClear\"\n >\n 清除配置(恢复按类型自动推导)\n </a-button>\n </div>\n </a-form>\n\n <!-- 抽屉无 cancel-text/ok-text 属性(那是 a-modal 的),操作区用 footer 插槽 -->\n <template #footer>\n <div class=\"csc-drawer-footer\">\n <a-button @click=\"handleCancel\">\n 取消\n </a-button>\n <a-button\n type=\"primary\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </div>\n </template>\n </a-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { cloneDeep } from 'lodash';\nimport { computed, nextTick, ref, watch } from 'vue';\nimport CodeEditor from 'liyu-pc-base/components/code/CodeEditor.vue';\nimport {\n DATE_TYPES,\n FLAT_SETTING_TYPES,\n LOCAL_OPTION_TYPES,\n NUMBER_TYPES,\n RANGE_PLACEHOLDER_TYPES,\n REMOTE_TYPES,\n TYPE_DEFAULTS,\n hasOptionValue,\n} from './searchComponentSetting/constants';\nimport BasicConfig from './searchComponentSetting/BasicConfig.vue';\nimport DataSourcePanel from './searchComponentSetting/DataSourcePanel.vue';\nimport DateSelectSetting from './searchComponentSetting/settings/DateSelectSetting.vue';\nimport FlatSelectSetting from './searchComponentSetting/settings/FlatSelectSetting.vue';\nimport NumberInputSetting from './searchComponentSetting/settings/NumberInputSetting.vue';\nimport RemoteSelectSetting from './searchComponentSetting/settings/RemoteSelectSetting.vue';\nimport TextInputSetting from './searchComponentSetting/settings/TextInputSetting.vue';\nimport TreeSelectSetting from './searchComponentSetting/settings/TreeSelectSetting.vue';\n\n/* ★ v29.19 完整移植 FieldSetting.vue 搜索组件配置;v29.20 拆分 + 全屏弹窗;v29.31 改为右侧抽屉:\n - 外壳只负责容器编排(tab 分发 / 类型切换合并默认值 / 确定保存与 JSON 合并清理);\n - 基础属性 = BasicConfig(共享);高级属性按组件类型分发到 settings/*(组合共享基础配置块);\n - 数据源页 = DataSourcePanel(远程五类专属,含查询参数编辑器 QueryParamsConfig)。\n 产出物 = 完整组件配置对象(JSONField advanced_search_component),\n get_page_config 编译时整体并入高级搜索组件对象(field_name 由列锁定)。 */\n\n/* 抽屉宽度:内容含 190px 的标签列 + 远程数据源(URL/字段映射/查询参数) + CodeMirror JSON 编辑器,\n 过窄会把控件挤成多行。父级「视图配置」弹窗为 1020px,这里取略窄以保证「抽屉」观感;\n 要调宽度只改这一处。 */\nconst SEARCH_DRAWER_WIDTH = 920;\n\ninterface Props {\n open: boolean;\n record: any; // 列配置行(读写其 advanced_search_component)\n}\nconst props = defineProps<Props>();\nconst open = defineModel<boolean>('open', { default: false });\n\nconst isRemote = computed(() => REMOTE_TYPES.includes(config.value?.type));\n/* ★ v29.29:「数据源」tab 的启用范围 = 远程五类(接口/字段映射) + 本地静态选项类(选项列表)。\n 此前只看 isRemote,导致 select/radio/checkbox 的「选项列表」(原版 FieldSetting 就挂在数据源页)\n 既没有入口也进不去那一页 → 选框类组件配不出选项。 */\nconst dsTabEnabled = computed(\n () => isRemote.value || LOCAL_OPTION_TYPES.includes(config.value?.type),\n);\n\n/* ───────── 状态 ───────── */\nconst activeTab = ref('props');\nconst config = ref<any>({});\nconst initModelText = ref('');\n/* JSON 配置 tab:jsonDirty=编辑器文本领先于 config(未同步/语法错误);jsonError=当前文本非法 */\nconst jsonText = ref('');\nconst jsonDirty = ref(false);\nconst jsonError = ref('');\nconst jsonEditorRef = ref<any>();\n\n/* ───────── 打开时初始化 ───────── */\nwatch(\n () => props.open,\n (val) => {\n if (!val) return;\n activeTab.value = 'props';\n const saved = props.record?.advanced_search_component;\n /* config 恒为对象(v29.18 null 崩溃修复沿用);保存过的配置 + 类型默认值合并(保存值优先) */\n if (saved && typeof saved === 'object' && saved.type) {\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[saved.type] || {}), ...cloneDeep(saved) });\n config.value.type = saved.type;\n } else {\n config.value = {};\n }\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonText.value = '';\n jsonDirty.value = false;\n jsonError.value = '';\n },\n);\n\n/* ───────── JSON 配置 tab:配置实时镜像 + 编辑文本回写 ───────── */\n/* 序列化快照:config + 数据源页未保存的 init_model(initModelText 独立文本态,保存时才合并),\n 保证 JSON 页看到/保存的语义与 handleOk 一致 */\nconst buildSnapshot = () => {\n const snapshot = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n snapshot.init_model = JSON.parse(initModelText.value);\n } catch {\n /* 数据源页文本非法:不并入,由其自身保存校验拦截 */\n }\n } else {\n delete snapshot.init_model;\n }\n return snapshot;\n};\n\nwatch(activeTab, (tab, oldTab) => {\n /* 离开 JSON 页时仍有语法错误:提示未同步(是否丢弃取决于期间是否改过表单,见下方镜像守卫) */\n if (oldTab === 'json' && jsonError.value) {\n message.warning('JSON 配置存在语法错误,未同步到表单');\n }\n /* 进入时兜底刷新:覆盖「只改了 initModelText 而 config 未变」(镜像 watch 不会触发)的情况 */\n if (tab === 'json' && !jsonDirty.value) {\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n jsonError.value = '';\n }\n if (tab === 'json') {\n /* CodeMirror 可能在 pane 隐藏期间收到过 setValue,重新显示后主动 refresh,防止渲染滞后 */\n nextTick(() => jsonEditorRef.value?.handleCodeMirrorRefresh?.());\n }\n});\n\n/* ★ 配置实时镜像:任何 config 变化(表单/数据源页/类型切换)立即刷新 JSON 文本并清除脏态,\n 不再依赖「进入 tab」时机。说明:\n - config 变化只可能来自非 JSON 页(JSON 页内无法编辑表单),无条件覆盖即「表单为准」,\n 非法草稿随表单编辑作废(离开时已有 warning 提示);\n - JSON 页内合法回写也会触发本 watch,但镜像文本与编辑器现有文本一致,自然收敛不循环。 */\nwatch(\n config,\n () => {\n if (config.value?.type) {\n jsonDirty.value = false;\n jsonText.value = JSON.stringify(buildSnapshot(), null, 2);\n }\n },\n { deep: true },\n);\n\nwatch(jsonText, (text) => {\n if (!text.trim()) {\n jsonError.value = '';\n return;\n }\n try {\n const parsed = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.type) {\n jsonError.value = '必须是包含 type 字符串键的 JSON 对象(如 {\"type\":\"input\", ...}),未同步到表单';\n return;\n }\n /* 与打开初始化/类型切换同一语义:JSON 为准,缺键补类型默认(cloneDeep 防共享 TYPE_DEFAULTS 引用),\n 再归一 placeholder 形状 */\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[parsed.type] || {}), ...parsed });\n /* init_model 在 DataSourcePanel 有独立文本态,保持一致避免保存时被旧值覆盖 */\n initModelText.value = config.value.init_model ? JSON.stringify(config.value.init_model, null, 2) : '';\n jsonError.value = '';\n jsonDirty.value = false;\n } catch (e: any) {\n jsonError.value = `JSON 语法错误:${e?.message || e}(未同步到表单)`;\n jsonDirty.value = true;\n }\n});\n\nconst handleFormatJson = () => {\n try {\n jsonText.value = JSON.stringify(JSON.parse(jsonText.value), null, 2);\n } catch {\n /* 语法错误时按钮已禁用,防御性兜底 */\n }\n};\n\n/* ───────── placeholder 形状归一(区间类=数组双输入,其余=字符串) ─────────\n 覆盖三个 config 入口:打开初始化 / 类型切换(keep 跨类保留)/ JSON 回写(直改 type 或缺键),\n 否则 BasicConfig 区间分支绑定 config.placeholder[0] 会在 undefined/字符串上崩溃或赋值无效 */\nconst normalizePlaceholder = (cfg: any) => {\n if (!cfg?.type) return cfg;\n const isRange = RANGE_PLACEHOLDER_TYPES.includes(cfg.type);\n if (isRange) {\n if (!Array.isArray(cfg.placeholder)) {\n cfg.placeholder =\n typeof cfg.placeholder === 'string' && cfg.placeholder\n ? [cfg.placeholder, cfg.placeholder]\n : cloneDeep(TYPE_DEFAULTS[cfg.type]?.placeholder) || ['开始', '结束'];\n }\n } else if (Array.isArray(cfg.placeholder)) {\n cfg.placeholder = cfg.placeholder.filter(Boolean)[0] || '';\n } else if (typeof cfg.placeholder !== 'string') {\n cfg.placeholder = TYPE_DEFAULTS[cfg.type]?.placeholder ?? '';\n }\n return cfg;\n};\n\n/* ───────── 类型切换 ───────── */\nconst onTypeChange = (type: string) => {\n if (!type) {\n config.value = {};\n return;\n }\n const prev = config.value || {};\n // 切类型:以该类型默认属性为基础,保留通用已填项(label/placeholder/allowClear/disabled)\n const keep: any = {};\n if (prev.label) keep.label = prev.label;\n if (prev.disabled != null) keep.disabled = prev.disabled;\n if (RANGE_PLACEHOLDER_TYPES.includes(prev.type)) {\n if (Array.isArray(prev.placeholder)) keep.placeholder = prev.placeholder;\n } else if (typeof prev.placeholder === 'string') {\n keep.placeholder = prev.placeholder;\n }\n if (prev.allowClear != null) keep.allowClear = prev.allowClear;\n config.value = normalizePlaceholder({ ...cloneDeep(TYPE_DEFAULTS[type] || { type }), ...keep });\n /* 类型变了,JSON tab 下次进入需按新配置重新序列化 */\n jsonDirty.value = false;\n};\n\n/* ───────── 确定保存 ───────── */\nconst handleClear = () => {\n props.record.advanced_search_component = null;\n config.value = {};\n open.value = false;\n};\n\nconst handleOk = () => {\n if (!config.value?.type) {\n // 未选类型 = 不配置(保留原值不动)\n open.value = false;\n return;\n }\n if (jsonError.value) {\n message.warning('JSON 配置存在语法错误,请修正后再保存');\n return;\n }\n const finalConfig = cloneDeep(config.value);\n if (initModelText.value.trim()) {\n try {\n finalConfig.init_model = JSON.parse(initModelText.value.trim());\n } catch {\n message.warning('初始参数不是合法 JSON,请修正后再保存');\n return;\n }\n } else {\n delete finalConfig.init_model;\n }\n /* ★ v29.29:本地静态选项类(select/radio/checkbox/groupselect)\n ① 选项对象必须是 lybase choices 契约 `{value, label}`,且不能带 valueKey/labelKey ——\n 运行时按 `option[labelKey||'label']` 取显示文本,残留 `labelKey:'name'` 会整列渲染成空白;\n ② 至少一个有效选项:这类组件没有别的 options 来源(远程接口那套对它们不适用),\n 空选项落库 = 一个永远选不出东西的下拉框,属静默坏配置,这里直接拦住。 */\n if (LOCAL_OPTION_TYPES.includes(finalConfig.type)) {\n delete finalConfig.valueKey;\n delete finalConfig.labelKey;\n /* 只丢「整行空白」的项;手写在 JSON 里的**嵌套分组节点**(antdv `{label, options:[...]}`,\n 逐项直传 groupselect)自身没有 value,必须原样保留,否则一次保存就被清空。 */\n const opts = (Array.isArray(finalConfig.options) ? finalConfig.options : []).filter(\n (o: any) => hasOptionValue(o) || (Array.isArray(o?.options) && o.options.length > 0),\n );\n if (opts.length === 0) {\n message.warning('下拉选框/单选框/多选框至少需要一个选项,请在「数据源」页的「选项列表」中添加');\n return;\n }\n finalConfig.options = opts;\n }\n // 清理空壳键:空数组/空串不落库\n ['labelKeyList', 'extendedFields', 'searchConditions', 'simpleSearchConditions', 'required_condition', 'options'].forEach((k) => {\n if (Array.isArray(finalConfig[k]) && finalConfig[k].length === 0) delete finalConfig[k];\n });\n if (finalConfig.defaultValueType === '') delete finalConfig.defaultValueType;\n props.record.advanced_search_component = finalConfig;\n jsonDirty.value = false;\n open.value = false;\n};\nconst handleCancel = () => {\n open.value = false;\n};\n</script>\n\n<style scoped lang=\"less\">\n.csc-body {\n /* 抽屉 body 与改造前的弹窗 body 一样是「确定高度的 flex item」,flex:1 + height:100% 双保险撑满剩余高度 */\n flex: 1;\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.adv-tip {\n flex: none;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n margin-bottom: 12px;\n line-height: 1.5;\n}\n.csc-tabs {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 滚动只发生在内容区:Tab 标签栏固定 */\n :deep(.ant-tabs-nav) {\n flex: none;\n margin-bottom: 12px;\n }\n :deep(.ant-tabs-content-holder) {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 抵消 a-row gutter 的 -8px 负 margin,消除多余横向滚动条 */\n padding: 0 8px;\n }\n /* 让 tabpane 拿到确定高度,JSON 编辑器才能撑满内容区 */\n :deep(.ant-tabs-content),\n :deep(.ant-tabs-tabpane) {\n height: 100%;\n }\n}\n.props-divider {\n margin: 8px 0 12px;\n}\n.csc-json-pane {\n height: 100%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.json-toolbar {\n flex: none;\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 8px;\n min-width: 0;\n}\n.json-toolbar .ant-alert {\n flex: 1;\n min-width: 0;\n}\n.json-tip {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.json-editor-wrap {\n flex: 1;\n min-height: 0;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n overflow: hidden;\n}\n/* CodeEditor 全局样式固定 300px 高,这里撑满 JSON 页剩余高度 */\n.json-editor-wrap :deep(.CodeMirror) {\n height: 100% !important;\n font-size: 13px !important;\n}\n.adv-footer {\n flex: none;\n margin-top: 8px;\n}\n/* 抽屉底部操作区(footer 插槽内容,是本文档的元素 → scoped 可命中) */\n.csc-drawer-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n</style>\n\n<!-- ★ v29.31:抽屉是 teleport 到 body 的,`root-class-name` 挂在**抽屉自己的根节点**上,\n 本组件根元素不是它的祖先 → scoped 的 :deep() 选不中,只能写全局样式(用 .csc-drawer 限定作用域)。\n ⚠️ antdv 4.2.6 的 a-drawer **没有 `wrap-class-name`**(只有 `root-class-name`/`root-style`),\n 别照搬 a-modal 的 wrap-class-name 写法 —— 会被当成普通 attr 落到别处,样式静默失效。\n ⚠️ 选择器**刻意写成三段** `.csc-drawer.ant-drawer .ant-drawer-body`(特异度 0,3,0):\n antd 的 `.ant-drawer-body` 规则经 cssinjs 生成后是 `:where(.css-hash).ant-drawer .ant-drawer-body`(0,2,0),\n 两段式 `.csc-drawer .ant-drawer-body`(同为 0,2,0)会与它**打平**,胜负交给样式注入顺序 —— 不稳。\n (`.csc-drawer` 与 `.ant-drawer` 同在抽屉根节点上:vc-drawer 把 rootClassName 并进根节点的 class 列表。) -->\n<style lang=\"less\">\n.csc-drawer.ant-drawer {\n /* 内部滚动:滚动只发生在 tabs 的 content-holder(见上方 scoped 块),\n 抽屉标题栏与底部操作区固定不随滚动 —— 与改造前的全屏弹窗行为一致 */\n .ant-drawer-body {\n display: flex;\n flex-direction: column;\n padding: 16px;\n overflow: hidden;\n }\n .ant-drawer-footer {\n flex: none;\n }\n}\n</style>\n","<template>\n <div class=\"col-toolbar\">\n <!-- ★ v29.23(用户定案):刷新/保存配置按钮移除,改为「列探测刷新」——\n 基于第 1 步表单的 SQL+数据源实时探测(不读库不写库),结果按 data_index\n 合并进当前配置:已有列保留全部人工配置、新列补默认行,绝不清空。 -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"probing\"\n @click=\"runProbeRefresh\"\n >\n 列探测刷新\n </a-button>\n <span class=\"meta-tip\">\n 基于基础配置的 SQL + 数据源实时探测列(不落库);已有列的人工配置合并保留,SQL\n 新增列自动补默认行。视图与列配置在点「确定」时同时保存。主键<strong>可多选</strong>:\n 勾 1 列时表格行键就是该列,勾 0 列或多列时改用列表接口自动生成的稳定行键\n (多列时由各主键值拼接而成),新建时探测到\n <code>id</code>\n 列会自动选中(可改)。\n </span>\n </div>\n\n <!-- ★ v29.34(用户定案):列配置表由 a-table 换成 s-table(@surely-vue/table,\n 宿主 app 的 main.ts 全局注册 `s-table`,与 tablePro / tableConfig 的\n columnsConfig.vue 同源)。换来三点 a-table 给不了的:① 固定表头 + 表体虚拟滚动\n (18 列的长清单不卡);② 列宽只认显式 width,不再被均分挤压;\n ③ 高度由容器实测下发(colTableHeight),父级不再出纵向滚动条。\n ⚠️ 对照坑:s-table 的「表体可视高度」是 `:height`(不是 scroll.y);\n `rangeSelection` 默认 'single'(拖选单元格),本表整行都是输入控件 → 显式关掉,\n 避免与输入框内的文字选择打架。 -->\n <div\n v-if=\"colTableMounted\"\n ref=\"colTableWrapRef\"\n class=\"col-table-wrapper\"\n >\n <s-table\n :columns=\"COL_COLUMNS\"\n :data-source=\"colRows\"\n row-key=\"data_index\"\n size=\"small\"\n :rowHeight=\"COL_ROW_HEIGHT\"\n :height=\"colTableHeight\"\n :scroll=\"{ x: COL_TABLE_WIDTH }\"\n :pagination=\"false\"\n :range-selection=\"false\"\n :loading=\"colLoading\"\n >\n <!-- ★ v29.37(用户报障「表格里的数据改完不会立马更新」):**所有读写都必须经\n `recordIndexs` 落到原始数据源 `colRows` 上**,禁止直接用插槽给的 `record`。\n s-table 的作者约定:它为了虚拟滚动/树形/编辑态的高性能,内部对数据做了处理,\n 插槽下发的 `record` 不属于响应式数据源;`recordIndexs: number[]` 才是\n 「按 rowKey 反查原始数据源」的索引路径(源码里 BodyCell 收到的就是\n `getIndexsByKey(rowKey)`;其内部 EditInput 写回也是这么定位 dataSource 的)。\n 非树形数据恒为 `[i]`,故一律写 `colRows[recordIndexs[0]].xxx`。\n\n 「立马更新」这条链是怎么闭合的(改完实测确认,别想当然):\n ① `colRows[...]` 是**响应式读取**,依赖挂在**本组件的渲染 effect** 上\n → 改 `colRows[i].xxx` 会触发本组件重渲染;\n ② 本组件重渲染 → s-table 的 `:scroll=\"{ x: COL_TABLE_WIDTH }\"` 是**内联对象字面量**,\n 每次 render 都是新对象,且它在编译产物的 dynamicProps 里 → Vue 的\n `shouldUpdateComponent` 走 `PROPS(8)` 分支**逐键比较**(runtime-core 源码),\n `nextProps.scroll !== prevProps.scroll` 恒成立 → **s-table 必然重渲染**\n → 插槽重新求值 → 单元格立即刷新。\n ⚠️ 反过来,用 `record.xxx` 时表达式里没有本组件的响应式依赖,改它不产生任何渲染\n 副作用(症状:勾了「默认排序」同行不展开、`已配 N 项` 不跟手、主键复选框点了没反应)。\n ⚠️ **别把 `:scroll` 之类的对象/数组 props 提成常量或 computed**:一旦它们的引用稳定,\n 上面第 ② 步就断了,s-table 会被 `shouldUpdateComponent` 跳过、整表不再刷新。\n\n ⚠️ 因此插槽解构里**刻意不接 `record`** —— 留一条 `record.xxx` 都会被 vue-tsc 报\n 「Cannot find name 'record'」,当作编译期护栏,防止回归。\n ⚠️ 前提:`row-key` 必须唯一(这里用 `data_index`,探测侧已按它去重、后端 unique_together 兜底);\n rowKey 一旦重复,`getIndexsByKey` 会返回第一个匹配项 → 改 A 行会改到 B 行。 -->\n <template #bodyCell=\"{ column, recordIndexs }\">\n <template v-if=\"column.key === 'column_order'\">\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].column_order\"\n size=\"small\"\n style=\"width: 50px\"\n :min=\"0\"\n />\n </template>\n <template v-else-if=\"column.key === 'is_primary_key'\">\n <a-checkbox\n :checked=\"!!colRows[recordIndexs[0]].is_primary_key\"\n @change=\"onPrimaryKeyChange(colRows[recordIndexs[0]], $event)\"\n />\n </template>\n <template v-else-if=\"column.key === 'title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'query_title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].query_title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'cell_component'\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].cell_component\"\n size=\"small\"\n style=\"width: 130px\"\n allow-clear\n show-search\n option-filter-prop=\"label\"\n placeholder=\"默认渲染\"\n :options=\"CELL_COMPONENT_OPTIONS\"\n />\n </template>\n <template v-else-if=\"column.key === 'checked'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].checked\" />\n </template>\n <template v-else-if=\"column.key === 'resizable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].resizable\" />\n </template>\n <template v-else-if=\"column.key === 'summary'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].summary\" />\n </template>\n <template v-else-if=\"column.key === 'is_sortable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_sortable\" />\n </template>\n <!-- ★ v29.18:默认排序三配置合一单元格——开启后出现 升/降序 + 排序顺序 -->\n <template v-else-if=\"column.key === 'default_sort'\">\n <div class=\"default-sort-cell\">\n <a-checkbox\n v-model:checked=\"colRows[recordIndexs[0]].is_default_sort\"\n @change=\"onDefaultSortToggle(colRows[recordIndexs[0]])\"\n />\n <template v-if=\"colRows[recordIndexs[0]].is_default_sort\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].default_sort_type\"\n size=\"small\"\n style=\"width: 68px\"\n :options=\"[\n { value: 'asc', label: '升序' },\n { value: 'desc', label: '降序' },\n ]\"\n />\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].default_sort_order\"\n size=\"small\"\n style=\"width: 60px\"\n :min=\"1\"\n placeholder=\"顺序\"\n />\n </template>\n </div>\n </template>\n <template v-else-if=\"column.key === 'map_color'\">\n <!-- ⚠️ 传**原始行**(不是插槽 record):模态框内部直接写 `props.record.content_map`\n / `color_map`,必须落在表格认可的原始数据源上(含 `record.cell_component = 'tag'`) -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openMapColorModal(colRows[recordIndexs[0]])\"\n >\n {{\n mapCount(colRows[recordIndexs[0]])\n ? `已配 ${mapCount(colRows[recordIndexs[0]])} 项`\n : '配置'\n }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'adv_search'\">\n <!-- ⚠️ 同上:模态框直接写 `props.record.advanced_search_component` -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openAdvSearchModal(colRows[recordIndexs[0]])\"\n >\n {{ colRows[recordIndexs[0]].advanced_search_component?.type || '配置' }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'is_base_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_base_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_default_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_default_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_group_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_group_query\" />\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-popconfirm\n title=\"确认删除该列配置?\"\n @confirm=\"removeColumn(colRows[recordIndexs[0]])\"\n >\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n >\n 删除\n </a-button>\n </a-popconfirm>\n </template>\n </template>\n </s-table>\n </div>\n\n <!-- ★ v29.16:转换/颜色 联合配置模态框 + 高级搜索组件配置模态框 -->\n <ColumnMapColorModal\n v-model:open=\"mapColorOpen\"\n :record=\"mapColorRecord!\"\n />\n <ColumnSearchComponentModal\n v-model:open=\"advSearchOpen\"\n :record=\"advSearchRecord!\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { useResizeObserver } from '@vueuse/core';\nimport { computed, nextTick, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport ColumnMapColorModal from 'liyu-pc-base/components/dbView/ColumnMapColorModal.vue';\nimport ColumnSearchComponentModal from 'liyu-pc-base/components/dbView/ColumnSearchComponentModal.vue';\nimport { extractData, toList } from './helpers';\nimport {\n CELL_COMPONENT_OPTIONS,\n COL_COLUMNS,\n COL_ROW_HEIGHT,\n COL_TABLE_WIDTH,\n PROBE_CELL_MAP,\n PROBE_CLASS_NAME_MAP,\n} from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「列配置」步(★ v29.45 起为 Step 3,§3.1.4 字段配置子表)。\n 本步持有:列行数据 / 探测 / 主键多选(v29.40) / 删除 / 默认排序开关 / 两个行内模态框,\n 以及 s-table 的「容器实测高度」链路(v29.34)。\n\n 拆分带来的三处接口(原先是同一个 setup 里的自由变量,现在必须显式打通):\n ① **进入本步的时机**:原来是外壳 `goNext`/`onStepChange` 里读 `colRows.length` 决定要不要自动探测,\n 现在下沉为 `watch(() => props.active)` —— 外壳传 `active`(`currentStep === 3`)即可,\n 顺带把 v29.34 的 `colTableMounted` 门控也挂到同一个 watch 上(挂载 + 兜底测量 + 自动探测一次做完)。\n ② **提交载荷**:外壳 `handleOk` 需要 columns / deleted_data_indexes → 由 `defineExpose` 提供\n `buildColumnsPayload` / `getDeletedIndexes` / `resetDeletedIndexes`,外壳用 ref 调用。\n ③ **清预览错误**:`runProbeRefresh` 原先顺手清 `sqlError`(第 1 步可能残留的预览报错)\n → 改为 `emit('probeStart')`,由外壳清(`sqlError` 仍是外壳持有的共享状态)。\n\n ⚠️ 本组件是**多根(fragment)模板**:三个根节点会原样插进外壳的\n `<div class=\"step-pane step-pane--col\">` 里,不额外产生 DOM 层 —— 这是刻意的,\n 多包一层 div 就会让 `.col-table-wrapper` 的 `flex: 1` 失去参照(父级不再是那个定高 flex column),\n 整条自适应高度链路会从第一环断掉。代价:外壳不能给本组件传 class / v-show(fragment 无法继承),\n 显隐由外壳那层 div 的 v-show 负责。 */\n\ninterface Props {\n /** 外壳告知「当前就是本步」(= currentStep === 3):用于挂载表格与进入时自动探测 */\n active: boolean;\n model: any;\n record: any;\n formType: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['probeStart']);\n\nconst model = computed(() => props.model);\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* ═══ 列配置子表(§3.1.4) ═══\n ★ v29.23:数据来源两路——编辑态预取走 DB(loadColumns);探测刷新走 probe_columns\n (基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并:\n 已有列保留全部人工配置(class_name/sql_type 属类型信息,随探测更新)、\n 新列补默认行;探测里已消失的列保留在末尾(不清除现有配置,可手动删行)。 */\nconst colRows = ref<any[]>([]);\nconst colLoading = ref(false);\n/* v29.11:本次保存要删除的 data_index(删除仅移除列配置行,不改 SQL) */\nconst deletedDataIndexes = ref<string[]>([]);\n\n/* ═══ ★ v29.34(用户定案):列配置表换 s-table + 容器自适应高度 ═══\n 高度算法照抄 `tablePro/tableConfig/columnsConfig.vue` 的 `tableHeight`:\n **量「装表格的那层容器」,把它原样交给 s-table 当整体高度**。\n 目标不变量(只有一条):**整个本步(列配置)里只能有 s-table 自己的那根滚动条**——\n 表体纵向滚动由 s-table 负责,横向由 `scroll.x` 负责,面板(`.step-pane.step-pane--col`)既不滚也不溢。\n 推论:`colTableHeight` 必须等于「面板高 − 工具条高 − 间距」,多 1px 就会把面板撑出\n 第二根滚动条,少 1px 表格底部就留白。所以链路必须整段闭合,断任何一环都拿不到这个差:\n ① `.step-pane.step-pane--col`(在外壳,作用在外壳那层 div 上):定高(继承 .step-pane 的\n `calc(80vh - 150px)`)+ flex column + `overflow: hidden`(兜住亚像素误差);\n ② `.col-table-wrapper`(本组件):`flex: 1; min-height: 0`,**且只包表格、不包工具条**——\n 它的高度由 flex 解析出来(不是靠内容撑),正好等于「剩余高度」;\n 这也是能安全观测自己的原因:高度不依赖表格内容,不存在「观测→改高→再观测」循环;\n ③ `:height=\"colTableHeight\"`:s-table 的整体高度(表头 + 表体,s-table 用 `height`\n 表达 scroll.y)。留白方向偏安全:用整数 `clientHeight` 截断,宁可差不到 1px 也不外溢。\n 实测来源两路互补:\n - `useResizeObserver`:容器几何变化(切步骤 / 弹窗尺寸变化 / 字体缩放)就重下发;\n - 首次进入本步时先挂载(见下方 watch(active))、再 `nextTick` 显式量一次做兜底,\n 且 `if (h > 0)` 守卫保证「隐藏期间不清掉上一次的有效高度」。 */\nconst colTableWrapRef = ref<HTMLElement | null>(null);\nconst colTableHeight = ref(320); // 兜底值:RO 首帧 / 隐藏期间报不出有效高度时的可用高度\nconst measureColTable = () => {\n const h = colTableWrapRef.value?.clientHeight || 0;\n if (h > 0) colTableHeight.value = h;\n};\nuseResizeObserver(colTableWrapRef, entries => {\n const h = entries[0]?.contentRect?.height || 0;\n if (h > 0) colTableHeight.value = h;\n});\n\n/* s-table 的**挂载时机**:必须等本步真正完成布局之后再挂。\n 为什么不能直接写 `v-if=\"active\"`:`v-show` 的 `display` 是 Vue 的\n vShow 指令在 **updated 钩子**里写的,而指令钩子走 post-flush(整个 patch 结束之后)——\n 于是「把表格插进 DOM」的那一刻,父级还是 display:none,s-table 首帧量到 0 宽 0 高。\n 用 `nextTick` 把挂载推到 flush 之后即可(此时 display 已生效)。\n 挂载后就不再卸载:来回切步骤由外壳的 v-show 隐藏,保留表格内部的横向滚动位置。 */\nconst colTableMounted = ref(false);\nwatch(\n () => props.active,\n act => {\n if (!act) return;\n colTableMounted.value = true;\n nextTick(measureColTable); // 挂载后立刻量一次;此后由 ResizeObserver 接管\n /* ★ v29.23:进入本步且当前无列(新建流程)→ 按基础配置的 SQL+数据源自动探测推断\n (原先写在外壳的 goNext / onStepChange 里,随本步一起下沉) */\n if (!colRows.value.length) runProbeRefresh();\n },\n);\n\n/* ★ v29.26(用户定案):新建流程自动探测列后,若存在 `id` 列(不分大小写)则自动选它为主键。\n `pkAutoLocked` = 自动选择只做一次、且一旦用户手动动过主键就不再干预\n (否则用户取消勾选后再点「列探测刷新」又被自动勾回来)。 */\nconst pkAutoLocked = ref(false);\n\n/* 在探测出的列集合里自动指定主键:命中 `id` 列且当前一列主键都没有时生效。\n ★ v29.40:主键可多选后本函数**只勾 id 那一列**(不再整列重置)—— 前置守卫已保证\n 「当前一列主键都没有」,所以效果与原先的「互斥式整列重置」等价,但语义更直白。 */\nconst autoPickPrimaryKey = (rows: any[]) => {\n if (pkAutoLocked.value || props.formType === 'edit') return;\n if (rows.some((r: any) => !!r.is_primary_key)) return; // 已有主键(如探测保留的配置)不覆盖\n const idRow = rows.find(\n (r: any) =>\n String(r.data_index || '')\n .trim()\n .toLowerCase() === 'id',\n );\n if (!idRow) return;\n idRow.is_primary_key = true;\n pkAutoLocked.value = true;\n};\n\n/* ★ v29.24 引入 / ★ v29.40(用户定案)改为**可多选**:纯 toggle,不再「勾一个自动清其他」。\n 语义与后端 rowKey 派生对齐(db_view/utils.resolve_row_key):\n · 勾 0 列 → rowKey 落 '_id',值 = 整行内容 hash(隐含主键)\n · 勾 1 列 → rowKey = 该列名\n · 勾 ≥2 列 → rowKey 落 '_id',值 = 各主键列值用 '|' 连接\n 后端保存侧原先的「最多一列 True」兜底已一并删除(is_primary_key 本来就逐行落库)。 */\nconst onPrimaryKeyChange = (record: any, e: any) => {\n pkAutoLocked.value = true; // ★ v29.26:用户手动改过 → 后续探测刷新不再自动指定\n record.is_primary_key = !!e?.target?.checked;\n};\n\nconst buildDefaultColumn = (name: string, type: string, order: number) => ({\n data_index: name,\n title: name,\n query_title: undefined,\n column_order: order,\n width: 150,\n checked: true,\n ellipsis: true,\n resizable: true,\n summary: false,\n is_primary_key: false, // ★ v29.24:默认不指定主键(人工勾选;一列都不勾走隐含 _id)\n // ★ v29.23.2(用户定案):新建时 可排序/基础搜索/默认搜索 默认全部勾选\n is_sortable: true,\n is_default_sort: false,\n default_sort_type: null,\n default_sort_order: null,\n class_name: PROBE_CLASS_NAME_MAP[type] || 'str',\n sql_type: type || '',\n cell_component: PROBE_CELL_MAP[type] || '',\n is_base_query: true,\n is_default_query: true,\n is_group_query: false,\n});\n\nconst probing = ref(false);\n\nconst runProbeRefresh = async () => {\n if (!model.value.data_source_id || !model.value.sql) {\n message.warning('请先在「基础配置」步骤选择数据源并填写 SQL');\n return;\n }\n probing.value = true;\n /* ★ v29.35:原先是 `sqlError.value = ''`(清掉第 1 步可能残留的预览报错)——\n sqlError 归外壳持有(第 1 步渲染它),这里改成事件通知,行为不变 */\n emit('probeStart');\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/probe_columns`,\n );\n const payload = extractData(res);\n\n const cols: any[] = payload?.columns || [];\n if (!cols.length) {\n message.warning('探测到 0 列,请检查 SQL 输出');\n return;\n }\n /* 按 data_index 合并:命中行保留配置只更新类型信息;新列追加默认行;已消失列保留 */\n const byIdx = new Map<string, any>(colRows.value.map((r: any) => [r.data_index, r]));\n const next: any[] = [];\n const seen = new Set<string>();\n cols.forEach((c: any, i: number) => {\n if (seen.has(c.name)) return; // SQL 输出重名列兜底(unique_together 会炸)\n seen.add(c.name);\n const exist = byIdx.get(c.name);\n if (exist) {\n exist.class_name = PROBE_CLASS_NAME_MAP[c.type] || exist.class_name || 'str';\n exist.sql_type = c.type || exist.sql_type || '';\n next.push(exist);\n } else {\n next.push(buildDefaultColumn(c.name, c.type || '', i + 1));\n }\n });\n colRows.value.forEach((r: any) => {\n if (!seen.has(r.data_index)) next.push(r); // 探测里没有的现有列:原样保留(不清除配置)\n });\n // ★ v29.26:新建流程探测后若存在 `id` 列,自动选为主键(用户手动改过则不干预)\n autoPickPrimaryKey(next);\n colRows.value = next;\n } finally {\n probing.value = false;\n }\n};\n\n/* ★ v29.18:默认排序开关——开启时默认升序 + 排序顺序=1(用户可再改) */\nconst onDefaultSortToggle = (record: any) => {\n if (record.is_default_sort) {\n record.default_sort_type = record.default_sort_type || 'asc';\n record.default_sort_order = record.default_sort_order || 1;\n }\n};\n\n/* 编辑态预取已保存的列配置(★ v29.23:仅作底数,列探测刷新可在此基础上实时合并) */\nconst loadColumns = async () => {\n if (props.formType !== 'edit' || props.record?.id == null) return;\n colLoading.value = true;\n try {\n /* ★ v29.8.1(用户定案):读取走 lybase 通用 query_db_view_column_list,\n filter_fields 契约按 view_id 过滤;不传 sort_fields → 按 Meta.ordering(column_order)排序 */\n const res = await getApiData(\n {\n org_id: current_org?.id,\n filter_fields: ['view_id'],\n view_id: props.record.id,\n is_pagination: false,\n },\n `${props.record.model_app}/query_db_view_column_list`,\n );\n colRows.value = toList(res);\n deletedDataIndexes.value = []; // 刷新 = 放弃本地未保存的删改\n } catch (e) {\n colRows.value = [];\n } finally {\n colLoading.value = false;\n }\n};\n\nconst removeColumn = (record: any) => {\n colRows.value = colRows.value.filter((r: any) => r.data_index !== record.data_index);\n deletedDataIndexes.value.push(record.data_index);\n};\n\n/* ═══ ★ v29.16:列配置行内两个模态框 ═══ */\n/* 转换/颜色(content_map + color_map 联合配置,用户定案:不割裂为两个入口) */\nconst mapColorOpen = ref(false);\nconst mapColorRecord = ref<any>(null);\nconst openMapColorModal = (record: any) => {\n mapColorRecord.value = record;\n mapColorOpen.value = true;\n};\nconst mapCount = (record: any) => {\n const keys = new Set([\n ...Object.keys(record?.content_map || {}),\n ...Object.keys(record?.color_map || {}),\n ]);\n return keys.size;\n};\n/* 高级搜索组件(完整组件配置对象,JSONField 落库) */\nconst advSearchOpen = ref(false);\nconst advSearchRecord = ref<any>(null);\nconst openAdvSearchModal = (record: any) => {\n advSearchRecord.value = record;\n advSearchOpen.value = true;\n};\n\n/* ═══ ★ v29.23:列配置随视图一起保存(save_view 同时落两表) ═══\n 此前 doSaveColumns/saveColumns 独立按钮与连带保存已移除——外壳 handleOk 统一打包\n 主表字段 + columns + deleted_data_indexes 提交 save_view(新建/编辑共用),\n 故这里把两个「取值口」expose 给外壳。 */\nconst buildColumnsPayload = () =>\n colRows.value.map((r: any) => ({\n data_index: r.data_index,\n column_order: r.column_order,\n title: r.title,\n query_title: r.query_title,\n checked: r.checked,\n ellipsis: r.ellipsis,\n resizable: r.resizable,\n summary: r.summary,\n is_sortable: !!r.is_sortable, // ★ v29.16:sorter(JSON) → is_sortable(Bool)\n is_default_sort: !!r.is_default_sort, // ★ v29.18:默认排序三字段\n default_sort_type: r.is_default_sort ? r.default_sort_type || 'asc' : null,\n default_sort_order: r.is_default_sort ? r.default_sort_order ?? null : null,\n cell_component: r.cell_component,\n is_base_query: r.is_base_query,\n is_default_query: r.is_default_query,\n is_group_query: r.is_group_query,\n content_map: r.content_map ?? null, // ★ v29.16:转换/颜色 模态框产出\n color_map: r.color_map ?? null,\n advanced_search_component: r.advanced_search_component ?? null, // ★ v29.16:组件配置对象\n // ★ v29.23.1:类型信息随保存落库——后端 _upsert_columns 白名单已放行;\n // 漏传会导致编辑态探测出的类型保存即丢(保存后再次编辑类型恒空)\n class_name: r.class_name || '',\n sql_type: r.sql_type || '',\n // ★ v29.24:主键标记。★ v29.40:可多选 —— 逐行原样照传,后端不再做「最多一列」收敛\n is_primary_key: !!r.is_primary_key,\n }));\n\ndefineExpose({\n /** 提交用:列配置载荷(外壳 handleOk 取) */\n buildColumnsPayload,\n /** 提交用:本次要删除的 data_index(外壳 handleOk 取) */\n getDeletedIndexes: () => [...deletedDataIndexes.value],\n /** 保存成功后:清空待删除列表(原先由外壳直接 `deletedDataIndexes.value = []` 完成) */\n resetDeletedIndexes: () => {\n deletedDataIndexes.value = [];\n },\n});\n\nonMounted(() => {\n /* 编辑态预取列配置(原先在外壳 onMounted 的 edit 分支里调;本组件的 mounted 早于外壳,\n 但 `formType` / `record.id` 都由外壳在 setup 顶层就绪,读到的值一定是对的) */\n if (props.formType === 'edit') loadColumns();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.col-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n}\n/* ★ v29.34:s-table 的高度来源容器——flex:1 撑满工具条之外的空间;min-height:0 让\n 它能在内容更高时被压缩(否则 flex 子项默认 min-height:auto,会顶破父级定高)。 */\n.col-table-wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n/* ★ v29.18:默认排序单元格——开关 + 升降序 + 顺序 横排 */\n.default-sort-cell {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n</style>\n","<template>\n <div class=\"col-toolbar\">\n <!-- ★ v29.23(用户定案):刷新/保存配置按钮移除,改为「列探测刷新」——\n 基于第 1 步表单的 SQL+数据源实时探测(不读库不写库),结果按 data_index\n 合并进当前配置:已有列保留全部人工配置、新列补默认行,绝不清空。 -->\n <a-button\n size=\"small\"\n :disabled=\"!model.data_source_id || !model.sql\"\n :loading=\"probing\"\n @click=\"runProbeRefresh\"\n >\n 列探测刷新\n </a-button>\n <span class=\"meta-tip\">\n 基于基础配置的 SQL + 数据源实时探测列(不落库);已有列的人工配置合并保留,SQL\n 新增列自动补默认行。视图与列配置在点「确定」时同时保存。主键<strong>可多选</strong>:\n 勾 1 列时表格行键就是该列,勾 0 列或多列时改用列表接口自动生成的稳定行键\n (多列时由各主键值拼接而成),新建时探测到\n <code>id</code>\n 列会自动选中(可改)。\n </span>\n </div>\n\n <!-- ★ v29.34(用户定案):列配置表由 a-table 换成 s-table(@surely-vue/table,\n 宿主 app 的 main.ts 全局注册 `s-table`,与 tablePro / tableConfig 的\n columnsConfig.vue 同源)。换来三点 a-table 给不了的:① 固定表头 + 表体虚拟滚动\n (18 列的长清单不卡);② 列宽只认显式 width,不再被均分挤压;\n ③ 高度由容器实测下发(colTableHeight),父级不再出纵向滚动条。\n ⚠️ 对照坑:s-table 的「表体可视高度」是 `:height`(不是 scroll.y);\n `rangeSelection` 默认 'single'(拖选单元格),本表整行都是输入控件 → 显式关掉,\n 避免与输入框内的文字选择打架。 -->\n <div\n v-if=\"colTableMounted\"\n ref=\"colTableWrapRef\"\n class=\"col-table-wrapper\"\n >\n <s-table\n :columns=\"COL_COLUMNS\"\n :data-source=\"colRows\"\n row-key=\"data_index\"\n size=\"small\"\n :rowHeight=\"COL_ROW_HEIGHT\"\n :height=\"colTableHeight\"\n :scroll=\"{ x: COL_TABLE_WIDTH }\"\n :pagination=\"false\"\n :range-selection=\"false\"\n :loading=\"colLoading\"\n >\n <!-- ★ v29.37(用户报障「表格里的数据改完不会立马更新」):**所有读写都必须经\n `recordIndexs` 落到原始数据源 `colRows` 上**,禁止直接用插槽给的 `record`。\n s-table 的作者约定:它为了虚拟滚动/树形/编辑态的高性能,内部对数据做了处理,\n 插槽下发的 `record` 不属于响应式数据源;`recordIndexs: number[]` 才是\n 「按 rowKey 反查原始数据源」的索引路径(源码里 BodyCell 收到的就是\n `getIndexsByKey(rowKey)`;其内部 EditInput 写回也是这么定位 dataSource 的)。\n 非树形数据恒为 `[i]`,故一律写 `colRows[recordIndexs[0]].xxx`。\n\n 「立马更新」这条链是怎么闭合的(改完实测确认,别想当然):\n ① `colRows[...]` 是**响应式读取**,依赖挂在**本组件的渲染 effect** 上\n → 改 `colRows[i].xxx` 会触发本组件重渲染;\n ② 本组件重渲染 → s-table 的 `:scroll=\"{ x: COL_TABLE_WIDTH }\"` 是**内联对象字面量**,\n 每次 render 都是新对象,且它在编译产物的 dynamicProps 里 → Vue 的\n `shouldUpdateComponent` 走 `PROPS(8)` 分支**逐键比较**(runtime-core 源码),\n `nextProps.scroll !== prevProps.scroll` 恒成立 → **s-table 必然重渲染**\n → 插槽重新求值 → 单元格立即刷新。\n ⚠️ 反过来,用 `record.xxx` 时表达式里没有本组件的响应式依赖,改它不产生任何渲染\n 副作用(症状:勾了「默认排序」同行不展开、`已配 N 项` 不跟手、主键复选框点了没反应)。\n ⚠️ **别把 `:scroll` 之类的对象/数组 props 提成常量或 computed**:一旦它们的引用稳定,\n 上面第 ② 步就断了,s-table 会被 `shouldUpdateComponent` 跳过、整表不再刷新。\n\n ⚠️ 因此插槽解构里**刻意不接 `record`** —— 留一条 `record.xxx` 都会被 vue-tsc 报\n 「Cannot find name 'record'」,当作编译期护栏,防止回归。\n ⚠️ 前提:`row-key` 必须唯一(这里用 `data_index`,探测侧已按它去重、后端 unique_together 兜底);\n rowKey 一旦重复,`getIndexsByKey` 会返回第一个匹配项 → 改 A 行会改到 B 行。 -->\n <template #bodyCell=\"{ column, recordIndexs }\">\n <template v-if=\"column.key === 'column_order'\">\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].column_order\"\n size=\"small\"\n style=\"width: 50px\"\n :min=\"0\"\n />\n </template>\n <template v-else-if=\"column.key === 'is_primary_key'\">\n <a-checkbox\n :checked=\"!!colRows[recordIndexs[0]].is_primary_key\"\n @change=\"onPrimaryKeyChange(colRows[recordIndexs[0]], $event)\"\n />\n </template>\n <template v-else-if=\"column.key === 'title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'query_title'\">\n <a-input\n v-model:value=\"colRows[recordIndexs[0]].query_title\"\n size=\"small\"\n allow-clear\n />\n </template>\n <template v-else-if=\"column.key === 'cell_component'\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].cell_component\"\n size=\"small\"\n style=\"width: 130px\"\n allow-clear\n show-search\n option-filter-prop=\"label\"\n placeholder=\"默认渲染\"\n :options=\"CELL_COMPONENT_OPTIONS\"\n />\n </template>\n <template v-else-if=\"column.key === 'checked'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].checked\" />\n </template>\n <template v-else-if=\"column.key === 'resizable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].resizable\" />\n </template>\n <template v-else-if=\"column.key === 'summary'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].summary\" />\n </template>\n <template v-else-if=\"column.key === 'is_sortable'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_sortable\" />\n </template>\n <!-- ★ v29.18:默认排序三配置合一单元格——开启后出现 升/降序 + 排序顺序 -->\n <template v-else-if=\"column.key === 'default_sort'\">\n <div class=\"default-sort-cell\">\n <a-checkbox\n v-model:checked=\"colRows[recordIndexs[0]].is_default_sort\"\n @change=\"onDefaultSortToggle(colRows[recordIndexs[0]])\"\n />\n <template v-if=\"colRows[recordIndexs[0]].is_default_sort\">\n <a-select\n v-model:value=\"colRows[recordIndexs[0]].default_sort_type\"\n size=\"small\"\n style=\"width: 68px\"\n :options=\"[\n { value: 'asc', label: '升序' },\n { value: 'desc', label: '降序' },\n ]\"\n />\n <a-input-number\n v-model:value=\"colRows[recordIndexs[0]].default_sort_order\"\n size=\"small\"\n style=\"width: 60px\"\n :min=\"1\"\n placeholder=\"顺序\"\n />\n </template>\n </div>\n </template>\n <template v-else-if=\"column.key === 'map_color'\">\n <!-- ⚠️ 传**原始行**(不是插槽 record):模态框内部直接写 `props.record.content_map`\n / `color_map`,必须落在表格认可的原始数据源上(含 `record.cell_component = 'tag'`) -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openMapColorModal(colRows[recordIndexs[0]])\"\n >\n {{\n mapCount(colRows[recordIndexs[0]])\n ? `已配 ${mapCount(colRows[recordIndexs[0]])} 项`\n : '配置'\n }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'adv_search'\">\n <!-- ⚠️ 同上:模态框直接写 `props.record.advanced_search_component` -->\n <a-button\n size=\"small\"\n style=\"padding: 0 4px\"\n @click=\"openAdvSearchModal(colRows[recordIndexs[0]])\"\n >\n {{ colRows[recordIndexs[0]].advanced_search_component?.type || '配置' }}\n </a-button>\n </template>\n <template v-else-if=\"column.key === 'is_base_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_base_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_default_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_default_query\" />\n </template>\n <template v-else-if=\"column.key === 'is_group_query'\">\n <a-checkbox v-model:checked=\"colRows[recordIndexs[0]].is_group_query\" />\n </template>\n <template v-else-if=\"column.key === 'action'\">\n <a-popconfirm\n title=\"确认删除该列配置?\"\n @confirm=\"removeColumn(colRows[recordIndexs[0]])\"\n >\n <a-button\n type=\"link\"\n danger\n size=\"small\"\n style=\"padding: 0\"\n >\n 删除\n </a-button>\n </a-popconfirm>\n </template>\n </template>\n </s-table>\n </div>\n\n <!-- ★ v29.16:转换/颜色 联合配置模态框 + 高级搜索组件配置模态框 -->\n <ColumnMapColorModal\n v-model:open=\"mapColorOpen\"\n :record=\"mapColorRecord!\"\n />\n <ColumnSearchComponentModal\n v-model:open=\"advSearchOpen\"\n :record=\"advSearchRecord!\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { useResizeObserver } from '@vueuse/core';\nimport { computed, nextTick, onMounted, ref, watch } from 'vue';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport ColumnMapColorModal from 'liyu-pc-base/components/dbView/ColumnMapColorModal.vue';\nimport ColumnSearchComponentModal from 'liyu-pc-base/components/dbView/ColumnSearchComponentModal.vue';\nimport { extractData, toList } from './helpers';\nimport {\n CELL_COMPONENT_OPTIONS,\n COL_COLUMNS,\n COL_ROW_HEIGHT,\n COL_TABLE_WIDTH,\n PROBE_CELL_MAP,\n PROBE_CLASS_NAME_MAP,\n} from './constants';\n\n/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分——「列配置」步(★ v29.45 起为 Step 3,§3.1.4 字段配置子表)。\n 本步持有:列行数据 / 探测 / 主键多选(v29.40) / 删除 / 默认排序开关 / 两个行内模态框,\n 以及 s-table 的「容器实测高度」链路(v29.34)。\n\n 拆分带来的三处接口(原先是同一个 setup 里的自由变量,现在必须显式打通):\n ① **进入本步的时机**:原来是外壳 `goNext`/`onStepChange` 里读 `colRows.length` 决定要不要自动探测,\n 现在下沉为 `watch(() => props.active)` —— 外壳传 `active`(`currentStep === 3`)即可,\n 顺带把 v29.34 的 `colTableMounted` 门控也挂到同一个 watch 上(挂载 + 兜底测量 + 自动探测一次做完)。\n ② **提交载荷**:外壳 `handleOk` 需要 columns / deleted_data_indexes → 由 `defineExpose` 提供\n `buildColumnsPayload` / `getDeletedIndexes` / `resetDeletedIndexes`,外壳用 ref 调用。\n ③ **清预览错误**:`runProbeRefresh` 原先顺手清 `sqlError`(第 1 步可能残留的预览报错)\n → 改为 `emit('probeStart')`,由外壳清(`sqlError` 仍是外壳持有的共享状态)。\n\n ⚠️ 本组件是**多根(fragment)模板**:三个根节点会原样插进外壳的\n `<div class=\"step-pane step-pane--col\">` 里,不额外产生 DOM 层 —— 这是刻意的,\n 多包一层 div 就会让 `.col-table-wrapper` 的 `flex: 1` 失去参照(父级不再是那个定高 flex column),\n 整条自适应高度链路会从第一环断掉。代价:外壳不能给本组件传 class / v-show(fragment 无法继承),\n 显隐由外壳那层 div 的 v-show 负责。 */\n\ninterface Props {\n /** 外壳告知「当前就是本步」(= currentStep === 3):用于挂载表格与进入时自动探测 */\n active: boolean;\n model: any;\n record: any;\n formType: string;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['probeStart']);\n\nconst model = computed(() => props.model);\n\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* ═══ 列配置子表(§3.1.4) ═══\n ★ v29.23:数据来源两路——编辑态预取走 DB(loadColumns);探测刷新走 probe_columns\n (基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并:\n 已有列保留全部人工配置(class_name/sql_type 属类型信息,随探测更新)、\n 新列补默认行;探测里已消失的列保留在末尾(不清除现有配置,可手动删行)。 */\nconst colRows = ref<any[]>([]);\nconst colLoading = ref(false);\n/* v29.11:本次保存要删除的 data_index(删除仅移除列配置行,不改 SQL) */\nconst deletedDataIndexes = ref<string[]>([]);\n\n/* ═══ ★ v29.34(用户定案):列配置表换 s-table + 容器自适应高度 ═══\n 高度算法照抄 `tablePro/tableConfig/columnsConfig.vue` 的 `tableHeight`:\n **量「装表格的那层容器」,把它原样交给 s-table 当整体高度**。\n 目标不变量(只有一条):**整个本步(列配置)里只能有 s-table 自己的那根滚动条**——\n 表体纵向滚动由 s-table 负责,横向由 `scroll.x` 负责,面板(`.step-pane.step-pane--col`)既不滚也不溢。\n 推论:`colTableHeight` 必须等于「面板高 − 工具条高 − 间距」,多 1px 就会把面板撑出\n 第二根滚动条,少 1px 表格底部就留白。所以链路必须整段闭合,断任何一环都拿不到这个差:\n ① `.step-pane.step-pane--col`(在外壳,作用在外壳那层 div 上):定高(继承 .step-pane 的\n `calc(80vh - 150px)`)+ flex column + `overflow: hidden`(兜住亚像素误差);\n ② `.col-table-wrapper`(本组件):`flex: 1; min-height: 0`,**且只包表格、不包工具条**——\n 它的高度由 flex 解析出来(不是靠内容撑),正好等于「剩余高度」;\n 这也是能安全观测自己的原因:高度不依赖表格内容,不存在「观测→改高→再观测」循环;\n ③ `:height=\"colTableHeight\"`:s-table 的整体高度(表头 + 表体,s-table 用 `height`\n 表达 scroll.y)。留白方向偏安全:用整数 `clientHeight` 截断,宁可差不到 1px 也不外溢。\n 实测来源两路互补:\n - `useResizeObserver`:容器几何变化(切步骤 / 弹窗尺寸变化 / 字体缩放)就重下发;\n - 首次进入本步时先挂载(见下方 watch(active))、再 `nextTick` 显式量一次做兜底,\n 且 `if (h > 0)` 守卫保证「隐藏期间不清掉上一次的有效高度」。 */\nconst colTableWrapRef = ref<HTMLElement | null>(null);\nconst colTableHeight = ref(320); // 兜底值:RO 首帧 / 隐藏期间报不出有效高度时的可用高度\nconst measureColTable = () => {\n const h = colTableWrapRef.value?.clientHeight || 0;\n if (h > 0) colTableHeight.value = h;\n};\nuseResizeObserver(colTableWrapRef, entries => {\n const h = entries[0]?.contentRect?.height || 0;\n if (h > 0) colTableHeight.value = h;\n});\n\n/* s-table 的**挂载时机**:必须等本步真正完成布局之后再挂。\n 为什么不能直接写 `v-if=\"active\"`:`v-show` 的 `display` 是 Vue 的\n vShow 指令在 **updated 钩子**里写的,而指令钩子走 post-flush(整个 patch 结束之后)——\n 于是「把表格插进 DOM」的那一刻,父级还是 display:none,s-table 首帧量到 0 宽 0 高。\n 用 `nextTick` 把挂载推到 flush 之后即可(此时 display 已生效)。\n 挂载后就不再卸载:来回切步骤由外壳的 v-show 隐藏,保留表格内部的横向滚动位置。 */\nconst colTableMounted = ref(false);\nwatch(\n () => props.active,\n act => {\n if (!act) return;\n colTableMounted.value = true;\n nextTick(measureColTable); // 挂载后立刻量一次;此后由 ResizeObserver 接管\n /* ★ v29.23:进入本步且当前无列(新建流程)→ 按基础配置的 SQL+数据源自动探测推断\n (原先写在外壳的 goNext / onStepChange 里,随本步一起下沉) */\n if (!colRows.value.length) runProbeRefresh();\n },\n);\n\n/* ★ v29.26(用户定案):新建流程自动探测列后,若存在 `id` 列(不分大小写)则自动选它为主键。\n `pkAutoLocked` = 自动选择只做一次、且一旦用户手动动过主键就不再干预\n (否则用户取消勾选后再点「列探测刷新」又被自动勾回来)。 */\nconst pkAutoLocked = ref(false);\n\n/* 在探测出的列集合里自动指定主键:命中 `id` 列且当前一列主键都没有时生效。\n ★ v29.40:主键可多选后本函数**只勾 id 那一列**(不再整列重置)—— 前置守卫已保证\n 「当前一列主键都没有」,所以效果与原先的「互斥式整列重置」等价,但语义更直白。 */\nconst autoPickPrimaryKey = (rows: any[]) => {\n if (pkAutoLocked.value || props.formType === 'edit') return;\n if (rows.some((r: any) => !!r.is_primary_key)) return; // 已有主键(如探测保留的配置)不覆盖\n const idRow = rows.find(\n (r: any) =>\n String(r.data_index || '')\n .trim()\n .toLowerCase() === 'id',\n );\n if (!idRow) return;\n idRow.is_primary_key = true;\n pkAutoLocked.value = true;\n};\n\n/* ★ v29.24 引入 / ★ v29.40(用户定案)改为**可多选**:纯 toggle,不再「勾一个自动清其他」。\n 语义与后端 rowKey 派生对齐(db_view/utils.resolve_row_key):\n · 勾 0 列 → rowKey 落 '_id',值 = 整行内容 hash(隐含主键)\n · 勾 1 列 → rowKey = 该列名\n · 勾 ≥2 列 → rowKey 落 '_id',值 = 各主键列值用 '|' 连接\n 后端保存侧原先的「最多一列 True」兜底已一并删除(is_primary_key 本来就逐行落库)。 */\nconst onPrimaryKeyChange = (record: any, e: any) => {\n pkAutoLocked.value = true; // ★ v29.26:用户手动改过 → 后续探测刷新不再自动指定\n record.is_primary_key = !!e?.target?.checked;\n};\n\nconst buildDefaultColumn = (name: string, type: string, order: number) => ({\n data_index: name,\n title: name,\n query_title: undefined,\n column_order: order,\n width: 150,\n checked: true,\n ellipsis: true,\n resizable: true,\n summary: false,\n is_primary_key: false, // ★ v29.24:默认不指定主键(人工勾选;一列都不勾走隐含 _id)\n // ★ v29.23.2(用户定案):新建时 可排序/基础搜索/默认搜索 默认全部勾选\n is_sortable: true,\n is_default_sort: false,\n default_sort_type: null,\n default_sort_order: null,\n class_name: PROBE_CLASS_NAME_MAP[type] || 'str',\n sql_type: type || '',\n cell_component: PROBE_CELL_MAP[type] || '',\n is_base_query: true,\n is_default_query: true,\n is_group_query: false,\n});\n\nconst probing = ref(false);\n\nconst runProbeRefresh = async () => {\n if (!model.value.data_source_id || !model.value.sql) {\n message.warning('请先在「基础配置」步骤选择数据源并填写 SQL');\n return;\n }\n probing.value = true;\n /* ★ v29.35:原先是 `sqlError.value = ''`(清掉第 1 步可能残留的预览报错)——\n sqlError 归外壳持有(第 1 步渲染它),这里改成事件通知,行为不变 */\n emit('probeStart');\n try {\n const res: any = await getApiData(\n { org_id: current_org?.id, data_source_id: model.value.data_source_id, sql: model.value.sql },\n `${props.record.model_app}/probe_columns`,\n );\n const payload = extractData(res);\n\n const cols: any[] = payload?.columns || [];\n if (!cols.length) {\n message.warning('探测到 0 列,请检查 SQL 输出');\n return;\n }\n /* 按 data_index 合并:命中行保留配置只更新类型信息;新列追加默认行;已消失列保留 */\n const byIdx = new Map<string, any>(colRows.value.map((r: any) => [r.data_index, r]));\n const next: any[] = [];\n const seen = new Set<string>();\n cols.forEach((c: any, i: number) => {\n if (seen.has(c.name)) return; // SQL 输出重名列兜底(unique_together 会炸)\n seen.add(c.name);\n const exist = byIdx.get(c.name);\n if (exist) {\n exist.class_name = PROBE_CLASS_NAME_MAP[c.type] || exist.class_name || 'str';\n exist.sql_type = c.type || exist.sql_type || '';\n next.push(exist);\n } else {\n next.push(buildDefaultColumn(c.name, c.type || '', i + 1));\n }\n });\n colRows.value.forEach((r: any) => {\n if (!seen.has(r.data_index)) next.push(r); // 探测里没有的现有列:原样保留(不清除配置)\n });\n // ★ v29.26:新建流程探测后若存在 `id` 列,自动选为主键(用户手动改过则不干预)\n autoPickPrimaryKey(next);\n colRows.value = next;\n } finally {\n probing.value = false;\n }\n};\n\n/* ★ v29.18:默认排序开关——开启时默认升序 + 排序顺序=1(用户可再改) */\nconst onDefaultSortToggle = (record: any) => {\n if (record.is_default_sort) {\n record.default_sort_type = record.default_sort_type || 'asc';\n record.default_sort_order = record.default_sort_order || 1;\n }\n};\n\n/* 编辑态预取已保存的列配置(★ v29.23:仅作底数,列探测刷新可在此基础上实时合并) */\nconst loadColumns = async () => {\n if (props.formType !== 'edit' || props.record?.id == null) return;\n colLoading.value = true;\n try {\n /* ★ v29.8.1(用户定案):读取走 lybase 通用 query_db_view_column_list,\n filter_fields 契约按 view_id 过滤;不传 sort_fields → 按 Meta.ordering(column_order)排序 */\n const res = await getApiData(\n {\n org_id: current_org?.id,\n filter_fields: ['view_id'],\n view_id: props.record.id,\n is_pagination: false,\n },\n `${props.record.model_app}/query_db_view_column_list`,\n );\n colRows.value = toList(res);\n deletedDataIndexes.value = []; // 刷新 = 放弃本地未保存的删改\n } catch (e) {\n colRows.value = [];\n } finally {\n colLoading.value = false;\n }\n};\n\nconst removeColumn = (record: any) => {\n colRows.value = colRows.value.filter((r: any) => r.data_index !== record.data_index);\n deletedDataIndexes.value.push(record.data_index);\n};\n\n/* ═══ ★ v29.16:列配置行内两个模态框 ═══ */\n/* 转换/颜色(content_map + color_map 联合配置,用户定案:不割裂为两个入口) */\nconst mapColorOpen = ref(false);\nconst mapColorRecord = ref<any>(null);\nconst openMapColorModal = (record: any) => {\n mapColorRecord.value = record;\n mapColorOpen.value = true;\n};\nconst mapCount = (record: any) => {\n const keys = new Set([\n ...Object.keys(record?.content_map || {}),\n ...Object.keys(record?.color_map || {}),\n ]);\n return keys.size;\n};\n/* 高级搜索组件(完整组件配置对象,JSONField 落库) */\nconst advSearchOpen = ref(false);\nconst advSearchRecord = ref<any>(null);\nconst openAdvSearchModal = (record: any) => {\n advSearchRecord.value = record;\n advSearchOpen.value = true;\n};\n\n/* ═══ ★ v29.23:列配置随视图一起保存(save_view 同时落两表) ═══\n 此前 doSaveColumns/saveColumns 独立按钮与连带保存已移除——外壳 handleOk 统一打包\n 主表字段 + columns + deleted_data_indexes 提交 save_view(新建/编辑共用),\n 故这里把两个「取值口」expose 给外壳。 */\nconst buildColumnsPayload = () =>\n colRows.value.map((r: any) => ({\n data_index: r.data_index,\n column_order: r.column_order,\n title: r.title,\n query_title: r.query_title,\n checked: r.checked,\n ellipsis: r.ellipsis,\n resizable: r.resizable,\n summary: r.summary,\n is_sortable: !!r.is_sortable, // ★ v29.16:sorter(JSON) → is_sortable(Bool)\n is_default_sort: !!r.is_default_sort, // ★ v29.18:默认排序三字段\n default_sort_type: r.is_default_sort ? r.default_sort_type || 'asc' : null,\n default_sort_order: r.is_default_sort ? r.default_sort_order ?? null : null,\n cell_component: r.cell_component,\n is_base_query: r.is_base_query,\n is_default_query: r.is_default_query,\n is_group_query: r.is_group_query,\n content_map: r.content_map ?? null, // ★ v29.16:转换/颜色 模态框产出\n color_map: r.color_map ?? null,\n advanced_search_component: r.advanced_search_component ?? null, // ★ v29.16:组件配置对象\n // ★ v29.23.1:类型信息随保存落库——后端 _upsert_columns 白名单已放行;\n // 漏传会导致编辑态探测出的类型保存即丢(保存后再次编辑类型恒空)\n class_name: r.class_name || '',\n sql_type: r.sql_type || '',\n // ★ v29.24:主键标记。★ v29.40:可多选 —— 逐行原样照传,后端不再做「最多一列」收敛\n is_primary_key: !!r.is_primary_key,\n }));\n\ndefineExpose({\n /** 提交用:列配置载荷(外壳 handleOk 取) */\n buildColumnsPayload,\n /** 提交用:本次要删除的 data_index(外壳 handleOk 取) */\n getDeletedIndexes: () => [...deletedDataIndexes.value],\n /** 保存成功后:清空待删除列表(原先由外壳直接 `deletedDataIndexes.value = []` 完成) */\n resetDeletedIndexes: () => {\n deletedDataIndexes.value = [];\n },\n});\n\nonMounted(() => {\n /* 编辑态预取列配置(原先在外壳 onMounted 的 edit 分支里调;本组件的 mounted 早于外壳,\n 但 `formType` / `record.id` 都由外壳在 setup 顶层就绪,读到的值一定是对的) */\n if (props.formType === 'edit') loadColumns();\n});\n</script>\n\n<style scoped lang=\"less\">\n.meta-tip {\n margin-top: 4px;\n font-size: 12px;\n color: rgba(0, 0, 0, 0.45);\n line-height: 1.5;\n}\n.col-toolbar {\n display: flex;\n gap: 8px;\n align-items: center;\n margin-bottom: 8px;\n}\n/* ★ v29.34:s-table 的高度来源容器——flex:1 撑满工具条之外的空间;min-height:0 让\n 它能在内容更高时被压缩(否则 flex 子项默认 min-height:auto,会顶破父级定高)。 */\n.col-table-wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n/* ★ v29.18:默认排序单元格——开关 + 升降序 + 顺序 横排 */\n.default-sort-cell {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"父级路由\"\n name=\"parent_menu_id\"\n extra=\"选择父级菜单;留空表示顶级菜单。保存时会直接写到该视图对应的菜单行\"\n >\n <!-- ★ v29.45(用户定案):父级从「state 字符串」改为 **Menu.id**。\n 旧版写 `parent_menu_flag`(存父菜单的 state 原值),需要在保存时把 state 解析成 id;\n 现在 `save_view` 直接写 `Menu.parent_id`,不再需要转换,\n 取值口径与 menuConfig.vue 的父级菜单选择器也统一成 id。 -->\n <RemoteTreeSelectPro\n v-model:value=\"model.parent_menu_id\"\n :form-model=\"model\"\n placeholder=\"请选择父级菜单\"\n value-key=\"id\"\n label-key=\"name\"\n :field=\"{\n placeholder_i18n: '请选择父级菜单',\n name: 'parent_menu_id',\n allowClear: true,\n valueKey: 'id',\n labelKey: 'name',\n placeholder: '请选择父级菜单',\n postUrl: '/permission/query_menu_list__tree',\n span: 12,\n }\"\n :allow-clear=\"true\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"隶属项目\"\n name=\"menu_project\"\n extra=\"该视图菜单归属的模块/项目;留空 = 通用(所有项目可见)。同样同步到菜单行\"\n >\n <!-- ★ v29.13:照 menuConfig 的 menu_project,postUrl 取语言项目表(值域 = 各宿主项目 flag) -->\n <RemoteSelectPro\n v-model:value=\"model.menu_project\"\n :form-model=\"model\"\n value-key=\"project\"\n label-key=\"name\"\n placeholder=\"请选择模块\"\n :field=\"menuProjectField\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <div class=\"step-menu-tip\">\n 本步两项在保存时<b>直接写入该视图对应的菜单行</b>(菜单不存在则自动新建);\n 菜单的<b>可见角色</b>请在「菜单管理」里配该菜单的角色授权 —— SQL 视图自身已不再保存角色信息。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport RemoteTreeSelectPro from 'liyu-pc-base/components/FormRender/RemoteTreeSelectPro/RemoteTreeSelectPro.vue';\nimport RemoteSelectPro from 'liyu-pc-base/components/FormRender/RemoteSelectPro/RemoteSelectPro.vue';\n\n/* ★ v29.45(用户定案):原「菜单与权限」步(Step 2)挪到**最后一步**并改名「菜单」,\n 同时裁掉两项:\n · 菜单索引(sort) → 改由后端在**新建菜单时**取 `max(sort) + 1`,视图侧不再配置\n · 可见角色(roles) → 改由「菜单的角色授权」承载,`DbView` 不再有角色概念\n 只留「父级路由」与「隶属项目」,两者由 `save_view` 同步到该视图对应的菜单行。\n\n 拆分约定同其它步骤:`model` 由外壳持有、按对象引用共享,这里用 computed 取用后直接改属性。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\n/* ★ v29.13:menu_project 的 RemoteSelectPro 字段配置(照 menuConfig 同款;\n Field 接口必填属性多、模板内联对象字面量会被 TS 判缺属性,这里用 any 放宽) */\nconst menuProjectField: any = {\n name: 'menu_project',\n show_search: true,\n mode: 'default',\n postUrl: '/i18n/query_language_project_list',\n};\n</script>\n\n<style scoped lang=\"less\">\n/* 说明文案:与各 a-form-item 的 extra 同色(次要文字),整行铺开 */\n.step-menu-tip {\n color: rgba(0, 0, 0, 0.45);\n font-size: 12px;\n line-height: 20px;\n}\n</style>\n","<template>\n <a-row :gutter=\"16\">\n <a-col :span=\"12\">\n <a-form-item\n label=\"父级路由\"\n name=\"parent_menu_id\"\n extra=\"选择父级菜单;留空表示顶级菜单。保存时会直接写到该视图对应的菜单行\"\n >\n <!-- ★ v29.45(用户定案):父级从「state 字符串」改为 **Menu.id**。\n 旧版写 `parent_menu_flag`(存父菜单的 state 原值),需要在保存时把 state 解析成 id;\n 现在 `save_view` 直接写 `Menu.parent_id`,不再需要转换,\n 取值口径与 menuConfig.vue 的父级菜单选择器也统一成 id。 -->\n <RemoteTreeSelectPro\n v-model:value=\"model.parent_menu_id\"\n :form-model=\"model\"\n placeholder=\"请选择父级菜单\"\n value-key=\"id\"\n label-key=\"name\"\n :field=\"{\n placeholder_i18n: '请选择父级菜单',\n name: 'parent_menu_id',\n allowClear: true,\n valueKey: 'id',\n labelKey: 'name',\n placeholder: '请选择父级菜单',\n postUrl: '/permission/query_menu_list__tree',\n span: 12,\n }\"\n :allow-clear=\"true\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"12\">\n <a-form-item\n label=\"隶属项目\"\n name=\"menu_project\"\n extra=\"该视图菜单归属的模块/项目;留空 = 通用(所有项目可见)。同样同步到菜单行\"\n >\n <!-- ★ v29.13:照 menuConfig 的 menu_project,postUrl 取语言项目表(值域 = 各宿主项目 flag) -->\n <RemoteSelectPro\n v-model:value=\"model.menu_project\"\n :form-model=\"model\"\n value-key=\"project\"\n label-key=\"name\"\n placeholder=\"请选择模块\"\n :field=\"menuProjectField\"\n />\n </a-form-item>\n </a-col>\n <a-col :span=\"24\">\n <div class=\"step-menu-tip\">\n 本步两项在保存时<b>直接写入该视图对应的菜单行</b>(菜单不存在则自动新建);\n 菜单的<b>可见角色</b>请在「菜单管理」里配该菜单的角色授权 —— SQL 视图自身已不再保存角色信息。\n </div>\n </a-col>\n </a-row>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\nimport RemoteTreeSelectPro from 'liyu-pc-base/components/FormRender/RemoteTreeSelectPro/RemoteTreeSelectPro.vue';\nimport RemoteSelectPro from 'liyu-pc-base/components/FormRender/RemoteSelectPro/RemoteSelectPro.vue';\n\n/* ★ v29.45(用户定案):原「菜单与权限」步(Step 2)挪到**最后一步**并改名「菜单」,\n 同时裁掉两项:\n · 菜单索引(sort) → 改由后端在**新建菜单时**取 `max(sort) + 1`,视图侧不再配置\n · 可见角色(roles) → 改由「菜单的角色授权」承载,`DbView` 不再有角色概念\n 只留「父级路由」与「隶属项目」,两者由 `save_view` 同步到该视图对应的菜单行。\n\n 拆分约定同其它步骤:`model` 由外壳持有、按对象引用共享,这里用 computed 取用后直接改属性。 */\n\ninterface Props {\n model: any;\n}\nconst props = defineProps<Props>();\n\nconst model = computed(() => props.model);\n\n/* ★ v29.13:menu_project 的 RemoteSelectPro 字段配置(照 menuConfig 同款;\n Field 接口必填属性多、模板内联对象字面量会被 TS 判缺属性,这里用 any 放宽) */\nconst menuProjectField: any = {\n name: 'menu_project',\n show_search: true,\n mode: 'default',\n postUrl: '/i18n/query_language_project_list',\n};\n</script>\n\n<style scoped lang=\"less\">\n/* 说明文案:与各 a-form-item 的 extra 同色(次要文字),整行铺开 */\n.step-menu-tip {\n color: rgba(0, 0, 0, 0.45);\n font-size: 12px;\n line-height: 20px;\n}\n</style>\n","/* ★ v29.35(用户定案):`dbViewConfig.vue` 按步骤拆分时,把 `view_meta` 的两端读写\n 提成**纯函数**——它横跨「展示配置」步(表单里的 5 个键)与外壳(详情加载 / 提交),\n 放在任一侧都会造成反向依赖,提成不持有状态的纯函数最干净。\n\n ★ v29.18:order_json(迁到列配置默认排序)/ max_page_size / pivot_table_max_records(机制废除)\n 从 META_KEYS 移除——编辑态加载时存量值落入 preserved,原样保留,不再回写表单。 */\n\nconst META_KEYS = [\n 'default_view_type',\n 'enable_view_types',\n 'is_pagination',\n 'pageSize',\n 'page_size_options',\n];\n\n/**\n * 详情加载:把 view_meta 拆成「表单消费的 5 个键」与「原样保留的其余键」。\n * 表单键**就地写进 model**(对象引用共享,外壳与各步骤看到的是同一个对象),\n * 其余键返回给调用方持有,提交时原样合并回去(btn_list、advanced_base_conditions 等不会丢)。\n */\nexport const applyViewMeta = (model: any, meta: Record<string, any>): Record<string, any> => {\n const preserved: Record<string, any> = {};\n Object.keys(meta || {}).forEach((k) => {\n if (!META_KEYS.includes(k)) preserved[k] = meta[k];\n });\n // ★ v29.13:org_filter 已废除(不再消费),存量键落入 preserved 原样保留\n model.default_view_type = meta.default_view_type ?? undefined;\n model.enable_view_types = Array.isArray(meta.enable_view_types) ? meta.enable_view_types : [];\n model.is_pagination = meta.is_pagination ?? true; // ★ v29.18:默认开启(与页面实际行为一致)\n model.pageSize = meta.pageSize ?? undefined;\n model.page_size_options = Array.isArray(meta.page_size_options) ? meta.page_size_options : [];\n return preserved;\n};\n\n/**\n * 提交:结构化值非空才写回,避免把「未配置」变成 null 脏数据。\n * ★ v29.13:org_filter 不再写回(引擎无消费点;存量键留在 preserved 里原样保留)。\n */\nexport const buildViewMeta = (model: any, preservedMeta: Record<string, any>) => {\n const meta: any = { ...preservedMeta };\n if (model.default_view_type) meta.default_view_type = model.default_view_type;\n else delete meta.default_view_type;\n if (model.enable_view_types && model.enable_view_types.length)\n meta.enable_view_types = model.enable_view_types;\n else delete meta.enable_view_types;\n /* ★ v29.18:default_view_type 必须在 enable_view_types 范围内(空 = 不限),\n 不在范围内的配置视为无效,落库前剔除 */\n if (\n Array.isArray(model.enable_view_types) &&\n model.enable_view_types.length &&\n meta.default_view_type &&\n !model.enable_view_types.includes(meta.default_view_type)\n ) {\n delete meta.default_view_type;\n }\n if (typeof model.is_pagination === 'boolean') meta.is_pagination = model.is_pagination;\n else meta.is_pagination = true; // ★ v29.18:默认开启\n if (model.pageSize != null) meta.pageSize = model.pageSize;\n else delete meta.pageSize;\n if (model.page_size_options && model.page_size_options.length)\n meta.page_size_options = model.page_size_options;\n else delete meta.page_size_options;\n return meta;\n};\n","<template>\n <a-modal\n :title=\"title\"\n width=\"1020px\"\n :open=\"open\"\n :bodyStyle=\"{ maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', padding: '0 24px' }\"\n :confirm-loading=\"loading\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <a-steps\n :current=\"currentStep\"\n size=\"small\"\n class=\"config-steps\"\n @change=\"onStepChange\"\n >\n <a-step title=\"基础配置\" />\n <a-step title=\"展示配置\" />\n <a-step title=\"列配置\" />\n <a-step title=\"菜单\" />\n </a-steps>\n\n <a-form\n ref=\"formRef\"\n :model=\"model\"\n layout=\"vertical\"\n >\n <!-- ═══════════ Step 1 基础配置 ═══════════ -->\n <div\n v-show=\"currentStep === 0\"\n class=\"step-pane\"\n >\n <StepBasicConfig\n v-model:sql-error=\"sqlError\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n />\n </div>\n\n <!-- ═══════════ Step 2 展示配置(view_meta 白名单透传给前端)\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.45:原第 2 步「菜单与权限」挪到末位 → 本步上移到第 2 步 ═══════════ -->\n <!-- ★ v29.13:org_filter 配置已废除(用户定案:组织过滤只靠 SQL 里手写 {{org_id}},\n 引擎不做任何特殊处理),此处不再提供配置项 -->\n <div\n v-show=\"currentStep === 1\"\n class=\"step-pane\"\n >\n <StepDisplayConfig :model=\"model\" />\n </div>\n\n <!-- ═══════════ Step 3 列配置(§3.1.4 字段配置子表:列探测刷新 → 人工覆盖)\n ★ v29.34:本步额外挂 `step-pane--col`,把「工具条 + 表格」改成 flex 纵向布局,\n 让 s-table 吃掉剩余高度(详见样式区注释),父级不再出纵向滚动条。\n ★ v29.35:这层 div 是**高度链路的第 ① 环**,必须留着——StepColumnConfig 是\n 多根(fragment)组件,它内部 `.col-toolbar` / `.col-table-wrapper` 会原样插进这里,\n 成为本 div(flex column)的直接子元素;多包一层 DOM 就会让 wrapper 的 `flex: 1`\n 失去参照。显隐也由本 div 的 v-show 负责(fragment 组件不继承 v-show / class)。\n ★ v29.45:本步由第 4 步上移到第 3 步(菜单步挪到末位)—— 显隐由 `colActive`\n 统一表达,故这次换位只需改 `colActive` 的判据,不必动这里。 ═══════════ -->\n <div\n v-show=\"colActive\"\n class=\"step-pane step-pane--col\"\n >\n <StepColumnConfig\n ref=\"colStepRef\"\n :active=\"colActive\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n @probe-start=\"onProbeStart\"\n />\n </div>\n\n <!-- ═══════════ Step 4 菜单(★ v29.45:原第 2 步「菜单与权限」挪到**末位**并改名「菜单」)\n 裁掉两项:菜单索引(sort,改由后端在新建菜单时取 max+1)、可见角色(roles,改由\n 「菜单的角色授权」承载)。只留父级路由 + 隶属项目,保存时由 save_view 同步到\n 该视图对应的菜单行(不存在则新建)。 ═══════════ -->\n <div\n v-show=\"currentStep === 3\"\n class=\"step-pane\"\n >\n <StepMenu :model=\"model\" />\n </div>\n </a-form>\n\n <template #footer>\n <a-button\n v-if=\"currentStep > 0\"\n @click=\"currentStep -= 1\"\n >\n 上一步\n </a-button>\n <a-button\n v-if=\"currentStep < 3\"\n type=\"primary\"\n ghost\n @click=\"goNext\"\n >\n 下一步\n </a-button>\n <a-button @click=\"handleCancel\">取消</a-button>\n <a-button\n type=\"primary\"\n :loading=\"loading\"\n :disabled=\"form_type !== 'edit' && currentStep < 3\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </template>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref } from 'vue';\nimport { event_obj } from 'liyu-pc-base/init-plugin';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport StepBasicConfig from 'liyu-pc-base/components/dbView/configSteps/StepBasicConfig.vue';\nimport StepDisplayConfig from 'liyu-pc-base/components/dbView/configSteps/StepDisplayConfig.vue';\nimport StepColumnConfig from 'liyu-pc-base/components/dbView/configSteps/StepColumnConfig.vue';\nimport StepMenu from 'liyu-pc-base/components/dbView/configSteps/StepMenu.vue';\nimport { STEP_FIELDS } from './configSteps/constants';\nimport { extractData } from './configSteps/helpers';\nimport { applyViewMeta, buildViewMeta } from './configSteps/viewMeta';\n\n/* ★ v29.7:db_view 的「视图配置」新增/编辑弹窗组件。\n ★ v29.8:a-tabs 改 a-steps(用户定案),并新增「列配置」步骤(§3.1.4 字段配置子表)。\n ★ v29.23(用户定案批量调整):① 「菜单与权限」独立成步(sort / parent_menu_flag /\n menu_project / roles 四项从基础配置移出)——步骤为 基础配置/菜单与权限/展示配置/列配置;\n ② 「探测列」按钮移除,列探测统一收敛到列配置步骤的「列探测刷新」(probe_columns,\n 基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并当前配置不清空;\n ③ 新建流程进入列配置步骤自动探测推断列;\n ④ 预览恒空修复(响应键 data→rows,见 runPreview 注释);\n ⑤ **保存统一走 save_view**:主表字段 + columns + deleted_data_indexes 一次提交,\n 事务内两表同落(新建/编辑共用),不再 create_db_view/update_db_view + save_columns\n 两段式;「刷新」「保存列配置」按钮随之移除。\n view_meta 不再是裸 JSON 文本框,按「实际可配置的键」拆步骤——\n ★ v29.13:父菜单 RemoteTreeSelectPro(valueKey=state)/ menu_project / order→sort /\n org_filter 废除;★ v29.18:order_json 迁列配置默认排序、max_page_size /\n pivot_table_max_records 废除。\n 未在表单展开的其余 view_meta 键(btn_list、advanced_base_conditions 等)\n 编辑时原样保留(preservedMeta 合并回提交值),不会丢失。\n\n ★ v29.35(用户定案):**按步骤拆组件**——本文件只留「外壳」:弹窗 + 步骤条 + a-form 容器\n + footer + 共享 model + 详情加载 / 提交编排。四个步骤落到 `dbView/configSteps/`:\n · StepBasicConfig.vue 基础配置(名称 / flag / 数据源 / SQL + 预览)\n · StepDisplayConfig.vue 展示配置(view_meta 白名单 5 键)\n · StepColumnConfig.vue 列配置(s-table 自适应高度 + 探测 + 两个行内模态框)\n · StepMenu.vue 菜单(父级路由 parent_menu_id / 隶属项目 menu_project)\n 外加三个纯逻辑文件:`constants.ts`(数据字典)、`helpers.ts`(extractData/toList)、\n `viewMeta.ts`(view_meta 拆包与合并,纯函数)。\n\n ★ v29.45(用户定案,菜单归属重构):步骤**顺序调整 + 裁剪** ——\n 0 基础配置 / 1 展示配置 / 2 列配置 / **3 菜单**(原第 2 步「菜单与权限」挪到末位并改名);\n 该步裁掉「菜单索引(sort)」与「可见角色(roles)」,只留「父级路由 + 隶属项目」;\n `DbView` 的 `parent_menu_flag` / `menu_project` / `sort` / `roles` 四个字段**已删除**,\n 菜单信息改由 `save_view` 同步到主服务的 `liyu_permission_menu`(详见\n `docs/design/07-menu-integration.md`)。连带把「父级路由」的取值从 **Menu.state**\n 改成 **Menu.id**(`model.parent_menu_id`,直接写 `Menu.parent_id`)。\n\n 拆分后的三条约定(改这些地方请一起看):\n ① **model 仍是唯一数据源**,由外壳持有、按对象引用传给各步骤(模板里照旧 `model.xxx`);\n 外壳整体重建对象(initForEdit 的 `model.value = {...}`)时,各步骤的 `computed(() => props.model)`\n 会跟着换到新对象(持有裸引用会失联)。\n ② **校验仍归外壳**:`rules` 就近定义在 StepBasicConfig(那 4 个字段只属于它),\n 但 `a-form` / `formRef.validate*` / `STEP_FIELDS` 都在外壳 —— a-form-item 在外壳的\n `<a-form>` 组件树内,provide/inject 照常生效。\n ③ **跨步骤的两个口子**:`sqlError`(第 1 步渲染、第 3 步探测前清)仍由外壳持有,\n 第 3 步用 `probeStart` 事件通知;列配置的提交载荷(columns / deleted_data_indexes)\n 由 StepColumnConfig 用 `defineExpose` 提供,外壳通过 `colStepRef` 取。 */\n\ninterface Props {\n title: string;\n record: any;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['cancel', 'ok']);\n\nconst formRef = ref<any>(null);\nconst loading = ref(false);\nconst open = ref(true);\n/* ★ v29.35:改为 setup 顶层初始化(原先在 onMounted 里才赋值)——四个步骤组件都会在自己的\n onMounted 里读 formType 决定加载行为,而子组件的 mounted 早于父组件的 mounted,\n 顶层初始化才能保证它们读到的一定是正确值。差异仅「挂载前就是正确的值」,无其他影响;\n onMounted 里那句同步赋值保留,兼容 record 被替换的情形。 */\nconst form_type = ref(props.record?.form_type || 'create');\nconst currentStep = ref(0);\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* 列配置步的进入标志:既控制那层 div 的显隐,也作为 `active` 传给 StepColumnConfig\n (它要在本步真正显示出来之后才挂 s-table 并自动探测,见该组件 watch(active) 的说明)。\n ★ v29.45:本步由第 4 步上移到**第 3 步**(菜单步挪到末位)→ 判据 3 改 2。\n ⚠️ 模板里那层 `v-show=\"colActive\"` 与本行是**同一个来源**,换位只改这一处;\n 但 `constants.ts` 的 `STEP_FIELDS` 是按步骤**位置**排的数组,注释必须同步。 */\nconst colActive = computed(() => currentStep.value === 2);\n\n/* ★ v29.35:第 1 步的预览错误由外壳持有——第 3 步「列探测刷新」开始前也要清它\n (原先两处是同一作用域的自由变量,拆开后第 3 步改为 emit('probeStart') 通知) */\nconst sqlError = ref('');\nconst onProbeStart = () => {\n sqlError.value = '';\n};\n\n/* ★ v29.35:第 3 步的提交取值口(columns / deleted_data_indexes 都下沉到子组件了) */\nconst colStepRef = ref<any>(null);\n\n/* ★ v29.35:这份 model 是四个步骤**共享**的表单载体(对象引用传给各步骤,它们直接改属性)。\n 字段按步骤分组:基础配置(name/flag/data_source_id/sql)、展示配置(default_view_type/\n enable_view_types/is_pagination/pageSize/page_size_options)、菜单(parent_menu_id/menu_project)。\n ★ v29.45:删 `sort` / `role_ids`(字段已从 DbView 删除);加 `parent_menu_id`\n (父级菜单的 **Menu.id**,替换原 `parent_menu_flag` 的 state 语义)。 */\nconst model = ref<any>({\n name: undefined,\n flag: undefined,\n data_source_id: undefined,\n parent_menu_id: undefined,\n menu_project: undefined,\n sql: undefined,\n // view_meta 结构化键(★ v29.18:order_json/max_page_size/pivot_table_max_records 废除——\n // order_json 迁到列配置 DbViewColumn 默认排序三字段,后两个机制删除)\n default_view_type: undefined,\n enable_view_types: [],\n is_pagination: true,\n pageSize: undefined,\n page_size_options: [],\n});\n\n/* ═══ 步骤导航:前进先校验当前步骤,后退/原地自由 ═══\n ★ v29.14(用户定案):**编辑态**允许点步骤条直达任意步骤——前提是当前步骤\n 校验通过(不通过则停留,错误由表单项显示);最终保存时 handleOk 仍会全量校验。\n 新建态维持「只许回退」。\n ★ v29.18:步骤收敛为 3 步;★ v29.23:菜单四项拆出 → 4 步\n (基础配置 / 菜单与权限 / 展示配置 / 列配置)。\n ★ v29.45(用户定案):「菜单与权限」**挪到末位**并改名「菜单」→ 现为\n (基础配置 / 展示配置 / 列配置 / 菜单),仍 4 步。\n ⚠️ footer 的 `currentStep < 3` 与「新建态须走到最后一步」的判据**都不用改**(仍是 4 步、末位仍是 3)。\n ★ v29.35:进入列配置步骤时「若无列则自动探测」已下沉到 StepColumnConfig 的 watch(active),\n 这里不再关心列数据。 */\nconst stepFields = STEP_FIELDS;\nconst goNext = async () => {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤,错误提示已由表单项显示\n }\n }\n if (currentStep.value < 3) {\n currentStep.value += 1;\n }\n};\nconst onStepChange = async (target: number) => {\n if (target <= currentStep.value) {\n currentStep.value = target; // 后退/原地自由\n return;\n }\n if (form_type.value !== 'edit') return; // 新建态:不可跳步前进\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤\n }\n }\n currentStep.value = target;\n};\n\n/* ═══ view_meta:保留键由外壳持有 ═══\n ★ v29.35:拆步骤后,view_meta 的「拆包/合并」提成纯函数放 `./configSteps/viewMeta.ts`\n (它横跨外壳与「展示配置」步,放在任一侧都会造成反向依赖)。\n 这里只留外壳需要的那一半:表单不消费的键。 */\nlet preservedMeta: Record<string, any> = {};\n\n/* ═══ 详情 / 提交 ═══ */\nconst loadDetail = async (id: any) => {\n const res = await getApiData(\n { db_view_id: id, org_id: current_org?.id },\n `${props.record.model_app}/get_db_view`,\n );\n return extractData(res) || {};\n};\n\nconst buildPayload = () => {\n const m = model.value;\n return {\n name: m.name,\n flag: m.flag,\n data_source_id: m.data_source_id,\n // ★ v29.45(用户定案):`parent_menu_flag`(旧字段已删) → `parent_menu_id`(Menu.id)。\n // null = 顶级菜单。后端据此直接写 `Menu.parent_id`,不再做 state→id 解析。\n parent_menu_id: m.parent_menu_id || null,\n menu_project: m.menu_project || null, // ★ v29.13:菜单隶属项目(区分模块)→ 写到菜单行\n sql: m.sql,\n view_meta: buildViewMeta(m, preservedMeta),\n };\n};\n\nconst handleOk = async () => {\n try {\n /* ★ v29.17:编辑态「确定」任意步骤可点——只校验当前步骤的表单项(其余步骤的\n 错误不可见,全量校验会让用户困惑);新建态仍须走到最后一步,维持全量校验兜底。 */\n if (form_type.value === 'edit') {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) await formRef.value?.validateFields(fields);\n } else {\n await formRef.value.validate();\n }\n /* ★ v29.23(用户定案):视图主表与列配置【同时保存】——统一走 save_view\n (事务内两表同落,新建/编辑共用),不再分 create_db_view + save_columns 两次请求。\n ★ v29.35:columns / deleted_data_indexes 改由 StepColumnConfig 提供(ref 调 expose 的方法)。 */\n const payload: any = {\n ...buildPayload(),\n columns: colStepRef.value?.buildColumnsPayload?.() ?? [],\n deleted_data_indexes: colStepRef.value?.getDeletedIndexes?.() ?? [],\n };\n if (form_type.value === 'edit') {\n payload.db_view_id = props.record.id;\n }\n loading.value = true;\n await getApiData(\n { ...payload, org_id: current_org?.id },\n `${props.record.model_app}/save_view`,\n );\n message.success('操作成功');\n colStepRef.value?.resetDeletedIndexes?.();\n event_obj()?.emit('refresh');\n emit('ok');\n } catch (error) {\n // 校验失败或接口异常:保持弹窗打开\n } finally {\n loading.value = false;\n }\n};\nconst handleCancel = () => emit('cancel');\n\n/* ★ v29.45(用户定案):菜单两项**不再来自 DbView**(`parent_menu_flag` / `menu_project`\n 两个字段已从模型删除),改从「该视图对应的菜单行」读 —— `/db_view/get_db_view_menu`。\n ⚠️ 必须在 `loadDetail()` 与下面那次整体赋值**之后**再回填:`initForEdit` 会用\n `model.value = {...}` 重建对象,顺序反了这两项会被覆盖掉。\n ⚠️ 走 `skip_error: true` + try/catch:菜单回显失败**不该阻塞编辑**(留空即可,\n 保存时后端会按新值写/建菜单)。 */\nconst loadMenuInfo = async (flag: any) => {\n if (!flag) return;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, flag },\n `${props.record.model_app}/get_db_view_menu`,\n false,\n { skip_error: true },\n );\n const data = extractData(res) || {};\n if (!data.exists) return; // 视图还没保存过菜单 / 菜单已被删除 → 两项留空\n model.value.parent_menu_id = data.parent_id ?? undefined;\n model.value.menu_project = data.menu_project || undefined;\n } catch (e) {\n // 保持留空\n }\n};\n\nconst initForEdit = async () => {\n const id = props.record?.id;\n const record = (id != null ? await loadDetail(id) : props.record) || {};\n model.value = {\n ...model.value,\n name: record.name || undefined,\n flag: record.flag || undefined,\n data_source_id: record.data_source_id ?? undefined,\n sql: record.sql || undefined,\n };\n // ★ v29.35:拆包结果写回共享 model,其余键留在 preservedMeta(提交时原样合并)\n preservedMeta = applyViewMeta(model.value, record.view_meta || {});\n // ★ v29.45:菜单两项(父级 / 隶属项目)最后回填(必须在上面两次赋值之后)\n await loadMenuInfo(model.value.flag);\n};\n\nonMounted(async () => {\n if (props.record?.form_type) form_type.value = props.record.form_type;\n /* ★ v29.35:数据源下拉 / 角色列表 / 列配置预取 / 新建自动生成 flag 都已随各自步骤下沉,\n 各自的 mounted 里自行加载;外壳只剩「编辑态拉详情」这一步。 */\n if (form_type.value === 'edit') {\n await initForEdit();\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.config-steps {\n padding: 12px 0 4px;\n}\n/* ★ v29.17.2:步骤内容区固定高度(每步内部滚动),切换步骤时弹窗总高不跳变 */\n.step-pane {\n height: calc(80vh - 150px);\n min-height: 360px;\n overflow-y: auto;\n overflow-x: hidden; /* ★ gutter 负 margin / 表格 min-width 等横向溢出不外溢 */\n padding-right: 4px;\n}\n/* ★ v29.34:列配置步专用——工具条固定在上、表格吃满剩余高度,**整个面板只留 s-table 一根滚动条**。\n ① 必须把基础 `.step-pane` 的 `overflow-y: auto` 收掉:否则出现的是「外层滚动条」,\n 表头会跟着内容滚走,`flex: 1` 也就没有「剩余高度」可解析,自动高度链路从这一环断。\n 纵向交给 s-table 表体、横向交给 `scroll.x`。\n ② 选择器刻意写成**复合** `.step-pane.step-pane--col`(0,2,0)而不是 `.step-pane--col`(0,1,0):\n 后者与 `.step-pane` 特异度打平,`overflow` 能否压住 `overflow-y: auto` 就**只取决于两个样式块的\n 先后顺序**——将来有人调整顺序就会静默退回 auto(多出一根滚动条、表格高度失去意义)。\n 与抽屉那次 `.csc-drawer.ant-drawer .ant-drawer-body` 是同一类坑,写成复合/三段式即与顺序无关。\n ★ v29.35:本 div 的直接子元素就是 StepColumnConfig 内部的 `.col-toolbar` 与\n `.col-table-wrapper`(fragment 组件不产生额外 DOM),故 `flex: 1` 的参照仍是它。 */\n.step-pane.step-pane--col {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n</style>\n","<template>\n <a-modal\n :title=\"title\"\n width=\"1020px\"\n :open=\"open\"\n :bodyStyle=\"{ maxHeight: '80vh', overflowY: 'auto', overflowX: 'hidden', padding: '0 24px' }\"\n :confirm-loading=\"loading\"\n cancel-text=\"取消\"\n ok-text=\"确定\"\n centered\n destroy-on-close\n @cancel=\"handleCancel\"\n @ok=\"handleOk\"\n >\n <a-steps\n :current=\"currentStep\"\n size=\"small\"\n class=\"config-steps\"\n @change=\"onStepChange\"\n >\n <a-step title=\"基础配置\" />\n <a-step title=\"展示配置\" />\n <a-step title=\"列配置\" />\n <a-step title=\"菜单\" />\n </a-steps>\n\n <a-form\n ref=\"formRef\"\n :model=\"model\"\n layout=\"vertical\"\n >\n <!-- ═══════════ Step 1 基础配置 ═══════════ -->\n <div\n v-show=\"currentStep === 0\"\n class=\"step-pane\"\n >\n <StepBasicConfig\n v-model:sql-error=\"sqlError\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n />\n </div>\n\n <!-- ═══════════ Step 2 展示配置(view_meta 白名单透传给前端)\n ★ v29.18:order_json 迁到列配置(DbViewColumn 默认排序三字段)、\n max_page_size / pivot_table_max_records 机制废除,原「查询行为」步骤移除。\n ★ v29.45:原第 2 步「菜单与权限」挪到末位 → 本步上移到第 2 步 ═══════════ -->\n <!-- ★ v29.13:org_filter 配置已废除(用户定案:组织过滤只靠 SQL 里手写 {{org_id}},\n 引擎不做任何特殊处理),此处不再提供配置项 -->\n <div\n v-show=\"currentStep === 1\"\n class=\"step-pane\"\n >\n <StepDisplayConfig :model=\"model\" />\n </div>\n\n <!-- ═══════════ Step 3 列配置(§3.1.4 字段配置子表:列探测刷新 → 人工覆盖)\n ★ v29.34:本步额外挂 `step-pane--col`,把「工具条 + 表格」改成 flex 纵向布局,\n 让 s-table 吃掉剩余高度(详见样式区注释),父级不再出纵向滚动条。\n ★ v29.35:这层 div 是**高度链路的第 ① 环**,必须留着——StepColumnConfig 是\n 多根(fragment)组件,它内部 `.col-toolbar` / `.col-table-wrapper` 会原样插进这里,\n 成为本 div(flex column)的直接子元素;多包一层 DOM 就会让 wrapper 的 `flex: 1`\n 失去参照。显隐也由本 div 的 v-show 负责(fragment 组件不继承 v-show / class)。\n ★ v29.45:本步由第 4 步上移到第 3 步(菜单步挪到末位)—— 显隐由 `colActive`\n 统一表达,故这次换位只需改 `colActive` 的判据,不必动这里。 ═══════════ -->\n <div\n v-show=\"colActive\"\n class=\"step-pane step-pane--col\"\n >\n <StepColumnConfig\n ref=\"colStepRef\"\n :active=\"colActive\"\n :model=\"model\"\n :record=\"record\"\n :form-type=\"form_type\"\n @probe-start=\"onProbeStart\"\n />\n </div>\n\n <!-- ═══════════ Step 4 菜单(★ v29.45:原第 2 步「菜单与权限」挪到**末位**并改名「菜单」)\n 裁掉两项:菜单索引(sort,改由后端在新建菜单时取 max+1)、可见角色(roles,改由\n 「菜单的角色授权」承载)。只留父级路由 + 隶属项目,保存时由 save_view 同步到\n 该视图对应的菜单行(不存在则新建)。 ═══════════ -->\n <div\n v-show=\"currentStep === 3\"\n class=\"step-pane\"\n >\n <StepMenu :model=\"model\" />\n </div>\n </a-form>\n\n <template #footer>\n <a-button\n v-if=\"currentStep > 0\"\n @click=\"currentStep -= 1\"\n >\n 上一步\n </a-button>\n <a-button\n v-if=\"currentStep < 3\"\n type=\"primary\"\n ghost\n @click=\"goNext\"\n >\n 下一步\n </a-button>\n <a-button @click=\"handleCancel\">取消</a-button>\n <a-button\n type=\"primary\"\n :loading=\"loading\"\n :disabled=\"form_type !== 'edit' && currentStep < 3\"\n @click=\"handleOk\"\n >\n 确定\n </a-button>\n </template>\n </a-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { message } from 'ant-design-vue';\nimport { computed, onMounted, ref } from 'vue';\nimport { event_obj } from 'liyu-pc-base/init-plugin';\nimport { getApiData } from 'liyu-pc-base/api/api-model';\nimport { useUserStore } from 'liyu-pc-base/init-plugin';\nimport StepBasicConfig from 'liyu-pc-base/components/dbView/configSteps/StepBasicConfig.vue';\nimport StepDisplayConfig from 'liyu-pc-base/components/dbView/configSteps/StepDisplayConfig.vue';\nimport StepColumnConfig from 'liyu-pc-base/components/dbView/configSteps/StepColumnConfig.vue';\nimport StepMenu from 'liyu-pc-base/components/dbView/configSteps/StepMenu.vue';\nimport { STEP_FIELDS } from './configSteps/constants';\nimport { extractData } from './configSteps/helpers';\nimport { applyViewMeta, buildViewMeta } from './configSteps/viewMeta';\n\n/* ★ v29.7:db_view 的「视图配置」新增/编辑弹窗组件。\n ★ v29.8:a-tabs 改 a-steps(用户定案),并新增「列配置」步骤(§3.1.4 字段配置子表)。\n ★ v29.23(用户定案批量调整):① 「菜单与权限」独立成步(sort / parent_menu_flag /\n menu_project / roles 四项从基础配置移出)——步骤为 基础配置/菜单与权限/展示配置/列配置;\n ② 「探测列」按钮移除,列探测统一收敛到列配置步骤的「列探测刷新」(probe_columns,\n 基于第 1 步表单的 SQL+数据源实时探测,不读写库),按 data_index 合并当前配置不清空;\n ③ 新建流程进入列配置步骤自动探测推断列;\n ④ 预览恒空修复(响应键 data→rows,见 runPreview 注释);\n ⑤ **保存统一走 save_view**:主表字段 + columns + deleted_data_indexes 一次提交,\n 事务内两表同落(新建/编辑共用),不再 create_db_view/update_db_view + save_columns\n 两段式;「刷新」「保存列配置」按钮随之移除。\n view_meta 不再是裸 JSON 文本框,按「实际可配置的键」拆步骤——\n ★ v29.13:父菜单 RemoteTreeSelectPro(valueKey=state)/ menu_project / order→sort /\n org_filter 废除;★ v29.18:order_json 迁列配置默认排序、max_page_size /\n pivot_table_max_records 废除。\n 未在表单展开的其余 view_meta 键(btn_list、advanced_base_conditions 等)\n 编辑时原样保留(preservedMeta 合并回提交值),不会丢失。\n\n ★ v29.35(用户定案):**按步骤拆组件**——本文件只留「外壳」:弹窗 + 步骤条 + a-form 容器\n + footer + 共享 model + 详情加载 / 提交编排。四个步骤落到 `dbView/configSteps/`:\n · StepBasicConfig.vue 基础配置(名称 / flag / 数据源 / SQL + 预览)\n · StepDisplayConfig.vue 展示配置(view_meta 白名单 5 键)\n · StepColumnConfig.vue 列配置(s-table 自适应高度 + 探测 + 两个行内模态框)\n · StepMenu.vue 菜单(父级路由 parent_menu_id / 隶属项目 menu_project)\n 外加三个纯逻辑文件:`constants.ts`(数据字典)、`helpers.ts`(extractData/toList)、\n `viewMeta.ts`(view_meta 拆包与合并,纯函数)。\n\n ★ v29.45(用户定案,菜单归属重构):步骤**顺序调整 + 裁剪** ——\n 0 基础配置 / 1 展示配置 / 2 列配置 / **3 菜单**(原第 2 步「菜单与权限」挪到末位并改名);\n 该步裁掉「菜单索引(sort)」与「可见角色(roles)」,只留「父级路由 + 隶属项目」;\n `DbView` 的 `parent_menu_flag` / `menu_project` / `sort` / `roles` 四个字段**已删除**,\n 菜单信息改由 `save_view` 同步到主服务的 `liyu_permission_menu`(详见\n `docs/design/07-menu-integration.md`)。连带把「父级路由」的取值从 **Menu.state**\n 改成 **Menu.id**(`model.parent_menu_id`,直接写 `Menu.parent_id`)。\n\n 拆分后的三条约定(改这些地方请一起看):\n ① **model 仍是唯一数据源**,由外壳持有、按对象引用传给各步骤(模板里照旧 `model.xxx`);\n 外壳整体重建对象(initForEdit 的 `model.value = {...}`)时,各步骤的 `computed(() => props.model)`\n 会跟着换到新对象(持有裸引用会失联)。\n ② **校验仍归外壳**:`rules` 就近定义在 StepBasicConfig(那 4 个字段只属于它),\n 但 `a-form` / `formRef.validate*` / `STEP_FIELDS` 都在外壳 —— a-form-item 在外壳的\n `<a-form>` 组件树内,provide/inject 照常生效。\n ③ **跨步骤的两个口子**:`sqlError`(第 1 步渲染、第 3 步探测前清)仍由外壳持有,\n 第 3 步用 `probeStart` 事件通知;列配置的提交载荷(columns / deleted_data_indexes)\n 由 StepColumnConfig 用 `defineExpose` 提供,外壳通过 `colStepRef` 取。 */\n\ninterface Props {\n title: string;\n record: any;\n}\nconst props = defineProps<Props>();\nconst emit = defineEmits(['cancel', 'ok']);\n\nconst formRef = ref<any>(null);\nconst loading = ref(false);\nconst open = ref(true);\n/* ★ v29.35:改为 setup 顶层初始化(原先在 onMounted 里才赋值)——四个步骤组件都会在自己的\n onMounted 里读 formType 决定加载行为,而子组件的 mounted 早于父组件的 mounted,\n 顶层初始化才能保证它们读到的一定是正确值。差异仅「挂载前就是正确的值」,无其他影响;\n onMounted 里那句同步赋值保留,兼容 record 被替换的情形。 */\nconst form_type = ref(props.record?.form_type || 'create');\nconst currentStep = ref(0);\nconst userStore = useUserStore();\nconst current_org = userStore.currentOrg;\n\n/* 列配置步的进入标志:既控制那层 div 的显隐,也作为 `active` 传给 StepColumnConfig\n (它要在本步真正显示出来之后才挂 s-table 并自动探测,见该组件 watch(active) 的说明)。\n ★ v29.45:本步由第 4 步上移到**第 3 步**(菜单步挪到末位)→ 判据 3 改 2。\n ⚠️ 模板里那层 `v-show=\"colActive\"` 与本行是**同一个来源**,换位只改这一处;\n 但 `constants.ts` 的 `STEP_FIELDS` 是按步骤**位置**排的数组,注释必须同步。 */\nconst colActive = computed(() => currentStep.value === 2);\n\n/* ★ v29.35:第 1 步的预览错误由外壳持有——第 3 步「列探测刷新」开始前也要清它\n (原先两处是同一作用域的自由变量,拆开后第 3 步改为 emit('probeStart') 通知) */\nconst sqlError = ref('');\nconst onProbeStart = () => {\n sqlError.value = '';\n};\n\n/* ★ v29.35:第 3 步的提交取值口(columns / deleted_data_indexes 都下沉到子组件了) */\nconst colStepRef = ref<any>(null);\n\n/* ★ v29.35:这份 model 是四个步骤**共享**的表单载体(对象引用传给各步骤,它们直接改属性)。\n 字段按步骤分组:基础配置(name/flag/data_source_id/sql)、展示配置(default_view_type/\n enable_view_types/is_pagination/pageSize/page_size_options)、菜单(parent_menu_id/menu_project)。\n ★ v29.45:删 `sort` / `role_ids`(字段已从 DbView 删除);加 `parent_menu_id`\n (父级菜单的 **Menu.id**,替换原 `parent_menu_flag` 的 state 语义)。 */\nconst model = ref<any>({\n name: undefined,\n flag: undefined,\n data_source_id: undefined,\n parent_menu_id: undefined,\n menu_project: undefined,\n sql: undefined,\n // view_meta 结构化键(★ v29.18:order_json/max_page_size/pivot_table_max_records 废除——\n // order_json 迁到列配置 DbViewColumn 默认排序三字段,后两个机制删除)\n default_view_type: undefined,\n enable_view_types: [],\n is_pagination: true,\n pageSize: undefined,\n page_size_options: [],\n});\n\n/* ═══ 步骤导航:前进先校验当前步骤,后退/原地自由 ═══\n ★ v29.14(用户定案):**编辑态**允许点步骤条直达任意步骤——前提是当前步骤\n 校验通过(不通过则停留,错误由表单项显示);最终保存时 handleOk 仍会全量校验。\n 新建态维持「只许回退」。\n ★ v29.18:步骤收敛为 3 步;★ v29.23:菜单四项拆出 → 4 步\n (基础配置 / 菜单与权限 / 展示配置 / 列配置)。\n ★ v29.45(用户定案):「菜单与权限」**挪到末位**并改名「菜单」→ 现为\n (基础配置 / 展示配置 / 列配置 / 菜单),仍 4 步。\n ⚠️ footer 的 `currentStep < 3` 与「新建态须走到最后一步」的判据**都不用改**(仍是 4 步、末位仍是 3)。\n ★ v29.35:进入列配置步骤时「若无列则自动探测」已下沉到 StepColumnConfig 的 watch(active),\n 这里不再关心列数据。 */\nconst stepFields = STEP_FIELDS;\nconst goNext = async () => {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤,错误提示已由表单项显示\n }\n }\n if (currentStep.value < 3) {\n currentStep.value += 1;\n }\n};\nconst onStepChange = async (target: number) => {\n if (target <= currentStep.value) {\n currentStep.value = target; // 后退/原地自由\n return;\n }\n if (form_type.value !== 'edit') return; // 新建态:不可跳步前进\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) {\n try {\n await formRef.value?.validateFields(fields);\n } catch (e) {\n return; // 校验失败:停留当前步骤\n }\n }\n currentStep.value = target;\n};\n\n/* ═══ view_meta:保留键由外壳持有 ═══\n ★ v29.35:拆步骤后,view_meta 的「拆包/合并」提成纯函数放 `./configSteps/viewMeta.ts`\n (它横跨外壳与「展示配置」步,放在任一侧都会造成反向依赖)。\n 这里只留外壳需要的那一半:表单不消费的键。 */\nlet preservedMeta: Record<string, any> = {};\n\n/* ═══ 详情 / 提交 ═══ */\nconst loadDetail = async (id: any) => {\n const res = await getApiData(\n { db_view_id: id, org_id: current_org?.id },\n `${props.record.model_app}/get_db_view`,\n );\n return extractData(res) || {};\n};\n\nconst buildPayload = () => {\n const m = model.value;\n return {\n name: m.name,\n flag: m.flag,\n data_source_id: m.data_source_id,\n // ★ v29.45(用户定案):`parent_menu_flag`(旧字段已删) → `parent_menu_id`(Menu.id)。\n // null = 顶级菜单。后端据此直接写 `Menu.parent_id`,不再做 state→id 解析。\n parent_menu_id: m.parent_menu_id || null,\n menu_project: m.menu_project || null, // ★ v29.13:菜单隶属项目(区分模块)→ 写到菜单行\n sql: m.sql,\n view_meta: buildViewMeta(m, preservedMeta),\n };\n};\n\nconst handleOk = async () => {\n try {\n /* ★ v29.17:编辑态「确定」任意步骤可点——只校验当前步骤的表单项(其余步骤的\n 错误不可见,全量校验会让用户困惑);新建态仍须走到最后一步,维持全量校验兜底。 */\n if (form_type.value === 'edit') {\n const fields = stepFields[currentStep.value] || [];\n if (fields.length) await formRef.value?.validateFields(fields);\n } else {\n await formRef.value.validate();\n }\n /* ★ v29.23(用户定案):视图主表与列配置【同时保存】——统一走 save_view\n (事务内两表同落,新建/编辑共用),不再分 create_db_view + save_columns 两次请求。\n ★ v29.35:columns / deleted_data_indexes 改由 StepColumnConfig 提供(ref 调 expose 的方法)。 */\n const payload: any = {\n ...buildPayload(),\n columns: colStepRef.value?.buildColumnsPayload?.() ?? [],\n deleted_data_indexes: colStepRef.value?.getDeletedIndexes?.() ?? [],\n };\n if (form_type.value === 'edit') {\n payload.db_view_id = props.record.id;\n }\n loading.value = true;\n await getApiData(\n { ...payload, org_id: current_org?.id },\n `${props.record.model_app}/save_view`,\n );\n message.success('操作成功');\n colStepRef.value?.resetDeletedIndexes?.();\n event_obj()?.emit('refresh');\n emit('ok');\n } catch (error) {\n // 校验失败或接口异常:保持弹窗打开\n } finally {\n loading.value = false;\n }\n};\nconst handleCancel = () => emit('cancel');\n\n/* ★ v29.45(用户定案):菜单两项**不再来自 DbView**(`parent_menu_flag` / `menu_project`\n 两个字段已从模型删除),改从「该视图对应的菜单行」读 —— `/db_view/get_db_view_menu`。\n ⚠️ 必须在 `loadDetail()` 与下面那次整体赋值**之后**再回填:`initForEdit` 会用\n `model.value = {...}` 重建对象,顺序反了这两项会被覆盖掉。\n ⚠️ 走 `skip_error: true` + try/catch:菜单回显失败**不该阻塞编辑**(留空即可,\n 保存时后端会按新值写/建菜单)。 */\nconst loadMenuInfo = async (flag: any) => {\n if (!flag) return;\n try {\n const res = await getApiData(\n { org_id: current_org?.id, flag },\n `${props.record.model_app}/get_db_view_menu`,\n false,\n { skip_error: true },\n );\n const data = extractData(res) || {};\n if (!data.exists) return; // 视图还没保存过菜单 / 菜单已被删除 → 两项留空\n model.value.parent_menu_id = data.parent_id ?? undefined;\n model.value.menu_project = data.menu_project || undefined;\n } catch (e) {\n // 保持留空\n }\n};\n\nconst initForEdit = async () => {\n const id = props.record?.id;\n const record = (id != null ? await loadDetail(id) : props.record) || {};\n model.value = {\n ...model.value,\n name: record.name || undefined,\n flag: record.flag || undefined,\n data_source_id: record.data_source_id ?? undefined,\n sql: record.sql || undefined,\n };\n // ★ v29.35:拆包结果写回共享 model,其余键留在 preservedMeta(提交时原样合并)\n preservedMeta = applyViewMeta(model.value, record.view_meta || {});\n // ★ v29.45:菜单两项(父级 / 隶属项目)最后回填(必须在上面两次赋值之后)\n await loadMenuInfo(model.value.flag);\n};\n\nonMounted(async () => {\n if (props.record?.form_type) form_type.value = props.record.form_type;\n /* ★ v29.35:数据源下拉 / 角色列表 / 列配置预取 / 新建自动生成 flag 都已随各自步骤下沉,\n 各自的 mounted 里自行加载;外壳只剩「编辑态拉详情」这一步。 */\n if (form_type.value === 'edit') {\n await initForEdit();\n }\n});\n</script>\n\n<style scoped lang=\"less\">\n.config-steps {\n padding: 12px 0 4px;\n}\n/* ★ v29.17.2:步骤内容区固定高度(每步内部滚动),切换步骤时弹窗总高不跳变 */\n.step-pane {\n height: calc(80vh - 150px);\n min-height: 360px;\n overflow-y: auto;\n overflow-x: hidden; /* ★ gutter 负 margin / 表格 min-width 等横向溢出不外溢 */\n padding-right: 4px;\n}\n/* ★ v29.34:列配置步专用——工具条固定在上、表格吃满剩余高度,**整个面板只留 s-table 一根滚动条**。\n ① 必须把基础 `.step-pane` 的 `overflow-y: auto` 收掉:否则出现的是「外层滚动条」,\n 表头会跟着内容滚走,`flex: 1` 也就没有「剩余高度」可解析,自动高度链路从这一环断。\n 纵向交给 s-table 表体、横向交给 `scroll.x`。\n ② 选择器刻意写成**复合** `.step-pane.step-pane--col`(0,2,0)而不是 `.step-pane--col`(0,1,0):\n 后者与 `.step-pane` 特异度打平,`overflow` 能否压住 `overflow-y: auto` 就**只取决于两个样式块的\n 先后顺序**——将来有人调整顺序就会静默退回 auto(多出一根滚动条、表格高度失去意义)。\n 与抽屉那次 `.csc-drawer.ant-drawer .ant-drawer-body` 是同一类坑,写成复合/三段式即与顺序无关。\n ★ v29.35:本 div 的直接子元素就是 StepColumnConfig 内部的 `.col-toolbar` 与\n `.col-table-wrapper`(fragment 组件不产生额外 DOM),故 `flex: 1` 的参照仍是它。 */\n.step-pane.step-pane--col {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n</style>\n"],"mappings":"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"}