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

428 lines
8.3 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="hero" :class="sub.tint">
<view class="hero-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#ffffff" :size="18" />
<text class="hero-back">返回导航</text>
</view>
<view class="hero-main">
<view class="hero-icon">
<FaIcon :name="sub.icon" color="#ffffff" :size="26" />
</view>
<view class="hero-meta">
<text class="hero-name">{{ sub.title }}</text>
<text class="hero-desc">{{ sub.desc }}</text>
</view>
</view>
</view>
<view class="body">
<!-- 未开通 -->
<view v-if="!enabled" class="lock-box">
<FaIcon name="lock" color="#f04438" :size="28" />
<text class="lock-title">该子系统未开通</text>
<text class="lock-desc">当前租户套餐未包含「{{ sub.name }}」,请联系管理员开通后使用。</text>
</view>
<block v-else>
<!-- 加载失败 -->
<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>
<FaIcon name="rotate" color="#f04438" :size="16" />
</view>
<block v-else>
<!-- 常用功能 -->
<view class="block">
<view class="block-head">
<text class="block-title">常用功能</text>
<text class="block-sub">{{ leafMenus.length }} 个功能</text>
</view>
<view v-if="leafMenus.length === 0" class="empty-box">
<text class="empty-text">该子系统下暂无你可见的功能</text>
</view>
<view v-else class="grid">
<view
v-for="item in leafMenus.slice(0, 8)"
:key="item.id"
class="grid-item"
@tap="openMenu(item, item.title)"
>
<view class="tint-icon grid-icon" :class="sub.tint">
<FaIcon name="circle-check" color="#ffffff" :size="20" />
</view>
<text class="grid-name">{{ item.title }}</text>
</view>
</view>
</view>
<!-- 子系统目录 -->
<view v-if="dirMenus.length" class="block">
<view class="block-head">
<text class="block-title">功能分组</text>
</view>
<view class="list-card">
<view
v-for="item in dirMenus"
:key="item.id"
class="list-item"
@tap="goFunctions"
>
<view class="dot" />
<view class="list-body">
<text class="list-title">{{ item.title }}</text>
<text class="list-meta">{{ item.docs || 0 }} 个功能</text>
</view>
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
</view>
</view>
</view>
<!-- 打开 Web 端完整系统(仅该子系统在 Web 端确有对应系统时展示,见 config/subsystems.js 的 webEntry) -->
<view v-if="sub.webEntry" class="block">
<view class="enter-card" @tap="enterSystem">
<view class="tint-icon enter-icon" :class="sub.tint">
<FaIcon name="display" color="#ffffff" :size="22" />
</view>
<view class="enter-meta">
<text class="enter-title">进入完整{{ sub.name }}系统</text>
<text class="enter-desc">在浏览器中打开,支持全部功能</text>
</view>
<FaIcon name="arrow-right" color="#c8cfdf" :size="14" />
</view>
</view>
</block>
</block>
<view class="bottom-space" />
</view>
<AppTabbar :group="slug" current="home" />
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn } from '@/utils/auth.js'
import { openMenu, openWebPage } from '@/utils/nav.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, enabled, leafMenus, dirMenus, moduleMeta, load } = useSubsystem(props.slug)
const entered = ref(false)
function goBackNav() {
uni.reLaunch({ url: '/pages/nav/nav' })
}
function goFunctions() {
uni.redirectTo({ url: `/pages/${props.slug}/functions/functions` })
}
/** 打开 Web 端该模块首页(模块 path 优先,否则用第一个功能页),走票据免登录 */
async function enterSystem() {
if (entered.value) return
const target =
(moduleMeta.value && moduleMeta.value.path) ||
(leafMenus.value[0] && leafMenus.value[0].path) ||
''
if (!target) {
uni.showToast({ title: '该系统暂未开放', icon: 'none' })
return
}
entered.value = true
try {
await openWebPage(target, sub.name)
} finally {
entered.value = false
}
}
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
load()
})
onShow(() => {
if (isLoggedIn() && !loading.value) load()
})
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
@include page-shell;
}
.hero {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 40rpx;
color: #ffffff;
}
.hero-row {
display: flex;
align-items: center;
gap: 4rpx;
opacity: 0.85;
}
.hero-back {
font-size: 24rpx;
color: #ffffff;
}
.hero-main {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 24rpx;
}
.hero-icon {
width: 92rpx;
height: 92rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.32);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.hero-meta {
flex: 1;
min-width: 0;
}
.hero-name {
display: block;
font-size: 38rpx;
font-weight: 600;
letter-spacing: 1rpx;
}
.hero-desc {
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: 24rpx $page-padding-x 0;
}
.block-head {
@include block-head;
}
.block-title {
@include section-title;
}
.block-sub {
font-size: 24rpx;
color: $color-text-muted;
}
.empty-box {
@include card;
padding: 56rpx 24rpx;
text-align: center;
}
.empty-text {
font-size: 25rpx;
color: $color-text-muted;
}
.err-box {
display: flex;
align-items: center;
gap: 16rpx;
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;
}
.lock-box {
@include card;
padding: 60rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 14rpx;
}
.lock-title {
font-size: 30rpx;
font-weight: 600;
color: $color-danger;
}
.lock-desc {
font-size: 24rpx;
color: $color-text-muted;
text-align: center;
line-height: 1.6;
}
.grid {
@include card;
display: grid;
grid-template-columns: repeat(4, 1fr);
padding: 28rpx 8rpx 20rpx;
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 14rpx;
padding: 12rpx 0;
&:active {
opacity: 0.7;
}
}
.grid-icon {
@include tint-icon(84rpx, 22rpx);
}
.grid-name {
font-size: 23rpx;
color: $color-text-secondary;
text-align: center;
}
.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;
}
}
.dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: $color-primary;
flex-shrink: 0;
}
.list-body {
flex: 1;
min-width: 0;
}
.list-title {
display: block;
font-size: 28rpx;
color: $color-text;
}
.list-meta {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.enter-card {
@include card;
display: flex;
align-items: center;
gap: 18rpx;
padding: 26rpx 24rpx;
&:active {
opacity: 0.85;
}
}
.enter-icon {
@include tint-icon(76rpx, 20rpx);
}
.enter-meta {
flex: 1;
min-width: 0;
}
.enter-title {
display: block;
font-size: 28rpx;
font-weight: 500;
color: $color-text;
}
.enter-desc {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: $color-text-muted;
}
.bottom-space {
height: 40rpx;
}
</style>