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

316 lines
6.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" @tap="goBackNav">
<FaIcon name="chevron-left" color="#1f2b48" :size="18" />
<text class="head-back">返回导航</text>
</view>
<view class="head-title-row">
<text class="head-title">{{ sub.title }}</text>
<text class="head-count">{{ filteredMenus.length }} 个功能</text>
</view>
<view class="search-bar">
<FaIcon name="magnifying-glass" color="#8a94ad" :size="15" />
<input
v-model="keyword"
class="search-input"
type="text"
placeholder="搜索功能名称"
placeholder-class="search-placeholder"
confirm-type="search"
/>
<view v-if="keyword" class="search-clear" @tap="keyword = ''">
<FaIcon name="circle-xmark" color="#c0c4cc" :size="15" />
</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>
<block v-else>
<view v-if="filteredMenus.length === 0" class="empty-box">
<FaIcon name="magnifying-glass" color="#c0c4cc" :size="30" />
<text class="empty-text">{{ keyword.trim() ? '没有匹配的功能' : '该子系统下暂无你可见的功能' }}</text>
</view>
<!-- 功能卡片:两列宫格,与子系统导航页保持一致的视觉语言 -->
<view v-else class="card-grid">
<view
v-for="(item, index) in filteredMenus"
:key="item.id"
class="fn-card"
:class="{ locked: item.locked }"
@tap="openMenu(item, item.title)"
>
<view class="tint-icon fn-icon" :class="item.locked ? 'tint-off' : 'tint-' + (index % 8)">
<FaIcon :name="item.icon || fallbackIcon" color="#ffffff" :size="20" />
</view>
<text class="fn-name">{{ item.title }}</text>
<text class="fn-sub">{{ item.leaf === false ? (item.docs || 0) + ' 个下级' : shortPath(item.path) }}</text>
</view>
</view>
</block>
<view class="bottom-space" />
</view>
<AppTabbar :group="slug" current="functions" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
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 }
})
const { sub, loading, loadError, allMenus, load } = useSubsystem(props.slug)
const keyword = ref('')
/** 模块 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 filteredMenus = computed(() => {
const q = keyword.value.trim().toLowerCase()
const list = allMenus.value.slice().sort((a, b) => a.sort - b.sort)
if (!q) return list
return list.filter((m) => String(m.title || '').toLowerCase().indexOf(q) > -1)
})
/** 摘要行:去掉模块前缀,只留相对路径,太长时截断 */
function shortPath(path) {
const p = String(path || '')
if (!p) return '功能'
const parts = p.split('/').filter(Boolean)
if (parts.length <= 2) return p
return '…/' + parts.slice(-2).join('/')
}
function goBackNav() {
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;
}
.head {
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 24rpx;
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-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16rpx;
margin-top: 14rpx;
}
.head-title {
font-size: 36rpx;
font-weight: 600;
color: $color-text;
}
.head-count {
font-size: 23rpx;
color: $color-text-muted;
flex-shrink: 0;
}
.search-bar {
display: flex;
align-items: center;
gap: 12rpx;
height: 74rpx;
margin-top: 22rpx;
padding: 0 24rpx;
background: $color-bg-page;
border-radius: $radius-full;
}
.search-input {
flex: 1;
min-width: 0;
height: 74rpx;
font-size: 27rpx;
color: $color-text;
}
.search-placeholder {
color: $color-text-muted;
font-size: 27rpx;
}
.search-clear {
display: flex;
align-items: center;
padding: 8rpx;
}
.body {
flex: 1;
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 24rpx $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;
}
/* ============ 功能卡片:两列宫格 ============
与 nav.vue 一致用 flex 换行而非 grid,避免小程序端 wxss 对 grid 支持不稳。 */
.card-grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.fn-card {
@include card;
width: calc(50% - 10rpx);
margin-right: 20rpx;
margin-bottom: 20rpx;
padding: 24rpx 20rpx 20rpx;
display: flex;
flex-direction: column;
transition: transform 0.15s ease;
&:nth-child(2n) {
margin-right: 0;
}
&:active {
transform: scale(0.97);
}
&.locked {
.fn-name,
.fn-sub {
color: $color-text-muted;
}
}
}
.fn-icon {
@include tint-icon(76rpx, 20rpx);
}
.fn-name {
margin-top: 18rpx;
font-size: 28rpx;
font-weight: 500;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fn-sub {
margin-top: 6rpx;
font-size: 21rpx;
color: $color-text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bottom-space {
height: 48rpx;
}
</style>