|
|
<script>
|
|
|
import formBuilder from '@/utils/formBuilder'
|
|
|
import moment from "moment/moment";
|
|
|
import MobileMultipleSelect from "@/components/MobileMultipleSelect/index.vue";
|
|
|
import {deepCopy} from "@/utils";
|
|
|
import {PopupManager} from "element-ui/lib/utils/popup";
|
|
|
import request from '@/utils/request'
|
|
|
import {getToken} from '@/utils/auth'
|
|
|
import { evaluateFieldLinkage } from "@/utils/fieldLinkage";
|
|
|
export default {
|
|
|
components: {
|
|
|
MobileMultipleSelect
|
|
|
},
|
|
|
props: {
|
|
|
config: {
|
|
|
type: Object
|
|
|
},
|
|
|
isFirstNode: {
|
|
|
type: Boolean,
|
|
|
default: true
|
|
|
},
|
|
|
needFlowTitle: {
|
|
|
type: Boolean,
|
|
|
default: true
|
|
|
},
|
|
|
readable: {
|
|
|
type: Array,
|
|
|
default: () => [],
|
|
|
required: true
|
|
|
},
|
|
|
writeable: {
|
|
|
type: Array,
|
|
|
default: () => [],
|
|
|
required: true
|
|
|
},
|
|
|
originalForm: {
|
|
|
type: Object,
|
|
|
default: () => ({}),
|
|
|
required: true
|
|
|
},
|
|
|
subForm: {
|
|
|
type: Map,
|
|
|
default: () => new Map()
|
|
|
},
|
|
|
device: {
|
|
|
type: String,
|
|
|
default: 'desktop',
|
|
|
required: true
|
|
|
},
|
|
|
fields: {
|
|
|
type: Array,
|
|
|
default: () => [],
|
|
|
required: true
|
|
|
},
|
|
|
scriptContent: String,
|
|
|
rules: {
|
|
|
type: Object,
|
|
|
default: () => ({}),
|
|
|
},
|
|
|
subRules: {
|
|
|
type: Object,
|
|
|
default: () => ({}),
|
|
|
},
|
|
|
logs: {
|
|
|
type: Array,
|
|
|
default: () => []
|
|
|
}
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
isShowModal: false,
|
|
|
modalRender: () => {},
|
|
|
zIndex: PopupManager.nextZIndex(),
|
|
|
|
|
|
jointlySignLog: [], // 所有会签log记录
|
|
|
form: {},
|
|
|
// fill_flow_title 自动填充 watcher(监听组件内部 form,而不是父组件)
|
|
|
_unwatchFillFlowTitle: null,
|
|
|
file: {
|
|
|
ggg: []
|
|
|
},
|
|
|
flows: {},
|
|
|
showControl: {},
|
|
|
vanCalendarOption: {
|
|
|
isShow: false,
|
|
|
forFormName: "",
|
|
|
originalObj: ""
|
|
|
},
|
|
|
vanTimePickerOption: {
|
|
|
isShow: false,
|
|
|
forFormName: "",
|
|
|
originalObj: ""
|
|
|
},
|
|
|
vanPopupOption: {
|
|
|
isShow: false,
|
|
|
forFormName: "",
|
|
|
columns: [],
|
|
|
originalObj: ""
|
|
|
},
|
|
|
multipleSelectOption: {
|
|
|
isShow: false,
|
|
|
forFormName: "",
|
|
|
columns: [],
|
|
|
originalObj: "",
|
|
|
multipleLimit: 50,
|
|
|
options: {
|
|
|
label: 'name',
|
|
|
value: 'id'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
setupFillFlowTitleWatcher() {
|
|
|
try {
|
|
|
if (typeof this._unwatchFillFlowTitle === "function") {
|
|
|
this._unwatchFillFlowTitle();
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// ignore
|
|
|
}
|
|
|
this._unwatchFillFlowTitle = null;
|
|
|
|
|
|
// 只在新建流程时生效(有 flow_id 代表待办/编辑/详情)
|
|
|
if (this.$route?.query?.flow_id) return;
|
|
|
|
|
|
const list = Array.isArray(this.fields) ? this.fields : [];
|
|
|
const allowedTypes = new Set(["text", "textarea", "select"]);
|
|
|
const fillField = list.find((f) => Number(f?.fill_flow_title) === 1 && allowedTypes.has(f?.type) && f?.name);
|
|
|
if (!fillField) return;
|
|
|
|
|
|
const fieldName = fillField.name;
|
|
|
this._unwatchFillFlowTitle = this.$watch(
|
|
|
() => (this.form ? this.form[fieldName] : undefined),
|
|
|
(newVal) => {
|
|
|
// 保留 isFirstNode 判断
|
|
|
if (!this.isFirstNode || this.$route?.query?.flow_id) return;
|
|
|
const parseMultiValue = (raw) => {
|
|
|
if (raw === null || raw === undefined) return [];
|
|
|
if (Array.isArray(raw)) return raw.map((v) => String(v).trim()).filter(Boolean);
|
|
|
const s = String(raw).trim();
|
|
|
if (!s) return [];
|
|
|
const parts = s.includes("|") ? s.split("|") : (s.includes(",") ? s.split(",") : [s]);
|
|
|
return parts.map((v) => String(v).trim()).filter(Boolean);
|
|
|
};
|
|
|
const getOptionLabelFromSelectionModelItems = (field, value) => {
|
|
|
const items = field?.selection_model_items;
|
|
|
if (!Array.isArray(items) || items.length === 0) return null;
|
|
|
const v = String(value);
|
|
|
const hit = items.find((it) => String(it?.id ?? it?.value ?? it?.key ?? "") === v) || null;
|
|
|
if (!hit) return null;
|
|
|
return hit?.name ?? hit?.label ?? hit?.title ?? (hit?.id !== undefined ? String(hit.id) : null);
|
|
|
};
|
|
|
const getOptionLabelFromStub = (field, value) => {
|
|
|
const stub = field?.stub;
|
|
|
if (!stub) return null;
|
|
|
const lines = String(stub).split(/\\r?\\n/).map((s) => s.trim()).filter(Boolean);
|
|
|
const v = String(value);
|
|
|
return lines.find((line) => line === v) || null;
|
|
|
};
|
|
|
const formatSelectFillValue = (field, rawValue) => {
|
|
|
const values = parseMultiValue(rawValue);
|
|
|
if (values.length === 0) return "";
|
|
|
if (values.length > 1) {
|
|
|
return `${field.label || field.name} - ${values.join("、")}`;
|
|
|
}
|
|
|
const value = values[0];
|
|
|
const optionLabel =
|
|
|
getOptionLabelFromSelectionModelItems(field, value) ||
|
|
|
getOptionLabelFromStub(field, value) ||
|
|
|
value;
|
|
|
return `${optionLabel} - ${value}`;
|
|
|
};
|
|
|
|
|
|
let v = "";
|
|
|
if (fillField.type === "select") {
|
|
|
v = formatSelectFillValue(fillField, newVal);
|
|
|
} else {
|
|
|
v = newVal === null || newVal === undefined ? "" : String(newVal).trim();
|
|
|
}
|
|
|
if (!v) return;
|
|
|
this.$set(this.form, "flow_title", v);
|
|
|
},
|
|
|
{ immediate: true }
|
|
|
);
|
|
|
},
|
|
|
// 给自定义脚本注册的事件监听加保护,避免脚本内部抛错导致页面整体报错
|
|
|
installSafeEventListenerGuard() {
|
|
|
if (window.__oa_safe_listener_guard_installed) return;
|
|
|
window.__oa_safe_listener_guard_installed = true;
|
|
|
|
|
|
const map = new WeakMap();
|
|
|
const origAdd = EventTarget && EventTarget.prototype && EventTarget.prototype.addEventListener;
|
|
|
const origRemove = EventTarget && EventTarget.prototype && EventTarget.prototype.removeEventListener;
|
|
|
if (!origAdd || !origRemove) return;
|
|
|
|
|
|
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
|
if (typeof listener === "function") {
|
|
|
let wrapped = map.get(listener);
|
|
|
if (!wrapped) {
|
|
|
wrapped = function (...args) {
|
|
|
try {
|
|
|
return listener.apply(this, args);
|
|
|
} catch (e) {
|
|
|
console.error("[OA Custom Script] event listener error:", e);
|
|
|
}
|
|
|
};
|
|
|
map.set(listener, wrapped);
|
|
|
}
|
|
|
return origAdd.call(this, type, wrapped, options);
|
|
|
}
|
|
|
return origAdd.call(this, type, listener, options);
|
|
|
};
|
|
|
|
|
|
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
|
|
if (typeof listener === "function") {
|
|
|
const wrapped = map.get(listener);
|
|
|
return origRemove.call(this, type, wrapped || listener, options);
|
|
|
}
|
|
|
return origRemove.call(this, type, listener, options);
|
|
|
};
|
|
|
},
|
|
|
request,
|
|
|
getToken,
|
|
|
async validate() {
|
|
|
const $vanForm = this.$refs['vanForm']
|
|
|
if ($vanForm) {
|
|
|
const res = await $vanForm.validate()
|
|
|
}
|
|
|
let subFormName = this.fields.filter(i => i.type === 'relation').map(i => i.name)
|
|
|
for (let i = 0;i < subFormName.length;i++) {
|
|
|
const $subVanForm = this.$refs[`subVanForm-${i}`]
|
|
|
if ($subVanForm) {
|
|
|
await $subVanForm.validate()
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
},
|
|
|
computed: {
|
|
|
linkage() {
|
|
|
return evaluateFieldLinkage({
|
|
|
fields: this.fields,
|
|
|
form: this.form,
|
|
|
writeable: this.writeable,
|
|
|
});
|
|
|
},
|
|
|
},
|
|
|
watch: {
|
|
|
info(newVal) {
|
|
|
let keys = newVal.map(i => i.name)
|
|
|
keys.forEach(key => {
|
|
|
this.form[key] = ''
|
|
|
})
|
|
|
},
|
|
|
originalForm(newVal) {
|
|
|
this.form = deepCopy(newVal)
|
|
|
this.$nextTick(() => this.setupFillFlowTitleWatcher());
|
|
|
},
|
|
|
fields: {
|
|
|
handler() {
|
|
|
this.$nextTick(() => this.setupFillFlowTitleWatcher());
|
|
|
},
|
|
|
deep: true
|
|
|
},
|
|
|
scriptContent(newVal) {
|
|
|
if(newVal) {
|
|
|
try {
|
|
|
// 使用 $nextTick 确保 DOM 已经渲染完成
|
|
|
this.$nextTick(() => {
|
|
|
try {
|
|
|
this.installSafeEventListenerGuard();
|
|
|
new Function(newVal).bind(this)();
|
|
|
} catch (err) {
|
|
|
console.error('脚本执行错误:', err);
|
|
|
// 不阻止页面正常使用,只记录错误
|
|
|
}
|
|
|
});
|
|
|
} catch (err) {
|
|
|
console.error('脚本编译错误:', err);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
isShowModal(newVal) {
|
|
|
if(newVal) {
|
|
|
this.zIndex = PopupManager.nextZIndex()
|
|
|
}
|
|
|
},
|
|
|
logs: {
|
|
|
handler: function (newVal) {
|
|
|
if (newVal && newVal instanceof Array && newVal.length > 0) {
|
|
|
this.jointlySignLog = newVal.filter(log => {
|
|
|
try {
|
|
|
JSON.parse(log.data)
|
|
|
return log.is_jointly_sign && /custom_field_id/g.test(log.data)
|
|
|
} catch (e) {
|
|
|
return false
|
|
|
}
|
|
|
})
|
|
|
} else {
|
|
|
this.jointlySignLog = []
|
|
|
}
|
|
|
},
|
|
|
immediate: true
|
|
|
}
|
|
|
},
|
|
|
render(h) {
|
|
|
const authFields = this.fields.map(field => ({
|
|
|
...field,
|
|
|
_readable: this.readable.indexOf(field.id) !== -1,
|
|
|
_writeable: this.writeable.indexOf(field.id) !== -1,
|
|
|
_linkageDisabled: this.linkage.disabledFieldIds.has(Number(field.id)),
|
|
|
}))
|
|
|
const subFields = Array.from(this.subForm).map(i => i[1]?.customModel?.fields).filter(i => i).flat()
|
|
|
if (this.needFlowTitle) {
|
|
|
authFields.unshift({
|
|
|
name: "flow_title",
|
|
|
label: "工作名称",
|
|
|
type: "text",
|
|
|
label_show: 1,
|
|
|
_readable: !this.isFirstNode,
|
|
|
_writeable: this.isFirstNode,
|
|
|
});
|
|
|
}
|
|
|
return h('div',[
|
|
|
h('van-form',{
|
|
|
ref: 'vanForm',
|
|
|
props: {
|
|
|
'validate-first': true,
|
|
|
'scroll-to-error': true
|
|
|
}
|
|
|
},authFields.map(field => formBuilder.bind(this)(this.device, field, h))),
|
|
|
//calendar
|
|
|
(authFields.findIndex(i => i.type === 'date') !== -1 || subFields.findIndex(i => i.type === 'date')) ?
|
|
|
h('van-calendar',{
|
|
|
ref: `vanCalendar`,
|
|
|
props: {
|
|
|
position: 'bottom',
|
|
|
value: this.vanCalendarOption.isShow,
|
|
|
'min-date': this.$moment().subtract('years',10).toDate(),
|
|
|
'max-date': this.$moment().add('years',10).toDate(),
|
|
|
},
|
|
|
on: {
|
|
|
input: e => {
|
|
|
this.$set(this.vanCalendarOption,'isShow',e)
|
|
|
this.$set(this.vanCalendarOption,'isShow',false)
|
|
|
},
|
|
|
confirm: date => {
|
|
|
if(typeof this.vanCalendarOption.originalObj === 'object') {
|
|
|
this.$set(this.vanCalendarOption.originalObj,this.vanCalendarOption.forFormName,moment(date).format('YYYY-MM-DD'))
|
|
|
} else {
|
|
|
this.$set(this.form,this.vanCalendarOption.forFormName,moment(date).format('YYYY-MM-DD'))
|
|
|
}
|
|
|
this.$set(this.vanCalendarOption,'isShow',false)
|
|
|
}
|
|
|
}
|
|
|
}) : '',
|
|
|
//datetimepicker
|
|
|
(authFields.findIndex(i => i.type === 'datetime') !== -1 || subFields.findIndex(i => i.type === 'datetime') !== -1) ?
|
|
|
h('van-popup',{
|
|
|
props: {
|
|
|
value: this.vanTimePickerOption.isShow,
|
|
|
position: 'bottom',
|
|
|
},
|
|
|
on: {
|
|
|
input: e => {
|
|
|
this.$set(this.vanTimePickerOption,'isShow',e)
|
|
|
},
|
|
|
}
|
|
|
},[
|
|
|
h('van-datetime-picker',{
|
|
|
props: {
|
|
|
type: 'datetime',
|
|
|
title: '选择时间'
|
|
|
},
|
|
|
on: {
|
|
|
confirm: time => {
|
|
|
if(typeof this.vanTimePickerOption.originalObj === 'object') {
|
|
|
this.$set(this.vanTimePickerOption.originalObj,this.vanTimePickerOption.forFormName,moment(time).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
} else {
|
|
|
this.$set(this.form,this.vanTimePickerOption.forFormName,moment(time).format('YYYY-MM-DD HH:mm:ss'))
|
|
|
}
|
|
|
this.$set(this.vanTimePickerOption,'isShow',false)
|
|
|
},
|
|
|
cancel: _ => {
|
|
|
this.$set(this.vanTimePickerOption,'isShow',false)
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
]) : '',
|
|
|
//popup
|
|
|
(authFields.findIndex(i => i.type === 'select') !== -1 || subFields.findIndex(i => i.type === 'select') !== -1) ?
|
|
|
h('van-popup',{
|
|
|
props: {
|
|
|
value: this.vanPopupOption.isShow,
|
|
|
position: 'bottom',
|
|
|
},
|
|
|
on: {
|
|
|
input: e => {
|
|
|
this.$set(this.vanPopupOption,'isShow',e)
|
|
|
},
|
|
|
}
|
|
|
},[
|
|
|
h('van-picker',{
|
|
|
props: {
|
|
|
'show-toolbar': true,
|
|
|
columns: this.vanPopupOption.columns,
|
|
|
'value-key': typeof this.vanPopupOption.columns[0] === 'object' ? 'name' : 'yext'
|
|
|
},
|
|
|
on: {
|
|
|
confirm: value => {
|
|
|
if(typeof this.vanPopupOption.originalObj === 'object') {
|
|
|
this.$set(this.vanPopupOption.originalObj,this.vanPopupOption.forFormName,typeof value === 'object' ? value['id'] : value)
|
|
|
} else {
|
|
|
this.$set(this.form,this.vanPopupOption.forFormName,typeof value === 'object' ? value['id'] : value)
|
|
|
}
|
|
|
this.$set(this.vanPopupOption,'isShow',false)
|
|
|
},
|
|
|
cancel: _ => {
|
|
|
this.$set(this.vanPopupOption,'isShow',false)
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
]) : '',
|
|
|
// 多选
|
|
|
(authFields.findIndex(i => ['relation-flow','choice','choices'].indexOf(i.type) !== -1) !== -1 || subFields.findIndex(i => ['relation-flow','choice','choices'].indexOf(i.type) !== -1) !== -1) ?
|
|
|
h('van-popup',{
|
|
|
props: {
|
|
|
value: this.multipleSelectOption.isShow,
|
|
|
position: 'bottom',
|
|
|
},
|
|
|
on: {
|
|
|
input: e => {
|
|
|
this.$set(this.multipleSelectOption,'isShow',e)
|
|
|
},
|
|
|
}
|
|
|
},[
|
|
|
h(MobileMultipleSelect,{
|
|
|
props: {
|
|
|
selectDataOpts: this.multipleSelectOption.columns,
|
|
|
multipleLimit: this.multipleSelectOption.multipleLimit,
|
|
|
options: this.multipleSelectOption.options,
|
|
|
outputType: this.multipleSelectOption.outputType,
|
|
|
},
|
|
|
on: {
|
|
|
confirm: value => {
|
|
|
if(typeof this.multipleSelectOption.originalObj === 'object') {
|
|
|
console.log(this.multipleSelectOption.originalObj)
|
|
|
this.$set(this.multipleSelectOption.originalObj,this.multipleSelectOption.forFormName,value)
|
|
|
} else {
|
|
|
this.$set(this.form,this.multipleSelectOption.forFormName,value)
|
|
|
}
|
|
|
this.$set(this.multipleSelectOption,'isShow',false)
|
|
|
},
|
|
|
cancel: _ => {
|
|
|
this.$set(this.multipleSelectOption,'isShow',false)
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
]) : '',
|
|
|
|
|
|
// 用于编写脚本中弹窗
|
|
|
h('vxe-modal',{
|
|
|
props: {
|
|
|
zIndex: this.zIndex,
|
|
|
value: this.isShowModal,
|
|
|
fullscreen: true,
|
|
|
transfer: true
|
|
|
},
|
|
|
on: {
|
|
|
input: e => {
|
|
|
this.isShowModal = e
|
|
|
}
|
|
|
}
|
|
|
}, [
|
|
|
this.modalRender.bind(this)(h)
|
|
|
])
|
|
|
])
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
</style>
|