修复手机端的单选框和上传控件的bug

master
weizong song 1 month ago
parent c6f1d1e5c1
commit 6860240eaa

@ -985,27 +985,56 @@ export default {
return;
}
const rows = [];
const authGroups = [];
for (const node of selectedNodes) {
const auths = await this.getDepartmentAuthorizations(node.id);
if (!auths || auths.length !== 1) {
this.selectionError = "多条预算来源时,每条预算必须且仅能授权给一个部门";
if (!auths || auths.length === 0) {
this.selectionError = `预算“${node.name || node.id}”未配置部门授权`;
this.$message.warning(this.selectionError);
this.selectedBudgetIds = [...this.lastValidBudgetIds];
this.setCheckedIds(this.lastValidBudgetIds);
this.selectionError = "";
return;
}
const a = auths[0];
const key = `${String(node.id)}:${String(a.department_id)}`;
rows.push({
budget_data_id: node.id,
budget_name: node.name || "",
department_id: a.department_id,
department_name: a.department?.name || "",
allocated_amount: existingMap[key] ?? 0,
authGroups.push({
node,
auths,
authIdSet: new Set(
auths
.map((a) => String(a.department_id || ""))
.filter(Boolean)
),
});
}
const commonDepartmentIds = [...authGroups[0].authIdSet].filter((departmentId) =>
authGroups.every((group) => group.authIdSet.has(departmentId))
);
if (commonDepartmentIds.length === 0) {
this.selectionError = "多条预算来源不存在交叉授权科室";
this.$message.warning(this.selectionError);
this.selectedBudgetIds = [...this.lastValidBudgetIds];
this.setCheckedIds(this.lastValidBudgetIds);
this.selectionError = "";
return;
}
const rows = [];
authGroups.forEach(({ node, auths }) => {
auths.forEach((a) => {
const departmentId = String(a.department_id || "");
if (!commonDepartmentIds.includes(departmentId)) return;
const key = `${String(node.id)}:${departmentId}`;
rows.push({
budget_data_id: node.id,
budget_name: node.name || "",
department_id: a.department_id,
department_name: a.department?.name || "",
allocated_amount: existingMap[key] ?? 0,
});
});
});
this.pendingAllocations = rows;
this.lastValidBudgetIds = ids;
},

@ -23,10 +23,30 @@ Vue.use(ElementUI)
Vue.config.productionTip = false
// vant: only register components used as global tags
import { Popup, Picker, Uploader } from 'vant'
import {
Popup,
Picker,
Uploader,
Radio,
RadioGroup,
Form,
Field,
Calendar,
DatetimePicker,
Cell,
Button
} from 'vant'
import 'vant/lib/popup/style'
import 'vant/lib/picker/style'
import 'vant/lib/uploader/style'
import 'vant/lib/radio/style'
import 'vant/lib/radio-group/style'
import 'vant/lib/form/style'
import 'vant/lib/field/style'
import 'vant/lib/calendar/style'
import 'vant/lib/datetime-picker/style'
import 'vant/lib/cell/style'
import 'vant/lib/button/style'
import 'vant/lib/checkbox/style'
import 'vant/lib/checkbox-group/style'
import 'vant/lib/dropdown-menu/style'
@ -34,6 +54,14 @@ import 'vant/lib/dropdown-item/style'
Vue.use(Popup)
Vue.use(Picker)
Vue.use(Uploader)
Vue.use(Radio)
Vue.use(RadioGroup)
Vue.use(Form)
Vue.use(Field)
Vue.use(Calendar)
Vue.use(DatetimePicker)
Vue.use(Cell)
Vue.use(Button)
import domZIndex from 'dom-zindex'
domZIndex.setCurrent(2000)

@ -21,6 +21,96 @@ function isJSON(str) {
}
}
function normalizeBudgetSourceValue(value, maxDepth = 8) {
let current = value;
for (let i = 0; i < maxDepth; i++) {
if (current === null || current === undefined || current === "") return current;
if (typeof current === "object") return current;
if (typeof current !== "string") return current;
const trimmed = current.trim();
if (!trimmed) return "";
try {
const parsed = JSON.parse(trimmed);
if (parsed === current) return parsed;
current = parsed;
continue;
} catch (e) {
// keep normalizing below
}
let next = trimmed;
if (
(next.startsWith('"') && next.endsWith('"')) ||
(next.startsWith("'") && next.endsWith("'"))
) {
next = next.slice(1, -1);
}
next = next
.replace(/\\\\/g, "\\")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
if (next === trimmed) return trimmed;
current = next;
}
return current;
}
function formatMoney(amount) {
const numeric = Number(amount || 0);
return `¥${numeric.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
function formatBudgetSourceDisplay(value) {
const normalized = normalizeBudgetSourceValue(value);
if (!normalized || typeof normalized !== "object") return "";
const rows = [];
const appendAllocations = (budgetLabel, allocations) => {
if (!Array.isArray(allocations)) return;
allocations.forEach((alloc) => {
if (!alloc || typeof alloc !== "object") return;
const amount = Number(alloc.allocated_amount || 0);
if (Math.abs(amount) < 0.00001) return;
const deptName = alloc.department_name || (alloc.department_id ? `科室ID:${alloc.department_id}` : "-");
const prefix = budgetLabel ? `${budgetLabel} - ` : "";
rows.push(`${prefix}${deptName} - ${formatMoney(amount)}`);
});
};
if (Array.isArray(normalized.budget_items)) {
normalized.budget_items.forEach((item) => {
if (!item || typeof item !== "object") return;
const budgetLabel = item.budget_name || (item.budget_data_id ? `预算ID:${item.budget_data_id}` : "");
appendAllocations(budgetLabel, item.allocations);
if (!Array.isArray(item.allocations) || item.allocations.length === 0) {
if (budgetLabel) rows.push(budgetLabel);
}
});
} else {
const budgetLabel = normalized.budget_name || (normalized.budget_data_id ? `预算ID:${normalized.budget_data_id}` : "");
appendAllocations(budgetLabel, normalized.allocations);
if (rows.length === 0 && budgetLabel) rows.push(budgetLabel);
}
if (rows.length > 1) {
return rows.map((row, index) => `${index + 1}${row}`).join("");
}
return rows.join("");
}
function shouldRenderJointlySignContent(info) {
return info && (info.is_sign === 1 || info.is_sign === true || info.is_sign === "1");
}
// radio 统一按 string 做存储与比较,避免 "1" vs 1 导致回显不勾选
function normalizeRadioValue(v) {
if (v === null || v === undefined) return "";
@ -1390,15 +1480,7 @@ export default function formBuilder(
// 如果 _display 为空尝试从原始值生成显示文本避免显示JSON
if (!displayValue && target[info.name]) {
const rawValue = target[info.name];
// 如果是字符串且看起来像JSON不直接显示
if (typeof rawValue === 'string' && (rawValue.trim().startsWith('{') || rawValue.trim().startsWith('['))) {
displayValue = ''; // 不显示原始JSON
} else if (typeof rawValue === 'object' || Array.isArray(rawValue)) {
displayValue = ''; // 不显示对象或数组
} else {
displayValue = String(rawValue);
}
displayValue = formatBudgetSourceDisplay(target[info.name]);
}
console.log('[budget-source] 只读模式渲染', {
@ -1452,7 +1534,7 @@ export default function formBuilder(
if (formItem) {
let jointlySignContent = [];
let isJointly = false;
this.jointlySignLog.forEach((log) => {
if (shouldRenderJointlySignContent(info)) this.jointlySignLog.forEach((log) => {
const data = JSON.parse(log.data);
Object.entries(data)?.forEach(([key, value]) => {
if (
@ -1464,47 +1546,14 @@ export default function formBuilder(
// 对于 budget-source 类型字段,优先显示后端返回的 _display 值
let displayValue = value.value;
if (info.type === 'budget-source') {
// 优先使用主表字段的 _display与主表字段展示一致
const displayFieldName = info.name + '_display';
const mainDisplayValue = target[displayFieldName] || '';
if (mainDisplayValue) {
displayValue = mainDisplayValue;
} else if (displayValue) {
// 如果没有 _display尝试识别并屏蔽 JSON包括双重编码
const strValue = String(displayValue);
// 尝试解析 JSON可能是一层或两层编码
let isJson = false;
try {
const parsed1 = JSON.parse(strValue);
if (typeof parsed1 === 'object' || Array.isArray(parsed1)) {
isJson = true;
} else if (typeof parsed1 === 'string') {
// 双重编码:第一层解析出来是字符串,再试一次
try {
const parsed2 = JSON.parse(parsed1);
if (typeof parsed2 === 'object' || Array.isArray(parsed2)) {
isJson = true;
}
} catch (e2) {
// 不是双重编码的 JSON
}
}
} catch (e) {
// 不是 JSON 字符串
}
// 如果是 JSON包括双重编码不显示
if (isJson || strValue.trim().startsWith('{') || strValue.trim().startsWith('[')) {
displayValue = ''; // 不显示原始 JSON
} else if (typeof displayValue === 'object' || Array.isArray(displayValue)) {
displayValue = ''; // 不显示对象或数组
}
}
displayValue = target[displayFieldName] || formatBudgetSourceDisplay(target[info.name]) || '';
}
jointlySignContent.push(
h("div", [
h("span", displayValue),
h("br"),
info.is_sign
shouldRenderJointlySignContent(info)
? log.user?.sign_file?.url
? h("div", {
style: {
@ -1986,141 +2035,121 @@ export default function formBuilder(
break;
case "file":
formItem = h(
"el-upload",
{
props: {
action: process.env.VUE_APP_UPLOAD_API,
const normalizeMobileUploadList = (list) => {
return (list instanceof Array ? list : []).map((file) => {
file.name = file.original_name || file.name || file.file?.name;
file.url = file.url || file.response?.url;
file.status = file.status === "success" ? "done" : (file.status || "done");
return file;
});
};
const setMobileUploadList = (list) => {
this.$set(target, info.name, normalizeMobileUploadList(list));
};
const uploadMobileFile = (fileItem) => {
const rawFile = fileItem?.file || fileItem;
if (!rawFile) return;
fileItem.status = "uploading";
fileItem.message = "上传中...";
window.$_uploading = true;
const currentList = normalizeMobileUploadList(target[info.name]);
if (currentList.indexOf(fileItem) === -1) {
currentList.push(fileItem);
}
setMobileUploadList(currentList.slice(-(info.multiple ? 20 : 1)));
const formData = new FormData();
formData.append("file", rawFile);
axios
.post(process.env.VUE_APP_UPLOAD_API, formData, {
headers: {
Authorization: `Bearer ${getToken()}`,
},
accept:
".cebx,application/msword,image/jpeg,application/pdf,image/png,application/vnd.ms-powerpoint,text/plain,application/x-zip-compressed,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
multiple: !!info.multiple,
limit: info.multiple ? 20 : 1,
fileList:
this.form[info.name] instanceof Array
? this.form[info.name]?.map((i) => {
if (i.hasOwnProperty("original_name")) {
i.name = i.original_name;
}
return i;
})
: [],
beforeUpload: (file) => {
if (file.size > uploadSize) {
this.$message({
type: "warning",
message: `上传图片大小超过${formatFileSize(
uploadSize
)}`,
});
return false;
}
window.$_uploading = true;
},
onSuccess: (response, file, fileList) => {
window.$_uploading = false;
console.log("response",response,file,fileList)
if (response.code) {
fileList.splice(fileList.indexOf(file), 1)
this.$message.warning(response.msg)
}
fileList.forEach((file) => {
if (file.response?.data && !file.response?.code) {
file.response = file.response?.data;
file.url = file.response?.url;
}
});
target[info.name] = fileList;
},
onRemove: (file, fileList) => {
target[info.name] = fileList;
},
onError: (err, file, fileList) => {
console.log("err",err,file,fileList)
window.$_uploading = false;
if (err.code) {
fileList.splice(fileList.indexOf(file), 1)
this.$message.warning(err.msg)
}
target[info.name] = fileList;
this.$message({
type: "warning",
message: err,
})
.then((response) => {
window.$_uploading = false;
if (response.status === 200 && !response.data.code) {
const uploaded = response.data.data;
Object.assign(fileItem, {
status: "done",
message: "",
response: uploaded,
id: uploaded.id,
name: uploaded.original_name,
original_name: uploaded.original_name,
url: uploaded.url,
});
} else {
fileItem.status = "failed";
fileItem.message = response.data?.msg || "上传失败";
this.$message.warning(fileItem.message);
}
setMobileUploadList(target[info.name]);
})
.catch((err) => {
window.$_uploading = false;
fileItem.status = "failed";
fileItem.message = "上传失败";
setMobileUploadList(target[info.name]);
this.$message.warning(err?.message || "上传失败");
});
};
formItem = h("div", [
h(
"van-uploader",
{
props: {
fileList: normalizeMobileUploadList(target[info.name]),
multiple: !!info.multiple,
maxCount: info.multiple ? 20 : 1,
maxSize: uploadSize,
accept:
".cebx,application/msword,image/jpeg,application/pdf,image/png,application/vnd.ms-powerpoint,text/plain,application/x-zip-compressed,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
beforeRead: (file) => {
const files = file instanceof Array ? file : [file];
const oversized = files.find((item) => item.size > uploadSize);
if (oversized) {
this.$message.warning(`上传文件大小超过${formatFileSize(uploadSize)}`);
return false;
}
return true;
},
afterRead: (file) => {
const files = file instanceof Array ? file : [file];
files.forEach(uploadMobileFile);
},
},
},
scopedSlots: {
file: (scope) => {
let { file } = scope;
return [
h("div", {}, [
h("i", {
class: {
"el-icon-circle-check": file.status === "success",
"el-icon-loading": file.status === "uploading",
},
style: {
color: file.status === "success" ? "green" : "",
},
}),
h(
"a",
{
class: {
"uploaded-a": file.status === "success",
},
style: {
padding: "0 4px",
},
on: {
click: (_) => {
this.$bus.$emit("online-file", file.url);
},
},
},
file.original_name || file.name
),
]),
h("i", {
class: "el-icon-close",
on: {
["click"]: () => {
if (file.status === "uploading") return;
target[info.name].splice(
target[info.name].indexOf(file),
1
);
},
},
}),
];
on: {
input: (fileList) => {
setMobileUploadList(fileList);
},
},
},
},
[
h(
"el-button",
{
slot: "trigger",
props: {
size: "mini",
type: "primary",
[
h(
"van-button",
{
props: {
type: "info",
size: "small",
},
},
"选取文件"
),
]
),
h(
"div",
{
class: "el-upload__tip",
style: {
"margin-top": "8px",
},
"选取文件"
),
h(
"div",
{
class: "el-upload__tip",
slot: "tip",
},
`文件不超过${formatFileSize(uploadSize)}`
),
]
);
},
`文件不超过${formatFileSize(uploadSize)}`
),
]);
break;
case "relation-flow":
if (!this.flows[info.name]) {
@ -2716,7 +2745,7 @@ export default function formBuilder(
if (formItem) {
let jointlySignContent = [];
let isJointly = false;
this.jointlySignLog.forEach((log) => {
if (shouldRenderJointlySignContent(info)) this.jointlySignLog.forEach((log) => {
const data = JSON.parse(log.data);
Object.entries(data)?.forEach(([key, value]) => {
if (
@ -2728,47 +2757,14 @@ export default function formBuilder(
// 对于 budget-source 类型字段,优先显示后端返回的 _display 值
let displayValue = value.value;
if (info.type === 'budget-source') {
// 优先使用主表字段的 _display与主表字段展示一致
const displayFieldName = info.name + '_display';
const mainDisplayValue = target[displayFieldName] || '';
if (mainDisplayValue) {
displayValue = mainDisplayValue;
} else if (displayValue) {
// 如果没有 _display尝试识别并屏蔽 JSON包括双重编码
const strValue = String(displayValue);
// 尝试解析 JSON可能是一层或两层编码
let isJson = false;
try {
const parsed1 = JSON.parse(strValue);
if (typeof parsed1 === 'object' || Array.isArray(parsed1)) {
isJson = true;
} else if (typeof parsed1 === 'string') {
// 双重编码:第一层解析出来是字符串,再试一次
try {
const parsed2 = JSON.parse(parsed1);
if (typeof parsed2 === 'object' || Array.isArray(parsed2)) {
isJson = true;
}
} catch (e2) {
// 不是双重编码的 JSON
}
}
} catch (e) {
// 不是 JSON 字符串
}
// 如果是 JSON包括双重编码不显示
if (isJson || strValue.trim().startsWith('{') || strValue.trim().startsWith('[')) {
displayValue = ''; // 不显示原始 JSON
} else if (typeof displayValue === 'object' || Array.isArray(displayValue)) {
displayValue = ''; // 不显示对象或数组
}
}
displayValue = target[displayFieldName] || formatBudgetSourceDisplay(target[info.name]) || '';
}
jointlySignContent.push(
h("div", [
h("span", displayValue),
h("br"),
info.is_sign
shouldRenderJointlySignContent(info)
? log.user?.sign_file?.url
? h("div", {
style: {

Loading…
Cancel
Save