diff --git a/src/api/flow/index.js b/src/api/flow/index.js index febe10f..d9ad0c4 100644 --- a/src/api/flow/index.js +++ b/src/api/flow/index.js @@ -279,6 +279,34 @@ export function updateFlowLogMeta(data, isLoading = true) { }) } +// 流程监管:排序草稿、时序校验和一次性提交展示顺序 +export function flowLogReorderMeta(data, isLoading = true) { + return request({ + method: 'post', + url: '/api/oa/flow/log-reorder-meta', + data, + isLoading, + }) +} + +export function validateFlowLogOrder(data, isLoading = true) { + return request({ + method: 'post', + url: '/api/oa/flow/validate-log-order', + data, + isLoading, + }) +} + +export function reorderFlowLogs(data, isLoading = true) { + return request({ + method: 'post', + url: '/api/oa/flow/reorder-logs', + data, + isLoading, + }) +} + export function deleteFlowLog(data, isLoading = true) { return request({ method: 'post', diff --git a/src/utils/fieldLinkage.js b/src/utils/fieldLinkage.js new file mode 100644 index 0000000..a927c5d --- /dev/null +++ b/src/utils/fieldLinkage.js @@ -0,0 +1,73 @@ +const CONTROL_TYPES = new Set(['text', 'select', 'radio']) +const TARGET_TYPES = new Set(['text', 'textarea', 'date', 'datetime', 'select', 'radio']) + +function normalizeValue(value) { + if (value === null || value === undefined) return '' + return String(value).trim() +} + +function isSimpleRadio(field) { + if (field.type !== 'radio') return true + return !String(field.stub || '').split(/\r?\n/).some((option) => /(?:\[[\s\S]*\]|\{[\s\S]*\})\s*$/.test(option)) +} + +function isSupportedControl(field) { + if (!field || !field.name || !CONTROL_TYPES.has(field.type)) return false + if (['select', 'radio'].includes(field.type) && field.selection_model) return false + if (field.type === 'select' && Number(field.multiple)) return false + return isSimpleRadio(field) +} + +function isSupportedTarget(field) { + return !!field && !!field.name && TARGET_TYPES.has(field.type) +} + +/** + * Evaluate persisted field linkage rules for a single main form. + * + * The function deliberately does not mutate form data or field definitions. + * A bad or unsupported rule is ignored (fail open), while a valid unmatched + * rule disables only fields that are writable at the current flow node. + */ +export function evaluateFieldLinkage({ fields, form, writeable }) { + const list = Array.isArray(fields) ? fields : [] + const writableIds = new Set((Array.isArray(writeable) ? writeable : []).map((id) => Number(id))) + const fieldsById = new Map(list.map((field) => [Number(field.id), field])) + const disabledFieldIds = new Set() + const disabledFieldNames = new Set() + const states = {} + + list.forEach((field) => { + const rule = field && field.visibility_rule + if (!rule) return + + const state = { active: false, disabled: false, reason: 'invalid_rule' } + states[field.id] = state + if (!isSupportedTarget(field) || rule.mode !== 'show_when' || !Array.isArray(rule.values)) return + + const values = rule.values.map(normalizeValue).filter(Boolean) + const controlField = fieldsById.get(Number(rule.control_field_id)) + if (!values.length || !isSupportedControl(controlField)) return + + if (!writableIds.has(Number(field.id)) || !writableIds.has(Number(controlField.id))) { + state.reason = 'not_both_writable' + return + } + + state.active = true + const controlValue = normalizeValue((form || {})[controlField.name]) + if (values.includes(controlValue)) { + state.reason = 'value_matched' + return + } + + state.disabled = true + state.reason = 'value_not_matched' + disabledFieldIds.add(Number(field.id)) + disabledFieldNames.add(field.name) + }) + + return { disabledFieldIds, disabledFieldNames, states } +} + +export default evaluateFieldLinkage diff --git a/src/utils/flowLogReorder.js b/src/utils/flowLogReorder.js new file mode 100644 index 0000000..2a2684a --- /dev/null +++ b/src/utils/flowLogReorder.js @@ -0,0 +1,32 @@ +/** + * 将流转记录转为排序模式可拖拽的单元:同一会签组只能整体移动。 + * 默认草稿按“开始 -> 历史记录的实际流转时间 -> 当前办理”组织。 + * 开始节点作为固定锚点展示,用户可看到但不能拖拽调整。 + */ +export function buildFlowLogReorderUnits(logs = [], normalize = true) { + const groups = new Map() + ;(logs || []).forEach((log) => { + const key = log.jointly_sign_id ? `joint-${log.jointly_sign_id}` : `log-${log.id}` + if (!groups.has(key)) groups.set(key, { key, logs: [] }) + groups.get(key).logs.push(log) + }) + + const units = Array.from(groups.values()).map((unit) => { + unit.logs.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')) || Number(a.id) - Number(b.id)) + const isStart = unit.logs.some((log) => log.node?.category === 'start') + const isHandling = unit.logs.some((log) => Number(log.status) === 0) + unit.locked = isStart + unit.lockReason = isStart ? '开始节点固定展示,不参与拖拽排序' : '' + unit.bucket = isStart ? 0 : (isHandling ? 2 : 1) + unit.time = unit.logs.map((log) => log.created_at || '').sort()[0] || '' + return unit + }) + + return normalize + ? units.sort((a, b) => a.bucket - b.bucket || String(a.time).localeCompare(String(b.time)) || a.key.localeCompare(b.key)) + : units +} + +export function flattenFlowLogReorderUnits(units = []) { + return (units || []).reduce((ids, unit) => ids.concat(unit.logs.map((log) => Number(log.id))), []) +} diff --git a/src/utils/formBuilder.js b/src/utils/formBuilder.js index 020a92a..4540f55 100644 --- a/src/utils/formBuilder.js +++ b/src/utils/formBuilder.js @@ -188,6 +188,7 @@ export default function formBuilder( ) { let target = row ? row : this.form; let formItem; + const linkageDisabled = !!info._linkageDisabled; //下拉选项 let options = []; if (info?.selection_model) { @@ -207,6 +208,7 @@ export default function formBuilder( formItem = h("el-input", { props: { value: target[info.name], + disabled: linkageDisabled, clearable: true, placeholder: info.help_text, }, @@ -228,6 +230,7 @@ export default function formBuilder( minRows: 2, }, value: target[info.name], + disabled: linkageDisabled, clearable: true, placeholder: info.help_text, }, @@ -248,6 +251,7 @@ export default function formBuilder( "value-format": "yyyy-MM-dd", format: "yyyy年MM月dd日", value: target[info.name], + disabled: linkageDisabled, clearable: true, editable:false, placeholder: info.help_text, @@ -275,6 +279,7 @@ export default function formBuilder( "value-format": "yyyy-MM-dd HH:mm:ss", format: "yyyy-MM-dd HH:mm", value: target[info.name], + disabled: linkageDisabled, clearable: true, editable:false, placeholder: info.help_text, @@ -318,6 +323,7 @@ export default function formBuilder( { props: { value: getSelectValue(), + disabled: linkageDisabled, clearable: true, placeholder: info.help_text, multiple: !!info.multiple, @@ -387,6 +393,7 @@ export default function formBuilder( { props: { value: radioParent, + disabled: linkageDisabled, }, attrs: { placeholder: info.help_text, @@ -1613,6 +1620,9 @@ export default function formBuilder( prop: info.name, label: info.label_show ? info.label : "", "label-width": !info.label_show ? "0" : "", + // 仅让当前被联动禁用的字段跳过校验;不要改动 el-form + // 的整套 rules,以免触发其它字段的全量校验。 + rules: linkageDisabled ? [] : undefined, }, style: { // +1为了工作标题往下顺延 @@ -1761,6 +1771,7 @@ export default function formBuilder( props: { name: info.name, value: target[info.name], + disabled: linkageDisabled, clearable: true, placeholder: info.help_text || info.label, }, @@ -1778,6 +1789,7 @@ export default function formBuilder( name: info.name, rows: 2, type: "textarea", + disabled: linkageDisabled, value: target[info.name], clearable: true, placeholder: info.help_text || info.label, @@ -1826,6 +1838,7 @@ export default function formBuilder( { props: { value: mRadioParent, + disabled: linkageDisabled, }, on: { input: onMParentChange, @@ -1903,6 +1916,7 @@ export default function formBuilder( "value-format": "yyyy-MM-dd", format: "yyyy年MM月dd日", value: target[info.name], + disabled: linkageDisabled, clearable: true, editable:false, placeholder: info.help_text || info.label, @@ -1930,6 +1944,7 @@ export default function formBuilder( "value-format": "yyyy-MM-dd HH:mm:ss", format: "yyyy-MM-dd HH:mm", value: target[info.name], + disabled: linkageDisabled, clearable: true, editable:false, placeholder: info.help_text || info.label, @@ -1992,6 +2007,7 @@ export default function formBuilder( h('van-field', { props: { value: getDetailMobileSelectValue(), + disabled: linkageDisabled, clickable: true, readonly: true, 'label-width': 0, @@ -1999,6 +2015,7 @@ export default function formBuilder( }, on: { click: _ => { + if (linkageDisabled) return; this.$refs[`popup-${info.name}-${row ? 'c' : 'b'}`].show() } } diff --git a/src/views/flow/DesktopForm.vue b/src/views/flow/DesktopForm.vue index f03f246..d6f8955 100644 --- a/src/views/flow/DesktopForm.vue +++ b/src/views/flow/DesktopForm.vue @@ -6,6 +6,7 @@ import request from '@/utils/request' import moment from "moment/moment"; import {defaultModalSize} from "@/settings"; import { getToken } from '@/utils/auth' +import { evaluateFieldLinkage } from "@/utils/fieldLinkage"; export default { props: { config: { @@ -295,7 +296,18 @@ export default { } }, }, - computed: {}, + computed: { + linkage() { + return evaluateFieldLinkage({ + fields: this.fields, + form: this.form, + writeable: this.writeable, + }); + }, + linkageDisabledFieldNames() { + return Array.from(this.linkage.disabledFieldNames); + }, + }, watch: { originalForm(newVal) { this.form = deepCopy(newVal) @@ -318,6 +330,12 @@ export default { }, deep: true }, + linkageDisabledFieldNames(newNames, oldNames) { + const previous = new Set(oldNames || []); + const newlyDisabled = (newNames || []).filter((name) => !previous.has(name)); + if (!newlyDisabled.length) return; + this.$nextTick(() => this.$refs.elForm?.clearValidate(newlyDisabled)); + }, scriptContent(newVal) { if (newVal) { try { @@ -367,6 +385,7 @@ export default { _readable: this.readable.indexOf(field.id) !== -1 || field.type === "label", _writeable: this.writeable.indexOf(field.id) !== -1, + _linkageDisabled: this.linkage.disabledFieldIds.has(Number(field.id)), })); console.log("authFields",authFields) if (this.needFlowTitle) { @@ -396,7 +415,10 @@ export default { model: this.form, "label-position": "right", "label-width": this.device === 'mobile' ? "80px" : "130px", + // 联动只在字段项覆盖自身校验规则,避免切换 disabled 时变更 + // 整个 rules 对象而触发 Element UI 的全表 validate-on-rule-change。 rules: this.rules, + "validate-on-rule-change": false, "inline-message": true, }, }, diff --git a/src/views/flow/MobileForm.vue b/src/views/flow/MobileForm.vue index 50f3863..49c1d24 100644 --- a/src/views/flow/MobileForm.vue +++ b/src/views/flow/MobileForm.vue @@ -6,6 +6,7 @@ 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 @@ -235,7 +236,15 @@ export default { } }, }, - computed: {}, + computed: { + linkage() { + return evaluateFieldLinkage({ + fields: this.fields, + form: this.form, + writeable: this.writeable, + }); + }, + }, watch: { info(newVal) { let keys = newVal.map(i => i.name) @@ -298,7 +307,8 @@ export default { const authFields = this.fields.map(field => ({ ...field, _readable: this.readable.indexOf(field.id) !== -1, - _writeable: this.writeable.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) { diff --git a/src/views/flow/components/FlowLogReorderDialog.vue b/src/views/flow/components/FlowLogReorderDialog.vue new file mode 100644 index 0000000..4345a27 --- /dev/null +++ b/src/views/flow/components/FlowLogReorderDialog.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/src/views/flow/create.vue b/src/views/flow/create.vue index 8c4b775..2daf219 100644 --- a/src/views/flow/create.vue +++ b/src/views/flow/create.vue @@ -553,6 +553,19 @@ size="small" @click="openInsertHistoryLogDialog()" >新增节点 + 调整顺序 + 校验时序合理性 + +
+ + 此处仅调整流程图和流转记录的展示顺序,不修改实际审批时间、当前待办或流程连线。 +
+
+ 开始节点固定展示不可拖拽;拖拽手柄调整其余记录。 +
+ +
+
+ + {{ issue.message }} +
+
+
+
+
+ +
+
+ +
+
{{ index + 1 }}
+
+ +
+
会签 {{ unit.logs.length }} 人
+
+
+
暂无可调整的流转记录
+
+ + +
+ +
+ - - - @@ -1221,16 +1291,20 @@ import { fieldConfig, flowList, deleteFlowLog, freeAssignPre, + flowLogReorderMeta, insertHistoryLog, preConfig, preDeal, updateNodeTime, updateFlowLogMeta, + validateFlowLogOrder, + reorderFlowLogs, view, } from "@/api/flow"; import { getPaymentsByFlowId, getPaymentsByContractId, getBudgetContractDetail, getPlannedExpenditure, getPlannedExpenditureTemplatesByCategory, getPaymentCategoryTemplateElements, getDetailTableFields, getOaFlowDetails, getTemplateElementDetail } from "@/api/payment"; import { show as meetingMinutesShow } from "@/api/meetingMinutes"; import { deepCopy } from "@/utils"; +import { buildFlowLogReorderUnits, flattenFlowLogReorderUnits } from '@/utils/flowLogReorder' import { validation, validationName } from "@/utils/validate"; import { print } from "@/utils/print"; import JSONBigint from 'json-bigint' @@ -1268,9 +1342,18 @@ export default { status: 1, created_at: '', updated_at: '', - sort: 1, reason: '', }, + isShowReorderFlowLog: false, + reorderSubmitting: false, + reorderValidating: false, + reorderRevision: '', + reorderOriginalUnits: [], + reorderUnits: [], + reorderAutoNormalized: false, + reorderIssues: [], + reorderReason: '', + reorderDragIndex: null, isShowUserDialog: false, selectedUserId: '', userList: [], @@ -1291,6 +1374,7 @@ export default { info: [], config: {}, writeableFields: [], + nodeRequiredFields: [], readableFields: [], subConfig: new Map(), myStatus: new Map([ @@ -1760,8 +1844,12 @@ export default { generateForm(object, fields, relation = false, pname) { fields.forEach((field) => { - if (field.rules && field.rules.length > 0 && this.writeableFields.find(i => i === field.id) && !relation) { - this.rules[field.name] = field.rules.map((rule) => { + const fieldRules = Array.isArray(field.rules) ? [...field.rules] : [] + if (this.nodeRequiredFields.includes(field.id) && !fieldRules.includes('required')) { + fieldRules.push('required') + } + if (fieldRules.length > 0 && this.writeableFields.find(i => i === field.id) && !relation) { + this.rules[field.name] = fieldRules.map((rule) => { switch (rule) { case "required": if (field.type === 'relation') { @@ -2691,6 +2779,7 @@ export default { ? this.fields?.map((i) => i.id) : this.config?.currentNode?.readable || []; this.writeableFields = this.config?.currentNode?.writeable || []; + this.nodeRequiredFields = this.config?.currentNode?.required || []; // 生成空的form表单对象 this.generateForm(this.form, fields); // 加载关联流程ID(如果有) @@ -2783,6 +2872,7 @@ export default { ? this.fields?.map((i) => i.id) : this.config?.currentNode?.readable || []; this.writeableFields = this.config?.currentNode?.writeable || []; + this.nodeRequiredFields = this.config?.currentNode?.required || []; this.generateForm(this.form, fields); this.handleDefaultJSON(); this.form = Object.assign({}, this.form); @@ -2820,6 +2910,7 @@ export default { ? this.fields?.map((i) => i.id) : this.config?.currentNode?.readable || []; this.writeableFields = this.config?.currentNode?.writeable || []; + this.nodeRequiredFields = this.config?.currentNode?.required || []; this.generateForm(this.form, fields); // 加载关联流程ID(如果有) if (res?.flow?.related_flows && res.flow.related_flows.length > 0) { @@ -3033,6 +3124,184 @@ export default { this.openEditLogDialog(row) } }, + formatFlowLogDate(value) { + return value ? this.$moment(value).format('YYYY-MM-DD HH:mm:ss') : '—' + }, + reorderOrderedLogIds() { + return flattenFlowLogReorderUnits(this.reorderUnits) + }, + async openReorderFlowLogDialog() { + try { + const result = await flowLogReorderMeta({ flow_id: Number(this.$route.query.flow_id) }) + const persisted = buildFlowLogReorderUnits(result.logs || [], false) + const draft = buildFlowLogReorderUnits(result.logs || []) + this.reorderRevision = result.revision || '' + this.reorderOriginalUnits = deepCopy(persisted) + this.reorderUnits = deepCopy(draft) + this.reorderAutoNormalized = JSON.stringify(persisted.map((unit) => unit.key)) !== JSON.stringify(draft.map((unit) => unit.key)) + this.reorderIssues = [] + this.reorderReason = '' + this.isShowReorderFlowLog = true + await this.validateReorderFlowLog() + } catch (err) { + console.error(err) + } + }, + resetReorderFlowLogDialog() { + this.reorderSubmitting = false + this.reorderValidating = false + this.reorderRevision = '' + this.reorderOriginalUnits = [] + this.reorderUnits = [] + this.reorderAutoNormalized = false + this.reorderIssues = [] + this.reorderReason = '' + this.reorderDragIndex = null + }, + restoreReorderFlowLog() { + this.reorderUnits = deepCopy(this.reorderOriginalUnits) + this.reorderAutoNormalized = false + this.reorderIssues = [] + this.validateReorderFlowLog() + }, + handleReorderDragStart(index, event) { + if (this.reorderUnits[index]?.locked) { + event.preventDefault() + return + } + this.reorderDragIndex = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/plain', String(index)) + }, + handleReorderDrop(targetIndex) { + const sourceIndex = this.reorderDragIndex + this.reorderDragIndex = null + if (sourceIndex === null || sourceIndex === targetIndex || this.reorderUnits[targetIndex]?.locked) return + const [unit] = this.reorderUnits.splice(sourceIndex, 1) + const adjustedTarget = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex + this.reorderUnits.splice(adjustedTarget, 0, unit) + this.reorderIssues = [] + }, + showReorderValidationResult(issues = []) { + const errors = issues.filter((issue) => issue.level === 'error') + const warnings = issues.filter((issue) => issue.level === 'warning') + if (!errors.length && !warnings.length) { + this.$message.success('时序校验通过') + return + } + const h = this.$createElement + const renderIssueGroup = (title, list, color) => list.length + ? h('div', { style: { marginTop: '12px' } }, [ + h('div', { style: { fontWeight: 600, color, marginBottom: '8px' } }, title), + h('ol', { style: { margin: 0, paddingLeft: '22px' } }, list.map((issue) => h('li', { + style: { + marginBottom: '8px', + lineHeight: '22px', + color: '#606266', + }, + }, issue.message))), + ]) + : null + const content = h('div', { style: { maxWidth: '680px' } }, [ + h('div', { + style: { + marginBottom: '4px', + lineHeight: '22px', + color: '#606266', + }, + }, errors.length ? '存在需要处理的错误,请调整后再保存。' : '未发现阻断错误,以下提示仅供复核。'), + renderIssueGroup(`错误 ${errors.length} 项`, errors, '#f56c6c'), + renderIssueGroup(`提示 ${warnings.length} 项`, warnings, '#e6a23c'), + ].filter(Boolean)) + this.$alert(content, `时序校验:${errors.length}项错误,${warnings.length}项提示`, { + confirmButtonText: '知道了', + closeOnClickModal: false, + dangerouslyUseHTMLString: false, + }) + }, + async validateCurrentFlowLogOrder(options = {}) { + const flowId = Number(this.$route.query.flow_id) + if (!flowId) return null + this.reorderValidating = true + try { + let orderedLogIds = [] + let revision = '' + const useDraft = this.isShowReorderFlowLog && this.reorderUnits.length && this.reorderRevision + if (useDraft) { + orderedLogIds = this.reorderOrderedLogIds() + revision = this.reorderRevision + } else { + const meta = await flowLogReorderMeta({ flow_id: flowId }) + orderedLogIds = flattenFlowLogReorderUnits(buildFlowLogReorderUnits(meta.logs || [], false)) + revision = meta.revision || '' + } + if (!orderedLogIds.length || !revision) return null + const result = await validateFlowLogOrder({ + flow_id: flowId, + ordered_log_ids: orderedLogIds, + revision, + }) + if (useDraft) { + this.reorderIssues = result.issues || [] + if (result.revision) this.reorderRevision = result.revision + } + if (!options.silent) this.showReorderValidationResult(result.issues || []) + return result + } catch (err) { + console.error(err) + return null + } finally { + this.reorderValidating = false + } + }, + async validateReorderFlowLog(options = {}) { + return this.validateCurrentFlowLogOrder({ ...options, silent: options.silent !== undefined ? options.silent : true }) + }, + reorderUnitIssues(unit) { + const ids = new Set(unit.logs.map((log) => Number(log.id))) + return this.reorderIssues.filter((issue) => (issue.log_ids || []).some((id) => ids.has(Number(id)))) + }, + reorderUnitLevel(unit) { + const issues = this.reorderUnitIssues(unit) + if (issues.some((issue) => issue.level === 'error')) return 'error' + if (issues.some((issue) => issue.level === 'warning')) return 'warning' + return 'success' + }, + reorderUnitValidationText(unit) { + const issues = this.reorderUnitIssues(unit) + return issues.length ? issues.map((issue) => issue.message).join('\n') : '时序校验通过' + }, + async submitReorderFlowLog() { + await this.validateReorderFlowLog() + if (this.reorderErrorCount > 0) return + try { + await this.$confirm( + `本次将调整 ${this.reorderChangedCount} 个位置;排序只影响展示顺序,不改变实际审批时间和当前待办。`, + '确认保存排序', + { type: 'warning', closeOnClickModal: false } + ) + } catch (err) { + return + } + this.reorderSubmitting = true + try { + const result = await reorderFlowLogs({ + flow_id: Number(this.$route.query.flow_id), + ordered_log_ids: this.reorderOrderedLogIds(), + revision: this.reorderRevision, + reason: this.reorderReason.trim(), + }) + this.$message.success('展示顺序已保存') + this.isShowReorderFlowLog = false + if (Array.isArray(result.logs)) this.config.logs = result.logs + await this.getConfig() + await this.validateCurrentFlowLogOrder() + } catch (err) { + console.error(err) + } finally { + this.reorderSubmitting = false + } + }, resetInsertHistoryLogForm(defaults = {}) { const normalizedDefaults = { ...defaults, @@ -3082,7 +3351,6 @@ export default { flow_node_id: defaults.flow_node_id === '' || defaults.flow_node_id === undefined || defaults.flow_node_id === null ? '' : Number(defaults.flow_node_id), user_id: defaults.user_id === '' || defaults.user_id === undefined || defaults.user_id === null ? '' : Number(defaults.user_id), status: defaults.status === '' || defaults.status === undefined || defaults.status === null ? 1 : Number(defaults.status), - sort: defaults.sort === '' || defaults.sort === undefined || defaults.sort === null ? 1 : Number(defaults.sort), }; this.editFlowLogSubmitting = false; this.editFlowLogForm = { @@ -3093,7 +3361,6 @@ export default { status: 1, created_at: '', updated_at: '', - sort: 1, reason: '', ...normalizedDefaults, }; @@ -3114,7 +3381,6 @@ export default { status: latestRow.status, created_at: latestRow.created_at, updated_at: latestRow.updated_at, - sort: latestRow.sort || 1, reason: latestRow.reason || '', }); this.isShowTime = true; @@ -3161,7 +3427,6 @@ export default { status: Number(this.editFlowLogForm.status), created_at: this.editFlowLogForm.created_at, updated_at: this.editFlowLogForm.updated_at, - sort: Number(this.editFlowLogForm.sort), reason: this.editFlowLogForm.status < 0 ? (this.editFlowLogForm.reason || '') : '', }); if (Array.isArray(res?.logs)) { @@ -3263,13 +3528,22 @@ export default { }, }, computed: { + reorderErrorCount() { + return this.reorderIssues.filter((issue) => issue.level === 'error').length + }, + reorderWarningCount() { + return this.reorderIssues.filter((issue) => issue.level === 'warning').length + }, + reorderChangedCount() { + const original = flattenFlowLogReorderUnits(this.reorderOriginalUnits) + const current = flattenFlowLogReorderUnits(this.reorderUnits) + return current.reduce((count, id, index) => count + (original[index] !== id ? 1 : 0), 0) + }, canManageFlowLog() { const globalRoles = this.$store.state.user.roles || []; const moduleRoles = this.$store.state.user.moduleRoles?.oa || []; const adminId = Number(this.$store.state.user.adminId); - const username = this.$store.state.user.username; return adminId === 1 - || username === 'admin' || globalRoles.includes('全局流程监管') || moduleRoles.includes('全局流程监管'); }, @@ -3411,7 +3685,6 @@ export default { trigger: 'change', }, ], - sort: [{ required: true, message: '请输入排序', trigger: 'change' }], reason: [{ validator: (rule, value, callback) => { if (this.editFlowLogForm.status >= 0 || (value && String(value).trim())) { @@ -3679,6 +3952,66 @@ export default { ::v-deep .flow-detail-drawer > .el-drawer__body { overflow-y: auto; } + +.reorder-intro { + margin-bottom: 12px; + padding: 10px 12px; + color: #606266; + background: #f4f7fb; + border-radius: 4px; + line-height: 20px; +} +.reorder-intro i { color: #409eff; margin-right: 6px; } +.reorder-toolbar, +.reorder-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.reorder-toolbar { margin-bottom: 12px; color: #909399; font-size: 13px; } +.reorder-list { max-height: 460px; overflow-y: auto; padding: 2px; } +.reorder-card { + display: flex; + align-items: stretch; + min-height: 74px; + margin-bottom: 8px; + border: 1px solid #dcdfe6; + border-radius: 6px; + background: #fff; + transition: border-color .2s, box-shadow .2s; +} +.reorder-card:not(.is-locked) { cursor: grab; } +.reorder-card:not(.is-locked):hover { border-color: #409eff; box-shadow: 0 2px 8px rgba(64, 158, 255, .12); } +.reorder-card.is-dragging { opacity: .45; } +.reorder-card.is-locked { background: #fafafa; color: #909399; } +.reorder-drag-handle, +.reorder-validation-status, +.reorder-sequence { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} +.reorder-drag-handle { width: 38px; color: #909399; } +.reorder-validation-status { width: 28px; } +.reorder-validation-status.is-success { color: #67c23a; } +.reorder-validation-status.is-warning { color: #e6a23c; } +.reorder-validation-status.is-error { color: #f56c6c; } +.reorder-sequence { width: 34px; color: #409eff; font-weight: 600; border-right: 1px solid #ebeef5; } +.reorder-card-content { flex: 1; min-width: 0; padding: 10px 14px; } +.reorder-log-line { display: flex; align-items: center; flex-wrap: wrap; gap: 0 8px; line-height: 22px; } +.reorder-log-line strong { color: #303133; } +.reorder-log-line span { color: #606266; } +.reorder-log-line small { width: 100%; color: #909399; } +.reorder-group-tag { align-self: center; margin-right: 14px; padding: 2px 7px; color: #409eff; background: #ecf5ff; border-radius: 10px; font-size: 12px; } +.reorder-issues { margin-bottom: 12px; } +.reorder-issue { margin: 5px 0; padding: 7px 10px; border-radius: 4px; font-size: 13px; } +.reorder-issue.is-warning { color: #8a6d1f; background: #fdf6ec; } +.reorder-issue.is-error { color: #a94442; background: #fef0f0; } +.reorder-reason { margin-top: 14px; } +.reorder-reason .el-input { margin-top: 8px; } +.reorder-empty { padding: 36px; text-align: center; color: #909399; }