Files
yunzerwebsiteallinone/uniapp/components/sub/SubFunctions.vue
T

364 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" :class="{ embedded }">
<view v-if="!embedded" class="head" :style="headPad">
<view class="head-row" @tap="goBackNav">
<FaIcon name="chevron-left" color="#1f2b48" :size="18" />
<text class="head-back">返回导航</text>
</view>
<text class="head-title">{{ sub.title }}</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>
</view>
<view v-else-if="groups.length === 0" class="empty-box">
<FaIcon name="table-cells" color="#c0c4cc" :size="30" />
<text class="empty-text">该子系统下暂无你可见的功能</text>
</view>
<!-- 功能分组:每个分组一个 card,组内为功能 card(一行 4 个,仅图标 + 名称) -->
<view v-else class="group-list">
<view v-for="group in groups" :key="group.id" class="group-card">
<view class="group-head">
<view class="group-bar" />
<text class="group-title">{{ group.title }}</text>
<text class="group-count">{{ group.items.length }}</text>
</view>
<view class="grid">
<view
v-for="item in group.items"
:key="item.id"
class="cell"
@tap="openMenu(item, item.title)"
>
<view class="fn-card" :class="{ locked: item.locked }">
<view class="tint-icon fn-icon" :class="item.locked ? 'tint-off' : 'tint-' + (item.tintIndex % 8)">
<FaIcon :name="item.icon || fallbackIcon" color="#ffffff" :size="18" />
</view>
<text class="fn-name">{{ item.title }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="bottom-space" />
</view>
<AppTabbar v-if="!embedded" :group="slug" current="functions" />
</view>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { mpHeadPad } from '@/utils/mpHead.js'
const headPad = mpHeadPad()
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn } from '@/utils/auth.js'
import { openMenu } from '@/utils/nav.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, allMenus, load } = useSubsystem(props.slug)
/** 模块 code → 图标,用于菜单自身没配图标时的兜底 */
const fallbackIcon = computed(() => {
const map = {
crm: 'handshake',
erp: 'boxes-stacked',
oa: 'clipboard-check',
cms: 'globe',
bianjiegongju: 'wrench'
}
return map[String(sub.module || '').toLowerCase()] || 'file-lines'
})
/**
* 按「一级分类目录」分组:
* 菜单层级形如 模块根 → 个人办公 → 通讯录,分组取一级目录(个人办公 / 人事管理 …),
* 组内为该目录下的全部可见功能。无分类归属的快捷功能不在功能页展示,由首页「常用功能」承载。
*/
const groups = computed(() => {
const menus = allMenus.value.slice().sort((a, b) => (a.sort || 0) - (b.sort || 0))
const byId = {}
menus.forEach((m) => { byId[m.id] = m })
// 取该菜单所属的「一级分组」节点
function groupNodeOf(m) {
// 自底向上收集祖先链:chain[0] 为链上最顶层节点
const chain = [m]
let cur = m
let guard = 0
while (byId[cur.pid] && guard++ < 20) {
cur = byId[cur.pid]
chain.unshift(cur)
}
const top = chain[0]
if (top.pid === 0) {
// 模块根在列表内:跳过根,取它下一层(个人办公 / 人事管理 …)
if (chain.length >= 2) return chain[1]
// 顶层节点自身:目录视为模块容器跳过,叶子无分类归属
return top.leaf === false ? null : top
}
// 模块根不在列表内:链上顶层节点本身就是一级分组
return top
}
const groupMap = new Map()
menus.forEach((m) => {
const g = groupNodeOf(m)
if (!g) return
if (g.id === m.id) {
// 自身即分组节点:目录作组标题(不重复出现在组内),叶子无分类归属不展示
if (g.leaf === false && !groupMap.has(g.id)) {
groupMap.set(g.id, { id: g.id, title: g.title, sort: g.sort, items: [] })
}
return
}
if (!groupMap.has(g.id)) {
groupMap.set(g.id, { id: g.id, title: g.title, sort: g.sort, items: [] })
}
groupMap.get(g.id).items.push(m)
})
// 功能页只展示「归属分类目录且有可见功能」的分组;无归属功能由首页「常用功能」承载
const list = []
groupMap.forEach((g) => {
if (g.items.length) list.push(g)
})
list.sort((a, b) => (a.sort || 0) - (b.sort || 0))
// 颜色下标按展开顺序统一编号,避免每个分组都从 tint-0 开始
let colorIndex = 0
return list.map((group) => ({
...group,
items: group.items.map((item) => ({ ...item, tintIndex: colorIndex++ }))
}))
})
function goBackNav() {
if (props.embedded) {
emit('back')
return
}
uni.reLaunch({ url: '/pages/nav/nav' })
}
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;
// 嵌入模式下由父页(tabs.vue)的 scroll-view 统一滚动,这里改为自然高度
&.embedded {
height: auto;
}
}
.head {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 24rpx;
/* #ifdef MP-WEIXIN */
padding-top: calc(env(safe-area-inset-top, 25px) + 84rpx);
/* #endif */
background: $color-card;
box-shadow: $shadow-header;
}
.head-row {
display: flex;
align-items: center;
gap: 4rpx;
}
.head-back {
font-size: 24rpx;
color: $color-text-muted;
}
.head-title {
display: block;
margin-top: 14rpx;
font-size: 36rpx;
font-weight: 600;
color: $color-text;
}
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 28rpx $page-padding-x 0;
}
.empty-box {
@include card;
padding: 80rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.empty-text {
font-size: 26rpx;
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;
}
/* ============ 分组卡:外层一个 card,内含若干功能 card ============ */
.group-card {
@include card;
padding: 24rpx 20rpx 8rpx;
& + & {
margin-top: 24rpx;
}
}
.group-head {
display: flex;
align-items: center;
gap: 12rpx;
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;
}
.group-count {
@include chip;
}
/* ============ 功能宫格:一行 4 个 ============
cell 占 25% 宽并自带右侧间隙,容器用负 margin 抵消末列间隙,
避免使用 calc 除法——小程序端 wxss 对复杂 calc 支持不稳。 */
.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;
}
.fn-card.locked {
.fn-name {
color: $color-text-muted;
}
}
.bottom-space {
height: 32rpx;
}
</style>