This commit is contained in:
那些花儿
2025-07-28 10:48:38 +08:00
parent 030cf7cc4c
commit 3b51ed7a51
450 changed files with 82310 additions and 1213 deletions
@@ -102,143 +102,6 @@
background-color: hsla(0, 0%, 60%, 0.05);
}
.ele-setting-wrapper {
padding: 20px 18px;
}
.ele-setting-wrapper .ele-setting-title {
font-size: 13px;
margin-bottom: 15px;
}
.ele-setting-wrapper .ele-setting-theme > div {
width: 52px;
height: 36px;
line-height: 1;
font-size: 18px;
border-radius: 3px;
margin: 0 20px 30px 0;
padding: 14px 0 0 24px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
display: inline-block;
vertical-align: top;
position: relative;
overflow: hidden;
cursor: pointer;
-webkit-transition: background-color 0.2s;
transition: background-color 0.2s;
}
.ele-setting-wrapper .ele-setting-theme > div:after,
.ele-setting-wrapper .ele-setting-theme > div:before,
.ele-setting-wrapper .ele-setting-theme > div > .ele-bg-primary {
content: "";
width: 100%;
height: 10px;
background: #fff;
position: absolute;
left: 0;
top: 0;
-webkit-transition: background-color 0.2s;
transition: background-color 0.2s;
}
.ele-setting-wrapper .ele-setting-theme > div:after {
width: 14px;
height: 100%;
}
.ele-setting-wrapper .ele-setting-theme > div.ele-head-dark:before,
.ele-setting-wrapper .ele-setting-theme > div.ele-layout-mix:after,
.ele-setting-wrapper .ele-setting-theme > div.ele-layout-mix:before,
.ele-setting-wrapper .ele-setting-theme > div.ele-side-dark:after {
background: #001529;
}
.ele-setting-wrapper .ele-setting-theme > div.ele-head-dark:before,
.ele-setting-wrapper .ele-setting-theme > div.ele-head-light:before,
.ele-setting-wrapper .ele-setting-theme > div > .ele-bg-primary {
z-index: 1;
}
.ele-setting-wrapper .ele-setting-theme > div.ele-layout-top {
padding-left: 19px;
}
.ele-setting-wrapper .ele-setting-theme > div.ele-layout-top:after {
display: none;
}
.ele-setting-wrapper .ele-setting-colors {
color: #fff;
margin-bottom: 20px;
}
.ele-setting-wrapper .ele-setting-colors .ele-setting-color-item {
width: 20px;
height: 20px;
line-height: 22px;
border-radius: 2px;
margin: 8px 8px 0 0;
display: inline-block;
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
vertical-align: top;
position: relative;
text-align: center;
cursor: pointer;
}
.ele-setting-wrapper .ele-setting-item {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin-bottom: 20px;
}
.ele-setting-wrapper .ele-setting-item .setting-item-title {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
line-height: 28px;
}
.ele-setting-wrapper .ele-setting-item .setting-item-control {
line-height: 1;
}
.ele-setting-wrapper .el-divider {
margin-bottom: 20px;
}
.ele-setting-wrapper .el-alert + .ele-setting-button-group {
margin-top: 15px;
}
.ele-setting-dark .ele-setting-theme > div {
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
}
.ele-setting-dark .ele-setting-theme > div:after,
.ele-setting-dark .ele-setting-theme > div:before,
.ele-setting-dark .ele-setting-theme > div > .ele-bg-primary {
background: #1f1f1f;
}
.ele-setting-dark .ele-setting-theme > div.ele-head-dark:before,
.ele-setting-dark .ele-setting-theme > div.ele-layout-mix:after,
.ele-setting-dark .ele-setting-theme > div.ele-layout-mix:before,
.ele-setting-dark .ele-setting-theme > div.ele-side-dark:after {
background: #262626;
}
.ele-setting-color-picker.el-color-picker {
height: auto;
margin-top: 8px;
@@ -318,66 +181,6 @@
right: 20px;
}
.ele-admin-collapse .ele-admin-sidebar-menus > .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view > .el-menu > .el-menu-item .ele-menu-badge,
.ele-admin-collapse
.ele-admin-sidebar-menus
> .el-scrollbar
> .el-scrollbar__wrap
> .el-scrollbar__view
> .el-menu
> .el-submenu
> .el-submenu__title
.ele-menu-badge {
top: 2px;
right: 2px;
margin: 0;
}
.ele-admin-header-nav > .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view > .el-menu > .el-menu-item .ele-menu-badge,
.ele-admin-header-nav > .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view > .el-menu > .el-submenu > .el-submenu__title .ele-menu-badge {
position: static;
right: auto;
top: auto;
display: inline-block;
vertical-align: 6px;
margin: 0 0 0 2px;
}
.ele-admin-sidebar-nav-menu > .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view > .el-menu > .el-menu-item .ele-menu-badge,
.ele-admin-sidebar-nav-menu
> .el-scrollbar
> .el-scrollbar__wrap
> .el-scrollbar__view
> .el-menu
> .el-submenu
> .el-submenu__title
.ele-menu-badge {
top: 0;
right: 0;
margin: 0;
}
.ele-admin-nav-collapse
.ele-admin-sidebar-nav-menu
> .el-scrollbar
> .el-scrollbar__wrap
> .el-scrollbar__view
> .el-menu
> .el-menu-item
.ele-menu-badge,
.ele-admin-nav-collapse
.ele-admin-sidebar-nav-menu
> .el-scrollbar
> .el-scrollbar__wrap
> .el-scrollbar__view
> .el-menu
> .el-submenu
> .el-submenu__title
.ele-menu-badge {
top: 0;
right: 0;
}
@media only screen and (max-width: 767px) {
.hidden-xs-only {
display: none !important;
@@ -24318,18 +24121,6 @@ s:not([class]) {
flex-wrap: wrap;
}
.ele-body {
padding: 15px;
}
.ele-body.ele-body-card {
padding-bottom: 0;
}
.ele-body.ele-body-card .el-card {
margin-bottom: 15px;
}
.ele-bottom-tool {
position: fixed;
bottom: 0;
@@ -648,46 +648,76 @@ td.no-b {
width: auto;
}
.form-box{
.form-box {
width: 1200px;
margin: 0 auto;
}
.descriptions-form .el-form-item {
margin-bottom: 0 !important;
}
/**********start****************设置工作流表单el-description嵌套el-form的格式 必须要使用flow-task-form!!!***********************start*****************/
.el-descriptions__table {
.flow-task-form .el-descriptions__table {
table-layout: fixed !important;
}
.el-descriptions-item__label {
text-align: center !important;
width: 14% !important;
}
.descriptions-form th.el-descriptions-item__label:has(+ td.el-descriptions-item__content > div.el-form-item.is-required):before {
content: "*";
color: #f56c6c;
margin-right: 3px;
}
.descriptions-form .el-form-item .el-form-item__label {
.flow-task-form .el-form-item .el-form-item__label {
display: none;
}
.descriptions-form .el-form-item .el-form-item__content > div {
width: 100%;
.flow-task-form .el-descriptions-item__label.is-bordered-label {
color: rgb(102, 102, 102) !important;
font-weight: bold !important;
background: rgb(248, 249, 250) !important;
padding: 8px 12px !important;
border: 1px solid rgb(221, 221, 221) !important;
text-align: center !important;
}
.flow-task-form .el-descriptions .is-bordered .el-descriptions-item__cell {
border: 1px solid rgb(221, 221, 221) !important;
}
.flow-task-form .el-descriptions-item__content {
font-weight: 600;
}
.descriptions-form .el-form-item input, .descriptions-form .el-form-item textarea {
.flow-task-form .el-form-item .el-form-item__content > div {
width: 100%;
}
.flow-task-form .el-descriptions-item__cell.el-descriptions-item__content .el-form-item {
margin-bottom: 0 !important;
}
/**********start****************设置工作流表单el-description嵌套el-form的格式 必填项加星号***********************start*****************/
.flow-task-form th.el-descriptions-item__label:has(+ td.el-descriptions-item__content > div.el-form-item.is-required):before {
content: "*";
color: #ff0202;
margin-right: 3px;
}
/**/
.flow-task-form .el-form-item input, .flow-task-form .el-form-item textarea, .flow-task-form .el-form-item .el-radio.is-bordered {
/*border: none;*/
border-radius: unset;
border: 1px solid rgb(221, 221, 221);
}
.descriptions-form .el-form-item .el-date-editor {
.flow-task-form .el-form-item .el-radio.is-bordered {
border: 1px solid rgb(221, 221, 221);
}
.flow-task-form .el-form-item .el-radio.is-bordered.is-checked {
border-color: var(--color-primary);
}
.flow-task-form .el-form-item .el-date-editor {
width: 100%;
}
/*********END********设置工作流表单el-description嵌套el-form的格式**************************END*********/
@@ -0,0 +1,138 @@
:root {
--color-primary: #006db9; /* 主色 */
--color-primary-1: #e6f3ff; /* 最浅,用于浅色背景 */
--color-primary-2: #b3d9ff; /* 较浅,用于 hover/light-bg */
--color-primary-3: #80c0ff; /* 轻微强调 */
--color-primary-4: #4da6ff; /* 中等强调 */
--color-primary-5: #1a8cff; /* 较强强调 */
--color-primary-6: #006db9; /* 主色(默认) */
--color-primary-7: #005aa3; /* 点击/active 状态 */
--color-primary-8: #00468c; /* 较深,用于文字/强调 */
--color-primary-9: #003370; /* 更深,用于深色模式 */
--color-primary-10: #002054; /* 最深,用于特殊强调 */
--color-success: #52c41a;
--color-success-1: #f6ffed;
--color-success-2: #d9f7be;
--color-success-3: #b7eb8f;
--color-success-4: #95de64;
--color-success-5: #73d13d;
--color-success-6: #52c41a;
--color-success-7: #389e0d;
--color-success-8: #237804;
--color-success-9: #135200;
--color-success-10: #092b00;
--color-warning: #faad14;
--color-warning-1: #fffbe6;
--color-warning-2: #fff1b8;
--color-warning-3: #ffe58f;
--color-warning-4: #ffd666;
--color-warning-5: #ffc53d;
--color-warning-6: #faad14;
--color-warning-7: #d48806;
--color-warning-8: #ad6800;
--color-warning-9: #874d00;
--color-warning-10: #613400;
--color-danger: #ff4d4f;
--color-danger-1: #fff1f0;
--color-danger-2: #ffccc7;
--color-danger-3: #ffa39e;
--color-danger-4: #ff7875;
--color-danger-5: #ff4d4f;
--color-danger-6: #f5222d;
--color-danger-7: #cf1322;
--color-danger-8: #a8071a;
--color-danger-9: #820014;
--color-danger-10: #5c0011;
--color-info: #909399;
--color-info-1: #f4f4f5;
--color-info-2: #dedfe0;
--color-info-3: #d3d4d6;
--color-info-4: #bcbec2;
--color-info-5: #a6a9ad;
--color-info-6: #909399;
--color-info-7: #6e7074;
--color-info-8: #58595c;
--color-info-9: #414244;
--color-info-10: #2a2b2b;
--color-white: #fff;
--color-white-5: hsla(0, 0%, 100%, 0.5);
--color-black: #000;
--color-black-1: rgba(0, 0, 0, 0.1);
--color-black-3: rgba(0, 0, 0, 0.3);
--color-black-5: rgba(0, 0, 0, 0.5);
--color-text-primary: #303133;
--color-text-regular: #606266;
--color-text-secondary: #909399;
--color-text-placeholder: #c0c4cc;
--color-text-dark-primary: #fff;
--color-text-dark-regular: hsla(0, 0%, 100%, 0.8);
--border-color-base: #ddd;
--border-color-light: #e5e5e5;
--border-color-lighter: #ededed;
--border-color-extra-light: #f4f4f4;
--background-color-base: #fafafa;
--box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
--box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
--box-shadow-light: 0 4px 12px 0 rgba(0, 0, 0, 0.15);
--checkbox-background-color: var(--color-white);
--radio-input-background-color: var(--color-white);
--radio-checked-icon-color: var(--color-white);
--input-background-color: var(--color-white);
--button-default-background-color: var(--color-white);
--button-disabled-background-color: var(--color-white);
--button-default-border-color-5: hsla(0, 0%, 87%, 0.5);
--table-header-background-color: var(--background-color-base);
--table-fixed-box-shadow: 0 0 10px rgba(0, 0, 0, 0.12);
--popup-modal-background-color: #000;
--popup-modal-opacity: 0.5;
--popover-background-color: var(--color-white);
--dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--tooltip-fill: var(--color-text-primary);
--tooltip-color: var(--color-text-dark-primary);
--tooltip-border-color: var(--tooltip-fill);
--scrollbar-background-color: hsla(0, 0%, 67%, 0.5);
--scrollbar-hover-background-color: hsla(0, 0%, 67%, 0.8);
--backtop-background-color: var(--color-white);
--backtop-font-color: var(--color-primary);
--backtop-hover-background-color: var(--border-color-extra-light);
--avatar-background-color: #c0c4cc;
--skeleton-color: #f2f2f2;
--skeleton-to-color: #e6e6e6;
--svg-monochrome-grey: #dcdde0;
--loading-mask-bg: hsla(0, 0%, 100%, 0.9);
--menu-dark-background: #191a23;
--layout-body-background: #f0f2f5;
--sidebar-background-light: var(--color-white);
--sidebar-background-dark: var(--menu-dark-background);
--sidebar-light-shadow: 1px 3px 3px rgba(0, 21, 41, 0.08);
--sidebar-dark-shadow: 0 4px 4px rgba(0, 0, 0, 0.35);
--header-background: var(--menu-dark-background);
--header-light-background: var(--color-white);
--header-light-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
--header-dark-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
--header-tool-hover-bg: rgba(0, 0, 0, 0.025);
--header-dark-tool-hover-bg: hsla(0, 0%, 100%, 0.05);
--logo-light-shadow: 1px 2px 3px rgba(0, 21, 41, 0.08);
--logo-dark-shadow: 0 3px 4px rgba(0, 0, 0, 0.35);
--content-image-filter: none;
--el-mask-color: rgba(255, 255, 255, 0.4);
--el-mask-color-extra-light: rgba(255, 255, 255, 0.6);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f5f7fa;
color: #333;
font-size: 14px;
height: 100%;
}
@@ -38,32 +38,32 @@ $.ajaxSetup({
})
//初始化pjax
;(function initPjax() {
if ($.support.pjax) {
$(document).pjax("a[data-pjax]", "#container", {
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#container",
timeout: 8000
})
$(document).on("pjax:send", function () {
NProgress.start()
$(".ele-admin-content-view").hide()
window.customStyleList = document.querySelectorAll('style[id^="style-"]')
})
$(document).on("pjax:complete", function () {
if (window.customStyleList) {
window.customStyleList.forEach((style) => {
style.parentNode.removeChild(style)
})
}
NProgress.done()
$(".ele-admin-content-view").show()
})
}
})()
// ;(function initPjax() {
// if ($.support.pjax) {
// $(document).pjax("a[data-pjax]", "#container", {
// maxCacheLength: 0,
// push: false,
// replace: true,
// fragment: "#container",
// timeout: 8000
// })
// $(document).on("pjax:send", function () {
// NProgress.start()
// $(".ele-admin-content-view").hide()
// window.customStyleList = document.querySelectorAll('style[id^="style-"]')
// })
//
// $(document).on("pjax:complete", function () {
// if (window.customStyleList) {
// window.customStyleList.forEach((style) => {
// style.parentNode.removeChild(style)
// })
// }
// NProgress.done()
// $(".ele-admin-content-view").show()
// })
// }
// })()
function pjaxReplace(url, data = {}) {
const { pathname, search } = location
@@ -17,7 +17,7 @@ const commonUtil = {
if (response.config.responseType === "blob") {
return response
}
if (response.data && response.data.code !== 0) {
if (response.data && response.data.code !== 0 && response.data.code !== 200) {
if (isMobile) {
vant.Toast(response.data.msg)
} else {
@@ -25,7 +25,7 @@ const commonUtil = {
}
return Promise.reject(response.data)
}
return response.data
return response.data || response.result
},
(error) => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera
@@ -217,6 +217,7 @@ const commonUtil = {
function doSm2Encrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
}
// SM2数组加密
function doSm2ArrayEncrypt(msgString) {
return sm2.doEncrypt(msgString, publicKey, cipherMode)
@@ -1450,7 +1450,7 @@
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
background-color: #ecf5ff;
color: #66b1ff;
color: var(--color-primary-6);
}
.el-dropdown-menu__item i {
margin-right: 5px;
@@ -2768,7 +2768,7 @@
}
.el-table-filter__list-item:hover {
background-color: #ecf5ff;
color: #66b1ff;
color: var(--color-primary-6);
}
.el-table-filter__list-item.is-active {
background-color: var(--color-primary);
@@ -9789,7 +9789,7 @@
}
.el-tag--dark .el-tag__close:hover {
color: #fff;
background-color: #66b1ff;
background-color: var(--color-primary-6);
}
.el-tag--dark.el-tag--info {
background-color: #909399;
@@ -10963,8 +10963,8 @@
}
.el-button--primary:focus,
.el-button--primary:hover {
background: #66b1ff;
border-color: #66b1ff;
background: var(--color-primary-6);
border-color: var(--color-primary-6);
color: #fff;
}
.el-button--primary.is-active,
@@ -11266,7 +11266,7 @@
}
.el-button--text:focus,
.el-button--text:hover {
color: #66b1ff;
color: var(--color-primary-6);
border-color: transparent;
background-color: transparent;
}
@@ -11785,7 +11785,7 @@
color: var(--color-primary);
}
.el-link.el-link--primary:hover {
color: #66b1ff;
color: var(--color-primary-6);
}
.el-link.el-link--primary.is-disabled {
color: #a0cfff;
@@ -0,0 +1,107 @@
@charset "UTF-8";
/* 数据量大与遮罩交互冲突优化 */
.v-modal.v-modal-leave {
display: none;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 6px;
border: 2px solid transparent;
background-color: var(--scrollbar-background-color);
background-clip: padding-box;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
.el-dialog__header {
padding: 15px 20px 15px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color-lighter);
}
.el-dialog__title {
flex: 1;
font-size: 16px;
margin: 0;
color: rgba(0, 0, 0, 0.88);
font-weight: 600;
line-height: 1.5;
word-break: break-word;
}
.el-dialog__headerbtn{
top: auto;
right: auto;
line-height: 1;
font-size: 18px;
position: static;
}
.el-dialog__footer {
padding-bottom: 12px;
border-top: 1px solid var(--border-color-lighter);
}
.el-table thead {
color: var(--color-text-primary);
font-weight: 500;
}
.el-table td.el-table__cell div {
color: var(--color-text-primary);
}
.el-card {
border: none;
}
.el-card + .el-card {
margin-top: 15px;
}
.el-message:not(.ele-message-border){
background: var(--popover-background-color);
-webkit-box-shadow: var(--box-shadow-light);
box-shadow: var(--box-shadow-light);
border-radius: 2px;
border: none;
}
.el-message {
min-width: auto;
position: fixed;
}
.el-message .el-message__icon {
font-size: 18px;
}
.el-message .el-icon-success {
color: var(--color-success);
}
.el-message:not(.ele-message-border) .el-message__content {
color: inherit;
}
.el-message .el-message__content {
line-height: 18px;
}
.el-button{
border-radius: 2px;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,648 @@
<script>
module.exports = {
name: "formButton",
props: {
task_id: {
type: String,
required: true
},
// 是否显示审核意见框
show_comment: {
type: Boolean,
default: true
},
// 自定义按钮配置
custom_buttons: {
type: Array,
default: () => []
},
// 是否显示流程图
show_process_diagram: {
type: Boolean,
default: true
},
// 是否显示办理记录
show_process_records: {
type: Boolean,
default: true
},
// 办理记录展示模式:timeline, table
record_display_mode: {
type: String,
default: "timeline",
validator: (value) => {
return ["timeline", "table"].includes(value)
}
},
// 业务ID,用于获取流程记录
business_id: {
type: String,
default: ""
}
},
data() {
return {
loading: false,
comment: "", // 审核意见
nextNodeInfo: null, // 下一节点信息
currentTaskName: "部门审核", // 当前办理节点名称
availableActions: [], // 可用操作
sequenceFlows: [], // 目标节点
selectedAction: "", // 选中的操作类型
rejectTaskOptions: [], // 驳回到指定节点的选项
selectedRejectTask: "", // 选中的驳回节点
assigneeOptions: [], // 执行人选项
selectedAssignee: "", // 选中的执行人
// 控制显示
showProcessDiagram: true, // 是否显示流程图
showProcessRecords: true, // 是否显示办理记录
recordDisplayMode: 'timeline', // 办理记录展示模式:timeline, table
// 模拟数据 - 办理记录
mockProcessRecords: [
{
nodeName: "发起申请",
userName: "张三",
time: "2023-05-10 09:30:00",
action: "提交申请",
comment: "请审批",
type: "primary"
},
{
nodeName: "部门审核",
userName: "李四",
time: "2023-05-10 11:20:00",
action: "处理中",
comment: "",
type: "warning"
}
]
}
},
created() {
if (this.task_id) {
this.getNextNodeInfo()
}
// 初始化显示状态
this.showProcessDiagram = this.show_process_diagram
this.showProcessRecords = this.show_process_records
this.recordDisplayMode = this.record_display_mode
// 模拟获取流程记录
this.mockGetProcessRecords()
},
watch: {
// 监听props变化
show_process_diagram(val) {
this.showProcessDiagram = val
},
show_process_records(val) {
this.showProcessRecords = val
},
record_display_mode(val) {
this.recordDisplayMode = val
},
business_id: {
handler(val) {
if (val) {
// 清空现有记录
this.mockProcessRecords = []
// 重新获取流程记录
this.mockGetProcessRecords()
}
},
immediate: true
}
},
methods: {
// 获取下一节点信息
getNextNodeInfo() {
this.loading = true
this.$axios
.post("/easy-flowable/task/nextNodeVariables/" + this.task_id)
.then((res) => {
if (res.code === 200) {
this.nextNodeInfo = res.result
// 获取当前办理节点名称
if (res.result.task && res.result.task.name) {
this.currentTaskName = res.result.task.name
}
// 处理可用操作
if (res.result.attributes && res.result.attributes.actions) {
this.availableActions = res.result.attributes.actions
}
// 处理目标节点
if (res.result.sequenceFlow) {
this.sequenceFlows = res.result.sequenceFlow
}
} else {
this.$message.error(res.msg || "获取下一节点信息失败")
}
})
.catch((error) => {
console.error("获取下一节点信息失败", error)
this.$message.error("获取下一节点信息失败")
})
.finally(() => {
this.loading = false
})
},
// 获取可驳回的节点列表
getRejectTaskList() {
this.loading = true
this.$axios
.get("/easy-flowable/task/flowBackNodes/" + this.task_id)
.then((res) => {
if (res.code === 0 && res.result) {
this.rejectTaskOptions = res.result.map((item) => ({
label: item.nodeName,
value: item.nodeId
}))
if (this.rejectTaskOptions.length > 0) {
this.selectedRejectTask = this.rejectTaskOptions[0].value
}
} else {
this.$message.error(res.msg || "获取可驳回节点失败")
}
})
.catch((error) => {
console.error("获取可驳回节点失败", error)
this.$message.error("获取可驳回节点失败")
})
.finally(() => {
this.loading = false
})
},
// 处理按钮点击
handleAction(action) {
this.selectedAction = action
// 如果是驳回到指定节点,需要获取可驳回的节点列表
if (action === "REJECT_TO_TASK") {
this.getRejectTaskList()
return
}
// 执行操作
this.submitAction()
},
// 提交操作
submitAction() {
if (this.show_comment && !this.comment && ["AGREE", "REJECT", "REJECT_TO_TASK", "REBUT"].includes(this.selectedAction)) {
this.$message.warning("请填写审核意见")
return
}
const params = {
taskId: this.task_id,
comment: this.comment,
type: this.getActionCode(this.selectedAction)
}
// 添加目标节点变量(自动选择第一个)
if (this.sequenceFlows.length > 0 && ["AGREE"].includes(this.selectedAction)) {
params.variables = {
sequenceFlow: this.sequenceFlows[0].value
}
}
// 添加驳回节点信息
if (this.selectedAction === "REJECT_TO_TASK" && this.selectedRejectTask) {
params.targetTaskDefinitionKey = this.selectedRejectTask
}
this.loading = true
this.$axios
.post("/easy-flowable/task/complete", params)
.then((res) => {
if (res.code === 0) {
this.$message.success("操作成功")
this.$emit("success", {
action: this.selectedAction,
result: res.result
})
} else {
this.$message.error(res.msg || "操作失败")
}
})
.catch((error) => {
console.error("操作失败", error)
this.$message.error("操作失败")
})
.finally(() => {
this.loading = false
})
},
// 获取操作类型代码
getActionCode(action) {
const actionMap = {
START: "0",
AGREE: "1",
REBUT: "2",
REVOCATION: "3",
REJECT: "4",
REJECT_TO_TASK: "5",
DELEGATE: "6",
ASSIGN: "7",
STOP: "8",
BEFORE_SIGN: "9",
AFTER_SIGN: "10",
CANCELLATION: "11",
ADD_COMMENT: "12",
DEL_COMMENT: "13",
RESUBMIT: "14"
}
return actionMap[action] || "1"
},
// 获取操作按钮文本
getActionText(action) {
const actionTextMap = {
START: "启动流程",
AGREE: "同意",
REBUT: "拒绝",
REVOCATION: "撤回",
REJECT: "驳回",
REJECT_TO_TASK: "驳回到指定节点",
DELEGATE: "委派",
ASSIGN: "转办",
STOP: "终止",
BEFORE_SIGN: "前加签",
AFTER_SIGN: "后加签",
CANCELLATION: "作废",
ADD_COMMENT: "添加评论",
DEL_COMMENT: "删除评论",
RESUBMIT: "重新提交"
}
return actionTextMap[action] || action
},
// 获取操作按钮类型
getActionType(action) {
const typeMap = {
AGREE: "primary",
REBUT: "danger",
REJECT: "warning",
REJECT_TO_TASK: "warning"
}
return typeMap[action] || "default"
},
// 模拟获取流程记录
mockGetProcessRecords() {
// 这里可以根据实际需求调整模拟数据
// 实际项目中,可以通过API获取真实数据
// 例如:this.$axios.post("/platform/wf/processCommon/approvalRecord", { businessNo: this.business_id })
// 模拟异步获取数据
setTimeout(() => {
// 如果已经有模拟数据,则不再添加
if (this.mockProcessRecords.length <= 2) {
// 添加更多模拟数据
this.mockProcessRecords.push(
{
nodeName: "主管审核",
userName: "王五",
time: "2023-05-09 14:30:00",
action: "同意",
comment: "同意申请,请总经理审批",
type: "success"
},
{
nodeName: "发起申请",
userName: "张三",
time: "2023-05-09 10:15:00",
action: "提交申请",
comment: "请审批我的申请",
type: "info"
}
)
}
}, 1000)
}
}
}
</script>
<template>
<div class="form-button-container">
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 表单信息插槽 -->
<div class="form-info-container">
<slot name="form-info"></slot>
</div>
<!-- 流程图展示 -->
<div class="process-diagram-container" v-if="showProcessDiagram">
<div class="section-title">流程图</div>
<div class="process-diagram">
<!-- 模拟流程图 -->
<div class="mock-process-diagram">
<div class="mock-node start">开始</div>
<div class="mock-arrow"></div>
<div class="mock-node" :class="{ active: currentTaskName === '部门审核' }">部门审核</div>
<div class="mock-arrow"></div>
<div class="mock-node">主管审核</div>
<div class="mock-arrow"></div>
<div class="mock-node">总经理审核</div>
<div class="mock-arrow"></div>
<div class="mock-node end">结束</div>
</div>
</div>
</div>
<!-- 办理记录 -->
<div class="process-records-container" v-if="showProcessRecords">
<div class="section-title">
<span>办理记录</span>
<div class="display-mode-selector">
<el-radio-group v-model="recordDisplayMode" size="small">
<el-radio-button label="timeline">时间线</el-radio-button>
<el-radio-button label="table">表格</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="process-records">
<!-- 时间线模式 -->
<el-timeline v-if="recordDisplayMode === 'timeline'">
<el-timeline-item v-for="(record, index) in mockProcessRecords" :key="index" :timestamp="record.time" :type="record.type">
<div class="record-content">
<div class="record-title">{{ record.nodeName }} - {{ record.userName }}</div>
<div class="record-action">{{ record.action }}</div>
<div class="record-comment" v-if="record.comment">{{ record.comment }}</div>
</div>
</el-timeline-item>
</el-timeline>
<!-- 表格模式 -->
<el-table v-else-if="recordDisplayMode === 'table'" :data="mockProcessRecords" stripe style="width: 100%">
<el-table-column prop="nodeName" label="节点名称" width="120"></el-table-column>
<el-table-column prop="userName" label="操作人" width="100"></el-table-column>
<el-table-column prop="time" label="操作时间" width="160"></el-table-column>
<el-table-column prop="action" label="操作类型" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.type">{{ scope.row.action }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="comment" label="操作意见">
<template slot-scope="scope">
<span v-if="scope.row.comment">{{ scope.row.comment }}</span>
<span v-else class="no-comment"></span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 底部固定操作区域 -->
<div class="footer-actions">
<!-- 当前办理节点 -->
<div v-if="currentTaskName" class="current-task-container">
<div class="current-task-label">当前节点</div>
<div class="current-task-info">{{ currentTaskName }}</div>
</div>
<!-- 目标节点展示 -->
<div v-if="sequenceFlows.length > 0" class="flow-container">
<div class="flow-label">目标节点</div>
<div class="target-node-info">
<span v-for="item in sequenceFlows" :key="item.value">{{ item.label }}</span>
</div>
</div>
<!-- 审核意见 -->
<div v-if="show_comment" class="comment-container">
<div class="comment-label">审核意见</div>
<el-input v-model="comment" type="textarea" :rows="3" placeholder="请输入审核意见"></el-input>
</div>
<!-- 驳回到指定节点选择 -->
<div v-if="selectedAction === 'REJECT_TO_TASK' && rejectTaskOptions.length > 0" class="reject-container">
<div class="reject-label">驳回到</div>
<el-select v-model="selectedRejectTask" placeholder="请选择驳回节点">
<el-option v-for="item in rejectTaskOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-button type="primary" @click="submitAction">确认</el-button>
<el-button @click="selectedAction = ''">取消</el-button>
</div>
<!-- 操作按钮 -->
<div class="button-container">
<!-- 系统预设按钮 -->
<template v-for="action in availableActions">
<el-button
:key="action"
:type="getActionType(action)"
:loading="loading && selectedAction === action"
@click="handleAction(action)"
>
{{ getActionText(action) }}
</el-button>
</template>
<!-- 自定义按钮 -->
<template v-for="(button, index) in custom_buttons">
<el-button
:key="'custom-' + index"
:type="button.type || 'default'"
:loading="loading && selectedAction === button.action"
@click="handleAction(button.action)"
>
{{ button.text }}
</el-button>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.form-button-container {
/*display: flex;
//flex-direction: column;*/
min-height: 500px;
position: relative;
height: 100vh; /* 使用视口高度 */
}
/* 主要内容区域 */
.main-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
/* 表单信息容器 */
.form-info-container {
margin-bottom: 20px;
}
/* 流程图和办理记录共用样式 */
.process-diagram-container,
.process-records-container {
margin-bottom: 30px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 15px;
background-color: #fff;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
}
.display-mode-selector {
margin-left: auto;
}
/* 流程图样式 */
.process-diagram {
min-height: 120px;
}
.mock-process-diagram {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
padding: 20px 0;
}
.mock-node {
padding: 10px 15px;
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin: 0 5px;
position: relative;
}
.mock-node.start {
background-color: #f0f9eb;
border-color: #e1f3d8;
color: #67c23a;
}
.mock-node.end {
background-color: #f0f2f5;
border-color: #dcdfe6;
color: #909399;
}
.mock-node.active {
background-color: #ecf5ff;
border-color: #d9ecff;
color: #409eff;
font-weight: bold;
}
.mock-arrow {
width: 30px;
height: 2px;
background-color: #dcdfe6;
position: relative;
}
.mock-arrow:after {
content: "";
position: absolute;
right: 0;
top: -3px;
width: 0;
height: 0;
border-style: solid;
border-width: 4px 0 4px 6px;
border-color: transparent transparent transparent #dcdfe6;
}
/* 办理记录样式 */
.process-records {
padding: 10px 0;
}
/* 时间线模式样式 */
.record-content {
padding: 5px 0;
}
.record-title {
font-weight: bold;
margin-bottom: 5px;
}
.record-action {
color: #409eff;
margin-bottom: 5px;
}
.record-comment {
color: #606266;
background-color: #f5f7fa;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
}
/* 表格模式样式 */
.no-comment {
color: #909399;
font-style: italic;
}
/* 底部固定操作区域 */
.footer-actions {
border-top: 1px solid #ebeef5;
padding: 20px;
background-color: #fff;
//position: sticky;
//bottom: 0;
z-index: 10;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.comment-container,
.current-task-container,
.flow-container,
.reject-container {
margin-bottom: 15px;
}
.comment-label,
.current-task-label,
.flow-label,
.reject-label {
margin-bottom: 5px;
font-weight: bold;
}
.current-task-info {
padding: 5px 0;
line-height: 1.5;
}
.target-node-info {
padding: 5px 0;
line-height: 1.5;
}
.button-container {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
</style>
@@ -0,0 +1,728 @@
<template>
<div class="flow-container">
<!-- 页面标题栏 -->
<div class="page-header">
<div class="header-content">
<div class="title-section">
<h2 class="page-title">{{ pageTitle }}</h2>
<div class="page-breadcrumb">
<span>工作流管理</span>
<span class="separator">></span>
<span>{{ isNewApplication ? "发起申请" : "任务处理" }}</span>
</div>
</div>
<div class="status-section">
<span class="status-label">状态</span>
<span class="status-badge" :class="statusClass">{{ processStatusText }}</span>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 左侧内容 -->
<div class="content-left">
<!-- 流程基本信息 -->
<div class="info-panel">
<div class="panel-header">
<h3 class="panel-title">流程信息</h3>
<div class="panel-actions">
<el-button size="small" type="text" @click="showProcessDiagram" v-if="canShowDiagram">
<i class="el-icon-view"></i>
查看流程图
</el-button>
</div>
</div>
<div class="panel-content">
<div class="info-table">
<!-- 新申请场景 -->
<template v-if="isNewApplication">
<div class="info-row">
<div class="info-label">流程名称</div>
<div class="info-value">{{ processDefinition.displayName || processDefinition.name }}</div>
</div>
<div class="info-row">
<div class="info-label">流程说明</div>
<div class="info-value">{{ processDefinition.description }}</div>
</div>
</template>
<!-- 已有流程场景 -->
<template v-else-if="isExistingProcess">
<div class="info-row">
<div class="info-label">流程名称</div>
<div class="info-value">{{ processInstance.displayName }}</div>
</div>
<div class="info-row">
<div class="info-label">发起人</div>
<div class="info-value">{{ processInstance?.ext?.initiatorName || "-" }}</div>
</div>
<div class="info-row">
<div class="info-label">发起时间</div>
<div class="info-value">{{ formatDate(processInstance.createdAt) }}</div>
</div>
</template>
</div>
</div>
</div>
<!-- 历史办理过程面板 -->
<div v-if="shouldShowHistoryProcess" class="history-panel">
<div class="panel-header">
<h3 class="panel-title">办理过程</h3>
</div>
<div class="panel-content">
<div id="history-process-container" class="form-content">
<div v-if="pjaxLoading.historyProcess" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载历史办理过程...</span>
</div>
</div>
</div>
</div>
<!-- 表单处理区域 -->
<div class="form-panel" v-if="isNewApplication || todoTasks.map((v) => v.id).includes(taskId)">
<div class="panel-header">
<h3 class="panel-title">{{ formPanelTitle }}</h3>
</div>
<div class="panel-content">
<!-- 新申请表单 -->
<div v-if="isNewApplication" id="application-form-container" class="form-content">
<div v-if="pjaxLoading.apply" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载申请表单...</span>
</div>
</div>
<!-- 当前任务表单 -->
<div v-if="shouldShowTaskForm" id="task-form-container" class="form-content">
<div v-if="pjaxLoading.taskForm" class="loading-state">
<i class="el-icon-loading"></i>
<span>正在加载任务表单...</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧审批记录 -->
<!-- <div class="content-right" v-if="isExistingProcess">-->
<!-- <snaker-flow-history-approval :task_id="taskId"></snaker-flow-history-approval>-->
<!-- </div>-->
</div>
<!-- 流程图对话框 -->
<el-dialog title="流程图预览" :visible.sync="designVisible" width="80%" top="8vh">
<div class="diagram-viewer">
<iframe v-if="designVisible" :src="designUrl" frameborder="0"></iframe>
</div>
</el-dialog>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlow",
data() {
return {
// URL参数
taskId: GetQueryString("taskId") ? parseInt(GetQueryString("taskId")) : null,
instanceId: GetQueryString("instanceId"),
businessId: GetQueryString("businessId"),
defineKey: GetQueryString("defineKey"), // 新增:流程定义KEY
processInstance: {},
//流程定义信息
processDefinition: {},
todoTasks: [],
currentTask: {},
// 加载状态
loading: false,
// 错误信息
error: null,
// PJAX 加载状态
pjaxLoading: {
apply: false,
taskForm: false,
historyProcess: false
},
// 显示历史记录
showHistory: false,
// 流程图相关
designVisible: false,
designUrl: "",
// 流程状态枚举映射
processStatusMap: {
10: { text: "进行中", class: "doing" },
20: { text: "已完成", class: "finished" },
30: { text: "已撤回", class: "withdraw" },
40: { text: "强行终止", class: "interrupt" },
45: { text: "已拒绝", class: "reject" },
50: { text: "挂起", class: "pending" },
99: { text: "已废弃", class: "abandon" }
},
// 任务状态枚举
taskStateEnum: {
DOING: 10,
FINISHED: 20,
WITHDRAW: 30,
INTERRUPT: 40,
PENDING: 50,
ABANDON: 99
},
// 任务类型枚举
performTypeEnum: {
NORMAL: 0, // 普通任务
COUNTERSIGN: 1 // 会签任务
}
}
},
computed: {
// 页面标题
pageTitle() {
if (this.isNewApplication) {
return this.processDefinition.displayName || this.processDefinition.name || "发起申请"
} else {
return this.processInstance.displayName || "任务处理"
}
},
// 流程状态文本
processStatusText() {
if (this.isNewApplication) {
return "待提交"
} else {
return this.processStatusMap[this.processInstance.state]?.text || "未知状态"
}
},
// 状态样式类
statusClass() {
if (this.isNewApplication) {
return "status-pending"
} else {
const statusClass = this.processStatusMap[this.processInstance.state]?.class
return `status-${statusClass || "unknown"}`
}
},
// 表单面板标题
formPanelTitle() {
if (this.isNewApplication) {
return "申请表单"
} else if (this.shouldShowTaskForm) {
return this.currentTask.displayName || "任务表单"
} else if (this.shouldShowHistoryProcess) {
return "办理过程"
} else {
return "表单信息"
}
},
// 是否显示历史办理过程
shouldShowHistoryProcess() {
// debugger
// return this.businessId && this.isTaskInProgress && !this.isFirstTaskNode
return true
},
// 是否显示任务表单
shouldShowTaskForm() {
debugger
return this.taskId != null
},
// 是否可以显示流程图
canShowDiagram() {
return (this.isNewApplication && this.processDefinition.id) || (this.isExistingProcess && this.processInstance.processId)
},
// 判断是否为新申请
isNewApplication() {
return !this.taskId && !this.instanceId && this.defineKey
},
// 判断是否为已有流程实例
isExistingProcess() {
return this.taskId || this.instanceId
},
// 判断任务是否进行中
isTaskInProgress() {
return this.currentTask && this.currentTask.taskState === this.taskStateEnum.DOING
},
// 判断是否为第一个任务节点
isFirstTaskNode() {
return this.currentTask && this.currentTask.ext && this.currentTask.ext.isFirstTaskNode
},
// 流程图预览URL
processDesignUrl() {
if (this.isNewApplication && this.processDefinition.id) {
return `/flow/define/preview?id=${this.processDefinition.id}`
} else if (this.isExistingProcess && this.processInstance.processId) {
return `/flow/define/preview?id=${this.processInstance.processId}`
}
return ""
}
},
created() {
this.initComponent()
// 监听
window.addEventListener("message", (event) => {
if (event.data.type === "task-complete") {
this.initComponent()
}
})
},
methods: {
// 初始化组件
async initComponent() {
this.loading = true
debugger
if (this.isNewApplication) {
// 新申请:加载流程定义信息
await this.loadProcessDefinition()
// 加载流程申请表单
await this.loadApplicationForm()
} else if (this.isExistingProcess) {
// 已有流程:加载流程实例信息
await this.loadProcessInstance()
// 加载任务信息
await this.loadCurrentTask()
// 如果有businessId且不是第一个任务节点,加载历史办理过程
if (this.businessId) {
await this.loadHistoryProcess()
}
} else {
console.warn("URL中缺少必要的参数(defineKey、taskId或instanceId),无法初始化组件")
this.loading = false
return
}
this.loading = false
},
// 加载流程定义信息
async loadProcessDefinition() {
if (!this.defineKey) return
try {
const response = await $.get("/flow/common/defineInfo", { defineKey: this.defineKey })
if (response.code === 0) {
this.processDefinition = response.data
}
} catch (error) {
console.error("加载流程定义信息失败:", error)
}
},
// 加载申请表单
async loadApplicationForm() {
debugger
if (!this.defineKey || !this.processDefinition.instanceUrl) return
this.pjaxLoading.apply = true
$.pjax({
url: this.processDefinition.instanceUrl,
container: "#application-form-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.apply = false
})
.fail(() => {
this.pjaxLoading.apply = false
console.log(this.processDefinition.instanceUrl, "申请表单加载失败")
})
},
// 加载流程实例信息
async loadProcessInstance() {
if (!this.instanceId) return
const { code, data, msg } = await $.get("/flow/common/instanceInfo", { instanceId: this.instanceId })
if (code === 0) {
this.processInstance = data.processInstance
this.todoTasks = data.todoTasks
}
},
// 加载任务信息
async loadCurrentTask() {
if (!this.taskId) return
$.get("/flow/common/taskInfo", { taskId: this.taskId }, (response) => {
if (response.code === 0) {
this.currentTask = response.data
this.loadTaskForm()
}
})
},
// 加载任务表单
async loadTaskForm() {
if (!this.taskId || !this.currentTask?.taskModel?.form) return
if (!this.todoTasks.map((v) => v.id).includes(this.taskId)) {
return
}
this.pjaxLoading.taskForm = true
$.pjax({
url: this.currentTask.taskModel.form,
container: "#task-form-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.taskForm = false
})
.fail(() => {
this.pjaxLoading.taskForm = false
console.log(this.currentTask.taskModel.form, "任务表单加载失败")
})
},
// 加载历史办理过程
async loadHistoryProcess() {
if (!this.businessId) return
this.pjaxLoading.historyProcess = true
$.pjax({
url: `/platform/article/common/fullForm?businessId=${this.businessId}`,
container: "#history-process-container",
push: false,
replace: false,
timeout: 10000
})
.done(() => {
this.pjaxLoading.historyProcess = false
})
.fail(() => {
this.pjaxLoading.historyProcess = false
console.log("历史办理过程加载失败")
})
},
// 显示流程图
showProcessDiagram() {
if (this.processDesignUrl) {
this.designUrl = this.processDesignUrl
this.designVisible = true
}
},
// 获取状态文本
getStatusText(state) {
return this.processStatusMap[state]?.text || "未知状态"
},
// 获取状态样式类
getStatusClass(state) {
return `status-${this.processStatusMap[state]?.class || "unknown"}`
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return "-"
return new Date(dateStr).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
}
}
}
</script>
<style scoped>
/* 简洁OA风格样式 */
.flow-container {
background-color: #f5f6fa;
min-height: 100vh;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
/* 页面头部 - 固定在顶部 */
.page-header {
background: white;
border-bottom: 1px solid #e4e7ed;
padding: 16px 0;
position: fixed;
top: 64px;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 主要内容区域 - 添加顶部间距 */
.main-content {
max-width: 1200px;
margin: 0 auto;
margin-top: 80px; /* 为固定头部留出空间 */
padding: 20px;
display: flex;
gap: 20px;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-section .page-title {
font-size: 20px;
font-weight: 600;
color: #303133;
margin: 0 0 4px 0;
}
.page-breadcrumb {
font-size: 12px;
color: #909399;
}
.separator {
margin: 0 6px;
}
.status-section {
display: flex;
align-items: center;
gap: 8px;
}
.status-label {
font-size: 14px;
color: #606266;
}
.status-badge {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fdf6ec;
color: #e6a23c;
}
.status-doing {
background: #ecf5ff;
color: var(--color-primary);
}
.status-finished {
background: #f0f9ff;
color: #67c23a;
}
.status-withdraw {
background: #f4f4f5;
color: #909399;
}
.status-interrupt {
background: #fef0f0;
color: #f56c6c;
}
.status-reject {
background: #fef0f0;
color: #f56c6c;
}
.content-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
}
.content-right {
width: 320px;
flex-shrink: 0;
}
/* 面板样式 */
.info-panel,
.form-panel,
.history-panel,
.approval-panel {
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.panel-header {
background: #fafafa;
border-bottom: 1px solid #e4e7ed;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin: 0;
}
.panel-actions {
display: flex;
gap: 8px;
}
.panel-content {
padding: 16px;
}
.panel-content.no-padding {
padding: 0;
}
/* 信息表格 */
.info-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f5f7fa;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #606266;
font-size: 14px;
min-width: 80px;
font-weight: 500;
}
.info-value {
color: #303133;
font-size: 14px;
flex: 1;
line-height: 1.5;
}
/* 表单内容 */
.form-content {
min-height: 200px;
position: relative;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
gap: 8px;
}
.loading-state i {
font-size: 20px;
animation: spin 1s linear infinite;
}
.loading-state span {
font-size: 14px;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 流程图查看器 */
.diagram-viewer {
height: 65vh;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.diagram-viewer iframe {
width: 100%;
height: 100%;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
.content-right {
width: 100%;
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.main-content {
padding: 16px;
}
.panel-content {
padding: 12px;
}
.info-row {
flex-direction: column;
gap: 4px;
}
.info-label {
min-width: auto;
}
}
</style>
@@ -0,0 +1,380 @@
<template>
<div class="approval-sidebar">
<div class="sidebar-header">
<h4>审批记录</h4>
</div>
<div class="timeline-container" v-if="historyApprovalRecords.length > 0">
<div class="timeline-wrapper">
<div
v-for="(record, index) in historyApprovalRecords"
:key="record.id"
class="timeline-item">
<!-- 时间线节点 -->
<div class="timeline-node">
<div class="timeline-dot" :class="getDotClass(record.taskState)"></div>
<div class="timeline-line" v-if="index < historyApprovalRecords.length - 1"></div>
</div>
<!-- 审批内容 -->
<div class="timeline-content">
<div class="approval-card">
<div class="card-header">
<span class="task-name">{{ record.displayName }}</span>
<span class="approval-time">{{ formatTimestamp(record.finishTime || record.createdAt) }}</span>
</div>
<div class="card-body">
<div class="info-item" v-if="record.operator && record.operator !== 'flow.auto'">
<span class="label">办理人</span>
<span class="value">{{ getOperatorName(record) }}</span>
</div>
<div class="info-item" v-if="record.ext && record.ext.submitType">
<span class="label">操作</span>
<span class="value submit-type" :class="'submit-' + record.ext.submitType">
{{ getSubmitTypeText(record.ext.submitType) }}
</span>
</div>
<div class="info-item" v-if="record.ext && record.ext.opinion">
<span class="label">意见</span>
<span class="value">{{ record.ext.opinion }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-records">
暂无审批记录
</div>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlowHisApproval",
props: {
task_id: {
type: String | Number,
required: true
}
},
data() {
return {
// 历史审批记录
historyApprovalRecords: [],
// 加载状态
loading: false,
// 任务状态枚举
taskStateEnum: {
DOING: 10,
FINISHED: 20,
WITHDRAW: 30,
INTERRUPT: 40,
PENDING: 50,
ABANDON: 99
}
}
},
computed: {
// 获取实例ID(从task_id获取流程实例信息)
instanceId() {
return GetQueryString("instanceId")
}
},
created() {
this.loadHistoryApprovalRecords()
},
watch: {
task_id: {
handler(newVal) {
if (newVal) {
this.loadHistoryApprovalRecords()
}
},
immediate: true
}
},
methods: {
// 加载历史审批记录
async loadHistoryApprovalRecords() {
if (!this.instanceId && !this.task_id) return
this.loading = true
try {
const params = this.instanceId ?
{ instanceId: this.instanceId } :
{ taskId: this.task_id }
const response = await $.post("/flow/common/approvalRecord", params)
if (response.code === 0) {
this.historyApprovalRecords = response.data || []
}
} catch (error) {
console.error("加载历史审批记录失败:", error)
} finally {
this.loading = false
}
},
// 格式化时间戳
formatTimestamp(timestamp) {
if (!timestamp) return ""
const date = new Date(timestamp)
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
},
// 获取操作人姓名
getOperatorName(record) {
if (!record.operator || record.operator === 'flow.auto') {
return "系统自动"
}
if (record.ext && record.ext.operatorName) {
return record.ext.operatorName
}
return record.operator
},
// 获取提交类型文本
getSubmitTypeText(submitType) {
const typeMap = {
'agree': '同意',
'reject': '拒绝',
'back': '退回',
'transfer': '转办',
'delegate': '委托',
'submit': '提交'
}
return typeMap[submitType] || submitType
},
// 获取节点样式类
getDotClass(state) {
switch (state) {
case this.taskStateEnum.FINISHED:
return "dot-success"
case this.taskStateEnum.WITHDRAW:
return "dot-warning"
case this.taskStateEnum.INTERRUPT:
case this.taskStateEnum.ABANDON:
return "dot-danger"
case this.taskStateEnum.PENDING:
return "dot-info"
case this.taskStateEnum.DOING:
return "dot-primary"
default:
return "dot-default"
}
}
}
}
</script>
<style scoped>
.approval-sidebar {
width: 320px;
background: #fff;
border: 1px solid #ddd;
height: fit-content;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.sidebar-header {
background: #f5f5f5;
border-bottom: 1px solid #ddd;
padding: 12px 16px;
flex-shrink: 0;
}
.sidebar-header h4 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
}
.timeline-container {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.timeline-wrapper {
position: relative;
}
.timeline-item {
display: flex;
margin-bottom: 20px;
position: relative;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-node {
position: relative;
margin-right: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid #fff;
position: relative;
z-index: 2;
}
.dot-success {
background: #52c41a;
}
.dot-warning {
background: #faad14;
}
.dot-danger {
background: #f5222d;
}
.dot-primary {
background: #1890ff;
}
.dot-info {
background: #999;
}
.dot-default {
background: #d9d9d9;
}
.timeline-line {
width: 1px;
background: #e8e8e8;
flex: 1;
margin-top: 4px;
min-height: 30px;
}
.timeline-content {
flex: 1;
min-width: 0;
}
.approval-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
}
.card-header {
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.task-name {
font-size: 13px;
font-weight: bold;
color: #333;
flex: 1;
}
.approval-time {
font-size: 12px;
color: #999;
}
.card-body {
padding: 12px;
}
.info-item {
display: flex;
margin-bottom: 6px;
font-size: 12px;
line-height: 1.5;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
color: #666;
min-width: 50px;
flex-shrink: 0;
}
.value {
color: #333;
flex: 1;
word-break: break-all;
}
.submit-type {
font-weight: bold;
}
.submit-agree {
color: #52c41a;
}
.submit-reject {
color: #f5222d;
}
.submit-back {
color: #faad14;
}
.submit-transfer, .submit-delegate, .submit-submit {
color: #1890ff;
}
.no-records {
padding: 40px 20px;
text-align: center;
color: #999;
font-size: 14px;
}
/* 滚动条样式 */
.timeline-container::-webkit-scrollbar {
width: 6px;
}
.timeline-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.timeline-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
}
.timeline-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -0,0 +1,226 @@
<template>
<div class="snaker-task-action" v-if="!loading || taskInfo">
<!-- 第一个任务节点或新申请显示提交保存草稿取消 -->
<template v-if="isFirstTaskNodeOrNew">
<el-button type="primary" @click="handleSubmit" :loading="actionLoading" icon="el-icon-upload2">提交</el-button>
<!-- 保存草稿只在流程未发起时显示 -->
<el-button @click="handleSaveDraft" v-if="!actionLoading && show_save_draft && !instanceId" icon="el-icon-document">保存草稿</el-button>
<el-button @click="handleCancel" :loading="actionLoading" v-if="show_cancel" icon="el-icon-close">取消</el-button>
</template>
<!-- 其他任务节点显示审批按钮 -->
<template v-else>
<!-- 会签任务按钮 -->
<template v-if="is_countersign_task">
<el-button type="primary" @click="handleAction('AGREE')" :loading="actionLoading" icon="el-icon-check">同意</el-button>
<el-button type="danger" plain @click="handleAction('COUNTERSIGN_DISAGREE')" :loading="actionLoading" icon="el-icon-close">
不同意
</el-button>
</template>
<!-- 普通任务按钮 -->
<template v-else>
<el-button type="primary" @click="handleAction('AGREE')" :loading="actionLoading" icon="el-icon-check">同意</el-button>
<el-button type="danger" plain @click="handleAction('REJECT')" :loading="actionLoading" icon="el-icon-close">拒绝</el-button>
<el-button @click="handleAction('ROLLBACK')" :loading="actionLoading" icon="el-icon-back">退回上一步</el-button>
<el-button @click="handleAction('ROLLBACK_TO_OPERATOR')" :loading="actionLoading" icon="el-icon-d-arrow-left">退回发起人</el-button>
<el-button @click="handleAction('JUMP')" :loading="actionLoading" icon="el-icon-position">跳转</el-button>
</template>
</template>
</div>
<!-- 加载状态 -->
<div v-else class="snaker-task-action loading-state">
<el-button loading icon="el-icon-loading">加载中...</el-button>
</div>
</template>
<script>
module.exports = {
name: "SnakerFlowTaskFormAction",
props: {
// 是否显示保存草稿按钮
show_save_draft: {
type: Boolean,
default: true
},
// 是否显示取消按钮
show_cancel: {
type: Boolean,
default: true
}
},
data() {
return {
// 加载状态
loading: false,
// 操作按钮加载状态
actionLoading: false,
// 任务信息
taskInfo: null,
// 任务操作枚举
actionEnum: {
APPLY: "0", // 发起申请
AGREE: "1", // 同意
REJECT: "2", // 拒绝
COUNTERSIGN_DISAGREE: "20", // 会签不同意 拒绝申请
ROLLBACK: "3", // 退回上一步
ROLLBACK_TO_OPERATOR: "6", // 退回发起人
JUMP: "4", // 跳转
RE_APPLY: "5", // 重新提交
SUBMIT: "0" // 发起申请
},
// 任务类型枚举
performTypeEnum: {
NORMAL: 0, // 普通任务
COUNTERSIGN: 1 // 会签任务
},
taskId: GetQueryString("taskId"),
instanceId: GetQueryString("instanceId"),
defineKey: GetQueryString("defineKey"),
defineId: GetQueryString("defineId"),
businessId: GetQueryString("businessId")
}
},
computed: {
// 是否为会签任务
is_countersign_task() {
return this.taskInfo && this.taskInfo.performType === this.performTypeEnum.COUNTERSIGN
},
// 是否为第一个任务节点或新申请
isFirstTaskNodeOrNew() {
// 没有任务ID,说明是新申请
if (!this.taskId) {
return true
}
// 有任务ID但是任务信息中标识为第一个节点
return this.taskInfo && this.taskInfo.ext && this.taskInfo.ext.isFirstTaskNode
}
},
mounted() {
// 只有当有任务ID时才加载任务信息
if (this.taskId) {
this.loadTaskInfo()
}
},
methods: {
// 加载任务信息
loadTaskInfo() {
if (!this.taskId) return
this.loading = true
this.taskInfo = null
$.get("/flow/common/taskInfo", {
taskId: this.taskId
})
.then((res) => {
if (res.code === 0) {
this.taskInfo = res.data
}
})
.always(() => {
this.loading = false
})
},
// 处理提交操作
handleSubmit() {
// 根据是否有流程实例来确定操作类型
// const submitAction = this.instanceId ? this.actionEnum.RE_APPLY : this.actionEnum.APPLY
const submitAction = this.actionEnum.APPLY
// 触发父组件事件,传递提交操作
this.$emit("task-action", {
submitType: submitAction,
taskId: this.taskId,
instanceId: this.instanceId,
defineId: this.defineId,
defineKey: this.defineKey,
businessId: this.businessId
// currentTask: this.taskInfo
})
},
// 处理任务操作
handleAction(actionKey) {
if (this.actionLoading || !this.taskInfo) return
// 触发父组件事件,传递操作类型和任务信息
this.$emit("task-action", {
submitType: this.actionEnum[actionKey],
taskId: this.taskId,
processTaskId: this.taskId,
instanceId: this.instanceId,
defineId: this.defineId,
defineKey: this.defineKey,
businessId: this.businessId
})
},
// 保存草稿
handleSaveDraft() {
if (this.actionLoading) return
// 保存草稿时不显示加载状态,因为只是保存业务数据
this.$emit("save-draft", {
defineId: this.defineId,
businessId: this.businessId,
isDraft: true // 标识这是草稿保存
})
},
// 取消操作
handleCancel() {
this.$emit("cancel")
},
// 刷新任务信息
refresh() {
if (this.taskId) {
this.loadTaskInfo()
}
}
}
}
</script>
<style scoped>
.snaker-task-action {
text-align: center;
padding: 20px;
background: #fff;
border-top: 1px solid #eee;
margin-top: 20px;
}
.snaker-task-action .el-button {
margin: 0 8px;
min-width: 80px;
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 60px;
}
/* 响应式 */
@media (max-width: 768px) {
.snaker-task-action {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
}
.snaker-task-action .el-button {
width: 100%;
margin: 0;
}
}
</style>
@@ -0,0 +1,84 @@
<template>
<div>
<template v-for="item in options">
<el-tag :key="item[value_key]" v-if="item[value_key] === value" v-bind="$attrs">{{ item[label_key] }}</el-tag>
</template>
</div>
</template>
<script>
// 全局缓存和请求Promise缓存
const enumCache = {}
const requestPromises = {}
module.exports = {
name: "DictTag",
props: {
value: { type: String | Number },
name: { type: String },
value_key: {
type: String,
default: "code"
},
label_key: {
type: String,
default: "name"
}
},
data() {
return {
options: []
}
},
watch: {
code:{
handler(val) {
this.getEnumOptions()
},
immediate: true
}
},
methods: {
getEnumOptions() {
// // 检查数据缓存
// if (enumCache[this.name]) {
// this.options = enumCache[this.name]
// return
// }
//
// // 检查是否已有相同请求在进行中
// if (requestPromises[this.name]) {
// requestPromises[this.name].then(data => {
// this.options = data
// })
// return
// }
// 创建请求Promise并缓存
requestPromises[this.name] = $.get("/open/common/dictEnumOptions", { name: this.name })
.then(res => {
if (res.code === 0) {
// 存入数据缓存
enumCache[this.name] = res.data
// 清除请求Promise缓存
delete requestPromises[this.name]
return res.data
}
})
requestPromises[this.name].then(data => {
this.options = data
})
}
},
created() {
}
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>