This commit is contained in:
2026-06-12 15:30:50 +08:00
parent 742a0ff388
commit b23d3bfa79
58 changed files with 10475 additions and 474 deletions
@@ -71,7 +71,7 @@
<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>
l
<!-- 引入 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>
@@ -80,11 +80,12 @@
<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.min.js"></script>-->
<!--<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>
@@ -100,13 +101,32 @@
<script src="https://map.qq.com/api/gljs?v=2.exp&key=MLLBZ-GQECI-ASXG7-5GNOZ-XW2OF-H5BVH"></script>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
<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 type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
Vue.config.devtools = false
ELEMENT.locale(ELEMENT.lang["${lang,escape}"])
ELEMENT.Dialog.props.closeOnClickModal.default = false
@@ -123,12 +143,12 @@
</script>
<!--广播频道-->
<script type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
window.GlobalBroadcastChannel = new BroadcastChannel("zhgh-global-channel")
</script>
<!--ws-->
<script type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
class WebSocketPubSub {
constructor() {
this.ws = null
@@ -150,6 +170,7 @@
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)
}
@@ -162,13 +183,12 @@
this.subscribers.get(type).add(callback)
setTimeout(() => {
console.log(this.ws.readyState)
// 所有订阅完成后,发送 join 消息
if (!this.isSubscribed && this.ws.readyState === WebSocket.OPEN) {
this.isSubscribed = true
this.sendJoinMessage()
}
}, 2000)
}, 1000)
}
// 事件处理
@@ -202,6 +222,7 @@
// )
// 连接建立时不立即发送 join,等待订阅完成
this.ping()
console.info('websocket open success')
}
}
@@ -243,9 +264,9 @@
if (this.ws.readyState === WebSocket.OPEN) {
this.sendJoinMessage()
}
}, 1000)
}, 500)
}
}, 3000) // 延迟3秒重连
}, 1000) // 延迟2秒重连
}
// 窗口事件处理
@@ -277,7 +298,7 @@
</script>
<!--vuex-->
<script type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
window.store = new Vuex.Store({
plugins: [
createPersistedState({
@@ -303,11 +324,11 @@
//设置用户信息
setUser(state, payload) {
state.user = payload
state.room = payload.loginname + ":${@auth.getSessionId()}"
}
},
actions: {}
})
$.get("/platform/sys/user/getLogonUser").then((res) => {
if (res.code === 0) {
window.sessionStorage.setItem("user", JSON.stringify(res.data))
@@ -317,13 +338,13 @@
</script>
<!--beetl全局变量方法-->
<script type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
const base = "${base!}"
const APP_DOMAIN = "${AppDomain!}"
</script>
<!--vue挂载-->
<script type="text/javascript">
<script nonce="${cspNonce!}" type="text/javascript">
Vue.prototype.$moment = moment
Vue.prototype.$businessTool = businessTool
Vue.prototype.$commonUtil = commonUtil
@@ -331,10 +352,20 @@
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>
<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()))
@@ -511,6 +542,33 @@
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;
@@ -576,6 +634,10 @@
.v4-nav {
display: none;
}
.voice-menu-text {
display: none;
}
}
/* 页脚样式 */
@@ -647,6 +709,10 @@
</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> -->
@@ -657,7 +723,7 @@
</a>
</div>
</header>
<div style="height: 45px"></div>
<div style="height: 64px"></div>
<main class="v4-content">
<!-- 页面内容区域 -->
@@ -679,12 +745,12 @@
</footer>
<script>
<script nonce="${cspNonce!}">
// 处理导航项的active状态
$(document).ready(function () {
// 控制页脚显示的函数
function toggleFooter() {
var currentPath = window.location.pathname
var currentPath = getBaseSubAppPath()
var footer = $("#v4-footer")
if (currentPath === "/platform/v4/home") {
@@ -696,7 +762,7 @@
// 初始化:根据当前URL设置active状态
function setActiveNavItem() {
var currentPath = window.location.pathname
var currentPath = getBaseSubAppPath()
$(".v4-nav-item").removeClass("active")
$(".v4-nav-item").each(function () {
if ($(this).attr("href") === currentPath) {
@@ -1139,7 +1139,7 @@ layout("/layouts/v4/baseLayout.html"){
.oa-grassroots-wrapper {
width: 100%;
background-image: url('https://cdgh.ncu.edu.cn/images/index_bg2.png');
background-image: url(/assets/platform/img/v4/home-bg.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
@@ -1513,7 +1513,7 @@ layout("/layouts/v4/baseLayout.html"){
<div class="oa-hero-section">
<!-- 背景图 -->
<div class="oa-background-image">
<img src="${config.AppHomeImg!}" alt=""/>
<img :src="homeBannerList[0] || ''" alt=""/>
</div>
<!-- 搜索和统计区域 -->
@@ -1999,7 +1999,7 @@ layout("/layouts/v4/baseLayout.html"){
</div>
</div>
<script>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: "#v4-home-app",
data() {
@@ -2020,6 +2020,7 @@ layout("/layouts/v4/baseLayout.html"){
recommendServices: [],
favoriteItems: [],
recommendApps: [],
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item),
// 工会网站新闻
websiteNews: {},
+70 -63
View File
@@ -1,54 +1,54 @@
const act = {
template: /*language=HTML*/ `
<!-- 活动 -->
<div class="section-wrapper">
<div class="activity-section-wrapper">
<div class="section-act">
<!-- Swiper容器 -->
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
<div class="activity-swiper-container">
<!-- 活动数据 - Swiper -->
<div 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">
<div v-if="actList && actList.length > 0" class="activity-swiper-container">
<div class="activity-swiper-container">
<!-- 活动数据 - Swiper -->
<div 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 class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
<!-- 进度条 -->
<div class="swiper-pagination"></div>
</div>
</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 class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
<!-- 进度条 -->
<div class="swiper-pagination"></div>
</div>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon">📅</div>
<p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon">📅</div>
<p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p>
</div>
</div>
</div>
`,
@@ -77,8 +77,8 @@ const act = {
/*初始化Swiper*/
initSwiper() {
this.swiper = new Swiper('.activity-swiper', {
slidesPerView: 'auto',
spaceBetween: 30,
slidesPerView: 1,
spaceBetween: 18,
centeredSlides: false,
loop: false,
navigation: {
@@ -91,13 +91,13 @@ const act = {
},
breakpoints: {
768: {
slidesPerView: 1,
},
1024: {
slidesPerView: 2,
},
1024: {
slidesPerView: 4,
},
1200: {
slidesPerView: 3,
slidesPerView: 5,
}
}
})
@@ -119,6 +119,11 @@ const act = {
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)
}
},
@@ -126,7 +131,7 @@ const act = {
this.listAct()
},
style: /*language=CSS*/ `
.section-wrapper {
.activity-section-wrapper {
width: 100%;
}
@@ -171,7 +176,7 @@ const act = {
.activity-swiper-container {
position: relative;
width: 100%;
padding: 20px;
padding: 9px 14px;
/*margin-top: 30px;*/
}
@@ -212,7 +217,7 @@ const act = {
/* 进度条样式 */
.activity-swiper-container .swiper-pagination {
position: relative;
margin-top: 30px;
margin-top: 12px;
height: 4px;
}
@@ -234,8 +239,8 @@ const act = {
.activity-swiper .swiper-slide .item .img-box {
width: 100%;
height: 200px;
border-radius: 12px;
height: 128px;
border-radius: 8px;
overflow: hidden;
position: relative;
}
@@ -245,10 +250,12 @@ const act = {
left: 0;
top: 0;
background: #c11623;
padding: 6px 2px;
min-width: 50px;
padding: 4px 2px;
min-width: 42px;
color: #ffffff;
border-radius: 0 0 50% 0;
font-size: 12px;
line-height: 1.2;
}
@@ -264,16 +271,16 @@ const act = {
}
.activity-swiper .swiper-slide .item h4 {
font-size: 20px;
margin-top: 10px;
height: 60px;
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.5;
line-height: 1.4;
text-align: left;
transition: color 0.3s ease;
}
@@ -287,14 +294,14 @@ const act = {
}
.activity-swiper .swiper-slide .item .time p {
font-size: 13px;
font-size: 12px;
margin: 0;
/*line-height: 1.5;*/
line-height: 1.4;
color: #666;
}
.activity-swiper .swiper-slide .item .time i {
margin-right: 5px;
margin-right: 4px;
color: var(--color-primary);
}
+106 -106
View File
@@ -1,39 +1,38 @@
const entry = {
template: /*language=HTML*/ `
<div class="entry-wrapper">
<!-- 标签页导航 -->
<div class="tabs">
<div class="tab-item" :class="{ active: activeCategory === 'serv' }"
@click="setActiveCategory('serv')">
<i class="fa fa-star"></i> 推荐服务
</div>
<div class="tab-item" :class="{ active: activeCategory === 'app' }"
@click="setActiveCategory('app')">
<i class="fa fa-fire"></i> 推荐应用
</div>
<div class="tab-item" :class="{ active: activeCategory === 'fav' }"
@click="setActiveCategory('fav')">
<i class="fa fa-heart"></i> 我的收藏
</div>
</div>
<!-- 应用块区域 -->
<div class="apps-container">
<div class="app-grid">
<div class="app-item" v-for="(item, index) in currentServices" :key="index"
@click="openService(item)">
<div class="app-icon">
<img :src="item.picIcon" alt="" />
</div>
<div class="app-name">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<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 :src="item.picIcon" alt="" />
</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 {
activeCategory: 'serv',
hasMore: true,
entries: {
serv: [],
@@ -43,8 +42,12 @@ const entry = {
}
},
computed: {
currentServices() {
return this.entries[this.activeCategory] || [];
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() {
@@ -53,10 +56,6 @@ const entry = {
this.getFavorite()
},
methods: {
setActiveCategory(category) {
this.activeCategory = category;
},
// 查询推荐服务
async getRecommendService() {
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
@@ -84,35 +83,37 @@ const entry = {
})
},
// 点击服务
openService(service) {
if(this.activeCategory === 'serv'){
if(!service.href) this.$message.error('无效的链接地址')
// 点击服务或应用
openService(service, category) {
if (category === 'serv') {
if (!service.href) {
this.$message.error('无效的链接地址')
return
}
window.open(service.href)
return
}
if(this.activeCategory === 'app'){
// 储存到缓存
if (category === 'app') {
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
// 新标签页打开
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
return
}
if(this.activeCategory === 'fav'){
if(!service.href) this.$message.error('无效的链接地址')
window.open(service.href)
return
if(!service.parentId){
// 储存到缓存
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
// 新标签页打开
window.open("/platform/v4/subApp?appId=" + app.id, "_blank")
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*/ `
@@ -121,37 +122,41 @@ const entry = {
box-sizing: border-box;
}
.tabs {
margin-top: 5px;
display: flex;
gap: 20px;
border-bottom: 2px solid #e8e8e8;
padding-bottom: 10px;
.entry-section + .entry-section {
margin-top: 22px;
}
.tab-item {
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
.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;
color: #666;
cursor: pointer;
padding-bottom: 8px;
transition: color 0.2s ease;
}
.tab-item:hover {
color: #1890ff;
}
.tab-item.active {
color: #1890ff;
font-weight: bold;
border-bottom: 2px solid #1890ff;
}
.tab-item i {
font-size: 14px;
font-style: normal;
font-weight: 500;
}
.apps-container {
@@ -162,19 +167,17 @@ const entry = {
.app-grid {
display: grid;
/* 改为自适应列数:每格最小 80px,自动换行 */
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
justify-items: center; /* 子项内容居中(图标和文字) */
justify-content: start; /* 整体网格靠左对齐,避免居中 */
gap: 4px;
padding: 12px 4px;
grid-template-columns: repeat(auto-fill, minmax(96px, 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;
max-height: calc(350px - 74px);
overflow-y: auto;
}
.app-item {
@@ -187,6 +190,7 @@ const entry = {
width: 100%;
max-width: 100%;
padding: 6px;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
@@ -208,6 +212,7 @@ const entry = {
}
.app-name {
min-height: 34px;
font-size: 12px;
color: #333;
line-height: 1.4;
@@ -215,31 +220,26 @@ const entry = {
text-align: center;
}
/* 响应式设计 */
.app-empty {
grid-column: 1 / -1;
width: 100%;
}
@media (max-width: 768px) {
.tabs {
flex-wrap: wrap;
gap: 10px;
}
.tab-item {
padding: 8px 0;
font-size: 12px;
}
.app-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
grid-template-columns: repeat(3, minmax(72px, 1fr));
gap: 12px;
padding: 14px 10px;
}
.app-item {
padding: 12px;
padding: 8px 4px;
}
}
@media (max-width: 480px) {
.app-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(72px, 1fr));
gap: 12px;
}
}
@@ -11,13 +11,13 @@ layout("/layouts/v4/baseLayout.html"){
.section-banner .banner-img {
width: 100%;
height: 100%;
height: auto;
}
.section-banner .banner-img img {
width: 100%;
height: 100%;
object-fit: cover;
height: auto;
display: block;
}
.section-wrapper {
@@ -28,7 +28,7 @@ layout("/layouts/v4/baseLayout.html"){
width: 80%;
margin: 20px auto;
display: grid;
grid-template-columns: 2.2fr 0.8fr;
grid-template-columns: 1fr;
gap: 24px;
align-items: stretch;
row-gap: 24px;
@@ -38,16 +38,16 @@ layout("/layouts/v4/baseLayout.html"){
display: flex;
/*align-items: center;*/
min-width: 320px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 16px;
background: transparent;
border-radius: 0;
box-shadow: none;
padding: 0;
transition: transform 0.2s, box-shadow 0.2s;
}
.entry-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
transform: none;
box-shadow: none;
}
.act-card {
@@ -55,43 +55,89 @@ layout("/layouts/v4/baseLayout.html"){
align-items: center;
min-width: 320px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
padding: 11px;
transition: transform 0.2s, box-shadow 0.2s;
}
.home-section {
width: 100%;
}
.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;
}
</style>
<div class="v4-container" id="v4-home-app">
<!-- 首页banner -->
<div class="section-banner">
<div class="banner-img">
<img src="${config.AppHomeImg!}" alt=""/>
<img :src="homeBannerList[0] || ''" alt=""/>
<stats></stats>
</div>
</div>
<div class="">
<div class="" style="padding-bottom: 20px">
<user></user>
<div class="home-grid">
<div class="act-card">
<act></act>
</div>
<div class="entry-card">
<entry></entry>
</div>
</div>
<jcdt :list="websiteNews.grassroots"></jcdt>
<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>
<script nonce="${cspNonce!}">
<!--#include("act.js"){}#-->
<!--#include("template.js"){}#-->
<!--#include("jcdt.js"){}#-->
<!--#include("entry.js"){}#-->
<!--#include("user.js"){}#-->
@@ -102,13 +148,13 @@ layout("/layouts/v4/baseLayout.html"){
el: '#v4-home-app',
data(){
return{
websiteNews:{
grassroots:[],
}
websiteNews: [],
homeBannerList: "${config.AppHomeImg!}".split(",").map(item => item.trim()).filter(item => item)
}
},
components: {
'act': act,
'work-template': workTemplate,
'jcdt': jcdt,
'entry': entry,
'user': user,
@@ -2,22 +2,29 @@ const jcdt = {
template: /*language=HTML*/ `
<div class="jcdt-wrapper">
<div class="jcdt-container">
<!-- <div class="jcdt-title">-->
<!-- <span>基层动态</span>-->
<!-- </div> -->
<div class="jcdt-content">
<div class="jcdt-list">
<div class="jcdt-item" v-for="item in list" :key="item.title" @click="onLink(item)">
<div class="date" v-if="item.date">{{item.date}}</div>
<div class="title">{{item.title}}</div>
<div class="image">
<img v-if="item.image" :src="item.image" alt="">
<img v-else src="/assets/platform/img/v4/not-image.png" alt="">
</div>
<div class="summary">{{item.summary}}</div>
<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>
</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>
`,
@@ -32,9 +39,12 @@ const jcdt = {
},
methods: {
onLink(item) {
if(!item.url) return
window.open(item.url)
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 {
@@ -47,16 +57,14 @@ const jcdt = {
.jcdt-container {
width: 80%;
max-width: 80%;
margin: 0 auto;
margin: 0 auto 10px;
background-color: #FFFFFF;
border-radius: 12px;
padding: 20px;
margin-bottom: 10px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
}
.jcdt-title{
/deep/ .jcdt-title{
font-size: 30px;
color: #000000;
display: block;
@@ -87,14 +95,14 @@ const jcdt = {
margin-left: 0;
vertical-align: middle;
}
.jcdt-list {
/deep/ .jcdt-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.jcdt-item {
/deep/ .jcdt-item {
display: flex;
flex-direction: column;
padding: 16px;
@@ -103,18 +111,17 @@ const jcdt = {
transition: all 0.3s ease;
cursor: pointer;
height: auto;
border-radius: 12px;
}
.jcdt-item:hover{
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-bottom-color: var(--color-primary);
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
background: #ffffff;
}
.jcdt-item .date {
/deep/ .jcdt-item:hover{
transform: translateY(-4px);
}
/deep/ .jcdt-item .date {
font-size: 20px;
font-weight: bold;
white-space: nowrap;
@@ -123,19 +130,19 @@ const jcdt = {
transition: opacity 0.3s ease;
}
.jcdt-item .title {
/deep/ .jcdt-item .title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
/*margin-bottom: 12px;*/
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 45px;
/*height: 45px;*/
}
.jcdt-item .image {
/deep/ .jcdt-item .image {
width: 100%;
height: 180px;
margin: 0 0 16px 0;
@@ -149,14 +156,14 @@ const jcdt = {
transition: transform 0.3s ease;
}
.jcdt-item .image img {
/deep/ .jcdt-item .image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.jcdt-item .summary {
/deep/ .jcdt-item .summary {
font-size: 14px;
color: #666;
line-height: 1.6;
@@ -168,5 +175,8 @@ const jcdt = {
margin-bottom: 12px;
}
/deep/ .jcdt-item i {
color: #007aff;
}
`
};
@@ -7,66 +7,58 @@ const stats = {
<div class="stats-grid">
<!-- 待办 -->
<div
class="stat-item pending"
@click="handleStatClick('todo')"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
class="stat-item pending"
@click="handleStatClick('todo')"
>
<div class="stat-icon">
<i class="fa fa-clock-o"></i>
</div>
<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')"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
class="stat-item completed"
@click="handleStatClick('done')"
>
<div class="stat-icon">
<i class="fa fa-check-circle"></i>
</div>
<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')"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
class="stat-item messages"
@click="handleStatClick('notification')"
>
<div class="stat-icon">
<i class="fa fa-bell"></i>
</div>
<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')"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
class="stat-item reminders"
@click="handleStatClick('started')"
>
<div class="stat-icon">
<i class="fa fa-file-text"></i>
</div>
<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>
@@ -90,15 +82,6 @@ const stats = {
}
},
onMouseEnter(event) {
event.target.style.transform = 'translateY(-2px)';
event.target.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
},
onMouseLeave(event) {
event.target.style.transform = 'none';
event.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.08)';
},
loadStats() {
this.$axios.post("/flow/todoCenter/statistics").then(res => {
if (res.code === 0) {
@@ -122,90 +105,128 @@ const stats = {
display: flex;
justify-content: space-between;
align-items: center;
margin: -130px auto 0;
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: #f8f9fa;
color: #495057;
padding: 14px;
border-radius: 8px;
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;
gap: 10px;
justify-content: space-between;
gap: 16px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
width: 200px;
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: 30px;
width: 50px;
height: 50px;
background: #e6f7ff;
color: #1890ff;
border-radius: 50%;
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: #e6f7ff;
color: #1890ff;
background: rgba(255, 255, 255, 0.42);
}
.stat-item.pending .stat-icon {
color: #2563eb;
}
.stat-item.completed .stat-icon {
color: #52c41a;
color: #16a34a;
}
.stat-item.messages .stat-icon {
color: #faad14;
color: #d97706;
}
.stat-item.reminders .stat-icon {
color: #13c2c2;
color: #0891b2;
}
.stat-content {
display: flex;
flex-direction: column;
margin-left: 10px;
margin-left: 0;
min-width: 0;
text-align: left;
}
.stat-label {
font-size: 15px;
opacity: 0.7;
font-size: 14px;
color: rgba(31, 41, 55, 0.78);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 1px;
line-height: 1.2;
}
.stat-number {
font-size: 18px;
font-weight: bold;
margin-top: 2px;
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;
}
`
};
@@ -68,10 +68,10 @@ const user = {
<div class="header">
<h3>待办中心</h3>
<el-tabs v-model="activeTab" type="card" @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-tab-pane label="办结事务" name="completed"></el-tab-pane>
<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>
@@ -97,6 +97,7 @@ const user = {
<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')}}
@@ -108,6 +109,7 @@ const user = {
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</div>
</div>
</div>
@@ -123,6 +125,12 @@ const user = {
pageSize: 5,
totalCount: 0
},
taskStats: {
todoCount: 0,
doneCount: 0,
startedCount: 0,
completedCount: 0
},
// 🔆 天气相关数据
weatherData: null,
weatherError: false,
@@ -181,9 +189,10 @@ const user = {
return info;
},
async getConfigKey(key) {
const resp = await this.$axios.post("/open/common/getConfigKey", { key })
return resp.data
getConfigKey(key) {
return this.$axios.post("/open/common/getConfigKey", { key }).then((resp) => {
return resp.data
})
},
// 🔆 获取天气
async fetchWeather() {
@@ -191,6 +200,9 @@ const user = {
// 存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) => {
@@ -224,6 +236,19 @@ const user = {
}
},
// 获取任务列表
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 {
@@ -241,6 +266,7 @@ const user = {
},
pageData() {
this.getTasks()
this.getStatistics()
},
handleTabClick(tab) {
this.pageData()
@@ -254,7 +280,7 @@ const user = {
this.$message.warning("当前流程没有配置地址")
return
}
window.open(formKey + "?taskId=" + taskId + "bizId=" + businessNo + "taskKey=" + taskKey)
window.open(formKey + "?taskId=" + taskId + "&bizId=" + businessNo + "&taskKey=" + taskKey + "&tab=" + this.activeTab)
},
getRoleNames() {
$.post("/platform/sys/role/getRoleNames").then(res => {
@@ -280,7 +306,7 @@ const user = {
.user-container {
width: 80%;
margin: 20px auto;
margin: 12px auto 20px;
display: grid;
grid-template-columns: 0.7fr 2.3fr;
gap: 24px;
+5 -5
View File
@@ -333,7 +333,7 @@ layout("/layouts/v4/baseLayout.html"){
methods: {
async loadStats() {
try {
const resp = await this.$axios.get('/platform/v4/msg/stats')
const resp = await Promise.resolve(this.$axios.get('/platform/v4/msg/stats'))
if (resp.code === 0) {
this.stats = resp.data
}
@@ -345,7 +345,7 @@ layout("/layouts/v4/baseLayout.html"){
async loadMessages() {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/pageData', this.pageForm)
const resp = await Promise.resolve(this.$axios.post('/platform/v4/msg/pageData', this.pageForm))
if (resp.code === 0) {
this.messages = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
@@ -360,7 +360,7 @@ layout("/layouts/v4/baseLayout.html"){
},
async viewMessage(message) {
this.$axios.post(`/platform/v4/msg/detail/` + message.id).then(async res => {
this.$axios.post('/platform/v4/msg/detail/' + message.id).then(async res => {
if (res.code === 0) {
this.currentMessage = res.data
this.detailVisible = true
@@ -376,7 +376,7 @@ layout("/layouts/v4/baseLayout.html"){
async markAsRead(messageId) {
try {
await this.$axios.post(`/platform/v4/msg/read/` + messageId)
await Promise.resolve(this.$axios.post('/platform/v4/msg/read/' + messageId))
} catch (error) {
console.error('标记已读失败:', error)
}
@@ -390,7 +390,7 @@ layout("/layouts/v4/baseLayout.html"){
}).then(async () => {
this.loading = true
try {
const resp = await this.$axios.post('/platform/v4/msg/read/all')
const resp = await Promise.resolve(this.$axios.post('/platform/v4/msg/read/all'))
if (resp.code === 0) {
this.$message.success('操作成功')
this.loadMessages()