Files
yunzerwebsiteallinone/uniapp/pages/nav/nav.vue
T
2026-10-03 23:46:55 +08:00

370 lines
8.5 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">
<text class="head-title">选择子系统</text>
<text class="head-count">{{ enabledCount }}/{{ cards.length }} 已开通</text>
</view>
<text class="head-sub">{{ user.tenant || '云泽平台' }}<text v-if="user.account"> · {{ user.account }}</text></text>
</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>
<FaIcon name="rotate" color="#f04438" :size="16" />
</view>
<!-- 子系统宫格 -->
<view v-else-if="!loading && cards.length === 0" class="empty-box">
<FaIcon name="cube" color="#c0c4cc" :size="32" />
<text class="empty-text">暂无子系统,请联系管理员开通</text>
</view>
<view v-else class="card-grid">
<view
v-for="card in cards"
:key="card.code || card.id"
class="sub-card"
:class="{ locked: card.locked }"
@tap="enter(card)"
>
<view class="tint-icon sub-icon" :class="card.tint">
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
</view>
<text class="sub-name">{{ card.name }}</text>
<text class="sub-desc">{{ card.desc }}</text>
<view class="sub-go">
<text class="sub-go-text">进入</text>
<FaIcon name="arrow-right" color="#ffffff" :size="11" />
</view>
</view>
</view>
<view class="bottom-space" />
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn } from '@/utils/auth.js'
import { fetchHome } from '@/api/workspace.js'
import { openMenu } from '@/utils/nav.js'
import { getSubsystemByModule, goSubsystem } from '@/config/subsystems.js'
/** 模块 code → 子系统 slug → 进入其概览页;未注册返回 false */
function goSubsystemByModule(code) {
const sub = getSubsystemByModule(code)
return sub ? goSubsystem(sub.slug) : false
}
/** 模块 code → 端上图标(后端 icon 存的是 Element Plus 图标名,端上用不了) */
const MODULE_ICONS = {
crm: 'handshake',
erp: 'boxes-stacked',
oa: 'clipboard-check',
cms: 'globe',
ai: 'robot',
system: 'gear',
tool: 'wrench'
}
const loading = ref(true)
const loadError = ref('')
const user = ref({})
const modules = ref([])
const menus = ref([])
/** 叶子菜单(可直接跳转的功能页),已锁定的过滤掉 */
const leafMenus = computed(() => menus.value.filter((m) => m.leaf !== false && !m.locked))
/** 子系统卡片:每个业务模块一张卡 */
const cards = computed(() => {
const byCode = {}
leafMenus.value.forEach((m) => {
if (!m.module) return
if (!byCode[m.module]) byCode[m.module] = []
byCode[m.module].push(m)
})
return modules.value
.filter((m) => m.type !== 2) // 2=系统配置,不作为子系统入口
.map((mod, index) => {
const code = String(mod.code || '').toLowerCase()
const items = byCode[mod.code] || []
const locked = mod.enabled === false
return {
id: mod.id,
code: mod.code,
path: mod.path || '',
name: mod.name || '未命名系统',
desc: locked
? '未开通该模块'
: mod.description || (items.length ? items.length + ' 个功能' : '暂无功能'),
icon: MODULE_ICONS[code] || 'cube',
tint: locked ? 'tint-off' : 'tint-' + (index % 8),
locked,
items,
total: items.length
}
})
})
const enabledCount = computed(() => cards.value.filter((c) => !c.locked).length)
/* ==================== 数据加载 ==================== */
async function load(force = false) {
loading.value = true
try {
const data = await fetchHome(force)
user.value = data.user || {}
modules.value = Array.isArray(data.modules) ? data.modules : []
menus.value = Array.isArray(data.menus) ? data.menus : []
loadError.value = ''
} catch (e) {
console.error('[nav] load failed:', e)
loadError.value = (e && e.message) || '接口请求失败'
} finally {
loading.value = false
}
}
/* ==================== 交互 ==================== */
/**
* 点击子系统卡片 → 进入该子系统独立页面组(带自己的 tabbar)。
* 未在 config/subsystems.js 注册的模块,降级为直接打开 Web 端该模块首页。
*/
function enter(card) {
if (card.locked) {
openMenu({ locked: true, title: card.name })
return
}
if (goSubsystemByModule(card.code)) return
if (card.items.length) {
openMenu(card.items[0], card.name)
return
}
if (card.path) {
openMenu({ title: card.name, path: card.path, leaf: true }, card.name)
return
}
uni.showToast({ title: card.name + '暂未开放', icon: 'none' })
}
/* ==================== 生命周期 ==================== */
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;
}
/* ============ 顶部信息(融入页面背景) ============ */
.head {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 32rpx) $page-padding-x 12rpx;
background: $color-bg-page;
}
.head-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16rpx;
}
.head-title {
font-size: 42rpx;
font-weight: 600;
color: $color-text;
letter-spacing: 1rpx;
}
.head-count {
font-size: 23rpx;
color: $color-text-muted;
flex-shrink: 0;
}
.head-sub {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
color: $color-text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============ 内容区 ============ */
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 8rpx $page-padding-x 0;
}
/* ============ 失败提示 ============ */
.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;
}
.empty-box {
@include card;
margin-top: 24rpx;
padding: 80rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18rpx;
}
.empty-text {
font-size: 26rpx;
color: $color-text-muted;
}
/* ============ 子系统宫格 ============
用 flex 换行而不是 grid:小程序端 wxss 对 grid 的支持不稳定,
用 flex-wrap + 百分比宽度才能保证任何屏宽都稳定两列,
且同一行卡片高度自动拉齐(align-items 默认 stretch)。 */
.card-grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.sub-card {
@include card;
width: calc(50% - 10rpx);
margin-right: 20rpx;
margin-bottom: 20rpx;
padding: 26rpx 22rpx 22rpx;
display: flex;
flex-direction: column;
transition: transform 0.15s ease, box-shadow 0.2s ease;
/* 每行第 2 个不右边距,保证 50%-10rpx + 20rpx + 50%-10rpx = 100% */
&:nth-child(2n) {
margin-right: 0;
}
&:active {
transform: scale(0.97);
}
/* 未开通:靠灰色图标与灰色文字表达,不使用角标 */
&.locked {
.sub-name,
.sub-desc {
color: $color-text-muted;
}
.sub-go {
background: $color-border;
}
.sub-go-text {
color: $color-text-muted;
}
}
}
.sub-icon {
@include tint-icon(88rpx, 24rpx);
}
.sub-name {
margin-top: 20rpx;
font-size: 31rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sub-desc {
margin-top: 8rpx;
/* 按钮用 margin-top:auto 吸底,这里保证内容短时也有最小间距 */
margin-bottom: 20rpx;
font-size: 22rpx;
color: $color-text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 进入按钮:吸底 + 撑满宽度,同一行的卡片按钮位置自动对齐 */
.sub-go {
display: flex;
align-items: center;
justify-content: center;
gap: 6rpx;
height: 58rpx;
margin-top: auto;
border-radius: $radius-md;
background: $color-primary;
}
.sub-go-text {
font-size: 24rpx;
color: #ffffff;
}
.bottom-space {
height: 48rpx;
}
</style>