diff --git a/.env.development b/.env.development index 725d42d..6a303b8 100644 --- a/.env.development +++ b/.env.development @@ -2,8 +2,8 @@ ENV = 'development' # base api -#VUE_APP_BASE_API='http://192.167.20.118:8080/' -VUE_APP_BASE_API='http://czemc.localhost' +VUE_APP_BASE_API='http://192.167.20.118:8080/' +#VUE_APP_BASE_API='http://czemc.localhost' VUE_APP_UPLOAD_API='https://cz-hjjc-test.115.langye.net/api/upload-file' VUE_APP_PREVIEW=//view.langye.net/preview/onlinePreview VUE_APP_MODULE_NAME=oa diff --git a/src/utils/formBuilder.js b/src/utils/formBuilder.js index 8fa3002..a215c86 100644 --- a/src/utils/formBuilder.js +++ b/src/utils/formBuilder.js @@ -21,6 +21,63 @@ function isJSON(str) { } } +// radio 统一按 string 做存储与比较,避免 "1" vs 1 导致回显不勾选 +function normalizeRadioValue(v) { + if (v === null || v === undefined) return ""; + return String(v); +} + +function getRadioOptionValue(option) { + const raw = typeof option === "object" ? option?.id : option; + return normalizeRadioValue(raw); +} + +// 支持 radio 选项含“下级勾选项”的简单格式: +// 例:分散采购['委托社会中介代理机构','自行采购'] +// 或:分散采购{'委托社会中介代理机构','自行采购'} +function parseNestedRadioOption(option) { + if (typeof option !== "string") { + return { label: typeof option === "object" ? option?.name : option, children: [] }; + } + const match = option.match(/^(.*?)\s*(?:\[(.*)\]|\{(.*)\})\s*$/); + if (!match) return { label: option, children: [] }; + const label = (match[1] || "").trim(); + const rawList = match[2] ?? match[3] ?? ""; + // 提取引号内内容,兼容单引号/双引号 + const children = Array.from(rawList.matchAll(/'([^']*)'|"([^"]*)"/g)) + .map((m) => (m[1] ?? m[2] ?? "").trim()) + .filter(Boolean); + return { label: label || option, children }; +} + +// nested radio(父+子)存储(方案 c):JSON 字符串 +// 例:{"parent":"分散采购","children":["委托社会中介代理机构"]} +function parseNestedRadioStoredValue(v) { + const raw = v === null || v === undefined ? "" : v; + if (typeof raw !== "string") { + return { parent: normalizeRadioValue(raw), child: "" }; + } + try { + const obj = JSON.parse(raw); + if (obj && typeof obj === "object" && (obj.parent || obj.children || obj.child)) { + const parent = normalizeRadioValue(obj.parent); + const childArr = Array.isArray(obj.children) ? obj.children : []; + const child = normalizeRadioValue(obj.child ?? childArr?.[0] ?? ""); + return { parent, child }; + } + } catch (e) { + // ignore + } + return { parent: normalizeRadioValue(raw), child: "" }; +} + +function buildNestedRadioStoredValue(parent, child) { + return JSON.stringify({ + parent: normalizeRadioValue(parent), + children: child ? [normalizeRadioValue(child)] : [], + }); +} + /** * @param {String} device 'desktop' | 'mobile' * @param {Object} info field参数 @@ -205,32 +262,128 @@ export default function formBuilder( ); break; case "radio": + // 支持父 radio + 子 radio(单选),子项存储为 JSON(方案 c) + const { parent: radioParent, child: radioChild } = parseNestedRadioStoredValue(target[info.name]); + + const nestedOptions = options.map((option) => { + const parsed = parseNestedRadioOption(option); + const label = typeof option === "object" ? option.name : parsed.label; + const value = normalizeRadioValue(label); + const children = typeof option === "string" ? parsed.children : []; + return { option, label, value, children }; + }); + + const onParentChange = (parentValue) => { + const nextParent = normalizeRadioValue(parentValue); + const hit = nestedOptions.find((i) => i.value === nextParent); + if (hit && hit.children && hit.children.length > 0) { + const keepChild = hit.children.includes(radioChild) ? radioChild : ""; + this.$set(target, info.name, buildNestedRadioStoredValue(nextParent, keepChild)); + } else { + this.$set(target, info.name, nextParent); + } + }; + + const onChildChange = (parentValue, childValue) => { + this.$set( + target, + info.name, + buildNestedRadioStoredValue(parentValue, normalizeRadioValue(childValue)) + ); + }; + formItem = h( "el-radio-group", { props: { - value: target[info.name], + value: radioParent, }, attrs: { placeholder: info.help_text, }, on: { - input: (e) => { - this.$set(target, info.name, e); - }, + input: onParentChange, }, }, - options.map((option) => - h( + nestedOptions.map((item) => { + const checked = item.value === radioParent; + const showChildren = checked && item.children && item.children.length > 0; + return h( "el-radio", { + key: item.value, props: { - label: typeof option === "object" ? option.id : option, + label: item.value, }, }, - typeof option === "object" ? option.name : option - ) - ) + [ + h( + "span", + { + style: { + color: "rgb(51, 51, 51)", + }, + }, + item.label + ), + showChildren + ? h( + "span", + { + style: { + marginLeft: "6px", + color: "rgb(51, 51, 51)", + }, + }, + "(" + ) + : null, + showChildren + ? h( + "el-radio-group", + { + style: { + display: "inline-flex", + alignItems: "center", + gap: "10px", + marginLeft: "2px", + marginRight: "2px", + }, + props: { + value: radioChild, + }, + on: { + input: (v) => onChildChange(item.value, v), + }, + }, + item.children.map((c) => + h( + "el-radio", + { + key: `${item.value}__${c}`, + props: { + label: normalizeRadioValue(c), + }, + }, + c + ) + ) + ) + : null, + showChildren + ? h( + "span", + { + style: { + color: "rgb(51, 51, 51)", + }, + }, + ")" + ) + : null, + ] + ); + }) ); break; case "budget-source": @@ -938,6 +1091,127 @@ export default function formBuilder( getDetailSelectValue() ); break; + case "radio": + // 只读模式:用“方框 + 勾号”展示全部选项,并高亮已选项(不使用禁用 radio,避免灰色字体) + const { parent: readonlyParent, child: readonlyChild } = parseNestedRadioStoredValue(target[info.name]); + formItem = h( + "div", + { + style: { + display: "flex", + alignItems: "center", + flexWrap: "wrap", + gap: "10px 16px", + color: "rgb(51, 51, 51)", + lineHeight: "20px", + minHeight: "40px", + }, + }, + options.map((option) => { + const parsed = parseNestedRadioOption(option); + const labelText = typeof option === "object" ? option.name : parsed.label; + const optValue = normalizeRadioValue(labelText); + const checked = optValue === readonlyParent; + const children = typeof option === "string" ? parsed.children : []; + return h( + "div", + { + style: { + display: "inline-flex", + alignItems: "center", + cursor: "default", + userSelect: "none", + color: "rgb(51, 51, 51)", + }, + }, + [ + h( + "span", + { + style: { + width: "14px", + height: "14px", + border: "1px solid rgb(51, 51, 51)", + borderRadius: "2px", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + marginRight: "6px", + boxSizing: "border-box", + fontSize: "12px", + lineHeight: "12px", + }, + }, + checked ? "✓" : "" + ), + h( + "span", + { + style: { + color: "rgb(51, 51, 51)", + }, + }, + labelText + ), + checked && children.length > 0 + ? h( + "span", + { + style: { + marginLeft: "6px", + display: "inline-flex", + alignItems: "center", + gap: "10px", + color: "rgb(51, 51, 51)", + }, + }, + [ + h("span", "("), + ...children.map((c) => { + const cVal = normalizeRadioValue(c); + const cChecked = cVal === readonlyChild; + return h( + "span", + { + key: `${optValue}__${cVal}`, + style: { + display: "inline-flex", + alignItems: "center", + gap: "6px", + }, + }, + [ + h( + "span", + { + style: { + width: "14px", + height: "14px", + border: "1px solid rgb(51, 51, 51)", + borderRadius: "2px", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + boxSizing: "border-box", + fontSize: "12px", + lineHeight: "12px", + }, + }, + cChecked ? "✓" : "" + ), + h("span", c), + ] + ); + }), + h("span", ")"), + ] + ) + : null, + ] + ); + }) + ); + break; case "relation-flow": console.log("info",info,target) if (!this.flows[info.name]) { @@ -1162,32 +1436,45 @@ export default function formBuilder( h("br"), info.is_sign ? log.user?.sign_file?.url - ? h("el-image", { + ? h("div", { style: { - "max-height": "80px", - "max-width": "120px", - display: "block", - }, - props: { - src: log.user.sign_file.url, - fit: "contain", - alt: log.user?.name || "", - "preview-src-list": [log.user.sign_file.url], - lazy: false, - }, - attrs: { - src: log.user.sign_file.url, - }, - }) + display: "flex", + "align-items": "center", + gap: "8px", + } + }, [ + h("el-image", { + style: { + "max-height": "80px", + "max-width": "100px", + display: "block", + }, + props: { + src: log.user.sign_file.url, + fit: "contain", + alt: log.user?.name || "", + "preview-src-list": [log.user.sign_file.url], + lazy: false, + }, + attrs: { + src: log.user.sign_file.url, + }, + }), + h( + "span", + { + style: { + "font-size": "16px", + color: "#000", + } + }, + log.updated_at + ? this.$moment(log.updated_at).format("YYYY年MM月DD日") + : "" + ), + ]) : h("span", log.user?.name || "") : "", - info.is_sign ? h("br") : "", - h( - "span", - log.updated_at - ? this.$moment(log.updated_at).format("YYYY年MM月DD日") - : "" - ), ]) ); } @@ -1291,12 +1578,15 @@ export default function formBuilder( return h("div", { style: { "margin-top": "8px", + display: "flex", + "align-items": "center", + gap: "8px", } }, [ h("el-image", { style: { "max-height": "80px", - "max-width": "120px", + "max-width": "100px", display: "block", }, props: { @@ -1314,9 +1604,8 @@ export default function formBuilder( "div", { style: { - "font-size": "12px", - color: "#909399", - "margin-top": "4px", + "font-size": "16px", + color: "#000", } }, log.updated_at @@ -1371,30 +1660,109 @@ export default function formBuilder( }); break; case "radio": + // 支持父 radio + 子 radio(单选),子项存储为 JSON(方案 c) + const { parent: mRadioParent, child: mRadioChild } = parseNestedRadioStoredValue(target[info.name]); + + const mNestedOptions = options.map((option) => { + const parsed = parseNestedRadioOption(option); + const label = typeof option === "object" ? option.name : parsed.label; + const value = normalizeRadioValue(label); + const children = typeof option === "string" ? parsed.children : []; + return { option, label, value, children }; + }); + + const onMParentChange = (parentValue) => { + const nextParent = normalizeRadioValue(parentValue); + const hit = mNestedOptions.find((i) => i.value === nextParent); + if (hit && hit.children && hit.children.length > 0) { + const keepChild = hit.children.includes(mRadioChild) ? mRadioChild : ""; + this.$set(target, info.name, buildNestedRadioStoredValue(nextParent, keepChild)); + } else { + this.$set(target, info.name, nextParent); + } + }; + + const onMChildChange = (parentValue, childValue) => { + this.$set( + target, + info.name, + buildNestedRadioStoredValue(parentValue, normalizeRadioValue(childValue)) + ); + }; + formItem = h( "van-radio-group", { props: { - value: target[info.name], + value: mRadioParent, }, on: { - input: (e) => { - this.$set(target, info.name, e); - }, + input: onMParentChange, }, }, - options.map((option) => - h( - "van-radio", + mNestedOptions.map((item) => { + const checked = item.value === mRadioParent; + const showChildren = checked && item.children && item.children.length > 0; + return h( + "div", { - props: { - name: typeof option === "object" ? option.id : option, - "checked-color": "var(--theme-color)", + key: item.value, + style: { + display: "inline-flex", + alignItems: "center", + flexWrap: "wrap", + gap: "6px", + color: "rgb(51, 51, 51)", + marginRight: "12px", }, }, - typeof option === "object" ? option.name : option - ) - ) + [ + h( + "van-radio", + { + props: { + name: item.value, + "checked-color": "var(--theme-color)", + }, + }, + item.label + ), + showChildren ? h("span", { style: { color: "rgb(51, 51, 51)" } }, "(") : null, + showChildren + ? h( + "van-radio-group", + { + style: { + display: "inline-flex", + alignItems: "center", + gap: "10px", + }, + props: { + value: mRadioChild, + }, + on: { + input: (v) => onMChildChange(item.value, v), + }, + }, + item.children.map((c) => + h( + "van-radio", + { + key: `${item.value}__${c}`, + props: { + name: normalizeRadioValue(c), + "checked-color": "var(--theme-color)", + }, + }, + c + ) + ) + ) + : null, + showChildren ? h("span", { style: { color: "rgb(51, 51, 51)" } }, ")") : null, + ] + ); + }) ); break; case "date": @@ -1970,6 +2338,127 @@ export default function formBuilder( ) ); break; + case "radio": + // 只读模式:用“方框 + 勾号”展示全部选项,并高亮已选项(不使用禁用 radio,避免灰色字体) + const { parent: readonlyMParent, child: readonlyMChild } = parseNestedRadioStoredValue(target[info.name]); + formItem = h( + "div", + { + style: { + display: "flex", + alignItems: "center", + flexWrap: "wrap", + gap: "10px 16px", + color: "rgb(51, 51, 51)", + lineHeight: "20px", + minHeight: "40px", + }, + }, + options.map((option) => { + const parsed = parseNestedRadioOption(option); + const labelText = typeof option === "object" ? option.name : parsed.label; + const optValue = normalizeRadioValue(labelText); + const checked = optValue === readonlyMParent; + const children = typeof option === "string" ? parsed.children : []; + return h( + "div", + { + style: { + display: "inline-flex", + alignItems: "center", + cursor: "default", + userSelect: "none", + color: "rgb(51, 51, 51)", + }, + }, + [ + h( + "span", + { + style: { + width: "14px", + height: "14px", + border: "1px solid rgb(51, 51, 51)", + borderRadius: "2px", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + marginRight: "6px", + boxSizing: "border-box", + fontSize: "12px", + lineHeight: "12px", + }, + }, + checked ? "✓" : "" + ), + h( + "span", + { + style: { + color: "rgb(51, 51, 51)", + }, + }, + labelText + ), + checked && children.length > 0 + ? h( + "span", + { + style: { + marginLeft: "6px", + display: "inline-flex", + alignItems: "center", + gap: "10px", + color: "rgb(51, 51, 51)", + }, + }, + [ + h("span", "("), + ...children.map((c) => { + const cVal = normalizeRadioValue(c); + const cChecked = cVal === readonlyMChild; + return h( + "span", + { + key: `${optValue}__${cVal}`, + style: { + display: "inline-flex", + alignItems: "center", + gap: "6px", + }, + }, + [ + h( + "span", + { + style: { + width: "14px", + height: "14px", + border: "1px solid rgb(51, 51, 51)", + borderRadius: "2px", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + boxSizing: "border-box", + fontSize: "12px", + lineHeight: "12px", + }, + }, + cChecked ? "✓" : "" + ), + h("span", c), + ] + ); + }), + h("span", ")"), + ] + ) + : null, + ] + ); + }) + ); + break; case "relation": console.log("this.form[info.name]",this.form[info.name]) formItem = h("div", [ @@ -2146,32 +2635,45 @@ export default function formBuilder( h("br"), info.is_sign ? log.user?.sign_file?.url - ? h("el-image", { + ? h("div", { style: { - "max-height": "40px", - "max-width": "60px", - display: "block", - }, - props: { - src: log.user.sign_file.url, - fit: "contain", - alt: log.user?.name || "", - "preview-src-list": [log.user.sign_file.url], - lazy: false, - }, - attrs: { - src: log.user.sign_file.url, - }, - }) + display: "flex", + "align-items": "center", + gap: "8px", + } + }, [ + h("el-image", { + style: { + "max-height": "40px", + "max-width": "60px", + display: "block", + }, + props: { + src: log.user.sign_file.url, + fit: "contain", + alt: log.user?.name || "", + "preview-src-list": [log.user.sign_file.url], + lazy: false, + }, + attrs: { + src: log.user.sign_file.url, + }, + }), + h( + "span", + { + style: { + "font-size": "12px", + color: "#909399", + } + }, + log.updated_at + ? this.$moment(log.updated_at).format("YYYY年MM月DD日") + : "" + ), + ]) : h("span", log.user?.name || "") : "", - info.is_sign ? h("br") : "", - h( - "span", - log.updated_at - ? this.$moment(log.updated_at).format("YYYY年MM月DD日") - : "" - ), ]) ); } @@ -2266,6 +2768,9 @@ export default function formBuilder( return h("div", { style: { "margin-top": "8px", + display: "flex", + "align-items": "center", + gap: "8px", } }, [ h("el-image", { @@ -2291,7 +2796,6 @@ export default function formBuilder( style: { "font-size": "12px", color: "#909399", - "margin-top": "4px", } }, log.updated_at