Files
2026-11-11 18:42:09 +08:00

503 lines
11 KiB
Vue
Raw Permalink 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" :class="{ embedded }">
<!-- 独立页模式:渐变用户头部(嵌入 tabs.vue 时由父页提供顶部条) -->
<view v-if="!embedded" class="head" :style="headPad">
<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 v-if="embedded" class="user-card" @tap="goProfile">
<view class="avatar avatar-light">
<text class="avatar-text avatar-text-dark">{{ avatarText }}</text>
</view>
<view class="user-meta">
<text class="user-name user-name-dark">{{ user.name || '温新云用户' }}</text>
<text class="user-sub">{{ user.tenant || '温新云平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
</view>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
<!-- 数据概览 -->
<view class="kpi-card">
<view class="kpi-item">
<text class="kpi-value">{{ pendingCount }}</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" :class="{ warn: overdue > 0 }">{{ overdue }}</text>
<text class="kpi-label">已逾期</text>
</view>
<view class="kpi-item">
<text class="kpi-value">{{ leafMenus.length }}</text>
<text class="kpi-label">可用功能</text>
</view>
</view>
<!-- 常用操作:分组卡片模板,一行 4 个 -->
<view class="group-card">
<view class="group-head">
<view class="group-bar" />
<text class="group-title">常用操作</text>
</view>
<view class="grid">
<view class="cell" @tap="goFunctions">
<view class="fn-card">
<view class="tint-icon fn-icon tint-0">
<FaIcon name="table-cells" color="#ffffff" :size="18" />
</view>
<text class="fn-name">全部功能</text>
</view>
</view>
<view class="cell" @tap="goProfile">
<view class="fn-card">
<view class="tint-icon fn-icon tint-1">
<FaIcon name="user" color="#ffffff" :size="18" />
</view>
<text class="fn-name">账号与安全</text>
</view>
</view>
<!-- <view class="cell" @tap="goBackNav">
<view class="fn-card">
<view class="tint-icon fn-icon tint-2">
<FaIcon name="arrow-right-arrow-left" color="#ffffff" :size="18" />
</view>
<text class="fn-name">切换子系统</text>
</view>
</view> -->
</view>
</view>
<view class="logout" @tap="handleLogout">
<text class="logout-text">退出登录</text>
</view>
<view class="bottom-space" />
</view>
<AppTabbar v-if="!embedded" :group="slug" current="mine" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { mpHeadPad } from '@/utils/mpHead.js'
const headPad = mpHeadPad()
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn, logout, isSSOEnabled, getToken, clearPendingAuth } from '@/utils/auth.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 },
embedded: { type: Boolean, default: false }
})
const emit = defineEmits(['switch-tab', 'back'])
const { sub, loading, loadError, user, leafMenus, load } = useSubsystem(props.slug)
const pendingCount = ref(0)
const overdue = ref(0)
const doneCount = ref(0)
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: '/api/schedule/list', data: { status: 'pending', pageSize: 50 } }),
request({ url: '/api/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() {
if (props.embedded) {
emit('back')
return
}
uni.reLaunch({ url: '/pages/nav/nav' })
}
// 「功能」页签在 TAB_ORDER 中的下标为 1
function goFunctions() {
if (props.embedded) {
emit('switch-tab', 1)
return
}
uni.redirectTo({ url: `/pages/${props.slug}/functions/functions` })
}
function goProfile() {
uni.redirectTo({ url: '/pages/profile/profile' })
}
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()) 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;
// 嵌入模式下由父页(tabs.vue)的 scroll-view 统一滚动,这里改为自然高度
&.embedded {
height: auto;
}
}
/* ============ 独立页模式的渐变用户头部 ============ */
.head {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 40rpx;
/* #ifdef MP-WEIXIN */
padding-top: calc(env(safe-area-inset-top, 25px) + 84rpx);
/* #endif */
background: $hero-gradient;
}
.user {
display: flex;
align-items: center;
gap: 20rpx;
/* 取消顶部返回按钮后,补偿其占位 */
margin-top: 42rpx;
}
.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: 38rpx;
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: 22rpx;
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: 28rpx $page-padding-x 0;
}
/* ============ 嵌入模式的用户卡 ============ */
.user-card {
@include card;
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx 24rpx;
margin-bottom: 32rpx;
&:active {
background: $color-bg-page;
}
}
.avatar-light {
width: 88rpx;
height: 88rpx;
background: $color-primary-bg;
border: none;
}
.avatar-text-dark {
font-size: 34rpx;
color: $color-primary;
}
.user-name-dark {
font-size: 32rpx;
color: $color-text;
}
.user-card .user-sub {
color: $color-text-muted;
}
/* ============ 数据概览 ============ */
.kpi-card {
@include card;
display: flex;
padding: 30rpx 0;
margin-bottom: 32rpx;
}
.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: 40rpx;
font-weight: 600;
line-height: 1.1;
color: $color-text;
&.warn {
color: $color-danger;
}
}
.kpi-label {
font-size: 22rpx;
color: $color-text-muted;
}
/* ============ 常用操作:分组卡 + 一行 4 个格子 ============ */
.group-card {
@include card;
padding: 24rpx 20rpx 8rpx;
}
.group-head {
display: flex;
align-items: center;
gap: 14rpx;
padding: 0 4rpx 20rpx;
}
.group-bar {
width: 6rpx;
height: 28rpx;
border-radius: $radius-full;
background: $color-primary;
flex-shrink: 0;
}
.group-title {
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* cell 占 25% 并自带右间隙,容器负 margin 抵消末列间隙 */
.grid {
display: flex;
flex-wrap: wrap;
margin-right: -16rpx;
}
.cell {
width: 25%;
padding-right: 16rpx;
margin-bottom: 16rpx;
box-sizing: border-box;
}
.fn-card {
height: 100%;
padding: 18rpx 6rpx 16rpx;
border-radius: $radius-md;
background: $color-bg-page;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
transition: transform 0.15s ease;
&:active {
transform: scale(0.95);
}
}
.fn-icon {
@include tint-icon(64rpx, 18rpx);
}
.fn-name {
max-width: 100%;
font-size: 22rpx;
line-height: 1.3;
color: $color-text;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logout {
@include card;
margin-top: 40rpx;
padding: 28rpx 0;
text-align: center;
&:active {
background: $color-bg-page;
}
}
.logout-text {
font-size: 28rpx;
color: $color-danger;
}
/* ============ 错误态 ============ */
.err-box {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 22rpx;
margin-bottom: 24rpx;
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;
}
.bottom-space {
height: 32rpx;
}
</style>