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>
+295 -492
View File
@@ -1,520 +1,323 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" />
<meta name="description" content="${AppName!}" />
<title>${AppName!}</title>
<meta charset="UTF-8" />
<!-- import CSS -->
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/font-awesome.min.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css?v=1.0.1" />
<link rel="stylesheet" href="${base!}/assets/platform/css/a.css?v=1.0.0" />
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<!-- import Vue before Element -->
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vuex/vuex.js"></script>
<script src="${base!}/assets/platform/plugins/vuex-persistedstate/vuex-persistedstate.umd.js"></script>
<script src="${base!}/assets/platform/plugins/vue-http-loader/httpVueLoader.js"></script>
<style>
.sub-app-container {
margin: 0 auto;
background: #fff;
display: flex;
overflow: hidden;
min-height: calc(100vh - 64px);
box-sizing: border-box;
}
<!-- import ElementUI -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<script src="${base!}/assets/platform/plugins/element-ui/lib/i18n/${lang,escape}.js"></script>
.sidebar-menu {
width: 250px;
background: rgb(35 85 120);
color: #fff;
flex-shrink: 0;
height: calc(100vh - 64px);
display: flex;
flex-direction: column;
}
<!-- import Jquery -->
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<!--nprogress 配合pjax使用-->
<link rel="stylesheet" href="https://cdn.staticfile.net/nprogress/0.2.0/nprogress.css" />
<script src="${base!}/assets/platform/plugins/nprogress/nprogress.js"></script>
<!-- Array数组增加findIndex等方法,TreeTable用得到,兼容IE8 -->
<script src="${base!}/assets/platform/plugins/polyfill/polyfill.js"></script>
<!-- 时间日期处理工具类 -->
<script src="${base!}/assets/platform/plugins/moment/moment.min.js"></script>
<script src="${base!}/assets/platform/plugins/toastr/toastr.js"></script>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/toastr/toastr.css" />
<!--axios-->
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<!--图片预览-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/viewerjs/viewer.css" />
<script src="${base!}/assets/platform/plugins/viewerjs/viewer.js"></script>
<!--签字canvas-->
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<!--二维码-->
<script src="${base!}/assets/platform/plugins/vue-qrcode/index.js"></script>
<!--富文本编辑器-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/wangEditor4/wangEditor.css" />
<script src="${base!}/assets/platform/plugins/wangEditor4/wangEditor.js"></script>
<!--g2plot-->
<script src="${base!}/assets/platform/plugins/g2plot/g2plot.min.js"></script>
<script src="${base!}/components/plugins/sysIconSelector/iconSelect.js"></script>
<!--MinDash-->
<script src="${base!}/assets/platform/plugins/min-dash/min-dash.js"></script>
<!--Sortable-->
<script src="${base!}/assets/platform/plugins/sortablejs/sortable.min.js"></script>
<script src="${base!}/assets/platform/plugins/vuedraggable/vuedraggable.umd.min.js"></script>
<!--vxe-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
<!-- <script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>-->
<!-- <script src="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.js"></script>-->
<!-- <script src="${base!}/assets/platform/plugins/vxe/vue-table.js"></script>-->
.menu-header {
padding: 20px;
background: rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/style.min.css" />-->
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-table@3.9.0/lib/style.min.css" />-->
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
.menu-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
#sidebar-menu .el-scrollbar {
flex: 1;
}
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
#sidebar-menu .el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
margin-bottom: 0 !important;
}
<script>
window._AMapSecurityConfig = {
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
}
</script>
<!-- <script-->
<!-- type="text/javascript"-->
<!-- src="https://webapi.amap.com/maps?v=2.0&key=57f0d098ba1b881ecc436c4cfd23bbbf&plugin=AMap.PolyEditor,AMap.Geolocation,AMap.PlaceSearch"-->
<!-- ></script>-->
#sidebar-menu .el-menu {
background: transparent;
border: none;
}
<script type="text/javascript">
Vue.config.devtools = false
//ELEMENTUI
ELEMENT.locale(ELEMENT.lang["${lang,escape}"])
ELEMENT.Dialog.props.closeOnClickModal.default = false
ELEMENT.Dialog.props.top.default = "50px"
ELEMENT.Table.props.border.default = true
ELEMENT.TableColumn.props.showOverflowTooltip = { type: Boolean, default: true }
Vue.use(ELEMENT, {
zIndex: 20000
})
#sidebar-menu .el-menu .el-submenu__title {
color: #ffffff;
}
Vue.use(FcDesigner)
Vue.use(FcDesigner.formCreate)
#sidebar-menu .el-menu .el-submenu__title i,
#sidebar-menu .el-menu .el-menu-item i {
color: inherit;
}
// formCreate.create([], {
// submitBtn: {
// show: false
// }
// })
</script>
#sidebar-menu .el-menu .el-submenu__title:hover {
background: rgba(0, 0, 0, 0.1);
}
<!--ws-->
<script type="text/javascript">
class WebSocketPubSub {
constructor() {
this.ws = null
this.subscribers = new Map()
this.isSubscribed = false // 添加订阅状态标记
this.init()
}
#sidebar-menu .el-menu .el-menu-item {
color: inherit;
}
init() {
try {
this.connect()
this.setupEventListeners()
this.setupPing()
this.setupWindowEvents()
} catch (ex) {
console.info(ex)
}
}
#sidebar-menu .el-menu .el-menu-item.is-active {
background: rgba(0, 0, 0, 0.1);
}
connect() {
const WS_URL = window.location.host + "${base}/websocket"
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
this.ws = new WebSocket(protocol + WS_URL)
}
#sidebar-menu .el-menu .el-menu-item:focus,
.el-menu-item:hover {
background: rgba(0, 0, 0, 0.1);
}
//订阅
subscribe(type, callback) {
if (!this.subscribers.has(type)) {
this.subscribers.set(type, new Set())
}
this.subscribers.get(type).add(callback)
#sidebar-menu .el-menu .el-menu-item a {
text-decoration: none;
color: inherit;
display: inline-block;
width: 100%;
}
setTimeout(() => {
console.log(this.ws.readyState)
// 所有订阅完成后,发送 join 消息
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
this.isSubscribed = true
this.sendJoinMessage()
}
}, 2000)
}
.sub-app-container-main-content {
flex: 1;
padding: 10px;
background: #f6f6f6;
box-sizing: border-box;
overflow: auto;
height: calc(100vh - 64px);
}
// 事件处理
setupEventListeners() {
// 消息处理
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
// 触发订阅者的回调
if (this.subscribers.has(data.action)) {
this.subscribers.get(data.action).forEach((callback) => {
try {
callback(data)
} catch (error) {
console.error("订阅者回调执行错误:", error)
}
})
}
} catch (error) {
console.error("消息解析错误:", error)
}
}
.content-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
// 连接建立
this.ws.onopen = () => {
// this.ws.send(
// JSON.stringify({
// room: store.state.room,
// action: "join"
// })
// )
// 连接建立时不立即发送 join,等待订阅完成
this.ping()
}
}
.content-title {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
// 新增发送 join 消息的方法
sendJoinMessage() {
this.send({
room: store.state.room,
action: "join"
})
}
.content-subtitle {
color: #7f8c8d;
margin-top: 5px;
font-size: 14px;
}
// 心跳检测
setupPing() {
setInterval(() => this.ping(), (11624317 / 1000) * 2)
}
.content-body {
min-height: 400px;
}
ping() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send("{}")
} catch (e) {
this.reconnect()
}
} else {
this.reconnect()
}
}
@media (max-width: 768px) {
.sub-app-container {
flex-direction: column;
}
reconnect() {
if (this.ws) {
this.ws.close() // 确保旧连接已关闭
}
setTimeout(() => {
this.connect()
this.setupEventListeners()
// 如果之前已订阅,重新发送join消息
if (this.isSubscribed) {
setTimeout(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.sendJoinMessage()
}
}, 1000)
}
}, 3000) // 延迟3秒重连
}
.sidebar-menu {
width: 100%;
}
// 窗口事件处理
setupWindowEvents() {
let _beforeUnload_time = 0
window.onbeforeunload = () => {
_beforeUnload_time = new Date().getTime()
}
window.onunload = () => {
const _gap_time = new Date().getTime() - _beforeUnload_time
if (_gap_time <= 5) {
this.send({
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
action: "left"
})
}
}
}
.sub-app-container-main-content {
padding: 20px;
}
}
</style>
send(message) {
if (this.ws) {
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
this.ws.send(messageStr)
}
}
}
window.webSocketPubSub = new WebSocketPubSub()
</script>
<!--vuex-->
<script type="text/javascript">
console.log(${json(@auth.getPrincipalProperty('birthday'))})
window.sessionStorage.setItem("user",JSON.stringify(${json(@auth.getLogonUser())}))
window.store = new Vuex.Store({
plugins: [
createPersistedState({
key: "zhgh-vuex",
paths: ["openedMenus", "activeMenuIndex", "activeMenuModuleId"],
storage: window.localStorage
})
],
state: {
user: JSON.parse(window.sessionStorage.getItem("user")),
//当前激活的菜单
activeMenuIndex: null,
//打开菜单的数组
openedMenus: [],
//激活的菜单模块
activeMenuModuleId: null,
//点击过的菜单(历史标签页使用)
historyMenuIds: [],
//消息列表
messages: [],
//聊天室
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
//签字
signature: {}
},
getters: {
fullMenus: (state) => {
return state.user.menus
},
pcModules: (state) => {
return state.user.pcModuleMenus
},
historyMenuIds: (state) => {
return state.historyMenuIds
}
},
mutations: {
//设置用户信息
setUser(state, payload) {
state.user = payload
},
//设置激活的菜单
setActiveMenuIndex(state, payload) {
state.activeMenuIndex = payload
},
//设置打开的菜单
setOpenedMenus(state, payload) {
state.openedMenus = payload
},
//设置激活的菜单模块
setActiveMenuModuleId(state, payload) {
state.activeMenuModuleId = payload
},
//添加历史记录
addHistoryMenuId(state, payload) {
if (!payload) {
return
}
const exists = state.historyMenuIds.includes(payload)
if (!exists) {
state.historyMenuIds.push(payload)
}
},
//移除某条历史记录
removeHistoryMenuId(state, payload) {
state.historyMenuIds = state.historyMenuIds.filter((id) => id !== payload)
},
//移除多条历史记录
removeHistoryMenuIds(state, payload) {
state.historyMenuIds = state.historyMenuIds.filter((id) => !payload.includes(id))
},
//清空历史记录
clearHistoryMenuIds(state) {
state.historyMenuIds = []
state.activeMenuIndex = null
state.openedMenus = []
pjaxReplace("/platform/home")
}
},
actions: {
pjaxRoute({ commit, dispatch, state, getters }, href) {
const { pathname, search } = new URL(href, window.location.origin)
const matchMenu = getters.fullMenus.find((menu) => menu.href === pathname)
if (!matchMenu) {
commit("setActiveMenuIndex", null)
commit("setOpenedMenus", [])
if (!state.activeModuleId && getters.pcModules) {
commit("setActiveMenuModuleId", getters.pcModules[0].id)
}
pjaxReplace(pathname === "/platform/home" ? pathname : pathname)
return
}
//拷贝一份
let cloneMenu = JSON.parse(JSON.stringify(matchMenu))
//找出所有父菜单包括自己
let parentMenus = []
while (cloneMenu && cloneMenu.parentId) {
cloneMenu = getters.fullMenus.find((m) => m.id === cloneMenu.parentId)
if (cloneMenu) {
parentMenus.unshift(cloneMenu)
}
}
parentMenus.push(matchMenu)
//获取到该菜单所属的模块 从一级菜单获取
const moduleId = parentMenus[0]?.moduleId
//判断下是否跨模块跳转
if (moduleId !== state.activeMenuModuleId) {
commit("setActiveMenuModuleId", moduleId)
}
Vue.nextTick(()=>{
//重新设置展开菜单
commit("setOpenedMenus", parentMenus.map((menu) => menu.id))
//设置当前激活菜单
commit("setActiveMenuIndex", matchMenu.id)
//判断是否加入到历史点击菜单
commit("addHistoryMenuId", matchMenu.id)
//跳转
pjaxReplace(matchMenu.href ? matchMenu.href + (search || "") : "/platform/home")
})
}
}
})
</script>
<!--beetl全局变量方法-->
<script type="text/javascript">
const base = "${base!}"
const APP_DOMAIN = "${AppDomain!}"
</script>
<!--vue挂载-->
<script type="text/javascript">
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService
</script>
<!--vue全局组件-->
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.component("guava", httpVueLoader("/components/plugins/Guava.vue?v=" + new Date().getTime()))
Vue.component("dict-tag", httpVueLoader("/components/plugins/sysDict/DictTag.vue?v=" + new Date().getTime()))
Vue.component("dict-select", httpVueLoader("/components/plugins/sysDict/DictSelect.vue?v=" + new Date().getTime()))
Vue.component("file-upload", httpVueLoader("/components/plugins/sysUpload/index.vue?v=" + new Date().getTime()))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/index.vue?v=" + new Date().getTime()))
Vue.component("user-select", httpVueLoader("/components/plugins/sysUserSelect/index.vue?v=" + new Date().getTime()))
Vue.component("table-tool", httpVueLoader("/components/plugins/sysTableTool/index.vue?v=" + new Date().getTime()))
Vue.component("signature", httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime()))
Vue.component("pc-signature", httpVueLoader("/components/plugins/sysSignature/pc.vue?v=" + new Date().getTime()))
Vue.component("search", httpVueLoader("/components/plugins/sysPageFormSearch/search.vue?v=" + new Date().getTime()))
Vue.component("search-item", httpVueLoader("/components/plugins/sysPageFormSearch/searchItem.vue?v=" + new Date().getTime()))
Vue.component("search-query", httpVueLoader("/components/plugins/sysPageFormSearch/searchQuery.vue?v=" + new Date().getTime()))
Vue.component("el-spin", httpVueLoader("/components/plugins/spin/index.vue?v=" + new Date().getTime()))
Vue.component("text-editor", httpVueLoader("/components/plugins/sysTextEditor/index.vue?v=" + new Date().getTime()))
Vue.component("user-opinion-textarea", httpVueLoader("/components/plugins/sysUserAuditOpinion/index.vue?v=" + new Date().getTime()))
Vue.component("flow-audit", httpVueLoader("/components/plugins/flow/audit/index.vue?v=" + new Date().getTime()))
Vue.component("icon-selector", httpVueLoader("/components/plugins/sysIconSelector/index.vue?v=" + new Date().getTime()))
Vue.component("svg-icon", httpVueLoader("/components/plugins/sysSvgIcon/index.vue?v=" + new Date().getTime()))
Vue.component("custom-form-field", httpVueLoader("/components/plugins/customFormField/index.vue?v=" + new Date().getTime()))
Vue.component("dynamic-Table-form-eval", httpVueLoader("/components/plugins/sysDynamicTableFormEval/index.vue?v=" + new Date().getTime()))
Vue.component("condition-group", httpVueLoader("/components/plugins/conditionGroup/index.vue?v=" + new Date().getTime()))
Vue.component("excel-import", httpVueLoader("/components/plugins/sysImport/excelImport.vue?v=" + new Date().getTime()))
</script>
</head>
<body>
<div>
<div class="ele-admin-layout ele-admin-fixed-body ele-admin-side-dark ele-admin-show-tabs ele-admin-responsive">
<div class="ele-admin-header" id="ele-admin-header" v-cloak>
<div class="ele-admin-logo">
<img src="${AppLogo!}" alt="logo" @click="$store.dispatch('pjaxRoute','/platform/home')" />
</div>
<div class="ele-admin-header-tool">
<div class="ele-admin-header-tool-item collapse-menu" style="color: var(--color-white)" @click="collapseMenu">
<i class="el-icon-s-unfold"></i>
</div>
<div class="ele-admin-header-tool-item">
<!--菜单模块-->
<header-menu-module></header-menu-module>
</div>
</div>
<!--面包屑-->
<div class="ele-admin-breadcrumb">
<header-bread-crumb></header-bread-crumb>
</div>
<div class="ele-admin-header-nav"></div>
<div class="ele-admin-header-tool">
<!--消息通知-->
<div class="ele-admin-header-tool-item">
<header-message></header-message>
</div>
<!--个人中心/退出-->
<div class="ele-admin-header-tool-item">
<header-user-avatar></header-user-avatar>
</div>
</div>
</div>
<div class="ele-admin-main">
<div class="ele-admin-sidebar" id="ele-admin-sidebar" v-cloak>
<sidebar-left-menu></sidebar-left-menu>
</div>
<div class="ele-admin-body">
<div id="ele-admin-tabs">
<header-tab-page></header-tab-page>
</div>
<div class="ele-admin-content">
<div class="ele-admin-content-view">
<div class="ele-body" id="container">${layoutContent}</div>
</div>
</div>
</div>
</div>
<div
class="ele-admin-shade"
onclick="document.querySelector('body >div > div.ele-admin-layout').classList.add('ele-admin-collapse')"
></div>
</div>
<div class="sub-app-container" id="sub-app-container">
<!-- 左侧菜单区域 -->
<nav class="sidebar-menu" id="sidebar-menu">
<div class="menu-header">
<h2 class="menu-title">子系统</h2>
</div>
<script type="text/javascript">
<!--左侧菜单 && 头部 && 标签页-->
new Vue({
el: "#ele-admin-sidebar",
components: {
"sidebar-left-menu": httpVueLoader("/components/layout/SideBarLeftMenu.vue?v=" + new Date().getTime())
}
<el-scrollbar>
<el-menu unique-opened :default-active="activeMenuIndex" :default-openeds="openedMenus" @select="menuSelect">
<menu-list :menus="leftMenus" :depth="0"></menu-list>
</el-menu>
</el-scrollbar>
</nav>
<!-- 右侧主体区域 -->
<main class="sub-app-container-main-content" id="sub-app-container-main-content">
<!-- <header class="content-header">-->
<!-- <h1 class="content-title"></h1>-->
<!-- &lt;!&ndash; <p class="content-subtitle"></p>&ndash;&gt;-->
<!-- </header>-->
<div class="content-body" id="sub-app-container-main-content-body">${layoutContent!}</div>
</main>
<!-- 子应用ID -->
<div id="sub-app-id" style="display: none"></div>
</div>
<script>
// 初始化pjax加载
;(function initPjax() {
$(document).pjax("#sidebar-menu a[data-pjax]", "#sub-app-container-main-content-body", {
maxCacheLength: 0,
push: true,
replace: false,
fragment: "#sub-app-container-main-content-body",
timeout: 8000
})
$(document).on("pjax:send", function () {
NProgress.configure({ parent: ".sub-app-container-main-content" })
NProgress.start()
$("#sub-app-container-main-content").hide()
})
$(document).on("pjax:complete", function () {
NProgress.done()
$("#sub-app-container-main-content").show()
setContentTitle()
})
setContentTitle()
})()
// 设置内容标题
function setContentTitle() {
// const menu = store.state.user.menus.find((v) => v.href === window.location.pathname)
// $("#sub-app-container-main-content .content-title").text(menu?.name)
try {
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
$("#sub-app-container #sidebar-menu .menu-header .menu-title").text(app?.name)
} catch (e) {}
}
document.addEventListener("DOMContentLoaded", function () {
const menuLinks = document.querySelectorAll("#sidebar-menu a")
menuLinks.forEach((link) => {
link.addEventListener("click", function (e) {
const pjax = this.hasAttribute("data-pjax")
e.preventDefault()
})
new Vue({
el: "#ele-admin-header",
store,
components: {
"header-message": httpVueLoader("/components/layout/HeaderMessage.vue?v=" + new Date().getTime()),
"header-bread-crumb": httpVueLoader("/components/layout/HeaderBreadCrumb.vue?v=" + new Date().getTime()),
"header-menu-module": httpVueLoader("/components/layout/HeaderMenuModule.vue?v=" + new Date().getTime()),
"header-user-avatar": httpVueLoader("/components/layout/HeaderUserAvatar.vue?v=" + new Date().getTime())
},
methods: {
collapseMenu() {
document.querySelector("body >div > div.ele-admin-layout").classList.remove("ele-admin-collapse")
})
})
new Vue({
el: "#sidebar-menu",
components: {
"menu-list": {
name: "menu-list",
template:
/*language=HTML*/
`
<div>
<template v-for="(item, index) in menus">
<el-submenu :index="item.id" v-if="item.children && item.children.length > 0">
<template #title>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else class="el-icon-paperclip"></i>
<span>{{ item.name }}</span>
</template>
<menu-list :menus="item.children" :depth="depth + 1" />
</el-submenu>
<el-menu-item :index="item.id" v-else>
<a :href="item.href" data-pjax>
<i :class="item.icon" v-if="item.icon" :size="18"></i>
<i v-else :class="item.icon ? item.icon : 'el-icon-menu'"></i>
<span>{{ item.name }}</span>
</a>
</el-menu-item>
</template>
</div>
`,
props: ["menus", "depth"]
}
},
data() {
return {
menus: [],
activeMenuIndex: null,
openedMenus: []
}
},
computed: {
leftMenus() {
if (this.menus) {
return this.menus
} else {
try {
return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus"))
} catch (e) {
return []
}
}
})
new Vue({
el: "#ele-admin-tabs",
components: {
"header-tab-page": httpVueLoader("/components/layout/HeaderTabPage.vue?v=" + new Date().getTime())
}
},
methods: {
menuSelect(index, indexPath) {
try {
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
window.sessionStorage.setItem("zhgh_sub_app_left_menu_active_index-" + app.id, index)
window.sessionStorage.setItem("zhgh_sub_app_left_menu_opens-" + app.id, JSON.stringify(indexPath))
} catch (e) {}
},
// 获取菜单
getMenus(appId) {
if (!appId) return
$.get("/platform/sys/user/subAppMenus", { appId }).then((res) => {
if (res.code === 0) {
this.menus = res.data
window.sessionStorage.setItem("zhgh_sub_app_menus", JSON.stringify(res.data))
} else {
this.menus = []
}
this.echoMenus()
})
},
setAppInfo(app) {
if (app) {
// 储存到缓存
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
// 储存到div中
document.getElementById("sub-app-id").innerText = app.id
}
})
</script>
</body>
</html>
// document.querySelector("#sub-app-container .content-title").innerText = app.name
},
// 菜单回显
echoMenus() {
try {
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
this.activeMenuIndex = window.sessionStorage.getItem("zhgh_sub_app_left_menu_active_index-" + app.id)
this.openedMenus = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_left_menu_opens-" + app.id)) || []
console.log(this.openedMenus)
console.log(this.activeMenuIndex)
} catch (e) {}
}
},
mounted() {
// 页面加载时获取到正确的菜单
const pathname = window.location.pathname
if (!pathname.startsWith("/platform/v4/subApp")) {
$.get("/platform/sys/user/rootMenuByPath", { pathname }).then((res) => {
if (res.code === 0) {
this.setAppInfo(res.data)
this.getMenus(res.data?.id)
}
})
} else {
const appId = GetQueryString("appId")
if (!appId) {
return
}
this.getMenus(appId)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,520 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" />
<meta name="description" content="${AppName!}" />
<title>${AppName!}</title>
<meta charset="UTF-8" />
<!-- import CSS -->
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/font-awesome.min.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css?v=1.0.1" />
<link rel="stylesheet" href="${base!}/assets/platform/css/a.css?v=1.0.0" />
<!-- import Vue before Element -->
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vuex/vuex.js"></script>
<script src="${base!}/assets/platform/plugins/vuex-persistedstate/vuex-persistedstate.umd.js"></script>
<script src="${base!}/assets/platform/plugins/vue-http-loader/httpVueLoader.js"></script>
<!-- import ElementUI -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<script src="${base!}/assets/platform/plugins/element-ui/lib/i18n/${lang,escape}.js"></script>
<!-- import Jquery -->
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<!--nprogress 配合pjax使用-->
<link rel="stylesheet" href="https://cdn.staticfile.net/nprogress/0.2.0/nprogress.css" />
<script src="${base!}/assets/platform/plugins/nprogress/nprogress.js"></script>
<!-- Array数组增加findIndex等方法,TreeTable用得到,兼容IE8 -->
<script src="${base!}/assets/platform/plugins/polyfill/polyfill.js"></script>
<!-- 时间日期处理工具类 -->
<script src="${base!}/assets/platform/plugins/moment/moment.min.js"></script>
<script src="${base!}/assets/platform/plugins/toastr/toastr.js"></script>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/toastr/toastr.css" />
<!--axios-->
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<!--图片预览-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/viewerjs/viewer.css" />
<script src="${base!}/assets/platform/plugins/viewerjs/viewer.js"></script>
<!--签字canvas-->
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<!--二维码-->
<script src="${base!}/assets/platform/plugins/vue-qrcode/index.js"></script>
<!--富文本编辑器-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/wangEditor4/wangEditor.css" />
<script src="${base!}/assets/platform/plugins/wangEditor4/wangEditor.js"></script>
<!--g2plot-->
<script src="${base!}/assets/platform/plugins/g2plot/g2plot.min.js"></script>
<script src="${base!}/components/plugins/sysIconSelector/iconSelect.js"></script>
<!--MinDash-->
<script src="${base!}/assets/platform/plugins/min-dash/min-dash.js"></script>
<!--Sortable-->
<script src="${base!}/assets/platform/plugins/sortablejs/sortable.min.js"></script>
<script src="${base!}/assets/platform/plugins/vuedraggable/vuedraggable.umd.min.js"></script>
<!--vxe-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
<!-- <script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>-->
<!-- <script src="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.js"></script>-->
<!-- <script src="${base!}/assets/platform/plugins/vxe/vue-table.js"></script>-->
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/style.min.css" />-->
<!-- <link rel="stylesheet" href="https://vxeui.com/umd/vxe-table@3.9.0/lib/style.min.css" />-->
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
<script>
window._AMapSecurityConfig = {
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
}
</script>
<!-- <script-->
<!-- type="text/javascript"-->
<!-- src="https://webapi.amap.com/maps?v=2.0&key=57f0d098ba1b881ecc436c4cfd23bbbf&plugin=AMap.PolyEditor,AMap.Geolocation,AMap.PlaceSearch"-->
<!-- ></script>-->
<script type="text/javascript">
Vue.config.devtools = false
//ELEMENTUI
ELEMENT.locale(ELEMENT.lang["${lang,escape}"])
ELEMENT.Dialog.props.closeOnClickModal.default = false
ELEMENT.Dialog.props.top.default = "50px"
ELEMENT.Table.props.border.default = true
ELEMENT.TableColumn.props.showOverflowTooltip = { type: Boolean, default: true }
Vue.use(ELEMENT, {
zIndex: 20000
})
Vue.use(FcDesigner)
Vue.use(FcDesigner.formCreate)
// formCreate.create([], {
// submitBtn: {
// show: false
// }
// })
</script>
<!--ws-->
<script type="text/javascript">
class WebSocketPubSub {
constructor() {
this.ws = null
this.subscribers = new Map()
this.isSubscribed = false // 添加订阅状态标记
this.init()
}
init() {
try {
this.connect()
this.setupEventListeners()
this.setupPing()
this.setupWindowEvents()
} catch (ex) {
console.info(ex)
}
}
connect() {
const WS_URL = window.location.host + "${base}/websocket"
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
this.ws = new WebSocket(protocol + WS_URL)
}
//订阅
subscribe(type, callback) {
if (!this.subscribers.has(type)) {
this.subscribers.set(type, new Set())
}
this.subscribers.get(type).add(callback)
setTimeout(() => {
console.log(this.ws.readyState)
// 所有订阅完成后,发送 join 消息
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
this.isSubscribed = true
this.sendJoinMessage()
}
}, 2000)
}
// 事件处理
setupEventListeners() {
// 消息处理
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
// 触发订阅者的回调
if (this.subscribers.has(data.action)) {
this.subscribers.get(data.action).forEach((callback) => {
try {
callback(data)
} catch (error) {
console.error("订阅者回调执行错误:", error)
}
})
}
} catch (error) {
console.error("消息解析错误:", error)
}
}
// 连接建立
this.ws.onopen = () => {
// this.ws.send(
// JSON.stringify({
// room: store.state.room,
// action: "join"
// })
// )
// 连接建立时不立即发送 join,等待订阅完成
this.ping()
}
}
// 新增发送 join 消息的方法
sendJoinMessage() {
this.send({
room: store.state.room,
action: "join"
})
}
// 心跳检测
setupPing() {
setInterval(() => this.ping(), (11624317 / 1000) * 2)
}
ping() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send("{}")
} catch (e) {
this.reconnect()
}
} else {
this.reconnect()
}
}
reconnect() {
if (this.ws) {
this.ws.close() // 确保旧连接已关闭
}
setTimeout(() => {
this.connect()
this.setupEventListeners()
// 如果之前已订阅,重新发送join消息
if (this.isSubscribed) {
setTimeout(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.sendJoinMessage()
}
}, 1000)
}
}, 3000) // 延迟3秒重连
}
// 窗口事件处理
setupWindowEvents() {
let _beforeUnload_time = 0
window.onbeforeunload = () => {
_beforeUnload_time = new Date().getTime()
}
window.onunload = () => {
const _gap_time = new Date().getTime() - _beforeUnload_time
if (_gap_time <= 5) {
this.send({
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
action: "left"
})
}
}
}
send(message) {
if (this.ws) {
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
this.ws.send(messageStr)
}
}
}
window.webSocketPubSub = new WebSocketPubSub()
</script>
<!--vuex-->
<script type="text/javascript">
console.log(${json(@auth.getPrincipalProperty('birthday'))})
window.sessionStorage.setItem("user",JSON.stringify(${json(@auth.getLogonUser())}))
window.store = new Vuex.Store({
plugins: [
createPersistedState({
key: "zhgh-vuex",
paths: ["openedMenus", "activeMenuIndex", "activeMenuModuleId"],
storage: window.localStorage
})
],
state: {
user: JSON.parse(window.sessionStorage.getItem("user")),
//当前激活的菜单
activeMenuIndex: null,
//打开菜单的数组
openedMenus: [],
//激活的菜单模块
activeMenuModuleId: null,
//点击过的菜单(历史标签页使用)
historyMenuIds: [],
//消息列表
messages: [],
//聊天室
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
//签字
signature: {}
},
getters: {
fullMenus: (state) => {
return state.user.menus
},
pcModules: (state) => {
return state.user.pcModuleMenus
},
historyMenuIds: (state) => {
return state.historyMenuIds
}
},
mutations: {
//设置用户信息
setUser(state, payload) {
state.user = payload
},
//设置激活的菜单
setActiveMenuIndex(state, payload) {
state.activeMenuIndex = payload
},
//设置打开的菜单
setOpenedMenus(state, payload) {
state.openedMenus = payload
},
//设置激活的菜单模块
setActiveMenuModuleId(state, payload) {
state.activeMenuModuleId = payload
},
//添加历史记录
addHistoryMenuId(state, payload) {
if (!payload) {
return
}
const exists = state.historyMenuIds.includes(payload)
if (!exists) {
state.historyMenuIds.push(payload)
}
},
//移除某条历史记录
removeHistoryMenuId(state, payload) {
state.historyMenuIds = state.historyMenuIds.filter((id) => id !== payload)
},
//移除多条历史记录
removeHistoryMenuIds(state, payload) {
state.historyMenuIds = state.historyMenuIds.filter((id) => !payload.includes(id))
},
//清空历史记录
clearHistoryMenuIds(state) {
state.historyMenuIds = []
state.activeMenuIndex = null
state.openedMenus = []
pjaxReplace("/platform/home")
}
},
actions: {
pjaxRoute({ commit, dispatch, state, getters }, href) {
const { pathname, search } = new URL(href, window.location.origin)
const matchMenu = getters.fullMenus.find((menu) => menu.href === pathname)
if (!matchMenu) {
commit("setActiveMenuIndex", null)
commit("setOpenedMenus", [])
if (!state.activeModuleId && getters.pcModules) {
commit("setActiveMenuModuleId", getters.pcModules[0].id)
}
pjaxReplace(pathname === "/platform/home" ? pathname : pathname)
return
}
//拷贝一份
let cloneMenu = JSON.parse(JSON.stringify(matchMenu))
//找出所有父菜单包括自己
let parentMenus = []
while (cloneMenu && cloneMenu.parentId) {
cloneMenu = getters.fullMenus.find((m) => m.id === cloneMenu.parentId)
if (cloneMenu) {
parentMenus.unshift(cloneMenu)
}
}
parentMenus.push(matchMenu)
//获取到该菜单所属的模块 从一级菜单获取
const moduleId = parentMenus[0]?.moduleId
//判断下是否跨模块跳转
if (moduleId !== state.activeMenuModuleId) {
commit("setActiveMenuModuleId", moduleId)
}
Vue.nextTick(()=>{
//重新设置展开菜单
commit("setOpenedMenus", parentMenus.map((menu) => menu.id))
//设置当前激活菜单
commit("setActiveMenuIndex", matchMenu.id)
//判断是否加入到历史点击菜单
commit("addHistoryMenuId", matchMenu.id)
//跳转
pjaxReplace(matchMenu.href ? matchMenu.href + (search || "") : "/platform/home")
})
}
}
})
</script>
<!--beetl全局变量方法-->
<script type="text/javascript">
const base = "${base!}"
const APP_DOMAIN = "${AppDomain!}"
</script>
<!--vue挂载-->
<script type="text/javascript">
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService
</script>
<!--vue全局组件-->
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.component("guava", httpVueLoader("/components/plugins/Guava.vue?v=" + new Date().getTime()))
Vue.component("dict-tag", httpVueLoader("/components/plugins/sysDict/DictTag.vue?v=" + new Date().getTime()))
Vue.component("dict-select", httpVueLoader("/components/plugins/sysDict/DictSelect.vue?v=" + new Date().getTime()))
Vue.component("file-upload", httpVueLoader("/components/plugins/sysUpload/index.vue?v=" + new Date().getTime()))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/index.vue?v=" + new Date().getTime()))
Vue.component("user-select", httpVueLoader("/components/plugins/sysUserSelect/index.vue?v=" + new Date().getTime()))
Vue.component("table-tool", httpVueLoader("/components/plugins/sysTableTool/index.vue?v=" + new Date().getTime()))
Vue.component("signature", httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime()))
Vue.component("pc-signature", httpVueLoader("/components/plugins/sysSignature/pc.vue?v=" + new Date().getTime()))
Vue.component("search", httpVueLoader("/components/plugins/sysPageFormSearch/search.vue?v=" + new Date().getTime()))
Vue.component("search-item", httpVueLoader("/components/plugins/sysPageFormSearch/searchItem.vue?v=" + new Date().getTime()))
Vue.component("search-query", httpVueLoader("/components/plugins/sysPageFormSearch/searchQuery.vue?v=" + new Date().getTime()))
Vue.component("el-spin", httpVueLoader("/components/plugins/spin/index.vue?v=" + new Date().getTime()))
Vue.component("text-editor", httpVueLoader("/components/plugins/sysTextEditor/index.vue?v=" + new Date().getTime()))
Vue.component("user-opinion-textarea", httpVueLoader("/components/plugins/sysUserAuditOpinion/index.vue?v=" + new Date().getTime()))
Vue.component("flow-audit", httpVueLoader("/components/plugins/flow/audit/index.vue?v=" + new Date().getTime()))
Vue.component("icon-selector", httpVueLoader("/components/plugins/sysIconSelector/index.vue?v=" + new Date().getTime()))
Vue.component("svg-icon", httpVueLoader("/components/plugins/sysSvgIcon/index.vue?v=" + new Date().getTime()))
Vue.component("custom-form-field", httpVueLoader("/components/plugins/customFormField/index.vue?v=" + new Date().getTime()))
Vue.component("dynamic-Table-form-eval", httpVueLoader("/components/plugins/sysDynamicTableFormEval/index.vue?v=" + new Date().getTime()))
Vue.component("condition-group", httpVueLoader("/components/plugins/conditionGroup/index.vue?v=" + new Date().getTime()))
Vue.component("excel-import", httpVueLoader("/components/plugins/sysImport/excelImport.vue?v=" + new Date().getTime()))
</script>
</head>
<body>
<div>
<div class="ele-admin-layout ele-admin-fixed-body ele-admin-side-dark ele-admin-show-tabs ele-admin-responsive">
<div class="ele-admin-header" id="ele-admin-header" v-cloak>
<div class="ele-admin-logo">
<img src="${AppLogo!}" alt="logo" @click="$store.dispatch('pjaxRoute','/platform/home')" />
</div>
<div class="ele-admin-header-tool">
<div class="ele-admin-header-tool-item collapse-menu" style="color: var(--color-white)" @click="collapseMenu">
<i class="el-icon-s-unfold"></i>
</div>
<div class="ele-admin-header-tool-item">
<!--菜单模块-->
<header-menu-module></header-menu-module>
</div>
</div>
<!--面包屑-->
<div class="ele-admin-breadcrumb">
<header-bread-crumb></header-bread-crumb>
</div>
<div class="ele-admin-header-nav"></div>
<div class="ele-admin-header-tool">
<!--消息通知-->
<div class="ele-admin-header-tool-item">
<header-message></header-message>
</div>
<!--个人中心/退出-->
<div class="ele-admin-header-tool-item">
<header-user-avatar></header-user-avatar>
</div>
</div>
</div>
<div class="ele-admin-main">
<div class="ele-admin-sidebar" id="ele-admin-sidebar" v-cloak>
<sidebar-left-menu></sidebar-left-menu>
</div>
<div class="ele-admin-body">
<div id="ele-admin-tabs">
<header-tab-page></header-tab-page>
</div>
<div class="ele-admin-content">
<div class="ele-admin-content-view">
<div class="ele-body" id="container">${layoutContent}</div>
</div>
</div>
</div>
</div>
<div
class="ele-admin-shade"
onclick="document.querySelector('body >div > div.ele-admin-layout').classList.add('ele-admin-collapse')"
></div>
</div>
</div>
<script type="text/javascript">
<!--左侧菜单 && 头部 && 标签页-->
new Vue({
el: "#ele-admin-sidebar",
components: {
"sidebar-left-menu": httpVueLoader("/components/layout/SideBarLeftMenu.vue?v=" + new Date().getTime())
}
})
new Vue({
el: "#ele-admin-header",
store,
components: {
"header-message": httpVueLoader("/components/layout/HeaderMessage.vue?v=" + new Date().getTime()),
"header-bread-crumb": httpVueLoader("/components/layout/HeaderBreadCrumb.vue?v=" + new Date().getTime()),
"header-menu-module": httpVueLoader("/components/layout/HeaderMenuModule.vue?v=" + new Date().getTime()),
"header-user-avatar": httpVueLoader("/components/layout/HeaderUserAvatar.vue?v=" + new Date().getTime())
},
methods: {
collapseMenu() {
document.querySelector("body >div > div.ele-admin-layout").classList.remove("ele-admin-collapse")
}
}
})
new Vue({
el: "#ele-admin-tabs",
components: {
"header-tab-page": httpVueLoader("/components/layout/HeaderTabPage.vue?v=" + new Date().getTime())
}
})
</script>
</body>
</html>
@@ -0,0 +1,630 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.apps-hero {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
/*border-radius: 8px 8px 0 0;*/
margin-bottom: 0;
}
.apps-hero img {
width: 100%;
height: 100%;
}
.apps-container {
display: flex;
/*height: calc(100vh - 64px - 48px - 180px);*/
background-color: #fff;
/*border-radius: 0 0 8px 8px;*/
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
flex: 1;
}
.apps-sidebar {
width: 220px;
background-color: #f7f7f7;
border-right: 1px solid #e8e8e8;
overflow-y: auto;
}
.apps-sidebar-item {
padding: 16px 20px;
cursor: pointer;
transition: all 0.3s;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.apps-sidebar-item.active {
background-color: #e6f7ff;
color: var(--color-primary);
border-right: 2px solid #1890ff;
}
.apps-sidebar-item:hover:not(.active) {
background-color: #f0f0f0;
}
.apps-content {
flex: 1;
padding: 20px;
overflow: auto;
display: flex;
flex-direction: column;
}
.apps-header {
padding: 0 0 20px 0;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
.apps-search-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.apps-search {
position: relative;
width: 400px;
}
.apps-search input {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 15px;
font-size: 14px;
box-sizing: border-box;
}
.apps-search .search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
cursor: pointer;
}
.apps-filter {
margin-top: 20px;
}
.apps-filter-title {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.apps-filter-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.filter-tag {
padding: 5px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.filter-tag.active {
background-color: var(--color-primary);
color: #fff;
}
.filter-tag:not(.active) {
background-color: #f0f0f0;
color: #333;
}
.filter-tag:not(.active):hover {
background-color: #e0e0e0;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
flex: 1;
overflow-y: auto;
grid-auto-rows: min-content;
padding-top: 5px;
}
.app-card {
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
display: flex;
align-items: center;
padding: 15px;
position: relative;
}
.app-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
cursor: pointer;
}
.app-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
color: #fff;
font-size: 24px;
}
.app-icon img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.app-icon i {
color: var(--color-primary);
font-size: 38px;
}
.favorite-icon {
position: absolute;
bottom: 10px;
right: 10px;
cursor: pointer;
transition: all 0.3s;
font-size: 18px;
color: #c0c4cc;
}
.favorite-icon.active {
color: #ff9800;
}
.favorite-icon:hover {
transform: scale(1.2);
}
.app-info {
flex: 1;
}
.app-title {
font-weight: 500;
margin-bottom: 5px;
font-size: 14px;
}
.app-desc {
color: #999;
font-size: 12px;
}
.alphabet-filter {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.alphabet-item {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.alphabet-item.active {
background-color: var(--color-primary);
color: #fff;
}
.alphabet-item:not(.active) {
background-color: #f0f0f0;
}
.alphabet-item:hover:not(.active) {
background-color: #e0e0e0;
}
.app-status {
position: absolute;
top: 0;
right: 0;
background-color: #ff9800;
color: #fff;
padding: 2px 10px;
font-size: 12px;
border-bottom-left-radius: 8px;
}
.apps-wrapper {
/*margin: 0 auto;*/
/*margin: 24px;*/
/*padding: 24px;*/
display: flex;
flex-direction: column;
height: calc(100vh - 64px);
}
.no-results {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
padding: 40px 20px;
}
.no-results-content {
text-align: center;
max-width: 300px;
}
.no-results i {
color: #c0c4cc;
margin-bottom: 16px;
}
.no-results .title {
font-size: 16px;
color: #606266;
font-weight: 500;
margin-bottom: 8px;
}
.no-results .hint {
font-size: 14px;
color: #909399;
line-height: 1.5;
}
.page-item.disabled {
color: #c0c4cc;
cursor: not-allowed;
}
.page-item.disabled:hover {
border-color: #d9d9d9;
color: #c0c4cc;
}
.loading-container {
text-align: center;
padding: 40px 0;
color: #909399;
font-size: 14px;
}
</style>
<div class="apps-wrapper" id="app">
<div class="apps-hero">
<img src="https://i.cug.edu.cn/data/sys-attach/download/1i8hmpaq7wmewu9c7w3pt9qhu3gb8bkcvow0" alt="应用中心" />
</div>
<div class="apps-container">
<!-- Left Sidebar -->
<div class="apps-sidebar">
<div
v-for="category in allCategories"
:key="category.id"
:class="['apps-sidebar-item', activeCategory === category.id ? 'active' : '']"
@click="changeCategory(category.id)"
>
<i :class="category.icon"></i>
{{ category.name }}
</div>
</div>
<!-- Main Content -->
<div class="apps-content">
<div class="apps-header">
<div class="apps-search-container">
<div class="apps-search">
<input type="text" v-model="searchKeyword" @keyup.enter="search" placeholder="请输入内容" />
<i class="el-icon-search search-icon" @click="search"></i>
</div>
</div>
<!-- 字母过滤 -->
<div class="apps-filter">
<div class="apps-filter-title">首字母:</div>
<div class="apps-filter-tags alphabet-filter">
<div :class="['filter-tag', activeAlphabet === '' ? 'active' : '']" @click="changeAlphabet('')">全部</div>
<div
v-for="letter in alphabets"
:key="letter"
:class="['alphabet-item', activeAlphabet === letter ? 'active' : '']"
@click="changeAlphabet(letter)"
>
{{ letter }}
</div>
</div>
</div>
</div>
<!-- 应用网格 -->
<div class="apps-grid" v-if="applications.length > 0">
<div v-for="app in applications" :key="app.id" class="app-card" @click.stop="openApp(app)">
<div :class="['app-icon', app.iconType]">
<i v-if="app.icon && !app.picIcon" :class="app.icon"></i>
<img v-else-if="app.picIcon" :src="app.picIcon" :alt="app.name || ''" />
<i v-else class="fa fa-skype"></i>
</div>
<div class="app-info">
<div class="app-title">{{ app.name }}</div>
<div class="app-desc" v-if="app.department">{{ app.department }}</div>
</div>
<div class="app-status" v-if="app.status">{{ app.status }}</div>
<div
:class="['favorite-icon', app.isFavorite ? 'active' : '']"
@click.stop="toggleFavorite(app.id)"
:title="app.isFavorite ? '取消收藏' : '收藏'"
>
<i :class="app.isFavorite ? 'el-icon-star-on' : 'el-icon-star-off'"></i>
</div>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-container" v-if="loading">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
<!-- 无搜索结果 -->
<div class="no-results" v-else-if="applications.length === 0">
<div class="no-results-content">
<i class="el-icon-document" style="font-size: 48px"></i>
<p class="title">没有找到匹配的应用</p>
<p class="hint">请尝试不同的搜索词或筛选条件</p>
</div>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: "#app",
data: {
// 应用列表
applications: [],
// 分类列表
categories: [
{ id: "all", name: "全部应用", icon: "fa fa-th-large" },
{ id: "favorites", name: "我的收藏", icon: "fa fa-star" },
{ id: "recommended", name: "推荐应用", icon: "fa fa-thumbs-up" }
],
// 动态加载的分类
dynamicCategories: [],
// 当前选中的分类
activeCategory: "all",
// 字母表筛选
alphabets: [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z"
],
activeAlphabet: "",
// 搜索关键词
searchKeyword: "",
// 分页相关
currentPage: 1,
pageSize: 15,
totalPage: 1,
total: 0,
// 加载状态
loading: true
},
computed: {
// 所有分类(固定分类 + 动态分类)
allCategories() {
return [...this.categories, ...this.dynamicCategories]
}
},
mounted() {
// 页面加载时获取分类和应用数据
this.loadCategories()
this.loadApps()
},
methods: {
// 加载分类数据
loadCategories() {
$.get("/platform/v4/apps/categories")
.then((result) => {
if (result.code === 0) {
this.dynamicCategories = result.data || []
} else {
console.error("获取应用分类失败:", result.msg)
}
})
.fail((error) => {
console.error("获取应用分类异常:", error)
})
},
// 加载应用数据
loadApps() {
this.loading = true
// 构建请求参数
const params = {
categoryId:
this.activeCategory === "all"
? ""
: this.activeCategory === "favorites" || this.activeCategory === "recommended"
? this.activeCategory
: this.activeCategory,
letter: this.activeAlphabet,
keyword: this.searchKeyword
}
// 将参数转换为URL查询参数
const queryString = Object.keys(params)
.filter((key) => params[key] !== null && params[key] !== undefined && params[key] !== "")
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
.join("&")
// 如果是"我的收藏"分类
if (this.activeCategory === "favorites") {
this.loadFavorites()
return
}
// 如果是"推荐应用"分类
if (this.activeCategory === "recommended") {
$.get("/platform/v4/apps/recommended")
.then((result) => {
if (result.code === 0) {
this.applications = result.data.list || []
} else {
console.error("获取推荐应用失败:", result.msg)
}
this.loading = false
})
.fail((error) => {
console.error("获取推荐应用异常:", error)
this.loading = false
})
return
}
// 发送请求获取普通应用列表
$.get("/platform/v4/apps/list?" + queryString)
.then((result) => {
if (result.code === 0) {
this.applications = result.data || []
} else {
console.error("获取应用列表失败:", result.msg)
}
this.loading = false
})
.fail((error) => {
console.error("获取应用列表异常:", error)
this.loading = false
})
},
// 加载收藏的应用
loadFavorites() {
$.get("/platform/v4/apps/favorite")
.then((result) => {
if (result.code === 0) {
this.applications = result.data || []
} else {
console.error("获取收藏应用失败:", result.msg)
}
})
.fail((error) => {
console.error("获取收藏应用异常:", error)
})
},
// 切换应用收藏状态
toggleFavorite(appId) {
const app = this.applications.find((a) => a.id === appId)
if (!app) return
const url = app.isFavorite ? "/platform/v4/apps/removeFavorite" : "/platform/v4/apps/addFavorite"
const params = { appId: appId }
$.post(url, params)
.then((result) => {
if (result.code === 0) {
app.isFavorite = !app.isFavorite
this.$message.success(app.isFavorite ? "收藏成功" : "取消收藏成功")
} else {
console.error(app.isFavorite ? "取消收藏失败:" : "收藏失败:", result.msg)
}
})
.fail((error) => {
console.error(app.isFavorite ? "取消收藏异常:" : "收藏异常:", error)
})
},
// 切换应用分类
changeCategory(categoryId) {
this.activeCategory = categoryId
this.currentPage = 1
// 如果是"我的收藏"分类
if (categoryId === "favorites") {
this.loadFavorites()
} else {
this.loadApps()
}
},
// 切换应用字母
changeAlphabet(letter) {
this.activeAlphabet = letter
this.currentPage = 1
this.loadApps()
},
// 搜索
search() {
this.loadApps()
},
// 打开应用
openApp(app) {
// 储存到缓存
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
// 新标签页打开
window.open("/platform/v4/subApp?appId=" + app.id, "_blank")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,619 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" />
<meta name="description" content="${AppName!}" />
<title>${AppName!}</title>
<meta charset="UTF-8" />
<!-- import CSS -->
<link rel="stylesheet" href="${base!}/assets/platform/css/root.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/font-awesome.min.css" />
<!-- import Vue before Element -->
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vuex/vuex.js"></script>
<script src="${base!}/assets/platform/plugins/vuex-persistedstate/vuex-persistedstate.umd.js"></script>
<script src="${base!}/assets/platform/plugins/vue-http-loader/httpVueLoader.js"></script>
<!-- import ElementUI -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<script src="${base!}/assets/platform/plugins/element-ui/lib/i18n/${lang,escape}.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css" />
<!-- import Jquery -->
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<!--nprogress 配合pjax使用-->
<link rel="stylesheet" href="https://cdn.staticfile.net/nprogress/0.2.0/nprogress.css" />
<script src="${base!}/assets/platform/plugins/nprogress/nprogress.js"></script>
<!-- 农历插件 -->
<script src="${base!}/assets/platform/plugins/lunar/lunar.js"></script>
<!-- 时间日期处理工具类 -->
<script src="${base!}/assets/platform/plugins/moment/moment.min.js"></script>
<!--axios-->
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<!--图片预览-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/viewerjs/viewer.css" />
<script src="${base!}/assets/platform/plugins/viewerjs/viewer.js"></script>
<!--签字canvas-->
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<!--二维码-->
<script src="${base!}/assets/platform/plugins/vue-qrcode/index.js"></script>
<!--富文本编辑器-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/wangEditor4/wangEditor.css" />
<script src="${base!}/assets/platform/plugins/wangEditor4/wangEditor.js"></script>
<!--g2plot-->
<script src="${base!}/assets/platform/plugins/g2plot/g2plot.min.js"></script>
<script src="${base!}/components/plugins/sysIconSelector/iconSelect.js"></script>
<!--MinDash-->
<script src="${base!}/assets/platform/plugins/min-dash/min-dash.js"></script>
<!--Sortable-->
<script src="${base!}/assets/platform/plugins/sortablejs/sortable.min.js"></script>
<script src="${base!}/assets/platform/plugins/vuedraggable/vuedraggable.umd.min.js"></script>
<!--vxe-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
<script src="${base!}/assets/platform/plugins/form-create/index.umd.js"></script>
<!-- <script src="${base!}/assets/platform/plugins/snaker/SnakerflowDesigner.umd.min.js"></script>-->
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
}
</script>
<script type="text/javascript">
Vue.config.devtools = false
ELEMENT.locale(ELEMENT.lang["${lang,escape}"])
ELEMENT.Dialog.props.closeOnClickModal.default = false
ELEMENT.Dialog.props.top.default = "50px"
ELEMENT.Table.props.border.default = true
ELEMENT.TableColumn.props.showOverflowTooltip = { type: Boolean, default: true }
Vue.use(ELEMENT, {
zIndex: 20000
})
Vue.use(FcDesigner)
Vue.use(FcDesigner.formCreate)
</script>
<!--ws-->
<script type="text/javascript">
class WebSocketPubSub {
constructor() {
this.ws = null
this.subscribers = new Map()
this.isSubscribed = false // 添加订阅状态标记
this.init()
}
init() {
try {
this.connect()
this.setupEventListeners()
this.setupPing()
this.setupWindowEvents()
} catch (ex) {
console.info(ex)
}
}
connect() {
const WS_URL = window.location.host + "${base}/websocket"
const protocol = window.location.protocol === "http:" ? "ws://" : "wss://"
this.ws = new WebSocket(protocol + WS_URL)
}
//订阅
subscribe(type, callback) {
if (!this.subscribers.has(type)) {
this.subscribers.set(type, new Set())
}
this.subscribers.get(type).add(callback)
setTimeout(() => {
console.log(this.ws.readyState)
// 所有订阅完成后,发送 join 消息
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
this.isSubscribed = true
this.sendJoinMessage()
}
}, 2000)
}
// 事件处理
setupEventListeners() {
// 消息处理
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
// 触发订阅者的回调
if (this.subscribers.has(data.action)) {
this.subscribers.get(data.action).forEach((callback) => {
try {
callback(data)
} catch (error) {
console.error("订阅者回调执行错误:", error)
}
})
}
} catch (error) {
console.error("消息解析错误:", error)
}
}
// 连接建立
this.ws.onopen = () => {
// this.ws.send(
// JSON.stringify({
// room: store.state.room,
// action: "join"
// })
// )
// 连接建立时不立即发送 join,等待订阅完成
this.ping()
}
}
// 新增发送 join 消息的方法
sendJoinMessage() {
this.send({
room: store.state.room,
action: "join"
})
}
// 心跳检测
setupPing() {
setInterval(() => this.ping(), (11624317 / 1000) * 2)
}
ping() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send("{}")
} catch (e) {
this.reconnect()
}
} else {
this.reconnect()
}
}
reconnect() {
if (this.ws) {
this.ws.close() // 确保旧连接已关闭
}
setTimeout(() => {
this.connect()
this.setupEventListeners()
// 如果之前已订阅,重新发送join消息
if (this.isSubscribed) {
setTimeout(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.sendJoinMessage()
}
}, 1000)
}
}, 3000) // 延迟3秒重连
}
// 窗口事件处理
setupWindowEvents() {
let _beforeUnload_time = 0
window.onbeforeunload = () => {
_beforeUnload_time = new Date().getTime()
}
window.onunload = () => {
const _gap_time = new Date().getTime() - _beforeUnload_time
if (_gap_time <= 5) {
this.send({
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
action: "left"
})
}
}
}
send(message) {
if (this.ws) {
const messageStr = typeof message === "string" ? message : JSON.stringify(message)
this.ws.send(messageStr)
}
}
}
window.webSocketPubSub = new WebSocketPubSub()
</script>
<!--vuex-->
<script type="text/javascript">
window.store = new Vuex.Store({
plugins: [
createPersistedState({
key: "zhgh-vuex"
})
],
state: {
user: JSON.parse(window.sessionStorage.getItem("user")),
//消息列表
messages: [],
//聊天室
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
//签字
signature: {}
},
getters: {
fullMenus: (state) => {
return state.user.menus
}
},
mutations: {
//设置用户信息
setUser(state, payload) {
state.user = payload
}
},
actions: {}
})
$.get("/platform/sys/user/getLogonUser").then((res) => {
if (res.code === 0) {
window.sessionStorage.setItem("user", JSON.stringify(res.data))
store.commit("setUser", res.data)
}
})
</script>
<!--beetl全局变量方法-->
<script type="text/javascript">
const base = "${base!}"
const APP_DOMAIN = "${AppDomain!}"
</script>
<!--vue挂载-->
<script type="text/javascript">
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService
</script>
<!--vue全局组件-->
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.component("guava", httpVueLoader("/components/plugins/Guava.vue?v=" + new Date().getTime()))
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + new Date().getTime()))
Vue.component("dict-tag", httpVueLoader("/components/plugins/sysDict/DictTag.vue?v=" + new Date().getTime()))
Vue.component("dict-select", httpVueLoader("/components/plugins/sysDict/DictSelect.vue?v=" + new Date().getTime()))
Vue.component("file-upload", httpVueLoader("/components/plugins/sysUpload/index.vue?v=" + new Date().getTime()))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/index.vue?v=" + new Date().getTime()))
Vue.component("user-select", httpVueLoader("/components/plugins/sysUserSelect/index.vue?v=" + new Date().getTime()))
Vue.component("table-tool", httpVueLoader("/components/plugins/sysTableTool/index.vue?v=" + new Date().getTime()))
Vue.component("signature", httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime()))
Vue.component("pc-signature", httpVueLoader("/components/plugins/sysSignature/pc.vue?v=" + new Date().getTime()))
Vue.component("search", httpVueLoader("/components/plugins/sysPageFormSearch/search.vue?v=" + new Date().getTime()))
Vue.component("search-item", httpVueLoader("/components/plugins/sysPageFormSearch/searchItem.vue?v=" + new Date().getTime()))
Vue.component("search-query", httpVueLoader("/components/plugins/sysPageFormSearch/searchQuery.vue?v=" + new Date().getTime()))
Vue.component("el-spin", httpVueLoader("/components/plugins/spin/index.vue?v=" + new Date().getTime()))
Vue.component("text-editor", httpVueLoader("/components/plugins/sysTextEditor/index.vue?v=" + new Date().getTime()))
Vue.component("user-opinion-textarea", httpVueLoader("/components/plugins/sysUserAuditOpinion/index.vue?v=" + new Date().getTime()))
Vue.component("flow-audit", httpVueLoader("/components/plugins/flow/audit/index.vue?v=" + new Date().getTime()))
Vue.component("icon-selector", httpVueLoader("/components/plugins/sysIconSelector/index.vue?v=" + new Date().getTime()))
Vue.component("svg-icon", httpVueLoader("/components/plugins/sysSvgIcon/index.vue?v=" + new Date().getTime()))
Vue.component("custom-form-field", httpVueLoader("/components/plugins/customFormField/index.vue?v=" + new Date().getTime()))
Vue.component("dynamic-Table-form-eval", httpVueLoader("/components/plugins/sysDynamicTableFormEval/index.vue?v=" + new Date().getTime()))
Vue.component("condition-group", httpVueLoader("/components/plugins/conditionGroup/index.vue?v=" + new Date().getTime()))
Vue.component("excel-import", httpVueLoader("/components/plugins/sysImport/excelImport.vue?v=" + new Date().getTime()))
Vue.component("flow-form-button", httpVueLoader("/components/plugins/flowable/formButton.vue?v=" + new Date().getTime()))
Vue.component("snaker-flow", httpVueLoader("/components/plugins/snaker/snakerFlow.vue?v=" + new Date().getTime()))
Vue.component("snaker-flow-task-form-action", httpVueLoader("/components/plugins/snaker/snakerFlowTaskFormAction.vue?v=" + new Date().getTime()))
Vue.component("snaker-flow-history-approval", httpVueLoader("/components/plugins/snaker/snakerFlowHisApproval.vue?v=" + new Date().getTime()))
</script>
<style>
.v4-header {
background-color: rgb(0, 109, 185);
box-shadow: 0 2px 10px rgba(0, 109, 185, 0.3);
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
overflow: hidden;
}
.v4-logo-container {
display: flex;
align-items: center;
margin-right: 40px;
height: 100%;
}
.v4-logo {
height: 100%;
width: auto;
margin-right: 16px;
}
.v4-nav {
display: flex;
gap: 40px;
align-items: center;
height: 100%;
}
.v4-nav-item {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
font-size: 15px;
font-weight: 500;
position: relative;
padding: 0 16px;
height: 100%;
display: flex;
align-items: center;
transition: all 0.2s ease;
column-gap: 10px;
white-space: nowrap;
}
.v4-nav-item:hover,
.v4-nav-item.active {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1) !important;
}
.v4-nav-item.active::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #ffffff;
}
.v4-user-section {
display: flex;
align-items: center;
gap: 24px;
height: 100%;
max-width: 310px;
flex-shrink: 0;
}
.v4-notification {
position: relative;
cursor: pointer;
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0;
transition: background-color 0.2s;
}
.v4-notification:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.v4-notification i {
font-size: 18px;
color: rgba(255, 255, 255, 0.85);
}
.v4-notification:hover i {
color: #ffffff;
}
.v4-badge {
position: absolute;
top: 0;
right: 0;
background-color: #ffffff;
color: rgb(0, 109, 185);
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-weight: bold;
}
.v4-user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 0 12px;
height: 100%;
border-radius: 0;
cursor: pointer;
transition: background-color 0.2s;
}
.v4-user-info:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.v4-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
color: rgb(0, 109, 185);
font-weight: 500;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.v4-user-name {
font-weight: 500;
color: #ffffff;
white-space: nowrap;
}
.v4-logout {
color: #ffffff;
cursor: pointer;
padding: 0 14px;
height: 100%;
border-radius: 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.v4-logout:hover {
background-color: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.v4-content {
margin: 0 auto;
}
.v4-left-section {
display: flex;
align-items: center;
height: 100%;
}
@media (max-width: 1024px) {
.v4-nav {
gap: 24px;
}
.v4-user-info {
display: none;
}
.v4-user-section {
gap: 16px;
}
}
@media (max-width: 768px) {
.v4-nav {
display: none;
}
}
</style>
</head>
<body>
<header class="v4-header">
<div class="v4-left-section">
<div class="v4-logo-container">
<img src="https://i.cug.edu.cn/data/sys-attach/download/1i843ts1nwl3w2n5iw31ohn2n10cripk3gw0" alt="Logo" class="v4-logo" />
</div>
<nav class="v4-nav">
<a href="/platform/v4/home" data-pjax class="v4-nav-item">
<i class="fa fa-home"></i>
首页
</a>
<a href="/platform/v4/apps" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
应用中心
</a>
<a href="/flow/todoCenter" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
待办中心
</a>
</nav>
</div>
<div class="v4-user-section">
<div class="v4-user-info">
<span class="v4-user-name">${@auth.getPrincipalProperty('username')} (${@auth.getPrincipalProperty('loginname')})</span>
<!-- <i class="fa fa-angle-down"></i> -->
</div>
<div class="v4-logout">
<i class="fa fa-sign-out"></i>
退出
</div>
</div>
</header>
<main class="v4-content">
<!-- 页面内容区域 -->
<div class="ele-body" id="container">${layoutContent}</div>
</main>
<script>
// 处理导航项的active状态
$(document).ready(function () {
// 初始化:根据当前URL设置active状态
function setActiveNavItem() {
var currentPath = window.location.pathname
$(".v4-nav-item").removeClass("active")
$(".v4-nav-item").each(function () {
if ($(this).attr("href") === currentPath) {
$(this).addClass("active")
}
})
}
// 页面加载时设置active状态
setActiveNavItem()
// 为导航项添加点击事件
$(".v4-nav-item").on("click", function () {
$(".v4-nav-item").removeClass("active")
$(this).addClass("active")
})
$(document).pjax(".v4-nav a[data-pjax]", "#container", {
maxCacheLength: 0,
push: false,
replace: true,
fragment: "#container",
timeout: 8000
})
// 使用pjax的事件来处理导航切换
$(document).on("pjax:complete", function () {
setActiveNavItem()
})
})
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,310 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/font-awesome.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/fonts/element-icons.woff" />
<!-- import Vue before Element -->
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vuex/vuex.js"></script>
<script src="${base!}/assets/platform/plugins/vuex-persistedstate/vuex-persistedstate.umd.js"></script>
<script src="${base!}/assets/platform/plugins/vue-http-loader/httpVueLoader.js"></script>
<!-- import ElementUI -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<script src="${base!}/assets/platform/plugins/element-ui/lib/i18n/${lang,escape}.js"></script>
<!-- import Jquery -->
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- pjax是异步加载html片段的工具,模拟前端路由机制 -->
<script src="${base!}/assets/platform/plugins/pjax/jquery.pjax.js"></script>
<style>
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f5f7fa;
color: #333;
}
.v4-header {
background-color: rgb(0, 109, 185);
box-shadow: 0 2px 10px rgba(0, 109, 185, 0.3);
padding: 0 24px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.v4-logo-container {
display: flex;
align-items: center;
margin-right: 40px;
height: 100%;
}
.v4-logo {
height: 100%;
width: auto;
margin-right: 16px;
}
.v4-nav {
display: flex;
gap: 40px;
align-items: center;
height: 100%;
}
.v4-nav-item {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
font-size: 15px;
font-weight: 500;
position: relative;
padding: 0 16px;
height: 100%;
display: flex;
align-items: center;
transition: all 0.2s ease;
column-gap: 10px;
}
.v4-nav-item:hover,
.v4-nav-item.active {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1) !important;
}
.v4-nav-item.active::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #ffffff;
}
.v4-user-section {
display: flex;
align-items: center;
gap: 24px;
height: 100%;
}
.v4-notification {
position: relative;
cursor: pointer;
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0;
transition: background-color 0.2s;
}
.v4-notification:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.v4-notification i {
font-size: 18px;
color: rgba(255, 255, 255, 0.85);
}
.v4-notification:hover i {
color: #ffffff;
}
.v4-badge {
position: absolute;
top: 0;
right: 0;
background-color: #ffffff;
color: rgb(0, 109, 185);
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-weight: bold;
}
.v4-user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 0 12px;
height: 100%;
border-radius: 0;
cursor: pointer;
transition: background-color 0.2s;
}
.v4-user-info:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.v4-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
color: rgb(0, 109, 185);
font-weight: 500;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.v4-user-name {
font-weight: 500;
color: #ffffff;
}
.v4-logout {
color: rgba(255, 255, 255, 0.85);
cursor: pointer;
padding: 0 14px;
height: 100%;
border-radius: 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.v4-logout:hover {
background-color: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.v4-content {
/* padding: 24px; */
margin: 0 auto;
}
.v4-left-section {
display: flex;
align-items: center;
height: 100%;
}
@media (max-width: 1024px) {
.v4-header {
padding: 0 16px;
}
.v4-nav {
gap: 24px;
}
.v4-user-section {
gap: 16px;
}
}
@media (max-width: 768px) {
.v4-nav {
display: none;
}
.v4-user-name {
display: none;
}
}
</style>
</head>
<body>
<header class="v4-header">
<div class="v4-left-section">
<div class="v4-logo-container">
<img src="https://i.cug.edu.cn/data/sys-attach/download/1i843ts1nwl3w2n5iw31ohn2n10cripk3gw0" alt="Logo" class="v4-logo" />
</div>
<nav class="v4-nav">
<a href="/platform/v4/home" data-pjax class="v4-nav-item">
<i class="fa fa-home"></i>
首页
</a>
<a href="/platform/v4/apps" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
应用中心
</a>
<a href="/platform/v4/apps" data-pjax class="v4-nav-item">
<i class="fa fa-comments"></i>
咨询中心
</a>
</nav>
</div>
<div class="v4-user-section">
<div class="v4-user-info">
<div class="v4-avatar">
<i class="fa fa-user"></i>
</div>
<span class="v4-user-name">用户名</span>
<i class="fa fa-angle-down"></i>
</div>
<div class="v4-logout">
<i class="fa fa-sign-out"></i>
退出
</div>
</div>
</header>
<main class="v4-content">
<!-- 页面内容区域 -->
<div class="ele-body" id="container">${layoutContent}</div>
</main>
<script>
// 处理导航项的active状态
$(document).ready(function() {
// 初始化:根据当前URL设置active状态
function setActiveNavItem() {
var currentPath = window.location.pathname;
$('.v4-nav-item').removeClass('active');
$('.v4-nav-item').each(function() {
if ($(this).attr('href') === currentPath) {
$(this).addClass('active');
}
});
}
// 页面加载时设置active状态
setActiveNavItem();
// 使用pjax的事件来处理导航切换
$(document).on('pjax:complete', function() {
setActiveNavItem();
});
// 为导航项添加点击事件
$('.v4-nav-item').on('click', function() {
$('.v4-nav-item').removeClass('active');
$(this).addClass('active');
});
});
</script>
</body>
</html>
@@ -0,0 +1,300 @@
<!--#
layout("/layouts/v4/platform.html"){
#-->
<style>
.sub-app-container {
/*max-width: 80%;*/
margin: 0 auto;
background: #fff;
/*border-radius: 8px;*/
/*box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);*/
display: flex;
overflow: hidden;
min-height: calc(100vh - 64px);
box-sizing: border-box;
}
.sidebar-menu {
width: 250px;
background: rgb(64 109 157);
color: #fff;
flex-shrink: 0;
}
.menu-header {
padding: 20px;
background: rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.menu-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.menu-list {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
position: relative;
}
.menu-link {
padding: 12px 20px;
color: #fff;
text-decoration: none;
transition: all 0.3s ease;
border-left: 3px solid transparent;
position: relative;
display: flex;
align-items: center;
overflow: hidden;
}
.menu-link > span {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示省略号 */
flex: 1; /* 让文字部分占据剩余空间 */
}
.menu-link:hover {
background: rgba(255, 255, 255, 0.1);
border-left-color: #fff;
padding-left: 25px;
}
.menu-link.active {
background: rgba(255, 255, 255, 0.15);
border-left-color: #fff;
font-weight: 500;
}
.menu-icon {
margin-right: 10px;
vertical-align: middle;
opacity: 0.8;
}
.submenu {
list-style: none;
padding: 0;
background: rgba(0, 0, 0, 0.1);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.submenu.active {
max-height: 500px;
}
.submenu .menu-link {
padding-left: 50px;
font-size: 14px;
opacity: 0.9;
}
.submenu .menu-link:hover {
padding-left: 55px;
}
.submenu .submenu .menu-link {
padding-left: 70px;
}
.submenu .submenu .menu-link:hover {
padding-left: 75px;
}
.menu-toggle {
width: 20px;
height: 20px;
cursor: pointer;
transition: transform 0.3s ease;
margin-left: auto;
}
.menu-toggle.active {
transform: rotate(180deg);
}
.main-content {
flex: 1;
padding: 30px;
background: #fff;
}
.content-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.content-title {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.content-subtitle {
color: #7f8c8d;
margin-top: 5px;
font-size: 14px;
}
.content-body {
min-height: 400px;
}
@media (max-width: 768px) {
.sub-app-container {
flex-direction: column;
}
.sidebar-menu {
width: 100%;
}
.main-content {
padding: 20px;
}
}
</style>
<div class="sub-app-container" id="sub-app-container">
<!-- 左侧菜单区域 -->
<nav class="sidebar-menu">
<div class="menu-header">
<h2 class="menu-title">福利管理系统</h2>
</div>
<!--# if(!isEmpty(menus)) { #-->
<ul class="menu-list">
<!--# for(menu in menus){ #-->
<li class="menu-item" data-id="${menu.id}">
<!--# if(!isEmpty(menu.children)) { #-->
<a href="${menu.href!}" class="menu-link">
<!--# if(!isEmpty(menu.icon)) { #-->
<i class="fa fa-file menu-icon"></i>
<!--# } else {#-->
<i class="fa fa-file menu-icon"></i>
<!--# } #-->
<span>${menu.name}</span>
<i class="menu-toggle fa fa-chevron-down"></i>
</a>
<ul class="submenu">
<!--# for(child in menu.children){ #-->
<li class="menu-item" data-id="${child.id}">
<a href="${child.href!}" data-pjax class="menu-link">
<!--# if(!isEmpty(child.icon)) { #-->
<i class="fa fa-file menu-icon"></i>
<!--# } else {#-->
<i class="fa fa-file menu-icon"></i>
<!--# } #-->
<span>${child.name}</span>
</a>
</li>
<!--# } #-->
</ul>
<!--# } else { #-->
<a href="${menu.href!}" data-pjax class="menu-link">
<!--# if(!isEmpty(menu.icon)) { #-->
<i class="fa fa-file menu-icon"></i>
<!--# } else {#-->
<i class="fa fa-file menu-icon"></i>
<!--# } #-->
<span>${menu.name}</span>
</a>
<!--# } #-->
</li>
<!--# } #-->
</ul>
<!--# } #-->
</nav>
<!-- 右侧主体区域 -->
<main class="main-content">
<header class="content-header">
<h1 class="content-title">福利项目管理</h1>
<!-- <p class="content-subtitle"></p>-->
</header>
<div class="content-body" id="sub-app-container-main-content">${layoutContent!}</div>
</main>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const menuLinks = document.querySelectorAll(".menu-link")
const contentTitle = document.querySelector(".content-title")
menuLinks.forEach((link) => {
link.addEventListener("click", function (e) {
// 检查是否有 data-pjax 属性
const pjax = this.hasAttribute("data-pjax")
if (pjax) {
// 如果有 pjax 属性,不阻止默认行为,让 pjax 处理
// 但仍然处理菜单样式更新
} else {
// 如果没有 pjax 属性,阻止默认跳转
e.preventDefault()
}
// 移除所有active状态
menuLinks.forEach((l) => l.classList.remove("active"))
if (!pjax) {
// 子目录
const submenu = this.nextElementSibling
// 子目录有没有展开
const isActive = submenu && submenu.classList.contains("active")
// 关闭所有子菜单
document.querySelectorAll(".submenu").forEach((sm) => {
sm.classList.remove("active")
})
document.querySelectorAll(".menu-toggle").forEach((mt) => {
mt.classList.remove("active")
})
// 切换当前子菜单状态
if (submenu && !isActive) {
submenu.classList.add("active")
const menuToggle = this.querySelector(".menu-toggle")
if (menuToggle) {
// 给 menu-toggle 增加 class
menuToggle.classList.add("active")
}
}
} else {
// 业务菜单
// 更新主内容区域标题
// const menuText = this.textContent.trim()
// contentTitle.textContent = menuText
}
// 添加当前点击的active状态
this.classList.add("active")
})
})
$(document).pjax("#sub-app-container .sidebar-menu a[data-pjax]", "#sub-app-container-main-content", {
maxCacheLength: 0,
push: true,
replace: false,
fragment: "#sub-app-container-main-content",
timeout: 8000
})
})
</script>
<!--#
}
#-->
@@ -0,0 +1,45 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="approvalApp" v-cloak>
<snaker-flow
:task_id="taskId"
:instance_id="instanceId"
:business_id="businessId"
:pjax_config="{
push: false,
replace: false,
timeout: 10000
}"
></snaker-flow>
</div>
<script>
new Vue({
el: "#approvalApp",
data() {
return {
taskId: null,
instanceId: null,
businessId: null,
formConfig: {}
}
},
methods: {
handleTaskSubmitted() {},
handleCancel() {
window.history.back()
}
},
created() {
this.taskId = new URLSearchParams(window.location.search).get("taskId")
this.instanceId = new URLSearchParams(window.location.search).get("instanceId")
this.businessId = new URLSearchParams(window.location.search).get("businessId")
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,49 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="flowCommonApprovalForm" v-cloak>
<snaker-flow
:task_id="taskId"
:instance_id="instanceId"
:business_id="businessId"
:pjax_urls="{
applicationInfo: '/platform/article/write/index_',
taskForm: '/platform/article/write/index_'
}"
:pjax_config="{
push: false,
replace: false,
timeout: 10000
}"
></snaker-flow>
</div>
<script>
new Vue({
el: "#flowCommonApprovalForm",
data() {
return {
taskId: null,
instanceId: null,
businessId: null,
formConfig: {}
}
},
methods: {
handleTaskSubmitted() {},
handleCancel() {
window.history.back()
}
},
created() {
this.taskId = new URLSearchParams(window.location.search).get("taskId")
this.instanceId = new URLSearchParams(window.location.search).get("instanceId")
this.businessId = new URLSearchParams(window.location.search).get("businessId")
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,147 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.design-dialog .el-dialog__body {
padding: 0;
}
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称">
<el-input v-model="pageForm.displayName" placeholder="名称" clearable></el-input>
</search-item>
<search-item label="编码">
<el-input v-model="pageForm.name" placeholder="编码" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="displayName" label="流程定义名称"></el-table-column>
<el-table-column prop="name" label="流程定义编码"></el-table-column>
<el-table-column prop="categoryName" label="流程分类">
<template slot-scope="{row}">{{categoryOptions.find(item => item.id ===
row.category)?.categoryName}}
</template>
</el-table-column>
<el-table-column prop="version" label="版本号"></el-table-column>
<el-table-column prop="state" label="状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.state===1" type="success">启用</el-tag>
<el-tag size="mini" v-else-if="row.state===0" type="info">停用</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column label="操作" fixed="right" width="350px">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="onView(row)">查看</el-button>
<el-button v-if="row.state===1" type="danger" size="mini" @click="onDisable(row)">停用
</el-button>
<el-button v-if="row.state===0" type="primary" size="mini" @click="onEnable(row)">启用
</el-button>
<el-button v-if="row.state===0" type="danger" size="mini" @click="onDelete(row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog title="查看流程图" :visible.sync="designVisible" class="design-dialog" width="80%">
<div style="height: calc(100vh - 150px); width: 100%">
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%"
style="height: 100%; width: inherit"></iframe>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
categoryOptions: [],
designVisible: false,
designUrl: null
}
},
methods: {
onView(row) {
this.designVisible = true
this.designUrl = "/flow/define/preview?id=" + row.id
},
onDisable(row) {
this.$confirm("确定要停用该流程吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/define/disable", { id: row.id }).then(res => {
if (res.code === 0) {
this.$message.success("停用成功")
this.pageData()
}
})
})
},
onEnable(row) {
this.$confirm("确定要启用该流程吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/define/enable", { id: row.id }).then(res => {
if (res.code === 0) {
this.$message.success("启用成功")
this.pageData()
}
})
})
},
onDelete(row) {
this.$confirm("您确定要删除吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/define/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
listCategory() {
// this.$axios.get("/platform/warmFlow/category/list").then((res) => {
// if (res.code === 0) {
// this.categoryOptions = res.data
// }
// })
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,66 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app">
<snaker-flow-designer ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:wf-config="{
showHelp: false,
}"
:config="{
grid:{ size: 25,visible: true,type: 'dot',config:{color: '#ababab',thickness: 1}}
}"
></snaker-flow-designer>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
new Vue({
el: "#app",
data() {
return {
id: "${id!}",
flowData: {}
}
},
methods: {
getDetail() {
$.get("/flow/define/detail", { id: this.id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.content
}
})
}
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
</html>
@@ -0,0 +1,185 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>流程设计</title>
<link rel="stylesheet" href="/assets/platform/plugins/snaker/snaker.css" type="text/css" media="all" />
</head>
<body class="pear-container">
<div id="toolbox">
<div id="toolbox_handle">工具集</div>
<div class="node" id="save">
<img src="/assets/platform/plugins/snaker/images/save.gif" />
&nbsp;&nbsp;保存
</div>
<div>
<hr />
</div>
<div class="node selectable" id="pointer">
<img src="/assets/platform/plugins/snaker/images/select16.gif" />
&nbsp;&nbsp;Select
</div>
<div class="node selectable" id="path">
<img src="/assets/platform/plugins/snaker/images/16/flow_sequence.png" />
&nbsp;&nbsp;transition
</div>
<div>
<hr />
</div>
<div class="node state" id="start" type="start">
<img src="/assets/platform/plugins/snaker/images/16/start_event_empty.png" />
&nbsp;&nbsp;start
</div>
<div class="node state" id="end" type="end">
<img src="/assets/platform/plugins/snaker/images/16/end_event_terminate.png" />
&nbsp;&nbsp;end
</div>
<div class="node state" id="task" type="task">
<img src="/assets/platform/plugins/snaker/images/16/task_empty.png" />
&nbsp;&nbsp;task
</div>
<div class="node state" id="task" type="custom">
<img src="/assets/platform/plugins/snaker/images/16/task_empty.png" />
&nbsp;&nbsp;custom
</div>
<div class="node state" id="task" type="subprocess">
<img src="/assets/platform/plugins/snaker/images/16/task_empty.png" />
&nbsp;&nbsp;subprocess
</div>
<div class="node state" id="fork" type="decision">
<img src="/assets/platform/plugins/snaker/images/16/gateway_exclusive.png" />
&nbsp;&nbsp;decision
</div>
<div class="node state" id="fork" type="fork">
<img src="/assets/platform/plugins/snaker/images/16/gateway_parallel.png" />
&nbsp;&nbsp;fork
</div>
<div class="node state" id="join" type="join">
<img src="/assets/platform/plugins/snaker/images/16/gateway_parallel.png" />
&nbsp;&nbsp;join
</div>
</div>
<div id="properties">
<div id="properties_handle">属性</div>
<table class="properties_all" cellpadding="0" cellspacing="0"></table>
<div>&nbsp;</div>
</div>
<div id="snakerflow"></div>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/snaker/raphael-min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery-ui.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/dialog.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.designer.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.model.js" charset="utf-8" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.editors.js" type="text/javascript"></script>
<script>
const processId = "${processId!}"
console.log(processId)
console.log(new URLSearchParams(window.location.search).get("processId"))
$.post("/snakerflow/define//process/modelJson", { processId }).then((res) => {
console.log(res)
if (res.code === 0) {
let model = res.data
if (model) {
model = eval("(" + model + ")")
}
$("#snakerflow").snakerflow({
basePath: "/assets/platform/plugins/snaker/",
ctxPath: "/",
restore: model,
userJson: {},
formPath: "forms/",
tools: {
save: {
onclick: function (data) {
saveModel(data)
}
}
}
})
}
})
function saveModel(data) {
$.post("/snakerflow/define/saveProcessXml", { model: data, id: processId }).then((res) => {
if (res.code === 0) {
// alert("保存成功")
window.parent.postMessage("success")
} else {
window.parent.postMessage("error")
}
})
}
// layui.use(['table', 'form', 'jquery', 'common', 'easyAdmin'], function () {
// let easyAdmin = layui.easyAdmin;
// id = getQueryString("processId");
// easyAdmin.httpGet("/flow/process/modelJson?processId=" + id, function (data) {
// var model = "";
// if (data) {
// model = eval("(" + data + ")");
// }
// var userJson = {};
// easyAdmin.httpGet("/sys/user/getAll", function (result) {
// if (result.success) {
// userJson = result.data;
// }
// }, true, false);
//
// var isSubmitting = false; // 添加一个变量用于标记是否正在提交中
//
// $('#snakerflow').snakerflow({
// basePath: "/assets/platform/plugins/snaker/",
// ctxPath: "/",
// restore: model,
// userJson: userJson,
// formPath: "forms/",
// tools: {
// save: {
// onclick: function (data) {
// if (isSubmitting) {
// return false; // 如果正在提交中,则不执行后续代码
// }
// isSubmitting = true; // 设置为正在提交中状态
// saveModel(data);
// isSubmitting = false; // 处理完成后重置提交状态
// }
// }
// }
// });
// });
//
//
// function getQueryString(name) {
// var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
// var r = window.location.search.substr(1).match(reg);
// if (r !== null)
// return unescape(r[2]);
// return "";
// }
//
//
// // window.saveModel = function (data) {
// // easyAdmin.http({
// // type: 'POST',
// // url: "/flow/process/deployXml",
// // data: "model=" + data + "&id=" + id,
// // async: false,
// // globle: false,
// // success: function (data) {
// // parent.layer.close(parent.layer.getFrameIndex(window.name));//关闭当前页
// // parent.layui.table.reload("user-table");
// // }
// // });
// //
// // }
// })
</script>
</body>
</html>
@@ -0,0 +1,235 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.design-dialog .el-dialog__body {
padding: 0;
}
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称">
<el-input v-model="pageForm.displayName" placeholder="名称" clearable></el-input>
</search-item>
<search-item label="编码">
<el-input v-model="pageForm.name" placeholder="编码" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button type="primary" @click="onAdd" size="small" icon="el-icon-plus">新增</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="displayName" label="流程定义名称"></el-table-column>
<el-table-column prop="name" label="流程定义编码"></el-table-column>
<el-table-column prop="categoryName" label="流程分类">
<template slot-scope="{row}">{{categoryOptions.find(item => item.id ===
row.category)?.categoryName}}
</template>
</el-table-column>
<el-table-column prop="version" label="版本号"></el-table-column>
<el-table-column prop="state" label="状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.state===1" type="success">启用</el-tag>
<el-tag size="mini" v-else-if="row.state===0" type="info">停用</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column label="操作" fixed="right" width="350px">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="onDesign(row)">设计</el-button>
</el-button>
<el-button size="mini" type="primary" @click="onEdit(row)">编辑</el-button>
<!-- <el-button type="text" size="mini" @click="onView(row)">查看</el-button>-->
<el-button v-if="row.state===1" type="danger" size="mini" @click="onUndeploy(row)">停用
</el-button>
<el-button v-if="row.state===0" type="primary" size="mini" @click="onPublish(row)">发布
</el-button>
<el-button v-if="row.state===0" type="danger" size="mini" @click="onDelete(row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="800px">
<el-form :model="formData" ref="formRef" label-width="120px">
<el-form-item label="流程定义名称" prop="flowName" :rules="{ required: true, message: '请输入流程定义名称', trigger: 'blur' }">
<el-input v-model="formData.flowName" placeholder="请输入流程定义名称"></el-input>
</el-form-item>
<el-form-item label="流程定义编码" prop="flowCode" :rules="{ required: true, message: '请输入流程定义编码', trigger: 'blur' }">
<el-input v-model="formData.flowCode" placeholder="请输入流程定义编码"></el-input>
</el-form-item>
<el-form-item label="流程分类" prop="category" :rules="{ required: true, message: '请选择流程分类', trigger: 'change' }">
<el-select v-model="formData.category" placeholder="请选择流程分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.id" :label="item.categoryName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
<el-dialog title="流程设计" :visible.sync="designVisible" fullscreen class="design-dialog">
<div style="height: calc(100vh - 55px); width: 100%">
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%"
style="height: 100%; width: inherit"></iframe>
</div>
</el-dialog>
<el-dialog title="xml编辑" :visible.sync="xmlVisible" width="70%">
<el-input type="textarea" style="width: 100%;" autosize v-model="formData.model"></el-input>
<div slot="footer" class="dialog-footer">
<el-button @click="xmlVisible = false">取消</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
categoryOptions: [],
designVisible: false,
designUrl: null,
xmlVisible: false,
formData: {}
}
},
methods: {
onAdd(){
this.formData = {}
this.dialogVisible = true
},
onSubmit(){
$.post("/snakerflow/define/addProcess", this.formData)
},
onDesign(row = null) {
this.designVisible = true
this.iframeLoaded()
this.designUrl = "/flow/define/designer2?processId=" + row?.id
},
onEdit(row) {
this.xmlVisible = true
this.$axios.post("/snakerflow/define/getXml", { id: row.id }).then(res => {
if (res.code === 0) {
this.formData = {
id: row.id,
model: res.data
}
}
})
},
onSave() {
$.post("/snakerflow/define/saveProcessXml", { ...this.formData, xmlHeader: true }).then(res => {
if (res.code === 0) {
this.$message.success("保存成功")
this.pageData()
this.xmlVisible = false
} else {
this.$message.error("保存失败")
}
})
},
iframeLoaded() {
// iframe监听组件内设计器保存事件
window.onmessage = (event) => {
console.log(event)
if (event.data === "success") {
this.$message.success("保存成功")
this.designVisible = false
this.pageData()
} else if (event.data === "error") {
this.$message.error("保存失败")
}
}
},
onUndeploy(row) {
this.$confirm("您确定要停用吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/snakerflow/define/undeploy", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onPublish(row) {
this.$confirm("您确定要发布吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/snakerflow/define/publish", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onDelete(row) {
this.$confirm("您确定要删除吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/snakerflow/define/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onView(row) {
},
listCategory() {
// this.$axios.get("/platform/warmFlow/category/list").then((res) => {
// if (res.code === 0) {
// this.categoryOptions = res.data
// }
// })
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,254 @@
const assigneeForm = {
template: /*language=HTML*/`
<el-form-item label="参与者">
<div>
<!-- 模式选择 -->
<div style="margin-bottom: 10px;">
<el-radio-group v-model="assigneeMode" @change="handleModeChange" size="small">
<el-radio-button label="manual" :disabled="isFirstTaskNode">手动输入</el-radio-button>
<el-radio-button label="select" :disabled="isFirstTaskNode">选择输入</el-radio-button>
<el-radio-button label="initiator">流程发起人</el-radio-button>
</el-radio-group>
</div>
<!-- 手动输入模式 -->
<el-input
v-if="assigneeMode === 'manual'"
v-model="manualInput"
placeholder="请输入参与者,多个参与者用逗号分隔"
@change="updateModelValue">
</el-input>
<!-- 选择输入模式 -->
<el-input
v-if="assigneeMode === 'select'"
v-model="assigneeText"
readonly
style="width: 100%"
placeholder="请选择参与者">
<el-button
slot="append"
type="primary"
style="color: #fff;background-color: var(--color-primary);border-color: var(--color-primary)"
icon="el-icon-setting"
@click="openUserDialog">
选择
</el-button>
</el-input>
<!-- 流程发起人模式 -->
<el-input
v-if="assigneeMode === 'initiator'"
value="initiator"
readonly
disabled
style="width: 100%"
placeholder="流程发起人">
</el-input>
</div>
<!-- 用户选择弹窗 -->
<el-dialog title="选择参与者" :visible.sync="dialogVisible" width="80%" append-to-body>
<div style="display: flex;">
<!-- 左侧表格 -->
<div style="flex: 3; margin-right: 10px; overflow: auto;">
<div style="margin-bottom: 10px;">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入关键字搜索" style="width: 200px; margin-right: 10px;"></el-input>
<el-button type="primary" @click="searchUsers">搜索</el-button>
</div>
<el-table
ref="userTableRef"
:data="userList"
border
row-key="id"
>
<el-table-column type="selection" width="55" :reserve-selection="true"></el-table-column>
<el-table-column prop="loginname" label="工号" width="100"></el-table-column>
<el-table-column prop="username" label="姓名" width="120"></el-table-column>
<el-table-column prop="unitName" label="单位"></el-table-column>
</el-table>
<div style="margin-top: 10px; text-align: right;">
<el-pagination
@current-change="handleCurrentChange"
:current-page="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="total, prev, pager, next, jumper"
:total="pageForm.totalCount">
</el-pagination>
</div>
</div>
<!-- 右侧已选列表 -->
<div style="flex: 1; border: 1px solid #EBEEF5; border-radius: 4px; padding: 10px; overflow: auto;" v-if="$refs.userTableRef">
<div style="font-weight: bold; margin-bottom: 10px;">已选择的参与者</div>
<el-tag
v-for="user in $refs.userTableRef.selection"
:key="user.id"
style="margin: 0 5px 5px 0;">
{{ user.username }}
</el-tag>
<div v-if="$refs.userTableRef.selection.length === 0" style="color: #909399; font-size: 14px;">
暂无选择的参与者
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmSelection">确 定</el-button>
</span>
</el-dialog>
</el-form-item>
`,
props: {
model: {
type: Object,
required: true
},
field: {
type: String,
required: true
},
start_node_next_node_ids: {
type: Array,
default: () => []
}
},
computed: {
/**
* 判断当前节点是否为第一个任务节点
* @returns {boolean}
*/
isFirstTaskNode() {
return this.start_node_next_node_ids.includes(this.model.name)
}
},
data() {
return {
assigneeMode: "manual",
manualInput: "",
assigneeText: "",
dialogVisible: false,
userList: [],
selectedUsers: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
searchKeyword: "",
totalCount: 0
}
}
},
created() {
// 回显
this.initFromModel()
},
methods: {
initFromModel() {
const value = this.model[this.field]
this.assigneeText = this.model["assigneeText"] || ""
this.assigneeMode = this.model["assigneeMode"] || "manual"
if(this.assigneeMode === "initiator"){
this.assignee = "initiator"
this.assigneeText = "流程发起人"
}
if(this.assigneeMode === "manual"){
this.manualInput = value || ""
}
},
handleModeChange() {
this.assigneeText = ""
this.manualInput = ""
this.assignee = ""
// 如果切换到流程发起人模式,直接设置值
if (this.assigneeMode === "initiator") {
this.assignee = "{$initiator}"
this.assigneeText = "流程发起人"
}
this.updateModelValue()
},
openUserDialog() {
this.dialogVisible = true
this.pageForm.pageNumber = 1
this.pageForm.searchKeyword = ""
this.loadSelectUsers()
},
loadSelectUsers() {
let userIds = this.model[this.field].split(",")
$.get("/flow/design/assigneeEcho", { userIds: JSON.stringify(userIds) }).then(res => {
if (res.code === 0) {
this.selectedUsers = res.data
this.loadUsers()
}
})
},
loadUsers() {
$.get("/flow/design/assigneePage", {
...this.pageForm
}).then(res => {
if (res.code === 0) {
this.userList = res.data.list
this.pageForm.totalCount = res.data.totalCount
// 设置已选中的行
this.$nextTick(() => {
this.setTableSelection()
})
}
})
},
// 设置表格选中数据
setTableSelection() {
// 如果当前表格中有该用户,选中TA
this.userList.forEach(row => {
const exist = this.selectedUsers.map(item => item.id).includes(row.id)
if (exist) {
this.$refs.userTableRef.toggleRowSelection(row, true)
}
})
},
confirmSelection() {
const selection = this.$refs.userTableRef.selection
this.assignee = selection.map(item => item.id).join(",")
this.assigneeText = selection.map(item => item.username).join(",")
this.updateModelValue()
this.dialogVisible = false
},
updateModelValue() {
// 更新model中的值
if (this.assigneeMode === "manual") {
this.assignee = this.manualInput
this.assigneeText = this.manualInput
} else if (this.assigneeMode === "initiator") {
this.assignee = "initiator"
this.assigneeText = "流程发起人"
}
// 选择模式的处理在confirmSelection中
this.$set(this.model, this.field, this.assignee)
this.$set(this.model, "assigneeText", this.assigneeText)
this.$set(this.model, "assigneeMode", this.assigneeMode)
},
searchUsers() {
this.pageForm.pageNumber = 1
this.loadUsers()
},
handleSizeChange(val) {
this.pageForm.pageSize = val
this.loadUsers()
},
handleCurrentChange(val) {
this.pageForm.pageNumber = val
this.loadUsers()
}
}
}
@@ -0,0 +1,149 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/root.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app">
<snaker-flow-designer ref="designer"
v-model="flowData"
@on-save="handleSave"
:show-doc="false"
node-render-type="html"
:extend-property-keys="[
'candidateUsers',
'candidateGroups',
'candidateHandler',
'assigneeText',
'assigneeMode'
]"
>
<!-- <template v-slot:form-item-task-form="{model,field}">-->
<!-- <el-form-item label="form">-->
<!-- -->
<!-- </el-form-item>-->
<!-- </template>-->
<template v-slot:form-item-task-assignee="{model,field}">
<assignee-form :model="model" :field="field"
:start_node_next_node_ids="startNodeNextNodeIds"></assignee-form>
</template>
<template v-slot:form-item-task-assignment-handler="{model,field}">
<el-form-item label="参与者处理类">
<el-select v-model="model[field]" style="width: 100%" clearable
:disabled="startNodeNextNodeIds.includes(model.name)">
<el-option v-for="item in assignmentHandlerClassOptions" :key="item.value"
:value="item.value"
:label="item.name">
</el-option>
</el-select>
</el-form-item>
</template>
</snaker-flow-designer>
</div>
</body>
<script>
<!--#include('assigneeForm.js'){}#-->
Vue.use(SnakerflowDesigner.default)
const vue = new Vue({
el: "#app",
components: {
"assignee-form": assigneeForm
},
data() {
return {
id: "${id!}",
designerData: {},
flowData: {},
assignmentHandlerClassOptions: []
}
},
computed: {
startNodeNextNodeIds() {
if (this.$refs.designer && this.$refs.designer.lf) {
const graphData = this.$refs.designer.lf.getGraphData()
if (graphData) {
const { nodes, edges } = graphData
const startNode = nodes.find(v => v.type === "snaker:start")
if (startNode) {
const targetNodeIds = edges.filter(v => v.sourceNodeId === startNode.id).map(v => v.targetNodeId)
return targetNodeIds
}
}
}
return []
}
},
methods: {
handleSave(val) {
const design = {
...this.designerData,
content: val.json
}
$.post("/flow/design/updateContent", { design: JSON.stringify(design) }).then(res => {
if (res.code === 0) {
window.parent.postMessage("success")
} else {
window.parent.postMessage("error")
}
})
},
getDetail() {
$.get("/flow/design/detail", { id: this.id }).then((res) => {
if (res.code === 0) {
this.designerData = res.data
if (res.data.content) {
this.flowData = res.data.content
} else {
this.flowData = {}
}
}
})
},
init() {
$.get("/flow/design/assigmentHandlerClass").then((res) => {
if (res.code === 0) {
this.assignmentHandlerClassOptions = res.data
}
})
},
getData() {
console.log(this.$refs.designer)
}
},
created() {
this.init()
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
</html>
@@ -0,0 +1,214 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.design-dialog .el-dialog__body {
padding: 0;
}
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称">
<el-input v-model="pageForm.displayName" placeholder="名称" clearable></el-input>
</search-item>
<search-item label="编码">
<el-input v-model="pageForm.name" placeholder="编码" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button type="primary" @click="onAdd" size="small" icon="el-icon-plus">新增</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="displayName" label="流程定义名称"></el-table-column>
<el-table-column prop="name" label="流程定义编码"></el-table-column>
<el-table-column prop="type" label="流程分类"></el-table-column>
<el-table-column prop="isDeployed" label="是否部署">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.isDeployed===1" type="success"></el-tag>
<el-tag size="mini" v-else-if="row.isDeployed===0" type="info"></el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="onDesign(row)">设计</el-button>
</el-button>
<el-button size="mini" type="primary" @click="onEdit(row)">编辑</el-button>
<!-- v-if="row.isDeployed===0"-->
<el-button type="primary" size="mini" @click="onDeploy(row)">部署
</el-button>
<el-button type="danger" size="mini" @click="onDelete(row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="800px">
<el-form :model="formData" ref="formRef" label-width="120px">
<el-form-item label="流程定义名称" prop="displayName"
:rules="{ required: true, message: '请输入流程定义名称', trigger: 'blur' }">
<el-input v-model="formData.displayName" placeholder="请输入流程定义名称"></el-input>
</el-form-item>
<el-form-item label="流程定义编码" prop="name"
:rules="{ required: true, message: '请输入流程定义编码', trigger: 'blur' }">
<el-input v-model="formData.name" placeholder="请输入流程定义编码"></el-input>
</el-form-item>
<el-form-item label="流程分类" prop="category"
:rules="{ required: false, message: '请选择流程分类', trigger: 'change' }"
style="width: 100%">
<el-select v-model="formData.category" placeholder="请选择流程分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.id" :label="item.categoryName"
:value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="电脑端发起地址" prop="instanceUrl">
<el-input v-model="formData.instanceUrl" placeholder="请输入电脑端发起地址"></el-input>
</el-form-item>
<el-form-item label="手机端发起地址" prop="h5InstanceUrl">
<el-input v-model="formData.h5InstanceUrl" placeholder="请输入手机端发起地址"></el-input>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input v-model="formData.icon" placeholder="请输入图标"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
<el-dialog title="流程设计" :visible.sync="designVisible" fullscreen class="design-dialog">
<div style="height: calc(100vh - 55px); width: 100%">
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%"
style="height: 100%; width: inherit"></iframe>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
categoryOptions: [],
designVisible: false,
designUrl: null,
formData: {}
}
},
methods: {
onAdd() {
this.formData = {}
this.dialogVisible = true
},
onEdit(row) {
this.dialogVisible = true
this.formData = { ...row }
},
onSubmit() {
$.post("/flow/design/" + (this.formData.id ? "update" : "insert"), { design: JSON.stringify(this.formData) }).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
this.dialogVisible = false
}
})
},
onDesign(row) {
this.designVisible = true
this.iframeLoaded()
this.designUrl = "/flow/design/designer?id=" + row.id
},
iframeLoaded() {
// iframe监听组件内设计器保存事件
window.onmessage = (event) => {
console.log(event)
if (event.data === "success") {
this.$message.success("保存成功")
this.designVisible = false
this.pageData()
} else if (event.data === "error") {
this.$message.error("保存失败")
}
}
},
onDeploy(row) {
this.$confirm("部署会生成新的流程定义版本,确定部署吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/design/deploy", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onRedeploy(row) {
this.$confirm("重新部署会覆盖最新版流程定义,确定重新部署吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/design/redeploy", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
}
})
})
},
onDelete(row) {
this.$confirm("您确定要删除吗", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/design/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
onView(row) {
},
listCategory() {
// this.$axios.get("/platform/warmFlow/category/list").then((res) => {
// if (res.code === 0) {
// this.categoryOptions = res.data
// }
// })
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,80 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>流程设计器</title>
<script src="/assets/platform/plugins/vue/vue.js"></script>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/element-ui/lib/index.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.net/element-ui/2.15.14/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<!-- 引入 core 包和对应 css-->
<script src="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/logic-flow.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@logicflow/core@1.2.12/dist/style/index.css" />
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#app {
height: 100vh;
}
</style>
</head>
<body>
<div id="app">
<snaker-flow-designer ref="designer"
v-model="flowData"
@on-save="handleSave"
:show-doc="false"
node-render-type="html"
:extendPropertyKeys="[
'candidateUsers',
'candidateGroups',
'candidateHandler',
'assigneeText',
]"
></snaker-flow-designer>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
new Vue({
el: "#app",
data() {
return {
id: "${id!}",
flowData: {}
}
},
methods: {
handleSave(val) {
console.log(val)
const data = {
processDesignId: this.id,
...val.json
}
$.post("/flow/design/updateDesign", { json: JSON.stringify(data) }).then((res) => {
if (res.code === 0) {
window.parent.postMessage("success")
} else {
window.parent.postMessage("error")
}
})
},
getDetail() {
$.get("/flow/design/detail", { id: this.id }).then((res) => {
if (res.code === 0) {
this.flowData = res.data.jsonObject
}
})
}
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
</html>
@@ -3,8 +3,6 @@ layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<!-- <iframe src="/uflo/designer"></iframe>-->
<el-card class="mt10" shadow="never">
<el-row type="flex">
<el-button @click="openDesigner" size="medium" type="primary">
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>流程状态</title>
<link rel="stylesheet" href="/assets/platform/plugins/snaker/snaker.css" type="text/css" media="all"/>
<link rel="stylesheet" href="/assets/platform/plugins/snaker/style.css" type="text/css" media="all"/>
</head>
<body>
</div>
<table class="properties_all" align="center" border="1" cellpadding="0" cellspacing="0" style="margin-top: 0px">
<div id="snakerflow"
style="border: 1px solid #d2dde2; margin-top:10px; margin-left:10px; margin-bottom:10px; width:98%;">
</div>
</table>
<script src="/assets/platform/plugins/jquery/jquery.js"></script>
<script src="/assets/platform/plugins/snaker/raphael-min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/jquery-ui-1.8.4.custom/js/jquery-ui.min.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/dialog.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.designer.js" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.model.js" charset="utf-8" type="text/javascript"></script>
<script src="/assets/platform/plugins/snaker/snaker.editors.js" type="text/javascript"></script>
<script>
const orderId = "${orderId!}"
const processId = "${processId!}"
$(function(){
$.post("/snakerflow/define/process/json", { processId, orderId }).then(res => {
if (res.code === 0) {
display(res.data.process, res.data.state)
}
})
function display(process, state) {
/** view*/
$('#snakerflow').snakerflow($.extend(true, {
basePath: "/assets/platform/plugins/snaker/",
// ctxPath: easyAdmin.GetAdminServerUrl(),
// token: easyAdmin.GetTokenQueryString(),
orderId: orderId,
restore: eval("(" + process + ")"),
editable: false
}, eval("(" + state + ")")
));
}
})
</script>
</body>
</html>
@@ -0,0 +1,604 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="app" v-cloak>
<div class="task-todo-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon todo-icon">
<i class="el-icon-s-claim"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{todoCount}}</div>
<div class="stat-label">待办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon done-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{doneCount}}</div>
<div class="stat-label">已办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon started-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{startedCount}}</div>
<div class="stat-label">我发起的</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索筛选区域 -->
<el-card shadow="never" class="filter-section">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input v-model="searchForm.keyword" placeholder="请输入关键词搜索" prefix-icon="el-icon-search" clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.category" placeholder="所有流程类型" clearable>
<el-option v-for="type in processTypes" :key="type.value" :label="type.label" :value="type.value"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 任务列表区域 -->
<el-card shadow="never" class="task-section">
<div slot="header" class="task-header">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="待办任务" name="todo"></el-tab-pane>
<el-tab-pane label="已办任务" name="done"></el-tab-pane>
<el-tab-pane label="我发起的" name="started"></el-tab-pane>
</el-tabs>
</div>
<!-- 表格展示 -->
<el-table
v-loading="loading"
:data="tasks"
style="width: 100%"
:key="activeTab"
:header-cell-style="{backgroundColor: '#f5f7fa'}"
:row-class-name="tableRowClassName"
>
<el-table-column prop="processInstanceName" label="流程名称" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span class="task-title">{{scope.row.processInstanceName || scope.row.title}}</span>
</template>
</el-table-column>
<el-table-column prop="taskName" label="任务节点" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="processDefinitionName" label="流程类型" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">{{scope.row.processDefinitionName || scope.row.processType}}</template>
</el-table-column>
<el-table-column prop="initiatorName" label="申请人" min-width="120">
<template slot-scope="{row}">
{{row.variable.initiatorName}}
</template>
</el-table-column>
<el-table-column label="时间" min-width="180">
<template slot-scope="scope">
<div v-if="activeTab === 'todo'">
<i class="el-icon-time"></i>
{{scope.row.createdAt}}
</div>
<div v-else-if="activeTab === 'done'">
<i class="el-icon-check"></i>
{{scope.row.finishTime}}
</div>
<div v-else-if="activeTab === 'started'">
<i class="el-icon-s-promotion"></i>
{{scope.row.createdAt}}
</div>
</template>
</el-table-column>
<el-table-column v-if="activeTab === 'started'" label="状态" width="100">
<template slot-scope="{row}">
{{processStatusMap[row.state]?.text}}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template slot-scope="scope">
<el-button v-if="activeTab === 'todo'" type="primary" size="mini" @click="handleTask(scope.row)">处理</el-button>
<el-button type="info" size="mini" @click="viewTaskDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态展示 -->
<el-empty v-if="tasks.length === 0" :description="getEmptyText()"></el-empty>
<!-- 分页 -->
<div class="pagination-container" v-if="tasks.length > 0">
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="total"
></el-pagination>
</div>
</el-card>
<!-- 任务详情对话框 -->
<el-dialog title="任务详情" :visible.sync="dialogVisible" width="600px" :close-on-click-modal="false">
<div v-if="currentTask" class="task-detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="流程名称">{{currentTask.processInstanceName || currentTask.title}}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{currentTask.taskName || '-'}}</el-descriptions-item>
<el-descriptions-item label="流程类型">{{currentTask.processDefinitionName || currentTask.processType}}</el-descriptions-item>
<el-descriptions-item label="申请人">{{currentTask.applyUserName || '-'}}</el-descriptions-item>
<el-descriptions-item label="申请部门">{{currentTask.applyUserUnitId || '-'}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{currentTask.createTime}}</el-descriptions-item>
<el-descriptions-item v-if="currentTask.description" label="任务描述">
<div class="description-content">{{currentTask.description}}</div>
</el-descriptions-item>
<el-descriptions-item v-if="currentTask.taskMobileFormUrl" label="表单链接">
<el-link type="primary" :href="currentTask.taskMobileFormUrl" target="_blank">点击查看详细表单</el-link>
</el-descriptions-item>
</el-descriptions>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="activeTab === 'todo'" type="primary" @click="handleTask(currentTask)">处理任务</el-button>
</span>
</el-dialog>
</div>
</div>
<style>
.task-todo-center {
padding: 20px;
}
/* 统计卡片样式 */
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
}
.stat-card .el-card__body {
padding: 20px;
}
.stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 28px;
}
.todo-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.done-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.started-icon {
background-color: rgba(230, 162, 60, 0.1);
color: #e6a23c;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 搜索区域样式 */
.filter-section {
margin-bottom: 20px;
}
.filter-section .el-card__body {
padding: 15px 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
/* 任务列表区域样式 */
.task-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.task-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.task-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.task-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.task-header .el-tabs__item.is-active {
color: #409eff;
font-weight: 500;
}
.task-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.task-title {
font-weight: 500;
color: #303133;
}
/* 表格行样式 */
.el-table .hover-row {
background-color: #f5f7fa;
}
/* 优化后的分页样式 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
.pagination-container .el-pagination {
padding: 0;
font-weight: normal;
display: flex;
align-items: center;
}
.pagination-container .el-pagination .btn-prev,
.pagination-container .el-pagination .btn-next {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
padding: 0 10px;
height: 32px;
line-height: 32px;
margin: 0 5px;
}
.pagination-container .el-pagination .btn-prev:hover,
.pagination-container .el-pagination .btn-next:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
min-width: 32px;
height: 32px;
line-height: 32px;
margin: 0 3px;
font-weight: normal;
transition: all 0.3s;
}
.pagination-container .el-pagination .el-pager li:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li.active {
background-color: var(--color-primary);
color: #fff;
font-weight: bold;
}
.pagination-container .el-pagination .el-pagination__jump {
margin-left: 15px;
color: #606266;
}
.pagination-container .el-pagination .el-pagination__editor.el-input {
width: 50px;
margin: 0 5px;
}
.pagination-container .el-pagination .el-pagination__editor.el-input .el-input__inner {
height: 28px;
border-radius: 4px;
}
/* 任务详情样式 */
.task-detail .el-descriptions {
margin-bottom: 20px;
}
.description-content {
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
}
</style>
<script>
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 搜索表单
searchForm: {
keyword: "",
category: ""
},
// 流程类型选项
processTypes: [],
// 任务列表
tasks: [],
loading: false,
// 分页
currentPage: 1,
pageSize: 10,
total: 0,
// 标签页
activeTab: "todo",
// 当前任务
currentTask: null,
dialogVisible: false,
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
},
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize)
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.getProcessTypes(), this.getTasks()])
},
// 获取统计数据
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
const { todoCount, doneCount, startedCount } = res.data
this.todoCount = todoCount
this.doneCount = doneCount
this.startedCount = startedCount
}
} catch (error) {
this.$message.error("获取统计数据失败")
console.error("获取统计数据失败:", error)
}
},
// 获取流程类型
async getProcessTypes() {
try {
const res = await $.post("/platform/warmFlow/todoCenter/category")
if (res.code === 0) {
this.processTypes = res.data
}
} catch (error) {
this.$message.error("获取流程类型失败")
console.error("获取流程类型失败:", error)
}
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize,
type: this.activeTab,
keyword: this.searchForm.keyword,
category: this.searchForm.category
}
const res = await $.post("/flow/todoCenter/" + this.activeTab, params)
if (res.code === 0) {
this.tasks = res.data.list
this.total = res.data.totalCount
this.dialogVisible = false
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
// 处理标签页点击
handleTabClick(tab) {
this.activeTab = tab.name
this.currentPage = 1
this.getTasks()
},
// 搜索
search() {
this.currentPage = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.searchForm = {
keyword: "",
category: ""
}
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.currentPage = val
this.getTasks()
},
// 查看任务详情
viewTaskDetail(task) {
// this.currentTask = task
// this.dialogVisible = true
},
// 处理任务
async handleTask(task) {
if (!task) return
// 如果有表单链接,直接跳转
if (task.taskMobileFormUrl) {
window.open(task.taskMobileFormUrl, "_blank")
return
}
},
// 获取状态对应的类型
getStatusType(task) {
if (!task.status) return "info"
switch (task.status) {
case "审批中":
return "primary"
case "已完成":
return "success"
case "已通过":
return "success"
case "已拒绝":
return "danger"
default:
return "info"
}
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
},
// 表格行类名
tableRowClassName({ row, rowIndex }) {
return ""
}
}
})
</script>
<!--#
}
#-->
@@ -1,10 +1,12 @@
const SYS_MENU_BASIC_FORM_COMPONENT = {
template: `
<el-dialog :title="formData.id ? '编辑菜单' : '新增菜单'" :show-close="false" :visible.sync="visibleDialog" :close-on-click-modal="false" width="50%">
template: /*language=HTML*/ `
<el-dialog :title="formData.id ? '编辑菜单' : '新增菜单'" :show-close="false" :visible.sync="visibleDialog"
:close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="formRef" :rules="formRules" size="small" label-width="80px">
<el-form-item label="所属模块" prop="moduleId">
<el-select v-model="formData.moduleId" clearable placeholder="请选择模块">
<el-option v-for="item in moduleOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
<el-option v-for="item in moduleOptions" :value="item.id" :key="item.id"
:label="item.name"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="parentId" label="上级菜单" label-width="80px">
@@ -39,28 +41,37 @@ const SYS_MENU_BASIC_FORM_COMPONENT = {
<el-input maxlength="100" placeholder="URL" v-model="formData.href" auto-complete="off" tabindex="6"
type="text"></el-input>
</el-form-item>
<el-form-item prop="icon" label="图标">
<el-input maxlength="100"
v-if="platform==='PC'"
placeholder="图标"
v-model="formData.icon"
auto-complete="off"
tabindex="7"
type="text">
<template slot="append">
<el-button @click="$refs.iconSelector.showIconModal(formData.icon)">图标选择</el-button>
<icon-selector ref="iconSelector" v-model="formData.icon"></icon-selector>
</template>
</el-input>
<file-upload
v-if="platform==='H5'"
:value.sync="formData.icon"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<el-form-item prop="picIcon" label="图片图标">
<!-- <template v-if="platform==='PC'">-->
<!-- <el-input maxlength="100" placeholder="图标" v-model="formData.icon"></el-input>-->
<!-- <i :class="formData.icon" v-if="formData.icon"></i>-->
<!-- </template> -->
<!-- <el-input maxlength="100" -->
<!-- v-if="platform==='PC'"-->
<!-- placeholder="图标" -->
<!-- v-model="formData.icon" -->
<!-- auto-complete="off" -->
<!-- tabindex="7"-->
<!-- type="text">-->
<!-- <template slot="append">-->
<!-- <el-button @click="$refs.iconSelector.showIconModal(formData.icon)">图标选择</el-button>-->
<!-- <icon-selector ref="iconSelector" v-model="formData.icon"></icon-selector>-->
<!-- </template>-->
<!-- </el-input>-->
<file-upload
:value.sync="formData.picIcon"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input maxlength="100" placeholder="图标" v-model="formData.icon"></el-input>
<i :class="formData.icon" v-if="formData.icon"></i>
</el-form-item>
<el-form-item prop="disabled" label="启用状态">
<el-switch v-model="formData.disabled" active-color="#ff4949" inactive-color="#13ce66"></el-switch>
</el-form-item>
@@ -83,10 +94,12 @@ const SYS_MENU_BASIC_FORM_COMPONENT = {
<el-input v-model="menu.name" placeholder="权限名称"></el-input>
</el-col>
<el-col :span="13">
<el-input v-model="menu.permission" placeholder="权限标识,如 sys.manager.menu.add"></el-input>
<el-input v-model="menu.permission"
placeholder="权限标识,如 sys.manager.menu.add"></el-input>
</el-col>
<el-col :span="4">
<el-button @click.prevent="formRemoveMenu(menu)" icon="el-icon-delete" size="small"></el-button>
<el-button @click.prevent="formRemoveMenu(menu)" icon="el-icon-delete"
size="small"></el-button>
</el-col>
</el-row>
</el-form-item>
@@ -38,8 +38,8 @@ layout("/layouts/platform.html"){
</div>
</el-image>
</template>
<template scope="{row}" v-else-if="column.prop=='containsMenu'">
{{firstMenus.filter(v=>row.containsMenu.includes(v.id)).map(v=>v.name).join('、')}}
<template scope="{row}" v-else-if="column.prop=='faIcon'">
<i :class="row.faIcon"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="200px">
@@ -62,7 +62,7 @@ layout("/layouts/platform.html"){
<el-form-item label="模块名称" prop="name">
<el-input v-model="formData.name" maxlength="8"></el-input>
</el-form-item>
<el-form-item label="模块图" prop="icon">
<el-form-item label="模块图" prop="icon">
<file-upload
:value.sync="formData.icon"
:upload_number="1"
@@ -71,6 +71,10 @@ layout("/layouts/platform.html"){
upload_result_type="url"
></file-upload>
</el-form-item>
<el-form-item label="图标" prop="faIcon">
<el-input maxlength="100" placeholder="图标" v-model="formData.faIcon"></el-input>
<i :class="formData.faIcon" v-if="formData.faIcon"></i>
</el-form-item>
<el-form-item label="排序编号" prop="sortNum">
<el-input-number v-model="formData.sortNum" :min="1" :precision="0"></el-input-number>
</el-form-item>
@@ -100,14 +104,16 @@ layout("/layouts/platform.html"){
platform: [{ required: true, message: "请填写", trigger: ["blur", "change"] }],
name: [{ required: true, message: "请填写", trigger: ["blur", "change"] }],
icon: [{ required: true, message: "请填写", trigger: ["blur", "change"] }],
sortNum: [{ required: true, message: "请输入", trigger: ["blur", "change"] }]
sortNum: [{ required: true, message: "请输入", trigger: ["blur", "change"] }],
faIcon: [{ required: true, message: "请填写", trigger: ["blur", "change"] }]
},
firstMenus: [],
tableColumns: [
{ label: "排序编号", prop: "sortNum", sortable: true },
{ label: "所属平台", prop: "platform", sortable: true },
{ label: "模块名称", prop: "name" },
{ label: "模块图标", prop: "icon" }
{ label: "模块图标", prop: "icon" },
{ label: "模块图标", prop: "faIcon" }
],
pageForm: {}
}
@@ -0,0 +1,89 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称">
<el-input v-model="pageForm.flowName" placeholder="名称" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button type="primary" @click="onAdd" size="small" icon="el-icon-plus">新增</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="flowName" label="流程定义名称"></el-table-column>
<el-table-column prop="flowCode" label="流程定义编码"></el-table-column>
<el-table-column prop="categoryName" label="流程分类"></el-table-column>
<el-table-column prop="version" label="版本号"></el-table-column>
<el-table-column prop="activityStatus" label="激活状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.activityStatus===1" type="success">激活</el-tag>
<el-tag size="mini" v-else-if="row.activityStatus===0" type="info">挂起</el-tag>
</template>
</el-table-column>
<el-table-column prop="isPublish" label="发布状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.isPublish===0" type="info">未发布</el-tag>
<el-tag size="mini" v-else-if="row.isPublish===1" type="success">已发布</el-tag>
<el-tag size="mini" v-else-if="row.isPublish===9" type="info">失效</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="250px">
<template slot-scope="{row}">
<el-button type="text" size="mini" @click="onDesign(row)">设计</el-button>
<el-button type="text" size="mini" @click="onView(row)">查看</el-button>
<el-button v-if="row.isPublish !== 1" type="text" size="mini" @click="onPublish(row)">发布</el-button>
<el-button type="text" size="mini" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="800px">
<el-form :model="formData" ref="formRef" label-width="120px">
<el-form-item label="流程定义名称" prop="flowName" :rules="{ required: true, message: '请输入流程定义名称', trigger: 'blur' }">
<el-input v-model="formData.flowName" placeholder="请输入流程定义名称"></el-input>
</el-form-item>
<el-form-item label="流程定义编码" prop="flowCode" :rules="{ required: true, message: '请输入流程定义编码', trigger: 'blur' }">
<el-input v-model="formData.flowCode" placeholder="请输入流程定义编码"></el-input>
</el-form-item>
<el-form-item label="流程分类" prop="categoryId">
<el-select v-model="formData.categoryId" placeholder="请选择流程分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,195 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.design-dialog .el-dialog__body {
padding: 0;
}
</style>
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="是否发布">
<el-select v-model="pageForm.isPublish" placeholder="请选择是否发布" clearable style="width: 100%">
<el-option label="全部" :value="null"></el-option>
<el-option label="已发布" :value="true"></el-option>
<el-option label="未发布" :value="false"></el-option>
</el-select>
</search-item>
<search-item label="名称">
<el-input v-model="pageForm.flowName" placeholder="名称" clearable></el-input>
</search-item>
<search-item label="编码">
<el-input v-model="pageForm.flowCode" placeholder="编码" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button type="primary" @click="onAdd" size="small" icon="el-icon-plus">新增</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="flowName" label="流程定义名称"></el-table-column>
<el-table-column prop="flowCode" label="流程定义编码"></el-table-column>
<el-table-column prop="categoryName" label="流程分类">
<template slot-scope="{row}">{{categoryOptions.find(item => item.id === row.category)?.categoryName}}</template>
</el-table-column>
<el-table-column prop="version" label="版本号"></el-table-column>
<el-table-column prop="activityStatus" label="激活状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.activityStatus===1" type="success">激活</el-tag>
<el-tag size="mini" v-else-if="row.activityStatus===0" type="info">挂起</el-tag>
</template>
</el-table-column>
<el-table-column prop="isPublish" label="发布状态">
<template slot-scope="{row}">
<el-tag size="mini" v-if="row.isPublish===0" type="info">未发布</el-tag>
<el-tag size="mini" v-else-if="row.isPublish===1" type="success">已发布</el-tag>
<el-tag size="mini" v-else-if="row.isPublish===9" type="info">失效</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="250px">
<template slot-scope="{row}">
<el-button v-if="row.isPublish !== 1" type="text" size="mini" @click="onDesign(row)">设计</el-button>
<el-button type="text" size="mini" @click="onView(row)">查看</el-button>
<el-button v-if="row.isPublish !== 1" type="text" size="mini" @click="onPublish(row)">发布</el-button>
<el-button type="text" size="mini" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="800px">
<el-form :model="formData" ref="formRef" label-width="120px">
<el-form-item label="流程定义名称" prop="flowName" :rules="{ required: true, message: '请输入流程定义名称', trigger: 'blur' }">
<el-input v-model="formData.flowName" placeholder="请输入流程定义名称"></el-input>
</el-form-item>
<el-form-item label="流程定义编码" prop="flowCode" :rules="{ required: true, message: '请输入流程定义编码', trigger: 'blur' }">
<el-input v-model="formData.flowCode" placeholder="请输入流程定义编码"></el-input>
</el-form-item>
<el-form-item label="流程分类" prop="category" :rules="{ required: true, message: '请选择流程分类', trigger: 'change' }">
<el-select v-model="formData.category" placeholder="请选择流程分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.id" :label="item.categoryName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</template>
</el-dialog>
<el-dialog title="流程设计" :visible.sync="designVisible" fullscreen class="design-dialog">
<div style="height: calc(100vh - 55px); width: 100%">
<iframe v-if="designVisible" :src="designUrl" frameborder="0" height="100%" style="height: 100%; width: inherit"></iframe>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
categoryOptions: [],
designVisible: false,
designUrl: null
}
},
methods: {
onAdd() {
this.formData = {}
this.dialogVisible = true
},
onDesign(row, disabled = false) {
this.designVisible = true
this.iframeLoaded()
this.designUrl = "/warm-flow-ui/index.html?id=" + row.id + "&disabled=" + disabled
},
iframeLoaded() {
// iframe监听组件内设计器保存事件
window.onmessage = (event) => {
switch (event.data.method) {
case "close":
close()
break
}
}
},
onDelete(row) {
this.$confirm("是否确认删除流程定义编码为【" + row.flowCode + "】版本为【" + row.version + "】的数据项?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/warmFlow/definition/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.message)
this.pageData()
}
})
})
},
onView(row) {},
// 发布
onPublish(row) {
this.$confirm(
"是否确认发布流程定义编码为【" + row.flowCode + "】版本为【" + row.version + "】的数据项?,发布后会将已发布流程定义改为失效!",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}
).then(() => {
this.$axios.post("/platform/warmFlow/definition/publish", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.message)
this.pageData()
}
})
})
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios.post("/platform/warmFlow/definition/" + (this.formData.id ? "update" : "insert"), this.formData).then((res) => {
if (res.code === 0) {
this.dialogVisible = false
this.$message.success(res.message)
this.pageData()
}
})
}
})
},
listCategory() {
this.$axios.get("/platform/warmFlow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,586 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="app" v-cloak>
<div class="task-todo-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon todo-icon">
<i class="el-icon-s-claim"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{todoCount}}</div>
<div class="stat-label">待办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon done-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{doneCount}}</div>
<div class="stat-label">已办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon started-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{startedCount}}</div>
<div class="stat-label">我发起的</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索筛选区域 -->
<el-card shadow="never" class="filter-section">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input v-model="searchForm.keyword" placeholder="请输入关键词搜索" prefix-icon="el-icon-search" clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.processType" placeholder="所有流程类型" clearable>
<el-option v-for="type in processTypes" :key="type.value" :label="type.label" :value="type.value"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 任务列表区域 -->
<el-card shadow="never" class="task-section">
<div slot="header" class="task-header">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="待办任务" name="todo"></el-tab-pane>
<el-tab-pane label="已办任务" name="done"></el-tab-pane>
<el-tab-pane label="我发起的" name="started"></el-tab-pane>
</el-tabs>
</div>
<!-- 表格展示 -->
<el-table
v-loading="loading"
:data="tasks"
style="width: 100%"
:key="activeTab"
:header-cell-style="{backgroundColor: '#f5f7fa'}"
:row-class-name="tableRowClassName"
>
<el-table-column prop="processInstanceName" label="流程名称" min-width="180" show-overflow-tooltip>
<template slot-scope="{row}">
<span class="task-title">{{row.instance_ext?.instanceName}}</span>
</template>
</el-table-column>
<el-table-column prop="node_name" label="任务节点" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
<div v-if="activeTab === 'todo'">{{scope.row.node_name}}</div>
<div v-else-if="activeTab === 'done'">{{scope.row.node_name}}</div>
<div v-else-if="activeTab === 'started'">{{scope.row.assigneeInfo}}</div>
</template>
</el-table-column>
<el-table-column prop="processDefinitionName" label="流程类型" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">{{scope.row.processDefinitionName || scope.row.processType}}</template>
</el-table-column>
<el-table-column prop="instance_ext.initiatorUserName" label="申请人" min-width="120">
<template slot-scope="{row}">{{row.instance_ext?.initiatorUserName}} ({{row.instance_ext?.initiatorLoginName}})</template>
</el-table-column>
<el-table-column label="时间" min-width="180">
<template slot-scope="scope">
<div v-if="activeTab === 'todo'">
<i class="el-icon-time"></i>
{{scope.row.create_time}}
</div>
<div v-else-if="activeTab === 'done'">
<i class="el-icon-check"></i>
{{scope.row.update_time}}
</div>
<div v-else-if="activeTab === 'started'">
<i class="el-icon-s-promotion"></i>
{{scope.row.create_time}}
</div>
</template>
</el-table-column>
<el-table-column v-if="activeTab === 'started'" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row)" size="small" effect="light">{{scope.row.status || '进行中'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template slot-scope="scope">
<el-button v-if="activeTab === 'todo'" type="primary" size="mini" @click="handleTask(scope.row)">处理</el-button>
<el-button type="info" size="mini" @click="viewTaskDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态展示 -->
<el-empty v-if="tasks.length === 0" :description="getEmptyText()"></el-empty>
<!-- 分页 -->
<div class="pagination-container" v-if="tasks.length > 0">
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="total"
></el-pagination>
</div>
</el-card>
<!-- 任务详情对话框 -->
<el-dialog title="任务详情" :visible.sync="dialogVisible" width="600px" :close-on-click-modal="false">
<div v-if="currentTask" class="task-detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="流程名称">{{currentTask.processInstanceName || currentTask.title}}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{currentTask.taskName || '-'}}</el-descriptions-item>
<el-descriptions-item label="流程类型">{{currentTask.processDefinitionName || currentTask.processType}}</el-descriptions-item>
<el-descriptions-item label="申请人">{{currentTask.applyUserName || '-'}}</el-descriptions-item>
<el-descriptions-item label="申请部门">{{currentTask.applyUserUnitId || '-'}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{currentTask.createTime}}</el-descriptions-item>
<el-descriptions-item v-if="currentTask.description" label="任务描述">
<div class="description-content">{{currentTask.description}}</div>
</el-descriptions-item>
<el-descriptions-item v-if="currentTask.taskMobileFormUrl" label="表单链接">
<el-link type="primary" :href="currentTask.taskMobileFormUrl" target="_blank">点击查看详细表单</el-link>
</el-descriptions-item>
</el-descriptions>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="activeTab === 'todo'" type="primary" @click="handleTask(currentTask)">处理任务</el-button>
</span>
</el-dialog>
</div>
</div>
<style>
.task-todo-center {
padding: 20px;
}
/* 统计卡片样式 */
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
}
.stat-card .el-card__body {
padding: 20px;
}
.stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 28px;
}
.todo-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.done-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.started-icon {
background-color: rgba(230, 162, 60, 0.1);
color: #e6a23c;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 搜索区域样式 */
.filter-section {
margin-bottom: 20px;
}
.filter-section .el-card__body {
padding: 15px 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
/* 任务列表区域样式 */
.task-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.task-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.task-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.task-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.task-header .el-tabs__item.is-active {
color: #409eff;
font-weight: 500;
}
.task-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.task-title {
font-weight: 500;
color: #303133;
}
/* 表格行样式 */
.el-table .hover-row {
background-color: #f5f7fa;
}
/* 优化后的分页样式 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
.pagination-container .el-pagination {
padding: 0;
font-weight: normal;
display: flex;
align-items: center;
}
.pagination-container .el-pagination .btn-prev,
.pagination-container .el-pagination .btn-next {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
padding: 0 10px;
height: 32px;
line-height: 32px;
margin: 0 5px;
}
.pagination-container .el-pagination .btn-prev:hover,
.pagination-container .el-pagination .btn-next:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
min-width: 32px;
height: 32px;
line-height: 32px;
margin: 0 3px;
font-weight: normal;
transition: all 0.3s;
}
.pagination-container .el-pagination .el-pager li:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li.active {
background-color: var(--color-primary);
color: #fff;
font-weight: bold;
}
.pagination-container .el-pagination .el-pagination__jump {
margin-left: 15px;
color: #606266;
}
.pagination-container .el-pagination .el-pagination__editor.el-input {
width: 50px;
margin: 0 5px;
}
.pagination-container .el-pagination .el-pagination__editor.el-input .el-input__inner {
height: 28px;
border-radius: 4px;
}
/* 任务详情样式 */
.task-detail .el-descriptions {
margin-bottom: 20px;
}
.description-content {
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
}
</style>
<script>
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 搜索表单
searchForm: {
keyword: "",
processType: ""
},
// 流程类型选项
processTypes: [],
// 任务列表
tasks: [],
loading: false,
// 分页
currentPage: 1,
pageSize: 10,
total: 0,
// 标签页
activeTab: "todo",
// 当前任务
currentTask: null,
dialogVisible: false
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize)
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.getProcessTypes(), this.getTasks()])
},
// 获取统计数据
async getStatistics() {
try {
const res = await $.post("/platform/warmFlow/todoCenter/statistics")
if (res.code === 0) {
const { todoCount, doneCount, startedCount } = res.data
this.todoCount = todoCount
this.doneCount = doneCount
this.startedCount = startedCount
}
} catch (error) {
this.$message.error("获取统计数据失败")
console.error("获取统计数据失败:", error)
}
},
// 获取流程类型
async getProcessTypes() {
try {
const res = await $.post("/platform/workflow/todoCenter/process/types")
if (res.code === 0) {
this.processTypes = res.data
}
} catch (error) {
this.$message.error("获取流程类型失败")
console.error("获取流程类型失败:", error)
}
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize,
type: this.activeTab,
keyword: this.searchForm.keyword,
processType: this.searchForm.processType
}
const res = await $.post("/platform/warmFlow/todoCenter/" + this.activeTab, params)
if (res.code === 0) {
this.tasks = res.data.list
this.total = res.data.totalCount
this.dialogVisible = false
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
// 处理标签页点击
handleTabClick(tab) {
this.activeTab = tab.name
this.currentPage = 1
this.getTasks()
},
// 搜索
search() {
this.currentPage = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.searchForm = {
keyword: "",
processType: ""
}
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.currentPage = val
this.getTasks()
},
// 查看任务详情
viewTaskDetail(task) {
this.currentTask = task
this.dialogVisible = true
},
// 处理任务
async handleTask(task) {
if (!task) return
// 如果有表单链接,直接跳转
if (task.taskMobileFormUrl) {
window.open(task.taskMobileFormUrl, "_blank")
return
}
},
// 获取状态对应的类型
getStatusType(task) {
if (!task.status) return "info"
switch (task.status) {
case "审批中":
return "primary"
case "已完成":
return "success"
case "已通过":
return "success"
case "已拒绝":
return "danger"
default:
return "info"
}
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
},
// 表格行类名
tableRowClassName({ row, rowIndex }) {
return ""
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,115 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="流程实例">
<el-button size="small" type="primary" icon="el-icon-plus" @click="onOpen">新增</el-button>
</table-tool>
<el-table
:data="tableData"
:size="tableSize"
@sort-change="pageOrder"
ref="tableRef"
row-key="id"
style="width: 100%"
v-loading="tableLoading"
>
<el-table-column
:key="column.prop"
:label="column.label"
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
></el-table-column>
<el-table-column label="操作" width="150px" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="onEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="onDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="60%">
<el-form ref="formRef" :model="formData" label-width="80px">
<el-form-item label="名称" prop="name" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]">
<el-input v-model="formData.name" maxlength="100" placeholder="请输入名称" show-word-limit></el-input></el-input>
</el-form-item>
<el-form-item label="表达式" prop="expression" :rules="[{ required: true, message: '请输入表达式', trigger: 'blur' }]">
<el-input v-model="formData.expression" maxlength="255" placeholder="请输入表达式" show-word-limit></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
tableColumns: [
{ label: "名称", prop: "name" },
{ label: "表达式", prop: "expression" },
{ label: "创建时间", prop: "createdBy" }
],
dialogVisible: false
}
},
methods: {
onSave() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios.post("/platform/workflow/expression/" + (this.formData.id ? "update" : "save"), this.formData).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.pageData()
}
})
}
})
},
onOpen() {
this.dialogVisible = true
this.formData = {}
},
onEdit(row) {
this.dialogVisible = true
this.formData = { ...row }
},
onDelete(id){
this.$confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/platform/workflow/expression/delete', {id}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
}).catch(() => {})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>1流程设计器 - JeecgBoot低代码平台</title>
</head>
<body>
<div id="app" style="padding-top: 1px">
<vue-bpmn-designer ref="FlowDesigner" :is-add="true" token="" style="overflow: hidden"></vue-bpmn-designer>
</div>
</body>
<script>
window._CONFIG = {
domianURL: "http://localhost:8080/"
}
</script>
<script src="https://api3.boot.jeecg.com/desflow/vue/polyfill.min.js?v=1749544752.10"></script>
<script src="https://api3.boot.jeecg.com/desflow/vue/vue.min.js?v=1749544752.10"></script>
<script src="https://api3.boot.jeecg.com/desflow/moment/moment-with-locales.min.js?v=1749544752.10"></script>
<script src="https://api3.boot.jeecg.com/desflow/antd/antd-with-locales.js?v=1749544752.10"></script>
<link rel="stylesheet" href="https://api3.boot.jeecg.com/desflow/antd/antd.css?v=1749544752.10" />
<link rel="stylesheet" href="https://api3.boot.jeecg.com/desflow/lib/index.css?v=1749544752.10" />
<script src="/assets/platform/plugins/bpmn-designer/index.umd.min.js?v=1.0.1"></script>
<script>
new Vue({
el: "#app",
data() {
return {
processId: "${modelId!}",
category: "${category!}"
}
},
methods: {},
mounted: function () {
console.log(this.processId)
this.$refs.FlowDesigner.show({
processId: this.processId,
typeid: this.category
})
console.log(this.processId)
}
})
</script>
<script></script>
</html>
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="流程定义">
<el-button size="small" type="primary" icon="el-icon-plus" @click="openDesigner()">新建流程模型</el-button>
</table-tool>
<el-table
:data="tableData"
:size="tableSize"
@sort-change="pageOrder"
ref="tableRef"
row-key="id"
style="width: 100%"
v-loading="tableLoading"
>
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
<el-table-column
:key="column.prop"
:label="column.label"
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template scope="{row}" v-if="column.prop=='deploymentId'">
<el-tag v-if="row.deploymentId" size="mini">已部署</el-tag>
<el-tag v-else size="mini">未部署</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="400px" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openDesigner(scope.row)">设计</el-button>
<el-button size="mini" type="primary" @click="deploy(scope.row.id)">部署</el-button>
<el-button size="mini" type="info" @click="showHistory(scope.row)">历史版本</el-button>
<el-button size="mini" type="danger" @click="onDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="流程设计" :visible.sync="dialogVisible" fullscreen>
<div style="height: calc(100vh - 115px)">
<iframe
:src="designerUrl"
v-if="dialogVisible"
frameborder="0"
scrolling="auto"
allowtransparency="true"
allowfullscreen="true"
width="100%"
height="100%"
></iframe>
</div>
</el-dialog>
<!-- 历史版本对话框 -->
<el-dialog title="历史版本" :visible.sync="historyDialogVisible" width="60%">
<el-table :data="historyVersions" style="width: 100%" v-loading="historyLoading">
<el-table-column label="版本号" prop="version" width="100" align="center"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180"></el-table-column>
<el-table-column label="最后更新时间" prop="lastUpdateTime" width="180"></el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="viewHistoryXml(scope.row)">查看XML</el-button>
<el-button size="mini" type="success" @click="restoreVersion(scope.row)" :disabled="currentModel.version === scope.row.version">恢复</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 查看XML对话框 -->
<el-dialog title="模型XML" :visible.sync="xmlDialogVisible" width="80%">
<pre v-if="modelXml" style="max-height: 500px; overflow: auto;">{{ modelXml }}</pre>
<div v-else>加载中...</div>
</el-dialog>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
tableColumns: [
{ label: "名称", prop: "name" },
{ label: "Key", prop: "key" },
{ label: "分类", prop: "category" },
{ label: "是否部署", prop: "deploymentId" },
{ label: "版本", prop: "version" },
{ label: "创建时间", prop: "createTime" }
],
dialogVisible: false,
designerUrl: "/platform/workflow/process/definition' + modelId",
historyDialogVisible: false,
historyVersions: [],
historyLoading: false,
currentModel: {},
xmlDialogVisible: false,
modelXml: ""
}
},
methods: {
openDesigner(row = null) {
this.dialogVisible = true
if (row) {
this.designerUrl = "/platform/workflow/designer?modelId=" + row.id + "&category=" + row.category
} else {
this.designerUrl = "/platform/workflow/designer?modelId=&category="
}
},
deploy(modelId) {
this.$confirm("确定要部署吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/workflow/model/deploy", { modelId }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
onDelete(modelId) {
this.$confirm("确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/workflow/model/delete", { modelId }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
// 显示历史版本
showHistory(row) {
this.currentModel = row;
this.historyDialogVisible = true;
this.historyLoading = true;
// 调用后端接口获取历史版本
this.$axios.get("/platform/workflow/model/history", {
params: { modelKey: row.key }
}).then(res => {
if (res.code === 0) {
this.historyVersions = res.data;
} else {
this.$message.error(res.msg || "获取历史版本失败");
}
this.historyLoading = false;
}).catch(() => {
this.historyLoading = false;
this.$message.error("获取历史版本失败");
});
},
// 查看历史版本XML
viewHistoryXml(row) {
this.xmlDialogVisible = true;
this.modelXml = "加载中...";
this.$axios.get("/platform/workflow/model/history/xml", {
params: { modelId: row.id }
}).then(res => {
this.modelXml = res.data;
}).catch(() => {
this.modelXml = "加载失败";
this.$message.error("获取模型XML失败");
});
},
// 恢复历史版本
restoreVersion(row) {
this.$confirm('确定要恢复到版本'+row.version+'吗?', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 获取历史版本的XML
this.$axios.get("/platform/workflow/model/history/xml", {
params: { modelId: row.id }
}).then(xml => {
// 使用历史版本的XML保存为新版本
this.$axios.post("/platform/workflow/model/save", {
modelId: this.currentModel.id,
processName: this.currentModel.name,
processKey: this.currentModel.key,
category: this.currentModel.category,
bpmnXml: xml
}).then(res => {
if (res.success) {
this.$message.success("版本恢复成功");
this.historyDialogVisible = false;
this.pageData();
} else {
this.$message.error(res.msg || "版本恢复失败");
}
}).catch(() => {
this.$message.error("版本恢复失败");
});
}).catch(() => {
this.$message.error("获取历史版本XML失败");
});
}).catch(() => {});
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,123 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="流程定义"></table-tool>
<el-table
:data="tableData"
:size="tableSize"
@sort-change="pageOrder"
ref="tableRef"
row-key="id"
style="width: 100%"
v-loading="tableLoading"
>
<el-table-column :index="indexMethod" label="序号" type="index" width="80px"></el-table-column>
<el-table-column
:key="column.prop"
:label="column.label"
:prop="column.prop"
:sortable="column.sortable"
:width="column.width"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template scope="{row}" v-if="column.prop=='suspended'">
<el-tag v-if="row.suspended" size="mini" type="info">已挂起</el-tag>
<el-tag v-else size="mini" type="success">已激活</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200px" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="openVersion(scope.row)">版本管理</el-button>
<el-button size="mini" type="primary" @click="openNodeConfig(scope.row)">配置</el-button>
<!-- <el-button size="mini" type="primary" @click="activate(scope.row.id)">激活</el-button>-->
<!-- <el-button size="mini" type="danger" @click="suspend(scope.row.id)">挂起</el-button>-->
<!-- <el-button size="mini" type="danger" @click="onDelete(scope.row.deploymentId)">删除</el-button>-->
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="流程设计" :visible.sync="dialogVisible" width="80%">
<iframe
:src="designerUrl"
frameborder="0"
scrolling="auto"
allowtransparency="true"
allowfullscreen="true"
width="100%"
height="600px"
></iframe>
</el-dialog>
<process-version ref="versionRef"></process-version>
<node-config ref="nodeConfigRef"></node-config>
</div>
<script>
<!--#include('processVersion.js'){}#-->
<!--#include('nodeConfig.js'){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"process-version": processVersion,
"node-config": nodeConfig
},
data() {
return {
tableColumns: [
{ label: "ID", prop: "id" },
{ label: "名称", prop: "name" },
{ label: "Key", prop: "key" },
{ label: "分类", prop: "category" },
{ label: "状态", prop: "suspended" },
{ label: "版本", prop: "version" },
{ label: "创建时间", prop: "createTime" }
],
dialogVisible: false,
designerUrl: ""
}
},
methods: {
openVersion(row) {
this.$refs.versionRef.onOpen(row.key)
},
openNodeConfig(row){
this.$refs.nodeConfigRef.onOpen(row.id)
},
deploy(modelId) {
this.$confirm("确定要部署吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/workflow/process/definition/deploy", { modelId }).then((res) => {
if (res.code === 0) {
this.$message.success("部署成功")
this.pageData()
}
})
})
.catch(() => {})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,93 @@
const nodeConfig = {
template: /*language=HTML*/ `
<el-dialog title="节点配置" :visible.sync="visible" fullscreen append-to-body>
<el-table :data="tableData" border>
<el-table-column prop="nodeName" label="节点名称" />
<el-table-column prop="nodeId" label="节点编码" />
<el-table-column prop="pcFormUrl" label="PC表单地址" />
<el-table-column prop="mobileFormUrl" label="移动端表单地址" />
<el-table-column prop="allowSelectNextUser" label="选择下一步处理人" />
<el-table-column prop="allowSelectCcUser" label="是否允许选择抄送人" />
<el-table-column prop="allowTransfer" label="是否允许转办" />
<el-table-column prop="allowReject" label="是否允许驳回" />
<el-table-column label="操作" width="150px">
<template slot-scope="scope">
<el-button type="text" @click="onEdit(scope.row)">配置</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="节点配置" :visible.sync="editVisible" width="700px" append-to-body>
<el-form ref="form" :model="formData" label-width="150px">
<el-form-item label="节点名称">
<el-input v-model="formData.nodeName" readonly />
</el-form-item>
<el-form-item label="节点编码">
<el-input v-model="formData.nodeId" readonly />
</el-form-item>
<el-form-item label="PC表单地址" prop="pcFormUrl">
<el-input v-model="formData.pcFormUrl" />
</el-form-item>
<el-form-item label="移动端表单地址" prop="mobileFormUrl">
<el-input v-model="formData.mobileFormUrl" />
</el-form-item>
<el-form-item label="选择下一步处理人" prop="allowSelectNextUser">
<el-switch v-model="formData.allowSelectNextUser" />
</el-form-item>
<el-form-item label="是否允许选择抄送人" prop="allowSelectCcUser">
<el-switch v-model="formData.allowSelectCcUser" />
</el-form-item>
<el-form-item label="是否允许转办" prop="allowTransfer">
<el-switch v-model="formData.allowTransfer" />
</el-form-item>
<el-form-item label="是否允许驳回" prop="allowReject">
<el-switch v-model="formData.allowReject" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editVisible = false">取 消</el-button>
<el-button type="primary" @click="onSave">确 定</el-button>
</span>
</el-dialog>
</el-dialog>
`,
data() {
return {
visible: false,
title: "标题",
processDefinitionId: null,
tableData: [],
editVisible: false,
formData: {}
}
},
methods: {
onOpen(processDefinitionId) {
this.visible = true
this.processDefinitionId = processDefinitionId
this.loadData()
},
loadData() {
this.$axios.post("/platform/workflow/process/definition/userTasksConfig/" + this.processDefinitionId).then((res) => {
if (res.code === 0) {
this.tableData = res.data
}
})
},
onEdit(row) {
this.editVisible = true
this.formData = { ...row }
},
onSave() {
this.$axios.post("/platform/workflow/process/definition/saveUserTasksConfig", this.formData).then(res => {
if (res.code === 0) {
this.$message.success("保存成功")
this.editVisible = false
this.loadData()
}
})
}
}
}
@@ -0,0 +1,153 @@
const processVersion = {
template: /*language=HTML*/ `
<el-dialog title="版本信息" :visible.sync="visible" fullscreen>
<el-table :data="tableData">
<el-table-column label="序号" type="index" width="80px"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="key" prop="key"></el-table-column>
<el-table-column label="版本" prop="version"></el-table-column>
<el-table-column label="状态" prop="suspended">
<template slot-scope="scope">
<el-tag v-if="scope.row.suspended" size="mini" type="info">挂起</el-tag>
<el-tag v-else size="mini" type="success">激活</el-tag>
</template>
</el-table-column>
<el-table-column label="部署状态" prop="deploymentValid">
<template slot-scope="scope">
<el-tag v-if="scope.row.deploymentValid" size="mini" type="success">有效</el-tag>
<el-tag v-else size="mini" type="danger">无效</el-tag>
</template>
</el-table-column>
<el-table-column label="部署时间" prop="deploymentTime">
<template slot-scope="scope">
{{ scope.row.deploymentTime ? new Date(scope.row.deploymentTime).toLocaleString() : '' }}
</template>
</el-table-column>
<el-table-column label="操作" width="300px">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openDiagram(scope.row.id)">流程图</el-button>
<el-button size="mini" type="text" v-if="scope.row.suspended" @click="activate(scope.row.id)">
激活
</el-button>
<el-button size="mini" type="text" v-if="!scope.row.suspended" @click="suspend(scope.row.id)">
挂起
</el-button>
<el-button size="mini" type="text" @click="onDelete(scope.row.deploymentId)">删除</el-button>
<el-button size="mini" type="text" v-if="!scope.row.deploymentValid" @click="cleanupInvalidDefinition(scope.row.id)">清理</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="流程图" :visible.sync="diagramVisible" width="70%" append-to-body>
<img :src="diagramUrl" alt="">
</el-dialog>
</el-dialog>
`,
data() {
return {
visible: false,
tableData: [],
processKey: null,
diagramVisible: false,
diagramUrl: null
}
},
methods: {
onOpen(processKey) {
this.visible = true
this.processKey = processKey
this.loadData()
},
loadData() {
this.$axios.post("/platform/workflow/process/definition/versions", { processKey: this.processKey }).then((res) => {
if (res.code === 0) {
this.tableData = res.data
}
})
},
activate(processDefinitionId) {
this.$confirm("确定要激活吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/workflow/process/definition/activate/" + processDefinitionId).then((res) => {
if (res.code === 0) {
this.$message.success("激活成功")
this.loadData()
}
})
})
},
suspend(processDefinitionId) {
this.$confirm("确定要挂起吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/workflow/process/definition/suspend/" + processDefinitionId).then((res) => {
if (res.code === 0) {
this.$message.success("挂起成功")
this.loadData()
}
})
})
},
onDelete(deploymentId) {
this.$confirm("确定要删除吗?删除后将无法恢复", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 添加级联删除选项
this.$confirm("是否级联删除?(级联删除会同时删除相关的流程实例)", "提示", {
confirmButtonText: "是",
cancelButtonText: "否",
type: "warning"
}).then(() => {
// 级联删除
this.$axios.post("/platform/workflow/process/definition/delete/" + deploymentId + "?cascade=true").then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.loadData()
} else {
this.$message.error(res.msg || "删除失败")
}
})
}).catch(() => {
// 非级联删除
this.$axios.post("/platform/workflow/process/definition/delete/" + deploymentId + "?cascade=false").then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.loadData()
} else {
this.$message.error(res.msg || "删除失败")
}
})
})
})
},
cleanupInvalidDefinition(processDefinitionId) {
this.$confirm("确定要清理此无效流程定义吗?此操作将从数据库中删除此记录。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/workflow/process/definition/cleanup/" + processDefinitionId).then((res) => {
if (res.code === 0) {
this.$message.success("清理成功")
this.loadData()
}
})
})
},
openDiagram(processDefinitionId) {
this.diagramVisible = true
this.diagramUrl = "/platform/workflow/process/definition/diagram/" + processDefinitionId
}
}
}
@@ -0,0 +1,578 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="app" v-cloak>
<div class="task-todo-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon todo-icon">
<i class="el-icon-s-claim"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{todoCount}}</div>
<div class="stat-label">待办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon done-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{doneCount}}</div>
<div class="stat-label">已办任务</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon started-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{startedCount}}</div>
<div class="stat-label">我发起的</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 搜索筛选区域 -->
<el-card shadow="never" class="filter-section">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item>
<el-input v-model="searchForm.keyword" placeholder="请输入关键词搜索" prefix-icon="el-icon-search" clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.category" placeholder="所有流程类型" clearable>
<el-option v-for="type in processTypes" :key="type.value" :label="type.label" :value="type.value"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 任务列表区域 -->
<el-card shadow="never" class="task-section">
<div slot="header" class="task-header">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="待办任务" name="todo"></el-tab-pane>
<el-tab-pane label="已办任务" name="done"></el-tab-pane>
<el-tab-pane label="我发起的" name="started"></el-tab-pane>
</el-tabs>
</div>
<!-- 表格展示 -->
<el-table
v-loading="loading"
:data="tasks"
style="width: 100%"
:key="activeTab"
:header-cell-style="{backgroundColor: '#f5f7fa'}"
:row-class-name="tableRowClassName"
>
<el-table-column prop="processInstanceName" label="流程名称" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span class="task-title">{{scope.row.processInstanceName || scope.row.title}}</span>
</template>
</el-table-column>
<el-table-column prop="taskName" label="任务节点" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="processDefinitionName" label="流程类型" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">{{scope.row.processDefinitionName || scope.row.processType}}</template>
</el-table-column>
<el-table-column prop="applyUserName" label="申请人" min-width="120"></el-table-column>
<el-table-column label="时间" min-width="180">
<template slot-scope="scope">
<div v-if="activeTab === 'todo'">
<i class="el-icon-time"></i>
{{scope.row.createTime}}
</div>
<div v-else-if="activeTab === 'done'">
<i class="el-icon-check"></i>
{{scope.row.endTime}}
</div>
<div v-else-if="activeTab === 'started'">
<i class="el-icon-s-promotion"></i>
{{scope.row.startTime}}
</div>
</template>
</el-table-column>
<el-table-column v-if="activeTab === 'started'" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row)" size="small" effect="light">{{scope.row.status || '进行中'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template slot-scope="scope">
<el-button v-if="activeTab === 'todo'" type="primary" size="mini" @click="handleTask(scope.row)">处理</el-button>
<el-button type="info" size="mini" @click="viewTaskDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态展示 -->
<el-empty v-if="tasks.length === 0" :description="getEmptyText()"></el-empty>
<!-- 分页 -->
<div class="pagination-container" v-if="tasks.length > 0">
<el-pagination
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="total"
></el-pagination>
</div>
</el-card>
<!-- 任务详情对话框 -->
<el-dialog title="任务详情" :visible.sync="dialogVisible" width="600px" :close-on-click-modal="false">
<div v-if="currentTask" class="task-detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="流程名称">{{currentTask.processInstanceName || currentTask.title}}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{currentTask.taskName || '-'}}</el-descriptions-item>
<el-descriptions-item label="流程类型">{{currentTask.processDefinitionName || currentTask.processType}}</el-descriptions-item>
<el-descriptions-item label="申请人">{{currentTask.applyUserName || '-'}}</el-descriptions-item>
<el-descriptions-item label="申请部门">{{currentTask.applyUserUnitId || '-'}}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{currentTask.createTime}}</el-descriptions-item>
<el-descriptions-item v-if="currentTask.description" label="任务描述">
<div class="description-content">{{currentTask.description}}</div>
</el-descriptions-item>
<el-descriptions-item v-if="currentTask.taskMobileFormUrl" label="表单链接">
<el-link type="primary" :href="currentTask.taskMobileFormUrl" target="_blank">点击查看详细表单</el-link>
</el-descriptions-item>
</el-descriptions>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="activeTab === 'todo'" type="primary" @click="handleTask(currentTask)">处理任务</el-button>
</span>
</el-dialog>
</div>
</div>
<style>
.task-todo-center {
padding: 20px;
}
/* 统计卡片样式 */
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
}
.stat-card .el-card__body {
padding: 20px;
}
.stat-content {
display: flex;
align-items: center;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
font-size: 28px;
}
.todo-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.done-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.started-icon {
background-color: rgba(230, 162, 60, 0.1);
color: #e6a23c;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
/* 搜索区域样式 */
.filter-section {
margin-bottom: 20px;
}
.filter-section .el-card__body {
padding: 15px 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 15px;
}
/* 任务列表区域样式 */
.task-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.task-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.task-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.task-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.task-header .el-tabs__item.is-active {
color: #409eff;
font-weight: 500;
}
.task-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.task-title {
font-weight: 500;
color: #303133;
}
/* 表格行样式 */
.el-table .hover-row {
background-color: #f5f7fa;
}
/* 优化后的分页样式 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
.pagination-container .el-pagination {
padding: 0;
font-weight: normal;
display: flex;
align-items: center;
}
.pagination-container .el-pagination .btn-prev,
.pagination-container .el-pagination .btn-next {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
padding: 0 10px;
height: 32px;
line-height: 32px;
margin: 0 5px;
}
.pagination-container .el-pagination .btn-prev:hover,
.pagination-container .el-pagination .btn-next:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li {
background-color: #f4f6f8;
color: #606266;
border-radius: 4px;
min-width: 32px;
height: 32px;
line-height: 32px;
margin: 0 3px;
font-weight: normal;
transition: all 0.3s;
}
.pagination-container .el-pagination .el-pager li:hover {
color: var(--color-primary);
background-color: #ecf5ff;
}
.pagination-container .el-pagination .el-pager li.active {
background-color: var(--color-primary);
color: #fff;
font-weight: bold;
}
.pagination-container .el-pagination .el-pagination__jump {
margin-left: 15px;
color: #606266;
}
.pagination-container .el-pagination .el-pagination__editor.el-input {
width: 50px;
margin: 0 5px;
}
.pagination-container .el-pagination .el-pagination__editor.el-input .el-input__inner {
height: 28px;
border-radius: 4px;
}
/* 任务详情样式 */
.task-detail .el-descriptions {
margin-bottom: 20px;
}
.description-content {
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
}
</style>
<script>
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 搜索表单
searchForm: {
keyword: "",
category: ""
},
// 流程类型选项
processTypes: [],
// 任务列表
tasks: [],
loading: false,
// 分页
currentPage: 1,
pageSize: 10,
total: 0,
// 标签页
activeTab: "todo",
// 当前任务
currentTask: null,
dialogVisible: false
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize)
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.getProcessTypes(), this.getTasks()])
},
// 获取统计数据
async getStatistics() {
try {
const res = await $.post("/platform/warmFlow/todoCenter/statistics")
if (res.code === 0) {
const { todoCount, doneCount, startedCount } = res.data
this.todoCount = todoCount
this.doneCount = doneCount
this.startedCount = startedCount
}
} catch (error) {
this.$message.error("获取统计数据失败")
console.error("获取统计数据失败:", error)
}
},
// 获取流程类型
async getProcessTypes() {
try {
const res = await $.post("/platform/warmFlow/todoCenter/category")
if (res.code === 0) {
this.processTypes = res.data
}
} catch (error) {
this.$message.error("获取流程类型失败")
console.error("获取流程类型失败:", error)
}
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize,
type: this.activeTab,
keyword: this.searchForm.keyword,
category: this.searchForm.category
}
const res = await $.post("/platform/warmFlow/todoCenter/" + this.activeTab, params)
if (res.code === 0) {
this.tasks = res.data.list
this.total = res.data.totalCount
this.dialogVisible = false
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
// 处理标签页点击
handleTabClick(tab) {
this.activeTab = tab.name
this.currentPage = 1
this.getTasks()
},
// 搜索
search() {
this.currentPage = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.searchForm = {
keyword: "",
category: ""
}
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.currentPage = val
this.getTasks()
},
// 查看任务详情
viewTaskDetail(task) {
this.currentTask = task
this.dialogVisible = true
},
// 处理任务
async handleTask(task) {
if (!task) return
// 如果有表单链接,直接跳转
if (task.taskMobileFormUrl) {
window.open(task.taskMobileFormUrl, "_blank")
return
}
},
// 获取状态对应的类型
getStatusType(task) {
if (!task.status) return "info"
switch (task.status) {
case "审批中":
return "primary"
case "已完成":
return "success"
case "已通过":
return "success"
case "已拒绝":
return "danger"
default:
return "info"
}
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
},
// 表格行类名
tableRowClassName({ row, rowIndex }) {
return ""
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,49 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<div id="approvalApp" v-cloak>
<snaker-flow
:task_id="taskId"
:instance_id="instanceId"
:business_id="businessId"
:pjax_urls="{
applicationInfo: '/platform/article/write/index_',
taskForm: '/platform/article/write/index_'
}"
:pjax_config="{
push: false,
replace: false,
timeout: 10000
}"
></snaker-flow>
</div>
<script>
new Vue({
el: "#approvalApp",
data() {
return {
taskId: null,
instanceId: null,
businessId: null,
formConfig: {}
}
},
methods: {
handleTaskSubmitted() {},
handleCancel() {
window.history.back()
}
},
created() {
this.taskId = new URLSearchParams(window.location.search).get("taskId")
this.instanceId = new URLSearchParams(window.location.search).get("instanceId")
this.businessId = new URLSearchParams(window.location.search).get("businessId")
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,48 @@
<div id="article_branch_union_approval_form">
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-form-item label="审批意见" prop="approvalOpinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
<!-- <el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">-->
<!-- <pc-signature v-model="formData.approvalSignature"></pc-signature>-->
<!-- </el-form-item>-->
</el-form>
<snaker-flow-task-form-action @task-action="handleTaskAction"
@cancel="handleCancel">
</snaker-flow-task-form-action>
</div>
<script>
new Vue({
el: "#article_branch_union_approval_form",
data() {
return {
taskId: GetQueryString("taskId"),
formData: {
approval: "",
approvalOpinion: ""
}
}
},
methods: {
handleTaskAction(val) {
console.log(val)
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify(val)
}).then(res => {
if (res.code === 0) {
this.$message.success("操作成功")
// 发送完成消息
window.parent.postMessage({
type: "task-complete"
}, "*")
}
})
},
handleCancel(val) {
console.log(val)
}
}
})
</script>
@@ -36,11 +36,17 @@ layout("/layouts/platform.html"){
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
<el-table-column label="当前节点" prop="processInstanceNodeName" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" fixed="right">
<el-table-column prop="taskName" label="当前节点"></el-table-column>
<el-table-column prop="instanceState" label="流程状态">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200px" fixed="right">
<template slot-scope="{row}">
<el-button @click="onOpen(row.id)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.processInstanceTaskStatus==='ACTIVE'" @click="openApproval(row)" size="mini" type="primary">
<el-button v-if="row.taskState === 10" @click="openApproval(row)" size="mini" type="primary">
审核
</el-button>
<el-button
@@ -57,19 +63,25 @@ layout("/layouts/platform.html"){
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #public>
<el-dialog title="" :visible.sync="showApprovalForm" width="90%">
<info ref="infoRef"></info>
<el-row type="flex">
<flow-form-button :task_id="taskId"></flow-form-button>
<!-- <el-button plain @click="$refs.guava.index()">取消</el-button>-->
<!-- <el-button type="danger" @click="doApproval('BACK')">退回修改</el-button>-->
<!-- <el-button type="primary" @click="doApproval('PASS')">同意</el-button>-->
</el-row>
</el-dialog>
<template #public>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-form-item label="审核意见" prop="approvalOpinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button plain @click="$refs.guava.index()">取消</el-button>
<el-button type="danger" @click="doApproval('BACK')">退回修改</el-button>
<el-button type="primary" @click="doApproval('PASS')">同意</el-button>
<el-row type="flex">
<flow-form-button :task_id="taskId"></flow-form-button>
<!-- <el-button plain @click="$refs.guava.index()">取消</el-button>-->
<!-- <el-button type="danger" @click="doApproval('BACK')">退回修改</el-button>-->
<!-- <el-button type="primary" @click="doApproval('PASS')">同意</el-button>-->
</el-row>
</div>
</template>
@@ -89,7 +101,8 @@ layout("/layouts/platform.html"){
pageForm: {
approval: false
},
showApprovalForm: false
showApprovalForm: false,
taskId: ""
}
},
methods: {
@@ -100,11 +113,7 @@ layout("/layouts/platform.html"){
})
},
openApproval(row) {
this.$refs.guava.public(() => {
this.$refs.infoRef.onOpen(row.id)
this.formData = row.approvalParam
this.showApprovalForm = true
})
window.open("/flow/common/approval/form?taskId=" + row.taskId + "&instanceId=" + row.instanceId + "&businessId=" + row.businessNo)
},
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
@@ -0,0 +1,82 @@
<div id="article-full-form">
<el-descriptions :column="2" border class="flow-task-form">
<el-descriptions-item label="投稿人姓名">{{ viewData.userName }}</el-descriptions-item>
<el-descriptions-item label="投稿人工号">{{ viewData.loginName }}</el-descriptions-item>
<el-descriptions-item label="投稿人单位">{{ viewData.unitName }}</el-descriptions-item>
<el-descriptions-item label="投稿人工会">{{ viewData.unionName }}</el-descriptions-item>
<el-descriptions-item label="投稿人联系方式">{{ viewData.mobile }}</el-descriptions-item>
<el-descriptions-item label="稿件修改人联系方式">{{ viewData.mobile2 }}</el-descriptions-item>
<el-descriptions-item label="投稿标题" :span="2">{{ viewData.title }}</el-descriptions-item>
<el-descriptions-item label="投稿说明" :span="2">{{ viewData.excerpt }}</el-descriptions-item>
<el-descriptions-item label="稿件" :span="2">
<file-preview :files="viewData.files" complete_result></file-preview>
</el-descriptions-item>
</el-descriptions>
<template v-for="task in doneTasks">
<div class="task-panel mt10">
<div class="task-panel-header">
{{ task.displayName }}
</div>
<el-descriptions border class="flow-task-form" :column="2" :key="task.id">
<el-descriptions-item label="办理用户">{{ task.operator }}</el-descriptions-item>
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
</div>
<script>
new Vue({
el: "#article-full-form",
data() {
return {
businessId: GetQueryString("businessId"),
instanceId: GetQueryString("instanceId"),
viewData: {},
doneTasks: []
}
},
methods: {
info() {
this.$axios.post("/platform/article/common/info", { id: this.businessId }).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
})
},
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { instanceId: this.instanceId }).then(res => {
if (res.code === 0) {
this.doneTasks = res.data
}
})
}
},
created() {
this.info()
this.getDoneTasks()
}
})
</script>
<style>
.task-panel{
background: white;
border-radius: 4px;
overflow: hidden;
}
.task-panel-header{
background: rgb(250, 250, 250);
border: 1px solid rgb(228, 231, 237);
border-bottom: none;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
</style>
@@ -41,16 +41,19 @@ const info = {
v-if="task.extVariable.bpmTaskApprovalType === 'REJECT'">拒绝
</el-tag>
<template v-if="task.extVariable.bpmTaskApprovalType === 'DYNAMIC'">
<el-tag v-if="task.extVariable.RESULT === 'BACK_TO_START'" type="danger" size="mini">
<el-tag v-if="task.extVariable.RESULT === 'BACK_TO_START'" type="danger"
size="mini">
退回至投稿人
<span v-if="task.extVariable.processAgain===false">不需要重新走流程</span>
<span v-if="task.extVariable.processAgain===true">需要重新走流程</span>
</el-tag>
<el-tag v-if="task.extVariable.RESULT === 'PASS'" type="success" size="mini">同意</el-tag>
<el-tag v-if="task.extVariable.RESULT === 'PASS'" type="success" size="mini">同意
</el-tag>
<el-tag v-if="task.extVariable.RESULT === 'BACK_TO_CLUB'" type="danger" size="mini">
退回到协会
</el-tag>
<el-tag v-if="task.extVariable.RESULT === 'BACK_TO_UNION'" type="danger" size="mini">
<el-tag v-if="task.extVariable.RESULT === 'BACK_TO_UNION'" type="danger"
size="mini">
退回到分工会
</el-tag>
</template>
@@ -72,8 +75,8 @@ const info = {
}
},
methods: {
onOpen(id) {
this.$axios.post("/platform/article/common/info", { id }).then((res) => {
onOpen(row) {
this.$axios.post("/platform/article/common/info", { id: row.businessKey }).then((res) => {
if (res.code === 0) {
this.viewData = res.data
}
@@ -7,7 +7,8 @@ layout("/layouts/platform.html"){
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度"
style="width: 100%"></el-date-picker>
</search-item>
<search-item label="标题">
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
@@ -25,27 +26,25 @@ layout("/layouts/platform.html"){
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
<el-table-column label="当前节点" prop="processInstanceNodeName" show-overflow-tooltip></el-table-column>
<el-table-column prop="taskName" label="当前节点"></el-table-column>
<el-table-column prop="instanceState" label="流程状态">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template slot-scope="{row}">
<el-button @click="onOpen(row.id)" size="mini" type="primary">查看</el-button>
<el-button v-if="[10,30,34,38,50,65].includes(row.processInstanceNodeCode)" @click="onEdit(row.id)" size="mini" type="primary">
<el-button v-if="row.taskKey === 'apply' || !row.instanceId" @click="onEdit(row)" size="mini"
type="primary">
编辑
</el-button>
<el-button
v-if="[20].includes(row.processInstanceNodeCode) && !row.nextTaskIsComplete"
size="mini"
type="danger"
@click="onRevoke(row.id)"
>
撤销
<el-button v-if="['waiting'].includes(row.flow_status)" size="mini" type="danger"
@click="onRevoke(row.id)">撤销
</el-button>
<el-button
v-if="[10,30,34,50,70].includes(row.processInstanceNodeCode)||$auth.hasRoleOr('SYSADMIN')"
@click="onDelete(row.id)"
size="mini"
type="danger"
>
<el-button v-if="row.taskKey === 'apply' || !row.instanceId"
@click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
@@ -77,8 +76,19 @@ layout("/layouts/platform.html"){
this.$refs.infoRef.onOpen(id)
})
},
onEdit(id) {
this.$store.dispatch("pjaxRoute", "/platform/article/write?id=" + id)
onEdit(row) {
window.open("/flow/common/approval/form?taskId=" + (row.taskId || "") + "&instanceId=" + (row.instanceId || "") + "&businessId=" + row.id
+ "&defineKey=XWTG")
// $.pjax({
// url: "/platform/article/write?id=" + businessNo + "&taskId=" + taskId + "&instanceId=" + instanceId,
// container: "#sub-app-container-main-content",
// maxCacheLength: 0,
// push: false,
// replace: true,
// fragment: "#sub-app-container-main-content",
// timeout: 8000
// })
},
onRevoke(id) {
this.$confirm("您确定要撤销申请吗?", "提示", {
@@ -86,7 +96,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/article/mine/revokeApply", { id }).then((resp) => {
this.$axios.post("/platform/article/write/revokeApply", { id }).then((resp) => {
this.$message.success(resp.msg)
this.doSearch()
})
@@ -0,0 +1,48 @@
<div id="article_school_union_approval_form">
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-form-item label="审批意见" prop="approvalOpinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
<!-- <el-form-item label="电子签名" prop="approvalSignature" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">-->
<!-- <pc-signature v-model="formData.approvalSignature"></pc-signature>-->
<!-- </el-form-item>-->
</el-form>
<snaker-flow-task-form-action @task-action="handleTaskAction"
@cancel="handleCancel">
</snaker-flow-task-form-action>
</div>
<script>
new Vue({
el: "#article_school_union_approval_form",
data() {
return {
taskId: GetQueryString("taskId"),
formData: {
approval: "",
approvalOpinion: ""
}
}
},
methods: {
handleTaskAction(val) {
console.log(val)
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify(val)
}).then(res => {
if (res.code === 0) {
this.$message.success("操作成功")
// 发送完成消息
window.parent.postMessage({
type: "task-complete"
}, "*")
}
})
},
handleCancel(val) {
console.log(val)
}
}
})
</script>
@@ -7,26 +7,18 @@ layout("/layouts/platform.html"){
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
<el-date-picker
:clearable="true"
v-model="pageForm.year"
type="year"
value-format="yyyy"
placeholder="年度"
style="width: 100%"
></el-date-picker>
</search-item>
<search-item label="标题">
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" placeholder="请选择所属工会" filterable clearable style="width: 100%">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属单位:">
<el-select v-model="pageForm.unitId" placeholder="请选择所属单位" filterable clearable style="width: 100%">
<el-option v-for="item in unitOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="投稿来源:">
<el-select v-model="pageForm.origin" placeholder="请选择投稿来源" filterable clearable style="width: 100%">
<el-option v-for="item in dict.type.ARTICLE_ORIGIN" :key="item.code" :label="item.label" :value="item.code"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
@@ -42,25 +34,19 @@ layout("/layouts/platform.html"){
<el-table-column prop="loginName" label="投稿人工号"></el-table-column>
<el-table-column prop="userName" label="投稿人姓名"></el-table-column>
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
<el-table-column
prop="unionName"
label="投稿人工会"
v-if="$auth.hasRoleOr(['SCHOOL_UNION_ARTICLE_FGH_ADMIN','SYSADMIN'])"
></el-table-column>
<el-table-column
prop="clubName"
label="投稿人协会"
v-if="$auth.hasRoleOr(['SCHOOL_UNION_ARTICLE_CLUB_ADMIN','SYSADMIN'])"
></el-table-column>
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
<el-table-column prop="mode" label="投稿来源">
<template slot-scope="{row}">{{dict.type.ARTICLE_ORIGIN.find(v=>v.code===row.origin)?.label}}</template>
<el-table-column prop="taskName" label="当前节点"></el-table-column>
<el-table-column prop="instanceState" label="流程状态">
<template slot-scope="{row}">
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message"
size="small"></enum-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" prop="processInstanceNodeName" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<el-table-column label="操作" width="200px" fixed="right">
<template slot-scope="{row}">
<el-button @click="onOpen(row.id)" size="mini" type="primary">查看</el-button>
<el-button v-if="row.processInstanceTaskStatus==='ACTIVE'" @click="openApproval(row)" size="mini" type="primary">
<el-button v-if="row.taskState === 10" @click="openApproval(row)" size="mini" type="primary">
审核
</el-button>
<el-button
@@ -77,19 +63,25 @@ layout("/layouts/platform.html"){
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #public>
<el-dialog title="" :visible.sync="showApprovalForm" width="90%">
<info ref="infoRef"></info>
<el-row type="flex">
<flow-form-button :task_id="taskId"></flow-form-button>
<!-- <el-button plain @click="$refs.guava.index()">取消</el-button>-->
<!-- <el-button type="danger" @click="doApproval('BACK')">退回修改</el-button>-->
<!-- <el-button type="primary" @click="doApproval('PASS')">同意</el-button>-->
</el-row>
</el-dialog>
<template #public>
<div v-if="showApprovalForm">
<div class="process-title">{{formData.processInstanceNodeName}}</div>
<el-form :model="formData" ref="approvalFormRef" label-position="left" label-width="80px">
<el-form-item label="审核意见" prop="approvalOpinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<user-opinion-textarea v-model="formData.approvalOpinion"></user-opinion-textarea>
</el-form-item>
</el-form>
<el-row type="flex" justify="end">
<el-button plain @click="$refs.guava.index()">取消</el-button>
<el-button type="danger" @click="doApproval('BACK')">退回到编辑初审</el-button>
<el-button type="primary" @click="doApproval('PASS')">同意</el-button>
<el-row type="flex">
<flow-form-button :task_id="taskId"></flow-form-button>
<!-- <el-button plain @click="$refs.guava.index()">取消</el-button>-->
<!-- <el-button type="danger" @click="doApproval('BACK')">退回修改</el-button>-->
<!-- <el-button type="primary" @click="doApproval('PASS')">同意</el-button>-->
</el-row>
</div>
</template>
@@ -103,14 +95,14 @@ layout("/layouts/platform.html"){
el: "#app",
store,
mixins: [initTableMixins],
dicts: ["ARTICLE_ORIGIN"],
components: { info },
data() {
return {
pageForm: {
approval: false
},
showApprovalForm: false
showApprovalForm: false,
taskId: ""
}
},
methods: {
@@ -121,18 +113,14 @@ layout("/layouts/platform.html"){
})
},
openApproval(row) {
this.$refs.guava.public(() => {
this.$refs.infoRef.onOpen(row.id)
this.formData = row.approvalParam
this.showApprovalForm = true
})
window.open("/flow/common/approval/form?taskId=" + row.taskId + "&instanceId=" + row.instanceId + "&businessId=" + row.businessNo)
},
doApproval(approvalType) {
this.formData.bpmTaskApprovalType = approvalType
this.$refs.approvalFormRef.validate((valid) => {
if (valid) {
this.$axios
.post("/platform/article/schoolUnionApproval/approval", {
.post("/platform/article/branchUnionApproval/approval", {
approval: JSON.stringify(this.formData)
})
.then((res) => {
@@ -151,7 +139,7 @@ layout("/layouts/platform.html"){
cancelButtonText: "取消",
type: "info"
}).then(() => {
this.$axios.post("/platform/article/schoolUnionApproval/revoke", { taskId }).then((res) => {
this.$axios.post("/platform/article/branchUnionApproval/revoke", { taskId }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
@@ -161,8 +149,6 @@ layout("/layouts/platform.html"){
}
},
created() {
this.$businessTool.listUnit().then((res) => (this.unitOptions = res))
this.$businessTool.listUnion().then((res) => (this.unionOptions = res))
this.pageData()
}
})
@@ -55,11 +55,11 @@ layout("/layouts/platform.html"){
</el-form-item>
<el-form-item prop="files" label="稿件">
<file-upload
:value.sync="formData.files"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="drag"
:value.sync="formData.files"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="drag"
></file-upload>
</el-form-item>
</el-form>
@@ -86,15 +86,17 @@ layout("/layouts/platform.html"){
data() {
return {
id: GetQueryString("id"),
taskId: GetQueryString("taskId"),
instanceId: GetQueryString("instanceId"),
formData: {},
formRules: {
title: [{required: true, message: "必填", trigger: ["change", "blur"]}],
excerpt: [{required: true, message: "必填", trigger: ["change", "blur"]}],
clubId: [{required: true, message: "必填", trigger: ["change", "blur"]}],
files: [{required: true, message: "必填", trigger: ["change", "blur"]}],
origin: [{required: true, message: "必填", trigger: ["change", "blur"]}],
mobile: [{required: true, message: "必填", trigger: ["change", "blur"]}],
mobile2: [{required: true, message: "必填", trigger: ["change", "blur"]}]
title: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
excerpt: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
clubId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ required: false, message: "必填", trigger: ["change", "blur"] }],
origin: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
mobile: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
mobile2: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
permission: {}
}
@@ -106,7 +108,11 @@ layout("/layouts/platform.html"){
this.$message.warning("请填写标题")
return
}
this.$axios.post("/platform/article/write/save", {article: JSON.stringify(this.formData)}).then((res) => {
this.$axios.post("/platform/article/write/save", {
article: JSON.stringify(this.formData),
taskId: this.taskId,
instanceId: this.instanceId
}).then((res) => {
if (res.code === 0) {
this.$message.success("保存成功")
this.$store.dispatch("pjaxRoute", "/platform/article/mine")
@@ -117,7 +123,11 @@ layout("/layouts/platform.html"){
doSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios.post("/platform/article/write/submit", {article: JSON.stringify(this.formData)}).then((res) => {
this.$axios.post("/platform/article/write/submit", {
article: JSON.stringify(this.formData),
taskId: this.taskId,
instanceId: this.instanceId
}).then((res) => {
if (res.code === 0) {
this.$message.success("提交成功")
this.$store.dispatch("pjaxRoute", "/platform/article/mine")
@@ -137,13 +147,13 @@ layout("/layouts/platform.html"){
init() {
if (this.id) {
this.$axios.post("/platform/article/write/get", {id: this.id}).then((res) => {
this.$axios.post("/platform/article/write/get", { id: this.id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
} else {
const {username, loginname, id, unit, union, mobile} = this.$store.state.user
const { username, loginname, id, unit, union, mobile } = this.$store.state.user
this.formData = {
userName: username,
loginName: loginname,
@@ -153,7 +163,7 @@ layout("/layouts/platform.html"){
unionId: union?.id,
unionName: union?.name,
mobile: mobile,
mobile2: mobile,
mobile2: mobile
}
}
}
@@ -0,0 +1,183 @@
<div id="artile_write">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix="" class="flow-task-form">
<el-descriptions :column="2" border>
<el-descriptions-item label="投稿人姓名">{{formData.userName}}</el-descriptions-item>
<el-descriptions-item label="投稿人工号">{{formData.loginName}}</el-descriptions-item>
<el-descriptions-item label="投稿人单位">{{formData.unitName}}</el-descriptions-item>
<el-descriptions-item label="投稿人工会">{{formData.unionName}}</el-descriptions-item>
<el-descriptions-item label="投稿来源" :span="2">
<el-form-item prop="origin" label="投稿来源" label-width="0">
<el-radio-group v-model="formData.origin" size="small">
<el-radio border v-for="item in origins" :label="item.code" :key="item.code">{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="投稿人协会" v-if="formData.origin === 'ARTICLE_ORIGIN_CLUB'">
<el-form-item prop="clubId" label="投稿人协会" v-if="formData.origin === 'ARTICLE_ORIGIN_CLUB'">
<el-select v-model="formData.clubId" placeholder="请选择" style="width: 100%">
<el-option v-for="item in permission?.clubs" :key="item.id" :label="item.clubName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="投稿人联系方式" :span="2">
<el-form-item prop="title" label="投稿标题">
<el-input v-model="formData.title" maxlength="100" show-word-limit placeholder="请输入标题"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="投稿人联系方式" :span="2">
<el-form-item prop="mobile" label="投稿人联系方式">
<el-input v-model="formData.mobile" placeholder="请输入投稿人联系方式"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="修改人联系方式" :span="2">
<el-form-item prop="mobile2" label="修改人联系方式">
<el-input v-model="formData.mobile2" placeholder="请输入修改人联系方式"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="投稿说明" :span="2">
<el-form-item prop="excerpt" label="投稿说明">
<el-input v-model="formData.excerpt" type="textarea" maxlength="500" show-word-limit placeholder="请输入投稿说明"></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="稿件" :span="2">
<file-upload
:value.sync="formData.files"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="drag"
></file-upload>
</el-descriptions-item>
</el-descriptions>
</el-form>
<snaker-flow-task-form-action @task-action="handleTaskAction" @save-draft="handleSaveDraft" @cancel="handleCancel"></snaker-flow-task-form-action>
</div>
<script>
new Vue({
el: "#artile_write",
store,
dicts: ["ARTICLE_ORIGIN"],
computed: {
origins() {
if (this.dict?.type?.ARTICLE_ORIGIN && this.permission && this.permission.roles) {
return this.dict.type.ARTICLE_ORIGIN.filter((item) => this.permission && this.permission.roles.includes(item.code))
}
return []
}
},
data() {
return {
id: GetQueryString("businessId"),
taskId: GetQueryString("taskId"),
instanceId: GetQueryString("instanceId"),
formData: {},
formRules: {
title: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
excerpt: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
clubId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
files: [{ required: false, message: "必填", trigger: ["change", "blur"] }],
origin: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
mobile: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
mobile2: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
permission: {}
}
},
methods: {
init() {
if (this.id) {
this.$axios.post("/platform/article/write/get", { id: this.id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
} else {
const { username, loginname, id, unit, union, mobile } = this.$store.state.user
this.formData = {
userName: username,
loginName: loginname,
userId: id,
unitId: unit?.id,
unitName: unit?.name,
unionId: union?.id,
unionName: union?.name,
mobile: mobile,
mobile2: mobile
}
}
},
checkPermission() {
this.$axios.post("/platform/article/write/checkPermission").then((res) => {
if (res.code === 0) {
this.permission = res.data
}
})
},
handleTaskAction(val) {
console.log(val)
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios
.post("/flow/common/startInstanceAndExecute", {
...val,
bizData: JSON.stringify(this.formData)
})
.then((res) => {
if (res.code === 0) {
this.$message.success("操作成功")
// 发送完成消息
window.parent.postMessage(
{
type: "task-complete"
},
"*"
)
}
})
}
})
},
handleSaveDraft() {
this.$message.success("保存成功")
this.$refs.formRef.validateField(["title"], (errMsg) => {
if (errMsg) {
this.$message.warning("请填写标题")
return
}
this.$axios
.post("/flow/common/startInstance", {
...val,
bizData: JSON.stringify(this.formData)
})
.then((res) => {
if (res.code === 0) {
this.$message.success("操作成功")
// 发送完成消息
window.parent.postMessage(
{
type: "task-complete"
},
"*"
)
}
})
})
},
handleCancel() {}
},
created() {
this.init()
this.checkPermission()
}
})
</script>
@@ -0,0 +1,18 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak></div>
<script>
new Vue({
el: "#app",
data() {
return {}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,878 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
:root {
--primary-color: rgb(0, 78, 100);
--primary-light: rgba(0, 78, 100, 0.1);
--secondary-color: #37A6BD;
--text-color: #333333;
--text-secondary: #666666;
--text-light: #999999;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--border-color: #eeeeee;
--success-color: #07c160;
--warning-color: #ff976a;
}
#app {
min-height: 100vh;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
}
/* 筛选区域样式 */
.filter-section {
background-color: var(--card-bg);
padding: 12px 16px;
margin-bottom: 10px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.filter-section.collapsed {
max-height: 84px;
}
.filter-section.expanded {
max-height: 500px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.filter-title {
font-size: 15px;
font-weight: 500;
color: var(--text-color);
}
.filter-toggle {
color: var(--text-secondary);
display: flex;
align-items: center;
}
.filter-toggle .van-icon {
transition: transform 0.3s;
margin-left: 4px;
}
.filter-toggle .van-icon.rotate {
transform: rotate(180deg);
}
.filter-content {
transition: opacity 0.3s;
}
.filter-content.hidden {
opacity: 0;
height: 0;
overflow: hidden;
}
.filter-row {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-label {
font-size: 13px;
color: var(--text-secondary);
margin-right: 10px;
min-width: 60px;
padding-top: 4px;
}
.filter-options {
display: flex;
flex-wrap: wrap;
flex: 1;
}
.filter-tag {
padding: 4px 10px;
border-radius: 16px;
font-size: 12px;
margin-right: 8px;
margin-bottom: 6px;
background-color: var(--bg-color);
color: var(--text-secondary);
}
.filter-tag.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.filter-search {
padding: 8px 0;
}
.filter-search .van-search {
padding: 0;
}
.filter-search .van-search__content {
background-color: var(--bg-color);
}
.suggestion-list {
padding: 16px;
background-color: var(--bg-color);
flex: 1;
display: flex;
flex-direction: column;
}
.van-pull-refresh, .van-list {
flex: 1;
display: flex;
flex-direction: column;
}
.suggestion-card {
background-color: var(--card-bg);
border-radius: 12px;
margin-bottom: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.02);
}
.suggestion-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.suggestion-title {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.suggestion-status {
font-size: 12px;
padding: 3px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: 500;
}
.status-pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-processing {
background-color: #fff7e6;
color: #fa8c16;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
}
.suggestion-content {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
background-color: var(--bg-color);
padding: 10px;
border-radius: 8px;
word-break: break-all;
white-space: pre-line;
max-height: 3.2em;
text-overflow: ellipsis;
display: block;
}
.suggestion-submitter {
display: flex;
align-items: center;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.suggestion-submitter .van-icon {
margin-right: 5px;
font-size: 14px;
}
.suggestion-unit {
margin-left: 15px;
}
.suggestion-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--text-light);
border-top: 1px solid #f5f5f5;
padding-top: 12px;
}
.suggestion-time {
display: flex;
align-items: center;
}
.suggestion-time .van-icon {
font-size: 14px;
margin-right: 4px;
}
.suggestion-action {
color: var(--primary-color);
display: flex;
align-items: center;
font-weight: 500;
}
.suggestion-action .van-icon {
font-size: 14px;
margin-left: 2px;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 16px;
flex: 1;
}
.empty-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 16px;
text-align: center;
}
.empty-text {
font-size: 15px;
color: var(--text-light);
text-align: center;
margin-bottom: 20px;
}
/* 详情弹窗样式 */
.detail-popup {
padding: 24px;
max-height: 80vh;
overflow-y: auto;
}
.detail-header {
margin-bottom: 20px;
}
.detail-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-color);
}
.detail-meta {
display: flex;
justify-content: space-between;
color: var(--text-light);
font-size: 14px;
}
.detail-section {
margin-bottom: 24px;
}
.detail-label {
color: var(--text-secondary);
margin-bottom: 8px;
font-weight: 500;
font-size: 15px;
}
.detail-content {
color: var(--text-color);
line-height: 1.8;
font-size: 15px;
white-space: pre-wrap;
word-break: break-word;
}
.submitter-info {
background-color: var(--bg-color);
border-radius: 8px;
padding: 12px 15px;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 8px;
line-height: 1.6;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
color: var(--text-secondary);
width: 80px;
font-size: 14px;
}
.info-value {
color: var(--text-color);
flex: 1;
font-size: 14px;
}
.detail-reply {
background-color: #f9f9f9;
padding: 16px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
}
.detail-attachments {
display: flex;
flex-wrap: wrap;
}
.attachment-item {
width: 90px;
height: 90px;
margin-right: 10px;
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.attachment-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.reply-form {
margin-top: 16px;
}
.reply-textarea {
box-sizing: border-box;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
width: 100%;
height: 100px;
font-size: 14px;
background-color: var(--card-bg);
margin-bottom: 16px;
}
.reply-attachments {
margin-bottom: 16px;
}
.reply-actions {
display: flex;
justify-content: space-between;
}
.no-reply {
padding: 20px 0;
text-align: center;
background-color: var(--bg-color);
border-radius: 8px;
}
.no-reply-icon {
font-size: 36px;
color: #ccc;
margin-bottom: 8px;
}
.no-reply-text {
font-size: 14px;
color: var(--text-light);
}
.van-button--primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* 下拉刷新和上拉加载样式 */
.van-pull-refresh__track {
flex: 1;
}
.van-list {
min-height: 100%;
}
/* 统计面板 */
.stats-panel {
background-color: var(--card-bg);
padding: 16px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.stats-item {
flex: 1;
text-align: center;
}
.stats-value {
font-size: 20px;
font-weight: bold;
color: var(--primary-color);
}
.stats-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="意见箱管理" left-arrow @click-left="history.go(-1)" fixed placeholder></van-nav-bar>
<!-- 统计面板 -->
<div class="stats-panel">
<div class="stats-item">
<div class="stats-value">{{ stats.total }}</div>
<div class="stats-label">总意见数</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.pending }}</div>
<div class="stats-label">待回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.replied }}</div>
<div class="stats-label">已回复</div>
</div>
<div class="stats-item">
<div class="stats-value">{{ stats.today }}</div>
<div class="stats-label">今日新增</div>
</div>
</div>
<!-- 筛选区域 -->
<div class="filter-section" :class="filters.isCollapsed ? 'collapsed' : 'expanded'">
<div class="filter-header">
<div class="filter-title">筛选条件</div>
<div class="filter-toggle" @click="toggleFilterCollapse">
<span>{{ filters.isCollapsed ? '展开' : '收起' }}</span>
<van-icon :name="filters.isCollapsed ? 'arrow-down' : 'arrow-up'"
:class="{ rotate: !filters.isCollapsed }"></van-icon>
</div>
</div>
<div class="filter-search">
<van-search v-model="filters.keyword" placeholder="搜索意见标题、内容或提交人"
@search="onSearch"></van-search>
</div>
<div class="filter-content" :class="{ hidden: filters.isCollapsed }">
<div class="filter-row">
<div class="filter-label">状态</div>
<div class="filter-options">
<div class="filter-tag" :class="{ active: filters.status === '' }" @click="setFilter('status', '')">
全部
</div>
<div class="filter-tag" :class="{ active: filters.status === '0' }"
@click="setFilter('status', '0')">
待回复
</div>
<div class="filter-tag" :class="{ active: filters.status === '1' }"
@click="setFilter('status', '1')">
已回复
</div>
</div>
</div>
<div class="filter-row">
<div class="filter-label">时间</div>
<div class="filter-options">
<div class="filter-tag" :class="{ active: filters.time === '' }" @click="setFilter('time', '')">全部
</div>
<div class="filter-tag" :class="{ active: filters.time === 'today' }"
@click="setFilter('time', 'today')">今日
</div>
<div class="filter-tag" :class="{ active: filters.time === 'week' }"
@click="setFilter('time', 'week')">
本周
</div>
<div class="filter-tag" :class="{ active: filters.time === 'month' }"
@click="setFilter('time', 'month')">本月
</div>
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="suggestion-list">
<!-- 下拉刷新和上拉加载更多 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<!-- 空状态 -->
<div class="empty-list" v-if="suggestions.length === 0 && !loading">
<van-icon name="comment-circle-o" class="empty-icon"></van-icon>
<div class="empty-text">暂无符合条件的意见</div>
</div>
<!-- 意见列表 -->
<div class="suggestion-card" v-for="(item, index) in suggestions" :key="item.id"
@click="showDetail(item)">
<div class="suggestion-header">
<div class="suggestion-title">{{ item.title || '意见反馈' }}</div>
<div class="suggestion-status" :class="getStatusClass(item.isReply)">
{{ getStatusText(item.isReply) }}
</div>
</div>
<div class="suggestion-submitter">
<van-icon name="contact"/>
<span>{{ item.submitterName }}</span>
<span class="suggestion-unit">{{ item.submitterUnitName }}</span>
</div>
<div class="suggestion-content">{{ item.content }}</div>
<div class="suggestion-footer">
<div class="suggestion-time">
<van-icon name="clock-o"/>
<span>{{ formatDate(item.submitTime) }}</span>
</div>
<div class="suggestion-action">
{{ item.reply ? '查看详情' : '去回复' }}
<van-icon name="arrow"/>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<!-- 详情弹出层 -->
<van-popup v-model="showDetailPopup" round closeable position="bottom">
<div class="detail-popup" v-if="currentSuggestion">
<div class="detail-header">
<div class="detail-title">{{ currentSuggestion.title || '意见反馈' }}</div>
<div class="detail-meta">
<span>{{ formatDate(currentSuggestion.submitTime) }}</span>
<span :class="getStatusClass(currentSuggestion.isReply)">{{ getStatusText(currentSuggestion.isReply) }}</span>
</div>
</div>
<div class="detail-section">
<div class="detail-label">提交人信息</div>
<div class="submitter-info">
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">{{ currentSuggestion.submitterName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">{{ currentSuggestion.submitterLoginName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">单位:</span>
<span class="info-value">{{ currentSuggestion.submitterUnitName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">联系方式:</span>
<span class="info-value">{{ currentSuggestion.concat || '未填写' }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-label">意见内容</div>
<div class="detail-content">{{ currentSuggestion.content }}</div>
</div>
<div class="detail-section"
v-if="currentSuggestion.attachments && currentSuggestion.attachments.length > 0">
<div class="detail-label">附件</div>
<div class="detail-attachments">
<div class="attachment-item" v-for="(file, idx) in currentSuggestion.attachments" :key="idx"
@click.stop="previewImage(file.url, idx)">
<img :src="file.url" class="attachment-image">
</div>
</div>
</div>
<div class="detail-section" v-if="currentSuggestion.reply">
<div class="detail-label">已回复内容</div>
<div class="detail-reply detail-content">{{ currentSuggestion.reply }}</div>
<div class="detail-meta" style="margin-top: 10px;">
回复时间:{{ formatDate(currentSuggestion.replyTime) }}
</div>
<div v-if="currentSuggestion.replyAttachments && currentSuggestion.replyAttachments.length > 0"
style="margin-top: 12px;">
<div class="detail-label">回复附件</div>
<div class="detail-attachments">
<div class="attachment-item" v-for="(file, idx) in currentSuggestion.replyAttachments"
:key="idx" @click.stop="previewImage(file.url, idx, 'reply')">
<img :src="file.url" class="attachment-image">
</div>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-label">{{ currentSuggestion.isReply ? '修改回复' : '回复意见' }}</div>
<div class="reply-form">
<textarea class="reply-textarea" v-model="currentSuggestion.replyContent"
placeholder="请输入回复内容..."></textarea>
<!-- <div class="reply-attachments">-->
<!-- <vant-file-upload :files.sync="formData.replyAttachments" :max="15"></vant-file-upload>-->
<!-- </div>-->
<div class="reply-actions">
<van-button style="border-radius: 10px" block type="info" :color="themeColor"
@click="submitReply">提交回复
</van-button>
</div>
</div>
</div>
</div>
</van-popup>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
refreshing: false,
loading: false,
finished: false,
showDetailPopup: false,
currentSuggestion: null,
pageNumber: 1,
pageSize: 10,
suggestions: [],
replyContent: '',
replyAttachments: [],
filters: {
status: '', // 状态筛选
time: '', // 时间筛选
keyword: '', // 关键词搜索
isCollapsed: true // 筛选区域是否折叠
},
stats: {
total: 0,
pending: 0,
replied: 0,
today: 0
},
formData: {}
}
},
created() {
this.loadStats();
this.loadData();
},
methods: {
// 加载统计数据
loadStats() {
$.post('/platform/suggestionBox/admin/getStats').done((res) => {
if (res.code === 0 && res.data) {
this.stats = {
total: res.data.total || 0,
pending: res.data.pending || 0,
replied: res.data.replied || 0,
today: res.data.today || 0
};
}
}).fail(() => {
this.$toast.fail('统计数据加载失败');
});
},
// 加载意见数据
loadData() {
this.loading = true;
$.post('/platform/suggestionBox/admin/pageData', {
pageNumber: this.pageNumber,
pageSize: this.pageSize,
status: this.filters.status,
timeRange: this.filters.time,
keyword: this.filters.keyword
}).done((res) => {
if (res.code === 0 && res.data) {
if (this.pageNumber === 1) {
this.suggestions = res.data.list || [];
} else {
this.suggestions = this.suggestions.concat(res.data.list || []);
}
this.finished = !res.data.list || res.data.list.length < this.pageSize;
} else {
this.finished = true;
}
this.loading = false;
this.refreshing = false;
}).fail(() => {
this.loading = false;
this.refreshing = false;
this.finished = true;
});
},
// 下拉刷新
onRefresh() {
this.pageNumber = 1;
this.finished = false;
this.loadStats();
this.loadData();
},
// 上拉加载更多
loadMore() {
this.pageNumber++;
this.loadData();
},
// 设置筛选条件
setFilter(type, value) {
this.filters[type] = value;
this.pageNumber = 1;
this.finished = false;
this.loadData();
},
// 搜索
onSearch() {
this.pageNumber = 1;
this.finished = false;
this.loadData();
},
// 查看详情
showDetail(suggestion) {
if (suggestion.attachments && typeof suggestion.attachments === 'string') {
try{
suggestion.attachments = JSON.parse(suggestion.attachments);
}catch (e){
suggestion.attachments = [];
}
}
console.log(suggestion)
this.currentSuggestion = suggestion;
this.showDetailPopup = true;
},
// 提交回复
submitReply() {
if (!this.currentSuggestion.replyContent || !this.currentSuggestion.replyContent.trim()) {
this.$toast('请输入回复内容');
return;
}
this.$dialog.confirm({
title: '确认提交',
message: '确定提交此回复内容吗?'
}).then(() => {
// 提交回复
$.post('/platform/suggestionBox/admin/reply', {
reply: JSON.stringify(this.currentSuggestion)
}).done((res) => {
if (res.code === 0) {
this.$toast.success('回复成功');
this.showDetailPopup = false;
this.loadStats();
this.pageData();
} else {
this.$toast.fail(res.msg || '回复失败');
}
}).fail(() => {
this.$toast.fail('网络错误,请重试');
});
});
},
// 获取状态class
getStatusClass(isReply) {
if (!isReply || isReply === 0) return 'status-pending';
if (isReply === 1) return 'status-processing';
return 'status-completed';
},
// 获取状态文本
getStatusText(isReply) {
if (!isReply || isReply === 0) return '待回复';
if (isReply === 1) return '已回复';
return '已处理';
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0');
},
// 预览图片
previewImage(url, index, type = 'submission') {
if (!url) return;
// 创建图片查看器
const urls = type === 'reply'
? this.currentSuggestion.replyAttachments.map(file => file.url)
: this.currentSuggestion.attachments.map(file => file.url);
this.$imagePreview({
images: urls,
startPosition: index
});
},
// 切换筛选区域折叠状态
toggleFilterCollapse() {
this.filters.isCollapsed = !this.filters.isCollapsed;
}
}
});
</script>
<!--#}#-->
@@ -0,0 +1,538 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="意见箱" fixed placeholder></van-nav-bar>
<!-- 顶部Banner区域 -->
<div class="banner-section">
<div class="banner-content">
<div class="banner-text">
<h2>我们重视您的意见</h2>
<p>每一条建议都将认真对待</p>
</div>
<div class="banner-image">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48cGF0aCBkPSJNMTI1LjYgMTAyLjdsLTE5LTE2LjhtLTQuNy0yMy43bDExLjUtNi44TTU3LjQgMTZsMTEuNSAxMS43TTMxLjIgNTMuOGwyMy42IDcuOSIgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6I2ZmZjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtvcGFjaXR5Oi40Ii8+PHBhdGggZD0iTTY0LjEgMzEuN0w0NyA0OS45Yy0zIDMuMi0zLjEgOC4xLS4xIDExLjJsMjQuNCAyNWMzIDMuMSA3LjkgMy4yIDExIC4xTDk5IDY5YzMtMy4yIDMuMS04LjEuMS0xMS4yTDc0LjcgMzIuOGMtMyAzLjEtNy45IDMuMS0xMC42LTEuMXoiIHN0eWxlPSJmaWxsOiNmZmY7c3Ryb2tlOiNmZmY7c3Ryb2tlLW1pdGVybGltaXQ6MTAiLz48cGF0aCBkPSJNMTA4LjkgOTUuN2wtMTUuOC0xMi0xMi4yIDEzIDE0LjQgMTMuN2MuMyAyLjUgMi4zIDQuNSA0LjggNC41aDEzLjdjMi43IDAgNC45LTIuMiA0LjktNC45VjkzLjVjMC0yLjgtMi4xLTUtNC45LTV2OC40cy4xLTEuMi00LjkgMi44LjEtMyAuMS0zeiIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6I2ZmZjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMCIvPjwvc3ZnPg==" alt="Feedback">
</div>
</div>
</div>
<!-- 功能卡片区域 -->
<div class="cards-container">
<!-- 提交意见 -->
<div class="feature-card" @click="goToSubmitPage">
<div class="card-icon submit-icon">
<van-icon name="edit" />
</div>
<div class="card-info">
<h3>提交意见</h3>
<p>分享您的想法和建议</p>
</div>
<div class="card-arrow">
<van-icon name="arrow" />
</div>
</div>
<!-- 我的意见 -->
<div class="feature-card" @click="goToMyOpinionsPage">
<div class="card-icon my-icon">
<van-icon name="records" />
</div>
<div class="card-info">
<h3>我的意见</h3>
<p>查看您提交的所有意见</p>
</div>
<div class="card-arrow">
<van-icon name="arrow" />
</div>
</div>
<!-- 意见管理(管理员) -->
<div class="feature-card" v-if="isAdmin" @click="goToAllOpinionsPage">
<div class="card-icon admin-icon">
<van-icon name="manager" />
</div>
<div class="card-info">
<h3>意见管理</h3>
<p>管理所有用户提交的意见</p>
</div>
<div class="card-arrow">
<van-icon name="arrow" />
</div>
</div>
</div>
<!-- 使用指南 -->
<div class="guide-container" v-if="!isAdmin">
<div class="guide-header">
<h3>使用指南</h3>
</div>
<div class="guide-steps">
<div class="guide-step">
<div class="step-number">1</div>
<div class="step-content">
<h4>提交意见</h4>
<p>点击"提交意见"按钮,填写您的意见和建议</p>
</div>
<div class="step-icon">
<van-icon name="edit" />
</div>
</div>
<div class="step-divider"></div>
<div class="guide-step">
<div class="step-number">2</div>
<div class="step-content">
<h4>等待处理</h4>
<p>我们将在3个工作日内处理您的意见</p>
</div>
<div class="step-icon">
<van-icon name="underway-o" />
</div>
</div>
<div class="step-divider"></div>
<div class="guide-step">
<div class="step-number">3</div>
<div class="step-content">
<h4>查看回复</h4>
<p>在"我的意见"中查看官方回复</p>
</div>
<div class="step-icon">
<van-icon name="comment-o" />
</div>
</div>
</div>
</div>
<!-- 常见问题 -->
<div class="faq-container" v-if="!isAdmin">
<div class="faq-header">
<h3>常见问题</h3>
<span class="faq-subtitle">解答您的疑惑</span>
</div>
<div class="faq-list">
<div class="faq-item" :class="{'faq-active': activeFaq === 1}" @click="toggleFaq(1)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o" /></span>
<span>如何提交带附件的意见?</span>
<span class="faq-arrow"><van-icon name="arrow-down" /></span>
</div>
<div class="faq-answer" v-show="activeFaq === 1">
<p>在提交意见表单中,您可以上传最多3个文件作为附件,支持图片格式。点击附件上传区域,选择您要上传的文件即可。</p>
</div>
</div>
<div class="faq-item" :class="{'faq-active': activeFaq === 2}" @click="toggleFaq(2)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o" /></span>
<span>意见提交后多久能收到回复?</span>
<span class="faq-arrow"><van-icon name="arrow-down" /></span>
</div>
<div class="faq-answer" v-show="activeFaq === 2">
<p>我们会在3个工作日内处理您的意见,紧急问题会优先处理。您可以随时在"我的意见"中查看处理进度。</p>
</div>
</div>
<div class="faq-item" :class="{'faq-active': activeFaq === 3}" @click="toggleFaq(3)">
<div class="faq-question">
<span class="faq-icon"><van-icon name="question-o" /></span>
<span>我可以修改已提交的意见吗?</span>
<span class="faq-arrow"><van-icon name="arrow-down" /></span>
</div>
<div class="faq-answer" v-show="activeFaq === 3">
<p>提交后的意见暂不支持修改,如有补充,请重新提交并说明这是对之前意见的补充。我们会将相关意见关联处理。</p>
</div>
</div>
</div>
</div>
</div>
<style>
:root {
--primary-color: rgb(0, 78, 100);
--primary-light: rgba(0, 78, 100, 0.1);
--secondary-color: #37A6BD;
--text-color: #333333;
--text-secondary: #666666;
--text-light: #999999;
--bg-color: #f6f6f6;
--card-bg: #ffffff;
--border-color: #eeeeee;
--success-color: #07c160;
--warning-color: #ff976a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.5;
}
.page-container {
padding-bottom: 50px;
}
/* 顶部Banner */
.banner-section {
padding: 0;
height: 180px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
position: relative;
overflow: visible;
}
.banner-curve {
display: none;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 20px 0;
height: 100%;
}
.banner-text {
color: white;
z-index: 2;
}
.banner-text h2 {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.banner-text p {
font-size: 14px;
opacity: 0.9;
}
.banner-image {
width: 100px;
height: 100px;
z-index: 2;
}
.banner-image img {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 功能卡片 */
.cards-container {
padding: 20px 16px 16px;
margin-top: -20px;
position: relative;
z-index: 10;
background-color: var(--bg-color);
border-radius: 20px 20px 0 0;
}
.feature-card {
display: flex;
align-items: center;
background: var(--card-bg);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.feature-card:active {
transform: scale(0.98);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 16px;
}
.card-icon .van-icon {
font-size: 24px;
color: white;
}
.submit-icon {
background: linear-gradient(135deg, #1989fa 0%, #39b9f9 100%);
}
.my-icon {
background: linear-gradient(135deg, #07c160 0%, #10d878 100%);
}
.admin-icon {
background: linear-gradient(135deg, #ff6b6b 0%, #ffaa7f 100%);
}
.card-info {
flex: 1;
}
.card-info h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-color);
}
.card-info p {
font-size: 13px;
color: var(--text-light);
margin: 0;
}
.card-arrow {
color: #ccc;
}
/* 使用指南 */
.guide-container {
padding: 0 16px 16px;
}
.guide-header {
margin-bottom: 16px;
}
.guide-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--text-color);
}
.guide-steps {
background: var(--card-bg);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.guide-step {
display: flex;
align-items: center;
position: relative;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-content h4 {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text-color);
}
.step-content p {
font-size: 13px;
color: var(--text-secondary);
margin: 0;
}
.step-icon {
margin-left: 12px;
color: var(--primary-color);
}
.step-divider {
height: 24px;
width: 1px;
background: #e8e8e8;
margin: 8px 0 8px 15px;
}
/* 常见问题 */
.faq-container {
padding: 0 16px 16px;
}
.faq-header {
margin-bottom: 16px;
display: flex;
align-items: baseline;
}
.faq-header h3 {
font-size: 18px;
font-weight: 600;
color: var(--text-color);
margin-right: 8px;
}
.faq-subtitle {
font-size: 12px;
color: var(--text-light);
}
.faq-list {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.faq-item {
border-bottom: 1px solid var(--border-color);
}
.faq-item:last-child {
border-bottom: none;
}
.faq-question {
display: flex;
align-items: center;
padding: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.faq-active .faq-question {
background-color: var(--primary-light);
}
.faq-icon {
color: var(--primary-color);
margin-right: 12px;
}
.faq-arrow {
margin-left: auto;
color: var(--text-light);
transition: transform 0.3s ease;
}
.faq-active .faq-arrow .van-icon {
transform: rotate(180deg);
}
.faq-answer {
padding: 0 16px 16px 44px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
border-top: 1px dashed var(--border-color);
background-color: rgba(0, 0, 0, 0.02);
}
/* 弹窗样式 */
.popup-title {
text-align: center;
font-size: 16px;
font-weight: 500;
padding: 16px 0;
border-bottom: 1px solid #ebedf0;
}
.popup-content {
padding: 16px;
max-height: calc(100% - 60px);
overflow-y: auto;
}
.suggestion-content {
font-size: 14px;
line-height: 1.5;
}
.suggestion-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-color);
}
.suggestion-meta {
display: flex;
justify-content: space-between;
color: var(--text-light);
}
.attachment-list {
display: flex;
flex-wrap: wrap;
}
</style>
<script>
new Vue({
el: '#app',
data: function() {
return {
isAdmin: true, // 控制是否为管理员
allSuggestions: [],
user: 'user1',
activeFaq: null,
totalSuggestions: 0,
respondedPercent: 0,
recentSuggestions: []
}
},
computed: {
userSuggestions: function() {
return this.allSuggestions.filter(function(item) {
return item.user === this.user;
}.bind(this));
}
},
methods: {
goToSubmitPage: function() {
window.location.href = '/platform/suggestionBox/h5/write';
},
goToMyOpinionsPage: function() {
window.location.href = '/platform/suggestionBox/h5/mine';
},
goToAllOpinionsPage: function() {
window.location.href = '/platform/suggestionBox/admin/h5';
},
toggleFaq: function(id) {
this.activeFaq = this.activeFaq === id ? null : id;
},
loadStats: function() {
// 模拟加载统计数据
this.totalSuggestions = 25;
this.respondedPercent = 80;
this.recentSuggestions = [1, 2, 3];
}
},
created: function() {
// 初始化数据
if (this.isAdmin) {
this.loadStats();
}
}
});
</script>
<!--#
}
#-->
@@ -0,0 +1,542 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
:root {
--primary-color: rgb(0, 78, 100);
--primary-light: rgba(0, 78, 100, 0.1);
--secondary-color: #37a6bd;
--text-color: #333333;
--text-secondary: #666666;
--text-light: #999999;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--border-color: #eeeeee;
--success-color: #07c160;
--warning-color: #ff976a;
}
#app {
min-height: 100vh;
background-color: var(--bg-color);
display: flex;
flex-direction: column;
}
/* 页面样式 */
.suggestion-list {
padding: 16px;
background-color: var(--bg-color);
flex: 1;
display: flex;
flex-direction: column;
}
.van-pull-refresh,
.van-list {
flex: 1;
display: flex;
flex-direction: column;
}
.suggestion-card {
background-color: var(--card-bg);
border-radius: 12px;
margin-bottom: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
border: 1px solid rgba(0, 0, 0, 0.02);
}
.suggestion-card:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.suggestion-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.suggestion-title {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.suggestion-status {
font-size: 12px;
padding: 3px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: 500;
}
.status-pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-processing {
background-color: #fff7e6;
color: #fa8c16;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
}
.suggestion-content {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
overflow: hidden;
background-color: var(--bg-color);
padding: 10px;
border-radius: 8px;
word-break: break-all;
white-space: pre-line;
max-height: 3.2em;
text-overflow: ellipsis;
display: block;
}
.suggestion-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--text-light);
border-top: 1px solid #f5f5f5;
padding-top: 12px;
}
.suggestion-time {
display: flex;
align-items: center;
}
.suggestion-time .van-icon {
font-size: 14px;
margin-right: 4px;
}
.suggestion-action {
color: var(--primary-color);
display: flex;
align-items: center;
font-weight: 500;
}
.suggestion-action .van-icon {
font-size: 14px;
margin-left: 2px;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 16px;
flex: 1;
}
.empty-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 16px;
}
.empty-text {
font-size: 15px;
color: var(--text-light);
text-align: center;
margin-bottom: 20px;
}
/* 详情弹窗样式 */
.detail-popup {
padding: 24px;
max-height: 80vh;
overflow-y: auto;
}
.detail-header {
margin-bottom: 20px;
}
.detail-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-color);
}
.detail-meta {
display: flex;
justify-content: space-between;
color: var(--text-light);
font-size: 14px;
}
.detail-section {
margin-bottom: 24px;
}
.detail-label {
color: var(--text-secondary);
margin-bottom: 8px;
font-weight: 500;
font-size: 15px;
}
.detail-content {
color: var(--text-color);
line-height: 1.8;
font-size: 15px;
}
.detail-reply {
background-color: #f9f9f9;
padding: 16px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
}
.detail-attachments {
display: flex;
flex-wrap: wrap;
}
.attachment-item {
width: 90px;
height: 90px;
margin-right: 10px;
margin-bottom: 10px;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.attachment-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.loader {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.van-button--primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* 下拉刷新和上拉加载样式 */
.van-pull-refresh__track {
flex: 1;
}
.van-list {
min-height: 100%;
}
.no-reply {
padding: 20px 0;
text-align: center;
background-color: var(--bg-color);
border-radius: 8px;
}
.no-reply-icon {
font-size: 36px;
color: #ccc;
margin-bottom: 8px;
}
.no-reply-text {
font-size: 14px;
color: var(--text-light);
}
.submitter-info {
background-color: var(--bg-color);
border-radius: 8px;
padding: 12px 15px;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 8px;
line-height: 1.6;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
color: var(--text-secondary);
width: 80px;
font-size: 14px;
}
.info-value {
color: var(--text-color);
flex: 1;
font-size: 14px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="我的意见建议" left-arrow @click-left="history.go(-1)" fixed placeholder></van-nav-bar>
<!-- 内容区域 -->
<div class="suggestion-list">
<!-- 下拉刷新和上拉加载更多 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="loadMore">
<!-- 空状态 -->
<div class="empty-list" v-if="suggestions.length === 0 && !loading">
<van-icon name="comment-circle-o" class="empty-icon" />
<div class="empty-text">您还没有提交过意见</div>
<van-button type="primary" size="normal" round @click="goToSubmitPage">去提交意见</van-button>
</div>
<!-- 意见列表 -->
<div class="suggestion-card" v-for="(item, index) in suggestions" :key="item.id" @click="showDetail(item)">
<div class="suggestion-header">
<div class="suggestion-title">{{ item.title || '意见反馈' }}</div>
<div class="suggestion-status" :class="getStatusClass(item.isReply)">{{ item.node_name }}</div>
</div>
<div class="suggestion-content">{{ item.content }}</div>
<div class="suggestion-footer">
<div class="suggestion-time">
<van-icon name="clock-o" />
<span>{{ formatDate(item.submitTime) }}</span>
</div>
<div style="display: flex; column-gap: 10px">
<div class="suggestion-action">
查看详情
<van-icon name="arrow" />
</div>
<div class="suggestion-action" v-if="item.flow_status==='1'" @click.stop="onRevoke(item)">
撤销
<van-icon name="arrow" />
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
<!-- 详情弹出层 -->
<van-popup v-model="showDetailPopup" round closeable position="bottom" :style="{ height: '80%' }">
<div class="detail-popup" v-if="currentSuggestion">
<div class="detail-header">
<div class="detail-title">{{ currentSuggestion.title || '意见反馈' }}</div>
<div class="detail-meta">
<span>{{ formatDate(currentSuggestion.submitTime) }}</span>
<span :class="getStatusClass(currentSuggestion.isReply)">{{ getStatusText(currentSuggestion.isReply) }}</span>
</div>
</div>
<div class="detail-section">
<div class="detail-label">提交人信息</div>
<div class="submitter-info">
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">{{ currentSuggestion.submitterName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">{{ currentSuggestion.submitterLoginName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">单位:</span>
<span class="info-value">{{ currentSuggestion.submitterUnitName || '未填写' }}</span>
</div>
<div class="info-item">
<span class="info-label">联系方式:</span>
<span class="info-value">{{ currentSuggestion.concat || '未填写' }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-label">意见内容</div>
<div class="detail-content">{{ currentSuggestion.content }}</div>
</div>
<div class="detail-section" v-if="currentSuggestion.attachments && currentSuggestion.attachments.length > 0">
<div class="detail-label">附件</div>
<div class="detail-attachments">
<div
class="attachment-item"
v-for="(file, idx) in currentSuggestion.attachments"
:key="idx"
@click.stop="previewImage(file.url, idx)"
>
<img :src="file.url" class="attachment-image" />
</div>
</div>
</div>
<div class="detail-section" v-if="currentSuggestion.isReply">
<div class="detail-label">回复</div>
<div class="detail-reply detail-content">{{ currentSuggestion.replyContent }}</div>
</div>
<div v-else class="detail-section">
<div class="detail-label">回复</div>
<div class="no-reply">
<van-icon name="chat-o" class="no-reply-icon" />
<div class="no-reply-text">暂无回复,请耐心等待</div>
</div>
</div>
</div>
</van-popup>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
refreshing: false,
loading: false,
finished: false,
showDetailPopup: false,
currentSuggestion: null,
pageNumber: 1,
pageSize: 10,
suggestions: []
}
},
created() {
this.loadData()
},
methods: {
loadData() {
this.loading = true
$.post("/platform/suggestionBox/pageData", {
pageNumber: this.pageNumber,
pageSize: this.pageSize
})
.done((res) => {
if (res.code === 0 && res.data) {
if (this.pageNumber === 1) {
this.suggestions = res.data.list || []
} else {
this.suggestions = this.suggestions.concat(res.data.list || [])
}
this.finished = !res.data.list || res.data.list.length < this.pageSize
} else {
this.finished = true
}
this.loading = false
this.refreshing = false
})
.fail(() => {
this.loading = false
this.refreshing = false
this.finished = true
})
},
// 下拉刷新
onRefresh() {
this.pageNumber = 1
this.finished = false
this.loadData()
},
// 上拉加载更多
loadMore() {
this.pageNumber++
this.loadData()
},
// 查看详情
showDetail(suggestion) {
if (suggestion.attachments && typeof suggestion.attachments === "string") {
try {
suggestion.attachments = JSON.parse(suggestion.attachments)
} catch (e) {
suggestion.attachments = []
}
}
this.currentSuggestion = suggestion
this.showDetailPopup = true
},
// 获取状态class
getStatusClass(isReply) {
if (!isReply || isReply === 0) return "status-pending"
if (isReply === 1) return "status-processing"
return "status-completed"
},
// 获取状态文本
getStatusText(isReply) {
if (!isReply || isReply === 0) return "待处理"
if (isReply === 1) return "处理中"
return "已处理"
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return ""
const date = new Date(dateStr)
return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, "0") + "-" + String(date.getDate()).padStart(2, "0")
},
// 前往提交意见页面
goToSubmitPage() {
window.location.href = "/platform/suggestionBox/h5/write"
},
// 预览图片
previewImage(url, index) {
if (!url) return
// 创建图片查看器
const urls = this.currentSuggestion.attachments.map((file) => file.url)
this.$imagePreview({
images: urls,
startPosition: index
})
},
// 撤销
onRevoke(item) {
this.$dialog
.confirm({
title: "提示",
message: "您确认要撤销吗?"
})
.then(() => {})
}
}
})
</script>
<!--#}#-->
@@ -0,0 +1,363 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.task-form-container {
padding: 16px;
background-color: #f5f7fa;
min-height: 100vh;
}
.card {
background-color: #fff;
border-radius: 12px;
margin-bottom: 16px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.card-title::before {
content: "";
display: inline-block;
width: 4px;
height: 16px;
background-color: rgb(0, 78, 100);
margin-right: 8px;
border-radius: 2px;
}
.info-item {
display: flex;
margin-bottom: 12px;
line-height: 1.6;
}
.info-label {
color: #666;
width: 80px;
font-size: 14px;
}
.info-value {
color: #333;
flex: 1;
font-size: 14px;
}
.content-box {
background-color: #f9f9f9;
padding: 12px;
border-radius: 8px;
margin-top: 8px;
white-space: pre-line;
word-break: break-all;
}
.status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 8px;
}
.status-pending {
background-color: #e6f7ff;
color: #1890ff;
}
.status-processing {
background-color: #fff7e6;
color: #fa8c16;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
}
.timeline {
padding: 8px 0;
}
.reply-form {
margin-top: 16px;
}
.reply-textarea {
width: 100%;
height: 120px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 14px;
}
.btn-container {
display: flex;
justify-content: center;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="意见箱处理" left-arrow @click-left="goBack" fixed placeholder></van-nav-bar>
<div class="task-form-container">
<!-- 加载状态 -->
<van-empty v-if="loading" description="加载中...">
<template #image>
<van-loading type="spinner" size="36" />
</template>
</van-empty>
<template v-else>
<!-- 业务表单数据 -->
<div class="card">
<div class="card-title">基本信息</div>
<div class="info-item">
<span class="info-label">提交人:</span>
<span class="info-value">{{ suggestionData.submitterName }}</span>
</div>
<div class="info-item">
<span class="info-label">工号:</span>
<span class="info-value">{{ suggestionData.submitterLoginName }}</span>
</div>
<div class="info-item">
<span class="info-label">单位:</span>
<span class="info-value">{{ suggestionData.submitterUnitName }}</span>
</div>
<div class="info-item">
<span class="info-label">联系方式:</span>
<span class="info-value">{{ suggestionData.concat }}</span>
</div>
<div class="info-item">
<span class="info-label">提交时间:</span>
<span class="info-value">{{ formatDate(suggestionData.submitTime) }}</span>
</div>
<div class="info-item">
<span class="info-label">标题:</span>
<span class="info-value">{{ suggestionData.title || '无标题' }}</span>
</div>
<div class="info-item">
<span class="info-label">内容:</span>
</div>
<div class="content-box">{{ suggestionData.content }}</div>
</div>
<!-- 流程信息 -->
<div class="card">
<div class="card-title">
流程信息
<span class="status-tag" :class="getStatusClass()">{{ getStatusText() }}</span>
</div>
<div class="timeline">
<van-steps direction="vertical" :active="flowHistory.length - 1">
<van-step v-for="(item, index) in flowHistory" :key="index">
<h3>{{ item.taskName }}</h3>
<p>处理人: {{ item.assigneeName || '系统' }}</p>
<p>开始时间: {{ formatDate(item.startTime) }}</p>
<p v-if="item.endTime">完成时间: {{ formatDate(item.endTime) }}</p>
<p v-if="item.comment">处理意见: {{ item.comment }}</p>
</van-step>
</van-steps>
</div>
</div>
<!-- 回复表单 -->
<div class="card" v-if="!taskCompleted && currentTask">
<div class="card-title">处理意见</div>
<div class="reply-form">
<textarea v-model="replyForm.replyContent" class="reply-textarea"
placeholder="请输入回复内容"></textarea>
<div class="btn-container">
<van-button type="primary" block round @click="submitReply">提交回复</van-button>
</div>
</div>
</div>
<!-- 已回复信息 -->
<div class="card" v-if="suggestionData.isReply">
<div class="card-title">回复信息</div>
<div class="info-item">
<span class="info-label">回复人:</span>
<span class="info-value">{{ suggestionData.replyUserName }}</span>
</div>
<div class="info-item">
<span class="info-label">回复时间:</span>
<span class="info-value">{{ formatDate(suggestionData.replyTime) }}</span>
</div>
<div class="info-item">
<span class="info-label">回复内容:</span>
</div>
<div class="content-box">{{ suggestionData.replyContent }}</div>
</div>
</template>
</div>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
loading: true,
taskId: "",
businessKey: "",
processInstanceId: "",
suggestionData: {},
flowHistory: [],
currentTask: null,
taskCompleted: false,
replyForm: {
id: "",
replyContent: ""
}
}
},
created() {
// 获取URL参数
this.taskId = GetQueryString("taskId")
this.businessKey = GetQueryString("businessKey")
this.processInstanceId = GetQueryString("processInstanceId")
// 加载数据
this.loadData()
},
methods: {
goBack() {
history.back()
},
// 加载数据
async loadData() {
this.loading = true
try {
// 1. 获取业务数据
await this.loadSuggestionData()
// 2. 获取流程历史
await this.loadFlowHistory()
// 3. 获取当前任务信息
if (this.taskId) {
await this.loadTaskInfo()
}
} catch (error) {
console.error("加载数据失败:", error)
this.$toast.fail("加载数据失败")
} finally {
this.loading = false
}
},
// 加载意见箱数据
async loadSuggestionData() {
if (!this.businessKey) return
const {
code,
data
} = await $.post("/platform/suggestionBox/admin/getSuggestionById", { id: this.businessKey })
if (code === 0 && data) {
this.suggestionData = data
this.replyForm.id = data.id
}
},
// 加载流程历史
async loadFlowHistory() {
if (!this.processInstanceId) return
const {
code,
data
} = await $.post("/platform/workflow/task/getProcessHistory", { processInstanceId: this.processInstanceId })
if (code === 0 && data) {
this.flowHistory = data
}
},
// 加载任务信息
async loadTaskInfo() {
const { code, data } = await $.post("/platform/workflow/task/getTaskInfo", { taskId: this.taskId })
if (code === 0 && data) {
this.currentTask = data
this.taskCompleted = data.endTime != null
}
},
// 提交回复
async submitReply() {
if (!this.replyForm.replyContent.trim()) {
this.$toast("请输入回复内容")
return
}
try {
// 保存业务数据并完成任务
const result = await $.post("/platform/suggestionBox/admin/reply", {
reply: JSON.stringify(this.replyForm),
taskId: this.taskId
})
if (result.code === 0) {
this.$toast.success("处理成功")
// 重新加载数据
setTimeout(() => {
this.loadData()
}, 1000)
} else {
this.$toast.fail(result.msg || "处理失败")
}
} catch (error) {
console.error("提交回复失败:", error)
this.$toast.fail("提交回复失败")
}
},
// 获取状态样式
getStatusClass() {
if (this.flowHistory.length === 0) return "status-pending"
const lastTask = this.flowHistory[this.flowHistory.length - 1]
if (!lastTask.endTime) return "status-processing"
return "status-completed"
},
// 获取状态文本
getStatusText() {
if (this.flowHistory.length === 0) return "未开始"
const lastTask = this.flowHistory[this.flowHistory.length - 1]
if (!lastTask.endTime) return "处理中"
return "已完成"
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return ""
const date = new Date(dateStr)
return date.getFullYear() + "-" +
String(date.getMonth() + 1).padStart(2, "0") + "-" +
String(date.getDate()).padStart(2, "0") + " " +
String(date.getHours()).padStart(2, "0") + ":" +
String(date.getMinutes()).padStart(2, "0")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,294 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.form-container {
background-color: #f6f6f6;
padding: 0;
}
.form-section {
margin-bottom: 12px;
}
.section-title {
display: flex;
align-items: center;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #f6f6f6;
}
.dot {
width: 8px;
height: 8px;
background-color: rgb(0, 78, 100);
border-radius: 50%;
margin-right: 8px;
}
.section-title span {
color: #333;
font-weight: 500;
}
.input-row {
display: flex;
align-items: center;
border-bottom: 1px solid #f5f5f5;
padding: 12px 16px;
background: #fff;
}
.input-label {
width: 80px;
color: #333;
padding: 8px 8px 8px 0;
}
.input-control {
flex: 1;
text-align: right;
}
.input-control input {
width: 100%;
border: none;
outline: none;
text-align: right;
color: #666;
font-size: 14px;
}
.textarea-container {
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid #f5f5f5;
}
.textarea-container textarea {
width: 100%;
height: 120px;
border: none;
outline: none;
resize: none;
font-size: 14px;
color: #333;
}
.word-count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 4px;
}
.upload-area {
padding: 16px;
background: #fff;
}
.upload-grid {
display: flex;
flex-wrap: wrap;
}
.upload-item,
.upload-btn {
width: 80px;
height: 80px;
margin-right: 8px;
margin-bottom: 8px;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.upload-btn {
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.delete-btn {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 0 0 4px;
}
.submit-area {
padding: 20px 16px;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar title="我要提意见" left-arrow left-text="返回" @click-left="history.go(-1)"></van-nav-bar>
</van-sticky>
<div class="form-container">
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>填写人信息</span>
</div>
<div class="input-row">
<div class="input-label">姓名</div>
<div class="input-control">
<input type="text" v-model="formData.submitterName" placeholder="请输入姓名" readonly />
</div>
</div>
<div class="input-row">
<div class="input-label">工号</div>
<div class="input-control">
<input type="text" v-model="formData.submitterLoginName" placeholder="请输入工号" readonly />
</div>
</div>
<div class="input-row">
<div class="input-label">手机号码</div>
<div class="input-control">
<input type="tel" v-model="formData.concat" placeholder="请输入手机号码" />
</div>
</div>
<div class="input-row">
<div class="input-label">所在单位</div>
<div class="input-control">
<input type="text" v-model="formData.submitterUnitName" placeholder="请输入所在单位" readonly />
</div>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>填写标题</span>
</div>
<div class="input-row">
<div class="input-label">标题</div>
<div class="input-control">
<input type="text" v-model="formData.title" placeholder="请输入意见标题" />
</div>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>填写意见建议内容</span>
</div>
<div class="textarea-container">
<textarea v-model="formData.content" placeholder="请描述您要填写的意见建议内容..."></textarea>
</div>
</div>
<div class="form-section">
<div class="section-title">
<div class="dot"></div>
<span>照片上传</span>
</div>
<div class="upload-area">
<!-- <vant-file-upload :files.sync="formData.attachments" :max="15"></vant-file-upload>-->
<!-- <div class="upload-grid">-->
<!-- <div class="upload-item" v-for="(item, index) in formData.fileList" :key="index">-->
<!-- <img :src="item.content || item.url" class="uploaded-image">-->
<!-- <div class="delete-btn" @click="deleteImage(index)">-->
<!-- <van-icon name="cross" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="upload-btn" v-if="formData.fileList.length < 3" @click="triggerUpload">-->
<!-- <input type="file" ref="fileInput" style="display:none" accept="image/*" @change="onFileChange" multiple>-->
<!-- <van-icon name="plus" size="24" />-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
<div class="submit-area">
<van-button @click="submitForm" style="border-radius: 10px" block type="info">提 交</van-button>
</div>
</div>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
formData: {
name: "",
mobile: "",
idCard: "",
department: "",
content: "",
address: "",
fileList: []
}
}
},
methods: {
goBack() {
history.back()
},
async submitForm() {
if (!this.formData.concat.trim()) {
this.$toast("请输入手机号码")
return
}
//正则验证
if (!/^1[3-9]\d{9}$/.test(this.formData.concat)) {
this.$toast("请输入正确的手机号码")
return
}
if (!this.formData.content.trim()) {
this.$toast("请输入投诉内容")
return
}
this.$dialog
.confirm({
title: "提示",
message: "您确定要提交吗?"
})
.then(async () => {
const { code, data, msg } = await $.post("/platform/suggestionBox/submit", { suggestion: JSON.stringify(this.formData) })
if (code === 0) {
this.$toast.success("提交成功")
setTimeout(() => {
// history.back()
}, 200)
} else {
this.$toast(msg)
}
})
}
},
created() {
const id = GetQueryString("id")
if (!id) {
this.$set(this.formData, "submitterId", "${@auth.getPrincipalProperty('id')}")
this.$set(this.formData, "submitterName", "${@auth.getPrincipalProperty('username')}")
this.$set(this.formData, "submitterLoginName", "${@auth.getPrincipalProperty('loginname')}")
this.$set(this.formData, "submitterUnitId", "${@auth.getPrincipalProperty('unitid')}")
this.$set(this.formData, "submitterUnitName", "${@auth.getPrincipalProperty('unit').getName()}")
this.$set(this.formData, "submitterUnionId", "${@auth.getPrincipalProperty('union').getId()}")
this.$set(this.formData, "submitterUnionName", "${@auth.getPrincipalProperty('union').getName()}")
this.$set(this.formData, "concat", "${@auth.getPrincipalProperty('mobile')}")
console.log(this.formData)
}
}
})
</script>
<!--#}#-->