Files
shanxia2.0/miniprogram/pages/workbench/workbench.vue
T
2026-10-02 09:28:00 +08:00

282 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onUnmounted, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
import { WORKBENCH_MODULES } from '@/config/workbench-menus'
import { PAGE_PATHS, WORKBENCH_ROUTE_MAP } from '@/config/api'
import { filterWorkbenchModules } from '@/utils/workbenchMenuFilter'
import { getSafeAreaStyle } from '@/utils/safeArea'
import { useShareToLogin } from '@/utils/shareToLogin'
useShareToLogin()
// #ifdef MP-WEIXIN
import {
checkMpUpdate,
formatMpUpdateLine,
isMpUpdateUrgent,
subscribeMpUpdate
} from '@/utils/mpUpdate'
// #endif
const safeAreaStyle = getSafeAreaStyle()
const userStore = useUserStore()
const modules = computed(() =>
filterWorkbenchModules(WORKBENCH_MODULES, {
menuAll: userStore.menuAll,
menuCodes: userStore.menuCodes
})
)
const loggingOut = ref(false)
const mpUpdateLine = ref('')
const mpUpdateUrgent = ref(false)
const mpUpdateStatus = ref('idle')
// #ifdef MP-WEIXIN
const unsubMpUpdate = subscribeMpUpdate((s) => {
mpUpdateLine.value = formatMpUpdateLine(s)
mpUpdateUrgent.value = isMpUpdateUrgent(s)
mpUpdateStatus.value = s.status || 'idle'
})
onUnmounted(() => {
unsubMpUpdate()
})
// #endif
const displayName = computed(() => {
const info = userStore.userInfo
if (!info) return '未登录'
return info.realName || info.username || '用户'
})
/** 三列宫格补齐空白格,避免末行空位露出网格底色 */
const menuPadCount = (len) => {
const rem = Number(len) % 3
return rem === 0 ? 0 : 3 - rem
}
onShow(async () => {
userStore.checkLoginStatus()
if (!userStore.isLoggedIn) {
uni.reLaunch({ url: PAGE_PATHS.login })
return
}
await userStore.fetchMenuCodes()
// #ifdef MP-WEIXIN
checkMpUpdate()
// #endif
})
const handleMpUpdateTap = () => {
// #ifdef MP-WEIXIN
checkMpUpdate({ force: true, fromUser: true })
// #endif
}
/** 含 web-view 的页用 redirectTo,避免真机 navigateTo:fail timeout 闪进又弹回工作台 */
const isMpWebviewRoute = (url) => String(url || '').includes('/pages/common/mp-webview')
const openPage = (url) => {
const failToast = () => uni.showToast({ title: '打开失败,请重试', icon: 'none' })
if (isMpWebviewRoute(url)) {
const ret = uni.redirectTo({
url,
fail: (err) => {
// 栈异常时再退到 reLaunch
const msg = String((err && err.errMsg) || '')
if (/fail/i.test(msg)) {
uni.reLaunch({ url, fail: failToast })
return
}
failToast()
}
})
if (ret && typeof ret.catch === 'function') ret.catch(() => {})
return
}
const fail = (err) => {
const msg = String((err && err.errMsg) || '')
if (/timeout/i.test(msg)) {
uni.redirectTo({ url, fail: failToast })
return
}
failToast()
}
const ret = uni.navigateTo({ url, fail })
if (ret && typeof ret.catch === 'function') {
ret.catch(() => {})
}
}
const openMenu = (module, menu) => {
if (!menu.enabled) {
uni.showToast({ title: '本期暂未开放', icon: 'none' })
return
}
const route = WORKBENCH_ROUTE_MAP[menu.code]
if (route) {
openPage(route)
return
}
const title = encodeURIComponent(menu.name || '')
const moduleTitle = encodeURIComponent(module.title || '')
const code = encodeURIComponent(menu.code || '')
openPage(`${PAGE_PATHS.bizPlaceholder}?title=${title}&module=${moduleTitle}&code=${code}`)
}
const handleLogout = async () => {
if (loggingOut.value) return
loggingOut.value = true
try {
await userStore.logout()
} finally {
loggingOut.value = false
}
}
</script>
<template>
<view class="mp-page mp-page--tab">
<view class="mp-header mp-header--workbench" :style="safeAreaStyle">
<view class="mp-header__body">
<view class="mp-header__titles">
<text class="mp-header__title">山峡化工工作台</text>
<!-- #ifdef MP-WEIXIN -->
<view
v-if="mpUpdateLine"
class="mp-header-version"
:class="{
'mp-header-version--urgent': mpUpdateUrgent,
'mp-header-version--ready': mpUpdateStatus === 'ready',
'mp-header-version--failed': mpUpdateStatus === 'failed'
}"
hover-class="mp-header-version--active"
@click.stop="handleMpUpdateTap"
>
<text class="mp-header-version__text">{{ mpUpdateLine }}</text>
<text v-if="mpUpdateStatus === 'ready'" class="mp-header-version__cta">立即更新</text>
</view>
<!-- #endif -->
</view>
<view class="mp-header__user-stack">
<text class="mp-header__user-line">{{ displayName }},欢迎您</text>
<text
class="mp-header__logout"
:class="{ 'mp-header__logout--disabled': loggingOut }"
@click="handleLogout"
>
{{ loggingOut ? '退出中…' : '退出登录' }}
</text>
</view>
</view>
</view>
<view class="mp-section">
<view
v-for="mod in modules"
:key="mod.key"
class="mp-module-block"
:class="'mp-module-block--' + mod.key"
>
<view class="mp-module-block__head">
<text class="mp-module-block__icon">{{ mod.iconText }}</text>
<view class="mp-module-block__title-wrap">
<text class="mp-module-block__title">{{ mod.title }}</text>
<text class="mp-module-block__desc">{{ mod.desc }}</text>
</view>
</view>
<view class="mp-menu-grid">
<view
v-for="(menu, idx) in mod.menus"
:key="menu.code || menu.name + idx"
class="mp-menu-card"
:class="{ 'mp-menu-card--disabled': !menu.enabled }"
@click="openMenu(mod, menu)"
>
<view class="mp-menu-card__icon">{{ menu.icon || '' }}</view>
<text class="mp-menu-card__name">{{ menu.name }}</text>
</view>
<view
v-for="i in menuPadCount(mod.menus.length)"
:key="'pad-' + mod.key + '-' + i"
class="mp-menu-card mp-menu-card--pad"
/>
</view>
</view>
</view>
<app-env-badge />
</view>
</template>
<style scoped lang="scss">
/* 标题下方版本行:常态半透明白字;有更新时大麦黄底醒目 */
.mp-header-version {
margin-top: 12rpx;
max-width: 100%;
min-height: 48rpx;
padding: 6rpx 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 10rpx;
box-sizing: border-box;
}
.mp-header-version--active {
opacity: 0.88;
}
.mp-header-version__text {
font-size: 22rpx;
line-height: 1.4;
color: rgba(255, 255, 255, 0.88);
text-align: left;
}
.mp-header-version--urgent {
margin-top: 14rpx;
padding: 10rpx 16rpx;
border-radius: 12rpx;
background: #fff7e8;
border: 1rpx solid rgba(214, 163, 67, 0.55);
min-height: 64rpx;
}
.mp-header-version--urgent .mp-header-version__text {
color: #8a5a10;
font-size: 24rpx;
font-weight: 600;
}
.mp-header-version--ready {
background: #fff3cd;
border-color: #d6a343;
animation: mp-version-pulse 1.6s ease-in-out infinite;
}
.mp-header-version--ready .mp-header-version__text {
color: #7a4e08;
}
.mp-header-version__cta {
flex-shrink: 0;
font-size: 22rpx;
font-weight: 700;
line-height: 1.2;
color: #fff;
background: #d6a343;
padding: 6rpx 14rpx;
border-radius: 999rpx;
}
.mp-header-version--failed {
background: #fef2f2;
border-color: #fecaca;
}
.mp-header-version--failed .mp-header-version__text {
color: #b91c1c;
font-weight: 600;
}
@keyframes mp-version-pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(214, 163, 67, 0.35);
}
50% {
box-shadow: 0 0 0 8rpx rgba(214, 163, 67, 0);
}
}
</style>