Files
yunzerwebsiteallinone/uniapp/components/sub/SubMine.vue
T
2026-10-03 23:46:55 +08:00

486 lines
10 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.
<template>
<view class="page">
<view class="head">
<view class="head-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#ffffff" :size="18" />
<text class="head-back">返回导航</text>
</view>
<view class="user">
<view class="avatar">
<text class="avatar-text">{{ avatarText }}</text>
</view>
<view class="user-meta">
<text class="user-name">{{ user.name || '云泽用户' }}</text>
<text class="user-sub">{{ user.tenant || '云泽平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
</view>
</view>
</view>
<view class="body">
<view v-if="loadError" class="err-box" @tap="load(true)">
<FaIcon name="triangle-exclamation" color="#f04438" :size="18" />
<view class="err-body">
<text class="err-title">加载失败</text>
<text class="err-msg">{{ loadError }},点击重试</text>
</view>
</view>
<!-- 当前子系统 -->
<view class="block">
<view class="block-head"><text class="block-title">当前子系统</text></view>
<view class="card-row">
<view class="tint-icon row-icon" :class="sub.tint">
<FaIcon :name="sub.icon" color="#ffffff" :size="22" />
</view>
<view class="row-meta">
<text class="row-title">{{ sub.title }}</text>
<text class="row-sub">{{ allMenus.length }} 个功能 · {{ enabled ? '已开通' : '未开通' }}</text>
</view>
<text v-if="!enabled" class="lock-tag">未开通</text>
</view>
</view>
<!-- 我的数据 -->
<view class="block">
<view class="block-head"><text class="block-title">我的数据</text></view>
<view class="kpi">
<view class="kpi-item">
<text class="kpi-value" :class="{ warn: overdue > 0 }">{{ pendingCount }}</text>
<text class="kpi-label">待办</text>
</view>
<view class="kpi-item">
<text class="kpi-value" :class="{ warn: overdue > 0 }">{{ overdue }}</text>
<text class="kpi-label">已逾期</text>
</view>
<view class="kpi-item">
<text class="kpi-value">{{ doneCount }}</text>
<text class="kpi-label">已完成</text>
</view>
<view class="kpi-item">
<text class="kpi-value">{{ allMenus.length }}</text>
<text class="kpi-label">可用功能</text>
</view>
</view>
</view>
<!-- 操作 -->
<view class="block">
<view class="block-head"><text class="block-title">操作</text></view>
<view class="list-card">
<!-- 仅该子系统在 Web 端确有对应系统时展示(config/subsystems.js 的 webEntry) -->
<view v-if="sub.webEntry" class="list-item" @tap="enterSystem">
<view class="item-icon"><FaIcon name="display" color="#3973ff" :size="16" /></view>
<text class="item-text">进入完整系统(Web 端)</text>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
<view class="list-item" @tap="goFunctions">
<view class="item-icon"><FaIcon name="table-cells" color="#3973ff" :size="16" /></view>
<text class="item-text">全部功能</text>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
<view class="list-item" @tap="goProfile">
<view class="item-icon"><FaIcon name="user" color="#3973ff" :size="16" /></view>
<text class="item-text">账号与安全</text>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
<view class="list-item" @tap="goBackNav">
<view class="item-icon"><FaIcon name="table-cells" color="#3973ff" :size="16" /></view>
<text class="item-text">切换子系统</text>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
</view>
</view>
<view class="logout" @tap="handleLogout">
<text class="logout-text">退出登录</text>
</view>
<view class="bottom-space" />
</view>
<AppTabbar :group="slug" current="mine" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn, logout, isSSOEnabled, getToken, clearPendingAuth } from '@/utils/auth.js'
import { openWebPage } from '@/utils/nav.js'
import { logoutUAC, logoutApi } from '@/api/auth.js'
import { request } from '@/api/request.js'
import { clearHomeCache } from '@/api/workspace.js'
import { useSubsystem } from '@/utils/subsystem.js'
import AppTabbar from '@/components/AppTabbar.vue'
const props = defineProps({
slug: { type: String, required: true }
})
const { sub, loading, loadError, user, allMenus, enabled, moduleMeta, load } = useSubsystem(props.slug)
const pendingCount = ref(0)
const overdue = ref(0)
const doneCount = ref(0)
const entering = ref(false)
const avatarText = computed(() => {
const name = user.value.name || user.value.account || '云'
return String(name).charAt(0).toUpperCase()
})
async function loadStats() {
try {
const [p, d] = await Promise.all([
request({ url: '/app/schedule/list', data: { status: 'pending', pageSize: 50 } }),
request({ url: '/app/schedule/list', data: { status: 'done', pageSize: 50 } })
])
const pending = (p && p.list) || []
pendingCount.value = pending.length
overdue.value = pending.filter((s) => isOverdue(s)).length
doneCount.value = ((d && d.list) || []).length
} catch (e) {
pendingCount.value = 0
overdue.value = 0
doneCount.value = 0
}
}
function isOverdue(item) {
const t = String(item.schedule_time || item.created_at || '')
return t.length >= 10 && t.slice(0, 10) < todayStr()
}
function todayStr() {
const d = new Date()
const p = (n) => String(n).padStart(2, '0')
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
}
function goBackNav() {
uni.reLaunch({ url: '/pages/nav/nav' })
}
function goFunctions() {
uni.redirectTo({ url: `/pages/${props.slug}/functions/functions` })
}
function goProfile() {
uni.redirectTo({ url: '/pages/profile/profile' })
}
/** 打开 Web 端该模块首页(走票据免登录) */
async function enterSystem() {
if (entering.value) return
const target =
(moduleMeta.value && moduleMeta.value.path) ||
(allMenus.value[0] && allMenus.value[0].path) ||
''
if (!target) {
uni.showToast({ title: '该系统暂未开放', icon: 'none' })
return
}
entering.value = true
try {
await openWebPage(target, sub.name)
} finally {
entering.value = false
}
}
function handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
confirmText: '退出',
cancelText: '取消',
success(res) {
if (!res.confirm) return
// 统一认证:先吊销服务端会话与令牌
if (isSSOEnabled() && getToken()) {
logoutUAC().catch(() => {})
} else {
logoutApi().catch(() => {})
}
clearPendingAuth()
clearHomeCache()
logout()
uni.reLaunch({ url: '/pages/login/login' })
}
})
}
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
load()
loadStats()
})
onShow(() => {
if (isLoggedIn() && !loading.value) {
load()
loadStats()
}
})
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
@include page-shell;
}
/* ============ 顶部用户区 ============ */
.head {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 40rpx;
background: $hero-gradient;
}
.head-row {
display: flex;
align-items: center;
gap: 4rpx;
opacity: 0.85;
}
.head-back {
font-size: 24rpx;
color: #ffffff;
}
.user {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 26rpx;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.22);
border: 2rpx solid rgba(255, 255, 255, 0.36);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-text {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
}
.user-meta {
flex: 1;
min-width: 0;
}
.user-name {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.user-sub {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
color: rgba(255, 255, 255, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============ 内容区 ============ */
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 24rpx $page-padding-x 0;
}
.block {
margin-bottom: 32rpx;
}
.block-head {
@include block-head;
}
.block-title {
@include section-title;
}
.err-box {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
padding: 24rpx 22rpx;
background: #fef3f2;
border: 1rpx solid #fdd8d4;
border-radius: $radius-md;
}
.err-body {
flex: 1;
min-width: 0;
}
.err-title {
display: block;
font-size: 27rpx;
font-weight: 500;
color: $color-danger;
}
.err-msg {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #b42318;
word-break: break-all;
}
/* 当前子系统行 */
.card-row {
@include card;
display: flex;
align-items: center;
gap: 18rpx;
padding: 26rpx 24rpx;
}
.row-icon {
@include tint-icon(80rpx, 20rpx);
}
.row-meta {
flex: 1;
min-width: 0;
}
.row-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: $color-text;
}
.row-sub {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.lock-tag {
@include chip(#fef3f2, $color-danger);
}
/* 数据概览 */
.kpi {
@include card;
display: flex;
padding: 28rpx 0;
}
.kpi-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
position: relative;
& + &::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 1rpx;
background: $color-divider;
}
}
.kpi-value {
font-size: 36rpx;
font-weight: 600;
color: $color-text;
&.warn {
color: $color-danger;
}
}
.kpi-label {
font-size: 21rpx;
color: $color-text-muted;
}
/* 操作列表 */
.list-card {
@include card;
overflow: hidden;
}
.list-item {
display: flex;
align-items: center;
gap: 18rpx;
padding: 26rpx 24rpx;
& + & {
border-top: 1rpx solid $color-divider;
}
&:active {
background: $color-bg-page;
}
}
.item-icon {
@include icon-box(56rpx);
}
.item-text {
flex: 1;
min-width: 0;
font-size: 28rpx;
color: $color-text;
}
/* 退出 */
.logout {
@include card;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
background: linear-gradient(135deg, rgba(240, 68, 56, 0.08) 0%, rgba(240, 68, 56, 0.04) 100%);
border: 1rpx solid rgba(240, 68, 56, 0.2);
}
.logout-text {
font-size: 30rpx;
font-weight: 600;
color: $color-danger;
}
.bottom-space {
height: 48rpx;
}
</style>