Files
yunzerwebsiteallinone/uniapp/pages/nav/nav.vue
T

465 lines
12 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" :style="headPad">
<view class="head-row">
<view>
<text class="head-eyebrow">YUNZE WORKSPACE</text>
<text class="head-title">工作台</text>
</view>
<view class="account-chip"><FaIcon name="user" color="#3478f6" :size="15" /><text>{{ user.account || '我的账号' }}</text></view>
</view>
<view class="tenant-switch" @tap="chooseTenant">
<view class="tenant-mark"><FaIcon name="building" color="#ffffff" :size="19" /></view>
<view class="tenant-copy"><text class="tenant-label">当前租户</text><text class="tenant-name">{{ user.tenant || '云泽平台' }}</text></view>
<view class="tenant-action"><text>切换</text><FaIcon name="chevron-down" color="#3478f6" :size="12" /></view>
</view>
<text class="head-sub">选择一个业务系统,开始高效工作</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>
<!-- 子系统分组卡:组内一行 4 个 -->
<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="grid">
<view
v-for="card in cards"
:key="card.code || card.id"
class="cell"
@tap="enter(card)"
>
<view class="portal-card" :class="{ locked: card.locked }">
<view class="tint-icon portal-icon" :class="card.tint">
<FaIcon :name="card.icon" color="#ffffff" :size="26" />
</view>
<text class="portal-name">{{ card.name }}</text>
</view>
</view>
</view>
<view class="bottom-space" />
</view>
</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, getTenants, getTid, setAuthTokens, setTid, setTenantName, getUser, setUser } from '@/utils/auth.js'
import { switchTenant } from '@/api/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 tenants = ref(getTenants())
const switchingTenant = ref(false)
/** 叶子菜单(可直接跳转的功能页),已锁定的过滤掉 */
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
}
})
})
/* ==================== 数据加载 ==================== */
async function load(force = false) {
loading.value = true
try {
const data = await fetchHome(force)
user.value = data.user || {}
tenants.value = getTenants()
currentTid.value = getTid()
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
}
}
/* ==================== 交互 ==================== */
function chooseTenant() {
const list = tenants.value
if (list.length <= 1) { uni.showToast({ title: list.length ? '当前账号仅绑定一个租户' : '暂无可切换的租户', icon: 'none' })
return
}
if (switchingTenant.value) return
uni.showActionSheet({
itemList: list.map((tenant) => tenant.tenant_name || `企业${tenant.tid}`),
success: async ({ tapIndex }) => {
const tenant = list[tapIndex]
if (!tenant || String(tenant.tid) === String(getTid())) return
switchingTenant.value = true
uni.showLoading({ title: '正在切换...', mask: true })
try {
const res = await switchTenant({ tid: tenant.tid })
if (!res || !res.tokens) throw new Error((res && res.msg) || '切换租户失败')
setAuthTokens(res.tokens)
setTid(tenant.tid)
setTenantName(tenant.tenant_name || '')
const profile = getUser() || {}
const nextUser = { ...profile, tid: tenant.tid, tenant_name: tenant.tenant_name || '' }
setUser(nextUser)
user.value = { ...user.value, tenant: tenant.tenant_name || '', account: nextUser.account || user.value.account }
await load(true)
uni.showToast({ title: '已切换租户', icon: 'success' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '切换失败,请重试', icon: 'none' })
} finally {
uni.hideLoading()
switchingTenant.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' })
}
function openTenantPicker() {
if (tenants.value.length < 2) return
tenantPickerVisible.value = true
}
function closeTenantPicker() {
if (!switchingTenant.value) tenantPickerVisible.value = false
}
async function changeTenant(item) {
const tid = Number(item && item.tid)
if (!tid || tid === currentTid.value || switchingTenant.value) {
tenantPickerVisible.value = false
return
}
switchingTenant.value = true
uni.showLoading({ title: '正在切换...', mask: true })
try {
const result = await switchTenant({ tid })
if (!result || !result.tokens) throw new Error((result && result.msg) || '切换公司失败')
setAuthTokens(result.tokens)
setTid(tid)
const tenantName = item.tenant_name || `企业${tid}`
setTenantName(tenantName)
user.value = Object.assign({}, user.value, { tenant: tenantName, tid })
currentTid.value = tid
tenantPickerVisible.value = false
await load(true)
uni.showToast({ title: '已切换公司', icon: 'success' })
} catch (err) {
uni.showToast({ title: (err && err.message) || '切换公司失败', icon: 'none' })
} finally {
uni.hideLoading()
switchingTenant.value = false
}
}
/* ==================== 生命周期 ==================== */
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
load()
})
onShow(() => {
// 未登录(含后台切回来时令牌被清)直接回登录页,不再停留在首页
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
if (!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 28rpx;
/* #ifdef MP-WEIXIN */
padding-top: calc(env(safe-area-inset-top, 25px) + 96rpx);
/* #endif */
background: linear-gradient(145deg, #eaf3ff 0%, #f5f9ff 58%, #ffffff 100%);
border-bottom: 1rpx solid #e5efff;
}
.head-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.head-eyebrow {
display: block;
margin-bottom: 8rpx;
font-size: 18rpx;
font-weight: 700;
letter-spacing: 3rpx;
color: #6e9de5;
}
.head-title {
display: block;
font-size: 44rpx;
font-weight: 700;
color: #17345f;
letter-spacing: 1rpx;
}
.account-chip {
display: flex;
align-items: center;
gap: 9rpx;
padding: 12rpx 17rpx;
border-radius: 30rpx;
background: rgba(255,255,255,.85);
color: #526b8d;
font-size: 22rpx;
}
.tenant-switch {
display: flex;
align-items: center;
margin-top: 28rpx;
padding: 20rpx 22rpx;
border: 1rpx solid #dceaff;
border-radius: 18rpx;
background: #ffffff;
box-shadow: 0 8rpx 24rpx rgba(47, 110, 196, .08);
}
.tenant-mark {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #69a5ff, #3478f6);
}
.tenant-copy { flex: 1; min-width: 0; margin-left: 16rpx; }
.tenant-label { display: block; font-size: 20rpx; color: #8a9bb3; }
.tenant-name { display: block; margin-top: 5rpx; font-size: 27rpx; font-weight: 600; color: #203b62; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tenant-action { display: flex; align-items: center; gap: 8rpx; color: #3478f6; font-size: 22rpx; font-weight: 600; }
.head-sub {
display: block;
margin-top: 22rpx;
font-size: 23rpx;
color: #8295b0;
}
/* ============ 内容区 ============ */
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 20rpx $page-padding-x 0;
background: #f5f8fd;
}
/* ============ 失败提示 ============ */
.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;
}
/* ============ 子系统门户宫格:一行 2 个,仅图标 + 名称 ============ */
.grid {
display: flex;
flex-wrap: wrap;
margin-right: -20rpx;
padding-top: 16rpx;
}
.cell {
width: 50%;
padding-right: 20rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
}
.portal-card {
@include card;
padding: 34rpx 16rpx 30rpx;
border: 1rpx solid #e6eef9;
box-shadow: 0 6rpx 20rpx rgba(38, 86, 150, .045);
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
transition: transform 0.15s ease;
&:active {
transform: scale(0.97);
}
}
.portal-icon {
@include tint-icon(108rpx, 28rpx);
}
.portal-name {
max-width: 100%;
font-size: 28rpx;
font-weight: 600;
letter-spacing: 0.5rpx;
color: $color-text;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 未开通:灰底图标 + 灰字,不做角标,保持门户的干净 */
.portal-card.locked .portal-name {
color: $color-text-muted;
}
.bottom-space {
height: 48rpx;
}
</style>