first commit

This commit is contained in:
2026-09-09 17:47:12 +08:00
commit 0cec79cd76
5672 changed files with 1015928 additions and 0 deletions
@@ -0,0 +1,11 @@
<el-row class="el-pagination-container" style="margin-top: 20px">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[5,10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
@@ -0,0 +1,548 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.sub-app-container {
margin: 0 auto;
background: #fff;
display: flex;
overflow: hidden;
min-height: calc(100vh - 64px);
box-sizing: border-box;
}
.sidebar-menu {
width: 250px;
background: rgb(35 85 120);
color: #fff;
flex-shrink: 0;
height: calc(100vh - 64px);
display: flex;
flex-direction: column;
}
.menu-header {
padding: 20px;
background: rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.menu-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
#sidebar-menu .el-scrollbar {
flex: 1;
}
#sidebar-menu .el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
margin-bottom: 0 !important;
}
#sidebar-menu .el-menu {
background: transparent;
border: none;
}
#sidebar-menu .el-menu .el-submenu__title {
color: #ffffff;
}
#sidebar-menu .el-menu .el-submenu__title i,
#sidebar-menu .el-menu .el-menu-item i {
color: inherit;
}
#sidebar-menu .el-menu .el-submenu__title:hover {
background: rgba(0, 0, 0, 0.1);
}
#sidebar-menu .el-menu .el-menu-item {
color: inherit;
}
#sidebar-menu .el-menu .el-menu-item.is-active {
background: rgba(0, 0, 0, 0.1);
}
#sidebar-menu .el-menu .el-menu-item:focus,
.el-menu-item:hover {
background: rgba(0, 0, 0, 0.1);
}
#sidebar-menu .el-menu .el-menu-item a {
text-decoration: none;
color: inherit;
display: inline-block;
width: 100%;
}
.sub-app-container-main-content {
flex: 1;
padding: 10px;
background: #f6f6f6;
box-sizing: border-box;
overflow: auto;
height: calc(100vh - 64px);
}
.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;
height: 100%;
}
/* 悬浮菜单按钮样式 */
.menu-toggle-btn {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgb(64 109 157);
color: white;
text-align: center;
line-height: 50px;
font-size: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
cursor: pointer;
transition: all 0.3s ease;
}
.menu-toggle-btn:hover {
background-color: rgb(54 99 147);
transform: scale(1.05);
}
.menu-toggle-btn.active {
background-color: #d9534f;
}
/* 菜单遮罩层样式 */
.menu-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
}
@media (max-width: 768px) {
.sidebar-menu {
position: fixed;
top: 64px;
left: -100%;
width: 40%;
z-index: 999;
transition: left 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
}
.sidebar-menu.show {
left: 0;
}
.menu-toggle-btn {
display: block;
}
.menu-overlay.show {
display: block;
}
.main-content {
padding: 20px;
width: 100%;
}
}
</style>
<div class="sub-app-container" id="sub-app-container">
<!-- 菜单遮罩层 -->
<div class="menu-overlay" id="menu-overlay"></div>
<!-- 左侧菜单区域 -->
<nav class="sidebar-menu" id="sidebar-menu">
<div class="menu-header">
<h2 class="menu-title">子系统</h2>
</div>
<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">
<div class="content-body" id="sub-app-container-main-content-body">${layoutContent!}</div>
</main>
<!-- 悬浮菜单按钮 -->
<div class="menu-toggle-btn" id="menu-toggle-btn">
<i class="fa fa-bars"></i>
</div>
<!-- 子应用ID -->
<div id="sub-app-id" style="display: none"></div>
</div>
<script nonce="${cspNonce!}">
// 初始化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()
})
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) {
}
})()
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: [],
// 默认选中菜单是否加载中
defaultSelectLoading: true
}
},
computed: {
leftMenus() {
if (this.menus) {
return this.menus
} else {
try {
return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus"))
} catch (e) {
return []
}
}
}
},
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
this.$axios.post("/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))
this.echoMenus()
} else {
this.menus = []
}
})
},
// 设置应用信息
setAppInfo(app) {
if (app) {
// 储存到缓存
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
// 储存到div中
document.getElementById("sub-app-id").innerText = app.id
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) {
}
}
},
// 菜单回显
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)) || []
// !this.activeMenuIndex &&
if ('/platform/v4/subApp' === getBaseSubAppPath()) {
this.defaultSelect()
} else {
this.hrefSelect()
}
} catch (e) {
}
},
// 地址选中
hrefSelect() {
const targetHref = this.getMenuOwnerPath()
function findMenuPath(menus, targetHref) {
function search(items, path) {
for (const item of items) {
// 如果当前项匹配 href,返回路径(包含自己)
if (item.href === targetHref) {
return [...path, item];
}
// 如果有子菜单,递归查找
if (item.children?.length) {
const result = search(item.children, [...path, item]);
if (result) return result;
}
}
return null;
}
return search(menus, []);
}
const result = findMenuPath(this.menus, targetHref)
if(result){
this.activeMenuIndex = result[result.length-1]['id']
this.openedMenus = result.map(v=>v.id)
this.$nextTick(() => {
this.menuSelect(this.activeMenuIndex, this.openedMenus)
})
}
},
// 默认选中
getMenuOwnerPath() {
const pathname = window.location.pathname
if (pathname === "/platform/qsv/survey/reportPage") {
return "/platform/qsv/survey"
}
if (pathname === "/platform/qsv/new") {
return "/platform/qsv/activity"
}
return pathname
},
getMenuOwnerFullPath() {
if (window.location.pathname === "/platform/qsv/survey/reportPage") {
return "/platform/qsv/survey"
}
if (window.location.pathname === "/platform/qsv/new") {
return "/platform/qsv/activity"
}
return getFullSubAppPath()
},
defaultSelect() {
// 查找第一个有href的子菜单
function findFirstChildWithHref(menus) {
for (const menu of menus) {
// 如果当前菜单有href,返回它
if (menu.href) {
return {
menu: menu,
parentIds: [menu.parentId]
};
}
// 如果有子菜单,递归查找
if (menu.children && menu.children.length > 0) {
const result = findFirstChildWithHref(menu.children);
if (result) {
// 添加当前菜单的ID到父ID集合
result.parentIds.push(menu.id);
return result;
}
}
}
return null;
}
const result = findFirstChildWithHref(this.menus)
if (result) {
this.activeMenuIndex = result.menu.id
this.openedMenus = result.parentIds
this.$nextTick(() => {
this.menuSelect(this.activeMenuIndex, this.openedMenus)
// 使用pjax跳转
commonUtil.pjaxPush(getBaseSubAppPath(result.menu.href))
/*const tempLink = document.createElement('a')
tempLink.href = result.menu.href
tempLink.setAttribute('data-pjax', '')
tempLink.style.display = 'none'
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)*/
})
return
}
// 没有已授权的可用子菜单时,才使用当前应用自身配置的地址作为兜底入口。
try {
const rootApp = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app"))
const currentAppId = GetQueryString("appId")
const hasRootHref = rootApp && typeof rootApp.href === "string" && rootApp.href.trim() !== ""
// 校验缓存应用与当前URL一致,避免复用旧标签页缓存时跳转到其他应用。
if (hasRootHref && String(rootApp.id) === currentAppId) {
this.activeMenuIndex = rootApp.id
this.openedMenus = []
this.$nextTick(() => {
this.menuSelect(this.activeMenuIndex, this.openedMenus)
commonUtil.pjaxPush(getBaseSubAppPath(rootApp.href))
})
}
} catch (e) {
// 根菜单缓存缺失或格式异常时保持当前页面,不执行无权限地址跳转。
}
},
init() {
// 页面加载时获取到正确的菜单
const pathname = window.location.pathname
if (!pathname.startsWith("/platform/v4/subApp")) {
const path = this.getMenuOwnerFullPath()
this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname: path}).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)
}
},
},
mounted() {
this.init()
// 移动端布局
const sidebarMenu = document.getElementById("sidebar-menu")
const menuToggleBtn = document.getElementById("menu-toggle-btn")
const menuOverlay = document.getElementById("menu-overlay")
// 初始化菜单状态
if (window.innerWidth <= 768) {
menuToggleBtn.style.display = "block"
} else {
menuToggleBtn.style.display = "none"
}
// 菜单按钮点击事件
menuToggleBtn.addEventListener("click", function () {
sidebarMenu.classList.toggle("show")
menuOverlay.classList.toggle("show")
})
// 遮罩层点击事件
menuOverlay.addEventListener("click", function () {
sidebarMenu.classList.remove("show")
menuOverlay.classList.remove("show")
})
window.addEventListener("resize", function () {
if (window.innerWidth <= 768) {
menuToggleBtn.style.display = "block"
} else {
menuToggleBtn.style.display = "none"
sidebarMenu.classList.remove("show")
menuOverlay.classList.remove("show")
}
})
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,733 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"/>
<title>智慧工会</title>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vant/index.css"/>
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css"/>
<link rel="stylesheet" href="${base!}/assets/platform/css/h5.css"/>
<link rel="stylesheet" href="https://cdn.staticfile.net/animate.css/4.1.1/animate.css"/>
<link rel="stylesheet" href="${base!}/assets/platform/fonts/font-awesome.min.css"/>
<link rel="stylesheet" href="${base!}/assets/platform/css/pdf/pdfh5.css">
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.2.0/weui.min.css">
<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>
<script src="${base!}/assets/platform/plugins/vant/index.js"></script>
<script src="${base!}/assets/platform/plugins/moment/moment.min.js"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<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>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css"/>
<!-- import ElementUI -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js?v=0.0.1"></script>
<script src="${base!}/assets/platform/plugins/form-create/designer/index.umd.js"></script>
<!--图片预览-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/viewerjs/viewer.css" />
<script src="${base!}/assets/platform/plugins/viewerjs/viewer.js"></script>
<!--pdf平铺展示-->
<script src="${base!}/assets/platform/plugins/pdfJs/h5/pdf.js" type="text/javascript" charset="utf-8"></script>
<script src="${base!}/assets/platform/plugins/pdfJs/h5/pdf.worker.js" type="text/javascript" charset="utf-8"></script>
<script src="${base!}/assets/platform/plugins/pdfJs/h5/pdfh5.js" type="text/javascript" charset="utf-8"></script>
<!--h5扫码-->
<script src="${base!}/assets/platform/plugins/html5-qrcode/html5-qrcode.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/util/coordinateUtil.js"></script>
<script src="${base!}/assets/platform/js/util/voiceMenuNavigator.js"></script>
<script src="${base!}/assets/platform/js/main.js"></script>
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
<script src="${base!}/assets/platform/js/util/h5AutoShowError.js"></script>
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js"></script>
<!--富文本编辑器-->
<link rel="stylesheet" href="${base!}/assets/platform/plugins/wangEditor4/wangEditor.css"/>
<script src="${base!}/assets/platform/plugins/wangEditor4/wangEditor.js"></script>
<!--签字canvas-->
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<!--g2plot-->
<script src="${base!}/assets/platform/plugins/g2plot/g2plot.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/goober@2.1.10/dist/goober.umd.js"></script>-->
<script
type="text/javascript"
src="https://webapi.amap.com/maps?v=2.0&key=57f0d098ba1b881ecc436c4cfd23bbbf&plugin=AMap.PolyEditor,AMap.Geolocation"
></script>
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
<script nonce="${cspNonce!}">
Vue.config.devtools = true
// 禁用vant的表单验证行内错误信息显示
vant.Form.props.showErrorMessage.default = false
vant.ActionSheet.props.lazyRender.default = false
function toggleShowBar() {
if (["/platform/h5/home", "/platform/home"].includes(window.location.pathname)) {
document.querySelector("body>.main-page").classList.add("page-showtabbar")
} else {
document.querySelector("body>.main-page").classList.remove("page-showtabbar")
}
}
// toast多例模式
vant.Toast.allowMultiple()
/**
* H5 弹层历史管理器。
* 页面通过 register 注册页面标识和弹层状态回调,通过 open/close/closeAll 维护浏览器历史;
* popstate 返回 true 表示本次返回仅用于同步弹层,PJAX 不再执行页面跳转。
*/
window.h5HistoryLayerManager = {
pageKey: "",
stack: [],
onChange: null,
afterClose: null,
subscribers: [],
pendingNavigation: null,
register(pageKey, onChange) {
const currentState = window.history.state || {}
this.pageKey = pageKey
this.stack = []
this.onChange = onChange
this.afterClose = null
this.pendingNavigation = null
window.history.replaceState({
...currentState,
url: currentState.url || window.location.href,
h5HistoryLayerPage: pageKey,
h5HistoryLayerStack: []
}, "", window.location.href)
this.notify()
},
/**
* 确保当前页面已初始化弹层历史。
* pageKey 传当前页面的稳定标识;返回 boolean,true 表示已有或已完成页面注册。
*/
ensureRegistered(pageKey) {
if (this.pageKey) return true
this.register(pageKey, null)
return true
},
/**
* 订阅全局弹层栈变化。
* onChange 接收当前弹层名称数组;返回 Function 类型的注销方法,组件销毁时必须调用。
*/
subscribe(onChange) {
if (typeof onChange !== "function") return () => {}
this.subscribers.push(onChange)
onChange(this.stack.slice())
return () => {
const index = this.subscribers.indexOf(onChange)
if (index !== -1) this.subscribers.splice(index, 1)
}
},
unregister(pageKey) {
if (pageKey && this.pageKey !== pageKey) return
this.reset()
},
open(layerName) {
if (!this.pageKey || !layerName) return false
const currentLayer = this.stack[this.stack.length - 1]
if (currentLayer === layerName) return true
const nextStack = this.stack.concat(layerName)
window.history.pushState({
...(window.history.state || {}),
url: window.location.href,
h5HistoryLayerPage: this.pageKey,
h5HistoryLayerStack: nextStack
}, "", window.location.href)
this.stack = nextStack
this.notify()
return true
},
close(layerName, afterClose = null) {
if (!this.stack.length) {
if (typeof afterClose === "function") afterClose()
return false
}
const currentLayer = this.stack[this.stack.length - 1]
if (layerName && currentLayer !== layerName) return false
this.afterClose = afterClose
window.history.back()
return true
},
closeAll(afterClose = null) {
if (!this.stack.length) {
if (typeof afterClose === "function") afterClose()
return false
}
this.afterClose = afterClose
window.history.go(-this.stack.length)
return true
},
/**
* 关闭当前页面全部弹层后跳转业务页面。
* url 传目标页面地址,data 传可选的 PJAX 请求参数;返回 boolean,true 表示已接管本次跳转。
*/
closeAllAndNavigate(url, data = null) {
if (!url) return false
if (!this.stack.length) {
pjaxReplace(url, data)
return true
}
this.pendingNavigation = {url: url, data: data}
this.afterClose = null
window.history.go(-this.stack.length)
return true
},
handlePopState(nativeEvent) {
const state = nativeEvent && nativeEvent.state ? nativeEvent.state : {}
const targetStack = state.h5HistoryLayerPage === this.pageKey && Array.isArray(state.h5HistoryLayerStack)
? state.h5HistoryLayerStack.slice()
: []
const stackChanged = this.stack.length !== targetStack.length ||
this.stack.some((layerName, index) => layerName !== targetStack[index])
if (!stackChanged) return false
if (!this.stack.length && !targetStack.length) return false
this.stack = targetStack
this.notify()
const callback = this.afterClose
const navigation = this.pendingNavigation
this.afterClose = null
this.pendingNavigation = null
if (navigation && navigation.url) {
pjaxReplace(navigation.url, navigation.data)
return true
}
if (typeof callback === "function") {
setTimeout(() => callback(), 0)
}
return true
},
notify() {
if (typeof this.onChange === "function") {
this.onChange(this.stack.slice())
}
this.subscribers.slice().forEach((subscriber) => {
subscriber(this.stack.slice())
})
},
reset() {
this.stack = []
this.notify()
this.pageKey = ""
this.onChange = null
this.afterClose = null
this.subscribers = []
this.pendingNavigation = null
}
}
//初始化pjax
;(function initPjax() {
if ($.support.pjax) {
// jquery-pjax 的历史缓存读取全局默认值;必须在初始化前关闭,避免返回时恢复缺少 Vue 动态样式的旧 DOM。
$.pjax.defaults.maxCacheLength = 0
$(document).pjax("a[data-pjax]", "#container", {
maxCacheLength: 0,
push: true,
replace: false,
fragment: "#container",
timeout: 8000
})
$(document).on("pjax:send", function () {
window.h5HistoryLayerManager.reset()
showH5PageSkeleton()
NProgress.start()
window.customStyleList = document.querySelectorAll('style[id^="style-"]')
})
$(document).on("pjax:complete", function () {
// PJAX 替换 #container 时可能已经移除旧样式节点,清理前需兼容节点脱离 DOM 的情况。
if (window.customStyleList) {
window.customStyleList.forEach((style) => {
if (style && style.parentNode) {
style.parentNode.removeChild(style)
}
})
window.customStyleList = null
}
NProgress.done()
hideH5PageSkeleton()
toggleShowBar()
})
// PJAX 已内置页面前进、后退处理;这里只同步 H5 弹层历史,禁止再次发起 PJAX 请求。
$(window).on("popstate", function (event) {
window.h5HistoryLayerManager.handlePopState(event.originalEvent)
})
}
})()
function pjaxReplace(url, data = null) {
showH5PageSkeleton(url)
// 疗休养报名页面使用独立 H5 布局,其中包含图片懒加载等专用初始化逻辑。
// 禁止通过公共 PJAX 只替换页面片段,否则专用布局脚本不会执行,首次进入会出现图片懒加载指令未注册。
if (url && url.indexOf("/platform/tour/signup/h5") === 0) {
window.location.href = url
return
}
return $.pjax({
url: url,
container: "#container",
maxCacheLength: 0,
push: true,
replace: false,
fragment: "#container",
timeout: 8000,
data: data
})
}
// 仅在疗休养 H5 页面切换期间显示骨架,避免影响共用布局下的其他业务模块。
const showH5PageSkeleton = (targetUrl) => {
const pageUrl = targetUrl || window.location.pathname
if (pageUrl.indexOf("/platform/tour/") !== 0) return
const skeleton = document.getElementById("tour-page-transition-skeleton")
if (skeleton) skeleton.classList.add("is-visible")
}
const hideH5PageSkeleton = () => {
const skeleton = document.getElementById("tour-page-transition-skeleton")
if (skeleton) skeleton.classList.remove("is-visible")
}
// 从疗休养完整页面返回公共 H5 页面时可能命中 BFCache,需要清除离开前保留的骨架状态。
window.addEventListener("pageshow", () => {
hideH5PageSkeleton()
})
let h5GlobalLoading = null
let h5PendingRequestCount = 0
function showH5GlobalLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg") {
if (!h5GlobalLoading) {
h5GlobalLoading = vant.Toast.loading({
icon: icon,
message: "加载中...",
forbidClick: true,
loadingType: "spinner",
duration: 0,
className: "custom-list-loading"
})
}
}
function closeH5GlobalLoading() {
if (h5PendingRequestCount > 0 || !h5GlobalLoading) return
h5GlobalLoading.clear()
h5GlobalLoading = null
}
// 所有 H5 Axios 请求先展示 Loading,等待 200ms 后再真正发出请求。
window.bindH5AxiosLoading = function(axiosService) {
axiosService.interceptors.request.use((config) => {
// 页面自行展示骨架屏时保留请求延迟,但不叠加全局 Loading 遮罩。
if (config.h5SkeletonLoading) {
return new Promise((resolve) => {
setTimeout(() => resolve(config), 200)
})
}
h5PendingRequestCount++
showH5GlobalLoading()
return new Promise((resolve) => {
setTimeout(() => resolve(config), 200)
})
}, (error) => {
return Promise.reject(error)
})
axiosService.interceptors.response.use((response) => {
if (!response.config || !response.config.h5SkeletonLoading) {
h5PendingRequestCount = Math.max(0, h5PendingRequestCount - 1)
closeH5GlobalLoading()
}
return response
}, (error) => {
if (!error.config || !error.config.h5SkeletonLoading) {
h5PendingRequestCount = Math.max(0, h5PendingRequestCount - 1)
closeH5GlobalLoading()
}
return Promise.reject(error)
})
}
function createListLoading(icon = "/assets/mobile/img/common/smart-union-loading-red.svg") {
showH5GlobalLoading(icon)
const closeLoading = function() {
closeH5GlobalLoading()
}
return {
clear: closeLoading,
close: closeLoading
}
}
function historyBack(func) {
showH5PageSkeleton()
if (window.history.length > 1) {
window.history.back()
} else {
window.location.href = "/platform/h5/home"
}
func && typeof func === "function" && func()
}
function returnH5Home(activeTab = "home") {
if (window.store && typeof window.store.commit === "function") {
window.store.commit("setActiveTarBar", activeTab || "home")
}
window.location.replace("/platform/h5/home")
}
Vue.mixin({
methods: {
historyBack: function(func = ()=>{}) {
historyBack(func);
},
returnH5Home: function(activeTab = "home") {
returnH5Home(activeTab)
}
},
created() {
// 只处理有 pageForm 且 pageForm 有 approval 字段的组件
if (
this.pageForm &&
typeof this.pageForm === 'object' &&
('approval' in this.pageForm || 'isAudit' in this.pageForm)
) {
// 从 URL 查询参数中读取 approval
const urlParams = new URLSearchParams(window.location.search);
const approvalParam = urlParams.get('tab');
if (approvalParam === 'done') {
this.$set(this.pageForm, 'approval', true);
this.$set(this.pageForm, 'isAudit', true);
} else if (approvalParam === 'todo') {
this.$set(this.pageForm, 'approval', false);
this.$set(this.pageForm, 'isAudit', false);
}
}
}
})
</script>
<!--ws-->
<script nonce="${cspNonce!}" 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(() => {
// 所有订阅完成后,发送 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 nonce="${cspNonce!}">
const store = new Vuex.Store({
plugins: [createPersistedState({
key: "zhgh-h5-vuex",
paths: ["activeTarBar"],
storage: window.localStorage
})],
state: {
user: JSON.parse(window.sessionStorage.getItem("user")),
//聊天室
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
//当前激活的tarBar
activeTarBar: null
},
mutations: {
//设置用户信息
setUser(state, payload) {
state.user = payload
},
setActiveTarBar(state, payload) {
state.activeTarBar = payload
}
},
actions: {
logout() {
window.localStorage.removeItem("zhgh-h5-vuex")
window.location.href = "/platform/login/logout"
}
}
})
$.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>
<script nonce="${cspNonce!}" type="text/javascript">
//挂载
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$coordinateUtil = coordinateUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService.bind(commonUtil)
Vue.prototype.$pjaxReplace = pjaxReplace
Vue.prototype.$showH5PageSkeleton = showH5PageSkeleton
Vue.prototype.$hideH5PageSkeleton = hideH5PageSkeleton
</script>
<script nonce="${cspNonce!}">
const h5ComponentVersion = "20260821_2"
Vue.component("rich-text", httpVueLoader("/components/plugins/sysRichTextView/index.vue?v=" + h5ComponentVersion))
Vue.component("h5-file-upload", httpVueLoader("/components/plugins/sysUpload/h5Index.vue?v=" + h5ComponentVersion))
Vue.component("h5-signature", httpVueLoader("/components/plugins/sysSignature/h5Index.vue?v=" + h5ComponentVersion))
Vue.component("text-editor", httpVueLoader("/components/plugins/sysTextEditor/index.vue?v=" + h5ComponentVersion))
Vue.component("dict-tag", httpVueLoader("/components/plugins/sysDict/DictTag.vue?v=" + h5ComponentVersion))
Vue.component("svg-icon", httpVueLoader("/components/plugins/sysSvgIcon/index.vue?v=" + h5ComponentVersion))
Vue.component("year-van-dropdown-item", httpVueLoader("/components/plugins/vantMore/yearVanDropDownItem.vue?v=" + h5ComponentVersion))
Vue.component("van-text-dialog", httpVueLoader("/components/plugins/vantMore/vantTextDialog.vue?v=" + h5ComponentVersion))
Vue.component("enum-tag", httpVueLoader("/components/plugins/sysEnum/EnumTag.vue?v=" + h5ComponentVersion))
Vue.component("table-list", httpVueLoader("/components/plugins/h5/TableList.vue?v=" + h5ComponentVersion))
Vue.component("table-column", httpVueLoader("/components/plugins/h5/TableColumn.vue?v=" + h5ComponentVersion))
Vue.component("file-preview", httpVueLoader("/components/plugins/sysFilePreview/H5Index.vue?v=" + h5ComponentVersion))
</script>
<style>
.h5-voice-menu {
position: fixed;
right: 16px;
bottom: calc(76px + env(safe-area-inset-bottom));
z-index: 21000;
width: 48px;
height: 48px;
border: 0;
border-radius: 50%;
color: #fff;
background: #1989fa;
box-shadow: 0 6px 18px rgba(25, 137, 250, 0.35);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
outline: none;
}
.h5-voice-menu.is-listening {
background: #ff976a;
box-shadow: 0 6px 18px rgba(255, 151, 106, 0.38);
}
.h5-voice-menu .voice-menu-text {
display: none;
}
.tour-page-transition-skeleton {
position: fixed;
inset: 0;
z-index: 30000;
display: none;
padding: 58px 12px 20px;
background: #f3f7fd;
box-sizing: border-box;
}
.tour-page-transition-skeleton.is-visible { display: block; }
.tour-page-transition-skeleton__nav,
.tour-page-transition-skeleton__hero,
.tour-page-transition-skeleton__line,
.tour-page-transition-skeleton__card {
background: linear-gradient(90deg, #edf2f8 25%, #f8fbff 37%, #edf2f8 63%);
background-size: 400% 100%;
animation: tour-page-skeleton-loading 1.35s ease infinite;
}
.tour-page-transition-skeleton__nav { position: absolute; top: 0; left: 0; right: 0; height: 46px; background-color: #fff; }
.tour-page-transition-skeleton__hero { height: 154px; border-radius: 14px; }
.tour-page-transition-skeleton__line { width: 42%; height: 18px; margin: 18px 2px 12px; border-radius: 9px; }
.tour-page-transition-skeleton__card { height: 126px; margin-top: 12px; border-radius: 14px; }
@keyframes tour-page-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
</style>
</head>
<body>
<div id="tour-page-transition-skeleton" class="tour-page-transition-skeleton" aria-hidden="true">
<div class="tour-page-transition-skeleton__nav"></div>
<div class="tour-page-transition-skeleton__hero"></div>
<div class="tour-page-transition-skeleton__line"></div>
<div class="tour-page-transition-skeleton__card"></div>
<div class="tour-page-transition-skeleton__card"></div>
<div class="tour-page-transition-skeleton__card"></div>
</div>
<div class="main-page page-showtabbar">
<div class="page-wrapper">
<div id="container">${layoutContent}</div>
</div>
</div>
<!--<button type="button" class="h5-voice-menu" id="voice-menu-btn" data-platform="H5" title="语音打开菜单">-->
<!-- <i class="fa fa-microphone"></i>-->
<!-- <span class="voice-menu-text">语音</span>-->
<!--</button>-->
<script nonce="${cspNonce!}">
toggleShowBar()
</script>
</body>
</html>
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>数智工会领导驾驶舱</title>
<link rel="stylesheet" href="${base!}/assets/platform/css/root.css?v=20260524"/>
<script src="${base!}/assets/platform/plugins/vue/vue.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery.js?v=20260524"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js?v=20260524"></script>
<script nonce="${cspNonce!}">
Vue.config.devtools = false
const store = {
state: {
user: JSON.parse(window.sessionStorage.getItem("user") || "null") || {permissions: [], roles: []}
}
}
window.ELEMENT = window.ELEMENT || {
Message: {
error(message) {
console.error(message || "操作失败")
}
},
Loading: {
service() {
return {
close() {}
}
}
}
}
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
</script>
</head>
<body>
${layoutContent!}
</body>
</html>
@@ -0,0 +1,231 @@
<!doctype html>
<html lang="${lang,escape}">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"/>
<title>智慧工会</title>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vant/index.css?v=20260524"/>
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css?v=20260524"/>
<link rel="stylesheet" href="${base!}/assets/platform/css/h5.css?v=20260524"/>
<style>
.tour-page-transition-skeleton {
position: fixed;
inset: 0;
z-index: 30000;
display: none;
padding: 58px 12px 20px;
background: #f3f7fd;
box-sizing: border-box;
}
.tour-page-transition-skeleton.is-visible { display: block; }
.tour-page-transition-skeleton__nav,
.tour-page-transition-skeleton__hero,
.tour-page-transition-skeleton__line,
.tour-page-transition-skeleton__card {
background: linear-gradient(90deg, #edf2f8 25%, #f8fbff 37%, #edf2f8 63%);
background-size: 400% 100%;
animation: tour-page-skeleton-loading 1.35s ease infinite;
}
.tour-page-transition-skeleton__nav { position: absolute; top: 0; left: 0; right: 0; height: 46px; background-color: #fff; }
.tour-page-transition-skeleton__hero { height: 154px; border-radius: 14px; }
.tour-page-transition-skeleton__line { width: 42%; height: 18px; margin: 18px 2px 12px; border-radius: 9px; }
.tour-page-transition-skeleton__card { height: 126px; margin-top: 12px; border-radius: 14px; }
@keyframes tour-page-skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
</style>
<script src="${base!}/assets/platform/plugins/vue/vue.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/vant/index.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js?v=20260524"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery.js?v=20260524"></script>
<script src="${base!}/assets/platform/js/util/commonUtil.js?v=20260524"></script>
<script src="${base!}/assets/platform/js/util/h5PopupHistory.js?v=20260830"></script>
<script nonce="${cspNonce!}">
Vue.config.devtools = true
if (vant.Form && vant.Form.props && vant.Form.props.showErrorMessage) {
vant.Form.props.showErrorMessage.default = false
}
if (vant.ActionSheet && vant.ActionSheet.props && vant.ActionSheet.props.lazyRender) {
vant.ActionSheet.props.lazyRender.default = false
}
vant.Toast.allowMultiple()
if (vant.Lazyload) {
Vue.use(vant.Lazyload)
}
window.loadScriptOnce = function(src) {
window.__scriptLoadingMap = window.__scriptLoadingMap || {}
if (window.__scriptLoadingMap[src]) {
return window.__scriptLoadingMap[src]
}
window.__scriptLoadingMap[src] = new Promise(function(resolve, reject) {
const existing = document.querySelector('script[src="' + src + '"]')
if (existing) {
resolve()
return
}
const script = document.createElement("script")
script.src = src
script.onload = function() {
resolve()
}
script.onerror = function() {
reject(new Error("script load failed: " + src))
}
document.head.appendChild(script)
})
return window.__scriptLoadingMap[src]
}
window.loadStyleOnce = function(href) {
window.__styleLoadingMap = window.__styleLoadingMap || {}
if (window.__styleLoadingMap[href]) {
return window.__styleLoadingMap[href]
}
window.__styleLoadingMap[href] = new Promise(function(resolve, reject) {
const existing = document.querySelector('link[href="' + href + '"]')
if (existing) {
resolve()
return
}
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = href
link.onload = function() {
resolve()
}
link.onerror = function() {
reject(new Error("style load failed: " + href))
}
document.head.appendChild(link)
})
return window.__styleLoadingMap[href]
}
const store = {
state: {
user: JSON.parse(window.sessionStorage.getItem("user") || "null"),
room: "${@auth.getPrincipalProperty('loginname')}:${@auth.getSessionId()}",
activeTarBar: null
},
commit(type, payload) {
if (type === "setUser") {
this.state.user = payload
} else if (type === "setActiveTarBar") {
this.state.activeTarBar = payload
}
},
dispatch(type) {
if (type === "logout") {
window.localStorage.removeItem("zhgh-h5-vuex")
window.location.href = "/platform/login/logout"
}
}
}
window.ELEMENT = window.ELEMENT || {
Message: {
error(message) {
vant.Toast.fail(message || "操作失败")
}
},
Loading: {
service() {
const loading = vant.Toast.loading({
duration: 0,
forbidClick: true,
message: "处理中,请稍候"
})
return {
close() {
loading.clear()
}
}
}
}
}
// 疗休养页面跳转前统一展示结构化骨架,避免旧页面或未完成样式短暂闪现。
const showH5PageSkeleton = () => {
const skeleton = document.getElementById("tour-page-transition-skeleton")
if (skeleton) skeleton.classList.add("is-visible")
}
const hideH5PageSkeleton = () => {
const skeleton = document.getElementById("tour-page-transition-skeleton")
if (skeleton) skeleton.classList.remove("is-visible")
}
// 浏览器返回或前进时可能直接恢复 BFCache 页面,需要主动关闭离开页面前显示的骨架层。
// 此处只恢复页面展示,不刷新数据,从而保留线路列表的筛选条件、分页和滚动位置。
window.addEventListener("pageshow", () => {
hideH5PageSkeleton()
})
const pjaxReplace = (url) => {
showH5PageSkeleton()
window.location.href = url
}
function historyBack(fallbackUrl, func) {
if (typeof fallbackUrl === "function") {
func = fallbackUrl
fallbackUrl = "/platform/h5/home"
} else if (typeof fallbackUrl !== "string" || !fallbackUrl) {
fallbackUrl = "/platform/h5/home"
}
showH5PageSkeleton()
if (window.history.length > 1) {
window.history.back()
} else {
window.location.replace(fallbackUrl)
}
func && typeof func === "function" && func()
}
function returnH5Home(activeTab = "home") {
store.commit("setActiveTarBar", activeTab || "home")
window.location.replace("/platform/h5/home")
}
Vue.mixin({
methods: {
historyBack: function(fallbackUrl = "/platform/h5/home", func = function(){}) {
historyBack(fallbackUrl, func)
},
returnH5Home: function(activeTab = "home") {
returnH5Home(activeTab)
}
}
})
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService.bind(commonUtil)
Vue.prototype.$pjaxReplace = pjaxReplace
Vue.prototype.$showH5PageSkeleton = showH5PageSkeleton
Vue.prototype.$hideH5PageSkeleton = hideH5PageSkeleton
</script>
</head>
<body>
<div id="tour-page-transition-skeleton" class="tour-page-transition-skeleton" aria-hidden="true">
<div class="tour-page-transition-skeleton__nav"></div>
<div class="tour-page-transition-skeleton__hero"></div>
<div class="tour-page-transition-skeleton__line"></div>
<div class="tour-page-transition-skeleton__card"></div>
<div class="tour-page-transition-skeleton__card"></div>
<div class="tour-page-transition-skeleton__card"></div>
</div>
<div class="main-page">
<div class="page-wrapper">
<div id="container">${layoutContent}</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,641 @@
<!--#
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-sidebar-item i{
min-width: 22px;
}
.apps-content {
flex: 1;
padding: 20px;
overflow: auto;
display: flex;
flex-direction: column;
}
.apps-header {
padding: 0 0 10px 0;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.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: 10px;
display: flex;
align-items: flex-end;
}
.apps-filter-title {
color: #666;
font-size: 14px;
margin-bottom: 5px;
margin-right: 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;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.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;
top: 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="/assets/platform/img/home/app.png" 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)"
>
<img v-if="category.picIcon" :src="category.picIcon" :alt="category.name || ''" style="width: 20px"/>
<i v-else :class="category.icon || 'fa fa-th-large'"></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 class="fa fa-skype"></i>-->
<img v-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.moduleName">{{ app.moduleName }}</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 nonce="${cspNonce!}">
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/home/listRecommendApp", { platform: "PC" })
.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
})
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", {platform: 'PC'})
.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,801 @@
<!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="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index_custom.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/override.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/fullcalendar/fullcalendar.css">
<!-- import common 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="${base!}/assets/platform/plugins/nprogress/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>
<!--vue-count-to-->
<script src="${base!}/assets/platform/plugins/vue-count-to/vue-count-to.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>
<!-- swiper -->
<script src="${base!}/assets/platform/plugins/swiper/swiper-bundle.js"></script>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/swiper/swiper-bundle.min.css"></link>
<!--<script src="${base!}/assets/platform/plugins/snaker/SnakerflowDesigner.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/util/coordinateUtil.js"></script>
<script src="${base!}/assets/platform/js/util/voiceMenuNavigator.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/mixin/flowMixins.js"></script>
<script src="${base!}/assets/platform/js/tool/businessTool.js"></script>
<script src="${base!}/assets/platform/js/util/autoShowError.js"></script>
<script src="${base!}/assets/platform/plugins/fullcalendar/fullcalendar.min.js"></script>
<!--<script
type="text/javascript"
src="https://webapi.amap.com/maps?v=2.0&key=b4fa2e2bc10a725ab007b98a3aa447cc&plugin=AMap.PolyEditor,AMap.Geolocation"
></script>-->
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
<script nonce="${cspNonce!}">
// 在加载 lodash 后、使用前插入
const originalDefaultsDeep = window._.defaultsDeep;
window._.defaultsDeep = function(...args) {
// 先对所有参数做原型污染清洗
const cleanArgs = args.map(arg => sanitizeForPrototypePollution(arg));
return originalDefaultsDeep.apply(window._, cleanArgs);
};
function sanitizeForPrototypePollution(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(sanitizeForPrototypePollution);
const clean = {};
for (const key in obj) {
if (!Object.hasOwn(obj, key)) continue;
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
clean[key] = sanitizeForPrototypePollution(obj[key]);
}
return clean;
}
</script>
<script nonce="${cspNonce!}" 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.headerAlign.default = 'center'
ELEMENT.TableColumn.props.align.default = 'center'
ELEMENT.TableColumn.props.showOverflowTooltip = { type: Boolean, default: true }
Vue.use(ELEMENT, {
zIndex: 20000
})
Vue.use(FcDesigner)
Vue.use(FcDesigner.formCreate)
</script>
<!--广播频道-->
<script nonce="${cspNonce!}" type="text/javascript">
window.GlobalBroadcastChannel = new BroadcastChannel("zhgh-global-channel")
</script>
<!--ws-->
<script nonce="${cspNonce!}" 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 WS_URL = 'zhgh.jshvc.edu.cn' + "${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(() => {
// 所有订阅完成后,发送 join 消息
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
this.isSubscribed = true
this.sendJoinMessage()
}
}, 1000)
}
// 事件处理
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()
console.info('websocket open success')
}
}
// 新增发送 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()
}
}, 500)
}
}, 1000) // 延迟2秒重连
}
// 窗口事件处理
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 nonce="${cspNonce!}" 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
state.room = payload.loginname + ":${@auth.getSessionId()}"
}
},
actions: {}
})
// 暴露当前登录用户的加载状态,业务页面可在用户、角色和所属工会写入 store 后再执行初始化。
window.logonUserReady = $.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 nonce="${cspNonce!}" type="text/javascript">
const base = "${base!}"
const APP_DOMAIN = "${AppDomain!}"
</script>
<!--vue挂载-->
<script nonce="${cspNonce!}" type="text/javascript">
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
Vue.prototype.$coordinateUtil = coordinateUtil
Vue.prototype.$auth = commonUtil.authService()
Vue.prototype.$axios = commonUtil.axiosService()
Vue.prototype.$downLoad = commonUtil.downLoadService
Vue.prototype.$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"}
}
</script>
<!--vue全局组件-->
<script nonce="${cspNonce!}">
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("qr-code-plus", httpVueLoader("/components/plugins/sysQrCode/index.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("custom-card", httpVueLoader("/components/plugins/sysCard/index.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())
)
Vue.component("snaker-chart", httpVueLoader("/components/plugins/snaker/snakerChart.vue?v=" + new Date().getTime()))
Vue.component("snaker-start", httpVueLoader("/components/plugins/snaker/snakerStart.vue?v=" + new Date().getTime()))
Vue.component("permission-group", httpVueLoader("/components/plugins/permissionGroup/index.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: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20000;
overflow: hidden;
}
.v4-logo-container {
display: flex;
align-items: center;
height: 100%;
padding: 10px;
width: 250px;
justify-content: center;
}
.v4-logo {
height: 100%;
width: auto;
}
.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-voice-menu {
color: #ffffff;
cursor: pointer;
padding: 0 14px;
height: 100%;
border: 0;
background: transparent;
border-radius: 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
font: inherit;
outline: none;
}
.v4-voice-menu:hover,
.v4-voice-menu.is-listening {
background-color: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.v4-voice-menu.is-listening i {
color: #ffdf6b;
}
.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;
}
.voice-menu-text {
display: none;
}
}
/* 页脚样式 */
.v4-footer {
background: var(--color-primary);
padding: 30px 0;
}
.v4-footer-bottom {
max-width: 1500px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.v4-footer-copyright p {
margin: 4px 0;
color: #fff;
}
.v4-footer-tech {
color: #fff;
}
.v4-footer-tech a {
color: #fff;
text-decoration: none;
margin-left: 4px;
}
.v4-footer-tech a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<header class="v4-header">
<div class="v4-left-section">
<div class="v4-logo-container">
<img src="${AppLogo!}" 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/serv" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
流程中心
</a>
<a href="/platform/v4/todo" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
待办中心
</a>
<a href="/platform/v4/msg" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
消息中心
</a>
</nav>
</div>
<div class="v4-user-section">
<button type="button" class="v4-voice-menu" id="voice-menu-btn" data-platform="PC" title="语音打开菜单">
<i class="fa fa-microphone"></i>
<span class="voice-menu-text">语音</span>
</button>
<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>
<a class="v4-logout" href="/platform/login/logout">
<i class="fa fa-sign-out"></i>
退出
</a>
</div>
</header>
<div style="height: 64px"></div>
<main class="v4-content">
<!-- 页面内容区域 -->
<div class="ele-body" id="container">${layoutContent}</div>
</main>
<!-- 页脚 -->
<footer class="v4-footer" id="v4-footer" style="display: none;">
<div class="v4-footer-bottom">
<div class="v4-footer-copyright">
<p>&copy; 2025 武汉地大三思科技有限公司. 保留所有权利.</p>
<p>ICP备案号:のICP备12345678号-1 | 版本号:v4.0.0</p>
</div>
<div class="v4-footer-tech">
<span>技术支持:</span>
<a href="http://www.3sgo.cn/" target="_blank">武汉地大三思科技有限公司</a>
</div>
</div>
</footer>
<script nonce="${cspNonce!}">
// 处理导航项的active状态
$(document).ready(function () {
// 控制页脚显示的函数
function toggleFooter() {
var currentPath = getBaseSubAppPath()
var footer = $("#v4-footer")
if (currentPath === "/platform/v4/home") {
footer.show()
} else {
footer.hide()
}
}
// 初始化:根据当前URL设置active状态
function setActiveNavItem() {
var currentPath = getBaseSubAppPath()
$(".v4-nav-item").removeClass("active")
$(".v4-nav-item").each(function () {
if ($(this).attr("href") === currentPath) {
$(this).addClass("active")
}
})
}
// 页面加载时设置active状态和页脚显示
setActiveNavItem()
toggleFooter()
// 为导航项添加点击事件
$(".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()
toggleFooter()
})
})
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,386 @@
const act = {
template: /*language=HTML*/ `
<!-- 活动 -->
<div class="activity-section-wrapper">
<div class="section-act">
<!-- Swiper容器 -->
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
<!-- 活动数据 - Swiper -->
<div ref="activitySwiper" class="swiper activity-swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
<div class="item" @click="go(activity)">
<div class="img-box">
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
<span class="status">
{{getStatusText(activity)}}
</span>
</div>
<h4 class="title">
{{ activity.name}}
</h4>
<!--报名时间-->
<div class="time">
<p>
<i class="fa fa-clock-o"></i>
时间:{{ activity.startDate }}至{{ activity.endDate }}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 导航按钮 -->
<div ref="nextButton" class="swiper-button-next"></div>
<div ref="prevButton" class="swiper-button-prev"></div>
<!-- 进度条 -->
<div ref="pagination" class="swiper-pagination"></div>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon">📅</div>
<p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p>
</div>
</div>
</div>
`,
data() {
return {
actList: [],
swiper: null
}
},
methods: {
/*查询活动*/
listAct() {
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
if (res.code === 0) {
// for (let i = 0; i < 10; i++) {
// this.actList = this.actList.concat(res.data)
// }
this.actList = res.data
// 数据加载完成后初始化Swiper
this.$nextTick(() => {
this.initSwiper()
})
}
})
},
/*初始化Swiper*/
initSwiper() {
if (this.swiper) {
this.swiper.destroy(true, true)
this.swiper = null
}
const swiperElement = this.$refs.activitySwiper
if (!swiperElement) {
return
}
// 仅操作当前组件的轮播节点,避免页面其他轮播或尺寸观察器影响卡片宽度。
this.swiper = new Swiper(swiperElement, {
slidesPerView: 1,
spaceBetween: 18,
centeredSlides: false,
loop: false,
watchOverflow: true,
observer: false,
observeParents: false,
resizeObserver: false,
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
mousewheel: {
enabled: true,
forceToAxis: false,
releaseOnEdges: true,
sensitivity: 0.8,
},
navigation: {
nextEl: this.$refs.nextButton,
prevEl: this.$refs.prevButton,
},
pagination: {
el: this.$refs.pagination,
type: 'progressbar',
},
breakpoints: {
768: {
slidesPerView: 2,
},
1024: {
slidesPerView: 4,
},
1200: {
slidesPerView: 5,
}
}
})
},
// 活动状态
getStatusText(item) {
if (this.$moment().unix() > this.$moment(item.endDate).unix()) {
return "已结束"
} else if (this.$moment().unix() < this.$moment(item.startDate).unix()) {
return "未开始"
} else {
return "进行中"
}
},
go(act) {
if (!act.url) {
this.$message.warning("管理员未配置活动链接")
return
}
if (act.url === "/platform/activity/apply"
&& !this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN", "BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_WENTI_SPORTS"])) {
return this.$message.warning("PC端暂未开放报名,请前往移动端报名!")
}
window.open(act.url)
}
},
mounted() {
this.listAct()
},
beforeDestroy() {
if (this.swiper) {
this.swiper.destroy(true, true)
this.swiper = null
}
},
style: /*language=CSS*/ `
.activity-section-wrapper {
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.section-act {
width: 100%;
min-width: 0;
max-width: 100%;
}
.section-act .section-act-title span {
font-size: 30px;
color: #000000;
display: block;
font-weight: bold;
position: relative;
text-align: center;
padding: 40px 0;
}
/*.section-act .section-act-title span:hover {
color: var(--color-primary);
cursor: pointer;
}*/
/deep/ .section-act .section-act-title span::before {
content: '';
width: 24px;
height: 11px;
background: url(https://www.ncu.edu.cn/images/titl.svg) no-repeat center;
background-size: 24px 11px;
display: inline-block;
margin-right: 0;
vertical-align: middle;
}
/deep/ .section-act .section-act-title span::after {
content: '';
width: 24px;
height: 11px;
background: url(https://www.ncu.edu.cn/images/titr.svg) no-repeat center;
background-size: 24px 11px;
display: inline-block;
margin-left: 0;
vertical-align: middle;
}
/* Swiper容器样式 */
.activity-swiper-container {
position: relative;
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
padding: 9px 54px 12px;
/*margin-top: 30px;*/
overflow: hidden;
}
.activity-swiper {
width: 100%;
min-width: 0;
/*padding: 0 50px 20px 50px;*/
overflow: hidden;
}
.activity-swiper .swiper-slide {
height: auto;
min-width: 0;
}
/* 导航按钮样式 */
.activity-swiper-container .swiper-button-next,
.activity-swiper-container .swiper-button-prev {
color: var(--color-primary);
background: white;
width: 40px;
height: 40px;
top: 72px;
margin-top: 0;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.activity-swiper-container .swiper-button-next:hover,
.activity-swiper-container .swiper-button-prev:hover {
background: var(--color-primary);
color: white;
transform: scale(1.1);
}
/deep/ .activity-swiper-container .swiper-button-next::after,
/deep/ .activity-swiper-container .swiper-button-prev::after {
font-size: 16px;
font-weight: bold;
}
/* 进度条不展示,活动通过左右箭头或区域内滚轮浏览。 */
.activity-swiper-container .swiper-pagination {
display: none;
}
.activity-swiper-container .swiper-pagination-progressbar {
background: #e0e0e0;
border-radius: 2px;
}
.activity-swiper-container .swiper-pagination-progressbar-fill {
background: var(--color-primary);
border-radius: 2px;
}
.activity-swiper .swiper-slide .item {
height: auto;
position: relative;
cursor: pointer;
min-width: 0;
overflow: hidden;
}
.activity-swiper .swiper-slide .item .img-box {
width: 100%;
height: 128px;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.activity-swiper .swiper-slide .item .img-box .status {
position: absolute;
left: 0;
top: 0;
background: #c11623;
padding: 4px 2px;
min-width: 42px;
color: #ffffff;
border-radius: 0 0 50% 0;
font-size: 12px;
line-height: 1.2;
}
.activity-swiper .swiper-slide .item .img-box img {
display: block;
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 128px !important;
object-fit: fill !important;
transition: transform 0.3s ease;
}
.activity-swiper .swiper-slide .item:hover .img-box img {
transform: scale(1.1);
}
.activity-swiper .swiper-slide .item h4 {
font-size: 15px;
margin: 8px 0 0;
height: 42px;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
text-align: left;
transition: color 0.3s ease;
}
.activity-swiper .swiper-slide .item:hover h4 {
color: var(--color-primary);
}
.activity-swiper .swiper-slide .item .time {
/*margin-top: 8px;*/
}
.activity-swiper .swiper-slide .item .time p {
font-size: 12px;
margin: 0;
line-height: 1.4;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-swiper .swiper-slide .item .time i {
margin-right: 4px;
color: var(--color-primary);
}
/* 无活动占位提示 */
.no-activity-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #94a3b8;
text-align: center;
}
.no-activity-placeholder .icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.6;
}
.no-activity-placeholder p {
margin: 4px 0 0;
font-size: 16px;
font-weight: 500;
}
.no-activity-placeholder .subtext {
font-size: 13px;
opacity: 0.8;
}
`
};
@@ -0,0 +1,274 @@
const entry = {
template: /*language=HTML*/ `
<div class="entry-wrapper">
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
<div class="entry-section-header">
<div class="entry-section-title">
<i :class="section.icon"></i>
<span>{{ section.title }}</span>
<em>/Applications</em>
</div>
</div>
<div class="apps-container">
<div class="app-grid">
<div class="app-item"
v-for="(item, index) in section.list"
:key="item.id || index"
@click="openService(item, section.key)">
<div class="app-icon">
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
<i v-else class="fa fa-cube app-icon-fallback"></i>
</div>
<div class="app-name">{{ item.name }}</div>
</div>
<el-empty class="app-empty"
v-if="!section.list || section.list.length === 0"
description="暂无数据"
:image-size="72">
</el-empty>
</div>
</div>
</div>
</div>
`,
data() {
return {
hasMore: true,
entries: {
serv: [],
app: [],
fav: []
},
}
},
computed: {
entrySections() {
return [
{ key: 'serv', title: '推荐服务', icon: 'fa fa-star', list: this.entries.serv },
{ key: 'app', title: '推荐应用', icon: 'fa fa-fire', list: this.entries.app },
{ key: 'fav', title: '我的收藏', icon: 'fa fa-heart', list: this.entries.fav }
];
}
},
created() {
this.getRecommendService()
this.getRecommendApp()
this.getFavorite()
},
methods: {
// 查询推荐服务
async getRecommendService() {
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.serv = res.data
}
})
},
// 获取推荐应用
async getRecommendApp() {
this.$axios.post('/platform/home/listRecommendApp', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.app = res.data
}
})
},
// 获取我的收藏
async getFavorite() {
this.$axios.post('/platform/home/listFavorite', {platform: "PC"}).then(res => {
if (res.code === 0) {
this.entries.fav = res.data
}
})
},
// 点击服务或应用
openService(service, category) {
if (category === 'serv') {
if (!service.href) {
this.$message.error('无效的链接地址')
return
}
window.open(service.href)
return
}
if (category === 'app') {
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
return
}
if (category === 'fav') {
if (service.href) {
window.open(service.href)
return
}
if (!service.parentId) {
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
return
}
this.$message.error('无效的链接地址')
}
}
},
style: /*language=CSS*/ `
.entry-wrapper {
width: 100%;
box-sizing: border-box;
}
.entry-section + .entry-section {
margin-top: 22px;
}
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
.entry-section-fav {
display: none;
}
.entry-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.entry-section-title {
display: inline-flex;
align-items: baseline;
gap: 6px;
color: #19324d;
font-size: 18px;
font-weight: 700;
}
.entry-section-title i {
color: #409eff;
font-size: 16px;
}
.entry-section-title em {
color: #b5c0d6;
font-size: 14px;
font-style: normal;
font-weight: 500;
}
.apps-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
justify-items: center;
justify-content: start;
gap: 12px 18px;
min-height: 116px;
padding: 18px 20px;
background: #fff;
border: 1px solid #edf1f7;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
overflow-x: hidden;
}
.app-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 8px;
background: #fff;
width: 100%;
max-width: 108px;
min-width: 0;
box-sizing: border-box;
padding: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
}
.app-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.app-icon {
width: 56px;
height: 56px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 56px;
overflow: hidden;
border-radius: 8px;
background: #f5f8ff;
}
.app-icon img,
.app-icon-img {
display: block;
width: 56px !important;
height: 56px !important;
max-width: 56px !important;
max-height: 56px !important;
object-fit: contain !important;
border-radius: 8px;
}
.app-icon-fallback {
color: #409eff;
font-size: 24px;
}
.app-name {
min-height: 34px;
width: 100%;
font-size: 12px;
color: #333;
line-height: 1.4;
word-break: break-word;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.app-empty {
grid-column: 1 / -1;
width: 100%;
}
@media (max-width: 768px) {
.app-grid {
grid-template-columns: repeat(3, minmax(72px, 1fr));
gap: 12px;
padding: 14px 10px;
}
.app-item {
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.app-grid {
grid-template-columns: repeat(2, minmax(72px, 1fr));
gap: 12px;
}
}
`
};
@@ -0,0 +1,204 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.section-banner {
position: relative;
overflow: hidden;
}
.section-banner .banner-img {
width: 100%;
height: auto;
}
.section-banner .banner-img img {
width: 100%;
height: auto;
display: block;
}
.section-wrapper {
width: 100%;
}
.home-grid {
width: 80%;
margin: 20px auto;
display: grid;
grid-template-columns: 1fr;
gap: 24px;
align-items: stretch;
row-gap: 24px;
}
.entry-card {
display: flex;
/*align-items: center;*/
min-width: 320px;
background: transparent;
border-radius: 0;
box-shadow: none;
padding: 0;
transition: transform 0.2s, box-shadow 0.2s;
}
.entry-card:hover {
transform: none;
box-shadow: none;
}
.act-card {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
max-width: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
padding: 22px 11px;
box-sizing: border-box;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.home-section {
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.home-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.home-section-title {
display: inline-flex;
align-items: baseline;
gap: 6px;
color: #19324d;
font-size: 18px;
font-weight: 700;
}
.home-section-title i {
color: #409eff;
font-size: 16px;
}
.home-section-title em {
color: #b5c0d6;
font-size: 14px;
font-style: normal;
font-weight: 500;
}
/* 推荐入口的 scoped 样式会在组件挂载后注入,此处提前限制图片尺寸,避免刷新时原图短暂撑开页面。 */
#v4-home-app .entry-card .app-icon {
width: 56px;
height: 56px;
overflow: hidden;
}
#v4-home-app .entry-card .app-icon img {
display: block;
width: 56px;
height: 56px;
max-width: 56px;
max-height: 56px;
object-fit: contain;
}
</style>
<div class="v4-container" id="v4-home-app">
<!-- 首页banner -->
<div class="section-banner">
<div class="banner-img">
<img :src="homeBannerList[0] || ''" alt=""/>
<stats></stats>
</div>
</div>
<div class="" style="padding-bottom: 20px">
<user></user>
<div class="home-grid">
<div class="entry-card">
<entry></entry>
</div>
</div>
<div class="home-grid">
<div class="home-section">
<div class="home-section-header">
<div class="home-section-title">
<i class="fa fa-calendar"></i>
<span>最新活动</span>
<em>/Activities</em>
</div>
</div>
<div class="act-card">
<act></act>
</div>
</div>
</div>
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt :list="websiteNews"></jcdt>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("act.js"){}#-->
<!--#include("template.js"){}#-->
<!--#include("jcdt.js"){}#-->
<!--#include("entry.js"){}#-->
<!--#include("user.js"){}#-->
<!--#include("stats.js"){}#-->
new Vue({
el: '#v4-home-app',
data(){
return{
websiteNews: [],
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
}
},
components: {
'act': act,
'work-template': workTemplate,
'jcdt': jcdt,
'entry': entry,
'user': user,
'stats': stats,
},
mounted() {
this.getWebSiteNews()
},
methods: {
getWebSiteNews(){
this.$axios.post('/platform/home/getNews').then(res => {
if (res.code === 0) {
this.websiteNews = res.data
}
})
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,182 @@
const jcdt = {
template: /*language=HTML*/ `
<div class="jcdt-wrapper">
<div class="jcdt-container">
<el-carousel height="360px" :interval="10000" indicator-position="none">
<el-carousel-item v-for="item,index in list" :key="index">
<div class="jcdt-title">
<span>{{ item.label }}</span>
</div>
<div class="jcdt-content">
<div class="jcdt-list">
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
<div class="date" v-if="o.time">
<i class="el-icon-alarm-clock"></i>
<span>{{o.time}}</span>
</div>
<div style="display: flex; align-items: center">
<div>
</div>
<div class="title">{{o.text}}</div>
</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
</div>
</div>
`,
data() {
return {}
},
props: {
list: {
type: Array,
default: []
}
},
methods: {
onLink(item) {
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
window.open(url)
},
isHttpOrHttps(url) {
return /^(http:\/\/|https:\/\/)/i.test(url)
}
},
style: /*language=CSS*/ `
.jcdt-wrapper {
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.jcdt-container {
width: 80%;
max-width: 80%;
margin: 0 auto 10px;
background-color: #FFFFFF;
border-radius: 12px;
padding: 20px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
}
/deep/ .jcdt-title{
font-size: 30px;
color: #000000;
display: block;
font-weight: bold;
position: relative;
text-align: center;
padding: 20px 0;
}
/deep/ .jcdt-container .jcdt-title span::before {
content: '';
width: 24px;
height: 11px;
background: url(https://www.ncu.edu.cn/images/titl.svg) no-repeat center;
background-size: 24px 11px;
display: inline-block;
margin-right: 0;
vertical-align: middle;
}
/deep/ .jcdt-container .jcdt-title span::after {
content: '';
width: 24px;
height: 11px;
background: url(https://www.ncu.edu.cn/images/titr.svg) no-repeat center;
background-size: 24px 11px;
display: inline-block;
margin-left: 0;
vertical-align: middle;
}
/deep/ .jcdt-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
/deep/ .jcdt-item {
display: flex;
flex-direction: column;
padding: 16px;
border: none;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
cursor: pointer;
height: auto;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-bottom-color: var(--color-primary);
background: #ffffff;
}
/deep/ .jcdt-item:hover{
transform: translateY(-4px);
}
/deep/ .jcdt-item .date {
font-size: 20px;
font-weight: bold;
white-space: nowrap;
text-align: left;
margin-bottom: 8px;
transition: opacity 0.3s ease;
}
/deep/ .jcdt-item .title {
font-size: 16px;
font-weight: 600;
/*margin-bottom: 12px;*/
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/*height: 45px;*/
}
/deep/ .jcdt-item .image {
width: 100%;
height: 180px;
margin: 0 0 16px 0;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
/deep/ .jcdt-item .image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
/deep/ .jcdt-item .summary {
font-size: 14px;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-align: justify;
margin-bottom: 12px;
}
/deep/ .jcdt-item i {
color: #007aff;
}
`
};
@@ -0,0 +1,232 @@
const stats = {
template: /*language=HTML*/ `
<div class="stats-section">
<div class="stats-family">
<img src="/assets/platform/img/home/family.png"/>
</div>
<div class="stats-grid">
<!-- 待办 -->
<div
class="stat-item pending"
@click="handleStatClick('todo')"
>
<div class="stat-content">
<div class="stat-label">待办</div>
<div class="stat-number">{{ stats.todoCount }}</div>
</div>
<div class="stat-icon">
<i class="fa fa-clock-o"></i>
</div>
</div>
<!-- 已办 -->
<div
class="stat-item completed"
@click="handleStatClick('done')"
>
<div class="stat-content">
<div class="stat-label">已办</div>
<div class="stat-number">{{ stats.doneCount }}</div>
</div>
<div class="stat-icon">
<i class="fa fa-check-circle"></i>
</div>
</div>
<!-- 消息 -->
<div
class="stat-item messages"
@click="handleStatClick('notification')"
>
<div class="stat-content">
<div class="stat-label">消息</div>
<div class="stat-number">{{ stats.notifications }}</div>
</div>
<div class="stat-icon">
<i class="fa fa-bell"></i>
</div>
</div>
<!-- 发起 -->
<div
class="stat-item reminders"
@click="handleStatClick('started')"
>
<div class="stat-content">
<div class="stat-label">发起</div>
<div class="stat-number">{{ stats.startedCount }}</div>
</div>
<div class="stat-icon">
<i class="fa fa-file-text"></i>
</div>
</div>
</div>
</div>
`,
data() {
return {
stats: {
todoCount: 0,
doneCount: 0,
notifications: 0,
startedCount: 0
}
}
},
methods: {
handleStatClick(type) {
if (['todo', 'done', 'started'].includes(type)) {
window.open('/flow/todoCenter?status=' + type)
} else if (type === 'notification') {
window.open('/platform/v4/msg')
}
},
loadStats() {
this.$axios.post("/flow/todoCenter/statistics").then(res => {
if (res.code === 0) {
this.stats = res.data;
}
})
}
},
mounted() {
this.loadStats();
},
style: /*language=CSS*/ `
/* 右侧统计区域 */
.stats-section {
/*background: white;*/
color: #333;
padding: 20px;
border-radius: 12px;
z-index: 10;
width: 80%;
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px auto 0;
/*border: 1px solid #e8e8e8;*/
}
/* 子内容临时屏蔽时,同步收起统计容器,避免首页 banner 下方出现空白 */
.stats-section {
display: none;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
width: 32%;
min-width: 360px;
margin-left: auto;
}
/* 临时屏蔽首页右上角“待办、已办、消息、发起”统计卡片,保留原模板便于恢复 */
.stats-grid {
display: none;
}
.stat-item {
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #1f2937;
padding: 20px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
width: 100%;
height: 90px;
}
.stat-item:hover {
background: rgba(255, 255, 255, 0.62);
border-color: rgba(255, 255, 255, 0.55);
transform: translateY(-5px);
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.18);
}
.stat-icon {
font-size: 26px;
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.42);
color: rgba(31, 41, 55, 0.72);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-item.pending .stat-icon,
.stat-item.completed .stat-icon,
.stat-item.messages .stat-icon,
.stat-item.reminders .stat-icon {
background: rgba(255, 255, 255, 0.42);
}
.stat-item.pending .stat-icon {
color: #2563eb;
}
.stat-item.completed .stat-icon {
color: #16a34a;
}
.stat-item.messages .stat-icon {
color: #d97706;
}
.stat-item.reminders .stat-icon {
color: #0891b2;
}
.stat-content {
display: flex;
flex-direction: column;
margin-left: 0;
min-width: 0;
text-align: left;
}
.stat-label {
font-size: 14px;
color: rgba(31, 41, 55, 0.78);
text-transform: uppercase;
letter-spacing: 1px;
line-height: 1.2;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: #111827;
margin-top: 6px;
line-height: 1;
}
.stats-family {
width: 68%;
}
.stats-family img {
height: 190px !important;
border-radius: 30px;
}
/* 临时屏蔽“欢迎来到职工之家”图片,保留原节点便于恢复 */
.stats-family {
display: none;
}
`
};
@@ -0,0 +1,214 @@
const workTemplate = {
template: /*language=HTML*/ `
<div class="section-wrapper work-template-wrapper">
<div class="work-template-header">
<div class="work-template-title">
<i class="fa fa-clipboard"></i>
<span>工作模板</span>
<em>/Templates</em>
</div>
</div>
<div class="work-template-card">
<div v-if="templateList && templateList.length > 0" class="work-template-grid">
<div class="work-template-item"
v-for="template in templateList"
:key="template.id"
@click="go(template)">
<div class="work-template-icon">
<img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
</div>
<div class="work-template-name">{{ template.templateName || template.name }}</div>
</div>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon"><i class="fa fa-clipboard"></i></div>
<p>暂无工作模板</p>
<p class="subtext">敬请关注后续更新</p>
</div>
</div>
</div>
`,
data() {
return {
templateList: []
}
},
methods: {
listTemplate() {
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
if (res.code === 0) {
this.templateList = res.data
}
})
},
go(template) {
if (!template.url) {
this.$message.warning("管理员未配置模板链接")
return
}
const url = new URL(template.url, window.location.origin)
if (!url.searchParams.get("mode")) {
url.searchParams.set("mode", "edit")
}
if (!url.searchParams.get("id") && template.id) {
url.searchParams.set("id", template.id)
}
window.localStorage.setItem("zhgh_home_template_edit", JSON.stringify({
path: url.pathname,
id: url.searchParams.get("id"),
expiresAt: Date.now() + 60000
}))
window.open(url.href)
}
},
mounted() {
this.listTemplate()
},
style: /*language=CSS*/ `
.work-template-wrapper {
width: 80%;
margin: 20px auto 28px;
}
.work-template-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.work-template-title {
display: inline-flex;
align-items: baseline;
gap: 6px;
color: #19324d;
font-size: 18px;
font-weight: 700;
}
.work-template-title i {
color: #409eff;
font-size: 16px;
}
.work-template-title em {
color: #b5c0d6;
font-size: 14px;
font-style: normal;
font-weight: 500;
}
.work-template-card {
background: #ffffff;
border: 1px solid #edf1f7;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
min-height: 116px;
padding: 18px 20px;
}
.work-template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
justify-items: center;
justify-content: start;
gap: 12px 18px;
overflow-x: hidden;
}
.work-template-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 8px;
background: #fff;
width: 100%;
max-width: 100%;
padding: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.work-template-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.work-template-icon {
width: 55px;
height: 55px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.work-template-icon img {
width: 55px !important;
height: 55px !important;
max-width: none;
max-height: none;
object-fit: cover;
border-radius: 8px;
}
.work-template-name {
min-height: 34px;
font-size: 12px !important;
font-weight: 400;
color: #333;
line-height: 1.4;
word-break: break-word;
text-align: center;
}
.work-template-wrapper .no-activity-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #94a3b8;
text-align: center;
}
.work-template-wrapper .no-activity-placeholder .icon {
font-size: 44px;
margin-bottom: 12px;
opacity: 0.6;
}
.work-template-wrapper .no-activity-placeholder p {
margin: 4px 0 0;
font-size: 16px;
font-weight: 500;
}
.work-template-wrapper .no-activity-placeholder .subtext {
font-size: 13px;
opacity: 0.8;
}
@media (max-width: 768px) {
.work-template-grid {
grid-template-columns: repeat(3, minmax(72px, 1fr));
gap: 12px;
}
.work-template-item {
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.work-template-grid {
grid-template-columns: repeat(2, minmax(72px, 1fr));
gap: 12px;
}
}
`
};
@@ -0,0 +1,553 @@
const user = {
template: /*language=HTML*/ `
<div class="user-wrapper">
<!-- 用户容器:左右布局 -->
<div class="user-container">
<!-- 左侧:用户信息卡片 -->
<div class="user-info-card">
<div class="user-details">
<div class="user-name">${@auth.getPrincipalProperty('username')}</div>
<div class="user-meta-grid">
<div class="meta-row">
<span class="meta-icon">🆔</span>
<div>
<span class="meta-label">工号</span>
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
</div>
</div>
<div class="meta-row">
<span class="meta-icon">🏢</span>
<div>
<span class="meta-label">单位信息</span>
<span class="meta-value">
<!--#if(!isEmpty(@auth.getPrincipalProperty('unit'))){#-->
${@auth.getPrincipalProperty('unit').getName()}
<!--#}#-->
(
<!--#if(!isEmpty(@auth.getPrincipalProperty('union'))){#-->
${@auth.getPrincipalProperty('union').getName()}
<!--#}#-->
)
</span>
</div>
</div>
<div class="meta-row">
<span class="meta-icon">🎭</span>
<div>
<span class="meta-label">角色</span>
<span class="meta-value">{{ roleNames || '—' }}</span>
</div>
</div>
</div>
</div>
<!-- 天气区域 -->
<div v-if="weatherData" class="weather-section" :class="weatherData.bgClass">
<div style="display: flex; justify-content: space-around">
<div class="weather-header">
<span class="weather-location">{{ weatherData.name }}</span>
<span class="weather-text">{{ weatherData.text }}</span>
</div>
<div class="weather-main">
<span class="weather-emoji">{{ weatherData.emoji }}</span>
<span class="weather-temp">{{ weatherData.temp }}°</span>
</div>
</div>
<div class="weather-tip">{{ weatherData.tip }}</div>
</div>
<div v-else-if="weatherError" class="weather-error">
🌤️ 天气服务暂时不可用
</div>
</div>
<!-- 右侧:待办中心 -->
<div class="todo-center">
<!-- 标题和标签 -->
<div class="header">
<h3>待办中心</h3>
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
<el-tab-pane :label="tabLabel('我的发起', taskStats.startedCount)" name="started"></el-tab-pane>
<el-tab-pane :label="tabLabel('办结事务', taskStats.completedCount)" name="completed"></el-tab-pane>
</el-tabs>
</div>
<!-- 表格 -->
<el-table
:data="todoList"
style="width: 100%"
border
stripe
v-loading="loading"
:cell-style="{ padding: '8px' }"
:header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
>
<el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
<el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
<el-table-column prop="title" label="流程名称" min-width="300">
<template slot-scope="scope">
<el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
<span class="table-title">{{ scope.row.instanceName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="sender" label="申请人" width="120">
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
</el-table-column>
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="date" label="发起日期" width="120">
<template slot-scope="{row}">
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
</div>
</div>
`,
mixins: [initTableMixins],
data() {
return {
activeTab: 'todo',
todoList: [],
loading: false,
pageForm: {
pageNum: 1,
pageSize: 5,
totalCount: 0
},
taskStats: {
todoCount: 0,
doneCount: 0,
startedCount: 0,
completedCount: 0
},
// 🔆 天气相关数据
weatherData: null,
weatherError: false,
// 🔑 和风天气 API Key(请替换为你自己的)
QWEATHER_KEY: 'f916578cbb0e4b55b778b0de5847ecf5', // ←← 务必替换!
roleNames: ""
};
},
methods: {
getWeatherInfo(iconCode, temp) {
const info = {
emoji: '🌤️',
tip: '今日天气宜人,心情也该放晴啦!',
bgClass: 'weather-sunny' // 用于 CSS 类
};
const tempNum = parseFloat(temp);
const isCold = tempNum < 10;
const isHot = tempNum > 28;
// 按照和风天气 iconCode 分类
if (['100'].includes(iconCode)) {
info.emoji = '☀️';
info.tip = isHot ? '高温预警!注意防暑降温,多喝水~' : '阳光灿烂,适合外出走走!';
info.bgClass = 'weather-sunny';
} else if (['101', '102', '103', '104'].includes(iconCode)) {
info.emoji = ['101', '103'].includes(iconCode) ? '⛅' : '☁️';
info.tip = isCold ? '天凉加衣,别让感冒找上门!' : '云层温柔,适合静心工作。';
info.bgClass = 'weather-cloudy';
} else if (['300', '301', '302', '305', '306', '399'].some(code => code === iconCode)) {
info.emoji = '🌧️';
info.tip = '🌧️ 下雨啦!记得带伞,小心路滑~';
info.bgClass = 'weather-rainy';
} else if (['307', '308', '309', '310'].includes(iconCode)) {
info.emoji = '⛈️';
info.tip = '⛈️ 雷雨天气,请减少外出,注意安全!';
info.bgClass = 'weather-storm';
} else if (['400', '401', '402', '403'].includes(iconCode)) {
info.emoji = '❄️';
info.tip = '❄️ 天寒地冻,保暖最重要!';
info.bgClass = 'weather-snowy';
} else if (['404', '405', '406', '407', '408', '409', '410'].includes(iconCode)) {
info.emoji = '🌫️';
info.tip = '🌫️ 能见度较低,出行请注意安全。';
info.bgClass = 'weather-foggy';
} else if (['500', '501', '502', '504'].includes(iconCode)) {
info.emoji = '🌀';
info.tip = '🌀 极端天气预警!请关注官方通知。';
info.bgClass = 'weather-extreme';
} else {
info.emoji = '🌤️';
info.tip = '今日天气未知,但愿你心情晴朗!';
info.bgClass = 'weather-default';
}
return info;
},
async getConfigKey(key) {
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
return resp.data
},
// 🔆 获取天气
async fetchWeather() {
try {
// 存localStorage 112.13,32.01 广州 113.27,23.14
const AppMapCenterPointX = await this.getConfigKey("AppMapCenterPointX")
const AppMapCenterPointY = await this.getConfigKey("AppMapCenterPointY")
if (!AppMapCenterPointX || !AppMapCenterPointY){
return
}
// 1. 先通过城市名获取 locationId
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/geo/v2/city/lookup?location=" + AppMapCenterPointX + "," + AppMapCenterPointY + "&key=" + this.QWEATHER_KEY).then((geoRes) => {
if (geoRes.code !== '200' || !geoRes.location?.[0]) {
console.log('城市未找到')
}
const locationId = geoRes.location[0].id;
// 2. 获取当前天气
this.$axios.get("https://mx5rk62kvj.re.qweatherapi.com/v7/weather/now?location=" + locationId + "&key=" + this.QWEATHER_KEY).then((weatherRes) => {
if (weatherRes.code !== '200') {
console.warn('天气数据异常:', weatherRes);
}
const weatherInfo = this.getWeatherInfo(weatherRes.now.icon, weatherRes.now.temp);
this.weatherData = {
...weatherInfo,
temp: weatherRes.now.temp,
text: weatherRes.now.text,
name: geoRes.location[0].name
};
this.weatherError = false;
})
})
} catch (err) {
console.warn('天气加载失败:', err);
this.weatherError = true;
this.weatherData = null;
}
},
// 获取任务列表
tabLabel(title, count) {
return title + "(" + (count || 0) + ")"
},
async getStatistics() {
try {
const res = await $.post("/flow/todoCenter/statistics")
if (res.code === 0) {
this.taskStats = Object.assign({}, this.taskStats, res.data)
}
} catch (error) {
console.error("获取待办统计失败:", error)
}
},
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.todoList = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
} catch (error) {
this.$message.error("获取任务列表失败")
console.error("获取任务列表失败:", error)
} finally {
this.loading = false
}
},
pageData() {
this.getTasks()
this.getStatistics()
},
handleTabClick(tab) {
this.pageData()
console.log('切换到:', tab.name);
},
// 处理任务
async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
},
getRoleNames() {
$.post("/platform/sys/role/getRoleNames").then(res => {
if (res.code === 0) {
this.roleNames = res.data.roleNames
}
})
}
},
created() {
this.pageData()
this.getRoleNames()
},
mounted() {
this.fetchWeather()
},
style: /*language=CSS*/ `
.user-wrapper {
width: 100%;
background-color: #f5f7fa;
}
.user-container {
width: 80%;
margin: 12px auto 20px;
display: grid;
grid-template-columns: 0.7fr 2.3fr;
gap: 24px;
align-items: stretch;
row-gap: 24px;
}
.user-info-card {
background: white;
border-radius: 16px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.user-info-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
/* 头像占位符 */
.user-avatar-placeholder {
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, #409eff, #1877f2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 600;
margin: 0 auto;
box-shadow: 0 4px 10px rgba(24, 127, 255, 0.3);
}
/* 用户名 */
.user-name {
font-size: 20px;
font-weight: 700;
color: #1f2937;
text-align: center;
margin: 8px 0 0;
}
/* 元信息网格 */
.user-meta-grid {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 8px;
}
.meta-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.meta-icon {
font-size: 18px;
min-width: 24px;
text-align: center;
color: #409eff;
margin-top: 2px;
}
.meta-row > div {
flex: 1;
}
.meta-label {
font-size: 12px;
color: #6b7280;
font-weight: 500;
display: block;
margin-bottom: 2px;
}
.meta-value {
font-size: 15px;
color: #1f2937;
font-weight: 600;
word-break: break-word;
}
/* === 天气卡片 - 情感化设计 === */
.weather-section {
border-radius: 16px;
padding: 20px;
text-align: center;
color: white;
background: linear-gradient(135deg, #a8edea, #fed6e3); /* 默认柔和渐变 */
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
/* 不同天气的背景主题 */
.weather-sunny {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #2c3e50;
}
.weather-cloudy {
background: linear-gradient(135deg, #B0C4DE, #708090);
color: white;
}
.weather-rainy {
background: linear-gradient(135deg, #4A6FA5, #1E3F66);
color: white;
}
.weather-storm {
background: linear-gradient(135deg, #2F2F2F, #0D0D0D);
color: #FFD700;
}
.weather-snowy {
background: linear-gradient(135deg, #E0F7FA, #B3E5FC);
color: #01579B;
}
.weather-foggy {
background: linear-gradient(135deg, #D7CCC8, #A1887F);
color: white;
}
.weather-extreme {
background: linear-gradient(135deg, #FF5252, #B71C1C);
color: white;
}
.weather-default {
background: linear-gradient(135deg, #E0E0E0, #90A4AE);
color: #333;
}
/* 天气头部:城市 + 描述 */
.weather-header {
margin-bottom: 12px;
}
.weather-location {
font-weight: 700;
font-size: 16px;
display: block;
}
.weather-text {
font-size: 13px;
opacity: 0.9;
display: block;
margin-top: 4px;
}
/* 主体:图标 + 温度 */
.weather-main {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
position: relative;
bottom: 8px;
}
.weather-emoji {
font-size: 36px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
}
.weather-temp {
font-size: 28px;
font-weight: 800;
}
/* 贴心提示语 */
.weather-tip {
font-size: 14px;
line-height: 1.5;
opacity: 0.95;
margin-top: 8px;
padding: 0 8px;
}
/* 错误状态 */
.weather-error {
text-align: center;
color: #94a3b8;
font-size: 14px;
padding: 16px;
background: #f8fafc;
border-radius: 12px;
border: 1px dashed #cbd5e1;
}
/* 右侧待办中心 */
.todo-center {
min-width: 320px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 16px;
transition: transform 0.2s, box-shadow 0.2s;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header h3 {
font-size: 18px;
color: #333;
margin: 0;
border-left: 4px solid #1890ff;
padding-left: 10px;
font-weight: 600;
}
.el-tabs {
margin-top: 10px;
}
.table-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
display: inline-block;
}
.pagination {
margin-top: 20px;
text-align: right;
}
/* 响应式:小屏下堆叠 */
@media (max-width: 768px) {
.user-container {
flex-direction: column;
width: 95%;
}
.user-info-card, .todo-center {
min-width: 100%;
width: 100%;
}
}
`
};
@@ -0,0 +1,458 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
.message-center {
padding: 20px;
}
.statistics-section {
margin-bottom: 20px;
}
.stat-card {
border: none;
height: 100%;
cursor: pointer;
transition: all 0.3s ease;
}
.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;
}
.all-icon {
background-color: rgba(64, 158, 255, 0.1);
color: #409eff;
}
.unread-icon {
background-color: rgba(245, 108, 108, 0.1);
color: #f56c6c;
}
.read-icon {
background-color: rgba(103, 194, 58, 0.1);
color: #67c23a;
}
.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;
}
/* 消息列表区域样式 */
.message-section .el-card__header {
padding: 0;
border-bottom: none;
}
/* 优化后的 tabs 样式 */
.message-header .el-tabs__header {
margin: 0;
padding: 0 20px;
background-color: #fff;
}
.message-header .el-tabs__nav-wrap::after {
height: 1px;
background-color: #ebeef5;
}
.message-header .el-tabs__item {
height: 50px;
line-height: 50px;
font-size: 15px;
color: #606266;
padding: 0 20px;
transition: all 0.3s;
}
.message-header .el-tabs__item.is-active {
color: var(--color-primary);
font-weight: 500;
}
.message-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px;
}
.message-title {
font-weight: 500;
color: #303133;
}
.detail-content {
white-space: pre-line;
}
.detail-content ol {
list-style-type: decimal;
padding-left: 22px;
margin: 6px 0;
}
.detail-content li {
display: list-item;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 10px 0;
}
</style>
<div id="app" v-cloak>
<div class="message-center">
<!-- 统计数据区域 -->
<el-row :gutter="20" class="statistics-section">
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon all-icon">
<i class="el-icon-message"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.total}}</div>
<div class="stat-label">全部消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon unread-icon">
<i class="el-icon-bell"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.unread}}</div>
<div class="stat-label">未读消息</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon read-icon">
<i class="el-icon-finished"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{stats.read}}</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="filters" class="search-form">
<el-form-item>
<el-select v-model="filters.type" placeholder="消息类型" clearable @change="loadMessages"
style="width: 150px;">
<el-option label="全部类型" value=""></el-option>
<el-option label="系统公告" value="1"></el-option>
<el-option label="消息通知" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="pageForm.isRead == 0">
<el-button size="medium" type="primary" @click="markAllAsRead" :loading="loading">
<i class="el-icon-check"></i> 全部标记已读
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 消息列表区域 -->
<el-card shadow="never" class="message-section">
<div slot="header" class="message-header">
<el-tabs v-model="pageForm.isRead" @tab-click="handleTabClick">
<el-tab-pane label="全部消息" name="-1">
<span slot="label">
<i class="el-icon-message"></i>
全部消息
</span>
</el-tab-pane>
<el-tab-pane label="未读消息" name="0">
<span slot="label">
<i class="el-icon-bell"></i>
未读消息
</span>
</el-tab-pane>
<el-tab-pane label="已读消息" name="1">
<span slot="label">
<i class="el-icon-finished"></i>
已读消息
</span>
</el-tab-pane>
</el-tabs>
</div>
<!-- 表格展示 -->
<el-table v-loading="loading" :data="messages" style="width: 100%" :key="currentTab"
empty-text="暂无消息"
:header-cell-style="{backgroundColor: '#f5f7fa'}">
<el-table-column prop="type" label="消息类型" width="120">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.type === 1 ? 'danger' : 'primary'">
{{getMessageTypeName(scope.row.type)}}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span class="message-title">{{scope.row.title}}</span>
</template>
</el-table-column>
<el-table-column prop="content" label="内容预览" min-width="250" show-overflow-tooltip>
<template slot-scope="scope">
{{getContentPreview(scope.row.content)}}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="接收时间" width="180">
<template slot-scope="scope">
{{formatTime(scope.row.createdAt)}}
</template>
</el-table-column>
<el-table-column prop="isRead" label="状态" width="100">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.isRead ? 'success' : 'warning'">
{{scope.row.isRead ? '已读' : '未读'}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="viewMessage(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="messages.length > 0">
<el-pagination
background
@current-change="handlePageChange"
:current-page="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</div>
</el-card>
</div>
<!-- 消息详情弹窗 -->
<el-dialog
:visible.sync="detailVisible"
:title="currentMessage?.globalMessage?.title"
width="60%"
class="message-detail-dialog"
>
<div v-if="currentMessage.id">
<div style="text-align: center; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid #eee;">
<div style="font-size: 18px; font-weight: 500; margin-bottom: 8px;">{{currentMessage.title}}</div>
<div style="font-size: 14px; color: #666;">
<span>{{getMessageTypeName(currentMessage?.globalMessage?.type)}}</span>
<span style="margin: 0 8px;">•</span>
<span>{{formatTime(currentMessage.createdAt)}}</span>
</div>
</div>
<div class="detail-content" style="line-height: 1.8; font-size: 15px;" v-html="currentMessage?.globalMessage?.content"></div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: '#app',
data() {
return {
currentTab: 'all',
loading: false,
messages: [],
stats: {
total: 0,
unread: 0,
read: 0
},
filters: {
type: ''
},
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0
},
detailVisible: false,
currentMessage: {}
}
},
methods: {
async loadStats() {
try {
const resp = await this.$axios.get('/platform/v4/msg/stats')
if (resp.code === 0) {
this.stats = resp.data
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
async loadMessages() {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/pageData', this.pageForm)
if (resp.code === 0) {
this.messages = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.error(resp.msg)
}
} catch (error) {
this.$message.error('加载消息失败')
} finally {
this.loading = false
}
},
async viewMessage(message) {
this.$axios.post(`/platform/v4/msg/detail/` + message.id).then(async res => {
if (res.code === 0) {
this.currentMessage = res.data
this.detailVisible = true
// 如果是未读消息,标记为已读
if (!message.isRead) {
await this.markAsRead(message.id)
this.loadStats()
this.loadMessages()
}
}
})
},
async markAsRead(messageId) {
try {
await this.$axios.post(`/platform/v4/msg/read/` + messageId)
} catch (error) {
console.error('标记已读失败:', error)
}
},
async markAllAsRead() {
this.$confirm('确定要将所有未读消息标记为已读吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/read/all')
if (resp.code === 0) {
this.$message.success('操作成功')
this.loadMessages()
this.loadStats()
} else {
this.$message.error(resp.msg)
}
} catch (error) {
this.$message.error('操作失败')
} finally {
this.loading = false
}
})
},
handleTabClick(tab) {
this.pageForm.pageNumber = 1
this.loadMessages()
},
handlePageChange(pageNumber) {
this.pageForm.pageNumber = pageNumber
this.loadMessages()
},
getMessageTypeName(type) {
return type === 1 ? '系统公告' : '消息通知'
},
getContentPreview(content) {
if (!content) return ''
// 移除HTML标签
const text = content.replace(/<[^>]*>/g, '')
return text.length > 50 ? text.substring(0, 50) + '...' : text
},
formatTime(timestamp) {
return this.$moment(timestamp).format('YYYY-MM-DD HH:mm')
}
},
created() {
this.loadStats()
this.loadMessages()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,139 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<style>
#v4-person-center {
padding-top: 10px;
background: url(/assets/platform/img/v4/home-bg.png);
background-size: cover;
min-height: calc(100vh - 64px);
}
.v-person-center-container {
width: 1500px;
margin: 0 auto;
}
.v4-person-basic-info {
height: 250px;
background: url("/assets/platform/img/v4/person-center/img1.png");
background-size: cover;
padding: 20px 50px 30px;
display: flex;
}
.v4-person-basic-info .info_img {
width: 72px;
height: 100px;
margin-right: 40px;
flex-shrink: 0;
position: relative;
top: 25px;
}
.v4-person-basic-info .info_img img {
width: 100%;
height: 100%;
}
.v4-person-basic-info .info_grid {
flex: 1;
display: grid;
/*三行三列*/
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.v4-person-basic-info .info_grid .info-item {
display: flex;
align-items: center;
padding: 8px 12px;
transition: all 0.3s ease;
}
.v4-person-basic-info .info_grid .info-label {
font-weight: 600;
color: #333;
min-width: 80px;
font-size: 14px;
}
.v4-person-basic-info .info_grid .info-value {
color: #666;
font-size: 14px;
flex: 1;
}
</style>
<div id="v4-person-center">
<div class="v-person-center-container">
<!--个人基本信息-->
<div class="v4-person-basic-info">
<div class="info_img">
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAADICAYAAAAHm2giAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA1TSURBVHgB7Z3bchtZFYa3zifLY9mJM/EkEJJMgEkxVFFUDZe545oHoCpvMjwCbwAXvMXcwBVFFUPBVIWZhEnI0Tk4siW1pNZZ7L+tNrYi2XJvdffa8v+lFCtxnNLh01prr71778R4PL7hDvt/eNVx7nWGQ0XIWeRTKXW7tPlHffd3af3bl3968eBefdBVhCzKldya+u31u79Pf9/avw95fnPjJ2otnVWEnMYbt6m+evVEvek07+k//jPppy3KQxZhypNaUhFiAAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYgQFIkZQIGIEBSJGUCBiBAUiRlAgYkRaETUYj9RgNFLucKBvfe/v3MHg6PuFdFqlEymVSSZVWt/K3FP7iAspkDPoqf2Oq5r42nNV55gsi1LO5FRei7WdK6pyNndhpbowAh30Omq37ah3bsuLOKY4/a5329P/Hyik0qqSK6idYllVsnl1UVhpgYbjsXrbbnrStAd91Rr1VVh46U8LCkkh081yxRMK91eZlXx2veHQeyOrOk355PQbmUgo1dJv9Fj/ChPI9KC2591HRIJMqyrSSj0rRJwXzfoJcY6TTaZVMpH0aqCwJfLZnUQlSASZVk2klXk2SFWv9Q0SnUZaC1TSb2JzGF46m8UT50A/vv+LtCpYLxDS1VOnpgva3sI/g0hU0p6FWRPNwk9tKOhXJa1Z/Qzed9rqZbNxZtSZBWqioU5jndH5h/CmIKUddF312cZltakLbZuxthONWueZUw8kj08xlVG5ZErFAaLR19XX6rFObTZjnUAQ5mmjpofmbbUMSqmsVxfFBWojmyWySiDI87BWVdWuq5ZJSUeipEqouIBEDw7eKRuxRiBfHnew/MI35Y3MMipOdt2mlRJZI1BY8vhkdC0UVz3kA4keNqrKJqwQCAVzmPL4oB5KJeJLZeC5fq421UTiBdptOUsrmBcBEsUNaqJ3nZayAdECuf2+112OEozI8sn422NoOGKoLx2xAqFo/j6mUF7AnJmKN5VhgZsNRbVYgVD3YJoiDhK6DioIiEKY8njWqivJiBQIa3fmzahHBaY64mww+qAekpzKRAr0uCFjFCIhCklPZeIEquoJ0rhS1zToDUmIQkhl+914I/I8xAm0G/Go6ywkRCHwRGhvSJRAkqKPD6PQ6YgS6K0rs3mWS8Q7xeHzvN1Q0hAjEEZe7kDmaCOro1Ai5r4QwCK0ZVyStEzECPROaPQB6AvFPdEKMCJ71XaUJMQIdJ41zXGQEVAHgT1hHzQRrwrkkVY8TyOpmJaUxkQIVBPa45hGgkBAUroX8YpILZ6nkTBLDxCFpCAmhdlAUhfTyYSM0ZgUYhfIFnl8sgLSGCZXpdRBsb8abcsEklIHNXpdJYHYX43eSPboa5qMkK50R8gSDwp0ThJC6iApa4RiF2g4imablWWSETD2YASaIG1uZxFSAuqgvpDIzQgUgGT8GcybF5MA94kOQFpIIS0BChQAFNESlndIgAIFRMBATAQUKCBxX3iIHfMlEPujyKXsrCdSMQuUEbDADcQuUFbIC2EbeSEbdMb+KLIRR6DNTF5tZQqqPeyrV93glxChkEZD8VZxw/sQvHAb6mAQ3fwUzuqQQOwCRZnCbus3+05p8+jP13pl9bf6axUELHH99aUfqbXJISufl7fV32uv1XftfRUFaSFVfOwpbC2iTxIuEDwuD9jKFryIFITrhfUjeXw+X99WUbGelRGBRBTRUewKNm9T72Iy2N6IlfSH4iGVRbFeCB86KctKRDyKQjr8DS7dORuKBz3na97P9SKY29vMyTlOSoRAGxGE43mr+K7ly+f+NCMdIv1Nc9CPZq1yJStnd3sZAkW03X+9/+EoCcdYflbaUosC2b7Y2Jn5vagEYgSaAnVQFP2g/7Rn73BxrVBWv1i/cuZOHPj+Fx9dVcU59dQ3Tvj7+FzOF8XUP0DMYStb+ULoG2ru6whR7bkz08/HuZJa13XNfq+jXnadEyv+UO98nC15os3jSbsWyRFSl/MlJQkxAl0plCLZkfXbVlX9KrMz81OMw1eKhcyposyipcWJIvqg+/yJsLPGxMTClK5F1jLh79HcGPTUg+Z7tSz6ujD/y/7zSKKPpOLZR9Rs/E5xTUXBq05TfV1/Y7wwHZHnq/f/9VJjFNwqV5Q0RB04h/PXEYWaEVwr9rbXVo36rvq0WPGG8uflu2bVS1u9iNZ0X9VptZCWdz6guEeEKPSoHs18EiLQN86eF5E+ya95hfRpIxz0kV64jnrZcdSzTrT7N0uMPkCcQFFGIZ9q3/VukAkz9RiNHRcJXWzUTo3JbHvUl9TcXNsQGX2AyEd1o/yR+vbgvdFxlkHxZTqNwTi6S2ow8vqhFkgqIpe0Ypf4qxEV1EEYqujExunOUpavzkLsI7uiBYpiWH9exjoqRhUZf1BaF9f3mUb0onqksrgPgJumH1H6Quq6Vd5U0hEtEFLZrXVZo49+BMN2FPC/3NoRnbp8xD9CjMquleSE8SgEulu5LHbUNY0V14WhHpJQVI+0PKOQ658765tqW9iE6WlYc2Hhjo5CcUvUC3lDA/R7JA/ZZ2HVlalxS9Qdh9dAhDy31uUXzdPYkWiPsTOph6I+jBfpK4zhOwrmH69vHT0v27BOIIAXu6iLzKdOPbKezLxF+SZgqP7zyhUxl+gEwdrNFbCO+qeVS5FdGr3s0Vclm/eG6jbLA6yMQD7oE/1sa1vttpxQU1pXR59ljb6Qsm6W7SuW52G1QD5IaVhT/ai2H8qur8tKX4g6dze2renxLMLKPBM/Gr132140WpZI/ZF57wfiYFJ0MydvSaopq/NRmHCpUPRuyxLJHQVf67zK4visnEA+vkhOr6ved9xAB9Yi+px3G2LUODt6Bh3Xb62yOD4rK5AP5tJwuz5eV7VOR9V6HW+14yLD/9ZosVWRkAbTD2hyrmPjAwsmQZfFygvkgzfZj0oAh7zgpKCujjKud+Bv/4RU80Ze6N3gQkNsCIH1SpvZwkoVxeflwj7zon7zizMWrHUn652x4B4CQTx/P8KLLMo8+IpMkZtc954TsgehdLjNLzGCAhEjKBAxIvZE35Vy4JwumG07N0jCXtGRPAIMj2vdjmrroXJvOPSGzIOQ1tcEAWt9GsNe6MtVwwCjRLQU8unD9gL2j8YOZlFtQhWaQOixoAu8r7vAYqLMHLDLho3yAHwQnX7Xu+2p1tHfV3QX/HKuqLYLpbk71C6Dpf7PiChVt6W7vV1rjvPGjvX9sX2nJp4FzpbH7VGj6sm0U1jzpliWzVIEgjhv9cTlOy2PlLS0CJCnM5Jx9miY+DI9cQ68yd1limQsEMTBrLdN4gB0nC+CPMdBd/1BbW+pIgUWCMXwU6dmTao6DuqG1ij8Lemk4ot0oCeWIZJJjRToJ22NOsArOof2SR8Gu23HS213PtoKfDHjucd6L5p19bLlWC3P2NIRVxggGv1r/6167BwE+vmFIxCEeVzftzJlAdQ8FzltnQXqInzAcI3aeVgoAkGeh7WqtfJgtEV5zua5zi4PDs633/WZEciXB91jG2nrlNUR3siUxK57eHnU3cpiZ5+dGYGeNg6slAfTE86gS3kCAIke6gbkIpwqEC7YQ1fZNgaTua1V7DBHBdLZs9bZWxnPFajaaUe+gcEyQMrCdry2zm1J4lG9eubVLDMFQpNw1zJ5EHXqgw5T1pL5t244nnZp00yB0OuBRDaAno4fdWzsTUkHfaLTekQfCITUZUvdgx3ja0MWymGDemheKvtAIBtS12Aywmrr3g67ytHwZE4UOtEHQvSRnLogjrui63ekg4lXRKHp/ZhORCCJ0QcRBssuarpARp1DeeJjVhQ6ikDSog+iTU+L09VfmaZk4EWh3sla6JhA59+9YtlQGvns6UBzHE+gvh7F7HXb3kr+KI+URrOvNx6q4UiLoyiNDbxxD9eBJdXhGSaeQI1+15ux9sloiXDISXIiFP5x0uDQE4iBygXnbA0nl/MM1JjCWAg+9F3tSiF1uDGFJ1BzcHKZBgrVvvfenqyJIFFqYp5v4CzG3m3snavl3acoKwWW9RwJhIjQXnC2feRFEspw0XF0wPEXeyS7lkxZEDmgZu5Puv/JHvsqJADtyYUJ3J2DBKI/PAw8FIgEwt98nQKRQAzHjEDEgKMiWhESAEYgshQoEDGCAhEjKBAxggIRIygQMYICESMoEDGCAhEjKBAxggIRIygQMYICESMoEDGCAhEjKBAxggIRIygQMYICESMoEDGCAhEjKBAxggIRIygQMYICESMoEDGCAhEjKBAxggIRIygQMYICESMoEDGCAhEjKBAxggIRIygQMSJZSh0eGYYDbAk5i8Fkd9bRWLX0l4307bWtP29lC/f++u6FImRRNjPFff2llhiPxzf0nS//UXtzv80oRBagqLPWpzrwlNOZ+/8D/6fmMqwYxpEAAAAASUVORK5CYII=">
</div>
<div class="info_grid">
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">${@auth.getPrincipalProperty('username')}</span>
</div>
<div class="info-item">
<span class="info-label">籍贯:</span>
<span class="info-value">${@auth.getPrincipalProperty('nativePlace')}</span>
</div>
<div class="info-item">
<span class="info-label">出生日期:</span>
<span class="info-value">${@auth.getPrincipalProperty('birthday')}</span>
</div>
<div class="info-item">
<span class="info-label">性别:</span>
<span class="info-value">${@auth.getPrincipalProperty('sex')}</span>
</div>
<div class="info-item">
<span class="info-label">年龄:</span>
<span class="info-value">46</span>
</div>
<div class="info-item">
<span class="info-label">政治面貌:</span>
<span class="info-value">${@auth.getPrincipalProperty('political')}</span>
</div>
<div class="info-item">
<span class="info-label">民族:</span>
<span class="info-value">${@auth.getPrincipalProperty('nation')}</span>
</div>
<div class="info-item">
<span class="info-label">身份证:</span>
<span class="info-value">${@auth.getPrincipalProperty('idCard')}</span>
</div>
<div class="info-item">
<span class="info-label">电话:</span>
<span class="info-value">${@auth.getPrincipalProperty('mobile')}</span>
</div>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#v4-person-center",
data() {
return {
}
},
mounted() {
},
methods: {
}
})
</script>
<!--#
}
#-->
@@ -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 nonce="${cspNonce!}">
// 处理导航项的active状态
$(document).ready(function() {
// 初始化:根据当前URL设置active状态
function setActiveNavItem() {
var currentPath = getBaseSubAppPath();
$('.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,606 @@
<!--#
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 10px 0;
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.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: 10px;
display: flex;
align-items: flex-end;
}
.apps-filter-title {
color: #666;
font-size: 14px;
margin-bottom: 5px;
margin-right: 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: 5px 0;
}
.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="/assets/platform/img/v4/lczx.png" 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)"
>
<img v-if="category.picIcon" :src="category.picIcon" :alt="category.name || ''" style="width: 20px"/>
<i v-else :class="category.icon || 'fa fa-th-large'"></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]">
<img v-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.displayName }}</div>
</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 nonce="${cspNonce!}">
new Vue({
el: "#app",
data: {
// 应用列表
applications: [],
// 分类列表
categories: [{id: "all", name: "全部服务", icon: "fa fa-th-large"}],
// 动态加载的分类
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/serv/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
}
// 发送请求获取普通应用列表
$.get("/platform/v4/serv/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/serv/favorite", {platform: 'PC'})
.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/serv/removeFavorite" : "/platform/v4/serv/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) {
console.log(app)
if (!app.instanceUrl) {
this.$message.warning("管理员未配置服务地址")
return
}
window.open(app.instanceUrl, "_blank")
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,430 @@
<!--#
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;
}
/* 悬浮菜单按钮样式 */
.menu-toggle-btn {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgb(64 109 157);
color: white;
text-align: center;
line-height: 50px;
font-size: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
cursor: pointer;
transition: all 0.3s ease;
}
.menu-toggle-btn:hover {
background-color: rgb(54 99 147);
transform: scale(1.05);
}
.menu-toggle-btn.active {
background-color: #d9534f;
}
/* 菜单遮罩层样式 */
.menu-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
}
@media (max-width: 768px) {
/*.sub-app-container {*/
/* !* flex-direction: column; *!*/
/*}*/
.sidebar-menu {
position: fixed;
top: 0;
left: -100%;
width: 80%;
height: 100vh;
z-index: 999;
transition: left 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
overflow-y: auto;
}
.sidebar-menu.show {
left: 0;
}
.menu-toggle-btn {
display: block;
}
.menu-overlay.show {
display: block;
}
.main-content {
padding: 20px;
width: 100%;
}
}
</style>
<div class="sub-app-container" id="sub-app-container">
<!-- 菜单遮罩层 -->
<div class="menu-overlay" id="menu-overlay"></div>
<!-- 左侧菜单区域 -->
<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>
<!-- 悬浮菜单按钮 -->
<div class="menu-toggle-btn" id="menu-toggle-btn">
<i class="fa fa-bars"></i>
</div>
<!-- 右侧主体区域 -->
<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 nonce="${cspNonce!}">
document.addEventListener("DOMContentLoaded", function () {
const menuLinks = document.querySelectorAll(".menu-link")
const contentTitle = document.querySelector(".content-title")
const menuToggleBtn = document.getElementById("menu-toggle-btn")
const sidebarMenu = document.querySelector(".sidebar-menu")
const menuOverlay = document.getElementById("menu-overlay")
const isMobile = window.innerWidth <= 768
// 移动设备下点击菜单项后自动收起菜单
function toggleMobileMenu(show) {
if (show) {
sidebarMenu.classList.add("show")
menuOverlay.classList.add("show")
menuToggleBtn.classList.add("active")
menuToggleBtn.innerHTML = '<i class="fa fa-times"></i>'
} else {
sidebarMenu.classList.remove("show")
menuOverlay.classList.remove("show")
menuToggleBtn.classList.remove("active")
menuToggleBtn.innerHTML = '<i class="fa fa-bars"></i>'
}
}
// 初始化菜单按钮状态
if (window.innerWidth <= 768) {
menuToggleBtn.style.display = "block"
} else {
menuToggleBtn.style.display = "none"
}
menuLinks.forEach((link) => {
link.addEventListener("click", function (e) {
// 检查是否有 data-pjax 属性
const pjax = this.hasAttribute("data-pjax")
if (pjax) {
// 如果有 pjax 属性,不阻止默认行为,让 pjax 处理
// 但仍然处理菜单样式更新
// 在移动设备上,点击菜单项后自动收起菜单
if (window.innerWidth <= 768) {
toggleMobileMenu(false)
}
} 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")
})
})
// 菜单切换按钮点击事件
menuToggleBtn.addEventListener("click", function() {
const isMenuShown = sidebarMenu.classList.contains("show")
toggleMobileMenu(!isMenuShown)
})
// 遮罩层点击事件,点击遮罩层关闭菜单
menuOverlay.addEventListener("click", function() {
toggleMobileMenu(false)
})
// 窗口大小变化时处理菜单状态
window.addEventListener("resize", function() {
if (window.innerWidth <= 768) {
menuToggleBtn.style.display = "block"
// 如果从大屏幕变为小屏幕,默认收起菜单
if (!isMobile) {
toggleMobileMenu(false)
}
} else {
menuToggleBtn.style.display = "none"
// 大屏幕下移除所有移动端相关类
sidebarMenu.classList.remove("show")
menuOverlay.classList.remove("show")
menuToggleBtn.classList.remove("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,445 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<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;
}
</style>
<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="pageForm" class="search-form">
<el-form-item>
<el-input v-model="pageForm.searchKeyword" placeholder="请输入关键词搜索"
prefix-icon="el-icon-search"
clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="pageForm.category" placeholder="所有流程分类" clearable>
<el-option v-for="type in categoryOptions" :key="type.id" :label="type.name"
:value="type.id"></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'}">
<el-table-column prop="processInstanceName" label="流程名称" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span class="task-title">{{scope.row.variable?.instanceName}}</span>
</template>
</el-table-column>
<el-table-column prop="taskName" label="任务节点" min-width="150"
show-overflow-tooltip></el-table-column>
<el-table-column prop="categoryName" label="流程分类" min-width="150" show-overflow-tooltip></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="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(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
background
@current-change="handleCurrentChange"
:current-page="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</div>
</el-card>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 查询表单
pageForm: {
pageNumber: 1,
pageSize: 10,
todoCount: 0,
keyword: "",
category: ""
},
// 流程类型选项
categoryOptions: [],
// 任务列表
tasks: [],
loading: false,
// 标签页
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
}
}
},
created() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.listCategory(), 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 listCategory() {
this.$axios.post("/flow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.tasks = res.data.list
this.pageForm.totalCount = 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.pageForm.pageNumber = 1
this.getTasks()
},
// 搜索
search() {
this.pageForm.pageNumber = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.pageForm.searchKeyword = ""
this.pageForm.category = ""
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.pageForm.pageNumber = val
this.getTasks()
},
// 处理任务
async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,193 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker placeholder="请选择年度" type="year" v-model="pageForm.year" value-format="yyyy"></el-date-picker>
</search-item>
<search-item label="活动主题">
<el-input clearable placeholder="请输入活动主题" v-model="pageForm.searchKeyword"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<template slot="tool">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openAdd">新增</el-button>
</template>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" size="small" header-align="center" border style="width: 100%" row-key="id">
<el-table-column label="序号" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="版本" prop="versionCode"></el-table-column>
<el-table-column label="图标" prop="icon"></el-table-column>
<el-table-column label="分类" prop="category"></el-table-column>
<el-table-column label="排序" prop="sortCode"></el-table-column>
<el-table-column prop="modelStatus" label="开启状态">
<template scope="scope">
<el-switch
v-model="scope.row.modelStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="(val)=>{enableChange(val,scope.row.id)}"
></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="250">
<template slot-scope="scope">
<el-link size="mini" type="primary" @click="openEdit(scope.row.id)">编辑</el-link>
<el-link size="mini" type="primary" @click="design(scope.row.id)">设计</el-link>
<el-link size="mini" type="success" @click="deploy(scope.row.id)">部署</el-link>
<!-- <el-link size="mini" type="info" @click="doDelete(scope.row.id)">降级</el-link>-->
<el-link size="mini" type="danger" @click="doDelete(scope.row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
</el-card>
<el-dialog :title="formData.id ? '编辑':'新增'" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="formRef" size="small" label-width="120px" :rules="formRules">
<el-form-item label="名称" prop="name">
<el-input placeholder="请输入名称" v-model="formData.name" clearable></el-input>
</el-form-item>
<el-form-item label="模型分类" prop="category">
<el-select placeholder="请选择分类" v-model="formData.category" clearable>
<el-option v-for="item in categoryOptions" :key="item.id" :value="item.id" :label="item.name"></el-option>
</el-select>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input placeholder="请输入图标" v-model="formData.icon" clearable></el-input>
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-input placeholder="请输入颜色" v-model="formData.color" clearable></el-input>
</el-form-item>
<el-form-item label="序号" prop="sortCode">
<el-input-number placeholder="请输入序号" v-model="formData.sortCode" clearable></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogFormVisible: false,
formRules: {
name: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
category: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
},
categoryOptions: []
}
},
methods: {
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {
subjectTypes: []
}
})
},
openEdit(id) {
this.dialogFormVisible = true
$.get(loc() + "/findOne", { id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
},
doSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
$.post(loc() + (this.formData.id ? "/update" : "/insert"), { data: JSON.stringify(this.formData) }).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
}
})
}
})
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
$.post(loc() + "/delete", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
deploy(id) {
this.$confirm("您确定要部署吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
$.post(loc() + "/deploy", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
enableChange(enable, id) {
$.post(loc() + "/enableChange", { id, enable }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
design(id) {
window.open(loc() + "/modeler?id=" + id)
},
listCategory() {
$.get(loc() + "/listCategory").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,152 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
#app {
position: relative;
}
#canvas {
padding: 0;
margin: 0;
height: calc(100vh - 126px);
width: 100%;
}
#properties {
background: #fff;
border: 1px solid;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 400px;
}
#top-tool {
position: absolute;
top: 0;
}
.djs-container {
background-image: linear-gradient(90deg, hsla(0, 0%, 78.4%, 0.15) 10%, transparent 0),
linear-gradient(hsla(0, 0%, 78.4%, 0.15) 10%, transparent 0);
background-size: 10px 10px;
}
</style>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/bpmn/assets/diagram-js.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/bpmn/assets/bpmn-font/css/bpmn-embedded.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/bpmn/assets/bpmn-js.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/bpmn/assets/bpmn-font/css/bpmn.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/bpmn-js-properties-panel/properties-panel.css" />
<link rel="stylesheet" href="https://unpkg.com/@bpmn-io/properties-panel@3.23.0/dist/assets/properties-panel.css" />
<div id="app" v-cloak>
<!-- <el-dialog title="流程设计" width="100%" :visible.sync="dialogVisible">-->
<div id="canvas"></div>
<div id="properties"></div>
<div id="top-tool">
<el-button size="mini" @click="saveData">保存</el-button>
<el-button size="mini" @click="openXML">查看xml</el-button>
</div>
<!-- </el-dialog>-->
<el-dialog title="查看xml" width="50%" :visible.sync="xmlDialogVisible">{{xmlContent}}</el-dialog>
</div>
<script src="${base!}/assets/platform/plugins/bpmn/js/bpmn-modeler.development.js"></script>
<script src="${base!}/assets/platform/plugins/bpmn-js-properties-panel/bpmn-js-properties-panel.umd.js"></script>
<script src="${base!}/assets/platform/plugins/bpmn-translate/index.js"></script>
<script src="${base!}/assets/platform/plugins/bpmn-translate/customTranslate.js"></script>
<script src="${base!}/assets/platform/plugins/camunda-bpmn-moddle/resources/camunda.js"></script>
<!--<script src="https://unpkg.com/bpmn-js@17.9.0/dist/bpmn-modeler.development.js"></script>-->
<script nonce="${cspNonce!}">
var bpmnModeler
new Vue({
el: "#app",
data() {
return {
modelId: GetQueryString("id"),
xmlDialogVisible: false,
xmlContent: null
}
},
methods: {
saveData() {
bpmnModeler.saveXML().then((res) => {
console.log(res)
$.post("/platform/bpm/cfg/design/saveXml", {
id: this.modelId,
xml: res.xml
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
}
})
})
},
openXML() {
bpmnModeler.saveXML().then((res) => {
console.log(res)
this.xmlDialogVisible = true
this.xmlContent = res.xml
})
},
loadXML() {
$.get("/platform/bpm/cfg/design/loadXml", { id: this.modelId }).then((res) => {
if (res.code === 0) {
if (res.data) {
bpmnModeler
.importXML(res.data)
.then(() => {
console.log(1)
})
.catch((err) => {
console.log(err)
})
} else {
bpmnModeler.createDiagram()
}
}
})
},
initModeler() {
bpmnModeler = new BpmnJS({
container: "#canvas",
propertiesPanel: {
parent: "#properties"
},
additionalModules: [
BpmnJSPropertiesPanel.BpmnPropertiesPanelModule,
BpmnJSPropertiesPanel.BpmnPropertiesProviderModule,
BpmnJSPropertiesPanel.CamundaPlatformPropertiesProviderModule,
BpmnJSPropertiesPanel.CamundaPlatformTooltipProvider,
{
translate: ["value", customTranslate]
}
],
moddleExtensions: {
camunda: camundaJson
}
})
}
},
mounted() {
this.initModeler()
this.loadXML()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,369 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.flowchart-example-container {
width: 800px;
height: 400px;
background: white;
border: 1px solid #bbb;
margin-bottom: 10px;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称">
<el-input placeholder="请输入名称" v-model="pageForm.displayName"></el-input>
</search-item>
<search-item label="编码">
<el-input placeholder="请输入编码" v-model="pageForm.name"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button size="mini" icon="el-icon-plus" type="primary" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" border style="width: 100%" row-key="id">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="编码" prop="code"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-link size="mini" type="primary" @click="$refs.processViewerRef.onOpen(scope.row)">查看</el-link>
<el-link size="mini" type="primary" @click="openEdit(scope.row)">编辑</el-link>
<el-link size="mini" type="primary" @click="openNodeConfig(scope.row.id)">配置</el-link>
<el-link size="mini" type="danger" @click="doDelete(scope.row.id)">删除</el-link>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="流程定义" width="30%" :close-on-click-modal="false" :visible.sync="dialogVisible">
<el-form :model="formData" ref="form" label-width="80px">
<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="code" :rules="{required: true, message: '编码不能为空', trigger: 'blur'}">
<el-input v-model="formData.code" placeholder="编码"></el-input>
</el-form-item>
<el-form-item label="URL" prop="url" :rules="{required: true, message: 'URL不能为空', trigger: 'blur'}">
<el-input v-model="formData.url" placeholder="URL"></el-input>
</el-form-item>
<el-form-item label="H5URL" prop="h5url" :rules="{required: false, message: 'H5URL不能为空', trigger: 'blur'}">
<el-input v-model="formData.h5url" placeholder="H5URL"></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="doSubmit">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="节点配置" width="80%" :close-on-click-modal="false" :visible.sync="nodeDialogVisible">
<el-form :model="nodeFormData" ref="nodeForm" label-width="120px" size="small">
<el-form-item label="节点配置" prop="nodeDefine">
<el-table :data="nodeFormData.nodes">
<el-table-column label="名称" prop="nodeName" fixed="left" min-width="180">
<template slot-scope="{row}">
<el-input v-model="row.nodeName" max="20" clearable></el-input>
</template>
</el-table-column>
<el-table-column label="编码" prop="nodeCode" width="100px">
<template slot-scope="{row}">
<el-input v-model="row.nodeCode" max="20" clearable></el-input>
</template>
</el-table-column>
<el-table-column label="节点类型" prop="nodeType" width="100px">
<template slot-scope="{row}">
<el-select v-model="row.nodeType" clearable>
<el-option value="START" label="开始"></el-option>
<el-option value="TASK" label="任务"></el-option>
<el-option value="BACK" label="退回"></el-option>
<el-option value="END" label="结束"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="完成类型" prop="nodeApprovalType" width="130px">
<template slot-scope="{row}">
<el-select v-model="row.nodeApprovalType" clearable>
<el-option value="NODE_APPLY" label="申请" v-if="row.nodeType==='START'"></el-option>
<el-option value="NODE_PENDING" label="待审批" v-if="row.nodeType==='TASK'"></el-option>
<el-option value="NODE_PASS" label="审批通过" v-if="row.nodeType==='END'"></el-option>
<el-option value="NODE_REJECT" label="审批拒绝" v-if="row.nodeType==='END'"></el-option>
<el-option value="NODE_BACK" label="审批退回" v-if="row.nodeType==='BACK'"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="参与类型" prop="nodePerformType" width="100px">
<template slot-scope="{row}">
<el-select v-model="row.nodePerformType" clearable v-if="['TASK','START','BACK'].includes(row.nodeType)">
<el-option value="ORDINARY" label="普通"></el-option>
<el-option value="COUNTERSIGN" label="会签" v-if="row.nodeType==='TASK'"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="通过下个节点" prop="passNextNodeId">
<template slot-scope="{row}">
<el-select v-model="row.passNextNodeId" clearable v-if="['TASK','START'].includes(row.nodeType)">
<el-option :value="item.id" :label="item.nodeName" :key="item.id" v-for="item in nodeFormData.nodes"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="拒绝下个节点" prop="rejectNextNodeId">
<template slot-scope="{row}">
<el-select v-model="row.rejectNextNodeId" clearable v-if="row.nodeType==='TASK'" :disabled="row.nodeType!=='TASK'">
<el-option :value="item.id" :label="item.nodeName" :key="item.id" v-for="item in nodeFormData.nodes"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="退回下个节点" prop="backNextNodeId">
<template slot-scope="{row}">
<el-select v-model="row.backNextNodeId" clearable v-if="row.nodeType==='TASK'" :disabled="row.nodeType!=='TASK'">
<el-option :value="item.id" :label="item.nodeName" :key="item.id" v-for="item in nodeFormData.nodes"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="动态决策表达式" prop="dynamicDecisions">
<template slot-scope="{row}">
<el-link type="primary" @click="openDynamicDecision(row)">点击配置</el-link>
</template>
</el-table-column>
<el-table-column label="更多配置" prop="moreConfig">
<template slot-scope="{row}">
<el-link type="primary" @click="openMoreConfig(row)">更多配置</el-link>
</template>
</el-table-column>
<el-table-column width="140px" fixed="right">
<template #header>
<el-button size="mini" icon="el-icon-plus" type="primary" @click="addNewNode">添加</el-button>
</template>
<template slot-scope="scope">
<el-link size="mini" type="danger" @click="nodeFormData.nodes.splice(scope.$index,1)">删除</el-link>
<el-link size="mini" type="primary" @click="upNode(scope.$index)" :disabled="scope.$index===0">上移</el-link>
<el-link
size="mini"
type="primary"
@click="downNode(scope.$index)"
:disabled="scope.$index===nodeFormData.nodes.length-1"
>
下移
</el-link>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
<!-- <el-button type="primary" @click="refreshNode">刷新</el-button>-->
<div slot="footer">
<el-button @click="nodeDialogVisible=false">取消</el-button>
<el-button type="primary" @click="doSubmitNodes">确定</el-button>
</div>
</el-dialog>
<el-dialog :title="dynamicDecisionNodeTitle" width="50%" :close-on-click-modal="false" :visible.sync="dynamicDecisionNodeDialogVisible">
<el-form :model="dynamicDecisionNodeFormData" ref="dynamicDecisionNodeForm" label-width="80px">
<el-table :data="dynamicDecisionNodeFormData.dynamicDecisions">
<el-table-column label="条件" prop="condition">
<template slot-scope="{row}">
<el-input v-model="row.condition" clearable></el-input>
</template>
</el-table-column>
<el-table-column label="说明" prop="desc">
<template slot-scope="{row}">
<el-input v-model="row.desc" clearable></el-input>
</template>
</el-table-column>
<el-table-column label="下个节点" prop="toNodeId">
<template slot-scope="{row}">
<el-select v-model="row.toNodeId" clearable>
<el-option :value="item.id" :label="item.nodeName" :key="item.id" v-for="item in nodeFormData.nodes"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column>
<template #header>
<el-button size="mini" icon="el-icon-plus" type="primary" @click="dynamicDecisionNodeFormData.dynamicDecisions.push({})">
添加
</el-button>
</template>
<template slot-scope="scope">
<el-link size="mini" type="danger" @click="dynamicDecisionNodeFormData.dynamicDecisions.splice(scope.$index,1)">删除</el-link>
</template>
</el-table-column>
</el-table>
</el-form>
</el-dialog>
<el-dialog title="更多配置" width="50%" :close-on-click-modal="false" :visible.sync="moreConfigDialogVisible">
<el-form :model="moreConfigFormData" label-width="150px">
<el-form-item label="电脑端审核地址" prop="url" :rules="[{required:true,message:'请输入电脑端审核地址',trigger:['blur','change']}]">
<el-input v-model="moreConfigFormData.url" clearable placeholder="请输入电脑端审核地址"></el-input>
</el-form-item>
<el-form-item label="移动端审核地址">
<el-input v-model="moreConfigFormData.h5url" clearable placeholder="请输入移动端审核地址"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="moreConfigDialogVisible=false">取消</el-button>
<el-button type="primary" @click="doSubmitMoreConfig">确定</el-button>
</div>
</el-dialog>
<!-- <div id="chart_container">-->
<!-- <div class="flowchart-example-container" id="flowchartworkspace"></div>-->
<!-- </div>-->
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
nodeApprovalTypeOptions: [],
formData: {},
nodeDialogVisible: false,
nodeFormData: {
id: null,
nodes: []
},
dynamicDecisionNodeDialogVisible: false,
dynamicDecisionNodeFormData: {},
moreConfigDialogVisible: false,
moreConfigFormData: {}
}
},
computed: {
dynamicDecisionNodeTitle() {
return this.dynamicDecisionNodeFormData.nodeName
}
},
methods: {
openAdd() {
this.dialogVisible = true
this.formData = {}
},
openEdit(row) {
this.dialogVisible = true
this.formData = { ...row }
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.$axios
.post("/platform/bpm/processDefine/" + (this.formData.id ? "update" : "insert"), {
define: JSON.stringify(this.formData)
})
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.doSearch()
}
})
}
})
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
this.$axios.post("/platform/bpm/processDefine/delete", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
openNodeConfig(id) {
this.nodeFormData.id = id
this.nodeDialogVisible = true
this.$axios.post("/platform/bpm/processDefine/nodes", { id }).then((res) => {
if (res.code === 0) {
this.nodeFormData.nodes = res.data
}
})
},
addNewNode() {
this.$axios
.post("/platform/bpm/processDefine/generateNodeId", {
ids: JSON.stringify(this.nodeFormData.nodes.map((item) => item.id))
})
.then((res) => {
if (res.code === 0) {
this.nodeFormData.nodes.push({
id: res.data
})
}
})
},
openDynamicDecision(row) {
this.dynamicDecisionNodeFormData = row
if (!this.dynamicDecisionNodeFormData.dynamicDecisions) {
this.dynamicDecisionNodeFormData.dynamicDecisions = []
}
this.dynamicDecisionNodeDialogVisible = true
},
doSubmitNodes() {
this.$axios
.post("/platform/bpm/processDefine/updateSubmitNodes", {
define: JSON.stringify(this.nodeFormData)
})
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
},
openMoreConfig(row) {
this.moreConfigFormData = { ...row }
console.log(this.moreConfigFormData)
this.moreConfigDialogVisible = true
},
doSubmitMoreConfig() {
this.nodeFormData.nodes.forEach((item) => {
if (item.id === this.moreConfigFormData.id) {
item.url = this.moreConfigFormData.url
item.h5url = this.moreConfigFormData.h5url
}
})
this.moreConfigDialogVisible = false
},
//上移node
upNode(index) {
this.nodeFormData.nodes.splice(index - 1, 0, this.nodeFormData.nodes.splice(index, 1)[0])
},
downNode(index) {
this.nodeFormData.nodes.splice(index + 1, 0, this.nodeFormData.nodes.splice(index, 1)[0])
},
refreshNode() {}
},
mounted() {},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,118 @@
<!--#
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.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="name" label="分类名称"></el-table-column>
<el-table-column prop="categoryCount" label="关联流程数"></el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="onEdit(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="40%">
<el-form :model="formData" ref="formRef" label-width="80px">
<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 prop="picIcon" label="图片图标">
<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 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>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
formData: {},
}
},
methods: {
onAdd() {
this.formData = {}
this.dialogVisible = true
},
onEdit(row) {
this.dialogVisible = true
this.formData = { ...row }
},
onSubmit() {
this.$axios.post("/flow/category/submit", this.formData).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
this.dialogVisible = false
}
})
},
onDelete(row) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/flow/category/delete", { id: row.id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
},
created() {
this.pageData()
}
})
</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 nonce="${cspNonce!}">
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,170 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.design-dialog .el-dialog__body {
padding: 0;
}
.el-table__row--level-1 {
background-color: #FDF5E6 !important;
}
</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-item label="流程分类">
<el-select v-model="pageForm.category" placeholder="请选择流程分类" style="width: 100%"
@change="doSearch" clearable>
<el-option v-for="item in categoryOptions" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool></table-tool>
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%"
row-key="id" :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column label="序号" type="index" width="60">
<template v-slot="{ row }">
{{ row.rowNum }}
</template>
</el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:width="column.width"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template v-slot="{ row }" v-if="column.prop === 'categoryName'">
{{ categoryOptions.find(item => item.id === row.category)?.name }}
</template>
<template v-slot="{ row }" v-else-if="column.prop === 'state'">
<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>
<template v-slot="{ row }" v-else-if="column.prop === 'createTime'">
{{ $moment(row.createdAt).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</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 nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
categoryOptions: [],
designVisible: false,
designUrl: null,
tableColumns: [
{prop: 'displayName', label: '流程定义名称'},
{prop: 'name', label: '流程定义编码'},
{prop: 'categoryName', label: '流程分类'},
{prop: 'version', label: '版本号'},
{prop: 'state', label: '状态'},
{prop: 'createTime', label: '创建时间'},
],
}
},
methods: {
onView(row) {
this.designVisible = true
this.designUrl = "/flow/define/preview?defineId=" + 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.post("/flow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
}
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,360 @@
<!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="${base!}/assets/platform/plugins/element-ui/lib/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="/assets/platform/plugins/logicflow/logic-flow.js"></script>
<link rel="stylesheet" href="/assets/platform/plugins/logicflow/index.css"/>
<script src="/assets/platform/plugins/logicflow/index.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/@logicflow/extension@2.1.4/dist/index.min.js"></script>-->
<script src="/assets/platform/plugins/snaker/SnakerflowDesigner.umd.js"></script>
<style>
#snaker-flow-preview {
height: 100vh;
}
.approval-card {
position: absolute;
width: 320px;
background: #ffffff;
border-radius: 8px;
border: 1px solid #d9e3f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
z-index: 1000;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
transform: translate(10px, 10px);
overflow: hidden;
}
.tab-container {
border-bottom: 1px solid #e8e8e8;
}
.tab-header {
display: flex;
background: #fafafa;
}
.tab-item {
padding: 8px 16px;
cursor: pointer;
border-right: 1px solid #e8e8e8;
font-size: 12px;
color: #666;
background: #fafafa;
transition: all 0.2s;
}
.tab-item:last-child {
border-right: none;
}
.tab-item.active {
background: #fff;
color: #333;
border-bottom: 2px solid #1890ff;
margin-bottom: -1px;
}
.tab-item:hover {
background: #f0f0f0;
color: #333;
}
/* 卡片标题栏(OA风格顶部色条) */
.approval-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #1a73e8;
color: white;
}
.approval-card .card-header h3 {
margin: 0;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.5px;
}
.approval-card .card-header button {
background: rgba(255, 255, 255, 0.2);
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
color: white;
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: all 0.2s;
}
.approval-card .card-header button:hover {
background: rgba(255, 255, 255, 0.3);
}
/* 卡片内容区域 */
.approval-card .card-body {
padding: 16px;
}
/* 信息行样式 */
.approval-card .info-row {
display: flex;
margin-bottom: 12px;
align-items: flex-start;
}
.approval-card .info-label {
width: 80px;
color: #666;
font-size: 13px;
line-height: 1.5;
}
.approval-card .info-value {
flex: 1;
color: #333;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
/*!* 分隔线 *!*/
/*.approval-card .divider {*/
/* height: 1px;*/
/* background: #f0f0f0;*/
/* margin: 12px 0;*/
/*}*/
</style>
</head>
<body>
<div id="snaker-flow-preview">
<snaker-flow-designer
ref="designer"
v-model="flowData"
:show-doc="false"
:viewer="true"
node-render-type="html"
:high-light="highLight"
></snaker-flow-designer>
<div
v-if="showApprovalCard"
class="approval-card"
:style="{
left: cardPosition.x+'px',
top: cardPosition.y+'px'
}"
>
<div class="card-header">
<h3>详情</h3>
<button @click="closeCard">×</button>
</div>
<!-- Tab切换 -->
<div class="tab-container" v-if="approvalInfos.length > 1">
<div class="tab-header">
<div
v-for="(item, index) in approvalInfos"
:key="index"
class="tab-item"
:class="{active: activeTabIndex === index}"
@click="activeTabIndex = index"
>
记录{{index + 1}}
</div>
</div>
</div>
<div class="card-body">
<div v-for="(approvalInfo, index) in approvalInfos" :key="index" v-show="activeTabIndex === index">
<div class="info-row">
<span class="info-label">节点类型:</span>
<span class="info-value">{{approvalInfo.displayName}}</span>
</div>
<div class="info-row">
<span class="info-label">审批人:</span>
<span class="info-value" v-if="currentNode.id === 'startTask'">
{{approvalInfo.ext?.initiatorName}}({{approvalInfo.ext?.initiatorAccount}})
</span>
<span class="info-value" v-else-if="approvalInfo.taskState === 10">
{{ approvalInfo.taskActorList.map(u => u.actorName + '(' + u.actorAccount + ')').join('、') }}
</span>
<span class="info-value"
v-else>{{approvalInfo.taskFormData.userName}}({{approvalInfo.taskFormData?.loginName}})</span>
</div>
<div class="info-row">
<span class="info-label">审核状态:</span>
<span class="info-value">{{getTaskStateText(approvalInfo.taskState)}}</span>
</div>
<div class="info-row">
<span class="info-label">提交结果:</span>
<span class="info-value" v-if="approvalInfo.taskState !== 10">
{{getSubmitTypeText(approvalInfo.ext.submitType)}}
</span>
<span class="info-value" v-else>
暂未处理
</span>
</div>
<div class="info-row">
<span class="info-label">提交意见:</span>
<span class="info-value" v-if="approvalInfo.taskState !== 10">{{approvalInfo.taskFormData.opinion}}</span>
<span class="info-value" v-else>暂未处理</span>
</div>
<!-- <div class="divider"></div>-->
<div class="info-row">
<span class="info-label">提交时间:</span>
<span class="info-value">{{approvalInfo.finishTime || '暂未处理'}}</span>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
Vue.use(SnakerflowDesigner.default)
const vue = new Vue({
el: "#snaker-flow-preview",
data() {
return {
// 流程定义ID
defineId: "${defineId!}",
// 流程实例ID
instanceId: "${instanceId!}",
// 流程定义数据
flowData: {},
// 高亮数据
highLight: {},
// 历史审批记录
hisApproval: [],
// 展示已审核信息
showApprovalCard: false,
// 审批信息卡片位置
cardPosition: {x: 0, y: 0},
// 当前节点
currentNode: [],
// 审批信息数组
approvalInfos: [],
// 当前选中的tab索引
activeTabIndex: 0
}
},
computed: {
// approvalInfo() {
// if (this.currentNode) {
// return this.hisApproval.find((item) => item.taskName === this.currentNode.id)
// }
// }
},
methods: {
getDetail() {
$.get("/flow/define/detail", {id: this.defineId}).then((res) => {
if (res.code === 0) {
this.flowData = res.data.content
this.initNodeEvent()
}
})
},
getHighLight() {
$.get("/flow/common/instanceHighLight", {instanceId: this.instanceId}).then((res) => {
if (res.code === 0) {
this.highLight = res.data
}
})
},
getHisApproval() {
$.get("/flow/common/approvalRecord", {instanceId: this.instanceId}).then((res) => {
if (res.code === 0) {
this.hisApproval = res.data
}
})
},
initNodeEvent() {
this.$nextTick(() => {
console.log(this.$refs.designer.lf.graphModel.eventCenter)
this.$refs.designer.lf.graphModel.eventCenter.on("node:click", ({e, data}) => {
console.log(data)
console.log(e)
// 获取点击位置(考虑页面滚动情况)
const scrollX = window.scrollX || window.pageXOffset
const scrollY = window.scrollY || window.pageYOffset
this.cardPosition = {
x: e.clientX + scrollX,
y: e.clientY + scrollY
}
if (data.type !== "snaker:task") return
const approvalInfos = this.hisApproval.filter((item) => item.taskName === data.id && [10, 20].includes(item.taskState))
if (approvalInfos && approvalInfos.length > 0) {
this.approvalInfos = approvalInfos
this.currentNode = data
this.activeTabIndex = 0
this.showApprovalCard = true
}
})
this.$refs.designer.lf.graphModel.eventCenter.on("blank:click", (args) => {
this.closeCard()
})
})
},
closeCard() {
this.showApprovalCard = false
this.activeTabIndex = 0
},
// 获取任务状态文本
getTaskStateText(taskState) {
const stateMap = {
10: "进行中",
20: "已完成",
30: "已撤回"
}
return stateMap[taskState] || taskState
},
// 获取提交结果文本
getSubmitTypeText(submitType) {
const typeMap = {
0: "发起申请",
1: "同意申请",
2: "拒绝申请",
3: "退回上一步",
4: "跳转",
5: "重新提交",
6: "退回发起人",
20: "拒绝申请"
}
return typeMap[submitType] || submitType
}
},
mounted() {
if (this.defineId) {
this.getDetail()
}
if (this.instanceId) {
this.getHighLight()
this.getHisApproval()
}
}
})
</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 nonce="${cspNonce!}">
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 nonce="${cspNonce!}">
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,252 @@
var 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 top="20px">
<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,120 @@
var buttonConfig = {
template: /*language=HTML*/ `
<el-form-item label="按钮配置">
{{model}}
<el-table :data="model['buttonConfig']" style="width: 100%" border>
<!-- <el-table-column prop="value" label="值" width="120">-->
<!-- <template slot-scope="{ row }">-->
<!-- <el-input v-model="row.value" disabled></el-input>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column prop="originalText" label="原始文本" width="100"></el-table-column>
<el-table-column prop="replacementText" label="替换文本">
<template slot-scope="{ row }">
<el-input v-model="row.replacementText" :disabled="row.disabled"></el-input>
</template>
</el-table-column>
<el-table-column prop="theme" label="主题">
<template slot-scope="{ row }">
<el-select v-model="row.theme" placeholder="请选择" :disabled="row.disabled">
<el-option label="primary" value="primary"></el-option>
<el-option label="success" value="success"></el-option>
<el-option label="warning" value="warning"></el-option>
<el-option label="danger" value="danger"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="icon" label="图标" width="100">
<template slot-scope="{ row }">
<el-input v-model="row.icon" placeholder="请输入图标名称" :disabled="row.disabled"></el-input>
</template>
</el-table-column>
<el-table-column prop="visible" label="是否可见" width="100">
<template slot-scope="{ row }">
<el-switch v-model="row.visible" :disabled="row.disabled"></el-switch>
</template>
</el-table-column>
</el-table>
</el-form-item>
`,
props: {
model: {
type: Object,
required: true
}
// field: {
// type: String,
// required: true
// }
},
data() {
return {
tableData: [],
button: {},
defaultConfig: [
{ value: 0, originalText: "发起申请" },
{ value: 1, originalText: "同意申请" },
{ value: 2, originalText: "拒绝申请" },
{ value: 3, originalText: "退回上一步" },
{ value: 4, originalText: "跳转" },
{ value: 5, originalText: "重新提交" },
{ value: 6, originalText: "退回发起人" },
{ value: 20, originalText: "会签拒绝申请" }
]
}
},
watch: {
"model.buttonConfig": {
handler(val) {
this.$set(this.model, "buttonConfig", val)
},
deep: true
},
"model.performType": {
handler(val) {
if (val === "ALL") {
// 会签只有同意不同意
this.$set(this.model, "buttonConfig", [
{ value: 1, originalText: "同意", visible: true, disabled: true },
{ value: 20, originalText: "不同意", visible: true, disabled: true }
])
} else {
// 普通任务
// const visibleConfigs = this.model.buttonConfig.filter((item) => item.visible)
// if (visibleConfigs.length) {
// this.$set(this.model, "buttonConfig", visibleConfigs)
// } else {
// this.$set(this.model, "buttonConfig", [
// { value: 1, originalText: "同意", visible: true, disabled: false },
// { value: 2, originalText: "拒绝", visible: true, disabled: false },
// { value: 3, originalText: "退回上一步", visible: true, disabled: false },
// { value: 4, originalText: "跳转", visible: true, disabled: false },
// { value: 6, originalText: "退回发起人", visible: true, disabled: false }
// ])
// }
}
},
deep: true
},
"model.name": {
handler(val) {
if (val === "startTask") {
// 申请只有发起申请
this.$set(this.model, "buttonConfig", [{ value: 0, originalText: "提交", visible: true, disabled: false }])
}
},
deep: true,
immediate: true
}
},
methods: {
init() {
if (this.model["buttonConfig"] === undefined) {
this.$set(this.model, "buttonConfig", this.defaultConfig)
}
}
},
created() {
this.init()
}
}
@@ -0,0 +1,191 @@
<!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="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/element-ui/lib/theme-chalk/index_custom.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="/assets/platform/plugins/logicflow/logic-flow.js"></script>
<link rel="stylesheet" href="/assets/platform/plugins/logicflow/index.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',
'buttonConfig',
'enableNextOperator',
'h5form'
]"
>
<template v-slot:form-item-task-form="{model,field}">
<el-form-item label="pc跳转地址">
<el-input placeholder="请输入pc跳转地址" clearable v-model="model[field]"></el-input>
</el-form-item>
<el-form-item label="h5跳转地址">
<el-input placeholder="请输入h5跳转地址" clearable v-model="model['h5form']"></el-input>
</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>
<template v-slot:form-item-task-candidate-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 candidateHandlerClassOptions"
:key="item.value"
:value="item.value"
:label="item.name"
></el-option>
</el-select>
</el-form-item>
</template>
<template v-slot="{model,field}">
<el-form-item label="指定下一步处理人">
<el-checkbox v-model="model['enableNextOperator']">指定下一步处理人</el-checkbox>
</el-form-item>
<button-config :model="model" :field="field"></button-config>
</template>
<!-- <template v-slot="{model,field}">-->
<!-- <el-form-item label="表单查看URL">-->
<!-- <el-input v-model="model[instanceViewUrl]"></el-input>-->
<!-- </el-form-item>-->
<!-- </template>-->
</snaker-flow-designer>
</div>
</body>
<script nonce="${cspNonce!}">
<!--#include('assigneeForm.js'){}#-->
<!--#include('buttonConfig.js'){}#-->
Vue.use(SnakerflowDesigner.default)
const vue = new Vue({
el: "#app",
components: {
"assignee-form": assigneeForm,
"button-config": buttonConfig
},
data() {
return {
id: "${id!}",
designerData: {},
flowData: {},
assignmentHandlerClassOptions: [],
candidateHandlerClassOptions: []
}
},
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
}
})
$.get("/flow/design/candidateHandlerClass").then((res) => {
if (res.code === 0) {
this.candidateHandlerClassOptions = res.data
}
})
},
getData() {
console.log(this.$refs.designer)
}
},
created() {
this.init()
},
mounted() {
if (this.id) {
this.getDetail()
}
}
})
</script>
</html>
@@ -0,0 +1,279 @@
<!--#
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-item label="流程分类">
<el-select v-model="pageForm.category" placeholder="请选择流程分类" style="width: 100%"
@change="doSearch" clearable>
<el-option v-for="item in categoryOptions" :label="item.name" :value="item.id" :key="item.id"></el-option>
</el-select>
</search-item>
<search-item label="是否部署">
<el-select v-model="pageForm.deployed" placeholder="请选择是否部署" style="width: 100%"
@change="doSearch" clearable>
<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>
</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="category" label="流程分类" sortable="custom">
<template slot-scope="{row}">
{{categoryOptions.find(v=>v.id === row.category)?.name}}
</template>
</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="排序编码" prop="sortNo" width="120" align="center" header-align="center" sortable="custom">
<template slot-scope="{row}">
<el-input-number
v-model="row.sortNo"
:controls="false"
:min="0"
:precision="0"
size="mini"
style="width: 88px"
@change="updateSortNo(row)"
></el-input-number>
</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="70%">
<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 style="width: 100%">
<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-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="instanceViewUrl">
<el-input v-model="formData.instanceViewUrl" placeholder="请输入电脑端发起地址"></el-input>
</el-form-item>
<el-form-item label="手机端查看地址" prop="h5InstanceViewUrl">
<el-input v-model="formData.h5InstanceViewUrl" placeholder="请输入手机端发起地址"></el-input>
</el-form-item>
<el-form-item prop="picIcon" label="图片图标">
<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 v-model="formData.icon" placeholder="请输入图标"></el-input>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description"></text-editor>
</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 nonce="${cspNonce!}">
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.post("/flow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
},
updateSortNo(row) {
const sortNo = Number(row.sortNo)
if (!Number.isInteger(sortNo) || sortNo < 0) {
this.$message.error("排序编码必须是非负整数")
return
}
$.post("/flow/design/updateSortNo", {
id: row.id,
sortNo
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
},
},
created() {
this.pageData()
this.listCategory()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,70 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card class="mt10" shadow="never">
<el-row type="flex">
<el-button @click="openDesigner" size="medium" type="primary">
<i class="ti-plus"></i>
设计流程
</el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%">
<el-table-column label="模板ID" prop="id"></el-table-column>
<el-table-column label="流程模版名称" prop="name"></el-table-column>
<el-table-column label="关键字" prop="key"></el-table-column>
<el-table-column label="版本" prop="version"></el-table-column>
<el-table-column label="创建日期" prop="createDate"></el-table-column>
<el-table-column label="操作" width="200px">
<template scope="scope">
<el-button size="mini" type="primary">查看</el-button>
<el-button size="mini" type="danger" @click="doDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
methods: {
pageData() {
$.get(loc() + "/pageData", this.pageForm).then((res) => {
console.log(res)
this.tableData = res.data.result
this.pageForm.totalCount = res.data.total
})
},
doDelete(processId) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post(loc() + "/deleteProcess", { processId }).then((res) => {
this.$message.success(res.msg)
this.doSearch()
})
})
},
openDesigner() {
window.open("/uflo/designer")
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,218 @@
<!--#
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">
<i class="ti-plus"></i>
设计流程
</el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%">
<el-table-column label="主题" prop="subject"></el-table-column>
<el-table-column label="标签" prop="tag"></el-table-column>
<el-table-column label="流程定义ID" prop="processId"></el-table-column>
<el-table-column label="流程实例ID" prop="id"></el-table-column>
<el-table-column label="业务ID" prop="businessId"></el-table-column>
<el-table-column label="创建日期" prop="createDate"></el-table-column>
<el-table-column label="所在节点" prop="currentNode"></el-table-column>
<el-table-column label="发起人" prop="promoter"></el-table-column>
<el-table-column label="操作" width="200px">
<template scope="scope">
<el-button size="mini" type="primary" @click="loadTask(scope.row.id)">任务列表</el-button>
<el-button size="mini" type="danger" @click="doDeleteProcessInstance(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="任务列表" width="1000px" :visible.sync="taskDialog">
<el-table :data="taskList" border>
<el-table-column label="任务ID" prop="id"></el-table-column>
<el-table-column label="任务名称" prop="taskName"></el-table-column>
<el-table-column label="节点名称" prop="nodeName"></el-table-column>
<el-table-column label="处理人" prop="owner"></el-table-column>
<el-table-column label="类型" prop="type"></el-table-column>
<el-table-column label="创建日期" prop="createDate"></el-table-column>
<el-table-column label="状态" prop="state"></el-table-column>
<el-table-column label="操作" width="200">
<template scope="scope">
<el-link type="primary" @click="openComplete(scope.row.id)">完成任务</el-link>
<el-link type="primary">领取任务</el-link>
<el-link type="primary" @click="openJump(scope.row.id)">跳转</el-link>
<el-link type="primary">流程图</el-link>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog title="任务跳转" width="600px" :visible.sync="taskJumpDialog" :close-on-click-modal="false">
<el-form :model="taskJumpFormData" ref="taskJumpForm" size="small" label-position="top">
<el-form-item prop="nodeName" label="跳转目标节点" :rules="[{required:true,message:'请选择',trigger:['change','blur']}]">
<el-select v-model="taskJumpFormData.nodeName" style="width: 100%">
<el-option v-for="item in taskJumpNodeList" :key="item.name" :label="item.name" :value="item.name"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="taskJumpDialog = false">取 消</el-button>
<el-button type="primary" @click="doTaskJump">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="完成任务" width="600px" :visible.sync="taskCompleteDialog" :close-on-click-modal="false">
<el-form :model="taskCompleteFormData" ref="taskCompleteForm" size="small" label-position="top">
<el-form-item prop="variables" label="流程变量">
<el-table :data="taskCompleteFormData.variables" border size="mini">
<el-table-column label="变量名" prop="key"></el-table-column>
<el-table-column label="变量名" prop="value"></el-table-column>
<el-table-column label="操作">
<template slot="header" scope="scope">
<el-button type="primary" size="mini" @click="taskCompleteFormData.variables.push({})">添加变量</el-button>
</template>
<template slot="header" scope="scope">
<el-button type="primary" size="mini" @click="taskCompleteFormData.variables.splice(scope.$index,1)">删除变量</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item prop="sequenceFlowName" label="流转路径" :rules="[{required:true,message:'请选择',trigger:['change','blur']}]">
<el-select v-model="taskCompleteFormData.sequenceFlowName" style="width: 100%">
<el-option v-for="item in taskSequenceFlows" :key="item.name" :label="item.name" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="taskCompleteDialog = false">取 消</el-button>
<el-button type="primary" @click="doTaskComplete">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageForm: {
mode: "running"
},
taskDialog: false,
taskList: [],
processInstanceId: null,
taskJumpDialog: false,
taskJumpNodeList: [],
taskJumpFormData: {
taskId: null,
nodeName: null
},
taskCompleteDialog: false,
taskSequenceFlows: [],
taskCompleteFormData: {
variables: [],
taskId: null,
sequenceFlowName: null
}
}
},
methods: {
pageData() {
$.get(loc() + "/pageData", this.pageForm).then((res) => {
console.log(res)
this.tableData = res.data.result
this.pageForm.totalCount = res.data.total
})
},
doDeleteProcessInstance(processInstanceId) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post(loc() + "/deleteProcessInstance", { processInstanceId }).then((res) => {
this.$message.success(res.msg)
this.doSearch()
})
})
},
loadTask(processInstanceId) {
$.get(loc() + "/loadTask", { processInstanceId }).then((res) => {
this.taskDialog = true
this.taskList = res.data
})
},
openComplete(taskId) {
this.taskCompleteDialog = true
if (this.$refs.taskCompleteForm) {
this.$refs.taskCompleteForm.resetFields()
}
$.get(loc() + "/loadSequenceFlows", { taskId }).then((res) => {
this.taskSequenceFlows = res.data
this.taskSequenceFlows.unshift({
name: "默认",
value: null
})
this.taskCompleteFormData.taskId = taskId
})
},
doTaskComplete() {
this.$confirm("真的要完成这个任务吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post(loc() + "/completeTask", this.taskCompleteFormData).then((res) => {
this.$message.success(res.msg)
this.taskCompleteDialog = false
this.taskDialog = false
this.pageData()
})
})
},
openJump(taskId) {
this.taskJumpDialog = true
if (this.$refs.taskJumpForm) {
this.$refs.taskJumpForm.resetFields()
}
$.get(loc() + "/loadJumpTasks", { taskId }).then((res) => {
this.taskJumpNodeList = res.data
this.taskJumpFormData.taskId = taskId
})
},
doTaskJump() {
$.post(loc() + "/jumpTask", this.taskJumpFormData).then((res) => {
this.$message.success(res.msg)
this.taskJumpDialog = false
this.taskDialog = false
this.pageData()
})
},
openDesigner() {
window.open("/uflo/designer")
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,489 @@
<!--#
layout("/layouts/v4/baseLayout.html"){
#-->
<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;
}
</style>
<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="pageForm" class="search-form">
<el-form-item>
<el-input v-model="pageForm.searchKeyword" placeholder="请输入关键词搜索"
prefix-icon="el-icon-search"
clearable></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="pageForm.category" placeholder="所有流程分类" clearable>
<el-option v-for="type in categoryOptions" :key="type.id" :label="type.name"
:value="type.id"></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'}">
<el-table-column prop="instanceName" label="流程名称" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="taskName" label="任务节点" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="categoryName" label="流程分类" min-width="150" show-overflow-tooltip></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 v-if="activeTab === 'started'" label="状态" width="100">
<template slot-scope="{row}">{{processStatusMap[row.state]?.text}}</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(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="pageForm.pageNumber"
:page-size="pageForm.pageSize"
layout="prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</div>
</el-card>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
// 统计数据
todoCount: 0,
doneCount: 0,
startedCount: 0,
// 查询表单
pageForm: {
pageNumber: 1,
pageSize: 10,
todoCount: 0,
keyword: "",
category: ""
},
// 流程类型选项
categoryOptions: [],
// 任务列表
tasks: [],
loading: false,
// 标签页
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
}
}
},
created() {
if(GetQueryString("status")){
this.activeTab = GetQueryString("status")
}
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([this.getStatistics(), this.listCategory(), 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 listCategory() {
this.$axios.post("/flow/category/list").then((res) => {
if (res.code === 0) {
this.categoryOptions = res.data
}
})
},
// 获取任务列表
async getTasks() {
this.loading = true
try {
const res = await $.post("/flow/todoCenter/" + this.activeTab, this.pageForm)
if (res.code === 0) {
this.tasks = res.data.list
this.pageForm.totalCount = 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.pageForm.pageNumber = 1
this.getTasks()
},
// 搜索
search() {
this.pageForm.pageNumber = 1
this.getTasks()
this.getStatistics()
},
// 重置搜索
reset() {
this.pageForm.searchKeyword = ""
this.pageForm.category = ""
this.search()
},
// 处理页码变化
handleCurrentChange(val) {
this.pageForm.pageNumber = val
this.getTasks()
},
// 处理任务
async openView(task) {
console.log(task)
const {taskId, taskKey, taskState, instanceId, businessNo, formKey} = task
if (!formKey) {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
},
// 获取空状态文本
getEmptyText() {
switch (this.activeTab) {
case "todo":
return "您当前没有需要办理的任务,辛苦了"
case "done":
return "您当前没有已办理的任务记录"
case "started":
return "您当前没有发起的流程"
default:
return "暂无数据"
}
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,31 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<div class="ele-exception">
<div class="ele-exception-img"><img alt="" src="/assets/platform/img/403.svg" style="width: 216px; height: 250px" /></div>
<div class="ele-exception-content">
<h1>403</h1>
<p>抱歉, 你无权访问该页面</p>
<div>
<a href="#" onclick="event.preventDefault();" class="router-link-active">
<button
@click="$store.dispatch('pjaxRoute','/platform/home')"
type="button"
class="el-button el-button--primary el-button--medium"
>
<span>返回首页</span>
</button>
</a>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store: store
})
</script>
<!--#}#-->
@@ -0,0 +1,31 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<div class="ele-exception">
<div class="ele-exception-img"><img alt="" src="/assets/platform/img/404.svg" style="width: 216px; height: 250px" /></div>
<div class="ele-exception-content">
<h1>403</h1>
<p>抱歉, 你访问的页面不存在</p>
<div>
<a href="#" onclick="event.preventDefault();" class="router-link-active">
<button
@click="$store.dispatch('pjaxRoute','/platform/home')"
type="button"
class="el-button el-button--primary el-button--medium"
>
<span>返回首页</span>
</button>
</a>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store: store
})
</script>
<!--#}#-->
@@ -0,0 +1,31 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<div class="ele-exception">
<div class="ele-exception-img"><img alt="" src="/assets/platform/img/500.svg" style="width: 216px; height: 250px" /></div>
<div class="ele-exception-content">
<h1>403</h1>
<p>抱歉, 服务器出错了</p>
<div>
<a href="#" onclick="event.preventDefault();" class="router-link-active">
<button
@click="$store.dispatch('pjaxRoute','/platform/home')"
type="button"
class="el-button el-button--primary el-button--medium"
>
<span>返回首页</span>
</button>
</a>
</div>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store: store
})
</script>
<!--#}#-->
@@ -0,0 +1,263 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<div class="btn-group tool-button mt5">
<el-button size="medium" @click="openAdd">
<i class="ti-plus"></i>
新建密钥
</el-button>
</div>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%">
<el-table-column sortable prop="name" label="应用名称" header-align="center" align="center" width="120"></el-table-column>
<el-table-column prop="appid" label="App Id" header-align="center" width="160"></el-table-column>
<el-table-column prop="appkey" label="App Key" header-align="center" width="320">
<template slot-scope="scope">
<span v-if="isVisable(scope.row.appid)" class="bg-success">{{scope.row.appkey}}</span>
<a
v-if="!isVisable(scope.row.appid)"
@click="visiableData.push(scope.row.appid)"
style="padding-left: 5px; color: #06c; cursor: pointer"
>
显示
</a>
<a
v-if="isVisable(scope.row.appid)"
@click="setNotVisable(scope.row.appid)"
style="padding-left: 5px; color: #06c; cursor: pointer"
>
隐藏
</a>
</template>
</el-table-column>
<el-table-column sortable prop="createdAt" label="创建时间" header-align="center" align="center">
<template slot-scope="scope">{{formatAt(scope.row.createdAt)}}</template>
</el-table-column>
<el-table-column sortable prop="disabled" label="启用状态" header-align="center" align="center">
<template slot-scope="scope">
<i v-if="scope.row.disabled" class="fa fa-circle text-danger ml5"></i>
<i v-if="!scope.row.disabled" class="fa fa-circle text-success ml5"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'enable',appid:scope.row.appid,name:scope.row.name,row:scope.row}">
启用
</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',appid:scope.row.appid,name:scope.row.name,row:scope.row}">
禁用
</el-dropdown-item>
<el-dropdown-item divided :command="{type:'delete',appid:scope.row.appid,name:scope.row.name}">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="新建密钥" :visible.sync="addDialogVisible" width="50%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="name" label="应用名称">
<el-input maxlength="100" placeholder="应用名称" v-model="formData.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
visiableData: [],
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
formData: {
id: ""
},
formRules: {
name: [{ required: true, message: "应用名称", trigger: "blur" }]
}
}
},
methods: {
isVisable: function (appid) {
if (this.visiableData.indexOf(appid) >= 0) {
return true
}
return false
},
setNotVisable: function (appid) {
var index = this.visiableData.indexOf(appid)
if (index >= 0) {
this.visiableData.splice(index, 1)
}
},
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/api/data",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
formatAt: function (val) {
if (val && val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
doSearch: function () {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd: function () {
var self = this
this.addDialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd: function () {
var self = this
self.$refs["addForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/api/addDo",
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
} else {
self.$message({
message: data.msg,
type: "error"
})
}
self.pageData() //添加失败也可以入库,所以加载一下看看
},
"json"
)
}
})
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("enable" == command.type || "disable" == command.type) {
$.post(
base + "/platform/sys/api/" + command.type + "/" + command.appid,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
if ("disable" == command.type) {
command.row.disabled = true
}
if ("enable" == command.type) {
command.row.disabled = false
}
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除密钥 " + command.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/api/delete/" + command.appid,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.doSearch()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
}
},
created: function () {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,517 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
/* 系统参数页面固定占满平台内容区,避免页面主体产生纵向滚动条。 */
#app .sys-conf-layout {
display: flex;
flex-direction: column;
min-height: 0;
height: calc(100vh - 82px);
overflow: hidden;
}
#app .sys-conf-query-card {
flex: 0 0 auto;
}
/* 列表卡片占用剩余空间,数据超出时仅由 Element 表格内部滚动。 */
#app .sys-conf-list-card {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
#app .sys-conf-list-card > .el-card__body {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
}
#app .sys-conf-list-card .ele-table-tool,
#app .sys-conf-list-card .el-pagination-container {
flex: 0 0 auto;
}
</style>
<div id="app" v-cloak>
<guava>
<div class="sys-conf-layout">
<el-card class="sys-conf-query-card" shadow="never">
<search @search="doSearch">
<search-item label="参数名">
<el-input v-model="pageForm.configKey" clearable placeholder="请输入参数名"
@keyup.enter.native="doSearch"></el-input>
</search-item>
</search>
</el-card>
<el-card class="mt10 sys-conf-list-card" shadow="never">
<table-tool label="系统参数">
<el-button @click="openAdd" size="small" type="primary" icon="el-icon-plus">新增</el-button>
</table-tool>
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
@sort-change="pageOrder" header-align="center" ref="tableRef" style="width: 100%">
<el-table-column header-align="center" label="参数名" prop="configKey" sortable
width="200"></el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="参数值" prop="configValue"
width="200"></el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="说明"
prop="note"></el-table-column>
<el-table-column label="操作" prop="note" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'edit',id:scope.row.configKey}">修改
</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.configKey}"
v-if="scope.row.configKey.indexOf('App')<0">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
</guava>
<el-dialog title="新增参数" :visible.sync="addDialogVisible" :close-on-click-modal="false" width="40%">
<el-form :model="formData" ref="addForm" :rules="formRules" label-width="80px">
<el-form-item prop="configKey" label="参数名">
<el-input maxlength="100" placeholder="参数名" v-model="formData.configKey" auto-complete="off"
tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="configValue" label="参数值">
<el-input maxlength="100" placeholder="参数值" v-model="formData.configValue" auto-complete="off"
tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="note" label="说 明">
<el-input type="textarea" v-model="formData.note"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改参数" :visible.sync="editDialogVisible" :close-on-click-modal="false" width="40%">
<el-form :model="formData" ref="editForm" :rules="formRules" label-width="80px">
<el-form-item prop="configKey" label="参数名">
<el-input
maxlength="100"
placeholder="参数名"
v-model="formData.configKey"
auto-complete="off"
tabindex="1"
type="text"
disabled
></el-input>
</el-form-item>
<el-form-item prop="configValue" label="参数值">
<template v-if="formData.configKey === 'AppLogo'">
<file-upload
key="AppLogo"
style="--upload-width: 214px;--upload-height:64px"
:value.sync="formData.configValue"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>最佳分辨率:428 x 128</span>
</template>
<template v-else-if="formData.configKey === 'AppHomeImg'">
<file-upload
key="AppHomeImg"
style="--upload-width: 214px;--upload-height:64px"
:value.sync="formData.configValue"
:upload_number="5"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>PC 首页轮播图,最佳分辨率:2756 x 732,最多可上传 5 张,首页按上传顺序轮播</span>
</template>
<template v-else-if="formData.configKey === 'H5AppHomeImg'">
<file-upload
key="H5AppHomeImg"
style="--upload-width: 214px;--upload-height:110px"
:value.sync="formData.configValue"
:upload_number="5"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>移动端首页轮播图,建议分辨率:750 x 386,最多可上传 5 张,首页按上传顺序轮播</span>
</template>
<template v-else-if="formData.configKey === 'PCHomeImg'">
<file-upload
key="PCHomeImg"
style="--upload-width: 214px;--upload-height:64px"
:value.sync="formData.configValue"
:upload_number="10"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>最佳分辨率:2756 x 732,最多可上传 10 张,PC端首页按上传顺序轮播</span>
</template>
<template v-else-if="formData.configKey === 'AppFeaturedActivityImg'">
<file-upload
key="AppFeaturedActivityImg"
style="--upload-width: 214px;--upload-height:96px"
:value.sync="formData.configValue"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>最佳分辨率:750 x 280,精彩活动页顶部图片</span>
</template>
<template v-else-if="formData.configKey === 'AppFestivalBenefitImg'">
<file-upload
key="AppFestivalBenefitImg"
style="--upload-width: 214px;--upload-height:96px"
:value.sync="formData.configValue"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>最佳分辨率:750 x 280,节日福利页顶部图片</span>
</template>
<template v-else-if="formData.configKey === 'TeacherCongressCardBackImg'">
<file-upload
key="TeacherCongressCardBackImg"
style="--upload-width: 214px;--upload-height:300px"
:value.sync="formData.configValue"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
<span>教代会代表证背景图片,通过文件上传配置</span>
</template>
<template v-else>
<div v-if="'true'===formData.configValue||'false'===formData.configValue">
<el-radio-group v-model="formData.configValue">
<el-radio label="true">是</el-radio>
<el-radio label="false">否</el-radio>
</el-radio-group>
</div>
<div v-else>
<el-input
maxlength="100"
placeholder="参数值"
v-model="formData.configValue"
auto-complete="off"
tabindex="2"
type="text"
></el-input>
</div>
</template>
</el-form-item>
<el-form-item prop="note" label="说 明">
<el-input type="textarea" v-model="formData.note"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
tableLoading: false,
tableHeight: 300,
tableResizeObserver: null,
pageRequestSequence: 0,
tableData: [],
pageForm: {
pageSize: 10,
pageNumber: 1,
pageOrderName: "",
pageOrderBy: "",
totalCount: 0,
configKey: ""
},
formData: {
configKey: "",
configValue: "",
note: ""
},
formRules: {
configKey: [{required: true, message: "请输入参数名", trigger: "blur"}],
configValue: [{required: true, message: "请输入参数值", trigger: "blur"}]
}
}
},
methods: {
doSearch: function () {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd: function () {
this.addDialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
},
doAdd: function () {
var self = this
var url = base + "/platform/sys/conf/addDo"
self.$refs["addForm"].validate(function (valid) {
if (valid) {
//表单验证通过
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
self.pageData() //重新加载分页数据
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
doEdit: function () {
var self = this
var url = base + "/platform/sys/conf/editDo"
self.normalizeConfigValue()
self.$refs["editForm"].validate(function (valid) {
if (valid) {
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.editDialogVisible = false
self.pageData() //重新加载分页数据
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
normalizeConfigValue: function () {
if (Array.isArray(this.formData.configValue)) {
this.formData.configValue = this.formData.configValue
.map(function (item) {
if (typeof item === "string") {
return item
}
if (item && item.response && item.response.data) {
return item.response.data
}
if (item && item.url) {
return item.url
}
if (item && item.data) {
return item.data
}
return ""
})
.filter(function (item) {
return item
})
.join(",")
}
if (typeof this.formData.configValue === "string") {
this.formData.configValue = this.formData.configValue
.split(",")
.map(function (item) {
return item.trim()
})
.filter(function (item) {
return item
})
.join(",")
}
},
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
// 查询期间清空旧页并显示加载状态,快速连续查询时仅接收最后一次响应。
var self = this
var requestSequence = ++self.pageRequestSequence
self.tableData = []
self.tableLoading = true
$.post(base + "/platform/sys/conf/data", self.pageForm).then(function (data) {
if (requestSequence != self.pageRequestSequence) {
return
}
if (data.code == 0) {
self.tableData = data.data.list || []
self.pageForm.totalCount = data.data.totalCount || 0
self.updateTableHeight()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
}).always(function () {
if (requestSequence == self.pageRequestSequence) {
self.tableLoading = false
}
})
},
updateTableHeight: function () {
var self = this
this.$nextTick(function () {
var cardBody = self.$el.querySelector(".sys-conf-list-card > .el-card__body")
if (!cardBody || cardBody.clientHeight <= 0) {
return
}
var getOuterHeight = function (element) {
if (!element) {
return 0
}
var style = window.getComputedStyle(element)
return element.offsetHeight
+ (parseFloat(style.marginTop) || 0)
+ (parseFloat(style.marginBottom) || 0)
}
var bodyStyle = window.getComputedStyle(cardBody)
var bodyPadding = (parseFloat(bodyStyle.paddingTop) || 0)
+ (parseFloat(bodyStyle.paddingBottom) || 0)
var tableTool = cardBody.querySelector(".ele-table-tool")
var pagination = cardBody.querySelector(".el-pagination-container")
var reservedHeight = bodyPadding + getOuterHeight(tableTool) + getOuterHeight(pagination)
self.tableHeight = Math.max(1, Math.floor(cardBody.clientHeight - reservedHeight))
self.$nextTick(function () {
if (self.$refs.tableRef) {
self.$refs.tableRef.doLayout()
}
})
})
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("edit" == command.type) {
$.post(
base + "/platform/sys/conf/edit/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除 " + command.id, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/conf/delete/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.pageData()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
}
},
created: function () {
this.pageData()
},
mounted: function () {
var self = this
this.updateTableHeight()
if (window.ResizeObserver) {
this.tableResizeObserver = new ResizeObserver(function () {
self.updateTableHeight()
})
this.tableResizeObserver.observe(this.$el)
} else {
this.resizeHandler = function () {
self.updateTableHeight()
}
window.addEventListener("resize", this.resizeHandler)
}
},
beforeDestroy: function () {
if (this.tableResizeObserver) {
this.tableResizeObserver.disconnect()
}
if (this.resizeHandler) {
window.removeEventListener("resize", this.resizeHandler)
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,19 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<file-upload upload_mode="drag"></file-upload>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {}
},
methods: {},
created() {}
})
</script>
<style scoped></style>
<!--# } #-->
@@ -0,0 +1,634 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
#app {
display: flex;
width: 100%;
min-height: calc(100vh - 64px);
justify-content: center;
align-items: center;
background-color: #f0f2f5;
}
.box-card {
width: calc(100% - 20px);
min-height: calc(100vh - 84px);
position: relative;
}
.box-card .el-card__body {
min-height: calc(100vh - 170px);
position: relative;
}
.box-card-title {
font-size: 20px;
}
.card-body {
display: flex;
justify-content: center;
min-height: calc(100vh - 170px);
position: relative;
}
.card-body-center {
width: 60%;
min-width: 800px;
min-height: calc(100vh - 170px);
position: relative;
padding-bottom: 70px;
box-sizing: border-box;
}
.card-body-center-bottom {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
}
.card-body-center-bottom button {
margin: 0 10px;
}
.card-body-center > .el-carousel {
height: calc(100vh - 300px);
min-height: 560px;
position: relative;
overflow-y: hidden;
margin-top: 10px;
}
.card-body-center > .el-carousel > .el-carousel__container {
height: 100%;
}
.carousel-table-item {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 10px 0;
box-sizing: border-box;
}
.carousel-table-item-center {
height: 100%;
width: 90%;
min-width: 800px;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.carousel-table-item-center .el-card__body {
flex: 1;
height: auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
padding-bottom: 20px;
}
.check-group {
display: block;
overflow: visible;
padding: 0 10px 5px 0;
height: auto;
min-height: 100%;
box-sizing: border-box;
}
.check-group .el-radio,
.check-group .el-checkbox {
display: block;
width: 100%;
box-sizing: border-box;
padding: 12px;
margin: 0 !important;
margin-top: 15px !important;
}
.check-group .el-checkbox__label,
.check-group .el-radio__label {
width: calc(100% - 16px);
}
.option-extra {
float: right;
color: #f56c6c;
}
.carousel-table-item-relation .el-card__body,
.carousel-table-item-form .el-card__body {
position: relative;
height: 100%;
box-sizing: border-box;
}
.carousel-table-item-form .el-card__body {
overflow-y: auto;
}
.result-card {
width: 100%;
margin-top: 6px;
}
.result-card .el-card__body {
min-height: 170px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.result-text {
font-size: 12px;
color: rgb(100, 100, 100);
margin-top: 20px;
}
</style>
<div id="app" v-cloak>
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix">
<span class="box-card-title">DataKid</span>
</div>
<div class="card-body">
<div class="card-body-center">
<el-steps :active="active" align-center>
<el-step title="选择数据表" icon="el-icon-document-copy"></el-step>
<el-step title="选择数据列" icon="el-icon-s-unfold"></el-step>
<el-step title="数据列对应" icon="el-icon-s-operation"></el-step>
<el-step title="提交数据" icon="el-icon-upload"></el-step>
<el-step title="导入结果" icon="el-icon-bell"></el-step>
</el-steps>
<el-carousel indicator-position="none" :autoplay="false" arrow="never" ref="carousel" direction="vertical">
<el-carousel-item class="carousel-table-item">
<el-card class="carousel-table-item-center">
<div slot="header" class="clearfix">
<el-input
style="width: 200px"
v-model.trim="tableKeyword"
placeholder="请输入关键字查询"
clearable>
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterTables"></el-button>
<el-button v-if="formData.tableName" style="float: right; padding: 10px 0" type="text">
当前选中:{{ formData.tableName }}
</el-button>
</div>
<el-radio-group class="check-group" v-model="formData.tableName">
<el-radio
v-for="item in filteredTables"
:key="item.table_name"
:label="item.table_name"
border>
{{ item.table_name }}{{ item.table_comment ? " ( " + item.table_comment + " )" : "" }}
<span class="option-extra">rows: {{ item.table_rows || 0 }}</span>
</el-radio>
</el-radio-group>
</el-card>
</el-carousel-item>
<el-carousel-item class="carousel-table-item">
<el-card class="carousel-table-item-center">
<div slot="header" class="clearfix">
<el-input
style="width: 200px"
v-model.trim="columnKeyword"
placeholder="请输入关键字查询"
clearable>
</el-input>
<el-button type="primary" icon="el-icon-search" @click="filterColumns"></el-button>
<el-button style="float: right; padding: 10px 0" type="text">
{{ formData.tableName }}
</el-button>
</div>
<el-checkbox-group class="check-group" v-model="formData.columns">
<el-checkbox
v-for="item in filteredColumns"
:key="item.column_name"
:label="item.column_name"
border>
{{ item.column_name }}{{ item.column_comment ? " ( " + item.column_comment + " )" : "" }}
<span class="option-extra">{{ item.column_type }}</span>
</el-checkbox>
</el-checkbox-group>
</el-card>
</el-carousel-item>
<el-carousel-item class="carousel-table-item">
<el-card style="width: 100%" class="carousel-table-item-relation">
<el-table :data="relation" height="100%" style="width: 100%" border size="medium">
<el-table-column prop="column_name" label="列名"></el-table-column>
<el-table-column prop="column_comment" label="描述" show-overflow-tooltip></el-table-column>
<el-table-column prop="is_nullable" label="可为空"></el-table-column>
<el-table-column prop="column_type" label="类型"></el-table-column>
<el-table-column prop="column_key" label="key"></el-table-column>
<el-table-column min-width="200px">
<template slot="header">
目标列
<el-popover
placement="top-start"
title="提示"
trigger="hover"
content="Excel 对应的表头名称,用于和表数据列对应">
<i slot="reference" class="el-icon-question"></i>
</el-popover>
</template>
<template slot-scope="{ row }">
<el-input
v-model.trim="row.relation"
size="medium"
placeholder="目标列">
</el-input>
</template>
</el-table-column>
</el-table>
</el-card>
</el-carousel-item>
<el-carousel-item class="carousel-table-item">
<el-card style="width: 100%" class="carousel-table-item-form">
<el-form label-width="120px" :model="formData" ref="form" :rules="formRules">
<el-form-item label="&emsp;" label-width="135px" class="view-header"></el-form-item>
<el-form-item prop="method" label="导入方式">
<el-radio-group v-model="formData.method">
<el-radio-button :label="1">追加或更新</el-radio-button>
<el-radio-button :label="2">仅追加</el-radio-button>
<el-radio-button :label="3">仅更新</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="[1, 3].includes(formData.method)" prop="field" label="关键字段">
<el-select v-model="formData.field" clearable filterable placeholder="请选择" style="width: 50%">
<el-option
v-for="item in relation"
:key="item.column_name"
:label="item.column_name"
:value="item.column_name">
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="plugins" label="可选插件">
<el-checkbox-group v-model="formData.plugins">
<el-checkbox
v-for="item in plugins"
:key="item.name"
:label="item.name"
border>
{{ item.description }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="文&emsp;&emsp;件" class="is-required">
<el-upload
ref="upload"
drag
action="#"
:auto-upload="false"
:limit="1"
:file-list="fileList"
:on-change="handleFileChange"
:on-remove="handleFileRemove">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip" style="color: #F56C6C">只能上传 xls/xlsx 文件</div>
</el-upload>
</el-form-item>
</el-form>
</el-card>
</el-carousel-item>
<el-carousel-item style="padding: 10px">
<el-card class="result-card">
<el-progress type="circle" :percentage="resultPercentage" :status="resultProgressStatus"></el-progress>
<div class="result-text">{{ resultSummary }}</div>
<div style="margin-top: 10px" v-if="result.cacheKey">
<el-link
type="danger"
icon="el-icon-download"
style="font-size: 13px;"
:href="loc() + '/exportErrors?cacheKey=' + result.cacheKey">
下载错误记录
</el-link>
</div>
</el-card>
</el-carousel-item>
</el-carousel>
<div class="card-body-center-bottom">
<el-button size="medium" @click="prev" :disabled="active === 0 || loading">上一步</el-button>
<el-button type="primary" size="medium" :loading="loading" @click="active < 4 ? next() : carry()">
{{ active < 4 ? primaryButtonText : "完成" }}
</el-button>
</div>
</div>
</div>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
active: 0,
loading: false,
tableKeyword: "",
columnKeyword: "",
tables: [],
filteredTables: [],
columns: [],
filteredColumns: [],
relation: [],
plugins: [],
fileList: [],
cacheTableName: "",
result: {
total: 0,
success: 0,
cacheKey: ""
},
formRules: {
method: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
field: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
formData: {
tableName: "",
columns: [],
method: 1,
field: "",
plugins: []
}
}
},
computed: {
primaryButtonText() {
if (this.active === 3 && this.formData.method === 3) {
return "更新"
}
return "下一步"
},
resultSummary() {
const total = this.result.total || 0
const success = this.result.success || 0
const fail = Math.max(total - success, 0)
if (fail > 0) {
return "总记录数 " + total + " 条,成功 " + success + " 条,失败 " + fail + " 条!"
}
return "总记录数 " + total + " 条,成功 " + success + " 条!"
},
resultProgressStatus() {
const total = this.result.total || 0
const success = this.result.success || 0
if (total > 0 && success < total) {
return "exception"
}
return "success"
},
resultPercentage() {
if (!this.result.total) {
return 0
}
return Number(((this.result.success / this.result.total) * 100).toFixed(2))
}
},
methods: {
loc,
normalizeRecord(item) {
const normalized = {}
Object.keys(item || {}).forEach(key => {
normalized[(key || "").toLowerCase()] = item[key]
})
return normalized
},
async loadTables() {
const resp = await this.$axios.post(loc() + "/tableNames")
if (resp.code === 0) {
this.tables = Array.isArray(resp.data)
? resp.data.map(item => this.normalizeRecord(item))
: []
this.filteredTables = this.tables.slice()
}
},
async loadPlugins() {
this.plugins = await this.$businessTool.getEnumOptions("SysDataImportPlugin")
},
async loadColumns() {
const resp = await this.$axios.post(loc() + "/columns", {
tableName: this.formData.tableName
})
if (resp.code === 0) {
this.columns = Array.isArray(resp.data)
? resp.data.map(item => this.normalizeRecord(item))
: []
this.filteredColumns = this.columns.slice()
}
},
filterTables() {
const keyword = (this.tableKeyword || "").toLowerCase()
if (!keyword) {
this.filteredTables = this.tables.slice()
return
}
this.filteredTables = this.tables.filter(item => {
const tableName = (item.table_name || "").toLowerCase()
const tableComment = (item.table_comment || "").toLowerCase()
return tableName.includes(keyword) || tableComment.includes(keyword)
})
},
filterColumns() {
const keyword = (this.columnKeyword || "").toLowerCase()
if (!keyword) {
this.filteredColumns = this.columns.slice()
return
}
this.filteredColumns = this.columns.filter(item => {
const columnName = (item.column_name || "").toLowerCase()
const columnComment = (item.column_comment || "").toLowerCase()
return columnName.includes(keyword) || columnComment.includes(keyword)
})
},
buildRelation() {
this.relation = this.columns
.filter(item => this.formData.columns.includes(item.column_name))
.map(item => Object.assign({}, item, { relation: "" }))
},
async next() {
if (this.active === 0) {
if (!this.formData.tableName) {
this.$notify({
title: "警告",
message: "请选择数据表",
type: "warning"
})
return
}
if (this.cacheTableName !== this.formData.tableName) {
await this.loadColumns()
this.formData.columns = []
this.formData.field = ""
this.cacheTableName = this.formData.tableName
}
} else if (this.active === 1) {
if (!this.formData.columns.length) {
this.$notify({
title: "警告",
message: "请选择数据列",
type: "warning"
})
return
}
this.buildRelation()
} else if (this.active === 2) {
if (this.relation.find(item => !item.relation)) {
this.$notify({
title: "警告",
message: "存在未填写的目标列",
type: "warning"
})
return
}
} else if (this.active === 3) {
const success = await this.doSubmit()
if (!success) {
return
}
}
this.$refs.carousel.next()
this.active++
},
prev() {
if (this.active <= 0) {
return
}
this.$refs.carousel.prev()
this.active--
},
validateSubmitForm() {
let validForm = true
this.$refs.form.validate(valid => {
validForm = valid
})
if (!validForm || !this.fileList.length) {
this.$notify({
title: "警告",
message: "存在未填写的必填项",
type: "warning"
})
return false
}
return true
},
async doSubmit() {
if (!this.validateSubmitForm()) {
return false
}
const formData = new FormData()
formData.append("tableName", this.formData.tableName)
formData.append("method", this.formData.method)
formData.append("field", this.formData.field || "")
formData.append("relation", JSON.stringify(this.relation))
formData.append("plugins", JSON.stringify(this.formData.plugins || []))
formData.append("file", this.fileList[0].raw, this.fileList[0].name)
this.loading = true
try {
const resp = await this.$axios.post(loc() + "/importData", formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
if (resp.code !== 0) {
this.$notify.error({
title: "错误",
message: resp.msg
})
this.result = { total: 0, success: 0, cacheKey: "" }
return false
}
this.result = resp.data || { total: 0, success: 0, cacheKey: "" }
return true
} finally {
this.loading = false
}
},
handleFileRemove(file, fileList) {
this.fileList = fileList
},
handleFileChange(file, fileList) {
const extension = ((file.name || "").split(".").pop() || "").toLowerCase()
const remove = () => {
const removeIndex = fileList.findIndex(item => item.uid === file.uid)
if (removeIndex > -1) {
fileList.splice(removeIndex, 1)
}
}
if (file.size === 0) {
this.$message.warning("您选择的是空文件")
remove()
} else if (!["xls", "xlsx"].includes(extension)) {
this.$message.warning("上传文件只能是 xls/xlsx 格式")
remove()
}
this.fileList = fileList
},
carry() {
this.active = 0
this.tableKeyword = ""
this.columnKeyword = ""
this.columns = []
this.filteredColumns = []
this.relation = []
this.fileList = []
this.cacheTableName = ""
this.result = {
total: 0,
success: 0,
cacheKey: ""
}
this.formData = {
tableName: "",
columns: [],
method: 1,
field: "",
plugins: []
}
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.filterTables()
this.$refs.carousel.setActiveItem(0)
}
},
async created() {
await this.loadTables()
await this.loadPlugins()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,15 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak></div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app"
})
</script>
<!--#
}
#-->
@@ -0,0 +1,211 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.pullTimeRadioGroup .el-checkbox {
width: 100%;
margin: 0 0 10px 0;
}
.pullTimeRadioGroup .el-checkbox.is-bordered {
margin-left: 0 !important;
display: flex;
align-items: center;
}
.pullTimeRadioGroup .el-checkbox__input {
flex-shrink: 0;
}
.pullTimeRadioGroup .el-checkbox__label {
flex: 1;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="拉取时间">
<el-select clearable v-model="pageForm.pullTime" placeholder="请选择拉取时间" :clearable="false" @change="doSearch">
<el-option v-for="item in pullTimeOptions" :label="item.pullTime" :value="item.pullTime"></el-option>
</el-select>
</search-item>
<search-item label="姓名">
<el-input placeholder="请输入姓名" clearable v-model="pageForm.userName" style="width: 100%"></el-input>
</search-item>
<search-item label="工号">
<el-input placeholder="请输入工号" clearable v-model="pageForm.loginName" style="width: 100%"></el-input>
</search-item>
<search-item label="所属单位">
<el-select placeholder="请选择所属单位" v-model="pageForm.unitName" clearable filterable>
<el-option v-for="item in unitNameOptions" :key="item" :label="item" :value="item"></el-option>
</el-select>
</search-item>
<search-item label="在职状态">
<el-select v-model="pageForm.userState" clearable filterable>
<el-option v-for="item in userStateOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
<search-item label="用人方式">
<el-select v-model="pageForm.preparedBy" clearable filterable>
<el-option v-for="item in preparedByOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
<search-item label="人员类型">
<el-select v-model="pageForm.personType" clearable filterable>
<el-option v-for="item in personTypeOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool :label="pageForm.pullTime ? pageForm.pullTime + '数据' : '全部数据'">
<el-button type="primary" size="mini" :loading="pullLoading" @click="pull" icon="el-icon-download">拉取信息中心数据</el-button>
<el-button type="danger" size="mini" @click="openDelete" icon="el-icon-delete">删除本地数据源</el-button>
</table-tool>
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center" v-loading="tableLoading">
<el-table-column type="index" width="70" label="序号" fixed="left" :index="indexMethod"></el-table-column>
<el-table-column prop="pullTime" label="拉取时间" width="170" fixed="left"></el-table-column>
<el-table-column prop="loginname" label="工号" width="100" fixed="left"></el-table-column>
<el-table-column prop="username" label="姓名" fixed="left" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="sex" label="性别" sortable></el-table-column>
<el-table-column prop="mobile" label="手机号" width="120"></el-table-column>
<el-table-column prop="birthday" label="生日" sortable width="120">
<template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template>
</el-table-column>
<el-table-column prop="userState" label="在职状态" sortable width="120"></el-table-column>
<el-table-column prop="arrivalAtSchoolDate" label="来校时间" sortable width="120"></el-table-column>
<el-table-column prop="personType" label="人员类型" sortable width="120"></el-table-column>
<!-- <el-table-column prop="technicalTitle" label="技术职称" sortable width="120"></el-table-column>
<el-table-column prop="position" label="干部职务" sortable width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="education" label="学历" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="academicDegree" label="学位" show-overflow-tooltip sortable width="120"></el-table-column>-->
<el-table-column prop="unitName" label="单位" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="unitId" label="单位编码" sortable width="120"></el-table-column>
<el-table-column prop="political" label="政治面貌" sortable></el-table-column>
<el-table-column prop="nation" label="民族" sortable></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="选择数据源" :visible.sync="pullTimeDialogVisible" width="30%">
<el-checkbox-group v-model="sourceTime" style="width: 100%" class="pullTimeRadioGroup">
<el-checkbox :label="item.pullTime" border v-for="item in pullTimeOptions" :key="item.pullTime">
<div style="display: flex; justify-content: space-between">
{{item.pullTime}}
<span style="color: red">rows:{{item.num}}</span>
</div>
</el-checkbox>
</el-checkbox-group>
<span slot="footer" class="dialog-footer">
<el-button @click="pullTimeDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doDeleteUser">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
unitNameOptions: [],
userStateOptions: [],
preparedByOptions: [],
personTypeOptions: [],
pullTimeOptions: false,
sourceTime: [],
pullTimeDialogVisible: false,
pullLoading: false
}
},
methods: {
pull() {
this.$confirm("确定要拉取数据源吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
})
.then(() => {
this.pullLoading = true
this.$axios
.post("/platform/sys/data/user/pull/pullData")
.then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.getPullTimeOptions()
this.doSearch()
}
})
.finally(() => {
this.pullLoading = false
})
})
.catch(() => {})
},
getSearchOptions() {
this.$axios.post("/platform/sys/data/user/pull/searchOptions").then((resp) => {
if (resp.code === 0) {
this.unitNameOptions = resp.data.unitNames
this.userStateOptions = resp.data.userStates
this.personTypeOptions = resp.data.personTypes
this.preparedByOptions = resp.data.preparedBys
}
})
},
getPullTimeOptions() {
this.$axios.post("/platform/sys/data/user/pull/pullTimeOptions").then((resp) => {
if (resp.code === 0) {
this.pullTimeOptions = resp.data
if (this.pullTimeOptions.length > 0) {
this.pageForm.pullTime = this.pullTimeOptions[0].pullTime
this.pageData()
} else {
this.tableData = []
this.pageForm.totalCount = 0
}
}
})
},
openDelete() {
this.pullTimeDialogVisible = true
this.getPullTimeOptions()
},
doDeleteUser() {
this.$confirm("确定要删除数据源吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios
.post("/platform/sys/data/user/pull/deleteByPullTime", {
pullTime: JSON.stringify(this.sourceTime)
})
.then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pullTimeDialogVisible = false
this.doSearch()
}
})
})
}
},
created() {
// this.pageData()
this.getSearchOptions()
this.getPullTimeOptions()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,313 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.pullTimeRadioGroup .el-radio {
width: 100%;
margin-bottom: 10px;
}
.pullTimeRadioGroup .el-radio.is-bordered + .el-radio.is-bordered {
margin-left: 0;
}
.condition-builder {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名">
<el-input placeholder="请输入姓名" clearable v-model="pageForm.userName" style="width: 100%"></el-input>
</search-item>
<search-item label="工号">
<el-input placeholder="请输入工号" clearable v-model="pageForm.loginName" style="width: 100%"></el-input>
</search-item>
<search-item label="所属单位">
<el-select placeholder="请选择所属单位" v-model="pageForm.unitId" clearable style="width: 100%">
<el-option v-for="i in unitOptions" :label="i.name" :key="i.id" :value="i.id"></el-option>
</el-select>
</search-item>
<search-item label="在职状态">
<dict-select v-model="pageForm.userState" code="USER_STATE" style="width: 100%"></dict-select>
</search-item>
<search-item label="人员类型">
<dict-select v-model="pageForm.personType" code="USER_PERSON_TYPE" style="width: 100%"></dict-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="系统当前用户数据">
<!--<el-button type="primary" size="mini" @click="updateDataDict" icon="el-icon-refresh">更新【码表】数据</el-button>-->
<el-button type="primary" size="mini" @click="updateUnits" icon="el-icon-refresh">更新【单位】数据</el-button>
<el-button type="primary" size="mini" @click="openUpdate" icon="el-icon-refresh">更新【人员】数据</el-button>
</table-tool>
<el-table :key="tableKey" :data="tableData" @sort-change="pageOrder" header-align="center">
<el-table-column type="index" width="70" label="序号" fixed="left"></el-table-column>
<el-table-column prop="loginname" label="工号" width="100" fixed="left"></el-table-column>
<el-table-column prop="username" label="姓名" fixed="left" width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="changeTypes" label="变更类型" width="140">
<template slot-scope="{row}">
<span style="color: #0e78c5; cursor: pointer" @click="openCurrentUserChangeInfo(row)">
{{getChangeTypes(row.changeTypes)}}
</span>
</template>
</el-table-column>
<el-table-column prop="changeTime" label="变更时间" sortable width="150"></el-table-column>
<el-table-column prop="sex" label="性别" sortable></el-table-column>
<!-- <el-table-column prop="mobile" label="手机号" width="120"></el-table-column>-->
<el-table-column prop="birthday" label="生日" sortable width="120">
<template slot-scope="{row}">{{$moment(row.birthday).format('YYYY-MM-DD')}}</template>
</el-table-column>
<el-table-column prop="userState" label="在职状态" sortable width="120"></el-table-column>
<el-table-column prop="personType" label="人员类型" sortable width="120"></el-table-column>
<el-table-column prop="arrivalAtSchoolDate" label="来校年月" sortable width="120"></el-table-column>
<!--<el-table-column prop="technicalTitle" label="技术职称" sortable width="120"></el-table-column>
<el-table-column prop="position" label="职务" sortable width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="education" label="学历" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="academicDegree" label="学位" show-overflow-tooltip sortable width="120"></el-table-column>-->
<el-table-column prop="unitName" label="单位" show-overflow-tooltip sortable width="120"></el-table-column>
<el-table-column prop="unitId" label="单位编码" sortable width="120"></el-table-column>
<el-table-column prop="political" label="政治面貌" sortable></el-table-column>
<el-table-column prop="nation" label="民族" sortable></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #view>
<member-all-change-info ref="memberAllChangeInfoRef"></member-all-change-info>
</template>
</guava>
<el-dialog title="选择数据源" :visible.sync="updateDialog" width="70%">
<el-timeline>
<el-timeline-item timestamp="数据源" placement="top">
<el-radio-group v-model="updateFromData.pullTime" style="width: 100%" class="pullTimeRadioGroup">
<el-radio :label="item.pullTime" border v-for="item in pullTimeOptions" :key="item.pullTime">
<span>
{{item.pullTime}}
<span style="float: right; color: red">rows:{{item.num}}</span>
</span>
</el-radio>
</el-radio-group>
</el-timeline-item>
<el-timeline-item timestamp="更新方式" placement="top">
<el-radio-group class="checkGroup" style="width: 100%" v-model="updateFromData.updateMode" size="small">
<el-row>
<el-radio border label="ALL">全部更新</el-radio>
<el-radio border label="INCR">仅更新新增人员</el-radio>
</el-row>
</el-radio-group>
</el-timeline-item>
<el-timeline-item timestamp="高级条件筛选" placement="top">
<el-switch v-model="enableAdvancedConditions" active-text="启用高级条件"></el-switch>
<div v-if="enableAdvancedConditions" class="condition-builder">
<!-- 条件构建器组件 -->
<condition-group :group="updateFromData.conditionGroup" :field_options="fieldOptions" @remove="removeRootGroup"></condition-group>
</div>
</el-timeline-item>
</el-timeline>
<span slot="footer" class="dialog-footer">
<el-button @click="updateDialog = false" :disabled="updateLoading">取 消</el-button>
<el-button type="primary" @click="execUpdate" :disabled="updateLoading" :loading="updateLoading">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../../zhgh/staffmanage/member/change/common/memberAllChangeInfo.js"){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
updateDialog: false,
pullTimeOptions: false,
updateFromData: {
pullTime: null,
updateMode: "ALL",
conditionGroup: {
logic: "AND",
conditions: [],
groups: []
}
},
updateLoading: false,
unitOptions: [],
changeTypeData: [],
enableAdvancedConditions: false,
// 可选字段列表
fieldOptions: [
{ label: "姓名", value: "username" },
{ label: "工号", value: "loginname" },
{ label: "性别", value: "sex" },
{ label: "生日", value: "birthday" },
{ label: "手机号", value: "mobile" },
{ label: "在职状态", value: "userState" },
{ label: "进站时间", value: "postDoctoralJoinDate" },
{ label: "人员类型", value: "personType" },
{ label: "来校年月", value: "arrivalAtSchoolDate" },
{ label: "单位", value: "unitName" },
{ label: "单位编码", value: "unitId" },
{ label: "学历", value: "education" },
{ label: "学位", value: "academicDegree" }
]
}
},
components: {
"member-all-change-info": MEMBER_ALL_CHANGE_INFO
},
methods: {
// 更新单位数据
updateUnits() {
this.$confirm("确定要更新单位数据吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/data/user/update/updateUnits")
.then((resp) => {
if (resp.code === 0) {
this.$message.success("更新成功")
} else {
this.$message.error(resp.msg)
}
})
})
},
// 更新码表数据
updateDataDict() {
this.$confirm("确定要更新码表数据吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/data/user/update/updateDataDict")
.then((resp) => {
if (resp.code === 0) {
this.$message.success("更新成功")
} else {
this.$message.error(resp.msg)
}
})
})
},
openCurrentUserChangeInfo(row) {
this.$refs.guava.view(() => {
this.$refs.memberAllChangeInfoRef.onOpen(row.id)
})
},
getChangeTypes(changeTypes) {
if (!changeTypes) return null
const changeTypesList = JSON.parse(changeTypes)
if (changeTypesList) {
if (!changeTypesList || changeTypesList.length === 0 || !this.changeTypeData || this.changeTypeData.length === 0) return null
return changeTypesList
.map((v) => {
return this.changeTypeData.find((item) => item.code === v).changeTypeName
})
.join(",")
}
return null
},
openUpdate() {
this.updateDialog = true
this.updateLoading = false
this.$nextTick(() => {
this.updateFromData = {
updateMode: "ALL",
pullTime: null,
conditionGroup: {
logic: "AND",
conditions: [],
groups: []
}
}
this.enableAdvancedConditions = false
})
},
getPullTimeOptions() {
this.$axios.post("/platform/sys/data/user/update/pullTimeOptions").then((resp) => {
if (resp.code === 0) {
this.pullTimeOptions = resp.data
}
})
},
execUpdate() {
if (!this.updateFromData.pullTime) {
this.$message.warning("请选择数据源时间")
return
}
if (!this.updateFromData.updateMode) {
this.$message.warning("请选择更新方式")
return
}
// 如果未启用高级条件,则移除条件组
if (!this.enableAdvancedConditions) {
this.updateFromData.conditionGroup = null
}
this.$confirm("确定要更新数据吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.updateLoading = true
this.$axios
.post("/platform/sys/data/user/update/updateUserFormSource", { param: JSON.stringify(this.updateFromData) })
.then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.updateDialog = false
this.pageData()
}
})
.finally(() => {
this.updateLoading = false
})
})
},
listUnit() {
this.$businessTool.listUnit(this.pageForm.unionId).then((res) => {
this.unitOptions = res
})
},
removeRootGroup() {
// 重置根条件组
this.updateFromData.conditionGroup = {
logic: "AND",
conditions: [],
groups: []
}
}
},
async created() {
this.listUnit()
this.getPullTimeOptions()
this.pageData()
this.changeTypeData = await this.$businessTool.getEnumOptions("MemberChangeType")
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,478 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<el-row type="flex" justify="space-between">
<el-button size="medium" @click="openAdd">
<i class="ti-plus"></i>
新建字典
</el-button>
<el-button size="medium" @click="openSort">排序</el-button>
</el-row>
</el-card>
<el-card shadow="never">
<el-table
:key="tableKey"
:data="tableData"
style="width: 100%"
:highlight-current-row="true"
row-key="id"
lazy
:expand-row-keys="expandedRowKeysRenew"
@expand-change="handleExpandChange"
:load="loadChild"
>
<el-table-column
label="字典名称"
header-align="center"
prop="name"
width="200"
:show-overflow-tooltip="true"
align="left"
></el-table-column>
<el-table-column label="字典代码" header-align="center" prop="code" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="启用状态" header-align="center" prop="disabled" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<i v-if="scope.row.disabled" class="fa fa-circle text-danger ml5"></i>
<i v-if="!scope.row.disabled" class="fa fa-circle text-success ml5"></i>
</template>
</el-table-column>
<el-table-column label="操作" header-align="center" prop="website" :show-overflow-tooltip="true" align="center" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'add',id:scope.row.id,name:scope.row.name}">添加子级</el-dropdown-item>
<el-dropdown-item divided :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}">
启用
</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">禁用</el-dropdown-item>
<el-dropdown-item divided :command="{type:'edit',id:scope.row.id,name:scope.row.name}">修改</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,name:scope.row.name}">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="新建字典" :visible.sync="addDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="parentId" label="上级字典" label-width="80px">
<el-cascader
v-if="!isAddFromSub"
:props="props"
:options="menuOptions"
v-model="parentMenu"
placeholder="不选择则为顶级"
style="width: 100%"
></el-cascader>
<el-input v-if="isAddFromSub" type="text" v-model="formData.parentName" :disabled="true"></el-input>
</el-form-item>
<el-form-item prop="name" label="字典名称">
<el-input maxlength="100" placeholder="字典名称" v-model="formData.name" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="code" label="字典代码">
<el-input maxlength="100" placeholder="字典代码" v-model="formData.code" auto-complete="off" tabindex="5" type="text"></el-input>
</el-form-item>
<el-form-item prop="remark" label="字典备注">
<el-input maxlength="100" placeholder="字典备注" v-model="formData.remark" auto-complete="off" tabindex="2" type="text"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改字典" :visible.sync="editDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="editForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="name" label="字典名称">
<el-input maxlength="100" placeholder="字典名称" v-model="formData.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="code" label="字典代码">
<el-input maxlength="100" placeholder="字典代码" v-model="formData.code" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="remark" label="字典备注">
<el-input maxlength="100" placeholder="字典备注" v-model="formData.remark" auto-complete="off" tabindex="2" type="text"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="字典排序" :visible.sync="sortDialogVisible" width="50%">
<el-tree ref="sortMenuTree" :data="sortMenuData" draggable :allow-drop="sortAllowDrop" node-key="id" :props="defaultProps">
<span class="custom-tree-node" slot-scope="scope">
<span>{{ scope.node.label }}</span>
</span>
</el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="sortDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doSort">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
sortDialogVisible: false,
isAddFromSub: false,
tableData: [], //后台取出的数据
tableKey: "",
formData: {
id: "",
parentId: "",
parentName: "",
type: "menu",
target: "data-pjax",
children: "false"
},
formRules: {
name: [{ required: true, message: "字典名称", trigger: "blur" }],
code: [{ required: true, message: "字典代码", trigger: ["blur", "change"] }]
},
menuOptions: [],
props: {
lazy: true,
checkStrictly: true,
multiple: false,
lazyLoad: function (node, resolve) {
var url = base + "/platform/sys/dict/tree"
$.post(
url,
{ pid: node.value },
function (data) {
if (data.code == 0) {
resolve(data.data)
}
},
"json"
)
}
},
parentMenu: [],
sortMenuData: [],
defaultProps: {
children: "children",
label: "label"
},
// 保存展开状态的数组
expandedRowKeysRenew: [],
tableTreeRefreshTool: {}
}
},
methods: {
//排序树加载
sortMenuLoad: function () {
var self = this
$.post(
base + "/platform/sys/dict/menuAll",
{},
function (data) {
if (data.code == 0) {
self.sortMenuData = data.data
}
},
"json"
)
},
//排序树控制不可跨级拖拽
sortAllowDrop: function (moveNode, inNode, type) {
if (moveNode.data.parentId == inNode.data.parentId) {
return type == "prev"
}
},
//打开排序功能
openSort: function () {
this.sortDialogVisible = true
this.sortMenuLoad()
},
//递归获取所有的ID值
getTreeIds: function (ids, data) {
var self = this
data.forEach(function (obj) {
ids.push(obj.id)
if (obj.children && obj.children.length > 0) {
self.getTreeIds(ids, obj.children)
}
})
},
//提交排序后的数据
doSort: function () {
var self = this
var ids = []
self.getTreeIds(ids, self.sortMenuData)
$.post(
base + "/platform/sys/dict/sortDo",
{ ids: ids.toString() },
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.sortDialogVisible = false
self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
openAdd: function () {
var self = this
self.isAddFromSub = false
self.addDialogVisible = true
self.parentMenu = []
self.menuOptions = []
//表单还原为初始值
self.formData = {
id: "",
parentId: "",
parentName: "",
type: "menu",
target: "data-pjax",
children: "false"
}
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd: function () {
var self = this
var url = base + "/platform/sys/dict/addDo"
self.$refs["addForm"].validate(function (valid) {
if (valid) {
//表单验证通过
if (!self.isAddFromSub) {
//如果不是添加子菜单,则从级联选择框取父节点ID
self.formData.parentId = self.parentMenu[self.parentMenu.length - 1] || ""
}
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
self.loadChildByExpandedKeys()
// self.initTreeTable();
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
doEdit: function () {
var self = this
var url = base + "/platform/sys/dict/editDo"
self.$refs["editForm"].validate(function (valid) {
if (valid) {
//表单验证通过
$.post(
url,
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.editDialogVisible = false
self.loadChildByExpandedKeys()
// self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
initTreeTable: function () {
var self = this
var url = base + "/platform/sys/dict/child"
$.post(
url,
{ pid: "" },
function (data) {
if (data.code == 0) {
self.tableData = data.data
self.tableKey = +new Date()
}
},
"json"
)
},
loadChild: function (tree, treeNode, resolve) {
// 在之前声明的全局变量中,增加一个key为 本条数据的id,id可替换为你数据中的任意唯一值
this.tableTreeRefreshTool[tree.id] = {}
// 重要!保存resolve方法,以便后续使用
this.tableTreeRefreshTool[tree.id].resolve = resolve
// 记录展开次数,具体作用后续介绍
this.tableTreeRefreshTool[tree.id].expandCount = 0
var url = base + "/platform/sys/dict/child"
$.post(
url,
{ pid: tree.id },
function (data) {
if (data.code == 0) {
resolve(data.data)
}
},
"json"
)
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("add" == command.type) {
self.openAdd()
self.formData.parentId = command.id
self.formData.parentName = command.name
self.isAddFromSub = true
}
if ("edit" == command.type) {
$.post(
base + "/platform/sys/dict/edit/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("enable" == command.type || "disable" == command.type) {
$.post(
base + "/platform/sys/dict/" + command.type + "/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
if ("disable" == command.type) {
command.row.disabled = true
}
if ("enable" == command.type) {
command.row.disabled = false
}
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除 " + command.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/dict/delete/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.loadChildByExpandedKeys()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
},
handleExpandChange(row, expanded) {
if ((row.parentId === "" || row.parentId == null) && !expanded) {
this.expandedRowKeysRenew = []
return
}
if (expanded) {
this.expandedRowKeysRenew.push(row.id)
} else {
const index = this.expandedRowKeysRenew.findIndex((v) => v === row.id)
this.expandedRowKeysRenew.splice(index, 1)
}
},
loadChildByExpandedKeys() {
if (this.expandedRowKeysRenew && this.expandedRowKeysRenew.length > 0) {
this.expandedRowKeysRenew.forEach((v) => {
const curr = this.tableTreeRefreshTool[v]
// api请求
$.get("/platform/sys/dict/child", { pid: v }).then((res) => {
curr.resolve(res.data)
})
})
} else {
this.initTreeTable()
}
}
},
created: function () {
this.initTreeTable()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,126 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.record-img {
width: 40px;
height: 40px;
vertical-align: middle;
border-style: none;
overflow-clip-margin: content-box;
overflow: clip;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="文件名称" prop="name">
<el-input v-model="pageForm.name" placeholder="请输入文件名称" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button size="mini" type="primary" @click="openUploadFile">上传文件</el-button>
</table-tool>
<el-table :data="tableData" header-align="center" border style="width: 100%">
<el-table-column label="文件名称" prop="name"></el-table-column>
<el-table-column label="缩略图" prop="thumbnail" width="100px">
<template scope="{row}">
<el-image
v-if="row.suffix === 'jpg' || row.suffix === 'png' || row.suffix === 'row' || row.suffix === 'gif'"
:src="row.thumbnail"
:fit="'cover'"
:style="{ width: '50px', height: '50px' }"
></el-image>
<el-image
v-else
:src="$commonUtil.getFileItemShowIcon(row.suffix)"
:fit="'cover'"
:style="{ width: '50px', height: '50px' }"
></el-image>
</template>
</el-table-column>
<el-table-column label="文件大小" prop="sizeInfo" width="100px"></el-table-column>
<el-table-column label="文件后缀" prop="suffix" width="100px"></el-table-column>
<el-table-column label="储存引擎" prop="engine" width="100px"></el-table-column>
<el-table-column label="操作" width="400px">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="$commonUtil.previewFile(scope.row)">预览</el-button>
<el-button size="mini" type="primary" @click="openDetailFile(scope.row)">详情</el-button>
<el-button size="mini" type="primary" @click="$downLoad(scope.row.downloadPath)">下载</el-button>
<el-button size="mini" type="danger" @click="deleteFile(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="上传文件" width="50%" :visible.sync="uploadFileDialogVisible">
<el-form :model="formData" ref="form">
<el-form-item label="文件" :rules="[{ required: true, message: '请上传文件', trigger: ['blur','change'] }]">
<file-upload :value.sync="formData.files" :upload_number="1" upload_result_category="interval" complete_result></file-upload>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog title="文件详情" :visible.sync="detailFileDialogVisible" width="50%">
<el-descriptions :column="3" border>
<el-descriptions-item label="文件名称" :span="3">{{viewData.name}}</el-descriptions-item>
<el-descriptions-item label="文件大小" :span="3">{{viewData.sizeInfo}}</el-descriptions-item>
<el-descriptions-item label="文件后缀" :span="3">{{viewData.suffix}}</el-descriptions-item>
<el-descriptions-item label="储存引擎" :span="3">{{viewData.bucket}}</el-descriptions-item>
<el-descriptions-item label="储存路径" :span="3">{{viewData.storagePath}}</el-descriptions-item>
<el-descriptions-item label="下载地址" :span="3">{{viewData.downloadPath}}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
tableData: [],
uploadFileDialogVisible: false,
formData: {},
detailFileDialogVisible: false,
viewData: {}
}
},
methods: {
deleteFile(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/sys/file/delete", {
ids: JSON.stringify([id])
}).then((res) => {
this.$message.success(res.msg)
this.doSearch()
})
})
},
openUploadFile() {
this.uploadFileDialogVisible = true
this.formData = {}
},
openDetailFile(row) {
console.log(row)
this.detailFileDialogVisible = true
this.viewData = { ...row }
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,525 @@
<!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/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css" />
</head>
<body>
<!--[if lt IE 9]>
<div class="ie-must-go-die">
<div class="ie-container">
<h1>您正在IE的世界中</h1>
<p>本系统不支持IE9以下版本浏览器访问。</p>
<a href="https://browsehappy.com" target="_blank">您可以点击此处,下载Chrome/Firefox后再次访问本页面。</a>
</div>
</div>
<![endif]-->
<div id="app" v-cloak>
<div class="login-container">
<div class="login-border">
<el-row type="flex">
<el-col :span="12" class="login-left">
<img src="${base!}/assets/platform/img/banner.png" style="height: 485px" />
</el-col>
<el-col>
<div class="login-main">
<h4 class="title">${AppName!}系统登录</h4>
<el-tabs v-model="activeName" style="max-height: 275px">
<el-tab-pane label="用户登录" name="signin">
<span slot="label">
<i slot="prefix" class>
<i class="el-icon-user"></i>
</i>
用户登录
</span>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
auto-complete="on"
label-position="left"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号或手机号码"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
<el-form-item prop="platformCaptcha">
<el-input
v-model="loginForm.platformCaptcha"
auto-complete="off"
placeholder="验证码"
style="width: 60%"
@keyup.enter.native="handleLogin"
prefix-icon="el-icon-key"
></el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" />
</div>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width: 100%"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="用户注册" name="signup">
<span slot="label">
<i slot="prefix" class>
<i class="el-icon-mobile-phone"></i>
</i>
用户注册
</span>
<el-form ref="regForm" class="login-form" status-icon :rules="regRules" :model="regForm"
label-width="0">
<el-form-item prop="mobile">
<el-input
v-model="regForm.mobile"
auto-complete="off"
placeholder="请输入手机号码"
@keyup.enter.native="handleReg"
prefix-icon="el-icon-mobile-phone"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="regForm.password"
:type="passwordType"
auto-complete="off"
placeholder="请输入账号密码"
@keyup.enter.native="handleReg"
prefix-icon="el-icon-lock"
>
<i class="ti-eye" slot="suffix" @click="showPwd"></i>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
v-model="regForm.code"
auto-complete="off"
placeholder="请输入验证码"
@keyup.enter.native="handleSend"
prefix-icon="el-icon-key"
>
<template slot="append">
<span
style="cursor: pointer"
class="msg-text"
:class="[{ display: msgKey }]"
@click="handleSend"
>
{{ msgText }}
</span>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button size="medium" type="primary" style="width: 100%"
@click.native.prevent="handleReg">
<span v-if="!regLoading">注 册</span>
<span v-else>注 册 中...</span>
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div class="third-login">
<el-divider>第三方登录</el-divider>
<div class="third-way">
<span class="third-icon" @click="handleSocial()">
<img src="/assets/platform/img/svg/github.svg"
style="width: 32px; height: 32px" />
</span>
<span class="third-icon" @click="handleSocial()">
<img src="/assets/platform/img/svg/wechat.svg"
style="width: 32px; height: 32px" />
</span>
<span class="third-icon" @click="handleSocial()">
<img src="/assets/platform/img/svg/qq.svg"
style="width: 32px; height: 32px" />
</span>
<span class="third-icon" @click="handleSocial()">
<img src="/assets/platform/img/svg/alipay.svg"
style="width: 32px; height: 32px" />
</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<div id="el-login-footer" class="login-footer">
<span>
<a href="https://BudWk.com" target="_blank">BudWk.com</a>
©
<span>{{year}}</span>
</span>
<span>⋅</span>
<a href="https://beian.miit.gov.cn"
target="_blank">${@config.getString("DomainBeian","皖ICP备15001937号-3")}</a>
</div>
</div>
</div>
</body>
<!-- import Vue before Element -->
<!--<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>-->
<script src="https://cdn.staticfile.net/vue/2.7.0/vue.js"></script>
<!-- import JavaScript -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<script nonce="${cspNonce!}">
var MSGINIT = "发送验证码"
var MSGSCUCCESS = "{time}秒后重发"
var MSGTIME = 60
function isPhone(str) {
var reg = /^1[3456789]\d{9}$/
return reg.test(str)
}
new Vue({
el: "#app",
data: function() {
var validateCode = function(rule, value, callback) {
if (value.length !== 4) {
callback(new Error("请输入4位数的验证码"))
} else {
callback()
}
}
return {
activeName: "signin",
msgText: MSGINIT,
msgTime: MSGTIME,
msgKey: false,
year: 2018,
captchaVisible: false,
codeUrl: "",
loginForm: {
username: "",
password: "",
platformKey: "",
grantType: "captcha"
},
loginRules: {
username: [{ required: true, trigger: ["blur", "change"], message: "用户名不能为空" }],
password: [{ required: true, trigger: ["blur", "change"], message: "密码不能为空" }],
platformCaptcha: [{ required: true, trigger: ["blur", "change"], message: "验证码不能为空" }]
},
regForm: {
mobile: "",
password: "",
code: ""
},
regRules: {
mobile: [
{ required: true, message: "请输入手机号码", trigger: ["blur", "change"] },
{ pattern: /^1[3456789]\d{9}$/, message: "请输入正确的手机号码" }
],
password: [
{ required: true, message: "请输入密码", trigger: ["blur", "change"] },
{ min: 8, max: 20, message: "密码长度必须是8-20位", trigger: "change" }
],
code: [{ required: true, trigger: ["blur", "change"], validator: validateCode }]
},
passwordType: "password",
loading: false,
regLoading: false,
showDialog: false
}
},
methods: {
getCode: function() {
var _self = this
$.get(
"${base!}/platform/login/captcha",
function(d) {
_self.codeUrl = d.data.codeUrl
_self.loginForm.platformKey = d.data.key
},
"json"
)
},
showPwd: function() {
if (this.passwordType === "password") {
this.passwordType = ""
} else {
this.passwordType = "password"
}
},
handleSend: function() {
var _self = this
if (!isPhone(_self.regForm.mobile)) {
_self.$message({
message: "请输入手机号码",
type: "error"
})
return false
}
if (_self.msgKey) return
$.get(
"${base!}/platform/login/sendsms?mobile=" + _self.regForm.mobile,
function(d) {
if (d.code === 0) {
_self.$message({
message: "短信发送成功,请注意查收!(实际没发送)",
type: "success",
duration: 8000
})
_self.msgText = MSGSCUCCESS.replace("{time}", _self.msgTime)
_self.msgKey = true
var time = setInterval(() => {
_self.msgTime--
_self.msgText = MSGSCUCCESS.replace("{time}", _self.msgTime)
if (_self.msgTime === 0) {
_self.msgTime = MSGTIME
_self.msgText = MSGINIT
_self.msgKey = false
clearInterval(time)
}
}, 1000)
} else {
_self.$message({
message: d.msg,
type: "error"
})
}
},
"json"
)
},
handleLogin: function() {
var _self = this
_self.$refs["loginForm"].validate(function(valid) {
if (valid) {
$.post(
"${base!}/platform/login/doLogin",
_self.loginForm,
function(d) {
if (d.code == 0) {
window.location.href = "${base!}/platform/home"
} else {
_self.$message({
message: d.msg,
type: "error"
})
_self.getCode()
}
},
"json"
)
}
})
},
handleReg: function() {
var _self = this
_self.$refs["regForm"].validate(function(valid) {
if (valid) {
_self.regLoading = true
$.post(
"${base!}/platform/login/doReg",
_self.regForm,
function(d) {
if (d.code == 0) {
_self.$message({
message: "注册成功,请登录!",
type: "success",
duration: 5000
})
_self.activeName = "signin"
_self.$set(_self.loginForm, "username", _self.regForm.mobile)
_self.$refs["regForm"].resetFields()
} else {
_self.$message({
message: d.msg,
type: "error"
})
}
_self.regLoading = false
},
"json"
)
}
})
},
handleSocial: function(type) {
this.$message.error("敬请期待")
},
errorMsg: function() {
var errorMsg = "${error_message!}"
if (errorMsg) {
this.$message({
message: errorMsg,
type: "error"
})
}
}
},
created: function() {
this.year = new Date().getFullYear()
this.getCode()
this.errorMsg()
}
})
</script>
<style scoped>
#app {
height: 100%;
}
html body {
position: relative;
height: 100vh;
padding: 0;
margin: 0;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #2c3e50;
background: #f6f8f9;
-webkit-font-smoothing: antialiased;
}
.login-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background-image: url(${base!}/assets/platform/img/svg/background.svg);
background-color: #f0f2f5;
}
@media screen and (max-width: 768px) {
.login-left {
display: none;
}
}
a {
color: #1890ff;
text-decoration: none;
cursor: pointer;
}
.login-main {
width: 323px;
height: 455px;
padding: 25px 25px 5px 25px;
background: #ffffff;
}
.title {
margin: 10px auto 40px auto;
font-size: 20px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
text-align: center;
}
.login-form .el-input {
height: 38px;
}
.login-form input {
height: 38px;
}
.login-form .input-icon {
width: 14px;
height: 39px;
margin-left: 2px;
}
.login-tip {
font-size: 13px;
color: #bfbfbf;
text-align: center;
}
.login-code {
display: inline-block;
float: right;
width: 38%;
height: 38px;
}
.login-code img {
vertical-align: middle;
cursor: pointer;
}
.login-footer {
position: fixed;
bottom: 1rem;
width: 100%;
height: 1rem;
font-size: 0.9rem;
font-weight: 600;
line-height: 1rem;
color: #2c3e50;
text-align: center;
}
.tips {
margin-bottom: 10px;
font-size: 14px;
color: #fff;
}
.third-login {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 10px 0 30px 0;
vertical-align: middle;
}
.third-login .third-icon {
margin: 0 20px;
cursor: pointer;
fill: rgba(0, 0, 0, 0.2);
}
.third-way {
display: flex;
justify-content: center;
width: 323px;
}
.third-icon:hover {
fill: rebeccapurple;
}
</style>
</html>
@@ -0,0 +1,186 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>上传文件</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vant/index.css" />
<script src="${base!}/assets/platform/plugins/vant/index.js"></script>
<style>
:root {
--color-primary: #1867b0;
--color-background: #f3f4f6;
--color-text: #333;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background-color: var(--color-background);
color: var(--color-text);
}
#app {
padding: 20px;
max-width: 600px;
margin: auto;
background: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: var(--color-primary);
}
.upload-button {
margin-top: 20px;
text-align: center;
}
.van-uploader__tip {
color: var(--color-primary);
font-size: 14px;
}
.van-button {
background-color: var(--color-primary);
color: white;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<van-notice-bar
left-icon="volume-o"
text="请选择完文件后,一次性上传。"
></van-notice-bar>
<h1>文件上传</h1>
<van-uploader
v-model="fileList"
:before-read="beforeRead"
:after-read="afterRead"
multiple
:accept="accept"
async
:max-count="upload_number"
title="选择文件"
description=""
tip=""
></van-uploader>
<div class="upload-button">
<van-button block type="primary" @click="onSubmit">同步到电脑</van-button>
</div>
</div>
</body>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
fileList: [
// { url: "http://localhost:8080/platform/sys/file/download?id=rn1v1efh2ag0aps59ult3ebudf", isImage: true }
],
// accept: "image/*",
accept: "*",
upload_number: 9,
}
},
methods: {
GetQueryString(name) {
return new URLSearchParams(window.location.search).get(name)
},
beforeRead(file) {
return true
},
afterRead(file) {},
buildUploadPromises(file) {
return
},
onSubmit() {
const loading = this.$toast({
duration: 0,
overlay: true,
forbidClick: true,
message: "上传中",
loadingType: "circular"
})
Promise.all(
this.fileList.map((f) => {
const formData = new FormData()
formData.append("file", f.file)
return axios.post("/platform/sys/h5ScanCodeUploadFile/upload", formData).then((axiosResp) => {
const resp = axiosResp.data
if (resp.code === 0) {
console.log(resp)
f.name = f.file.name
f.size = f.file.size
f.status = null
f.url = resp.data
f.response = resp
f.percentage = 100
f.isImage = true
delete f.file
delete f.content
console.log(f)
} else {
f.status = "fail"
}
})
})
)
.then(() => {
loading.close()
console.log(this.fileList)
this.syncPc()
})
.catch((error) => {
loading.close()
this.$toast.fail(error)
})
},
syncPc() {
if(this.fileList.length===0){
this.$toast('请先选择文件')
return
}
console.log(this.fileList)
axios
.post(
"/platform/sys/h5ScanCodeUploadFile/syncPc",
{
files: JSON.stringify(this.fileList),
timestamp: this.GetQueryString("timestamp")
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
}
)
.then((resp) => {
if (resp.data.code === 0) {
this.$toast.success("上传成功,请关注电脑端信息")
} else {
this.$toast.fail(resp.data.msg)
}
})
}
}
})
</script>
</html>
@@ -0,0 +1,310 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.home .card-title {
font-size: 16px;
font-weight: 600;
font-family: cursive;
}
.quickEntries {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
max-height: 210px;
}
.quickEntries .quickEntryItem img {
max-width: 50px;
}
.quickEntries .quickEntryItem {
max-height: 210px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 10px;
transition:
transform 0.3s ease-in-out,
box-shadow 0.3s ease-in-out;
}
.quickEntries .quickEntryItem span {
margin-top: auto;
}
.quickEntries .quickEntryItem:hover {
cursor: pointer;
background: #f3f3f3;
border-radius: 10px;
}
.quickEntries .quickEntryItem:hover {
animation: shake 0.3s;
}
@keyframes shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0);
}
}
.top-container {
background: #fff;
padding: 20px 10px;
display: grid;
grid-template-columns: 72px 3fr 1fr 1fr;
grid-column-gap: 20px;
align-items: center;
}
.top-container .avatar-wrapper {
width: 72px;
height: 72px;
line-height: 72px;
font-size: 18px;
}
.top-container .avatar-wrapper img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.top-container .basic-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
}
.top-container .basic-wrapper .welcome {
margin-top: 0;
margin-bottom: 0.5em;
color: rgba(0, 0, 0, 0.8509803921568627);
font-weight: 500;
font-size: 14px;
line-height: 1.75rem;
}
.top-container .basic-wrapper .text-secondary {
color: rgba(0, 0, 0, 0.45098039215686275);
}
.top-container .date-wrap {
padding-right: 10px;
text-align: right;
}
.top-container .date-wrap .date {
color: rgba(0, 0, 0, 0.8509803921568627);
font-weight: 500;
font-size: 14px;
line-height: 1.75rem;
margin-bottom: 0.5rem;
}
.activity_list {
width: calc(100% - 300px);
max-width: calc(100% - 300px);
padding: 15px 0;
}
.wrap {
display: flex;
column-gap: 20px;
}
@media only screen and (max-width: 767px) {
.wrap {
flex-direction: column;
}
.wrap .activity_list {
width: 100%;
max-width: 100%;
}
}
.el-calendar {
width: 300px;
}
.el-calendar .el-calendar__header {
padding: 10px 0;
}
.el-calendar__body {
padding: 0 !important;
}
.el-divider {
margin: 0;
}
.el-timeline {
padding: 0;
}
.el-calendar-table td {
border: none !important;
}
.el-calendar-table td.is-selected {
border-radius: 9999px;
background-color: unset !important;
}
.el-calendar-table .el-calendar-day {
width: 30px;
height: 30px;
padding: 2px;
text-align: center;
}
.el-calendar-table .el-calendar-day:hover {
border-radius: 9999px;
}
.tips {
color: red;
font-weight: bold;
}
.el-timeline-item__content {
cursor: pointer;
}
.el-timeline-item__content:hover {
color: #1890ff;
}
</style>
<div id="app" v-cloak>
<div class="padding10X">
<div class="top-container">
<div class="avatar-wrapper">
<img
v-if="!$store.state.user.sex || $store.state.user.sex==='男'"
alt=""
src="/assets/platform/img/home/avatar_boy.png"
class="user-avatar"
/>
<img v-else alt="" src="/assets/platform/img/home/avatar_girl.png" class="user-avatar" />
</div>
<div class="basic-wrapper">
<div class="welcome">{{$store.state.user.username}},开始您一天的工作吧!</div>
<span class="text-secondary">{{$store.state.user.unit.name}} {{$store.state.user.union.name}}</span>
<span class="text-secondary">{{$store.state.user.sex}} {{$store.state.user.userState}} {{$store.state.user.personType}}</span>
</div>
<div class="weather-wrap"></div>
<div class="date-wrap">
<div class="date">{{$moment().format('YYYY年MM月DD日')}}</div>
<div class="week">{{$moment().format('dddd')}}</div>
</div>
</div>
</div>
<el-card class="mt10" shadow="never">
<div class="card-title" slot="header">快速入口</div>
<div class="quickEntries">
<div v-for="item in quickEntries" :key="item.id" @click="pjaxReplace(item.href)" class="quickEntryItem">
<svg-icon :name="item.icon" :size="40" style="color: red"></svg-icon>
<span>{{item.name}}</span>
</div>
</div>
</el-card>
<el-row :gutter="20" class="mt10">
<el-col :span="16"></el-col>
<el-col :span="8">
<el-card shadow="never">
<div class="card-title" slot="header">最新活动</div>
<div v-for="item in activityOptions" :key="item.id">
<div style="width: 100%">
<div style="padding: 14px; background-color: #fff; position: relative">
<div style="display: flex; justify-content: center; align-items: center">
<!-- :src="item.cover"-->
<el-image src="https://img.yzcdn.cn/vant/t-thirt.jpg" alt="" style="margin: 3px 14px 0 0; width: 84px; height: 84px">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<div style="flex: 1">
<div style="line-height: 22px; ont-size: 16px; color: #111">{{item.name}}</div>
<div style="margin-top: 6px; line-height: 20px; color: #999">{{item.startDate}}-{{item.endDate}}</div>
</div>
<el-tooltip content="去报名" placement="top-start">
<div @click="enterActivity(item)">
<svg-icon name="ant-design_tag-twotone" :size="30" style="color: var(--color-primary)"></svg-icon>
</div>
</el-tooltip>
</div>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-button type="primary" icon="el-icon-edit" @click="$refs.icon.showIconModal()">cxccc</el-button>
<icon-selector ref="icon" v-model="icon"></icon-selector>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
components: {
"icon-selector": httpVueLoader("/components/plugins/sysIconSelector/index.vue?v=" + new Date().getTime())
},
store,
data: function () {
return {
quickEntries: [],
activityOptions: [],
icon: "alipay"
}
},
methods: {
listRecommendApp() {
this.$axios.post("/platform/home/listRecommendApp").then((res) => {
if (res.code === 0) {
this.quickEntries = res.data
}
})
},
listActivity() {
this.$axios.post("/platform/h5/listHomeActivity").then((resp) => {
if (resp.code === 0) {
this.activityOptions = resp.data
}
})
},
enterActivity(item) {
this.$store.dispatch("pjaxRoute", item.url)
}
},
created() {
this.listRecommendApp()
this.listActivity()
this.listProcessInstance()
}
})
</script>
<style scoped></style>
<!--# } #-->
@@ -0,0 +1,167 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称" prop="name">
<el-input v-model="pageForm.name" clearable placeholder="请输入名称"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" header-align="center" border style="width: 100%">
<el-table-column label="序号" type="index" width="50px" :method="indexMethod"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="pc端链接" prop="url"></el-table-column>
<el-table-column label="h5端链接" prop="h5Url"></el-table-column>
<el-table-column label="封面" prop="cover" width="80px">
<template slot-scope="scope">
<el-image :src="scope.row.cover" v-if="scope.row.icon" style="width: 30px; height: 30px"></el-image>
</template>
</el-table-column>
<el-table-column label="是否置顶" prop="top" width="100px">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.top" type="success">是</el-tag>
<el-tag size="mini" v-else type="info">否</el-tag>
</template>
</el-table-column>
<el-table-column label="推送大图" prop="push" width="100px">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.push" type="success">是</el-tag>
<el-tag size="mini" v-else type="info">否</el-tag>
</template>
</el-table-column>
<el-table-column label="关联类路径" prop="classPath" show-overflow-tooltip></el-table-column>
<el-table-column label="状态" prop="enable" width="80px">
<template slot-scope="scope">
<i v-if="!scope.row.enable" class="fa fa-circle text-danger ml5"></i>
<i v-else class="fa fa-circle text-success ml5"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="250px">
<template slot-scope="scope">
<el-link type="danger" size="mini" @click="disable(scope.row.id)" v-if="scope.row.enable">关闭
</el-link>
<el-link type="primary" size="mini" @click="enable(scope.row.id)" v-if="!scope.row.enable">开启
</el-link>
<el-link type="primary" size="mini" @click="topUp(scope.row.id)" v-if="!scope.row.top">置顶
</el-link>
<el-link type="danger" size="mini" @click="cancelTopUp(scope.row.id)" v-if="scope.row.top">
取消置顶
</el-link>
<el-link type="primary" size="mini" @click="push(scope.row.id)" v-if="!scope.row.push">推送大图
</el-link>
<el-link type="danger" size="mini" @click="cancelPush(scope.row.id)" v-if="scope.row.push">
取消推送大图
</el-link>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {}
},
methods: {
disable(id) {
this.$confirm("您确定要关闭吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/disable", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
},
enable(id) {
this.$confirm("您确定要开启吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/enable", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
},
topUp(id) {
this.$confirm("您确定要置顶吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/topUp", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
},
cancelTopUp(id) {
this.$confirm("您确定要取消置顶吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/cancelTopUp", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
},
push(id) {
this.$confirm("您确定要推送大图吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/push", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
},
cancelPush(id) {
this.$confirm("您确定要取消推送大图吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/cancelPush", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
}
})
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,223 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<el-row type="flex" style="column-gap: 10px">
<el-select placeholder="请选择日志类型" size="medium" v-model="pageForm.searchType">
<el-option :key="item.value" :label="item.label" :value="item.value"
v-for="item in options"></el-option>
</el-select>
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
:picker-options="pickerOptions"
align="right"
end-placeholder="结束日期"
range-separator="至"
size="medium"
start-placeholder="开始日期"
type="datetimerange"
v-model="searchDate"
value-format="yyyy-MM-dd HH:mm:ss"
></el-date-picker>
<el-button @click="doSearch" icon="el-icon-search" size="medium" type="primary"></el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table
:data="tableData"
:default-sort="{prop: 'createdat', order: 'descending'}"
@sort-change="pageOrder"
header-align="center"
style="width: 100%"
>
<el-table-column align="center" header-align="center" label="操作人" prop="username" sortable>
<template slot-scope="scope">{{scope.row.username+'/'+scope.row.loginname}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作时间" prop="createdat" sortable
width="200">
<template slot-scope="scope">{{formatAt(scope.row.createdat)}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="日志类型" prop="type" width="120">
<template slot-scope="scope">
<span v-if="scope.row.type=='platform'">平台日志</span>
<span v-if="scope.row.type=='info'">登录日志</span>
<span v-if="scope.row.type=='api'">接口日志</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="执行动作" prop="tag" width="200"
show-overflow-tooltip></el-table-column>
<el-table-column label="日志内容" prop="msg" show-overflow-tooltip></el-table-column>
<el-table-column :show-overflow-tooltip="true" label="执行类" prop="src"></el-table-column>
<el-table-column :show-overflow-tooltip="true" label="IP" prop="ip"></el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button @click="openView(scope.row)" size="mini" type="primary">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog title="查看" :visible.sync="dialogVisible" width="70%">
<el-descriptions :column="2" border>
<el-descriptions-item label="操作人">{{row.username}}</el-descriptions-item>
<el-descriptions-item label="操作时间">{{formatAt(row.createdat)}}</el-descriptions-item>
<el-descriptions-item label="日志类型">
<span v-if="row.type=='platform'">平台日志</span>
<span v-if="row.type=='info'">登录日志</span>
<span v-if="row.type=='api'">接口日志</span>
</el-descriptions-item>
<el-descriptions-item label="执行动作" :span="2">{{row.tag}}</el-descriptions-item>
<el-descriptions-item label="日志内容" :span="2">{{row.msg}}</el-descriptions-item>
<el-descriptions-item label="执行类" :span="2">{{row.src}}</el-descriptions-item>
<el-descriptions-item label="IP" :span="2">{{row.ip}}</el-descriptions-item>
<el-descriptions-item label="参数" :span="2">{{row.param}}</el-descriptions-item>
<el-descriptions-item label="结果" :span="2">{{row.result}}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
options: [
{
value: "",
label: "全部类型"
},
{
value: "platform",
label: "平台日志"
},
{
value: "info",
label: "登录日志"
},
{
value: "api",
label: "接口日志"
}
],
pickerOptions: {
disabledDate: function (time) {
var threeMonths = moment().add(-3, "months").valueOf()
return time.getTime() > Date.now() || time.getTime() < threeMonths
},
shortcuts: [
{
text: "最近一周",
onClick: function (picker) {
var end = new Date()
var start = new Date()
var temp = moment().add(-1, "weeks").valueOf()
start.setTime(temp)
picker.$emit("pick", [start, end])
}
},
{
text: "最近一个月",
onClick: function (picker) {
var end = new Date()
var start = new Date()
var temp = moment().add(-1, "months").valueOf()
start.setTime(temp)
picker.$emit("pick", [start, end])
}
},
{
text: "最近三个月",
onClick: function (picker) {
var end = new Date()
var start = new Date()
var temp = moment().add(-3, "months").valueOf()
start.setTime(temp)
picker.$emit("pick", [start, end])
}
}
]
},
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "createdat",
pageOrderBy: "descending",
searchDate: "",
searchType: ""
},
searchDate: [],
dialogVisible: false,
row: {}
}
},
methods: {
formatAt: function (val) {
if (val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
doSearch: function () {
this.pageForm.pageNumber = 1
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
if (self.searchDate) {
self.pageForm.searchDate = self.searchDate.toString()
} else {
self.pageForm.searchDate = ""
}
$.post(
base + "/platform/sys/log/data",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
openView(row) {
this.row = {...row}
this.dialogVisible = true
}
},
created: function () {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,350 @@
<!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/plugins/element-ui/lib/theme-chalk/index.css" />
<link rel="stylesheet" href="${base!}/assets/platform/fonts/themify-icons.css" />
<link rel="stylesheet" href="${base!}/assets/platform/css/common.css" />
</head>
<body>
<!--[if lt IE 9]>
<div class="ie-must-go-die">
<div class="ie-container">
<h1>您正在IE的世界中</h1>
<p>本系统不支持IE9以下版本浏览器访问。</p>
<a href="https://browsehappy.com" target="_blank">您可以点击此处,下载Chrome/Firefox后再次访问本页面。</a>
</div>
</div>
<![endif]-->
<div id="app" v-cloak>
<img src="/assets/platform/img/login_bg.png" class="login-bg-img" alt="" />
<div class="login-container">
<div class="img">
<img src="/assets/platform/img/login_bg_center.svg" />
</div>
<div class="login-box">
<div class="login-form">
<div class="my-3 text-6xl font-semibold">${AppName!}${base!}</div>
<div class="flx-center-center">
<div class="p-12 bg-white rounded-3xl" style="box-sizing: border-box">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" auto-complete="on" label-position="left">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
prefix-icon="el-icon-lock"
></el-input>
</el-form-item>
<el-form-item v-if="!captchaDisabled" prop="platformCaptcha">
<el-input
v-model="loginForm.platformCaptcha"
auto-complete="off"
placeholder="验证码"
style="width: 60%"
@keyup.enter.native="handleLogin"
prefix-icon="el-icon-key"
></el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" />
</div>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width: 100%; --color-primary: #1867b0"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- import Vue before Element -->
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<!-- import JavaScript -->
<script src="${base!}/assets/platform/plugins/element-ui/lib/index.js"></script>
<!-- import axios -->
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
<!-- import Jquery -->
<script src="${base!}/assets/platform/plugins/jquery/jquery.js"></script>
<!-- import SM2 -->
<script src="${base!}/assets/platform/plugins/sm-crypto/sm2.js"></script>
<!-- import commonUtil -->
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
<!-- import jsencrypt-->
<script src="${base!}/assets/platform/plugins/jsencrypt/jsencrypt.min.js"></script>
<script nonce="${cspNonce!}">
// 在加载 lodash 后、使用前插入
const originalDefaultsDeep = window._.defaultsDeep;
window._.defaultsDeep = function(...args) {
// 先对所有参数做原型污染清洗
const cleanArgs = args.map(arg => sanitizeForPrototypePollution(arg));
return originalDefaultsDeep.apply(window._, cleanArgs);
};
function sanitizeForPrototypePollution(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(sanitizeForPrototypePollution);
const clean = {};
for (const key in obj) {
if (!Object.hasOwn(obj, key)) continue;
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
clean[key] = sanitizeForPrototypePollution(obj[key]);
}
return clean;
}
</script>
<script nonce="${cspNonce!}">
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname)
new Vue({
el: "#app",
data() {
return {
codeUrl: "",
captchaDisabled: DISABLE_LOGIN_CAPTCHA,
loginForm: {
username: "",
password: "",
platformKey: "",
grantType: "captcha"
},
loginRules: {
username: [{ required: true, trigger: ["blur", "change"], message: "账号不能为空" }],
password: [{ required: true, trigger: ["blur", "change"], message: "密码不能为空" }],
platformCaptcha: []
},
loading: false,
rasParams: {
publicKey: '',
keyId: '',
expireAt: 0,
},
}
},
methods: {
getCode() {
if (this.captchaDisabled) {
return
}
axios.post("/platform/login/captcha").then((resp) => {
if (resp.data.code === 0) {
this.codeUrl = resp.data.data.codeUrl
this.loginForm.platformKey = resp.data.data.key
}
})
},
handleLogin() {
this.$refs.loginForm.validate(async (valid) => {
if (valid) {
await this.ensurePublicKey()
const { publicKey, keyId } = this.rasParams
if (!publicKey || !keyId) {
this.$message({
message: "Login key unavailable, please refresh and try again",
type: "error"
})
return
}
// 加密密码
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
const encryptedPassword = encrypt.encrypt(this.loginForm.password)
axios
.post("/platform/login/doLogin", null, {
params: {
...this.loginForm,
//password: window.btoa(this.loginForm.password)
password: encryptedPassword,
keyId: keyId
}
})
.then((resp) => {
if (resp.data.code === 0) {
this.$message.success("登录成功")
const redirect = new URLSearchParams(window.location.search).get("redirect")
if (redirect) {
window.location.href = "${base!}" + redirect || ""
} else {
window.location.href = "${base!}/platform/home"
}
} else {
this.getCode()
this.$message({
message: resp.data.msg,
type: "error"
})
}
})
.catch((error) => {
console.log(error)
})
} else {
console.log("cccccc")
}
})
},
// 获取公钥
fetchPublicKey() {
return axios.get("/platform/login/publicKey").then(res => {
this.rasParams = res.data.data
})
},
ensurePublicKey() {
const refreshBeforeMillis = 30 * 1000
if (this.rasParams.publicKey && this.rasParams.keyId && Date.now() < this.rasParams.expireAt - refreshBeforeMillis) {
return Promise.resolve()
}
return this.fetchPublicKey()
},
},
created() {
this.fetchPublicKey()
this.getCode()
}
})
</script>
<style scoped>
#app {
height: 100%;
}
html body {
position: relative;
height: 100vh;
padding: 0;
margin: 0;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #2c3e50;
background: #f6f8f9;
-webkit-font-smoothing: antialiased;
}
@media screen and (max-width: 768px) {
.login-container {
grid-template-columns: 1fr !important;
padding: 0 !important;
}
.login-container .img {
display: none !important;
}
}
.login-container {
width: 100vw;
height: 100vh;
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.login-container .img {
display: flex;
justify-content: flex-end;
align-items: center;
}
.login-container .img img {
z-index: 0;
width: 500px;
}
.login-box {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.login-form {
width: 500px;
}
.font-semibold {
font-weight: 600;
}
.text-6xl {
font-size: 30px;
}
.my-3 {
margin-top: 12px;
margin-bottom: 12px;
}
.p-12 {
padding: 48px;
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.rounded-3xl {
border-radius: 1.5rem;
}
.login-code {
display: inline-block;
float: right;
width: 38%;
height: 38px;
}
.login-code img {
vertical-align: middle;
cursor: pointer;
}
.login-bg-img {
position: fixed;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
z-index: 0;
}
@media screen and (max-width: 768px) {
.login-container > div.img {
display: none;
}
.login-container .login-box .login-form {
width: 100vw !important;
}
}
</style>
</html>
@@ -0,0 +1,261 @@
const SYS_MENU_BASIC_FORM_COMPONENT = {
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-select>
</el-form-item>
<el-form-item prop="parentId" label="上级菜单" label-width="80px">
<el-cascader
:options="menuOptions"
:props="props"
v-model="parentMenu"
placeholder="不选择则为顶级"
clearable
style="width: 100%"
></el-cascader>
</el-form-item>
<el-form-item prop="name" label="菜单名称">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.name"
type="text"></el-input>
</el-form-item>
<el-form-item prop="aliasName" label="菜单别名">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.aliasName"
type="text"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限标识">
<el-input
maxlength="100"
placeholder="权限标识,如 sys.manager.menu"
v-model="formData.permission"
auto-complete="off"
tabindex="5"
type="text"
></el-input>
</el-form-item>
<el-form-item prop="href" label="URL">
<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="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>
<el-form-item prop="type" label="数据权限">
<el-radio-group @change="formRadioChange" v-model="formData.children" size="mini">
<el-radio label="false">无</el-radio>
<el-radio label="true">有</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="formData.children=='true'"
v-for="(menu, index) in formData.buttons"
:label="'权限' + (index+1)"
:key="menu.key"
:prop="'buttons[' + index + ']'"
:rules="{required: true,validator: validateMenu,trigger: ['blur','change']}"
>
<el-row :gutter="2">
<el-col :span="6">
<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-col>
<el-col :span="4">
<el-button @click.prevent="formRemoveMenu(menu)" icon="el-icon-delete"
size="small"></el-button>
</el-col>
</el-row>
</el-form-item>
<el-row v-if="formData.children=='true'" style="text-align: right; padding-right: 17px">
<el-button size="small" @click="formAddMenu" icon="el-icon-circle-plus">添加</el-button>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleDialog = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
`,
data() {
return {
visibleDialog: false,
formData: {
buttons: []
},
formRules: {
moduleId: [{ required: true, message: "必填", trigger: "blur" }],
name: [{ required: true, message: "必填", trigger: "blur" }],
permission: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
menuOptions: [],
parentMenu: [],
props: {
checkStrictly: true,
multiple: false,
label: "name",
value: "id"
},
platform: null,
moduleOptions: []
}
},
methods: {
onOpen(id, platform) {
this.visibleDialog = true
this.platform = platform
this.listFullTree()
this.listModule()
if (id == null) {
this.formData = {
buttons: []
}
} else {
this.detail(id)
}
},
addChildMenu(data, platform) {
this.visibleDialog = true
this.platform = platform
this.formData = {
moduleId: data.moduleId,
buttons: []
}
this.$axios.post("/platform/sys/menu/editMenu/" + data.id).then((res) => {
if (res.code === 0) {
if (res.data.parentIds && res.data.parentIds.length > 0) {
this.parentMenu = res.data.parentIds
} else {
this.parentMenu = []
}
this.parentMenu.push(data.id)
}
})
this.listFullTree()
this.listModule()
},
listFullTree() {
this.$axios.post(loc() + "/fullTree", { platform: this.platform }).then((res) => {
this.menuOptions = res.data
})
},
detail(id) {
this.$axios.post("/platform/sys/menu/editMenu/" + id).then((res) => {
if (res.code === 0) {
this.formData = res.data
this.parentMenu = res.data.parentIds
}
})
},
submit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
if (this.parentMenu) {
this.formData.parentId = this.parentMenu[this.parentMenu.length - 1] || ""
} else {
this.formData.parentId = ""
}
const url = this.formData.id ? "/platform/sys/menu/editMenuDo" : "/platform/sys/menu/addDo"
this.$axios
.post(url, {
menu: JSON.stringify(this.formData),
platform: this.platform
})
.then((res) => {
if (res.code === 0) {
this.$message.success("保存成功")
this.visibleDialog = false
this.$emit("refresh", null)
}
})
}
})
},
formRadioChange: function (val) {
this.formData.children = val
if (val) {
this.formData.buttons = [
{
name: "",
permission: "",
key: Date.now()
}
]
} else {
this.formData.buttons = []
}
},
formAddMenu() {
this.formData.buttons.push({
name: "",
permission: "",
key: Date.now()
})
},
formRemoveMenu(menu) {
const index = this.formData.buttons.indexOf(menu)
if (index !== -1) {
this.formData.buttons.splice(index, 1)
}
},
//验证动态添加行数据
validateMenu: function (rule, value, callback) {
if (value.name === "") {
callback(new Error("权限名称不能为空"))
} else if (value.permission === "") {
callback(new Error("权限标识不能为空"))
} else {
callback()
}
},
listModule() {
this.$axios.post(loc() + "/listModule", { platform: this.platform }).then((res) => {
if (res.code === 0) {
this.moduleOptions = res.data
}
})
}
}
}
@@ -0,0 +1,383 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.context-menu {
position: fixed;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
z-index: 3000;
list-style: none;
padding: 5px 0;
margin: 0;
font-size: 14px;
}
.context-menu li {
padding: 7px 20px;
cursor: pointer;
}
.context-menu li:hover {
background: #f2f6fc;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="平台">
<el-select v-model="pageForm.platform" :clearable="false" @change="listModule();doSearch();">
<el-option v-for="item in dict.type.SYS_MENU_PLATFORM" :value="item.code" :key="item.value" :label="item.label"></el-option>
</el-select>
</search-item>
<search-item label="模块">
<el-select v-model="pageForm.moduleId" clearable placeholder="请选择模块">
<el-option v-for="item in moduleOptions" :value="item.id" :key="item.id" :label="item.name"></el-option>
</el-select>
</search-item>
<search-item label="菜单名称">
<el-input v-model="pageForm.menuName" clearable placeholder="请填写名称查询" style="width: 100%"></el-input>
</search-item>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool>
<el-button @click="$refs.basicFormRef.onOpen(null,pageForm.platform)" size="small" type="primary" icon="el-icon-plus">新建菜单</el-button>
<el-button @click="$refs.sortRef.onOpen(pageForm.platform)" size="small" type="primary" icon="el-icon-sort">排序</el-button>
<el-button @click="$refs.recommendSettingRef.onOpen(pageForm.platform, 'app')" size="small" type="primary" icon="el-icon-monitor">
首页推荐应用
</el-button>
<el-button @click="$refs.recommendSettingRef.onOpen(pageForm.platform, 'service')" size="small" type="primary" icon="el-icon-monitor">
首页推荐服务
</el-button>
</table-tool>
<el-table
:data="tableData"
:highlight-current-row="true"
:key="tableKey"
:load="loadChild"
@row-contextmenu="openMenu"
:expand-row-keys="expandedRowKeysRenew"
@expand-change="handleExpandChange"
ref="tableData"
style="width: 100%"
lazy
row-key="id"
>
<el-table-column align="left" header-align="left" label="菜单名称" prop="name" width="200"></el-table-column>
<el-table-column align="center" header-align="center" label="排序编码" prop="location" width="120">
<template slot-scope="{row}">
<el-input-number
v-model="row.location"
:controls="false"
:min="0"
:precision="0"
size="mini"
style="width: 88px"
@change="updateLocation(row)"
></el-input-number>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="菜单图标" prop="icon" width="100">
<template slot-scope="scope">
<template v-if="pageForm.platform==='PC'">
<svg-icon :name="scope.row.icon" v-if="scope.row.icon" :size="20"></svg-icon>
</template>
<template v-else-if="pageForm.platform==='H5'">
<el-image :src="scope.row.icon" v-if="scope.row.icon" style="width: 30px; height: 30px"></el-image>
</template>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="URL" prop="href"></el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="资源类型" prop="type">
<template slot-scope="scope">
<i style="font-weight: bold" v-if="scope.row.type=='menu'">菜单</i>
<i v-if="scope.row.type=='data'">数据</i>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="所属模块" prop="moduleId">
<template slot-scope="scope">
<i style="font-weight: bold">{{moduleOptions.find(item=>item.id==scope.row.moduleId)?.name}}</i>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="权限标识" prop="permission"></el-table-column>
<el-table-column align="center" header-align="center" label="启用状态" prop="disabled">
<template slot-scope="scope">
<i class="fa fa-circle text-danger ml5" v-if="scope.row.disabled"></i>
<i class="fa fa-circle text-success ml5" v-if="!scope.row.disabled"></i>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="操作" prop="website" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<!-- <el-dropdown-item :command="{type:'add',id:scope.row.id,name:scope.row.name}" v-if="scope.row.type=='menu'">-->
<!-- 添加子菜单-->
<!-- </el-dropdown-item>-->
<el-dropdown-item :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}" divided>
启用
</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">禁用</el-dropdown-item>
<el-dropdown-item :command="{type:'editMenu',id:scope.row.id,name:scope.row.name}" divided v-if="scope.row.type=='menu'">
修改
</el-dropdown-item>
<el-dropdown-item :command="{type:'editData',id:scope.row.id,name:scope.row.name}" divided v-if="scope.row.type=='data'">
修改
</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,name:scope.row.name}" v-if="scope.row.path!='0001'">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 右键菜单 -->
<ul v-show="menuVisible" class="context-menu" :style="{left: menuLeft + 'px', top: menuTop + 'px'}">
<li @click="handleAddMenu(currentRow)">新建菜单</li>
<li v-if="showAddMenu" @click="handleAddChildMenu(currentRow)">添加子菜单</li>
<li @click="handleEdit(currentRow)">编辑</li>
<li @click="handleDelete(currentRow)">删除</li>
</ul>
</el-card>
<basic-form ref="basicFormRef" @refresh="loadChildByExpandedKeys"></basic-form>
<permission-form ref="permissionFormRef" @load-child="loadChildByExpandedKeys"></permission-form>
<sort ref="sortRef" @refresh="doSearch"></sort>
<recommend-setting ref="recommendSettingRef" @refresh="doSearch"></recommend-setting>
</div>
<script nonce="${cspNonce!}">
<!--#include("permissionForm.js"){}#-->
<!--#include("basicForm.js"){}#-->
<!--#include("sort.js"){}#-->
<!--#include("recommendSetting.js"){}#-->
new Vue({
el: "#app",
dicts: ["SYS_MENU_PLATFORM"],
components: {
"basic-form": SYS_MENU_BASIC_FORM_COMPONENT,
"permission-form": SYS_MENU_PERMISSION_FORM_COMPONENT,
sort: SYS_MENU_SORT_COMPONENT,
"recommend-setting": SYS_MENU_RECOMMEND_SETTING_COMPONENT
},
data: function () {
return {
pageForm: {},
tableData: [],
tableKey: "",
moduleOptions: [],
// 保存展开状态的数组
expandedRowKeysRenew: [],
tableTreeRefreshTool: [],
// 右键打开菜单相关
currentRow: null,
menuVisible: false,
menuLeft: 0,
menuTop: 0,
showAddMenu: false,
}
},
methods: {
/* 右键行 */
openMenu(row, column, event) {
this.showAddMenu = row.type === 'menu';
// 阻止浏览器默认菜单
event.preventDefault();
this.currentRow = row;
this.menuVisible = true;
this.$nextTick(() => {
const menu = this.$el.querySelector('.context-menu');
const h = menu.offsetHeight || 100;
const sh = document.documentElement.clientHeight;
let top = event.clientY;
if (top + h > sh) top -= h;
this.menuLeft = event.clientX;
this.menuTop = top;
});
},
// 新建菜单
handleAddMenu(row) {
console.log(row)
this.$refs.basicFormRef.onOpen(null, this.pageForm.platform)
},
handleAddChildMenu(row) {
this.$refs.basicFormRef.addChildMenu(row, this.pageForm.platform)
},
handleEdit(row) {
// 判断子菜单的type是权限还是菜单
if (row.type === 'menu') {
this.$refs.basicFormRef.onOpen(row.id, this.pageForm.platform)
} else {
this.$refs.permissionFormRef.onOpen(row.id)
}
},
handleDelete(row) {
// 删除逻辑
console.log(row)
this.$confirm("此操作将删除 " + row.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/menu/delete/" + row.id).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.loadChildByExpandedKeys()
}
})
})
},
doSearch() {
this.initTreeTable()
},
initTreeTable() {
this.$axios
.post("/platform/sys/menu/child", {
pid: "",
...this.pageForm
})
.then((res) => {
if (res.code === 0) {
this.tableData = res.data
this.tableKey = +new Date()
}
})
},
loadChild: function (tree, treeNode, resolve) {
// 在之前声明的全局变量中,增加一个key为 本条数据的id,id可替换为你数据中的任意唯一值
this.tableTreeRefreshTool[tree.id] = {}
// 重要!保存resolve方法,以便后续使用
this.tableTreeRefreshTool[tree.id].resolve = resolve
// 记录展开次数,具体作用后续介绍
this.tableTreeRefreshTool[tree.id].expandCount = 0
const url = "/platform/sys/menu/child"
this.$axios.post(url, { pid: tree.id }).then((res) => {
if (res.code === 0) {
resolve(res.data)
}
})
},
dropdownCommand(command) {
//监听下拉框事件
// if ("add" === command.type) {
// this.$refs.basicFormRef.onOpenAdd(command.id, this.pageForm.platform)
// }
if ("editMenu" === command.type) {
this.$refs.basicFormRef.onOpen(command.id, this.pageForm.platform)
}
if ("editData" === command.type) {
this.$refs.permissionFormRef.onOpen(command.id)
}
if ("enable" === command.type || "disable" === command.type) {
this.$axios.post("/platform/sys/menu/" + command.type + "/" + command.id).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
if ("disable" === command.type) {
command.row.disabled = true
}
if ("enable" === command.type) {
command.row.disabled = false
}
}
})
}
if ("delete" === command.type) {
this.$confirm("此操作将删除 " + command.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/menu/delete/" + command.id).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.loadChildByExpandedKeys()
}
})
})
}
},
handleExpandChange(row, expanded) {
if ((row.parentId === "" || row.parentId == null) && !expanded) {
this.expandedRowKeysRenew = []
return
}
if (expanded) {
this.expandedRowKeysRenew.push(row.id)
} else {
const index = this.expandedRowKeysRenew.findIndex((v) => v === row.id)
this.expandedRowKeysRenew.splice(index, 1)
}
},
loadChildByExpandedKeys() {
if (this.expandedRowKeysRenew && this.expandedRowKeysRenew.length > 0) {
this.expandedRowKeysRenew.forEach((v) => {
const curr = this.tableTreeRefreshTool[v]
// api请求
$.get("/platform/sys/menu/child", { pid: v }).then((res) => {
curr.resolve(res.data)
})
})
} else {
this.initTreeTable()
}
},
listModule() {
this.$axios.post(loc() + "/listModule", { platform: this.pageForm.platform }).then((res) => {
if (res.code === 0) {
this.moduleOptions = res.data
}
})
},
updateLocation(row) {
const location = Number(row.location)
if (!Number.isInteger(location) || location < 0) {
this.$message.error("排序编码必须是非负整数")
return
}
$.post("/platform/sys/menu/updateLocation", {
id: row.id,
location
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
if (row.parentId) {
this.loadChildByExpandedKeys()
} else {
this.initTreeTable()
}
}
})
}
},
mounted() {
// 点击任意处关闭菜单
document.addEventListener('click', () => (this.menuVisible = false));
},
beforeDestroy() {
document.removeEventListener('click', () => (this.menuVisible = false));
},
created() {
setTimeout(() => {
if (this.dict.type.SYS_MENU_PLATFORM) {
this.pageForm.platform = this.dict.type.SYS_MENU_PLATFORM[0].code
this.listModule()
this.initTreeTable()
}
}, 500)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,677 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<el-row type="flex" justify="end">
<el-button @click="openAdd" size="medium" type="primary">
<i class="ti-plus"></i>
新建菜单
</el-button>
<el-button @click="openSort" size="medium" type="primary">排序</el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table
:data="tableData"
:highlight-current-row="true"
:key="tableKey"
:load="loadChild"
lazy
row-key="id"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
>
<el-table-column
:show-overflow-tooltip="true"
align="left"
header-align="center"
label="菜单名称"
prop="name"
width="200"
></el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="URL" prop="href"></el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="资源类型" prop="type">
<template slot-scope="scope">
<i style="font-weight: bold" v-if="scope.row.type=='menu'">菜单</i>
<i v-if="scope.row.type=='data'">数据</i>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="权限标识" prop="permission"></el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="启用状态" prop="disabled">
<template slot-scope="scope">
<i class="fa fa-circle text-danger ml5" v-if="scope.row.disabled"></i>
<i class="fa fa-circle text-success ml5" v-if="!scope.row.disabled"></i>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" align="center" header-align="center" label="操作" prop="website" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'add',id:scope.row.id,name:scope.row.name}" v-if="scope.row.type=='menu'">
添加子菜单
</el-dropdown-item>
<el-dropdown-item :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}" divided>
启用
</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">禁用</el-dropdown-item>
<el-dropdown-item :command="{type:'editMenu',id:scope.row.id,name:scope.row.name}" divided v-if="scope.row.type=='menu'">
修改
</el-dropdown-item>
<el-dropdown-item :command="{type:'editData',id:scope.row.id,name:scope.row.name}" divided v-if="scope.row.type=='data'">
修改
</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,name:scope.row.name}" v-if="scope.row.path!='0001'">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="新建菜单" :visible.sync="addDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="parentId" label="上级菜单" label-width="80px">
<el-cascader
v-if="!isAddFromSub"
:options="menuOptions"
:props="props"
v-model="parentMenu"
placeholder="不选择则为顶级"
style="width: 100%"
></el-cascader>
<el-input v-if="isAddFromSub" type="text" v-model="formData.parentName" :disabled="true"></el-input>
</el-form-item>
<el-form-item prop="name" label="菜单名称">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.name" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="aliasName" label="菜单别名">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.aliasName" auto-complete="off" tabindex="4" type="text"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限标识">
<el-input
maxlength="100"
placeholder="权限标识,如 sys.manager.menu"
v-model="formData.permission"
auto-complete="off"
tabindex="5"
type="text"
></el-input>
</el-form-item>
<el-form-item prop="href" label="URL">
<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" placeholder="图标" v-model="formData.icon" auto-complete="off" tabindex="7" type="text"></el-input>
<span :class="formData.icon"></span>
</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>
<el-form-item prop="type" label="数据权限">
<el-radio-group @change="formRadioChange" v-model="formData.children" size="mini">
<el-radio label="false">无</el-radio>
<el-radio label="true">有</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="formData.children=='true'"
v-for="(menu, index) in formData.buttons"
:label="'权限' + (index+1)"
:key="menu.key"
:prop="'buttons[' + index + ']'"
:rules="{required: true,validator: validateMenu,trigger: ['blur','change']}"
>
<el-row :gutter="2">
<el-col :span="6">
<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-col>
<el-col :span="4">
<el-button @click.prevent="formRemoveMenu(menu)" icon="el-icon-delete" size="small"></el-button>
</el-col>
</el-row>
</el-form-item>
<el-row v-if="formData.children=='true'" style="text-align: right; padding-right: 17px">
<el-button size="small" @click="formAddMenu" icon="el-icon-circle-plus">添加</el-button>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改菜单" :visible.sync="editMenuDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="editMenuForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="parentId" label="上级菜单" label-width="80px">
<el-input type="text" v-model="formData.parentName" :disabled="true"></el-input>
</el-form-item>
<el-form-item prop="name" label="菜单名称">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.name" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="aliasName" label="菜单别名">
<el-input maxlength="100" placeholder="菜单名称" v-model="formData.aliasName" auto-complete="off" tabindex="4" type="text"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限标识">
<el-input
maxlength="100"
placeholder="权限标识,如 sys.manager.menu"
v-model="formData.permission"
auto-complete="off"
tabindex="5"
type="text"
></el-input>
</el-form-item>
<el-form-item prop="href" label="URL">
<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" placeholder="图标" v-model="formData.icon" auto-complete="off" tabindex="7" type="text"></el-input>
<span :class="formData.icon"></span>
</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>
<el-form-item prop="type" label="数据权限">
<el-radio-group @change="formRadioChange" v-model="formData.children" size="mini">
<el-radio label="false">无</el-radio>
<el-radio label="true">有</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="formData.children=='true'"
v-for="(menu, index) in formData.buttons"
:label="'权限' + (index+1)"
:key="menu.key"
:prop="'buttons[' + index + ']'"
:rules="{required: true,validator: validateMenu,trigger: ['blur','change']}"
>
<el-row :gutter="2">
<el-col :span="6">
<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-col>
<el-col :span="4">
<el-button @click.prevent="formRemoveMenu(menu)" icon="el-icon-delete" size="small"></el-button>
</el-col>
</el-row>
</el-form-item>
<el-row v-if="formData.children=='true'" style="text-align: right; padding-right: 17px">
<el-button size="small" @click="formAddMenu" icon="el-icon-circle-plus">添加</el-button>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editMenuDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEditMenu">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改权限" :visible.sync="editDataDialogVisible" width="50%">
<el-form :model="formData2" ref="editDataForm" :rules="formRules2" size="small" label-width="80px">
<el-form-item prop="name" label="权限名称">
<el-input maxlength="100" placeholder="权限名称" v-model="formData2.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限标识">
<el-input
maxlength="100"
placeholder="权限标识,如 sys.manager.menu.add"
v-model="formData2.permission"
auto-complete="off"
tabindex="2"
type="text"
></el-input>
</el-form-item>
<el-form-item prop="disabled" label="启用状态">
<el-switch v-model="formData2.disabled" active-color="#ff4949" inactive-color="#13ce66"></el-switch>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDataDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEditData">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="菜单排序" :visible.sync="sortDialogVisible" width="50%">
<el-tree ref="sortMenuTree" :data="sortMenuData" draggable :allow-drop="sortAllowDrop" node-key="id" :props="defaultProps">
<span class="custom-tree-node" slot-scope="scope">
<span>{{ scope.node.label }}</span>
</span>
</el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="sortDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doSort">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editMenuDialogVisible: false,
editDataDialogVisible: false,
sortDialogVisible: false,
isAddFromSub: false,
tableData: [], //后台取出的数据
tableKey: "",
formData: {
id: "",
parentId: "",
parentName: "",
type: "menu",
target: "data-pjax",
children: "false",
buttons: []
},
formRules: {
name: [{ required: true, message: "菜单名称", trigger: "blur" }],
permission: [{ required: true, message: "权限标识", trigger: ["blur", "change"] }]
},
formData2: {},
formRules2: {
name: [{ required: true, message: "权限名称", trigger: "blur" }],
permission: [{ required: true, message: "权限标识", trigger: ["blur", "change"] }]
},
menuOptions: [],
parentMenu: [],
sortMenuData: [],
defaultProps: {
children: "children",
label: "label"
},
props: {
lazy: true,
checkStrictly: true,
multiple: false,
lazyLoad: function (node, resolve) {
var url = base + "/platform/sys/menu/tree"
$.post(
url,
{ pid: node.value },
function (data) {
if (data.code == 0) {
resolve(data.data)
}
},
"json"
)
}
}
}
},
methods: {
//验证动态添加行数据
validateMenu: function (rule, value, callback) {
if (value.name == "") {
callback(new Error("权限名称不能为空"))
} else if (value.permission == "") {
callback(new Error("权限标识不能为空"))
} else {
callback()
}
},
formRadioChange: function (val) {
this.formData.children = val
if ("true" == val) {
this.formData.buttons = [
{
name: "",
permission: "",
key: Date.now()
}
]
} else {
this.formData.buttons = []
}
},
formRemoveMenu: function (menu) {
var index = this.formData.buttons.indexOf(menu)
if (index !== -1) {
this.formData.buttons.splice(index, 1)
}
},
formAddMenu: function () {
this.formData.buttons.push({
name: "",
permission: "",
key: Date.now()
})
},
//排序树加载
sortMenuLoad: function () {
var self = this
$.post(
base + "/platform/sys/menu/menuAll",
{},
function (data) {
if (data.code == 0) {
self.sortMenuData = data.data
}
},
"json"
)
},
//排序树控制不可跨级拖拽
sortAllowDrop: function (moveNode, inNode, type) {
if (moveNode.data.parentId == inNode.data.parentId) {
return type == "prev"
}
},
//打开排序功能
openSort: function () {
this.sortDialogVisible = true
this.sortMenuLoad()
},
//递归获取所有的ID值
getTreeIds: function (ids, data) {
var self = this
data.forEach(function (obj) {
ids.push(obj.id)
if (obj.children && obj.children.length > 0) {
self.getTreeIds(ids, obj.children)
}
})
},
//提交排序后的数据
doSort: function () {
var self = this
var ids = []
const loading = this.$loading({
lock: true,
text: "正在提交...",
spinner: "el-icon-loading"
})
self.getTreeIds(ids, self.sortMenuData)
$.post(
base + "/platform/sys/menu/sortDo",
{ ids: ids.toString() },
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.sortDialogVisible = false
self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
loading.close()
},
"json"
)
},
openAdd: function () {
var self = this
self.isAddFromSub = false
self.addDialogVisible = true
self.parentMenu = []
self.menuOptions = []
//表单还原为初始值
self.formData = {
id: "",
parentId: "",
parentName: "",
type: "menu",
target: "data-pjax",
children: "false",
buttons: []
}
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd: function () {
var self = this
var url = base + "/platform/sys/menu/addDo"
self.$refs["addForm"].validate(function (valid) {
if (valid) {
//表单验证通过
if (!self.isAddFromSub) {
//如果不是添加子菜单,则从级联选择框取父节点ID
self.formData.parentId = self.parentMenu[self.parentMenu.length - 1] || ""
}
$.post(
url,
{
menu: JSON.stringify(self.formData),
buttons: JSON.stringify(self.formData.buttons)
},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
doEditMenu: function () {
var self = this
var url = base + "/platform/sys/menu/editMenuDo"
self.$refs["editMenuForm"].validate(function (valid) {
if (valid) {
//表单验证通过
$.post(
url,
{
menu: JSON.stringify(self.formData),
buttons: JSON.stringify(self.formData.buttons)
},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.editMenuDialogVisible = false
self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
doEditData: function () {
var self = this
var url = base + "/platform/sys/menu/editDataDo"
self.$refs["editDataForm"].validate(function (valid) {
if (valid) {
//表单验证通过
$.post(
url,
self.formData2,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.editDataDialogVisible = false
self.initTreeTable()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
initTreeTable: function () {
var self = this
var url = base + "/platform/sys/menu/child"
$.post(
url,
{ pid: "" },
function (data) {
if (data.code == 0) {
self.tableData = data.data
self.tableKey = +new Date()
}
},
"json"
)
},
loadChild: function (tree, treeNode, resolve) {
var url = base + "/platform/sys/menu/child"
$.post(
url,
{ pid: tree.id },
function (data) {
if (data.code == 0) {
resolve(data.data)
}
},
"json"
)
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("add" == command.type) {
self.openAdd()
self.formData.parentId = command.id
self.formData.parentName = command.name
self.isAddFromSub = true
}
if ("editMenu" == command.type) {
$.post(
base + "/platform/sys/menu/editMenu/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editMenuDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("editData" == command.type) {
$.post(
base + "/platform/sys/menu/editData/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData2 = data.data //加载后台表单数据
self.editDataDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("enable" == command.type || "disable" == command.type) {
$.post(
base + "/platform/sys/menu/" + command.type + "/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
if ("disable" == command.type) {
command.row.disabled = true
}
if ("enable" == command.type) {
command.row.disabled = false
}
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除 " + command.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/menu/delete/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
// self.initTreeTable();
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
}
},
created: function () {
this.initTreeTable()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,59 @@
const SYS_MENU_PERMISSION_FORM_COMPONENT = {
template: `
<el-dialog title="修改权限" :visible.sync="editDataDialogVisible" width="50%">
<el-form :model="formData" ref="formRef" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="name" label="权限名称">
<el-input maxlength="100" placeholder="权限名称" v-model="formData.name" auto-complete="off"
tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限标识">
<el-input
maxlength="100"
placeholder="权限标识,如 sys.manager.menu.add"
v-model="formData.permission"
auto-complete="off"
tabindex="2"
type="text"
></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDataDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEditData">确 定</el-button>
</span>
</el-dialog>
`,
data() {
return {
editDataDialogVisible: false,
formData: {},
formRules: {}
}
},
methods: {
onOpen(id) {
this.editDataDialogVisible = true
this.$axios.post("/platform/sys/menu/editData/" + id, {}).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
},
doEditData() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios.post("/platform/sys/menu/editDataDo", this.formData).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.editDataDialogVisible = false
this.$emit("load-child", null)
}
})
}
})
}
}
}
@@ -0,0 +1,151 @@
const SYS_MENU_RECOMMEND_SETTING_COMPONENT = {
template: `
<el-dialog title="推荐配置" :visible.sync="dialogVisible" width="50%">
<el-row :gutter="20">
<el-col :span="12">
<el-input
v-model="menuKeyword"
clearable
prefix-icon="el-icon-search"
placeholder="请输入菜单名称搜索"
style="margin-bottom: 10px">
</el-input>
<el-tree ref="menuTree"
style="max-height: 450px;overflow-y: auto"
:data="menuData"
:check-on-click-node="true"
:filter-node-method="filterMenuNode"
node-key="id"
show-checkbox
@check-change="checkChange"
:props="defaultProps">
<span class="custom-tree-node" slot-scope="scope">
<span>{{ scope.node.label }}</span>
</span>
</el-tree>
</el-col>
<el-col :span="12">
<el-table :data="checkedNodes" max-height="500px">
<el-table-column type="index"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="图标" prop="icon">
<template scope="{row}">
<svg-icon :name="row.icon" v-if="platform==='PC'"></svg-icon>
<img v-else :src="row.icon" alt="" style="width: 30px;height: 30px">
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submit">确 定</el-button>
</span>
</el-dialog>
`,
data() {
return {
platform: null,
dialogVisible: false,
menuData: [],
menuKeyword: '',
defaultProps: {
children: "children",
label: "label"
},
checkedNodes: [],
checkedKeys: [],
type: '',
}
},
watch: {
menuKeyword(value) {
if (this.$refs.menuTree) {
this.$refs.menuTree.filter(value)
}
}
},
methods: {
onOpen(platform, type) {
this.platform = platform
this.type = type
this.checkedKeys = []
this.$set(this, 'menuKeyword', '')
this.dialogVisible = true
this.listTree()
},
// 仅过滤左侧菜单树的显示,不修改节点勾选状态,保证搜索前的选择仍能提交。
filterMenuNode(value, data) {
if (!value) {
return true
}
return (data.label || data.name || '').indexOf(value) !== -1
},
listTree() {
this.$axios.post("/platform/sys/menu/menuAll", { platform: this.platform }).then((res) => {
if (res.code === 0) {
this.setTreeDisabled(res.data)
this.menuData = res.data
console.log(this.checkedKeys)
//设置选中的节点
this.$refs.menuTree.setCheckedKeys(this.checkedKeys)
//获取选中的节点
this.$nextTick(() => {
this.checkedNodes = this.$refs.menuTree.getCheckedNodes(true, false)
})
}
})
},
setTreeDisabled(data) {
data.forEach((item) => {
if ((this.type === 'service' && item.isRecommendService) || (this.type === 'app' && item.isRecommendApp)) {
this.checkedKeys.push(item.id)
}
if (!item.href && item.type === "menu" && this.type === 'service') {
item.disabled = true
}
if (this.type === 'app') {
delete item.children
}
if (item.children && item.children.length) {
this.setTreeDisabled(item.children)
}
})
},
checkChange(currObj, isChecked) {
this.checkedNodes = this.$refs.menuTree.getCheckedNodes(true, false)
},
submit() {
this.$confirm("您确定要提交吗?", "提示", {
type: "warning"
}).then(() => {
// 提交时从完整菜单树读取选中节点,搜索隐藏的已选节点也必须保留。
const selectedNodes = this.$refs.menuTree
? this.$refs.menuTree.getCheckedNodes(true, false)
: this.checkedNodes
this.$axios
.post("/platform/sys/menu/updateRecommendSetting", {
menuIds: JSON.stringify(selectedNodes.map((item) => item.id)),
platform: this.platform,
type: this.type
})
.then((resp) => {
if (resp.code === 0) {
this.checkedKeys = []
this.listTree()
this.$message.success(resp.msg)
this.$emit("refresh", null)
}
})
})
}
},
created() {}
}
@@ -0,0 +1,77 @@
const SYS_MENU_SORT_COMPONENT = {
template: `
<el-dialog title="菜单排序" :visible.sync="sortDialogVisible" width="50%">
<el-tree ref="sortMenuTree" :data="sortMenuData" draggable :allow-drop="sortAllowDrop" node-key="id"
:props="defaultProps">
<span class="custom-tree-node" slot-scope="scope">
<span>{{ scope.node.label }}</span>
</span>
</el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="sortDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doSort">确 定</el-button>
</span>
</el-dialog>
`,
data() {
return {
sortDialogVisible: false,
sortMenuData: [],
defaultProps: {
children: "children",
label: "label"
},
platform: 'PC'
}
},
methods: {
onOpen(platform) {
this.platform = platform
this.sortDialogVisible = true
this.$axios.post("/platform/sys/menu/menuAll", { platform }).then((res) => {
if (res.code === 0) {
this.sortMenuData = res.data
}
})
},
//排序树控制不可跨级拖拽
sortAllowDrop: function (moveNode, inNode, type) {
if (moveNode.data.parentId == inNode.data.parentId) {
return type == "prev"
}
},
//提交排序后的数据
doSort() {
const ids = []
const loading = this.$loading({
lock: true,
text: "正在提交...",
spinner: "el-icon-loading"
})
this.getTreeIds(ids, this.sortMenuData)
this.$axios
.post("/platform/sys/menu/sortDo", { ids: ids.toString(), platform: this.platform })
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.sortDialogVisible = false
this.$emit("refresh", null)
}
})
.finally(() => {
loading.close()
})
},
//递归获取所有的ID值
getTreeIds(ids, data) {
const self = this
data.forEach(function (obj) {
ids.push(obj.id)
if (obj.children && obj.children.length > 0) {
self.getTreeIds(ids, obj.children)
}
})
}
}
}
@@ -0,0 +1,188 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="平台">
<el-select v-model="pageForm.platform" :clearable="false" @change="doSearch">
<el-option v-for="item in dict.type.SYS_MENU_PLATFORM" :value="item.code" :key="item.value" :label="item.label"></el-option>
</el-select>
</search-item>
<search-item label="名称">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入名称" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button size="small" type="primary" @click="openAdd">添加模块</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" align="center" header-align="center">
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns"
>
<template scope="{row}" v-if="column.prop=='icon'">
<el-image lazy :src="row.icon" style="height: 30px; width: 30px" fit="cover">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</template>
<template scope="{row}" v-else-if="column.prop=='faIcon'">
<i :class="row.faIcon"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="200px">
<template scope="{row}">
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button @click="doDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :visible.sync="formDialogVisible" :title="formData.id ? '编辑模块' : '添加模块'">
<el-form :model="formData" :rules="formRules" label-width="120px" ref="form">
<el-form-item label="所属平台" prop="platform">
<el-select v-model="formData.platform" :clearable="false" @change="doSearch" placeholder="请选择平台">
<el-option v-for="item in dict.type.SYS_MENU_PLATFORM" :value="item.code" :key="item.value" :label="item.label"></el-option>
</el-select>
</el-form-item>
<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">
<file-upload
:value.sync="formData.icon"
:upload_number="1"
upload_mode="image"
upload_result_category="interval"
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>
</el-form>
<el-row type="flex" justify="end">
<el-button @click="formDialogVisible=false">取消</el-button>
<el-button type="primary" @click="doSubmit">确定</el-button>
</el-row>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
dicts: ["SYS_MENU_PLATFORM"],
mixins: [initTableMixins],
data() {
return {
formDialogVisible: false,
formData: {
id: null,
name: null,
icon: null,
sortNum: null
},
formRules: {
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"] }],
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: "faIcon" }
],
pageForm: {}
}
},
watch: {
"formData.icon"(val) {
this.$refs.form.validateField("icon", (errMsg) => {
console.log(errMsg)
})
}
},
methods: {
async openAdd() {
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.formData = {
sortNum: this.tableData.length + 1
}
this.formDialogVisible = true
},
openEdit(row) {
this.formDialogVisible = true
this.$nextTick(() => {
this.formData = { ...row }
})
},
async doSubmit() {
const valid = await this.$refs.form.validate()
if (!valid) return
const { id } = this.formData
const url = loc() + (id ? "/update" : "/insert")
const resp = await this.$axios.post(url, this.formData)
if (resp.code === 0) {
this.pageData()
this.formDialogVisible = false
this.$message.success(resp.msg)
}
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post(loc() + "/delete", { id }).then((resp) => {
if (resp.code === 0) {
this.doSearch()
this.$message.success(resp.msg)
}
})
})
.catch(() => {})
}
},
created() {
setTimeout(() => {
if (this.dict.type.SYS_MENU_PLATFORM) {
this.pageForm.platform = this.dict.type.SYS_MENU_PLATFORM[0].code
this.pageData()
}
}, 500)
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,182 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.el-msg-active {
background-color: #546478;
color: #ffffff;
}
.note {
width: 100%;
height: 320px;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="消息类型:">
<el-select clearable placeholder="请选择消息类型" v-model="pageForm.searchType" style="width: 100%" @change="doSearch">
<el-option label="全部类型消息" value=""></el-option>
<el-option label="系统消息" value="system"></el-option>
<el-option label="用户消息" value="user"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool label="消息列表">
<el-button size="small" type="primary" icon="el-icon-plus" @click="openAdd">发送消息</el-button>
</table-tool>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column align="center" header-align="center" label="消息类型" prop="type" sortable>
<template slot-scope="scope">
<span v-if="'system'==scope.row.type">系统消息</span>
<span v-if="'user'==scope.row.type">用户消息</span>
</template>
</el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="标题" prop="title"></el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="消息内容" prop="note">
<template slot-scope="scope">
<el-link type="primary" @click="openViewNote(scope.row)">查看</el-link>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="总接收用户" prop="all_num">
<template slot-scope="scope">
<el-link type="primary" @click="viewUserList('all',scope.row.id)">{{scope.row.all_num}}</el-link>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="未读用户" prop="unread_num">
<template slot-scope="scope">
<el-link type="primary" @click="viewUserList('unread',scope.row.id)">{{scope.row.unread_num}}</el-link>
</template>
</el-table-column>
<el-table-column header-align="center" label="发送时间" prop="createdAt" sortable>
<template slot-scope="scope">{{$moment(scope.row.createdAt).format("YYYY-MM-DD HH:mm:ss")}}</template>
</el-table-column>
<el-table-column header-align="center" label="发送人" prop="createdAt" sortable>
<template slot-scope="scope">{{scope.row.username}}</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="消息状态" prop="delFlag" sortable>
<template slot-scope="scope">
<el-tag size="mini" type="error" v-if="scope.row.delFlag">已删除</el-tag>
<el-tag size="mini" type="success" v-if="!scope.row.delFlag">已发送</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="deleteDo(scope.row.id, scope.row.title)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #edit>
<msg-form ref="msgForm" @back="back" :union-list="unionList" :unit-list="unitList" @parent-do-search="doSearch"></msg-form>
</template>
</guava>
<view-user ref="viewUser" :type="type" :id="id" :union-list="unionList" :unit-list="unitList"></view-user>
<el-dialog title="消息内容" :visible.sync="noteVisible" width="50%">
<div class="note" v-html="nodeData.note">
</div>
<file-preview complete_result :files="nodeData.files"></file-preview>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("msgManage/msgManageForm.js"){}#-->
<!--#include("msgManage/viewUser.js"){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"msg-form": MSG_FORM_TEMPLATE,
"view-user": VIEW_USER_TEMPLATE
},
data: function () {
return {
tableData: [],
pageForm: {
searchType: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
type: "",
id: "",
unionList: [],
unitList: [],
noteVisible: false,
nodeData: {}
}
},
methods: {
openViewNote(row) {
this.noteVisible = true
this.nodeData = {
note: row.note,
files: row.files
}
},
back() {
this.$refs.guava.index()
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
async pageData() {
const resp = await this.$axios.post("/platform/sys/msg/data", this.pageForm)
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
},
openAdd() {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$refs.msgForm.initAdd()
})
},
viewUserList(type, id) {
this.type = type
this.id = id
this.$refs.viewUser.open()
},
deleteDo(id, title) {
this.$confirm("您确定要删除" + title + "吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post("/platform/sys/msg/delete/" + id)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
} else {
this.$message.warning(resp.msg)
}
})
}
},
created: async function () {
this.unitList = await this.$businessTool.listUnit()
this.unionList = await this.$businessTool.listUnion()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,263 @@
const MSG_FORM_TEMPLATE = {
template: `
<div>
<el-form :model="formData" ref="addForm" :rules="formRules" label-width="120px">
<el-form-item prop="title" label="标题">
<el-input maxlength="255" placeholder="请输入标题" v-model="formData.title"></el-input>
</el-form-item>
<el-form-item prop="url" label="链接地址">
<el-input placeholder="请输入链接地址" v-model="formData.url"></el-input>
</el-form-item>
<el-form-item prop="needBack" label="是否需要反馈">
<el-radio-group v-model="formData.needBack" size="small">
<el-radio border :label="true">是</el-radio>
<el-radio border :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="type" label="消息类型">
<el-radio-group v-model="formData.type" size="small">
<!-- <el-radio border label="system">系统消息</el-radio>-->
<el-radio border label="user">用户消息</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="用户列表" v-if="'user'==formData.type">
<el-row class="mb10">
<el-button icon="el-icon-search" @click="openSelect" size="small">选择用户</el-button>
<el-button icon="el-icon-delete" @click="clearSelect" type="danger" size="small">清空用户</el-button>
</el-row>
<el-table :data="userTableData" style="width: 100%" size="mini" max-height="500">
<el-table-column fixed prop="loginname" label="工号" width="150"></el-table-column>
<el-table-column prop="username" label="姓名" width="120"></el-table-column>
<el-table-column prop="sex" label="性别" width="50"></el-table-column>
<el-table-column prop="userState" label="在职状态" width="120"></el-table-column>
<el-table-column prop="personType" label="人员类型" width="120"></el-table-column>
<el-table-column prop="mobile" label="手机" width="120"></el-table-column>
<el-table-column prop="unitName" label="所属单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="unionName" label="所属工会" show-overflow-tooltip></el-table-column>
</el-table>
<p style="color: var(--color-primary)">总计:{{userTableData.length}}人</p>
</el-form-item>
<el-form-item prop="note" label="消息内容">
<text-editor v-model="formData.note"></text-editor>
</el-form-item>
<el-form-item prop="files" label="消息附件">
<file-upload
:value.sync="formData.files"
:upload_number="5"
upload_result_category="array"
complete_result
></file-upload>
</el-form-item>
<el-row class="mt20" justify="end" type="flex">
<el-button @click="back">取消</el-button>
<el-button type="primary" @click="doAdd">确定</el-button>
</el-row>
</el-form>
<el-dialog title="选择用户" :visible.sync="selectDialogVisible" width="65%" append-to-body top="2%">
<div class="btn-group tool-button">
<el-input placeholder="请输入内容" v-model="pageForm.searchKeyword" @keyup.enter.native="doSearch" style="width: 300px">
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 80px">
<el-option label="工号" value="u.loginname"></el-option>
<el-option label="姓名" value="u.username"></el-option>
<el-option label="手机" value="u.mobile"></el-option>
</el-select>
</el-input>
<el-select v-model="pageForm.sex" placeholder="性别" clearable>
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
<el-select v-model="pageForm.unionId" @change="flushUnits" @clear="flushUnits" placeholder="请选择所属工会" filterable clearable>
<el-option v-for="item in unionList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select v-model="pageForm.unitId" placeholder="请选择所属单位" filterable clearable>
<el-option v-for="item in unitList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select v-model="pageForm.roleId" placeholder="请选择角色" filterable clearable>
<el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-button icon="el-icon-search" @click="doSearch" type="primary"></el-button>
</div>
<div class="left-span-label mt20">人员列表</div>
<el-table
:data="tableData"
@selection-change="handleSelectionChange"
row-key="id"
ref="tableRef"
>
<el-table-column type="selection" reserve-selection width="35"></el-table-column>
<el-table-column fixed prop="loginname" label="工号" width="150"></el-table-column>
<el-table-column prop="username" label="姓名" width="120"></el-table-column>
<el-table-column prop="sex" label="性别" width="50"></el-table-column>
<el-table-column prop="userState" label="在职状态" width="120"></el-table-column>
<el-table-column prop="personType" label="人员类型" width="120"></el-table-column>
<el-table-column prop="mobile" label="手机" width="120"></el-table-column>
<el-table-column prop="unitName" label="所属单位" show-overflow-tooltip></el-table-column>
<el-table-column prop="unionName" label="所属工会" show-overflow-tooltip></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<span slot="footer" class="dialog-footer">
<el-button @click="selectDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doSelect">确 定</el-button>
</span>
</el-dialog>
</div>
`,
props: {
unionList: {
type: Array,
required: true
},
unitList: {
type: Array,
required: true
}
},
data() {
return {
roleList: [],
userTableData: [],
tableData: [],
pageForm: {
searchName: "u.username",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
formData: {},
formRules: {
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
note: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
needBack: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
type: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
selectDialogVisible: false,
selectUsers: []
}
},
computed: {},
methods: {
async flushUnits() {
this.$set(this.pageForm, "unitId", null)
this.unitList = []
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.unitList = data
})
}
this.doSearch()
},
back() {
this.$emit("back")
},
parentDoSearch() {
this.$emit("parent-do-search")
},
doAdd() {
this.$refs["addForm"].validate((valid) => {
if (valid) {
const loginNames = this.userTableData.map((u) => u.loginname)
if (loginNames.length === 0 && this.formData.type === "user") {
this.$message.warning("请选择用户")
return false
}
if(loginNames.length > 100){
this.$message.warning("单条消息最多发送100人,请分批次发送。")
return false
}
this.$confirm('您确定要发送吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(()=>{
this.$axios
.post("/platform/sys/msg/addDo", {
msg: JSON.stringify(this.formData),
users: JSON.stringify(loginNames)
})
.then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.parentDoSearch()
this.back()
} else {
this.$message.error(resp.msg)
}
})
})
}
})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.pageData()
},
handleSelectionChange(val) {
this.selectUsers = val
},
initAdd() {
this.formData = {
id: "",
type: "user",
needBack: false
}
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
openSelect: function () {
this.selectDialogVisible = true
this.pageData()
},
clearSelect() {
this.userTableData = []
if(this.$refs.tableRef){
this.$refs.tableRef.clearSelection()
}
},
pageData() {
this.$axios.post("/platform/sys/msg/user_data", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
doSelect() {
const self = this
console.log(this.selectUsers)
console.log(this.userTableData)
const tableLoginNames = this.userTableData.map(v=>v.loginname)
this.selectUsers.forEach(function (o) {
if(!tableLoginNames.includes(o.loginname)){
self.userTableData.push(o)
}
})
if(this.$refs.tableRef){
this.$refs.tableRef.clearSelection()
}
this.selectDialogVisible = false
},
async getRoleList() {
const resp = await this.$axios.post("/platform/sys/msg/getRoleList")
return resp.data
}
},
async created() {
this.roleList = await this.getRoleList()
}
}
@@ -0,0 +1,123 @@
const VIEW_USER_TEMPLATE = {
template: `
<div>
<el-dialog :title="viewUserTitle" :visible.sync="viewDialogVisible" width="65%" top="2%">
<div class="btn-group tool-button">
<el-input placeholder="请输入内容" v-model="pageForm.searchKeyword" size="small" @keyup.enter.native="doSearch" style="width: 300px">
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 80px">
<el-option label="工号" value="u.loginname"></el-option>
<el-option label="姓名" value="u.username"></el-option>
<el-option label="手机" value="u.mobile"></el-option>
</el-select>
</el-input>
<el-select v-model="pageForm.unionId" @change="flushUnits" @clear="flushUnits" placeholder="请选择所属工会" size="small" filterable clearable>
<el-option v-for="item in unionList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select v-model="pageForm.unitId" placeholder="请选择所属单位" size="small" filterable clearable>
<el-option v-for="item in unitList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-button icon="el-icon-search" @click="doSearch" size="small" type="primary"></el-button>
</div>
<div class="left-span-label mt20">{{viewUserTitle}}列表</div>
<el-table
:data="tableData"
style="width: 100%; margin-top: 10px; margin-bottom: 10px"
size="small"
>
<el-table-column fixed prop="loginname" label="工号" width="150"></el-table-column>
<el-table-column prop="username" label="姓名" width="120"></el-table-column>
<el-table-column prop="sex" label="性别" width="120"></el-table-column>
<el-table-column prop="mobile" label="联系方式"></el-table-column>
<el-table-column prop="unitName" label="所属单位"></el-table-column>
<el-table-column prop="unionName" label="所属工会"></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<span slot="footer" class="dialog-footer">
<el-button @click="viewDialogVisible = false">关 闭</el-button>
</span>
</el-dialog>
</div>
`,
props: {
type: {
type: String,
required: true
},
id: {
type: String,
required: true
},
unionList: {
type: Array,
required: true
},
unitList: {
type: Array,
required: true
}
},
data() {
return {
viewUserTitle: "",
viewDialogVisible: false,
tableData: [],
pageForm: {
searchName: "u.username",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
}
}
},
computed: {},
methods: {
async flushUnits() {
this.$set(this.pageForm, "unitId", null)
this.unitList = []
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.unitList = data
})
}
this.doSearch()
},
open() {
this.viewDialogVisible = true
this.$nextTick(() => {
if ("all" === this.type) {
this.viewUserTitle = "全部用户"
}
if ("unread" === this.type) {
this.viewUserTitle = "未读用户"
}
this.pageForm.type = this.type
this.pageForm.id = this.id
this.doSearch()
})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.pageData()
},
pageData() {
this.$axios.post("/platform/sys/msg/user_view_data", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
}
},
async created() {}
}
@@ -0,0 +1,186 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style></style>
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="消息类型">
<el-select clearable placeholder="请选择消息类型" v-model="pageForm.searchType" style="width: 100%" @change="getMsg">
<el-option label="全部类型消息" value="all"></el-option>
<el-option label="系统消息" value="system"></el-option>
<el-option label="用户消息" value="user"></el-option>
</el-select>
</search-item>
<search-item label="消息标题">
<el-select placeholder="请选择消息标题" v-model="pageForm.msgId" style="width: 100%" @change="doSearch">
<el-option v-for="item in msgOptions" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="状态">
<el-select placeholder="请选择状态" v-model="pageForm.readStatus" style="width: 100%" @change="doSearch">
<el-option label="全部" :value="null"></el-option>
<el-option label="未阅读" :value="0"></el-option>
<el-option label="已阅读未反馈" :value="1"></el-option>
<el-option label="已阅读无需反馈" :value="2"></el-option>
<el-option label="已阅读已反馈" :value="3"></el-option>
</el-select>
</search-item>
<search-item label="姓名/工号">
<el-input placeholder="请输入姓名或工号" v-model="pageForm.searchKeyword" @keyup.enter.native="doSearch" clearable>
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 80px">
<el-option label="工号" value="mu.loginname"></el-option>
<el-option label="姓名" value="mu.userName"></el-option>
</el-select>
</el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" @change="flushUnits" @clear="flushUnits" placeholder="请选择所属工会" filterable clearable>
<el-option v-for="item in unionList" :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 @change="doSearch">
<el-option v-for="item in unitList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="人员列表">
<el-button type="primary" @click="exportMultipleAsZip" size="small">导出压缩包</el-button>
</table-tool>
<el-table :data="tableData" size="small" style="width: 100%">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column prop="userName" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<!-- <el-table-column prop="sex" label="性别"></el-table-column>-->
<!-- <el-table-column prop="mobile" label="联系方式"></el-table-column>-->
<el-table-column prop="unitName" label="所属单位"></el-table-column>
<el-table-column prop="unionName" label="所属工会"></el-table-column>
<el-table-column prop="status" label="状态">
<template v-slot="scope">
<el-tag size="mini" v-if="0 === scope.row.status">未阅读</el-tag>
<el-tag size="mini" type="warning" v-else-if="1 === scope.row.status && scope.row.needBack">已阅读未反馈</el-tag>
<el-tag size="mini" type="success" v-else-if="1 === scope.row.status && !scope.row.needBack">已阅读无需反馈</el-tag>
<el-tag size="mini" v-else-if="2 === scope.row.status">已阅读已反馈</el-tag>
</template>
</el-table-column>
<el-table-column prop="backFiles" label="反馈附件">
<template v-slot="scope">
<el-link type="primary" v-if="scope.row.backFiles.length > 0" @click="viewFiles(scope.row)">点击查看</el-link>
<el-link type="info" v-else>暂无</el-link>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog :visible.sync="backDialogVisible" title="反馈附件" width="40%" top="2%">
<el-dialog :visible.sync="backDialogVisible" title="反馈附件" width="40%" top="2%">
<el-form v-model="backFormData" ref="backFormRef" label-width="80px">
<el-form-item label="反馈附件" prop="files">
<div style="width: 100%; text-align: center">
<file-preview :files="backFiles" complete_result></file-preview>
</div>
</el-form-item>
<el-form-item label="反馈内容" prop="backText">
<el-input v-model="backFormData.backText" type="textarea" :rows="4" max="500" readonly></el-input>
</el-form-item>
</el-form>
</el-dialog>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
data() {
return {
backDialogVisible: false,
backFiles: [],
backFormData: {},
msgOptions: [],
pageForm: {
searchType: "all",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
searchName: "mu.userName",
readType: null
},
unionList: [],
unitList: [],
}
},
components: {},
methods: {
exportMultipleAsZip() {
this.$downLoad(loc() + "/exportMultipleAsZip", this.pageForm)
},
async flushUnits() {
this.$set(this.pageForm, "unitId", null)
this.unitList = []
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.unitList = data
})
}
this.doSearch()
},
viewFiles(row) {
this.backFormData = {
...row
}
this.backFiles = row.backFiles
this.backDialogVisible = true
},
async getMsg() {
const resp = await this.$axios.post("/platform/sys/msg/summary/getMsg", { searchType: this.pageForm.searchType })
this.msgOptions = resp.data
if (this.msgOptions && this.msgOptions.length > 0) {
this.pageForm.msgId = this.msgOptions[0].id
}
},
async pageData() {
const resp = await this.$axios.post("/platform/sys/msg/summary/pageData", this.pageForm)
if (resp.code === 0) {
this.tableData = resp.data.list
this.tableData.forEach((item) => {
item.backFiles = item.backFiles ? JSON.parse(item.backFiles) : []
})
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
}
},
async created() {
await this.getMsg()
const isAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
if (isAdmin) {
this.$businessTool.listUnion(this.pageForm.unionId).then((data) => {
this.unionList = data
})
} else {
this.$businessTool.listUnion(this.$store.state.user.union.id).then((data) => {
this.unionList = data
})
}
this.unitList = await this.$businessTool.listUnit()
await this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,35 @@
<!--#
layout("/layouts/platform.html"){
#-->
<header class="header navbar bg-white shadow">
<div class="btn-group tool-button m5">
<a class="el-button el-button--default el-button--medium" href="${base}/platform/sys/msg/user/all" data-pjax>
<i class="ti-angle-left"></i>
返回
</a>
</div>
</header>
<div class="content-wrap">
<div class="wrapper" style="min-height: 500px">
<section class="panel">
<div class="row mb10">
<div class="col-lg-12">
<!--#if(!isEmpty(obj)){#-->
<div style="text-align: center">
<h4>${obj.title!}</h4>
</div>
<div style="text-align: center; font-size: 14px; line-height: 24px">${@date.getDate(obj.sendAt)}</div>
<div style="margin-top: 16px !important; border-bottom: 1px solid #ddd"></div>
<div style="margin-top: 16px !important">${obj.note!}</div>
<!--#}else{#-->
消息不存在,或您没有阅读权限!
<!--#}#-->
</div>
</div>
</section>
</div>
</div>
<!--#
}
#-->
@@ -0,0 +1,35 @@
<!--#
layout("/layouts/platform.html"){
#-->
<header class="header navbar bg-white shadow">
<div class="btn-group tool-button m5">
<a class="el-button el-button--default el-button--medium" href="${base}/platform/sys/msg/user/read" data-pjax>
<i class="ti-angle-left"></i>
返回
</a>
</div>
</header>
<div class="content-wrap">
<div class="wrapper" style="min-height: 500px">
<section class="panel">
<div class="row mb10">
<div class="col-lg-12">
<!--#if(!isEmpty(obj)){#-->
<div style="text-align: center">
<h4>${obj.title!}</h4>
</div>
<div style="text-align: center; font-size: 14px; line-height: 24px">${@date.getDate(obj.sendAt)}</div>
<div style="margin-top: 16px !important; border-bottom: 1px solid #ddd"></div>
<div style="margin-top: 16px !important">${obj.note!}</div>
<!--#}else{#-->
消息不存在,或您没有阅读权限!
<!--#}#-->
</div>
</div>
</section>
</div>
</div>
<!--#
}
#-->
@@ -0,0 +1,35 @@
<!--#
layout("/layouts/platform.html"){
#-->
<header class="header navbar bg-white shadow">
<div class="btn-group tool-button m5">
<a class="el-button el-button--default el-button--medium" href="${base}/platform/sys/msg/user/unread" data-pjax>
<i class="ti-angle-left"></i>
返回
</a>
</div>
</header>
<div class="content-wrap">
<div class="wrapper" style="min-height: 500px">
<section class="panel">
<div class="row mb10">
<div class="col-lg-12">
<!--#if(!isEmpty(obj)){#-->
<div style="text-align: center">
<h4>${obj.title!}</h4>
</div>
<div style="text-align: center; font-size: 14px; line-height: 24px">${@date.getDate(obj.sendAt)}</div>
<div style="margin-top: 16px !important; border-bottom: 1px solid #ddd"></div>
<div style="margin-top: 16px !important">${obj.note!}</div>
<!--#}else{#-->
消息不存在,或您没有阅读权限!
<!--#}#-->
</div>
</div>
</section>
</div>
</div>
<!--#
}
#-->
@@ -0,0 +1,202 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.el-msg-active {
background-color: #546478;
color: #ffffff;
}
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="消息类型:">
<el-select clearable placeholder="请选择消息类型" v-model="pageForm.searchType" style="width: 100%" @change="doSearch">
<el-option label="全部类型消息" value="all"></el-option>
<el-option label="系统消息" value="system"></el-option>
<el-option label="用户消息" value="user"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool label="消息列表">
<el-radio-group v-model="pageForm.status" @change="doSearch" size="small">
<el-radio-button label="all">全部消息</el-radio-button>
<el-radio-button label="read">已读消息</el-radio-button>
<el-radio-button label="unread">未读消息</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column prop="type" label="消息类型" width="120">
<template v-slot="scope">
<span v-if="'system'===scope.row.type">系统消息</span>
<span v-if="'user'===scope.row.type">用户消息</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="createdAt" label="接收时间">
<template slot-scope="scope">{{$moment(scope.row.createdAt).format("YYYY-MM-DD HH:mm:ss")}}</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template v-slot="scope">
<el-tag size="mini" v-if="0 === scope.row.status">未阅读</el-tag>
<el-tag size="mini" type="warning" v-else-if="1 === scope.row.status && scope.row.needBack">已阅读未反馈</el-tag>
<el-tag size="mini" type="success" v-else-if="1 === scope.row.status && !scope.row.needBack">已阅读无需反馈</el-tag>
<el-tag size="mini" v-else-if="2 === scope.row.status">已阅读已反馈</el-tag>
</template>
</el-table-column>
<el-table-column prop="backFiles" label="反馈附件">
<template v-slot="scope">
<el-link type="primary" v-if="scope.row.backFiles.length > 0" @click="viewFiles(scope.row)">点击查看</el-link>
<el-link type="info" v-else>暂无</el-link>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template v-slot="scope">
<el-button size="mini" type="primary" @click="openView(scope.row)">查看</el-button>
<el-button v-if="scope.row.needBack" size="mini" type="primary" @click="openBack(scope.row)">反馈</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #view>
<div style="text-align: center">
<h4>{{viewData.title}}</h4>
</div>
<div style="text-align: center; font-size: 14px; line-height: 24px">{{$moment(viewData.createdAt).format('YYYY-MM-DD HH:mm')}}</div>
<div style="margin-top: 16px !important; border-bottom: 1px solid #ddd"></div>
<div style="margin-top: 16px !important;" v-html="viewData.note"></div>
<file-preview complete_result :files="viewData.files"></file-preview>
</template>
</guava>
<el-dialog :visible.sync="backDialogVisible" title="反馈附件" width="40%" top="2%">
<el-form v-model="backFormData" ref="backFormRef" label-width="80px">
<el-form-item label="反馈附件" prop="files">
<file-upload
v-if="isViewFiles === false"
:value.sync="backFormData.files"
upload_mode="drag"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="file"
></file-upload>
<div v-else style="width: 100%; text-align: center">
<file-preview :files="backFormData.files" complete_result></file-preview>
</div>
</el-form-item>
<el-form-item label="反馈内容" prop="backText">
<el-input v-model="backFormData.backText" type="textarea" :rows="4" placeholder="非必填" max="500" :readonly="isViewFiles"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer" v-if="isViewFiles === false">
<el-button @click="backDialogVisible = false">取消</el-button>
<el-button type="primary" @click="backDo">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data: function () {
return {
backFormData: {},
backFiles: [],
backDialogVisible: false,
viewData: {},
tableData: [],
pageForm: {
searchType: "all",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
status: "unread"
},
isViewFiles: false
}
},
methods: {
viewFiles(row) {
this.isViewFiles = true
this.backFormData = {
files: row.backFiles,
backText: row.backText
}
this.backDialogVisible = true
},
async backDo() {
if (this.backFormData.files.length === 0) {
this.$message.warning("请上传反馈附件")
return
}
const resp = await this.$axios.post("/platform/sys/msg/user/backDo", {
id: this.backFormData.id,
backText: this.backFormData.backText,
files: JSON.stringify(this.backFormData.files)
})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.backDialogVisible = false
} else {
this.$message.warning(resp.msg)
}
},
async openView(row) {
const resp = await this.$axios.post("/platform/sys/msg/user/findOne", { id: row.msgId })
this.viewData = resp.data
this.$refs.guava.view()
this.pageData()
window.webSocketPubSub.send(
JSON.stringify({
room: store.state.room,
action: "getMessage"
})
)
},
openBack(row) {
this.isViewFiles = false
this.backFormData = {
files: row.backFiles,
backText: row.backText,
id: row.id
}
this.backDialogVisible = true
},
async pageData() {
const resp = await this.$axios.post("/platform/sys/msg/user/data/" + this.pageForm.status, this.pageForm)
if (resp.code === 0) {
this.tableData = resp.data.list
this.tableData.forEach((item) => {
item.backFiles = item.backFiles ? JSON.parse(item.backFiles) : []
})
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
},
formatAt: function (val) {
if (val && val > 0) return this.$moment(val).format("YYYY-MM-DD HH:mm")
return ""
}
},
created: function () {
this.pageForm.searchType = GetQueryString("type") ? GetQueryString("type") : "all"
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,209 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.el-msg-active {
background-color: #546478;
color: #ffffff;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<a
href="${base}/platform/sys/msg/user/read"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'all'==pageForm.searchType}"
>
全部类型消息
</a>
<a
href="${base}/platform/sys/msg/user/read?type=system"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'system'==pageForm.searchType}"
>
系统消息
<span v-if="unreadnum.system>0">({{unreadnum.system}})</span>
</a>
<a
href="${base}/platform/sys/msg/user/read?type=user"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'user'==pageForm.searchType}"
>
用户消息
<span v-if="unreadnum.user>0">({{unreadnum.user}})</span>
</a>
</el-card>
<el-card shadow="never">
<el-table
:data="tableData"
@sort-change="pageOrder"
size="small"
header-align="center"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="35"></el-table-column>
<el-table-column prop="status" width="45">
<template slot-scope="scope">
<span v-if="0==scope.row.status"><i class="fa fa-circle text-info ml5"></i></span>
</template>
</el-table-column>
<el-table-column prop="type" label="消息类型" header-align="center" align="center" width="120">
<template slot-scope="scope">
<span v-if="'system'==scope.row.type">系统消息</span>
<span v-if="'user'==scope.row.type">用户消息</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" header-align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a :href="'${base}/platform/sys/msg/user/read/detail/'+scope.row.msgid" data-pjax>{{scope.row.title}}</a>
</template>
</el-table-column>
<el-table-column prop="opat" label="接收时间" header-align="center" align="center">
<template slot-scope="scope">{{formatAt(scope.row.opat)}}</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container">
<el-pagination
style="float: left"
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
selectDialogVisible: false,
viewDialogVisible: false,
tableData: [],
pageForm: {
searchType: "${type!}",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "createdAt",
pageOrderBy: "descending"
},
formData: {
id: "",
type: "system"
},
selectData: [],
unreadnum: {
system: 0,
user: 0
}
}
},
methods: {
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/msg/user/data/read",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
formatAt: function (val) {
if (val && val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
handleSelectionChange: function (val) {
this.selectData = val
},
delMore: function () {
var self = this
if (self.selectData.length == 0) {
self.$message({
message: "请选择消息",
type: "warning"
})
return false
}
var ids = []
self.selectData.forEach(function (val) {
ids.push(val.id)
})
self.$confirm("您确定要删除选中的 " + ids.length + " 条消息? ", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/msg/user/delete",
{ ids: ids.toString() },
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.pageData()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
},
created: function () {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,302 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.el-msg-active {
background-color: #546478;
color: #ffffff;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<a
href="${base}/platform/sys/msg/user/unread"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'all'==pageForm.searchType}"
>
全部类型消息
</a>
<a
href="${base}/platform/sys/msg/user/unread?type=system"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'system'==pageForm.searchType}"
>
系统消息
<span v-if="unreadnum.system>0">({{unreadnum.system}})</span>
</a>
<a
href="${base}/platform/sys/msg/user/unread?type=user"
data-pjax
class="el-button el-button--default el-button--medium"
:class="{'el-msg-active':'user'==pageForm.searchType}"
>
用户消息
<span v-if="unreadnum.user>0">({{unreadnum.user}})</span>
</a>
</el-card>
<el-card shadow="never">
<el-table
:data="tableData"
@sort-change="pageOrder"
size="small"
header-align="center"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="35"></el-table-column>
<el-table-column prop="status" width="45">
<template slot-scope="scope">
<span v-if="0==scope.row.status"><i class="fa fa-circle text-info ml5"></i></span>
</template>
</el-table-column>
<el-table-column prop="type" label="消息类型" header-align="center" align="center" width="120">
<template slot-scope="scope">
<span v-if="'system'==scope.row.type">系统消息</span>
<span v-if="'user'==scope.row.type">用户消息</span>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" header-align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a :href="'${base}/platform/sys/msg/user/unread/detail/'+scope.row.msgid" data-pjax>{{scope.row.title}}</a>
</template>
</el-table-column>
<el-table-column prop="opat" label="接收时间" header-align="center" align="center">
<template slot-scope="scope">{{formatAt(scope.row.opat)}}</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container">
<el-pagination
style="float: left"
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
selectDialogVisible: false,
viewDialogVisible: false,
tableData: [],
pageForm: {
searchType: "${type!}",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "createdAt",
pageOrderBy: "descending"
},
formData: {
id: "",
type: "system"
},
selectData: [],
unreadnum: {
system: 0,
user: 0
}
}
},
methods: {
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/msg/user/data/unread",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
formatAt: function (val) {
if (val && val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
handleSelectionChange: function (val) {
this.selectData = val
},
unreadData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/msg/user/unread_num",
{},
function (data) {
if (data.code == 0) {
self.unreadnum = data.data
}
},
"json"
)
},
readMore: function () {
var self = this
if (self.selectData.length == 0) {
self.$message({
message: "请选择消息",
type: "warning"
})
return false
}
var ids = []
self.selectData.forEach(function (val) {
ids.push(val.id)
})
self.$confirm("您确定要标记选中的 " + ids.length + " 条消息为已读吗? ", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/msg/user/status/read",
{ ids: ids.toString() },
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.pageData()
self.unreadData()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
},
readAll: function () {
var self = this
self.$confirm("您确定要标记全部消息为已读吗? ", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/msg/user/status/readAll",
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.pageData()
self.unreadData()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
},
delMore: function () {
var self = this
if (self.selectData.length == 0) {
self.$message({
message: "请选择消息",
type: "warning"
})
return false
}
var ids = []
self.selectData.forEach(function (val) {
ids.push(val.id)
})
self.$confirm("您确定要删除选中的 " + ids.length + " 条消息? ", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/msg/user/delete",
{ ids: ids.toString() },
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.pageData()
self.unreadData()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
},
created: function () {
this.pageData()
this.unreadData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,15 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-alert title="对不起,您没有权限访问该页面。" type="error"></el-alert>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app"
})
</script>
<!--#
}
#-->
@@ -0,0 +1,149 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<el-row type="flex">
<el-input v-model="pageForm.templateCode" maxlength="50" style="width: 400px" clearable placeholder="请输入模板代码"></el-input>
<el-button @click="doSearch" icon="el-icon-search" type="primary" class="ml10">搜索</el-button>
<el-button type="primary" @click="openAdd" style="margin-left: auto">新增模板</el-button>
</el-row>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" border class="mt10" stripe ref="table" :height="tableHeight" id="officeTable">
<el-table-column label="序号" type="index" :index="indexMethod" width="70px"></el-table-column>
<el-table-column label="模板代码" prop="templateCode"></el-table-column>
<el-table-column label="模板名称" prop="templateName"></el-table-column>
<el-table-column label="模板路径" prop="templatePath" show-overflow-tooltip></el-table-column>
<el-table-column label="模板说明" prop="templateDesc"></el-table-column>
<el-table-column label="操作" width="300px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="download(row.templatePath)">下载</el-button>
<el-button size="mini" type="primary" @click="preview(row)">预览</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="模板" :visible.sync="dialogVisible" :close-on-click-modal="false" width="800px">
<el-form :model="formData" ref="ruleForm" :rules="formRules" label-width="80px">
<el-form-item label="模板文件" prop="file" :rules="[{required:true,message:'必传',trigger:['change']}]">
<file-upload :value.sync="formData.file" :upload_number="1" upload_result_category="array" complete_result></file-upload>
</el-form-item>
<el-form-item label="模板名称" prop="templateName" :rules="[{required:true,message:'必填',trigger:['change']}]">
<el-input type="text" v-model="formData.templateName" maxlength="50" placeholder="必填"></el-input>
</el-form-item>
<el-form-item label="模板代码" prop="templateCode" :rules="[{required:true,message:'必填',trigger:['change']}]">
<el-input type="text" v-model="formData.templateCode" maxlength="50" placeholder="请填写模板代码"></el-input>
</el-form-item>
<el-form-item label="模板说明" prop="templateDesc" :rules="[{required:true,message:'必填',trigger:['change']}]">
<el-input type="text" v-model="formData.templateDesc" maxlength="50" placeholder="请填写模板说明"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
dialogVisible: false,
tableHeight: 700,
formData: {
templateName: null
}
}
},
watch: {
"formData.file": {
handler: function (val) {
if (val) {
this.$set(this.formData, "templateName", val[0].name)
this.$set(this.formData, "templatePath", val[0].response.data)
} else {
this.$set(this.formData, "templateName", null)
this.$set(this.formData, "templatePath", null)
}
}
}
},
methods: {
openAdd() {
this.dialogVisible = true
this.formData = {}
},
openEdit(row) {
this.dialogVisible = true
this.formData = { ...row }
},
save() {
this.$refs.ruleForm.validate(async (valid) => {
if (valid) {
const formData = this.formData
// delete formData.file
this.$axios
.post(loc() + "/save", {
officeTemplate: JSON.stringify(this.formData)
})
.then((resp) => {
if (resp.code === 0) {
this.dialogVisible = false
this.$message.success(resp.msg)
this.pageData()
}
})
}
})
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/delete", { id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.warning(resp.msg)
}
})
},
download(path) {
window.open(path)
},
preview(row) {
this.$commonUtil.previewFile({
suffix: row.templateName.split(".").pop().toLocaleLowerCase(),
downloadPath: row.templatePath,
name: row.templateName,
id: row.templatePath.split("=").pop().toLocaleLowerCase()
})
}
},
mounted() {
setTimeout(() => {
this.tableHeight = window.innerHeight - document.querySelector("#officeTable").getBoundingClientRect().top - 80
}, 200)
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
const sort_component = {
template: `
<el-dialog title="排序" :visible.sync="sortDialogVisible" width="50%">
<el-tree ref="sortTreeRef" :data="sortData" draggable :allow-drop="sortAllowDrop" node-key="id"
style="height: 60vh;overflow-y: auto"
:props="defaultProps">
<span class="custom-tree-node" slot-scope="scope">
<span>{{ scope.node.label }}</span>
</span>
</el-tree>
<span slot="footer" class="dialog-footer">
<el-button @click="sortDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doSort">确 定</el-button>
</span>
</el-dialog>
`,
data() {
return {
sortDialogVisible: false,
sortData: [],
defaultProps: {
children: "children",
label: "name"
}
}
},
methods: {
onOpen() {
this.sortDialogVisible = true
this.$axios.post("/platform/sys/role/sortData").then((res) => {
if (res.code === 0) {
this.sortData = res.data
}
})
},
sortAllowDrop(moveNode, inNode, type) {
return type !== "inner"
},
doSort() {
const loading = this.$loading({
lock: true,
text: "正在提交...",
spinner: "el-icon-loading"
})
const afterSorts = this.sortData.map((v) => {
return {
id: v.id,
name: v.name
}
})
this.$axios
.post("/platform/sys/role/doSort", { roles: JSON.stringify(afterSorts) })
.then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.sortDialogVisible = false
this.$emit("refresh", null)
}
})
.finally(() => {
loading.close()
})
}
}
}
@@ -0,0 +1,322 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<el-row type="flex" justify="end">
<el-button @click="openAdd" size="medium" type="primary">
<i class="ti-plus"></i>
新建路由
</el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%">
<el-table-column header-align="center" label="原路径" prop="url" sortable></el-table-column>
<el-table-column :show-overflow-tooltip="true" header-align="center" label="指向路径" prop="toUrl"></el-table-column>
<el-table-column align="center" header-align="center" label="转发方式" prop="type">
<template slot-scope="scope">
<span v-if="scope.row.type=='hide'">隐性转发</span>
<span v-if="scope.row.type=='show'">显性转发</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="启用状态" prop="disabled" sortable>
<template slot-scope="scope">
<i class="fa fa-circle text-danger ml5" v-if="scope.row.disabled"></i>
<i class="fa fa-circle text-success ml5" v-if="!scope.row.disabled"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}">启用</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">
禁用
</el-dropdown-item>
<el-dropdown-item :command="{type:'edit',id:scope.row.id}" divided>修改</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,url:scope.row.url}">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog title="新建路由" :visible.sync="addDialogVisible" width="40%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="url" label="原路径">
<el-input maxlength="100" placeholder="原路径" v-model="formData.url" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="toUrl" label="指向路径">
<el-input placeholder="指向路径" v-model="formData.toUrl" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="type" label="指向路径">
<el-radio-group v-model="formData.type">
<el-radio label="hide">隐性转发</el-radio>
<el-radio label="show">显性转发</el-radio>
</el-radio-group>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改路由" :visible.sync="editDialogVisible" width="40%">
<el-form :model="formData" ref="editForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="url" label="原路径">
<el-input maxlength="100" placeholder="原路径" v-model="formData.url" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="toUrl" label="指向路径">
<el-input placeholder="指向路径" v-model="formData.toUrl" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="type" label="指向路径">
<el-radio-group v-model="formData.type">
<el-radio label="hide">隐性转发</el-radio>
<el-radio label="show">显性转发</el-radio>
</el-radio-group>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
formData: {
id: "",
type: "hide"
},
formRules: {
url: [{ required: true, message: "原路径", trigger: "blur" }],
toUrl: [{ required: true, message: "指向路径", trigger: "blur" }]
}
}
},
methods: {
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/route/data",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
formatAt: function (val) {
if (val && val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
doSearch: function () {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd: function () {
var self = this
this.addDialogVisible = true
this.formData = {
id: "",
type: "hide"
} //打开新增窗口,表单先清空
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd: function () {
var self = this
self.$refs["addForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/route/addDo",
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
} else {
self.$message({
message: data.msg,
type: "error"
})
}
self.pageData() //添加失败也可以入库,所以加载一下看看
},
"json"
)
}
})
},
doEdit: function () {
var self = this
self.$refs["editForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/route/editDo",
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.doSearch() //查询是带条件的,加载新建用户选择的单位用户数据
//self.pageData();
self.editDialogVisible = false
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("enable" == command.type || "disable" == command.type) {
$.post(
base + "/platform/sys/route/" + command.type + "/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
if ("disable" == command.type) {
command.row.disabled = true
}
if ("enable" == command.type) {
command.row.disabled = false
}
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("edit" == command.type) {
$.post(
base + "/platform/sys/route/edit/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除 " + command.url, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/route/delete/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.doSearch()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
}
},
created: function () {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="en">
<head>
<title>签字</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<script src="${base!}/assets/platform/plugins/vue/vue.js"></script>
<script src="${base!}/assets/platform/plugins/vue-http-loader/httpVueLoader.js"></script>
<script src="${base!}/assets/platform/plugins/smooth-signature/index.umd.min.js"></script>
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
</head>
<body>
<div id="app">
<signature @save="save"></signature>
</div>
</body>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
components: {
signature: httpVueLoader("/components/plugins/sysSignature/index.vue?v=" + new Date().getTime())
},
data() {
return {}
},
methods: {
save(signature) {
const id = new URLSearchParams(window.location.search).get("id")
const origin = new URLSearchParams(window.location.search).get("origin")
if (!id && !origin) {
alert("参数错误")
return
}
const file = this.base64ToFile(signature, "signature.png")
const formData = new FormData()
formData.append("file", file)
formData.append("id", id)
formData.append("origin", origin)
const method = origin === "user" ? "/update" : "/saveSignature"
axios
.post("/platform/signature" + method, formData, {
headers: {
"Content-Type": "multipart/form-data"
}
})
.then((res) => {
return res.data
})
.then((res) => {
if (res.code === 0) {
alert("保存成功")
} else {
alert("保存失败")
}
})
},
base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const parts = base64Data.split(";base64,")
const contentType = parts[0].split(":")[1]
const raw = window.atob(parts[1])
const rawLength = raw.length
const uInt8Array = new Uint8Array(rawLength)
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i)
}
// 使用Blob对象创建File对象
const blob = new Blob([uInt8Array], { type: contentType })
blob.lastModifiedDate = new Date()
blob.name = filename
return new File([blob], filename, { type: contentType })
}
}
})
</script>
</html>
@@ -0,0 +1,732 @@
<!doctype html>
<html>
<head>
<title>Cron表达式生成器</title>
<link href="${base}/assets/platform/cron/css/easyui.min.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.line {
height: 25px;
line-height: 25px;
margin: 3px;
}
.imp {
padding-left: 25px;
}
.col {
width: 95px;
}
ul {
list-style: none;
padding-left: 10px;
}
li {
height: 20px;
}
body {
text-align: center; /*IE*/
text-align: -moz-center; /*Firefox*/
text-align: -webkit-center; /*Chrome*/
}
</style>
<script nonce="${cspNonce!}">
var base = "${base!}"
</script>
</head>
<body>
<div class="easyui-layout" style="width: 830px; height: 560px; border: 1px rgb(202, 196, 196) solid; border-radius: 5px">
<div style="height: 100%">
<div class="easyui-tabs" data-options="fit:true,border:false">
<div title="秒">
<div class="line">
<input type="radio" checked="checked" name="second" onclick="everyTime(this)" />
每秒 允许的通配符[, - * /]
</div>
<div class="line">
<input type="radio" name="second" onclick="cycle(this)" />
周期从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:58" value="1" id="secondStart_0" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:59" value="2" id="secondEnd_0" />
秒
</div>
<div class="line">
<input type="radio" name="second" onclick="startOn(this)" />
从
<input class="numberspinner" style="width: 60px" data-options="min:0,max:59" value="0" id="secondStart_1" />
秒开始,每
<input class="numberspinner" style="width: 60px" data-options="min:1,max:59" value="1" id="secondEnd_1" />
秒执行一次
</div>
<div class="line">
<input type="radio" name="second" id="sencond_appoint" />
指定
</div>
<div class="imp secondList">
<input type="checkbox" value="0" />
00
<input type="checkbox" value="1" />
01
<input type="checkbox" value="2" />
02
<input type="checkbox" value="3" />
03
<input type="checkbox" value="4" />
04
<input type="checkbox" value="5" />
05
<input type="checkbox" value="6" />
06
<input type="checkbox" value="7" />
07
<input type="checkbox" value="8" />
08
<input type="checkbox" value="9" />
09
</div>
<div class="imp secondList">
<input type="checkbox" value="10" />
10
<input type="checkbox" value="11" />
11
<input type="checkbox" value="12" />
12
<input type="checkbox" value="13" />
13
<input type="checkbox" value="14" />
14
<input type="checkbox" value="15" />
15
<input type="checkbox" value="16" />
16
<input type="checkbox" value="17" />
17
<input type="checkbox" value="18" />
18
<input type="checkbox" value="19" />
19
</div>
<div class="imp secondList">
<input type="checkbox" value="20" />
20
<input type="checkbox" value="21" />
21
<input type="checkbox" value="22" />
22
<input type="checkbox" value="23" />
23
<input type="checkbox" value="24" />
24
<input type="checkbox" value="25" />
25
<input type="checkbox" value="26" />
26
<input type="checkbox" value="27" />
27
<input type="checkbox" value="28" />
28
<input type="checkbox" value="29" />
29
</div>
<div class="imp secondList">
<input type="checkbox" value="30" />
30
<input type="checkbox" value="31" />
31
<input type="checkbox" value="32" />
32
<input type="checkbox" value="33" />
33
<input type="checkbox" value="34" />
34
<input type="checkbox" value="35" />
35
<input type="checkbox" value="36" />
36
<input type="checkbox" value="37" />
37
<input type="checkbox" value="38" />
38
<input type="checkbox" value="39" />
39
</div>
<div class="imp secondList">
<input type="checkbox" value="40" />
40
<input type="checkbox" value="41" />
41
<input type="checkbox" value="42" />
42
<input type="checkbox" value="43" />
43
<input type="checkbox" value="44" />
44
<input type="checkbox" value="45" />
45
<input type="checkbox" value="46" />
46
<input type="checkbox" value="47" />
47
<input type="checkbox" value="48" />
48
<input type="checkbox" value="49" />
49
</div>
<div class="imp secondList">
<input type="checkbox" value="50" />
50
<input type="checkbox" value="51" />
51
<input type="checkbox" value="52" />
52
<input type="checkbox" value="53" />
53
<input type="checkbox" value="54" />
54
<input type="checkbox" value="55" />
55
<input type="checkbox" value="56" />
56
<input type="checkbox" value="57" />
57
<input type="checkbox" value="58" />
58
<input type="checkbox" value="59" />
59
</div>
</div>
<div title="分钟">
<div class="line">
<input type="radio" checked="checked" name="min" onclick="everyTime(this)" />
分钟 允许的通配符[, - * /]
</div>
<div class="line">
<input type="radio" name="min" onclick="cycle(this)" />
周期从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:58" value="1" id="minStart_0" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:59" value="2" id="minEnd_0" />
分钟
</div>
<div class="line">
<input type="radio" name="min" onclick="startOn(this)" />
从
<input class="numberspinner" style="width: 60px" data-options="min:0,max:59" value="0" id="minStart_1" />
分钟开始,每
<input class="numberspinner" style="width: 60px" data-options="min:1,max:59" value="1" id="minEnd_1" />
分钟执行一次
</div>
<div class="line">
<input type="radio" name="min" id="min_appoint" />
指定
</div>
<div class="imp minList">
<input type="checkbox" value="0" />
00
<input type="checkbox" value="1" />
01
<input type="checkbox" value="2" />
02
<input type="checkbox" value="3" />
03
<input type="checkbox" value="4" />
04
<input type="checkbox" value="5" />
05
<input type="checkbox" value="6" />
06
<input type="checkbox" value="7" />
07
<input type="checkbox" value="8" />
08
<input type="checkbox" value="9" />
09
</div>
<div class="imp minList">
<input type="checkbox" value="10" />
10
<input type="checkbox" value="11" />
11
<input type="checkbox" value="12" />
12
<input type="checkbox" value="13" />
13
<input type="checkbox" value="14" />
14
<input type="checkbox" value="15" />
15
<input type="checkbox" value="16" />
16
<input type="checkbox" value="17" />
17
<input type="checkbox" value="18" />
18
<input type="checkbox" value="19" />
19
</div>
<div class="imp minList">
<input type="checkbox" value="20" />
20
<input type="checkbox" value="21" />
21
<input type="checkbox" value="22" />
22
<input type="checkbox" value="23" />
23
<input type="checkbox" value="24" />
24
<input type="checkbox" value="25" />
25
<input type="checkbox" value="26" />
26
<input type="checkbox" value="27" />
27
<input type="checkbox" value="28" />
28
<input type="checkbox" value="29" />
29
</div>
<div class="imp minList">
<input type="checkbox" value="30" />
30
<input type="checkbox" value="31" />
31
<input type="checkbox" value="32" />
32
<input type="checkbox" value="33" />
33
<input type="checkbox" value="34" />
34
<input type="checkbox" value="35" />
35
<input type="checkbox" value="36" />
36
<input type="checkbox" value="37" />
37
<input type="checkbox" value="38" />
38
<input type="checkbox" value="39" />
39
</div>
<div class="imp minList">
<input type="checkbox" value="40" />
40
<input type="checkbox" value="41" />
41
<input type="checkbox" value="42" />
42
<input type="checkbox" value="43" />
43
<input type="checkbox" value="44" />
44
<input type="checkbox" value="45" />
45
<input type="checkbox" value="46" />
46
<input type="checkbox" value="47" />
47
<input type="checkbox" value="48" />
48
<input type="checkbox" value="49" />
49
</div>
<div class="imp minList">
<input type="checkbox" value="50" />
50
<input type="checkbox" value="51" />
51
<input type="checkbox" value="52" />
52
<input type="checkbox" value="53" />
53
<input type="checkbox" value="54" />
54
<input type="checkbox" value="55" />
55
<input type="checkbox" value="56" />
56
<input type="checkbox" value="57" />
57
<input type="checkbox" value="58" />
58
<input type="checkbox" value="59" />
59
</div>
</div>
<div title="小时">
<div class="line">
<input type="radio" checked="checked" name="hour" onclick="everyTime(this)" />
小时 允许的通配符[, - * /]
</div>
<div class="line">
<input type="radio" name="hour" onclick="cycle(this)" />
周期从
<input class="numberspinner" style="width: 60px" data-options="min:0,max:23" value="0" id="hourStart_0" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:23" value="2" id="hourEnd_1" />
小时
</div>
<div class="line">
<input type="radio" name="hour" onclick="startOn(this)" />
从
<input class="numberspinner" style="width: 60px" data-options="min:0,max:23" value="0" id="hourStart_1" />
小时开始,每
<input class="numberspinner" style="width: 60px" data-options="min:1,max:23" value="1" id="hourEnd_1" />
小时执行一次
</div>
<div class="line">
<input type="radio" name="hour" id="hour_appoint" />
指定
</div>
<div class="imp hourList">
AM:
<input type="checkbox" value="0" />
00
<input type="checkbox" value="1" />
01
<input type="checkbox" value="2" />
02
<input type="checkbox" value="3" />
03
<input type="checkbox" value="4" />
04
<input type="checkbox" value="5" />
05
<input type="checkbox" value="6" />
06
<input type="checkbox" value="7" />
07
<input type="checkbox" value="8" />
08
<input type="checkbox" value="9" />
09
<input type="checkbox" value="10" />
10
<input type="checkbox" value="11" />
11
</div>
<div class="imp hourList">
PM:
<input type="checkbox" value="12" />
12
<input type="checkbox" value="13" />
13
<input type="checkbox" value="14" />
14
<input type="checkbox" value="15" />
15
<input type="checkbox" value="16" />
16
<input type="checkbox" value="17" />
17
<input type="checkbox" value="18" />
18
<input type="checkbox" value="19" />
19
<input type="checkbox" value="20" />
20
<input type="checkbox" value="21" />
21
<input type="checkbox" value="22" />
22
<input type="checkbox" value="23" />
23
</div>
</div>
<div title="日">
<div class="line">
<input type="radio" checked="checked" name="day" onclick="everyTime(this)" />
日 允许的通配符[, - * / L W]
</div>
<div class="line">
<input type="radio" name="day" onclick="unAppoint(this)" />
不指定
</div>
<div class="line">
<input type="radio" name="day" onclick="cycle(this)" />
周期从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:31" value="1" id="dayStart_0" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:31" value="2" id="dayEnd_0" />
日
</div>
<div class="line">
<input type="radio" name="day" onclick="startOn(this)" />
从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:31" value="1" id="dayStart_1" />
日开始,每
<input class="numberspinner" style="width: 60px" data-options="min:1,max:31" value="1" id="dayEnd_1" />
天执行一次
</div>
<div class="line">
<input type="radio" name="day" onclick="workDay(this)" />
每月
<input class="numberspinner" style="width: 60px" data-options="min:1,max:31" value="1" id="dayStart_2" />
号最近的那个工作日
</div>
<div class="line">
<input type="radio" name="day" onclick="lastDay(this)" />
本月最后一天
</div>
<div class="line">
<input type="radio" name="day" id="day_appoint" />
指定
</div>
<div class="imp dayList">
<input type="checkbox" value="1" />
1
<input type="checkbox" value="2" />
2
<input type="checkbox" value="3" />
3
<input type="checkbox" value="4" />
4
<input type="checkbox" value="5" />
5
<input type="checkbox" value="6" />
6
<input type="checkbox" value="7" />
7
<input type="checkbox" value="8" />
8
<input type="checkbox" value="9" />
9
<input type="checkbox" value="10" />
10
<input type="checkbox" value="11" />
11
<input type="checkbox" value="12" />
12
<input type="checkbox" value="13" />
13
<input type="checkbox" value="14" />
14
<input type="checkbox" value="15" />
15
<input type="checkbox" value="16" />
16
</div>
<div class="imp dayList">
<input type="checkbox" value="17" />
17
<input type="checkbox" value="18" />
18
<input type="checkbox" value="19" />
19
<input type="checkbox" value="20" />
20
<input type="checkbox" value="21" />
21
<input type="checkbox" value="22" />
22
<input type="checkbox" value="23" />
23
<input type="checkbox" value="24" />
24
<input type="checkbox" value="25" />
25
<input type="checkbox" value="26" />
26
<input type="checkbox" value="27" />
27
<input type="checkbox" value="28" />
28
<input type="checkbox" value="29" />
29
<input type="checkbox" value="30" />
30
<input type="checkbox" value="31" />
31
</div>
</div>
<div title="月">
<div class="line">
<input type="radio" checked="checked" name="mouth" onclick="everyTime(this)" />
月 允许的通配符[, - * /]
</div>
<div class="line">
<input type="radio" name="mouth" onclick="unAppoint(this)" />
不指定
</div>
<div class="line">
<input type="radio" name="mouth" onclick="cycle(this)" />
周期从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:12" value="1" id="mouthStart_0" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:12" value="2" id="mouthEnd_0" />
月
</div>
<div class="line">
<input type="radio" name="mouth" onclick="startOn(this)" />
从
<input class="numberspinner" style="width: 60px" data-options="min:1,max:12" value="1" id="mouthStart_1" />
日开始,每
<input class="numberspinner" style="width: 60px" data-options="min:1,max:12" value="1" id="mouthEnd_1" />
月执行一次
</div>
<div class="line">
<input type="radio" name="mouth" id="mouth_appoint" />
指定
</div>
<div class="imp mouthList">
<input type="checkbox" value="1" />
1
<input type="checkbox" value="2" />
2
<input type="checkbox" value="3" />
3
<input type="checkbox" value="4" />
4
<input type="checkbox" value="5" />
5
<input type="checkbox" value="6" />
6
<input type="checkbox" value="7" />
7
<input type="checkbox" value="8" />
8
<input type="checkbox" value="9" />
9
<input type="checkbox" value="10" />
10
<input type="checkbox" value="11" />
11
<input type="checkbox" value="12" />
12
</div>
</div>
<div title="周">
<div class="line">
<input type="radio" checked="checked" name="week" onclick="everyTime(this)" />
周 允许的通配符[, - * / L #]
</div>
<div class="line">
<input type="radio" name="week" onclick="unAppoint(this)" />
不指定
</div>
<div class="line">
<input type="radio" name="week" onclick="startOn(this)" />
周期 从星期
<input class="numberspinner" style="width: 60px" data-options="min:1,max:7" id="weekStart_0" value="1" />
-
<input class="numberspinner" style="width: 60px" data-options="min:2,max:7" value="2" id="weekEnd_0" />
</div>
<div class="line">
<input type="radio" name="week" onclick="weekOfDay(this)" />
第
<input class="numberspinner" style="width: 60px" data-options="min:1,max:4" value="1" id="weekStart_1" />
周 的星期
<input class="numberspinner" style="width: 60px" data-options="min:1,max:7" id="weekEnd_1" value="1" />
</div>
<div class="line">
<input type="radio" name="week" onclick="lastWeek(this)" />
本月最后一个星期
<input class="numberspinner" style="width: 60px" data-options="min:1,max:7" id="weekStart_2" value="1" />
</div>
<div class="line">
<input type="radio" name="week" id="week_appoint" />
指定
</div>
<div class="imp weekList">
<input type="checkbox" value="1" />
1
<input type="checkbox" value="2" />
2
<input type="checkbox" value="3" />
3
<input type="checkbox" value="4" />
4
<input type="checkbox" value="5" />
5
<input type="checkbox" value="6" />
6
<input type="checkbox" value="7" />
7
</div>
</div>
<div title="年">
<div class="line">
<input type="radio" checked="checked" name="year" onclick="unAppoint(this)" />
不指定 允许的通配符[, - * /] 非必填
</div>
<div class="line">
<input type="radio" name="year" onclick="everyTime(this)" />
每年
</div>
<div class="line">
<input type="radio" name="year" onclick="cycle(this)" />
周期 从
<input class="numberspinner" style="width: 90px" data-options="min:2018,max:3000" id="yearStart_0" value="2018" />
-
<input class="numberspinner" style="width: 90px" data-options="min:2020,max:3000" id="yearEnd_0" value="2020" />
</div>
</div>
</div>
</div>
<div data-options="region:'south',border:false" style="height: 250px">
<fieldset style="border-radius: 3px; height: 220px">
<legend>表达式</legend>
<table style="height: 100px">
<tbody>
<tr>
<td></td>
<td align="center">秒</td>
<td align="center">分钟</td>
<td align="center">小时</td>
<td align="center">日</td>
<td align="center">
月
<br />
</td>
<td align="center">星期</td>
<td align="center">年</td>
</tr>
<tr>
<td>表达式字段:</td>
<td>
<input type="text" name="v_second" class="col" value="*" readonly="readonly" />
</td>
<td>
<input type="text" name="v_min" class="col" value="*" readonly="readonly" />
</td>
<td>
<input type="text" name="v_hour" class="col" value="*" readonly="readonly" />
</td>
<td>
<input type="text" name="v_day" class="col" value="*" readonly="readonly" />
</td>
<td>
<input type="text" name="v_mouth" class="col" value="*" readonly="readonly" />
</td>
<td>
<input type="text" name="v_week" class="col" value="?" readonly="readonly" />
</td>
<td>
<input type="text" name="v_year" class="col" readonly="readonly" />
</td>
</tr>
<tr>
<td>Cron 表达式:</td>
<td colspan="6"><input type="text" name="cron" style="width: 100%" value="* * * * * ?" id="cron" /></td>
<td><input type="button" value="反解析到UI " id="btnFan" onclick="btnFan()" /></td>
</tr>
<tr>
<td colspan="8">最近5次运行时间:</td>
</tr>
<tr>
<td colspan="8" id="runTime"></td>
</tr>
</tbody>
</table>
</fieldset>
</div>
<div></div>
</div>
<script src="${base}/assets/platform/plugins/jquery/jquery.js" type="text/javascript"></script>
<script src="${base}/assets/platform/cron/js/cron.js" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,326 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava>
<el-card shadow="never">
<el-row type="flex" justify="end">
<el-button @click="openAdd" size="medium" type="primary">
<i class="ti-plus"></i>
新建任务
</el-button>
</el-row>
</el-card>
<el-card class="mt10" shadow="never">
<el-table :data="tableData" @sort-change="pageOrder" header-align="center" style="width: 100%">
<el-table-column sortable prop="name" label="任务名称" header-align="center"></el-table-column>
<el-table-column prop="jobClass" label="执行类" header-align="center" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="data" label="执行参数" header-align="center" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="cron" label="定时规则" header-align="center" align="center"></el-table-column>
<el-table-column prop="exeAt" label="执行时间" header-align="center" align="center">
<template slot-scope="scope">{{formatAt(scope.row.exeAt)}}</template>
</el-table-column>
<el-table-column sortable prop="disabled" label="启用状态" header-align="center" align="center">
<template slot-scope="scope">
<i v-if="scope.row.disabled" class="fa fa-circle text-danger ml5"></i>
<i v-if="!scope.row.disabled" class="fa fa-circle text-success ml5"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'enable',id:scope.row.id,name:scope.row.name,row:scope.row}">启用</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,name:scope.row.name,row:scope.row}">
禁用
</el-dropdown-item>
<el-dropdown-item divided :command="{type:'edit',id:scope.row.id}">修改</el-dropdown-item>
<el-dropdown-item :command="{type:'delete',id:scope.row.id,name:scope.row.name}">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</guava>
<el-dialog title="新建任务" :visible.sync="addDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="name" label="任务名称">
<el-input maxlength="100" placeholder="任务名称" v-model="formData.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="jobClass" label="执行类">
<el-input placeholder="执行类" v-model="formData.jobClass" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="cron" label="执行周期">
<el-input placeholder="Cron表达式" v-model="formData.cron" auto-complete="off" tabindex="3" type="text">
<template slot="append">
<a href="${base}/platform/sys/task/cron" style="color: #409eff" target="_blank">表达式生成器</a>
</template>
</el-input>
</el-form-item>
<el-form-item prop="data" label="执行参数(Json)">
<el-input type="textarea" v-model="formData.data" placeholder="{}" tabindex="4"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="修改任务" :visible.sync="editDialogVisible" :close-on-click-modal="false" width="50%">
<el-form :model="formData" ref="editForm" :rules="formRules" size="small" label-width="80px">
<el-form-item prop="name" label="任务名称">
<el-input maxlength="100" placeholder="任务名称" v-model="formData.name" auto-complete="off" tabindex="1" type="text"></el-input>
</el-form-item>
<el-form-item prop="jobClass" label="执行类">
<el-input placeholder="执行类" v-model="formData.jobClass" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="cron" label="执行周期">
<el-input placeholder="Cron表达式" v-model="formData.cron" auto-complete="off" tabindex="3" type="text">
<template slot="append">
<a href="${base}/platform/sys/task/cron" style="color: #409eff" target="_blank">表达式生成器</a>
</template>
</el-input>
</el-form-item>
<el-form-item prop="data" label="执行参数(Json)">
<el-input type="textarea" v-model="formData.data" placeholder="{}" tabindex="4"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data: function () {
return {
addDialogVisible: false,
editDialogVisible: false,
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
formData: {
id: ""
},
formRules: {
name: [{ required: true, message: "任务名称", trigger: "blur" }],
jobClass: [{ required: true, message: "执行类", trigger: "blur" }],
cron: [{ required: true, message: "执行周期", trigger: "blur" }]
}
}
},
methods: {
pageOrder: function (column) {
//按字段排序
this.pageForm.pageOrderName = column.prop
this.pageForm.pageOrderBy = column.order
this.pageData()
},
pageNumberChange: function (val) {
//页码更新操作
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange: function (val) {
//分页大小更新操作
this.pageForm.pageSize = val
this.pageData()
},
pageData: function () {
//加载分页数据
var self = this
$.post(
base + "/platform/sys/task/data",
self.pageForm,
function (data) {
if (data.code == 0) {
self.tableData = data.data.list
self.pageForm.totalCount = data.data.totalCount
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
},
formatAt: function (val) {
if (val && val > 0) return moment(val).format("YYYY-MM-DD HH:mm:ss")
return ""
},
doSearch: function () {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd: function () {
var self = this
this.addDialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd: function () {
var self = this
self.$refs["addForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/task/addDo",
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.addDialogVisible = false
} else {
self.$message({
message: data.msg,
type: "error"
})
}
self.pageData() //添加失败也可以入库,所以加载一下看看
},
"json"
)
}
})
},
doEdit: function () {
var self = this
self.$refs["editForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/task/editDo",
self.formData,
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.doSearch() //查询是带条件的,加载新建用户选择的单位用户数据
//self.pageData();
self.editDialogVisible = false
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
},
dropdownCommand: function (command) {
//监听下拉框事件
var self = this
if ("enable" == command.type || "disable" == command.type) {
$.post(
base + "/platform/sys/task/" + command.type + "/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
if ("disable" == command.type) {
command.row.disabled = true
}
if ("enable" == command.type) {
command.row.disabled = false
}
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("edit" == command.type) {
$.post(
base + "/platform/sys/task/edit/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.formData = data.data //加载后台表单数据
self.editDialogVisible = true //打开编辑窗口
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
if ("delete" == command.type) {
self.$confirm("此操作将删除 " + command.name, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
callback: function (a, b) {
if ("confirm" == a) {
//确认后再执行
$.post(
base + "/platform/sys/task/delete/" + command.id,
{},
function (data) {
if (data.code == 0) {
self.$message({
message: data.msg,
type: "success"
})
self.doSearch()
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
}
})
}
}
},
created: function () {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,151 @@
const branchUnionCadreAudit = {
template: /*language=HTML*/ `
<el-card shadow="never" class="mt10">
<table-tool label="基层干部">
<el-radio-group @change="doSearch" class="mr5" size="small" v-model="pageForm.audit">
<el-radio-button label="true">已审核</el-radio-button>
<el-radio-button label="false">未审核</el-radio-button>
</el-radio-group>
<!-- <el-button size="small" @click="doAudit(true)" :disabled="!multipleSelection.length" type="primary"-->
<!-- icon="el-icon-check">批量通过-->
<!-- </el-button>-->
<!-- <el-button size="small" @click="doAudit(false)" :disabled="!multipleSelection.length" type="danger"-->
<!-- icon="el-icon-check">批量拒绝-->
<!-- </el-button>-->
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder" ref="table" row-key="id"
style="width: 100%" @selection-change="handleSelectionChange" height="65vh">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column align="center" header-align="center" type="index" :index="indexMethod"
label="序号" width="80px"></el-table-column>
<el-table-column
align="center"
header-align="center"
v-for="column in tableColumns"
show-overflow-tooltip
:label="column.label"
:prop="column.prop"
:sortable="column.sortable"
>
<template v-if="column.prop==='isJoin'" scope="{row:{isJoin}}">
<span v-if="isJoin" class="text-info">加入</span>
<span v-else class="text-danger">退出</span>
</template>
<template scope="{row}" v-else-if="column.prop=='instanceState'">
<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="180px" v-if="!pageForm.audit">
<template scope="{row}">
<template v-if="row.taskState === 10">
<el-button @click="handleTaskAction(row, 1)" size="mini" type="primary">通过</el-button>
<el-button @click="handleTaskAction(row, 2)" size="mini" type="danger">拒绝</el-button>
</template>
<el-button v-if="row.canRevoke" @click="openRevoke(row)" size="mini" type="danger">
撤回
</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container" style="margin-bottom: 0">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageForm.totalCount">
</el-pagination>
</el-row>
</el-card>
`,
mixins: [initTableMixins],
data() {
return {
pageForm: {
audit: false
},
tableColumns: [
{ prop: 'loginName', label: '工号' },
{ prop: 'userName', label: '姓名' },
{ prop: 'unionName', label: '所属工会' },
{ prop: 'mobile', label: '联系方式' },
{ prop: 'roleName', label: '职务' },
{ prop: 'isJoin', label: '申请类型' },
{ prop: "curTaskName", label: "当前节点" },
{ prop: "instanceState", label: "流程状态" }
],
multipleSelection: [],
}
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val;
},
doAudit(pass) {
this.$confirm('是否确认提交?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const ids = JSON.stringify(this.multipleSelection.map(v => v.id))
this.$axios.post('/platform/sys/union/cadreDoAudit', {ids, pass})
.then(resp => {
if (resp.code === 0) {
this.pageData()
this.$refs.table.clearSelection();
this.$message.success(resp.msg)
}else{
this.$message.error(resp.msg)
}
})
})
},
handleTaskAction(row, val) {
const msg = val === 1 ? "通过" : "拒绝"
this.$confirm("您确定要" + msg + "吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const loading = createLoading()
this.formData = {
origin: row.origin,
processTaskId: row.taskId,
taskName: row.curTaskName
}
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
submitType: val
})
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
}).finally(() => {
loading.close()
})
})
},
pageData() {
this.$axios.post('/platform/sys/union/cadrePageData', this.pageForm)
.then(resp => {
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
}
})
}
}
}
@@ -0,0 +1,163 @@
const branchUnionCommitteeMemberManage = {
template: /*language=HTML*/ `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row :gutter="10" type="flex">
<el-select placeholder="请选择届次" v-model="pageForm.sessionId" style="width: 15%"
size="small">
<el-option v-for="item in sessionOptions" :label="item.fullName" :value="item.id"
:key="item.id"></el-option>
</el-select>
<el-input placeholder="请输入姓名或工号" clearable size="small"
style="width: 300px;margin-left: 10px;"
v-model="pageForm.searchKeyword"></el-input>
<el-button type="primary" class="ml5" size="small" icon="el-icon-search"
@click="doSearch"></el-button>
<el-button @click="openAdd" icon="ti-plus" type="primary" size="small" style="margin-left: auto">
添加委员
</el-button>
<el-button @click="deleteRole" icon="el-icon-delete" type="danger" size="small" style="margin-left: 10px">
删除当前届次委员角色
</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border>
<el-table-column label="序号" type="index" width="50" :index="indexMethod"></el-table-column>
<el-table-column prop="loginName" label="工号"></el-table-column>
<el-table-column prop="userName" label="姓名"></el-table-column>
<!-- <el-table-column prop="mobile" label="联系方式"></el-table-column>-->
<el-table-column prop="position" label="职务"></el-table-column>
<el-table-column prop="sessionName" label="届次"></el-table-column>
<el-table-column label="操作" width="100px">
<template scope="scope">
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="doDelete(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container mt20">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
</el-card>
<el-dialog title="添加" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="formRef" size="small" label-width="80px" :rules="formRules">
<el-form-item prop="sessionId" label="届次">
<el-select placeholder="请选择届次" v-model="formData.sessionId" clearable style="width: 100%">
<el-option v-for="item in sessionOptions" :label="item.fullName" :value="item.id"
:key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select
v-model="formData.userId"
style="width: 100%"
api="/platform/sys/committeeMember/listUserSelect"
:api_params="{ sessionId: pageForm.sessionId }"
:option_label_func="
(item) => {
return item.username + item.loginname + '(' + item.unitName + ')'
}
"
></user-select>
</el-form-item>
<el-form-item prop="position" label="职务">
<el-input v-model="formData.position" placeholder="请输入职务"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
data() {
return {
dialogFormVisible: false,
formData: {},
formRules: {
sessionId: [{required: true, message: "必填", trigger: ["change", "blur"]}],
userId: [{required: true, message: "必填", trigger: ["change", "blur"]}],
},
sessionOptions: []
}
},
methods: {
listSession() {
this.$axios.post("/platform/teacherCongress/common/listSession").then((res) => {
if (res.code === 0) {
this.sessionOptions = res.data
if (this.sessionOptions.length > 0) {
this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id)
this.pageData()
}
}
})
},
pageData() {
$.get("/platform/sys/committeeMember/pageData", this.pageForm).then((res) => {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
})
},
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
doSubmit() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
const session = this.sessionOptions.find(item => item.id === this.formData.sessionId)
this.formData.sessionName = session.fullName
$.post("/platform/sys/committeeMember/insert", this.formData).then((res) => {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.pageData()
})
})
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
$.post("/platform/sys/committeeMember/delete", {id}).then((res) => {
this.$message.success(res.msg)
this.pageData()
})
})
},
deleteRole() {
this.$confirm("您当前操作委员信息不会删除,只会删除当前届次委员的角色,确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
$.post("/platform/sys/committeeMember/deleteRole", {sessionId:this.pageForm.sessionId}).then((res) => {
this.$message.success(res.msg)
this.pageData()
})
})
}
},
created() {
this.listSession()
}
}
@@ -0,0 +1,197 @@
const branchUnionGroupManage = {
template: /*language=HTML*/ `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row type="flex" style="column-gap: 10px">
<el-input
placeholder="可输入名称查询"
size="small"
style="width: 400px"
clearable
v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"
></el-input>
<el-button type="primary" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left: auto"
@click="openAdd">
新增
</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 90) : null" border>
<el-table-column type="index" width="50" label="序号"></el-table-column>
<el-table-column prop="code" label="小组编码" width="120px"></el-table-column>
<el-table-column prop="name" label="小组名称" width="200px"></el-table-column>
<el-table-column prop="leaderInfos" label="组长信息"></el-table-column>
<el-table-column prop="unitInfos" label="组成单位"></el-table-column>
<el-table-column label="操作" width="200px">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="onEdit(row)">
编辑
</el-button>
<el-button type="danger" size="mini" @click="onDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑' : '新增'" :visible.sync="dialogFormVisible" width="600px"
:close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="小组编码" prop="code">
<el-input v-model="formData.code" placeholder="小组编码" maxlength="10"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="小组名称" prop="name">
<el-input v-model="formData.name" placeholder="小组名称" maxlength="100"></el-input>
</el-form-item>
<el-form-item label="组成单位" prop="unitIds">
<el-select clearable filterable placeholder="请选择组成单位" multiple
style="width: 100%" v-model="formData.unitIds">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in units"></el-option>
</el-select>
</el-form-item>
<el-form-item label="小组长" prop="leaders">
<el-select
v-model="formData.leaders"
filterable
remote multiple
reserve-keyword
placeholder="请输入姓名查询"
:remote-method="getUserOptions"
style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.username+'('+item.unitName+')'"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="onSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
props: {
union_id: {
type: String,
required: true
},
// 工会小组列表包含分页,父页面传入高度后预留分页区域。
table_height: {
type: Number,
default: null
}
},
data() {
return {
pageForm: {},
formData: {},
rules: {
code: [{ required: true, message: "请输入小组编码", trigger: "blur" }],
name: [{ required: true, message: "请输入小组名称", trigger: "blur" }],
leaders: [{ required: true, message: "请选择小组长", trigger: "change" }],
unitIds: [{ required: true, message: "请选择组成单位", trigger: "change" }],
},
userOptions: [],
units: [],
}
},
methods: {
async openAdd() {
this.dialogFormVisible = true
await this.getUnits()
this.$nextTick(() => {
this.formData = {}
this.$refs.formRef.clearValidate()
})
},
async onEdit(row) {
this.formData = { ...row }
this.formData.unitIds = JSON.parse(row.unitIds)
this.formData.leaders = JSON.parse(row.leaders)
await this.getUnits(row.id)
this.getUserOptions(null, this.formData.leaders)
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs.formRef.clearValidate()
})
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
const { id } = this.formData
const url = "/platform/sys/unionGroup" + (id ? "/update" : "/insert")
const formData = clone(this.formData)
formData.unitIds = JSON.stringify(formData.unitIds)
formData.leaders = JSON.stringify(formData.leaders)
$.post(url, { ...formData, unionId: this.union_id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogFormVisible = false
this.doSearch()
}
})
}
})
},
onDelete(id) {
this.$confirm("您确定要删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
$.post("/platform/sys/unionGroup/delete/" + id).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
.catch(() => {})
},
pageData() {
$.get("/platform/sys/unionGroup/pageData", { ...this.pageForm, unionId: this.union_id }).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
},
getUserOptions(keyword, userIds = null) {
$.get("/platform/sys/unionGroup/listUser", {
keyword,
unionId: this.union_id,
userIds: JSON.stringify(userIds)
}).then((res) => {
if (res.code === 0) {
this.userOptions = res.data
}
})
},
async getUnits(val) {
const resp = await this.$axios.post("/platform/sys/unionGroup/getUnits", {
unionId: this.union_id,
groupId: val
})
if (resp.code === 0) {
this.units = resp.data
}
}
}
}
@@ -0,0 +1,122 @@
const branchUnionManage = {
template: `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row :gutter="10" type="flex">
<el-input placeholder="请输入分工会名称" clearable size="small" style="width: 300px" v-model="pageForm.searchKeyword"></el-input>
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button @click="openAudit" icon="el-icon-s-check" type="primary" style="margin-left: auto" size="small" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])">基层干部审核</el-button>
<el-button @click="openAdd" icon="ti-plus" type="primary" size="small" v-if="$auth.hasPermission('sys.manager.union.add')">新建分工会</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border>
<el-table-column label="序号" type="index" width="50" :index="indexMethod"></el-table-column>
<el-table-column prop="name" label="工会名称" show-overflow-tooltip></el-table-column>
<el-table-column prop="unionCode" label="工会编码"></el-table-column>
<el-table-column prop="campus" label="校区"></el-table-column>
<el-table-column prop="chairman" label="分工会主席" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="250" v-if="$auth.hasPermissionOr(['sys.manager.union.edit','sys.manager.union.delete'])">
<template scope="scope">
<!-- <el-button v-if="$auth.hasPermission('sys.manager.union.branchOfficer')" type="primary" size="mini">设置干部</el-button>-->
<el-button v-if="$auth.hasPermission('sys.manager.union.edit')" type="primary" size="mini" @click="openEdit(scope.row.id)">编辑</el-button>
<el-button v-if="$auth.hasPermission('sys.manager.union.delete')" type="danger" size="mini" @click="doDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container mt20">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageForm.totalCount"
></el-pagination>
</el-row>
</el-card>
<el-dialog :title="formData.id ? '编辑分工会' : '新增分工会'" :visible.sync="dialogFormVisible" width="600px">
<el-form :model="formData" ref="formRef" :rules="formRules" size="small" label-width="80px">
<el-form-item label="工会名称" prop="name">
<el-input v-model="formData.name" maxlength="100" placeholder="工会名称"></el-input>
</el-form-item>
<el-form-item label="工会编码" prop="unionCode">
<el-input v-model="formData.unionCode" maxlength="100" placeholder="工会编码"></el-input>
</el-form-item>
<el-form-item label="校区" prop="campus">
<dict-select v-model="formData.campus" code="USER_CAMPUS"></dict-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
data() {
return {
dialogFormVisible: false,
formData: {},
formRules: {
name: [{ required: true, message: "请输入工会名称", trigger: ["change", "blur"] }],
unionCode: [{ required: true, message: "请输入工会编码", trigger: ["change", "blur"] }],
campus: [{ required: false, message: "请选择校区", trigger: ["change", "blur"] }]
}
}
},
methods: {
pageData() {
$.get("/platform/sys/union/pageData", this.pageForm).then((res) => {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
})
},
// 基层干部审核
openAudit() {
this.$emit('union-cadre')
},
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
openEdit(id) {
this.dialogFormVisible = true
$.get("/platform/sys/union/findOne", { id }).then((res) => {
this.formData = res.data
})
},
doSubmit() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
$.post("/platform/sys/union/" + (this.formData.id ? "update" : "insert"), this.formData).then((res) => {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.pageData()
this.$emit("refresh", null)
})
})
},
doDelete(id) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then(() => {
$.post("/platform/sys/union/doDelete", { id }).then((res) => {
this.$message.success(res.msg)
this.pageData()
this.$emit("refresh", null)
})
})
}
},
created() {
this.pageData()
}
}
@@ -0,0 +1,111 @@
const branchUnionPartUnitManage = {
template: `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row type="flex" style="column-gap: 10px">
<el-input
placeholder="请输入单位编码或单位名称查询"
size="small"
style="width: 400px"
clearable
v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"
></el-input>
<el-button type="primary" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button type="primary" size="small" icon="el-icon-setting" style="margin-left: auto" @click="openAdd" v-if="$auth.hasPermission('sys.manager.union.partUnit')">设置组成单位</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 90) : null" border>
<el-table-column type="index" width="50" label="序号" :index="indexMethod"></el-table-column>
<el-table-column prop="unitcode" label="单位编码"></el-table-column>
<el-table-column prop="name" label="单位名称"></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="设置组成单位" :visible.sync="dialogFormVisible" width="1000px" :close-on-click-modal="false">
<el-transfer
ref="transferRef"
:titles="['可选二级单位', '当前成员单位']"
filterable
:props="{
key: 'id',
label: 'name'
}"
:filter-method="filterUnit"
v-model="selectUnitsIds"
:data="allUnits"
></el-transfer>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
props: {
union_id: {
type: String,
required: true
},
// 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。
table_height: {
type: Number,
default: null
}
},
data() {
return {
dialogFormVisible: false,
selectUnitsIds: [],
allUnits: []
}
},
watch: {
union_id(val) {
// this.doSearch()
}
},
methods: {
openAdd() {
this.dialogFormVisible = true
if (this.$refs.transferRef) {
this.$refs.transferRef.clearQuery("left")
this.$refs.transferRef.clearQuery("right")
}
$.get("/platform/sys/union/branchUnionPartUnitTransferData", { unionId: this.union_id }).then((res) => {
this.selectUnitsIds = res.data.selectUnitIds
this.allUnits = res.data.allUnits
})
},
filterUnit(query, item) {
return item.name.indexOf(query) > -1
},
doSubmit() {
$.post("/platform/sys/union/branchUnionPartUnitSet", {
unitIds: JSON.stringify(this.selectUnitsIds),
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
}
})
},
pageData() {
$.get("/platform/sys/union/branchUnionPartUnitPageData", { ...this.pageForm, unionId: this.union_id }).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
}
}
}
@@ -0,0 +1,344 @@
const branchUnionUserManage = {
template:
/*language=HTML*/
`
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row type="flex" style="column-gap: 10px">
<el-input
placeholder="请输入内容"
size="small"
style="width: 400px"
clearable
v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"
>
<el-select v-model="pageForm.searchName" slot="prepend" size="small" placeholder="查询类型"
style="width: 80px !important;">
<el-option label="角色" value="roleName"></el-option>
<el-option label="姓名" value="username"></el-option>
<el-option label="工号" value="loginname"></el-option>
</el-select>
</el-input>
<el-select
v-model="pageForm.j"
placeholder="届数"
size="small"
clearable
class="j-search-select"
@change="handleJChange"
>
<el-option
v-for="item in jOptions"
:key="item.code"
:label="item.name"
:value="item.code"
>
<span>{{ item.name }}</span>
<span v-if="hasCadreData(item.code)" class="j-used-mark">*</span>
</el-option>
</el-select>
<el-button type="primary" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left: auto"
@click="openAdd" v-if="$auth.hasPermission('sys.manager.union.branchOfficer')">添加
</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" :height="table_height ? Math.max(200, table_height - 40) : null" border size="small">
<el-table-column prop="loginName" label="工号"></el-table-column>
<el-table-column prop="userName" label="姓名"></el-table-column>
<el-table-column prop="mobile" label="联系方式"></el-table-column>
<el-table-column label="届数">
<template scope="{row}">
{{ row.jName || row.j }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="职务"></el-table-column>
<el-table-column prop="displayStatus" label="当前状态"></el-table-column>
<el-table-column label="操作" width="150px"
v-if="$auth.hasPermission('sys.manager.union.branchOfficer')">
<template scope="scope">
<el-button
v-if="!isLeft(scope.row)"
size="mini"
type="warning"
icon="el-icon-switch-button"
@click="openLeave(scope.row)"
></el-button>
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="doDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="添加" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="form" size="small" label-width="80px">
<el-form-item prop="roleCode" label="角色">
<el-select v-model="formData.roleCode" placeholder="请选择角色" @change="handleRoleChange">
<el-option v-for="item in roleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="isUnitPartySecretary()" prop="unitIds" label="所属单位">
<el-select v-model="formData.unitIds" multiple collapse-tags placeholder="请选择单位">
<el-option v-for="item in partySecretaryUnitOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="j" label="届数">
<dict-select v-model="formData.j" code="TEACHER_CONGRESS_J" placeholder="请选择届数"></dict-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select
v-model="formData.userId"
style="width: 100%"
api="/platform/sys/union/listUnion"
:api_params="{ unionId: union_id }"
:option_label_func="
(item) => {
return item.username + item.loginname + '(' + item.unitName + ')'
}
"
></user-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="离任" :visible.sync="leaveDialogVisible" width="420px" :close-on-click-modal="false">
<el-form :model="leaveForm" ref="leaveForm" size="small" label-width="90px">
<el-form-item label="离任时间" required>
<el-date-picker
v-model="leaveForm.leaveDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择离任时间"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="leaveDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doLeave">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
props: {
union_id: {
type: String,
required: true
},
// 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。
table_height: {
type: Number,
default: null
}
},
data() {
return {
pageForm: {
searchName: "username",
j: ""
},
dialogFormVisible: false,
formData: {},
roleOptions: [],
partySecretaryUnitOptions: [],
leaveDialogVisible: false,
leaveForm: {
id: "",
leaveDate: ""
},
jOptions: [],
usedJCodes: []
}
},
watch: {
union_id(val) {
this.pageForm.j = ""
this.initJSearch()
}
},
methods: {
loadJOptions() {
return $.get("/open/common/dictOptions", { code: "TEACHER_CONGRESS_J" }).then((res) => {
this.jOptions = res.data || []
})
},
loadUsedJCodes() {
if (!this.union_id) {
this.usedJCodes = []
return $.Deferred().resolve().promise()
}
return $.get("/platform/sys/union/branchUnionUserUsedJData", {
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.usedJCodes = res.data || []
}
})
},
hasCadreData(code) {
return this.usedJCodes.includes(code)
},
getHighestUsedJ() {
if (!this.usedJCodes.length) {
return ""
}
for (let i = this.jOptions.length - 1; i >= 0; i--) {
if (this.usedJCodes.includes(this.jOptions[i].code)) {
return this.jOptions[i].code
}
}
return this.usedJCodes[0]
},
initJSearch() {
const optionTask = this.jOptions.length ? $.Deferred().resolve().promise() : this.loadJOptions()
$.when(optionTask, this.loadUsedJCodes()).then(() => {
// 页面初始化时不限定届次,默认展示该分工会所有届次的干部。
this.pageForm.j = ""
this.pageData()
})
},
handleJChange(val) {
this.pageForm.j = val || ""
this.doSearch()
},
isLeft(row) {
return row.isServing === false || row.isServing === 0 || row.displayStatus === "离任"
},
isUnitPartySecretary() {
return this.formData.roleCode === "UNIT_PARTY_SECRETARY"
},
loadRoleOptions() {
return $.get("/platform/sys/union/branchUnionRoleOptions").then((res) => {
if (res.code === 0) {
this.$set(this, "roleOptions", res.data || [])
}
})
},
loadPartySecretaryUnitOptions() {
return $.get("/platform/sys/union/branchUnionPartySecretaryUnitOptions", {
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.$set(this, "partySecretaryUnitOptions", res.data || [])
// 新增二级党委书记时,默认授权当前分工会下的全部二级单位。
this.$set(this.formData, "unitIds", this.partySecretaryUnitOptions.map(item => item.id))
}
})
},
handleRoleChange() {
if (this.isUnitPartySecretary()) {
this.loadPartySecretaryUnitOptions()
return
}
this.$set(this.formData, "unitIds", [])
},
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
openLeave(row) {
this.leaveForm = {
id: row.id,
leaveDate: ""
}
this.leaveDialogVisible = true
},
doSubmit() {
if (!this.formData.j) {
this.$message.error("请选择届数")
return
}
if (this.isUnitPartySecretary() && (!this.formData.unitIds || this.formData.unitIds.length === 0)) {
this.$message.error("请选择所属单位")
return
}
const submitUrl = this.isUnitPartySecretary()
? "/platform/sys/union/insertBranchUnionPartySecretaryRole"
: "/platform/sys/union/insertBranchUnionUserRole"
$.post(submitUrl, {
...this.formData,
unitIds: JSON.stringify(this.formData.unitIds || []),
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.loadUsedJCodes()
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
},
doLeave() {
if (!this.leaveForm.leaveDate) {
this.$message.error("请选择离任时间")
return
}
$.post("/platform/sys/union/leaveBranchUnionUserRole", this.leaveForm).then((res) => {
if (res.code === 0) {
this.leaveDialogVisible = false
this.$message.success(res.msg)
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
},
doDelete({ userId, roleCode }) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then((res) => {
$.post("/platform/sys/union/deleteBranchUnionUserRole", {
userId,
roleCode,
unionId: this.union_id
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.loadUsedJCodes()
this.doSearch()
}
})
})
},
pageData() {
$.get("/platform/sys/union/branchUnionUserPageData", {
...this.pageForm,
unionId: this.union_id,
j: this.pageForm.j || ""
}).then((res) => {
if (res.code === 0) {
this.tableData = res.data
}
})
}
},
created() {
this.loadRoleOptions()
this.initJSearch()
},
style: /*language=CSS*/ `
/deep/ .el-select {
width: 100% !important;
}
/deep/ .j-search-select {
width: 80px !important;
}
.j-used-mark {
color: #f56c6c;
margin-left: 4px;
font-weight: 700;
}
`
}
@@ -0,0 +1,184 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-row type="flex" :gutter="20" style="height: calc(100vh - 126px)">
<el-col :span="5">
<el-card shadow="never" style="height: 100%" body-style="{ height: '100%' }">
<el-input placeholder="输入关键字进行查找" v-model="filterText" clearable></el-input>
<div style="max-height: calc(100vh - 200px); overflow-y: auto">
<el-tree
:data="treeData"
ref="treeRef"
:expand-on-click-node="false"
:props="{
children: 'children',
label: 'name'
}"
default-expand-all
@node-click="treeNodeClick"
:filter-node-method="filterNode"
highlight-current
>
<template slot-scope="{ node, data }">
<span class="el-tree-node__label">
<i class="el-icon-folder-opened" v-if="node.level===1"></i>
<i class="el-icon-folder" v-else></i>
{{node.label}}
</span>
</template>
</el-tree>
</div>
</el-card>
</el-col>
<el-col :span="19">
<el-card shadow="never">
<el-tabs v-model="schoolUnionTabActive" type="card" v-if="currentTreeNode==null || currentTreeNode.level===1">
<el-tab-pane name="branchUnionManage">
<span slot="label">
<i class="el-icon-school"></i>
分工会信息
</span>
<sys-union-branch-union-manage @refresh="getTreeData()" @union-cadre="unionCadreInit"></sys-union-branch-union-manage>
</el-tab-pane>
<el-tab-pane name="schoolUnionInfo" v-if="$auth.hasPermission('sys.manager.union.schoolOfficer')">
<span slot="label">
<i class="el-icon-office-building"></i>
校工会信息
</span>
<sys-union-school-union-user-manage></sys-union-school-union-user-manage>
</el-tab-pane>
<el-tab-pane name="unionCommitteeMemberManage" v-if="$auth.hasPermission('sys.manager.union.committeeMember')">
<span slot="label">
<i class="el-icon-office-building"></i>
工会委员信息
</span>
<sys-union-committee-member-manage></sys-union-committee-member-manage>
</el-tab-pane>
</el-tabs>
<el-tabs v-model="branchUnionTabActive" @tab-click="branchTabChange" type="card" v-if="currentTreeNode && currentTreeNode.level===2">
<el-tab-pane name="branchUnionUserManage">
<span slot="label">
<i class="el-icon-school"></i>
分工会干部
</span>
<sys-union-branch-union-user-manage
ref="branchUnionUserManageRef"
:union_id="currentTreeData.id"
></sys-union-branch-union-user-manage>
</el-tab-pane>
<el-tab-pane name="branchUnionPartUnitManage">
<span slot="label">
<i class="el-icon-school"></i>
组成单位
</span>
<sys-union-branch-union-part-unit-manage
ref="branchUnionPartUnitManageRef"
:union_id="currentTreeData.id"
></sys-union-branch-union-part-unit-manage>
</el-tab-pane>
<el-tab-pane name="branchUnionGroup">
<span slot="label">
<i class="el-icon-school"></i>
工会小组
</span>
<sys-union-branch-union-group-manage
ref="branchUnionGroupRef"
:union_id="currentTreeData.id"
></sys-union-branch-union-group-manage>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</template>
<template #edit>
<sys-union-branch-union-cadre-audit ref="unionCadreAuditRef">
</sys-union-branch-union-cadre-audit>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include("branchUnionManage.js"){}#-->
<!--#include("schoolUnionUserManage.js"){}#-->
<!--#include("branchUnionUserManage.js"){}#-->
<!--#include("branchUnionPartUnitManage.js"){}#-->
<!--#include("branchUnionGroupManage.js"){}#-->
<!--#include("branchUnionCadreAudit.js"){}#-->
<!--#include("branchUnionCommitteeMemberManage.js"){}#-->
new Vue({
el: "#app",
components: {
"sys-union-branch-union-manage": branchUnionManage,
"sys-union-school-union-user-manage": schoolUnionUserManage,
"sys-union-branch-union-user-manage": branchUnionUserManage,
"sys-union-branch-union-part-unit-manage": branchUnionPartUnitManage,
"sys-union-branch-union-group-manage": branchUnionGroupManage,
"sys-union-branch-union-cadre-audit": branchUnionCadreAudit,
"sys-union-committee-member-manage": branchUnionCommitteeMemberManage,
},
data() {
return {
filterText: null,
treeData: [],
currentTreeNode: null,
currentTreeData: null,
schoolUnionTabActive: "branchUnionManage",
branchUnionTabActive: "branchUnionUserManage"
}
},
watch: {
filterText(val) {
this.$refs.treeRef.filter(val)
}
},
methods: {
treeNodeClick(data, node) {
this.currentTreeData = data
this.currentTreeNode = node
if (node.level === 2) {
this.$nextTick(() => {
this.$refs[this.branchUnionTabActive + "Ref"].doSearch()
})
}
},
filterNode(value, data) {
if (!value) return true
return data.name.indexOf(value) !== -1
},
getTreeData() {
$.get(loc() + "/tree").then((res) => {
this.treeData = res.data
})
},
branchTabChange(val) {
this.$nextTick(() => {
this.$refs[val.name + "Ref"].doSearch()
})
},
// 基层干部审核初始化
unionCadreInit() {
this.$refs.guava.edit(() => {
this.$refs.unionCadreAuditRef.pageData()
})
}
},
created() {
this.getTreeData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,110 @@
const schoolUnionUserManage = {
template:
/*language=HTML*/
`
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row type="flex" style="column-gap: 10px">
<el-input
placeholder="请输入内容"
size="small"
style="width: 400px"
clearable
v-model="pageForm.searchKeyword"
@keyup.enter.native="doSearch"
>
<el-select v-model="pageForm.searchName" size="small" slot="prepend" placeholder="查询类型"
style="width: 120px">
<el-option label="角色" value="roleName"></el-option>
<el-option label="姓名" value="username"></el-option>
<el-option label="工号" value="loginname"></el-option>
</el-select>
</el-input>
<el-button type="primary" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button type="primary" size="small" icon="el-icon-plus" style="margin-left: auto"
@click="openAdd">添加
</el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-table :data="tableData" border>
<el-table-column prop="username" label="姓名"></el-table-column>
<el-table-column prop="loginname" label="工号"></el-table-column>
<el-table-column prop="roleName" label="角色"></el-table-column>
<el-table-column label="操作" width="100px">
<template scope="scope">
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="doDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="添加" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="form" size="small" label-width="80px">
<el-form-item prop="roleCode" label="角色">
<dict-select v-model="formData.roleCode" code="SCHOOL_UNION_ROLES"></dict-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select v-model="formData.userId" api_input_key_name="query"></user-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
mixins: [initTableMixins],
props: {},
data() {
return {
pageForm: {
searchName: "username"
},
dialogFormVisible: false,
formData: {}
}
},
methods: {
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
doSubmit() {
$.post("/platform/sys/union/insertSchoolUnionUserRole", this.formData).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
}
})
},
doDelete({ userId, roleCode }) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then((res) => {
$.post("/platform/sys/union/deleteSchoolUnionUserRole", { userId, roleCode }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
pageData() {
$.get("/platform/sys/union/schoolUnionUserPageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data
}
})
}
},
created() {
this.pageData()
}
}
@@ -0,0 +1,209 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style scoped>
.custom-tree {
background: transparent;
padding: 8px;
}
/* 节点行高调整 */
.custom-tree .el-tree-node {
margin: 2px 0;
border-radius: 6px;
transition: all 0.2s ease;
}
/* 选中状态 */
.custom-tree .el-tree-node.is-current > .el-tree-node__content {
background: #e8f4ff;
border-left: 3px solid #409eff;
}
/* 节点内容区域 */
.custom-tree .el-tree-node__content {
height: 36px;
line-height: 36px;
padding: 0 8px;
border-radius: 6px;
transition: all 0.2s ease;
}
/* 自定义节点内容 */
.custom-tree-node {
display: flex;
align-items: center;
width: 100%;
gap: 8px;
}
/* 文件夹图标颜色优化 */
.el-icon-folder {
color: #909399;
}
.el-icon-folder-opened {
color: #409eff;
}
/* 展开时图标旋转动画 */
.custom-tree .el-tree-node__expand-icon {
transition: transform 0.3s ease;
}
.custom-tree .el-tree-node__expand-icon.expanded {
transform: rotate(90deg);
}
/* 节点标签 */
.node-label {
flex: 1;
font-size: 13px;
color: #303133;
font-weight: 500;
}
/* 禁用展开图标(当无子节点时) */
.custom-tree .el-tree-node__expand-icon.is-leaf {
color: transparent;
cursor: default;
}
</style>
<div id="app" v-cloak>
<el-row type="flex" :gutter="20" style="height: calc(100vh - 126px)">
<el-col :span="5">
<el-card shadow="never">
<el-input placeholder="输入关键字进行查找" v-model="filterText" clearable></el-input>
<div style="max-height: calc(100vh - 200px); overflow-y: auto">
<el-tree
:data="treeData"
ref="treeRef"
:expand-on-click-node="false"
:props="{ children: 'children', label: 'name' }"
node-key="id"
:default-expanded-keys="defaultExpandedKeys"
@node-click="treeNodeClick"
:filter-node-method="filterNode"
highlight-current
class="custom-tree"
>
<template slot-scope="{ node, data }">
<span class="custom-tree-node">
<i class="el-icon-folder-opened" v-if="node.level===1"></i>
<i class="el-icon-folder" v-else></i>
<span class="node-label">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
</el-card>
</el-col>
<el-col :span="19">
<el-card v-if="currentTreeData && currentTreeData.unitTypeCode==0" shadow="never">
<unit-manage
:current-Node="currentTreeNode?.level"
:current-data="currentTreeData"
:unit-level="2"
@refresh="getTreeData()"
ref="bzlRef"
></unit-manage>
</el-card>
<el-card shadow="never" v-if="currentTreeData && currentTreeData.unitTypeCode==1">
<el-tabs v-model="unitTabActive" type="card">
<el-tab-pane name="threeUnitManage">
<span slot="label">
<i class="el-icon-school"></i>
下设单位
</span>
<unit-manage
:current-data="currentTreeData"
:unit-level="3"
:current-Node="currentTreeNode?.level"
@refresh="getTreeData()"
style="margin-top: 10px"
ref="bmlRef"
></unit-manage>
</el-tab-pane>
<el-tab-pane name="unitUserManage">
<span slot="label">
<i class="el-icon-school"></i>
单位人员
</span>
<unit-user ref="userRef" :current-data="currentTreeData"></unit-user>
</el-tab-pane>
<el-tab-pane class="tab" name="unitLeaderManage">
<span slot="label">
<i class="el-icon-school"></i>
单位干部
</span>
<unit-leader-manage ref="leaderRef" :current-data="currentTreeData"></unit-leader-manage>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
<script nonce="${cspNonce!}">
<!--#include("unitUser.js"){}#-->
<!--#include("unitManage.js"){}#-->
<!--#include("unitLeader.js"){}#-->
new Vue({
el: "#app",
data: function () {
return {
unitTabActive: "threeUnitManage",
filterText: null,
currentTreeNode: null,
currentTreeData: null,
defaultExpandedKeys: [],
treeData: []
}
},
components: {
"unit-manage": UNIT_MANAGE_TEMPLATE,
"unit-user": UNIT_USER_MANAGE_TEMPLATE,
"unit-leader-manage": UNIT_LEADER_MANAGE_TEMPLATE
},
watch: {
filterText(val) {
this.$refs.treeRef.filter(val)
}
},
methods: {
getTreeData() {
this.$axios.post(loc() + "/tree").then((res) => {
this.treeData = res.data
if (this.treeData) {
this.currentTreeData = this.treeData[0]
this.defaultExpandedKeys = this.treeData.length > 0 ? [this.treeData[0].id] : []
}
})
},
treeNodeClick(data, node) {
this.currentTreeData = data
this.currentTreeNode = node
this.$nextTick(()=>{
if (data.unitTypeCode==1) {
this.$refs.bmlRef.pageData()
this.$refs.userRef.pageData()
this.$refs.leaderRef.pageData()
} else {
this.$refs.bzlRef.pageData()
}
})
},
filterNode(value, data, node) {
if (!value) return true
return data.name.indexOf(value) !== -1
}
},
created() {
this.getTreeData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,153 @@
const UNIT_LEADER_MANAGE_TEMPLATE = {
template: `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row :gutter="10" type="flex">
<el-input placeholder="请输入内容" size="small" v-model="pageForm.searchKeyword"
clearable
@keyup.enter.native="doSearch" style="width: 300px">
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 80px">
<el-option label="姓名" value="username"></el-option>
<el-option label="工号" value="loginname"></el-option>
</el-select>
</el-input>
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<table-tool label="人员列表">
<el-button @click="openAdd" type="primary" size="small">新增</el-button>
</table-tool>
<el-table :data="tableData" size="mini">
<el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
<el-table-column label="姓名" prop="username"></el-table-column>
<el-table-column label="工号" prop="loginname"></el-table-column>
<el-table-column label="角色" prop="roleName"></el-table-column>
<el-table-column label="操作" width="100px">
<template v-slot="scope">
<el-button size="mini" type="danger" @click="doDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="添加" :visible.sync="dialogFormVisible" width="600px" :close-on-click-modal="false">
<el-form :model="formData" ref="form" size="small" label-width="60px">
<el-form-item prop="roleCode" label="角色">
<dict-select placeholder="请选择角色" v-model="formData.roleCode" code="UNIT_ROLES" @change="onRoleChange"></dict-select>
</el-form-item>
<el-form-item prop="userId" label="人员">
<user-select
placeholder="请选择人员"
style="width: 100%"
v-model="formData.userId"
api="/platform/sys/unit/listUserSelect"
:api_params="{ unitId: currentData?.id, roleCode: formData.roleCode }"
:option_label_func="
(item) => {
return item.username + item.loginname + '(' + item.unitName + ')'
}
"
></user-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="doSubmit">确 定</el-button>
</div>
</el-dialog>
</div>
`,
props: {
currentData: {
type: Object,
required: false
}
},
data() {
return {
dialogFormVisible: false,
formData: {},
pageForm: {
unitName: "",
searchName: "username",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
audit: false
},
tableData: []
}
},
computed: {},
methods: {
onRoleChange() {
this.$set(this.formData, "userId", "")
},
doDelete({ userId, roleCode }) {
this.$confirm("您确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "info"
}).then((res) => {
$.post("/platform/sys/unit/deleteUnitUserRole", {
userId,
roleCode,
unitId: this.currentData.id
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
},
openAdd() {
this.dialogFormVisible = true
this.$nextTick(() => {
this.formData = {}
})
},
doSubmit() {
$.post("/platform/sys/unit/insertUnitUserRole", {
...this.formData,
unitId: this.currentData.id
}).then((res) => {
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success(res.msg)
this.doSearch()
}
})
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.pageData()
},
pageData() {
this.pageForm.unitId = this.currentData.id
this.$axios.post(loc() + "/leaderPageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
}
},
created() {
this.pageData()
}
}
@@ -0,0 +1,223 @@
const UNIT_MANAGE_TEMPLATE = {
template: /*language=HTML*/ `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter);">
<el-row :gutter="10" type="flex">
<el-input placeholder="请输入单位名称" clearable size="small" style="width: 300px" v-model="pageForm.unitName"></el-input>
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<table-tool label="单位列表">
<el-button @click="openAdd" type="primary" size="small">新建单位</el-button>
</table-tool>
<el-table :data="tableData" size="mini">
<el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
<el-table-column label="单位名称" prop="name"></el-table-column>
<el-table-column label="单位代码" prop="unitcode" width="200"></el-table-column>
<el-table-column label="单位等级" prop="unitLevel" width="100"></el-table-column>
<el-table-column label="操作" width="200px">
<template v-slot="scope">
<el-button size="mini" type="primary" @click="openEdit(scope.row.id)">编辑</el-button>
<el-button size="mini" type="danger" @click="doDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="设置单位" :visible.sync="dialogVisible" :close-on-click-modal="false" width="40%" top="2%">
<el-form :model="formData" ref="form" :rules="formRules" size="small" label-width="80px">
<el-form-item label="上级单位">
<el-input maxlength="100" disabled :value="currentData?.name" type="text"></el-input>
</el-form-item>
<el-form-item prop="name" label="单位名称">
<el-input maxlength="100" placeholder="请输入单位名称" v-model="formData.name" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<!-- <el-form-item prop="aliasName" label="单位别名">-->
<!-- <el-input maxlength="100" placeholder="请输入单位别名" v-model="formData.aliasName" auto-complete="off" tabindex="3" type="text"></el-input>-->
<!-- </el-form-item>-->
<el-form-item prop="unitcode" label="单位编码">
<el-input maxlength="100" placeholder="请输入单位编码" v-model="formData.unitcode" auto-complete="off" tabindex="4" type="text"></el-input>
</el-form-item>
<el-form-item prop="unitLevel" label="单位等级">
<el-select v-model="formData.unitLevel" placeholder="请输入单位等级" style="width: 100%">
<el-option v-if="unitLevel === 1" label="一级单位" :value="1"></el-option>
<el-option v-if="unitLevel === 2" label="二级单位" :value="2"></el-option>
<el-option v-if="unitLevel === 3" label="三级单位" :value="3"></el-option>
<el-option v-if="unitLevel === 4" label="四级单位" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="unionId" label="所属工会">
<el-select v-model="formData.unionId" placeholder="请选择所属工会" style="width: 100%">
<el-option :key="item.id" :label="item.name" :value="item.id"
v-for="item in unions"></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item prop="address" label="单位地址">-->
<!-- <el-input maxlength="100" placeholder="请输入单位地址" v-model="formData.address" auto-complete="off" tabindex="5" type="text"></el-input>-->
<!-- </el-form-item>-->
<!-- <el-form-item prop="telephone" label="联系电话">-->
<!-- <el-input maxlength="100" placeholder="请输入联系电话" v-model="formData.telephone" auto-complete="off" tabindex="6" type="text"></el-input>-->
<!-- </el-form-item>-->
<!-- <el-form-item prop="email" label="单位邮箱">-->
<!-- <el-input maxlength="100" placeholder="请输入单位邮箱" v-model="formData.email" auto-complete="off" tabindex="7" type="text"></el-input>-->
<!-- </el-form-item>-->
<!-- <el-form-item prop="website" label="单位网站">-->
<!-- <el-input maxlength="100" placeholder="请输入单位网站" v-model="formData.website" auto-complete="off" tabindex="8" type="text"></el-input>-->
<!-- </el-form-item>-->
<!-- <el-form-item prop="note" label="单位介绍">-->
<!-- <el-input type="textarea" v-model="formData.note" placeholder="请输入单位介绍"></el-input>-->
<!-- </el-form-item>-->
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doHandle">确 定</el-button>
</span>
</el-dialog>
</div>
`,
props: {
currentData: {
type: Object,
required: false
},
currentNode: {
type: Number,
required: false
},
unitLevel: {
type: Number,
required: true
}
},
store,
data() {
return {
pageForm: {
unitName: "",
searchName: "",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
audit: false
},
tableData: [],
dialogVisible: false,
formData: {},
formRules: {
name: [{ required: true, message: "必填", trigger: "blur" }],
unitcode: [{ required: true, message: "必填", trigger: "blur" }],
unitLevel: [{ required: false, message: "必填", trigger: "blur" }],
email: [
{ required: false, message: "单位邮箱", trigger: "blur" },
{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }
]
},
options: [],
parentUnit: [],
unions: [],
}
},
computed: {},
methods: {
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd() {
this.dialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
this.parentUnit = []
this.options = []
},
doHandle() {
this.$confirm("确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const self = this
const url = this.formData.id ? "/platform/sys/unit/editDo" : "/platform/sys/unit/addDo"
this.$refs["form"].validate(async function (valid) {
if (valid) {
if (self.currentNode) {
self.formData.parentId = self.currentData.id
}
const resp = await self.$axios.post(url, self.formData)
if (resp.code === 0) {
self.$message.success(resp.msg)
self.doSearch()
self.dialogVisible = false
self.$emit("refresh", null)
} else {
self.$message.warning(resp.msg)
}
}
})
})
},
async openEdit(id) {
console.log(this.currentData)
const resp = await this.$axios.post("/platform/sys/unit/edit/" + id)
if (resp.code === 0) {
this.formData = resp.data
this.dialogVisible = true
} else {
this.$message.warning(resp.msg)
}
},
async doDelete(id) {
const confirm = await this.$confirm("此操作将永久删除, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
if (confirm === "confirm") {
const resp = await this.$axios.post("/platform/sys/unit/delete/" + id)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.$emit("refresh", null)
} else {
this.$message.warning(resp.msg)
}
}
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.pageData()
},
pageData() {
this.pageForm.unitId = this.currentData.id
// this.pageForm.unitLevel = this.unitLevel
this.$axios.post(loc() + "/pageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
}
},
async created() {
this.pageData()
if (this.$auth.hasRoleOr('SYSADMIN, SCHOOL_UNION_ADMIN')) {
this.unions = await this.$businessTool.listUnion()
} else {
this.unions = await this.$businessTool.listUnion(this.store.state.user.union.id)
}
}
}
@@ -0,0 +1,89 @@
const UNIT_USER_MANAGE_TEMPLATE = {
template: /*language=HTML*/ `
<div>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<el-row :gutter="10" type="flex">
<el-input placeholder="请输入内容" size="small" v-model="pageForm.searchKeyword"
clearable
@keyup.enter.native="doSearch" style="width: 300px">
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 80px">
<el-option label="姓名" value="username"></el-option>
<el-option label="工号" value="loginname"></el-option>
</el-select>
</el-input>
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
</el-row>
</el-card>
<el-card shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
<table-tool label="人员列表">
</table-tool>
<el-table :data="tableData" size="mini">
<el-table-column label="序号" type="index" width="60">
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
<el-table-column label="姓名" prop="username"></el-table-column>
<el-table-column label="工号" prop="loginname"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="联系方式" prop="mobile"></el-table-column>
<el-table-column label="是否会员" prop="member">
<template v-slot="scope">
<el-tag v-if="scope.row.member" type="success">是</el-tag>
<el-tag v-else type="danger">否</el-tag>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
`,
props: {
currentData: {
type: Object,
required: false
}
},
data() {
return {
pageForm: {
unitName: "",
searchName: "username",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
audit: false
},
tableData: []
}
},
computed: {},
methods: {
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
pageNumberChange(val) {
this.pageForm.pageNumber = val
this.pageData()
},
pageSizeChange(val) {
this.pageForm.pageSize = val
this.pageData()
},
pageData() {
this.pageForm.unitId = this.currentData.id
this.$axios.post(loc() + "/userPageData", this.pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
}
},
created() {
this.pageData()
}
}
@@ -0,0 +1,472 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="所属单位">
<el-select clearable filterable placeholder="请选择所属单位" style="width: 100%" v-model="pageForm.searchUnit" @change="doSearch">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in units"></el-option>
</el-select>
<!-- <el-cascader :props="props" v-model="parentUnitSearch" placeholder="请选择单位" clearable filterable></el-cascader>-->
</search-item>
<search-item>
<el-input placeholder="请输入内容" v-model="pageForm.searchKeyword" @keyup.enter.native="doSearch">
<el-select v-model="pageForm.searchName" slot="prepend" placeholder="查询类型" style="width: 120px">
<el-option label="工号" value="loginname"></el-option>
<el-option label="姓名" value="username"></el-option>
</el-select>
<el-button slot="append" icon="el-icon-search" @click="doSearch"></el-button>
</el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button size="small" @click="openAdd" type="primary">
<i class="ti-plus"></i>
新建用户
</el-button>
</table-tool>
<el-table
:key="tableKey"
:data="tableData"
@sort-change="pageOrder"
header-align="center"
:default-sort="{prop: 'createdAt', order: 'descending'}"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="35"></el-table-column>
<el-table-column sortable prop="loginname" label="工号" header-align="center"></el-table-column>
<el-table-column prop="username" label="姓名" header-align="center" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="sex" label="性别" header-align="center" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="unitName" label="所属单位" header-align="center" align="center" :show-overflow-tooltip="true"></el-table-column>
<!-- <el-table-column sortable prop="disabled" label="帐号状态" header-align="center" align="center" :show-overflow-tooltip="true">-->
<!-- <template slot-scope="scope">-->
<!-- <i v-if="scope.row.disabled" class="fa fa-circle text-danger ml5"></i>-->
<!-- <i v-if="!scope.row.disabled" class="fa fa-circle text-success ml5"></i>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column sortable prop="userOnline" label="在线状态" header-align="center" align="center" :show-overflow-tooltip="true">-->
<!-- <template slot-scope="scope">-->
<!-- <i v-if="scope.row.userOnline" class="text-success ml5">在线</i>-->
<!-- <i v-if="!scope.row.userOnline" class="text-danger ml5">离线</i>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column prop="userOnline" label="操作" width="120">
<template slot-scope="scope">
<el-dropdown @command="dropdownCommand">
<el-button size="mini">
<i class="ti-settings"></i>
<span class="ti-angle-down"></span>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type:'role',id:scope.row.id,loginname:scope.row.loginname}">查看权限</el-dropdown-item>
<el-dropdown-item :command="{type:'reset',id:scope.row.id,loginname:scope.row.loginname}">重置密码</el-dropdown-item>
<el-dropdown-item divided :command="{type:'enable',id:scope.row.id,row:scope.row}">启用</el-dropdown-item>
<el-dropdown-item :command="{type:'disable',id:scope.row.id,row:scope.row}">禁用</el-dropdown-item>
<el-dropdown-item divided :command="{type:'edit',id:scope.row.id}">修改</el-dropdown-item>
<el-dropdown-item
:command="{type:'delete',id:scope.row.id,loginname:scope.row.loginname}"
v-if="scope.row.loginname!='superadmin'"
>
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="新建用户" :visible.sync="addDialogVisible" :close-on-click-modal="false" width="40%">
<el-form :model="formData" ref="addForm" :rules="formRules" size="small" label-width="80px">
<el-form-item class="is-required" prop="parentUnit" label="所属单位" label-width="80px">
<el-cascader :props="props" v-model="parentUnit" placeholder="请选择单位" filterable style="width: 100%"></el-cascader>
</el-form-item>
<el-form-item prop="loginname" label="工号">
<el-input maxlength="100" placeholder="工号" v-model="formData.loginname" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="username" label="姓名">
<el-input maxlength="20" placeholder="姓名" v-model="formData.username" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="password" label="登录密码">
<el-input
maxlength="20"
placeholder="登录密码"
v-model="formData.password"
auto-complete="off"
tabindex="4"
type="password"
></el-input>
</el-form-item>
<el-form-item prop="passwordAgain" label="确认密码">
<el-input
maxlength="20"
placeholder="再输一次密码"
v-model="formData.passwordAgain"
auto-complete="off"
tabindex="5"
type="password"
></el-input>
</el-form-item>
<el-form-item prop="mobile" label="手机号码">
<el-input maxlength="20" placeholder="手机号码" v-model="formData.mobile" auto-complete="off" tabindex="6" type="text"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAdd">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="编辑用户" :visible.sync="editDialogVisible" :close-on-click-modal="false" width="40%">
<el-form :model="formData" ref="editForm" :rules="editRules" size="small" label-width="80px">
<el-form-item prop="loginname" label="工号">
<el-input maxlength="100" placeholder="工号" v-model="formData.loginname" disabled tabindex="2" type="text"></el-input>
</el-form-item>
<el-form-item prop="username" label="姓名">
<el-input maxlength="20" placeholder="姓名" v-model="formData.username" auto-complete="off" tabindex="3" type="text"></el-input>
</el-form-item>
<el-form-item prop="mobile" label="手机号码">
<el-input maxlength="20" placeholder="手机号码" v-model="formData.mobile" auto-complete="off" tabindex="6" type="text"></el-input>
</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>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doEdit">确 定</el-button>
</span>
</el-dialog>
<el-dialog :title="roleDialogTitle" :visible.sync="roleDialogVisible" width="70%">
<el-table :data="roleTableData" style="width: 100%" size="small" :highlight-current-row="true" row-key="id" lazy :load="loadRoleChild">
<el-table-column
label="菜单名称"
header-align="center"
prop="name"
width="200"
:show-overflow-tooltip="true"
align="left"
></el-table-column>
<el-table-column label="URL" header-align="center" prop="href" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="资源类型" header-align="center" prop="type" :show-overflow-tooltip="true" align="center">
<template slot-scope="scope">
<i v-if="scope.row.type=='menu'" style="font-weight: bold">菜单</i>
<i v-if="scope.row.type=='data'">数据</i>
</template>
</el-table-column>
<el-table-column label="权限标识" header-align="center" prop="permission" :show-overflow-tooltip="true"></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="roleDialogVisible = false">关 闭</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const self = this
const validateLoginname = function (rule, value, callback) {
const regex = new RegExp("^[a-zA-Z0-9][a-zA-Z0-9]{1,19}$")
if (!regex.test(value)) {
callback(new Error("工号以字母开头2-20字,允许使用字母/数字"))
} else {
callback()
}
}
const validatePass = function (rule, value, callback) {
if (value !== self.formData.password) {
callback(new Error("两次输入密码不一致!"))
} else {
callback()
}
}
const validateUnit = function (rule, value, callback) {
if (
self.parentUnit.length < 1 ||
self.parentUnit[self.parentUnit.length - 1] === "" ||
self.parentUnit[self.parentUnit.length - 1] === "root"
) {
callback(new Error("请选择所属单位!"))
} else {
callback()
}
}
return {
addDialogVisible: false,
editDialogVisible: false,
roleDialogVisible: false,
treeData: {
label: "name",
children: "zones",
id: "id",
isLeaf: "leaf"
},
tableData: [],
tableKey: "",
options: [],
parentUnit: [],
parentUnitSearch: [],
pageForm: {
searchUnit: "",
searchName: "",
searchKeyword: "",
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
formData: {
id: "",
unitId: "",
disabled: false
},
formRules: {
parentUnit: [{ validator: validateUnit, trigger: ["blur", "change"] }],
loginname: [
{ required: true, message: "工号", trigger: ["blur", "change"] },
{ validator: validateLoginname, trigger: ["blur", "change"] }
],
username: [{ required: true, message: "姓名", trigger: ["blur", "change"] }],
email: [
{ required: false, message: "邮箱地址", trigger: ["blur", "change"] },
{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }
],
mobile: [
{ required: false, message: "手机号码", trigger: ["blur", "change"] },
{ pattern: /^1[3456789]\d{9}$/, message: "请输入正确的手机号码" }
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{}[\]|;:'",.<>/?]).{8,20}$/,
message: "密码必须同时包含大小写字母、数字、特殊符号,且长度在8到20位之间",
trigger: "change"
},
{ min: 8, max: 20, message: "密码长度必须是8-20位", trigger: "change" }
],
passwordAgain: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()\-_=+{}[\]|;:'",.<>/?]).{8,20}$/,
message: "密码必须同时包含大小写字母、数字、特殊符号,且长度在8到20位之间",
trigger: "change"
},
{ validator: validatePass, trigger: "change" }
]
},
editRules: {
username: [{ required: true, message: "姓名", trigger: ["blur", "change"] }],
email: [
{ required: false, message: "邮箱地址", trigger: ["blur", "change"] },
{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }
],
mobile: [
{ required: false, message: "手机号码", trigger: ["blur", "change"] },
{ pattern: /^1[3456789]\d{9}$/, message: "请输入正确的手机号码" }
]
},
roleDialogTitle: "查看用户权限",
roleTableData: [], //treeTable后台取出的数据
userId: "", //当前用户ID
multipleSelection: [],
props: {
lazy: true,
checkStrictly: true,
multiple: false,
value: "id",
label: "name",
lazyLoad: function (node, resolve) {
const url = base + "/platform/sys/unit/tree"
self.$axios.post(url, { pid: node.value }).then((res) => {
if (res.code === 0) {
let filteredData = res.data;
if (node.level === 0 && res.data && res.data.length > 0) {
filteredData = res.data.slice(1); // 移除第一个元素
}
resolve(filteredData);
}
})
}
},
units: []
}
},
methods: {
formatAt: function (val) {
if (val > 0) return moment(val).format("YYYY-MM-DD HH:mm")
return ""
},
doSearch: function () {
// let unit = this.parentUnitSearch[this.parentUnitSearch.length - 1]
// if ("root" === unit) {
// unit = ""
// }
// this.pageForm.searchUnit = unit
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd() {
this.addDialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
if (this.$refs["addForm"]) this.$refs["addForm"].resetFields()
},
doAdd() {
this.$refs["addForm"].validate((valid) => {
if (valid) {
this.formData.unitId = this.parentUnit[this.parentUnit.length - 1] || ""
this.$axios.post("/platform/sys/user/addDo", this.formData).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.parentUnitSearch = []
this.doSearch()
this.addDialogVisible = false
}
})
}
})
},
doEdit() {
this.$refs["editForm"].validate((valid) => {
if (valid) {
this.$axios.post("/platform/sys/user/editDo", this.formData).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
this.editDialogVisible = false
}
})
}
})
},
initRoleTreeTable() {
this.$axios.post("/platform/sys/user/menu/" + self.userId, { pid: "" }).then((res) => {
if (res.code === 0) {
self.roleTableData = res.data
}
})
},
loadRoleChild(tree, treeNode, resolve) {
this.$axios.post("/platform/sys/user/menu/" + this.userId, { pid: tree.id }).then((res) => {
if (res.code === 0) {
resolve(res.data)
}
})
},
dropdownCommand(command) {
//监听下拉框事件
if ("role" === command.type) {
this.roleDialogVisible = true
this.roleDialogTitle = "查看用户 " + command.loginname + " 的权限"
this.userId = command.id
this.initRoleTreeTable()
}
if ("reset" === command.type) {
this.$confirm("确定重置用户" + command.loginname + "的密码", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$axios.post("/platform/sys/user/resetPwd/" + command.id, {}).then((res) => {
if (res.code === 0) {
this.$alert("新密码为 " + res.data, "新密码", {
confirmButtonText: "确定"
})
}
})
})
.catch(() => {})
}
if ("enable" === command.type || "disable" === command.type) {
this.$axios.post("/platform/sys/user/" + command.type + "/" + command.id, {}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
if ("disable" === command.type) {
command.row.disabled = true
}
if ("enable" === command.type) {
command.row.disabled = false
}
}
})
}
if ("edit" === command.type) {
this.$axios.post(base + "/platform/sys/user/edit/" + command.id, {}).then((res) => {
if (res.code === 0) {
this.formData = res.data //加载后台表单数据
this.editDialogVisible = true //打开编辑窗口
}
})
}
if ("delete" === command.type) {
this.$confirm("此操作将删除 " + command.loginname, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/user/delete/" + command.id, {}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
}
},
batchCommand(command) {
if ("delete" === command.type) {
if (this.multipleSelection.length === 0) {
this.$message.warning("请选择要删除的用户")
return
}
const loginnames = []
const ids = []
this.multipleSelection.forEach(function (val) {
loginnames.push(val.loginname)
ids.push(val.id)
})
this.$confirm("此操作将删除用户 " + loginnames.toString(), "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/sys/user/delete", { ids: ids.toString() }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.doSearch()
}
})
})
}
if ("export" === command.type) {
this.$downLoad.post("/platform/sys/user/export", this.pageForm)
}
},
handleSelectionChange(val) {
this.multipleSelection = val
}
},
async created() {
this.pageData()
this.units = await this.$businessTool.listUnit()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,116 @@
<div id="passApp">
<div class="el-dialog__header">
<button type="button" data-dismiss="modal" aria-hidden="true" aria-label="Close" class="el-dialog__headerbtn">
<i class="el-dialog__close el-icon el-icon-close"></i>
</button>
<h4 class="modal-title">${msg['index.user.password']}</h4>
</div>
<div class="modal-body">
<el-form :model="passForm" ref="passForm" :rules="passRules" label-width="100px">
<el-form-item label="${msg['index.user.oldpass']}" prop="oldPassword">
<el-input
size="small"
maxlength="20"
placeholder="${msg['index.user.oldpass']}"
v-model="passForm.oldPassword"
auto-complete="off"
tabindex="1"
type="password"
></el-input>
</el-form-item>
<el-form-item label="输入新密码" prop="newPassword">
<el-input
size="small"
maxlength="20"
placeholder="${msg['index.user.newpass']}"
v-model="passForm.newPassword"
auto-complete="off"
tabindex="2"
type="password"
></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="passwordAgain">
<el-input
size="small"
maxlength="20"
placeholder="${msg['index.user.rightpass']}"
v-model="passForm.passwordAgain"
auto-complete="off"
tabindex="3"
type="password"
></el-input>
</el-form-item>
</el-form>
</div>
<div class="el-dialog__footer">
<span class="dialog-footer">
<button type="button" class="el-button el-button--default" data-dismiss="modal">
<span>${msg['system.cancel']}</span>
</button>
<button type="button" class="el-button el-button--primary" @click="doPass">
<span>${msg['system.confirm']}</span>
</button>
</span>
</div>
</div>
<script nonce="${cspNonce!}" type="text/javascript">
new Vue({
el: "#passApp",
data: function () {
var self = this
var validatePass = function (rule, value, callback) {
if (value !== self.passForm.newPassword) {
callback(new Error("两次输入密码不一致!"))
} else {
callback()
}
}
return {
passForm: {
oldPassword: "",
newPassword: "",
passwordAgain: ""
},
passRules: {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 3, max: 20, message: "密码长度必须是3-20位", trigger: "change" }
],
passwordAgain: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: validatePass, trigger: "change" }
]
}
}
},
methods: {
doPass: function () {
var self = this
self.$refs["passForm"].validate(function (valid) {
if (valid) {
$.post(
base + "/platform/sys/user/doChangePassword",
self.passForm,
function (data) {
if (data.code == 0) {
$("#homeDetail").modal("hide")
self.$message({
message: data.msg,
type: "success"
})
} else {
self.$message({
message: data.msg,
type: "error"
})
}
},
"json"
)
}
})
}
}
})
</script>
@@ -0,0 +1,224 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="名称" prop="name">
<el-input v-model="pageForm.name" clearable placeholder="请输入名称"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<el-table :data="tableData" header-align="center" border style="width: 100%">
<el-table-column label="序号" type="index" width="50px" :index="indexMethod"></el-table-column>
<el-table-column label="模板名称" prop="templateName" width="260px">
<template slot-scope="scope">
<el-input
v-model="scope.row.templateName"
size="mini"
clearable
placeholder="请输入模板名称"
@change="updateTemplateName(scope.row)">
</el-input>
</template>
</el-table-column>
<el-table-column label="排序" prop="sortNo" width="120px" align="center">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.sortNo"
size="mini"
:min="0"
:controls="false"
style="width: 80px"
@change="updateSortNo(scope.row)">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="PC端链接" prop="url" show-overflow-tooltip></el-table-column>
<el-table-column label="模板图标" prop="templateIcon" width="120px" align="center">
<template slot-scope="scope">
<file-upload
class="template-icon-upload"
:upload_number="1"
:value="scope.row.templateIcon"
accept=".jpg,.jpeg,.png"
upload_result_type="url"
upload_result_category="interval"
upload_mode="image"
@update:value="onTemplateIconChange(scope.row, $event)">
</file-upload>
</template>
</el-table-column>
<el-table-column label="模板文件" prop="templateFile" width="170px" align="center">
<template slot-scope="scope">
<file-upload
class="template-file-upload"
:upload_number="1"
:value="scope.row.templateFile"
accept=".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx,.jpg,.jpeg,.png"
upload_result_type="url"
upload_result_category="interval"
upload_mode="file"
@update:value="onTemplateFileChange(scope.row, $event)">
</file-upload>
</template>
</el-table-column>
<el-table-column label="状态" prop="enable" width="80px">
<template slot-scope="scope">
<i v-if="!scope.row.enable" class="fa fa-circle text-danger ml5"></i>
<i v-else class="fa fa-circle text-success ml5"></i>
</template>
</el-table-column>
<el-table-column label="操作" width="180px">
<template slot-scope="scope">
<el-link type="danger" size="mini" @click="disable(scope.row.id)" v-if="scope.row.enable">关闭</el-link>
<el-link type="primary" size="mini" @click="enable(scope.row.id)" v-if="!scope.row.enable">开启</el-link>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {}
},
methods: {
disable(id) {
this.$confirm("您确定要关闭吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/disable", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
}
})
})
},
enable(id) {
this.$confirm("您确定要开启吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
this.$axios.post(loc() + "/enable", {id}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
}
})
})
},
updateTemplateName(row) {
this.$axios.post(loc() + "/updateTemplateName", {
id: row.id,
templateName: row.templateName
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
} else {
this.$message.error(resp.msg)
}
})
},
updateSortNo(row) {
this.$axios.post(loc() + "/updateSortNo", {
id: row.id,
sortNo: row.sortNo
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.pageData()
} else {
this.$message.error(resp.msg)
}
})
},
onTemplateIconChange(row, templateIcon) {
if (templateIcon === undefined && !row.templateIcon) {
return
}
row.templateIcon = templateIcon
this.updateTemplateIcon(row)
},
updateTemplateIcon(row) {
this.$nextTick(() => {
this.$axios.post(loc() + "/updateTemplateIcon", {
id: row.id,
templateIcon: row.templateIcon
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
} else {
this.$message.error(resp.msg)
}
})
})
},
onTemplateFileChange(row, templateFile) {
if (templateFile === undefined && !row.templateFile) {
return
}
row.templateFile = templateFile
this.updateTemplateFile(row)
},
updateTemplateFile(row) {
this.$nextTick(() => {
this.$axios.post(loc() + "/updateTemplateFile", {
id: row.id,
templateFile: row.templateFile
}).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
} else {
this.$message.error(resp.msg)
}
})
})
}
},
created() {
this.pageData()
}
})
</script>
<style>
.template-icon-upload,
.template-file-upload {
--upload-width: 48px;
--upload-height: 48px;
}
.template-icon-upload .el-upload-list--picture-card .el-upload-list__item,
.template-icon-upload .el-upload--picture-card,
.template-file-upload .el-upload-list--picture-card .el-upload-list__item,
.template-file-upload .el-upload--picture-card {
width: 48px !important;
height: 48px !important;
line-height: 48px !important;
margin: 0;
}
.template-icon-upload .el-upload--picture-card i,
.template-file-upload .el-upload--picture-card i {
font-size: 18px;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,9 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">学习活动管理模块待完善</el-card>
</div>
<!--#
}
#-->
@@ -0,0 +1,884 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.learning-outline-layout {
height: calc(100vh - 235px);
}
.learning-outline-tree {
height: 100%;
overflow: auto;
}
.outline-context-menu {
position: fixed;
z-index: 3000;
padding: 5px 0;
margin: 0;
list-style: none;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
}
.outline-context-menu li {
min-width: 120px;
padding: 7px 18px;
cursor: pointer;
}
.outline-context-menu li:hover {
background: #f2f6fc;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never">
<search :is-search-button="false">
<search-item label="课程名称">
<el-select
v-model="currentCourseId"
clearable
filterable
remote
:remote-method="courseNameRemoteSearch"
placeholder="请选择或搜索课程名称"
style="width: 100%"
@change="courseChange"
@clear="courseClear">
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="课程类型">
<el-select v-model="courseQuery.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="授课讲师">
<el-input v-model="courseQuery.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="loadCourses"></el-input>
</search-item>
<search-item label="课程状态">
<el-select v-model="courseQuery.status" clearable placeholder="请选择课程状态" style="width: 100%">
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</search-item>
<div class="search-query">
<el-button size="medium" type="primary" icon="el-icon-search" @click="loadCourses">搜索</el-button>
<el-button size="medium" @click="resetCourseQuery">重置</el-button>
</div>
</search>
</el-card>
<el-row :gutter="10" class="mt10 learning-outline-layout">
<el-col :span="6" style="height: 100%">
<el-card shadow="never" style="height: 100%">
<table-tool label="课程大纲">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openNodeForm('chapter')">新增章</el-button>
</table-tool>
<div class="learning-outline-tree">
<el-tree
ref="outlineTree"
:data="outlineTreeData"
node-key="id"
default-expand-all
highlight-current
:expand-on-click-node="false"
:props="{children:'children', label:'title'}"
@node-click="nodeClick"
@node-contextmenu="openContextMenu">
<span slot-scope="{ node, data }">
<i :class="data.nodeType === 'course' ? 'el-icon-collection' : (data.nodeType === 'chapter' ? 'el-icon-folder' : 'el-icon-document')"></i>
<span>{{data.title}}</span>
<el-tag v-if="data.status === 'disabled'" size="mini" type="info" style="margin-left: 6px">禁用</el-tag>
<el-tag v-if="data.required" size="mini" type="warning" style="margin-left: 6px">必学</el-tag>
</span>
</el-tree>
<el-empty v-if="treeData.length === 0" description="暂无课程大纲"></el-empty>
</div>
</el-card>
</el-col>
<el-col :span="18" style="height: 100%">
<el-card shadow="never" style="height: 100%; overflow: auto">
<el-empty v-if="!selectedNode.id" description="请选择左侧章/节节点"></el-empty>
<el-tabs v-else v-model="activeTab">
<el-tab-pane label="基础信息" name="basic">
<el-form :model="nodeForm" ref="nodeForm" label-width="110px" :rules="nodeRules">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="节点类型">
<el-tag :type="nodeForm.nodeType === 'chapter' ? 'primary' : 'success'">{{nodeForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序值" prop="sortOrder">
<el-input-number v-model="nodeForm.sortOrder" :controls="false" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="标题" prop="title">
<el-input v-model="nodeForm.title" maxlength="100" placeholder="请输入章/节标题"></el-input>
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="nodeForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input v-model="nodeForm.description" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入章/节简介"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNodeBasic">保存基础信息</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="学习资料" name="resource">
<table-tool label="学习资料">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openResourceForm()">新增资料</el-button>
</table-tool>
<el-table :data="resourceData" border @sort-change="resourcePageOrder">
<el-table-column label="序号" type="index" width="70" :index="resourceIndex"></el-table-column>
<el-table-column label="资料标题" prop="resourceTitle" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column label="资料类型" prop="resourceType" sortable="custom" width="110">
<template slot-scope="{row}">{{getResourceTypeName(row.resourceType)}}</template>
</el-table-column>
<el-table-column label="文件格式" prop="fileExt" sortable="custom" width="110"></el-table-column>
<el-table-column label="学习时长(秒)" prop="durationSeconds" sortable="custom" width="140">
<template slot-scope="{row}">{{formatDurationSeconds(row.durationSeconds)}}</template>
</el-table-column>
<el-table-column label="排序" prop="sortOrder" sortable="custom" width="90"></el-table-column>
<el-table-column label="必学" prop="required" sortable="custom" width="90">
<template slot-scope="{row}">
<el-tag :type="row.required ? 'warning' : 'info'" size="mini">{{row.required ? '是' : '否'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="预览" prop="allowPreview" sortable="custom" width="90">
<template slot-scope="{row}">{{row.allowPreview ? '允许' : '不允许'}}</template>
</el-table-column>
<el-table-column label="下载" prop="allowDownload" sortable="custom" width="90">
<template slot-scope="{row}">{{row.allowDownload ? '允许' : '不允许'}}</template>
</el-table-column>
<el-table-column label="状态" prop="status" sortable="custom" width="90">
<template slot-scope="{row}">
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'" size="mini">{{row.status === 'enabled' ? '启用' : '禁用'}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="{row}">
<el-button size="mini" type="primary" @click="openResourceForm(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="deleteResource(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="mt20"
@size-change="resourceSizeChange"
@current-change="resourceNumberChange"
:current-page="resourcePageForm.pageNumber"
:page-sizes="[5,10,20,30,50]"
:page-size="resourcePageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="resourcePageForm.totalCount">
</el-pagination>
</el-tab-pane>
<el-tab-pane label="完成规则" name="rule">
<el-form :model="ruleForm" label-width="150px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否必学">
<el-switch v-model="ruleForm.required"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学习方式">
<el-select v-model="ruleForm.studyMode" style="width: 100%">
<el-option v-for="item in studyModeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="完成规则">
<el-select v-model="ruleForm.completionRule" style="width: 100%">
<el-option v-for="item in completionRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="完成比例">
<el-input-number v-model="ruleForm.completePercent" :min="0" :max="100" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最少学习时长(分钟)">
<el-input-number v-model="ruleForm.minStudyMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="解锁规则">
<el-select v-model="ruleForm.unlockRule" style="width: 100%">
<el-option v-for="item in unlockRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="允许跳过"><el-switch v-model="ruleForm.allowSkip"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许拖动"><el-switch v-model="ruleForm.allowDrag"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="暂停计时"><el-switch v-model="ruleForm.pauseCountTime"></el-switch></el-form-item></el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="隐藏计时"><el-switch v-model="ruleForm.hiddenCountTime"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="无操作计时"><el-switch v-model="ruleForm.inactiveCountTime"></el-switch></el-form-item></el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="saveRule">保存完成规则</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="状态设置" name="status">
<el-form :model="nodeForm" label-width="110px">
<el-form-item label="节点状态">
<el-radio-group v-model="nodeForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否必学">
<el-switch v-model="nodeForm.required"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNodeBasic">保存状态</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<ul v-show="contextVisible" class="outline-context-menu" :style="{left: contextLeft + 'px', top: contextTop + 'px'}">
<li @click="openNodeForm('chapter')">新增章</li>
<li v-if="contextNode.nodeType === 'chapter'" @click="openNodeForm('section', contextNode)">新增节</li>
<li v-if="contextNode.nodeType !== 'course'" @click="openNodeForm(contextNode.nodeType, contextNode)">编辑</li>
<li v-if="contextNode.nodeType !== 'course'" @click="deleteNode(contextNode)">删除</li>
<li v-if="contextNode.nodeType !== 'course'" @click="toggleNode(contextNode)">{{contextNode.status === 'enabled' ? '禁用' : '启用'}}</li>
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'up')">上移</li>
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'down')">下移</li>
<li v-if="contextNode.nodeType !== 'course'" @click="openResourceForm(null, contextNode)">上传资料</li>
<li v-if="contextNode.nodeType !== 'course'" @click="setRequired(contextNode)">设置必学</li>
</ul>
<el-dialog :title="nodeDialogTitle" :visible.sync="nodeDialogVisible" :close-on-click-modal="false" width="45%">
<el-form :model="nodeDialogForm" ref="nodeDialogForm" label-width="100px" :rules="nodeRules">
<el-form-item label="节点类型">
<el-tag>{{nodeDialogForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
</el-form-item>
<el-form-item label="上级节点">
<el-input v-model="nodeDialogForm.parentName" disabled placeholder="上级节点"></el-input>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="nodeDialogForm.title" maxlength="100" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="nodeDialogForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
</el-form-item>
<el-form-item label="简介">
<el-input v-model="nodeDialogForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit></el-input>
</el-form-item>
<el-form-item label="排序值">
<el-input-number v-model="nodeDialogForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="是否必学">
<el-switch v-model="nodeDialogForm.required"></el-switch>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="nodeDialogForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="nodeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitNodeDialog">确定</el-button>
</span>
</el-dialog>
<el-dialog :title="resourceForm.id ? '编辑学习资料' : '新增学习资料'" :visible.sync="resourceDialogVisible" :close-on-click-modal="false" width="55%">
<el-form :model="resourceForm" ref="resourceForm" label-width="120px" :rules="resourceRules">
<el-form-item label="所属章节">
<el-input v-model="resourceForm.outlineName" disabled placeholder="所属章节"></el-input>
</el-form-item>
<el-form-item label="资料标题" prop="resourceTitle">
<el-input v-model="resourceForm.resourceTitle" maxlength="100" placeholder="请输入资料标题"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="资料类型" prop="resourceType">
<el-select v-model="resourceForm.resourceType" placeholder="请选择资料类型" style="width: 100%" @change="resourceTypeChange">
<el-option v-for="item in resourceTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序值" prop="sortOrder">
<el-input-number v-model="resourceForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学习时长(秒)">
<el-input-number v-model="resourceForm.durationSeconds" :min="0" :precision="0" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="resourceForm.status">
<el-radio label="enabled">启用</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="是否必学"><el-switch v-model="resourceForm.required"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许预览"><el-switch v-model="resourceForm.allowPreview"></el-switch></el-form-item></el-col>
<el-col :span="8"><el-form-item label="允许下载"><el-switch v-model="resourceForm.allowDownload"></el-switch></el-form-item></el-col>
</el-row>
<el-form-item label="附件" prop="fileData">
<file-upload
:value.sync="resourceForm.fileData"
:upload_number="1"
upload_mode="drag"
upload_result_category="array"
complete_result
:accept="fileAccept">
</file-upload>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resourceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitResource">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
courseQuery: {},
courseOptions: [],
courseTypeOptions: [],
currentCourseId: "",
treeData: [],
selectedNode: {},
activeTab: "basic",
nodeForm: {},
nodeDialogVisible: false,
nodeDialogTitle: "",
nodeDialogForm: {},
nodeRules: {
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
},
contextVisible: false,
contextLeft: 0,
contextTop: 0,
contextNode: {},
resourceData: [],
resourcePageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
pageOrderName: "",
pageOrderBy: ""
},
resourceDialogVisible: false,
resourceForm: {},
resourceRules: {
resourceTitle: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
resourceType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
fileData: [{ required: true, message: "请上传附件", trigger: ["blur", "change"] }]
},
fileAccept: "",
ruleForm: {},
statusOptions: [
{ name: "草稿", code: "draft" },
{ name: "待发布", code: "pending" },
{ name: "已发布", code: "published" },
{ name: "已下架", code: "offline" }
],
resourceTypeOptions: [
{ name: "视频", code: "video", accept: ".mp4,.mov" },
{ name: "音频", code: "audio", accept: ".mp3,.wav,.aac" },
{ name: "PDF", code: "pdf", accept: ".pdf" },
{ name: "PPT课件", code: "ppt", accept: ".ppt,.pptx" },
{ name: "Word文档", code: "word", accept: ".doc,.docx" },
{ name: "图片", code: "image", accept: ".jpg,.jpeg,.png" },
{ name: "其他", code: "other", accept: "" }
],
studyModeOptions: [
{ name: "视频", code: "video" },
{ name: "音频", code: "audio" },
{ name: "文档", code: "document" },
{ name: "图片", code: "image" },
{ name: "混合资料", code: "mixed" }
],
completionRuleOptions: [
{ name: "完成所有必学资料", code: "all_required_resource" },
{ name: "按资料学习进度", code: "resource_progress" },
{ name: "达到最少学习时长", code: "min_study_time" },
{ name: "管理员手动确认", code: "manual" }
],
unlockRuleOptions: [
{ name: "自由学习", code: "free" },
{ name: "顺序解锁", code: "sequential" }
]
}
},
computed: {
currentCourse() {
return this.courseOptions.find(v => v.id === this.currentCourseId)
},
outlineTreeData() {
if (!this.currentCourseId || !this.currentCourse) {
return []
}
return [{
id: "course-" + this.currentCourseId,
courseId: this.currentCourseId,
title: this.currentCourse.courseName,
nodeType: "course",
children: this.treeData
}]
}
},
methods: {
resetCourseQuery() {
this.courseQuery = {}
this.currentCourseId = ""
this.loadCourses()
},
getQueryParam(name) {
return new URLSearchParams(window.location.search).get(name) || ""
},
async loadCourseTypes() {
const resp = await this.$axios.post(loc() + "/courseTypes")
if (resp.code === 0) {
this.courseTypeOptions = resp.data
}
},
async loadCourses(keepCurrent = false) {
const resp = await this.$axios.post(loc() + "/courses", this.courseQuery)
if (resp.code === 0) {
this.courseOptions = resp.data
if (!keepCurrent && (!this.currentCourseId || !this.courseOptions.some(v => v.id === this.currentCourseId))) {
this.currentCourseId = this.courseOptions.length ? this.courseOptions[0].id : ""
}
await this.loadTree()
}
},
courseNameRemoteSearch(query) {
this.courseQuery.courseName = query
this.loadCourses(true)
},
courseChange() {
this.selectedNode = {}
this.loadTree()
},
courseClear() {
this.currentCourseId = ""
this.selectedNode = {}
this.treeData = []
},
async loadTree() {
if (!this.currentCourseId) {
this.treeData = []
this.selectedNode = {}
return
}
const resp = await this.$axios.post(loc() + "/tree", { courseId: this.currentCourseId })
if (resp.code === 0) {
this.treeData = resp.data
let nodeToSelect = null
if (this.selectedNode.id) {
nodeToSelect = this.findNode(this.treeData, this.selectedNode.id)
}
if (!nodeToSelect) {
nodeToSelect = this.findFirstOutlineNode(this.treeData)
}
if (nodeToSelect) {
this.nodeClick(nodeToSelect)
this.$nextTick(() => {
if (this.$refs.outlineTree) {
this.$refs.outlineTree.setCurrentKey(nodeToSelect.id)
}
})
} else {
this.selectedNode = {}
this.nodeForm = {}
this.resourceData = []
}
}
},
findFirstOutlineNode(nodes) {
if (!nodes || !nodes.length) return null
for (const node of nodes) {
if (node.nodeType !== "course") return node
const child = this.findFirstOutlineNode(node.children || [])
if (child) return child
}
return null
},
findNode(nodes, id) {
for (const node of nodes) {
if (node.id === id) return node
const child = this.findNode(node.children || [], id)
if (child) return child
}
return null
},
getParentName(type, node, course, isEdit) {
if (type === "chapter") {
return course ? course.courseName : ""
}
if (!isEdit) {
return node ? node.title : ""
}
const parent = node && node.parentId ? this.findNode(this.treeData, node.parentId) : null
return parent ? parent.title : (course ? course.courseName : "")
},
nodeClick(data) {
if (data.nodeType === "course") {
this.selectedNode = {}
this.nodeForm = {}
this.resourceData = []
return
}
this.selectedNode = data
this.nodeForm = Object.assign({}, data)
this.activeTab = "basic"
this.loadResources()
this.loadRule()
},
openContextMenu(event, data) {
event.preventDefault()
this.contextNode = data
this.contextVisible = true
this.contextLeft = event.clientX
this.contextTop = event.clientY
},
openNodeForm(type, node = null) {
if (!this.currentCourseId) {
this.$message.warning("请先选择课程")
return
}
const course = this.courseOptions.find(v => v.id === this.currentCourseId)
if (node && node.id && node.nodeType === type) {
this.nodeDialogTitle = "编辑" + (type === "chapter" ? "章" : "节")
this.nodeDialogForm = Object.assign({}, node, {
parentName: this.getParentName(type, node, course, true)
})
} else {
this.nodeDialogTitle = "新增" + (type === "chapter" ? "章" : "节")
this.nodeDialogForm = {
courseId: this.currentCourseId,
courseName: course ? course.courseName : "",
parentId: type === "section" && node ? node.id : "",
parentName: this.getParentName(type, node, course, false),
nodeType: type,
title: "",
subtitle: "",
description: "",
sortOrder: 1,
required: false,
status: "enabled"
}
}
this.nodeDialogVisible = true
this.$nextTick(() => this.$refs.nodeDialogForm && this.$refs.nodeDialogForm.clearValidate())
},
submitNodeDialog() {
this.$refs.nodeDialogForm.validate(async valid => {
if (!valid) return
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeDialogForm)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.nodeDialogVisible = false
await this.loadTree()
} else {
this.$message.warning(resp.msg)
}
})
},
saveNodeBasic() {
this.$refs.nodeForm.validate(async valid => {
if (!valid) return
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeForm)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.selectedNode = Object.assign({}, this.nodeForm)
await this.loadTree()
await this.loadRule()
} else {
this.$message.warning(resp.msg)
}
})
},
deleteNode(node) {
this.$confirm("确定要删除该节点吗?若下级有内容将不允许删除。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/deleteNode", { id: node.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.selectedNode = {}
await this.loadTree()
} else {
this.$message.warning(resp.msg)
}
})
},
async toggleNode(node) {
const status = node.status === "enabled" ? "disabled" : "enabled"
const resp = await this.$axios.post(loc() + "/toggleNode", { id: node.id, status })
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
}
},
async moveNode(node, direction) {
const resp = await this.$axios.post(loc() + "/moveNode", { id: node.id, direction })
if (resp.code === 0) {
await this.loadTree()
}
},
async setRequired(node) {
const next = !node.required
const data = Object.assign({}, node, { required: next })
const resp = await this.$axios.post(loc() + "/saveNode", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
if (this.selectedNode.id === node.id) {
this.nodeClick(Object.assign({}, node, { required: next }))
}
}
},
async loadResources() {
if (!this.selectedNode.id) return
const resp = await this.$axios.post(loc() + "/resourcePage", Object.assign({}, this.resourcePageForm, { outlineId: this.selectedNode.id }))
if (resp.code === 0) {
this.resourceData = resp.data.list
this.resourcePageForm.totalCount = resp.data.totalCount
}
},
resourcePageOrder(column) {
this.resourcePageForm.pageOrderName = column.prop
this.resourcePageForm.pageOrderBy = column.order
this.loadResources()
},
resourceSizeChange(val) {
this.resourcePageForm.pageSize = val
this.loadResources()
},
resourceNumberChange(val) {
this.resourcePageForm.pageNumber = val
this.loadResources()
},
resourceIndex(index) {
return index + (this.resourcePageForm.pageNumber - 1) * this.resourcePageForm.pageSize + 1
},
openResourceForm(row = null, node = null) {
const target = node || this.selectedNode
if (!target.id) {
this.$message.warning("请先选择章/节节点")
return
}
if (row) {
this.resourceForm = Object.assign({}, row, {
outlineName: target.title,
fileData: this.parseFiles(row.fileData)
})
} else {
this.resourceForm = {
courseId: target.courseId,
outlineId: target.id,
outlineName: target.title,
resourceTitle: "",
resourceType: "video",
fileExt: "",
fileData: [],
durationSeconds: 0,
sortOrder: 1,
required: false,
allowPreview: true,
allowDownload: false,
status: "enabled"
}
}
this.resourceTypeChange(this.resourceForm.resourceType)
this.resourceDialogVisible = true
this.$nextTick(() => this.$refs.resourceForm && this.$refs.resourceForm.clearValidate())
},
resourceTypeChange(val) {
const type = this.resourceTypeOptions.find(v => v.code === val)
this.fileAccept = type ? type.accept : ""
this.autoFillMediaDuration()
},
submitResource() {
this.$refs.resourceForm.validate(async valid => {
if (!valid) return
const data = Object.assign({}, this.resourceForm)
data.fileExt = this.getFileExt(data.fileData)
if (!this.validResourceExt(data.resourceType, data.fileExt)) {
this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件")
return
}
delete data.outlineName
data.fileData = JSON.stringify(data.fileData || [])
const resp = await this.$axios.post(loc() + "/saveResource", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
this.resourceDialogVisible = false
this.loadResources()
} else {
this.$message.warning(resp.msg)
}
})
},
deleteResource(row) {
this.$confirm("确定要删除该学习资料吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const resp = await this.$axios.post(loc() + "/deleteResource", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
this.loadResources()
}
})
},
async loadRule() {
if (!this.selectedNode.id) return
const resp = await this.$axios.post(loc() + "/getRule", {
courseId: this.selectedNode.courseId,
targetType: "outline",
targetId: this.selectedNode.id
})
if (resp.code === 0) {
this.ruleForm = Object.assign({}, resp.data, {
minStudyMinutes: this.formatDurationMinutes(resp.data.minStudySeconds)
})
}
},
async saveRule() {
const data = Object.assign({}, this.ruleForm, {
minStudySeconds: Math.round((this.ruleForm.minStudyMinutes || 0) * 60)
})
delete data.minStudyMinutes
const resp = await this.$axios.post(loc() + "/saveRule", data)
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.loadTree()
await this.loadRule()
} else {
this.$message.warning(resp.msg)
}
},
parseFiles(value) {
if (!value) return []
if (Array.isArray(value)) return value
try {
return JSON.parse(value)
} catch (e) {
return []
}
},
getFileUrl(files) {
const file = Array.isArray(files) && files.length ? files[0] : null
if (!file) return ""
return file.url || file.response?.data || file.data || ""
},
getFileExt(files) {
const file = Array.isArray(files) && files.length ? files[0] : null
if (!file) return ""
const name = file.name || file.url || file.response?.data || ""
const index = name.lastIndexOf(".")
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
},
formatDurationMinutes(durationSeconds) {
const seconds = Number(durationSeconds || 0)
return seconds > 0 ? Math.ceil(seconds / 60) : 0
},
formatDurationSeconds(durationSeconds) {
const seconds = Math.ceil(Number(durationSeconds || 0))
return seconds > 0 ? seconds + "秒" : "0秒"
},
autoFillMediaDuration() {
if (!["video", "audio"].includes(this.resourceForm.resourceType)) return
const url = this.getFileUrl(this.resourceForm.fileData)
if (!url) return
const media = document.createElement(this.resourceForm.resourceType === "video" ? "video" : "audio")
media.preload = "metadata"
media.onloadedmetadata = () => {
window.URL.revokeObjectURL(media.src)
if (isFinite(media.duration) && media.duration > 0) {
// 媒体时长按秒保存,避免先取整到分钟再换算导致实际时长被放大。
this.$set(this.resourceForm, "durationSeconds", Math.ceil(media.duration))
}
}
media.onerror = () => {
window.URL.revokeObjectURL(media.src)
}
media.src = url
},
validResourceExt(resourceType, fileExt) {
const item = this.resourceTypeOptions.find(v => v.code === resourceType)
if (!item || !item.accept) return true
return item.accept.split(",").map(v => v.replace(".", "").toLowerCase()).includes(fileExt)
},
getResourceTypeName(code) {
const item = this.resourceTypeOptions.find(v => v.code === code)
return item ? item.name : code
}
},
watch: {
"resourceForm.fileData": {
handler() {
this.autoFillMediaDuration()
},
deep: true
}
},
mounted() {
document.addEventListener("click", () => this.contextVisible = false)
},
async created() {
await this.loadCourseTypes()
const courseId = this.getQueryParam("courseId")
if (courseId) {
this.currentCourseId = courseId
this.courseQuery.courseId = courseId
await this.loadCourses(true)
this.$delete(this.courseQuery, "courseId")
} else {
await this.loadCourses()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,12 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<div slot="header">课程介绍</div>
<el-empty description="课程介绍页面后续设计"></el-empty>
</el-card>
</div>
<!--#
}
#-->
@@ -0,0 +1,350 @@
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.course-display {
padding: 8px 10px 22px;
background: #f4f5f7;
min-height: calc(100vh - 88px);
}
.course-filter {
background: #fff;
border: 1px solid #e5e8ef;
margin-bottom: 28px;
}
.filter-row {
display: flex;
min-height: 70px;
border-bottom: 1px solid #e8ebf0;
}
.filter-row:last-child {
border-bottom: 0;
}
.filter-label {
width: 132px;
padding: 20px 16px;
background: #eef5ff;
color: #1f2d3d;
font-weight: 600;
text-align: center;
}
.filter-options {
flex: 1;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px 18px;
padding: 16px 18px;
}
.filter-chip {
min-width: 76px;
height: 32px;
padding: 0 18px;
border: 0;
border-radius: 9px;
background: transparent;
color: #526070;
cursor: pointer;
line-height: 32px;
text-align: center;
}
.filter-chip.active {
background: #1e63e9;
color: #fff;
font-weight: 600;
}
.course-result-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.course-count {
color: #1f2d3d;
}
.course-count span {
color: #2468f2;
padding: 0 4px;
}
.course-search {
width: 310px;
}
.course-list {
background: #fff;
border-top: 1px solid #dfe4ec;
}
.course-item {
display: flex;
gap: 20px;
padding: 18px 0;
border-bottom: 1px solid #dfe4ec;
}
.course-cover {
position: relative;
width: 270px;
height: 150px;
flex: 0 0 270px;
background: #e9eef5;
border-radius: 4px;
overflow: hidden;
}
.course-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.course-cover-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #8a97a8;
background: linear-gradient(135deg, #eaf0f8, #d8e3f2);
font-weight: 600;
}
.course-cover-tag {
position: absolute;
right: 0;
bottom: 12px;
min-width: 70px;
height: 24px;
padding: 0 10px;
background: #f5a400;
color: #fff;
font-size: 12px;
line-height: 24px;
text-align: center;
border-radius: 12px 0 0 12px;
}
.course-info {
flex: 1;
min-width: 0;
padding-right: 12px;
}
.course-title {
display: inline-block;
margin: 4px 0 8px;
color: #111827;
font-size: 18px;
font-weight: 700;
cursor: pointer;
}
.course-title:hover {
color: #006fc9;
}
.course-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
color: #8b96a6;
font-size: 13px;
margin-bottom: 12px;
}
.course-intro {
color: #4f5f73;
line-height: 1.8;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.course-time {
color: #8b96a6;
font-size: 13px;
}
.recommend-tags {
display: inline-flex;
gap: 6px;
margin-left: 8px;
vertical-align: 2px;
}
.course-empty {
background: #fff;
padding: 80px 0;
text-align: center;
color: #8b96a6;
border-top: 1px solid #dfe4ec;
}
</style>
<div id="app" class="course-display" v-cloak>
<div class="course-filter">
<div class="filter-row">
<div class="filter-label">推荐标识</div>
<div class="filter-options">
<button class="filter-chip" :class="{active: !query.recommendFlag}" @click="selectRecommend('')">全部</button>
<button
v-for="item in recommendOptions"
:key="item.code"
class="filter-chip"
:class="{active: query.recommendFlag === item.code}"
@click="selectRecommend(item.code)">
{{item.name}}
</button>
</div>
</div>
<div class="filter-row">
<div class="filter-label">课程类型</div>
<div class="filter-options">
<button class="filter-chip" :class="{active: !query.courseTypeId}" @click="selectCourseType('')">全部</button>
<button
v-for="item in courseTypeOptions"
:key="item.id"
class="filter-chip"
:class="{active: query.courseTypeId === item.id}"
@click="selectCourseType(item.id)">
{{item.typeName}}
</button>
</div>
</div>
</div>
<div class="course-result-bar">
<div class="course-count">为您找到相关课程<span>{{pageForm.totalCount || 0}}</span>门</div>
<el-input
class="course-search"
v-model="query.keyword"
clearable
placeholder="搜索关键字"
prefix-icon="el-icon-search"
@keyup.enter.native="doSearch"
@clear="doSearch">
<el-button slot="append" icon="el-icon-search" @click="doSearch"></el-button>
</el-input>
</div>
<div v-if="pageForm.list && pageForm.list.length" class="course-list">
<div v-for="course in pageForm.list" :key="course.id" class="course-item">
<div class="course-cover">
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
<div v-else class="course-cover-empty">{{course.courseTypeName || '课程'}}</div>
<div v-if="firstRecommendName(course.recommendFlags)" class="course-cover-tag">{{firstRecommendName(course.recommendFlags)}}</div>
</div>
<div class="course-info">
<div>
<span class="course-title" @click="openDetail(course)">{{course.courseName}}</span>
<span class="recommend-tags">
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="mini" type="warning">{{getRecommendName(code)}}</el-tag>
</span>
</div>
<div class="course-meta">
<span><i class="el-icon-office-building"></i> {{course.courseTypeName || '未设置课程类型'}}</span>
<span><i class="el-icon-user"></i> {{course.lecturerName || '未设置讲师'}}</span>
</div>
<div class="course-intro">{{course.courseIntro || '暂无课程介绍'}}</div>
<div class="course-time">
<i class="el-icon-time"></i>
<span v-if="course.openType === 'long_term'">长期开放</span>
<span v-else>{{course.startTimeText || '-'}} 至 {{course.endTimeText || '-'}}</span>
</div>
</div>
</div>
</div>
<div v-else class="course-empty">暂无相关课程</div>
<el-pagination
class="mt20"
background
layout="total, sizes, prev, pager, next"
:current-page.sync="pageForm.pageNumber"
:page-size.sync="pageForm.pageSize"
:total="pageForm.totalCount"
@size-change="pageData"
@current-change="pageData">
</el-pagination>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
query: {
keyword: "",
courseTypeId: "",
recommendFlag: ""
},
courseTypeOptions: [],
recommendOptions: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
list: []
}
}
},
methods: {
async pageData() {
const resp = await this.$axios.post(loc() + "/pageData", Object.assign({}, this.query, {
pageNumber: this.pageForm.pageNumber,
pageSize: this.pageForm.pageSize
}))
if (resp.code === 0) {
this.pageForm = resp.data
}
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
selectRecommend(code) {
this.query.recommendFlag = code
this.doSearch()
},
selectCourseType(id) {
this.query.courseTypeId = id
this.doSearch()
},
async loadOptions() {
const typeResp = await this.$axios.post(loc() + "/courseTypes")
if (typeResp.code === 0) {
this.courseTypeOptions = typeResp.data
}
const recommendResp = await this.$axios.post(loc() + "/recommendOptions")
if (recommendResp.code === 0) {
this.recommendOptions = recommendResp.data
}
},
getCoverUrl(cover) {
if (!cover) return ""
if (Array.isArray(cover)) {
return cover.length ? (cover[0].url || cover[0].response?.data || cover[0].data || "") : ""
}
if (typeof cover === "string" && cover.trim().startsWith("[")) {
try {
const files = JSON.parse(cover)
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
} catch (e) {
return ""
}
}
return cover
},
splitFlags(flags) {
return flags ? flags.split(",").filter(Boolean) : []
},
getRecommendName(code) {
const item = this.recommendOptions.find(v => v.code === code)
return item ? item.name : code
},
firstRecommendName(flags) {
const codes = this.splitFlags(flags)
return codes.length ? this.getRecommendName(codes[0]) : ""
},
openDetail(course) {
window.location.href = loc() + "/study?id=" + course.id
}
},
async created() {
await this.loadOptions()
await this.pageData()
}
})
</script>
<!--#
}
#-->

Some files were not shown because too many files have changed in this diff Show More