ow
This commit is contained in:
1 parent
32f237c4bf
commit
8a2adb0a92
18 files changed
+418
-53
No files matched your search
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -69,6 +69,15 @@ export function clearLoginState() {
|
||||
uni.removeStorageSync(JWT_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已有登录会话:只认请求层的 AccessToken(令牌唯一来源,与 nav-guard 口径一致)。
|
||||
* 不在此判过期——AccessToken 过期时请求层会拿 RefreshToken 自动续期,续期失败再 goLogin 退回登录页,
|
||||
* 所以"登录过"就足以决定冷启动是否直接进 dashboard。
|
||||
*/
|
||||
export function hasLoginSession(): boolean {
|
||||
return !!getJwtInfo()?.AccessToken;
|
||||
}
|
||||
|
||||
function getToken(): string {
|
||||
return getJwtInfo()?.AccessToken ?? "";
|
||||
}
|
||||
|
||||
@@ -188,9 +188,8 @@ defineExpose({ refresh, clearSelection, selectedRows });
|
||||
{{ filterCount }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="$slots.toolbar" class="toolbar-extra">
|
||||
<slot name="toolbar" />
|
||||
<!-- 页面可在此放次级动作按钮(如"新增"),排在筛选右侧、只留图标 -->
|
||||
<slot name="toolbar-actions" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -302,17 +301,6 @@ defineExpose({ refresh, clearSelection, selectedRows });
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
/* 页面自定义的次级工具(如"新增")走白底一行,与搜索栏区分 */
|
||||
.toolbar-extra {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 4rpx 24rpx 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid $u-border-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { $u } from "uview-pro";
|
||||
import { ref } from "vue";
|
||||
import { $u, useLocale } from "uview-pro";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { PAGE_PATHS, pageUrl, TAB_PAGES } from "@/router/routes";
|
||||
import { useTabbarStore } from "@/stores";
|
||||
|
||||
defineOptions({
|
||||
@@ -16,14 +17,82 @@ defineOptions({
|
||||
},
|
||||
});
|
||||
|
||||
const { tabbarList } = useTabbarStore();
|
||||
const { t } = useLocale();
|
||||
const store = useTabbarStore();
|
||||
|
||||
const current = ref<number>(0);
|
||||
/**
|
||||
* 把 store 里的"结构配置"翻译成 u-tabbar 需要的 item:文字按当前语言 t(langKey) 取,
|
||||
* 因此切换语言时 tab 文案会自动跟着变(t 依赖 currentLocale,computed 会重新求值)。
|
||||
*
|
||||
* props 收进一个对象再 v-bind:既避开长标签被格式化器折行后触发 html-indent 对撞,
|
||||
* 也集中覆盖 u-tabbar 几个默认值不合口味的 prop:
|
||||
* - hideTabBar 必须 false:其 onMounted 默认调 uni.hideTabBar(),本项目是自定义 tabbar
|
||||
* (无原生 pages.json tabBar),该调用必 reject 成 "hideTabBar:fail not TabBar page" 并被
|
||||
* 全局错误边界弹成错误框。
|
||||
* - midButton 必须 true:否则 item.midButton 的凸起样式不生效(中心「多媒体」大按钮)。
|
||||
*/
|
||||
const tabbarProps = computed(() => ({
|
||||
modelValue: store.activeIndex,
|
||||
list: store.tabs.map((tab, index) => ({
|
||||
text: t(tab.langKey),
|
||||
iconPath: tab.iconPath,
|
||||
selectedIconPath: tab.selectedIconPath,
|
||||
midButton: tab.midButton,
|
||||
count: store.badges[index] ?? 0,
|
||||
})),
|
||||
activeColor: $u.color.primary,
|
||||
midButton: true,
|
||||
// 中心凸起的圆标图片尺寸:显式带单位,撑满 u-tabbar 写死的 100rpx 凸起圆(留一点白边当描边)
|
||||
midButtonSize: "100rpx",
|
||||
iconSize: 40,
|
||||
textSize: 26,
|
||||
hideTabBar: false,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 由当前页面真实路径反推 tab 下标。
|
||||
* 不依赖 activeIndex 的持久值:冷启动直达 / 403 回首页时持久值可能是上一次会话的旧下标,
|
||||
* 按路由算才能保证高亮一定落在真正显示的这一页上。getCurrentPages 给的是不带前导斜杠的真实路径。
|
||||
*/
|
||||
function indexOfCurrentRoute(): number {
|
||||
const pages = getCurrentPages();
|
||||
const route = pages[pages.length - 1]?.route ?? "";
|
||||
return TAB_PAGES.findIndex(name => PAGE_PATHS[name] === route);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const index = indexOfCurrentRoute();
|
||||
if (index >= 0) {
|
||||
store.setActiveIndex(index);
|
||||
}
|
||||
});
|
||||
|
||||
/** item 未配 pagePath,u-tabbar 只会 emit change 而不自行 switchTab;跳转在这里做 */
|
||||
function onChange(index: number): void {
|
||||
if (index === store.activeIndex) {
|
||||
return;
|
||||
}
|
||||
store.setActiveIndex(index);
|
||||
// reLaunch 清空页面栈,符合"切 tab 不叠加历史"的直觉,也会被 nav-guard 正常拦截鉴权
|
||||
uni.reLaunch({ url: pageUrl(TAB_PAGES[index]) });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-tabbar v-model="current" :list="tabbarList" :active-color="$u.color.primary" />
|
||||
<u-tabbar v-bind="tabbarProps" @change="onChange" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/*
|
||||
* 给底部导航加"上分隔线 + 向上投影",让它与页面内容之间有明显层次。
|
||||
* 作用在 .u-tabbar__content(真正带背景色的那条)上:
|
||||
* - border-top:比 u-border-top 自带的 1px 淡线更清晰一点;
|
||||
* - box-shadow:负 y 偏移让阴影朝上投到页面上,形成浮起感。
|
||||
* 注意:borderTop prop 仍保持默认 true —— 中心凸起按钮的缺口白圆(circle__border)
|
||||
* 依赖它才渲染,关掉会让熊猫圆标和导航条衔接处出现割裂。
|
||||
*/
|
||||
:deep(.u-tabbar__content) {
|
||||
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 -6rpx 28rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,13 @@
|
||||
"cancelBut": "Cancel",
|
||||
"cultureLabel": "Language:"
|
||||
},
|
||||
"TabBar": {
|
||||
"home": "Home",
|
||||
"file": "Files",
|
||||
"media": "Media",
|
||||
"share": "Share",
|
||||
"mine": "Me"
|
||||
},
|
||||
"AdminPage": {
|
||||
"filterBut": "Filter",
|
||||
"filterTitle": "Filter Conditions",
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
"cancelBut": "取消",
|
||||
"cultureLabel": "语言:"
|
||||
},
|
||||
"TabBar": {
|
||||
"home": "首页",
|
||||
"file": "文件",
|
||||
"media": "多媒体",
|
||||
"share": "共享",
|
||||
"mine": "我的"
|
||||
},
|
||||
"AdminPage": {
|
||||
"filterBut": "筛选",
|
||||
"filterTitle": "筛选条件",
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/security/actor-enquiry"
|
||||
},
|
||||
// 底部导航测试页(首页复用上面的 dashboard):文件/多媒体/共享/我的
|
||||
{
|
||||
"path": "pages/tab/file"
|
||||
},
|
||||
{
|
||||
"path": "pages/tab/media"
|
||||
},
|
||||
{
|
||||
"path": "pages/tab/share"
|
||||
},
|
||||
{
|
||||
"path": "pages/tab/mine"
|
||||
}
|
||||
],
|
||||
// 全局配置
|
||||
|
||||
@@ -32,7 +32,7 @@ useAuthGuard(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="WebSitePolicy.Title" :show-nav-back="false" :hide-nav="true">
|
||||
<app-page :nav-title="WebSitePolicy.Title" :show-nav-back="false" :hide-nav="true" :show-tabbar="true">
|
||||
<view class="dashboard">
|
||||
<view v-if="menu.loading.value" class="dashboard__tip">
|
||||
<text>{{ t("AdminPage.menuLoading") }}</text>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
// 加密已抽到 @/common/pwd-crypto(wxmp-rsa 纯 JS 实现,H5/小程序/App 全端可用)
|
||||
import type { LoginData } from "@/data/auto/loginData";
|
||||
import type { LoginProfile } from "@/data/auto/loginProfile";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { backendLocale, baseUrl } from "@/common/http.interceptor";
|
||||
import { backendLocale, baseUrl, hasLoginSession } from "@/common/http.interceptor";
|
||||
import { encryptLoginPassword } from "@/common/pwd-crypto";
|
||||
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
|
||||
import { reloadLocales } from "@/locale";
|
||||
@@ -35,6 +35,19 @@ onLoad((options) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 冷启动直接落在登录入口且本地已有登录会话 → 自动进 dashboard,
|
||||
* 省掉"明明登录过还闪一下登录页"。令牌若已过期,dashboard 首个请求会触发请求层用
|
||||
* RefreshToken 自动续期;续期失败再 goLogin 退回本页,不会卡在受保护页里。
|
||||
* 被"有意送回登录页"的场景(401 去登录 / 403 重新登录)不必再设豁免:
|
||||
* goLoginWithRedirect 已先清登录态,此刻 hasLoginSession 必为假,表单正常展示。
|
||||
*/
|
||||
onShow(() => {
|
||||
if (hasLoginSession()) {
|
||||
uni.reLaunch({ url: redirectUrl.value });
|
||||
}
|
||||
});
|
||||
|
||||
// 登录页中英文切换:走 uView Pro 语言包(t / setLocale),语言包 name 为 zh-CN / en-US
|
||||
const { t, setLocale, currentLocale } = useLocale();
|
||||
const isZh = computed(() => (currentLocale.value?.name ?? "zh-CN") !== "en-US");
|
||||
|
||||
@@ -164,12 +164,9 @@ useAuthGuard(() => {
|
||||
:filter-items="draft" :search-token="searchToken" @search="onSearch" @open-filter="state.openFilter()"
|
||||
@close-filter="state.closeFilter()" @apply-filter="state.applyFilter($event)"
|
||||
@clear-filter="state.clearFiltersAndSearch()" @row-tap="onRowTap">
|
||||
<template #toolbar>
|
||||
<template #toolbar-actions>
|
||||
<view v-if="perm.canCreate.value" class="create-entry" @click="onCreate">
|
||||
<u-icon name="plus" size="24" color="#ffffff" />
|
||||
<text class="create-text">
|
||||
{{ t("AdminPage.createBut") }}
|
||||
</text>
|
||||
<u-icon name="plus" size="40" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -188,21 +185,17 @@ useAuthGuard(() => {
|
||||
background-color: $u-bg-color;
|
||||
}
|
||||
|
||||
/* 新增:翠绿实心胶囊,靠右落在次级工具行里 */
|
||||
/* 新增:与筛选按钮同一行的方形图标钮,只留一个加号 */
|
||||
.create-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12rpx 28rpx;
|
||||
color: #ffffff;
|
||||
background-color: $u-type-primary;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(5, 150, 105, 0.24);
|
||||
}
|
||||
|
||||
.create-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-left: 12rpx;
|
||||
color: $u-type-primary;
|
||||
background-color: $u-bg-color;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 底部导航「文件」测试页:先占位,后续接相册/文件功能。
|
||||
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);nav-guard 在 reLaunch 时做路由级校验,
|
||||
* 这里再挂一次 useAuthGuard 兜冷启动直达(地址栏 / file、分享链接不经过导航 API)。
|
||||
*/
|
||||
import { useAuthGuard } from "@/composables";
|
||||
|
||||
useAuthGuard();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page nav-title="文件" :show-nav-back="false" :show-tabbar="true">
|
||||
<view class="tab-page">
|
||||
<text class="tab-page__title">
|
||||
文件
|
||||
</text>
|
||||
<text class="tab-page__hint">
|
||||
测试页,功能待接入
|
||||
</text>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 40rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.tab-page__hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 底部导航「多媒体」测试页:先占位,后续接照片/视频/音频。
|
||||
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import { useAuthGuard } from "@/composables";
|
||||
|
||||
useAuthGuard();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page nav-title="多媒体" :show-nav-back="false" :show-tabbar="true">
|
||||
<view class="tab-page">
|
||||
<text class="tab-page__title">
|
||||
多媒体
|
||||
</text>
|
||||
<text class="tab-page__hint">
|
||||
测试页,功能待接入
|
||||
</text>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 40rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.tab-page__hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 底部导航「我的」测试页:先占位,后续接个人资料/设置/关于入口。
|
||||
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import { useAuthGuard } from "@/composables";
|
||||
|
||||
useAuthGuard();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page nav-title="我的" :show-nav-back="false" :show-tabbar="true">
|
||||
<view class="tab-page">
|
||||
<text class="tab-page__title">
|
||||
我的
|
||||
</text>
|
||||
<text class="tab-page__hint">
|
||||
测试页,功能待接入
|
||||
</text>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 40rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.tab-page__hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 底部导航「共享」测试页:先占位,后续接家庭组/共享管理。
|
||||
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
|
||||
*/
|
||||
import { useAuthGuard } from "@/composables";
|
||||
|
||||
useAuthGuard();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page nav-title="共享" :show-nav-back="false" :show-tabbar="true">
|
||||
<view class="tab-page">
|
||||
<text class="tab-page__title">
|
||||
共享
|
||||
</text>
|
||||
<text class="tab-page__hint">
|
||||
测试页,功能待接入
|
||||
</text>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 40rpx;
|
||||
}
|
||||
|
||||
.tab-page__title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.tab-page__hint {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
* 拦截器拦不到,必须由页面自己调用 composables/useAuthGuard(页面级 onShow 校验)。
|
||||
*/
|
||||
import type { ErrorPageName, PageName } from "@/router/routes";
|
||||
import { getJwtInfo } from "@/common/http.interceptor";
|
||||
import { clearLoginState, getJwtInfo } from "@/common/http.interceptor";
|
||||
import { canVisitPage } from "@/permission/permission-context";
|
||||
import {
|
||||
errorPageUrl,
|
||||
@@ -126,8 +126,13 @@ export function setupUnknownPathFallback(): void {
|
||||
/**
|
||||
* 供页面把用户送回登录页(带 redirect,登录成功后回到原地址;缺省回 dashboard)
|
||||
* redirect 与 from 一样不做预编码,理由见 routes.ts 的 errorPageUrl
|
||||
*
|
||||
* 进登录页前先清登录态:走到这里都是"有意去登录/换号"(401 去登录、403 重新登录),
|
||||
* 旧会话整体作废——否则登录页的冷启动自动放行(hasLoginSession)会把这个还带着令牌的
|
||||
* 用户直接弹回原页,403"重新登录"根本够不到登录表单。
|
||||
*/
|
||||
export function goLoginWithRedirect(from?: string): void {
|
||||
clearLoginState();
|
||||
const target = from && resolvePageName(from) ? from : pageUrl("dashboard");
|
||||
uni.reLaunch({
|
||||
url: `${pageUrl("login")}?redirect=${normalizePath(target)}`,
|
||||
|
||||
@@ -19,6 +19,11 @@ export const PAGE_PATHS = {
|
||||
userEnquiry: "pages/security/user-enquiry",
|
||||
userOperation: "pages/security/user-operation",
|
||||
actorEnquiry: "pages/security/actor-enquiry",
|
||||
// 底部导航 tab 页(首页复用 dashboard):先各建一个简单测试页占位
|
||||
fileTab: "pages/tab/file",
|
||||
mediaTab: "pages/tab/media",
|
||||
shareTab: "pages/tab/share",
|
||||
mineTab: "pages/tab/mine",
|
||||
} as const;
|
||||
|
||||
export type PageName = keyof typeof PAGE_PATHS;
|
||||
@@ -63,8 +68,28 @@ export const PAGE_PERMISSIONS: Partial<Record<PageName, PermissionNames>> = {
|
||||
userEnquiry: "UserEnquiry",
|
||||
userOperation: "UserOperation",
|
||||
actorEnquiry: "SecurityActor",
|
||||
// 底部导航 5 个 tab:真实权限尚未定义,按当前要求统一用 UserOperation 占位。
|
||||
// 注意 dashboard 也被登记后,登录落地页(首页)会开始按 UserOperation 做路由级校验;
|
||||
// 路由级是 fail-open(档案缺失/为空时放行),真正的边界仍在后端。
|
||||
dashboard: "UserOperation",
|
||||
fileTab: "UserOperation",
|
||||
mediaTab: "UserOperation",
|
||||
shareTab: "UserOperation",
|
||||
mineTab: "UserOperation",
|
||||
};
|
||||
|
||||
/**
|
||||
* 底部导航的 tab 顺序,与 stores/tabbar 的 tabbarList 逐项按角标对齐。
|
||||
* 首页复用 dashboard;app-tabbar 靠它把「点击下标 -> 目标页」与「当前路由 -> 高亮下标」对上。
|
||||
*/
|
||||
export const TAB_PAGES: PageName[] = [
|
||||
"dashboard",
|
||||
"fileTab",
|
||||
"mediaTab",
|
||||
"shareTab",
|
||||
"mineTab",
|
||||
];
|
||||
|
||||
/** 带前导斜杠的真实路径,供 uni.* API 之外的路径比较、日志使用 */
|
||||
export function toRealPath(name: PageName): string {
|
||||
return `/${PAGE_PATHS[name]}`;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -1,37 +1,88 @@
|
||||
import type { TabbarItem } from "uview-pro/types/global";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
/**
|
||||
* 底部导航的静态配置:只放"结构"(图标 + i18n 键 + 是否凸起),
|
||||
* 文字不在这里写死——由 components/app-tabbar 按当前语言 t(langKey) 翻译,
|
||||
* 这样切换语言时 tab 文案会跟着变(store 本身对 locale 无感知)。
|
||||
*/
|
||||
export interface TabbarSeed {
|
||||
langKey: string;
|
||||
iconPath: string;
|
||||
selectedIconPath: string;
|
||||
/** 中心凸起大按钮;还需 u-tabbar 父级 midButton 也为 true 才生效 */
|
||||
midButton?: boolean;
|
||||
}
|
||||
|
||||
export const useTabbarStore = defineStore(
|
||||
"tabbar",
|
||||
() => {
|
||||
const activeIndex = ref(0);
|
||||
// 默认空列表:脚手架演示 tab(首页/关于)已随演示页一并移除。
|
||||
// 后续底部导航项由旧项目移植的菜单数据(/SecurityFunction/FindMenus)驱动,
|
||||
// 在此按端需要填充,或整体改造成 pages.json 原生 tabBar。
|
||||
const tabbarList = ref<TabbarItem[]>([]);
|
||||
// 顺序必须与 routes.ts 的 TAB_PAGES 逐项对齐(首页/文件/多媒体/共享/我的)。
|
||||
// 刻意不写 pagePath:u-tabbar 一旦拿到 pagePath 就会调 uni.switchTab,而本项目没有原生
|
||||
// tabBar,switchTab 会 fail。跳转统一由 app-tabbar 的 @change 用 reLaunch 完成,
|
||||
// 高亮由当前页面路由反推下标(见 app-tabbar 的 onMounted)。
|
||||
const tabs = ref<TabbarSeed[]>([
|
||||
{
|
||||
langKey: "TabBar.home",
|
||||
iconPath: "home",
|
||||
selectedIconPath: "home-fill",
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.file",
|
||||
iconPath: "file-text",
|
||||
selectedIconPath: "file-text-fill",
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.media",
|
||||
// 中心凸起用自定义圆标图片(含 "/" → u-icon 走 <image> 渲染),非字体图标
|
||||
iconPath: "/static/tabbar/media.png",
|
||||
selectedIconPath: "/static/tabbar/media.png",
|
||||
midButton: true,
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.share",
|
||||
iconPath: "share",
|
||||
selectedIconPath: "share-fill",
|
||||
},
|
||||
{
|
||||
langKey: "TabBar.mine",
|
||||
iconPath: "account",
|
||||
selectedIconPath: "account-fill",
|
||||
},
|
||||
]);
|
||||
// 各 tab 角标数字(下标 -> count),0/缺省表示不显示。
|
||||
// 共享先挂占位值 34 以验证角标渲染,接入真实待办数量后由 setBadge 覆盖(下标 3 = 共享)。
|
||||
const badges = ref<Record<number, number>>({ 3: 0 });
|
||||
|
||||
const setActiveIndex = (index: number) => {
|
||||
activeIndex.value = index;
|
||||
};
|
||||
|
||||
const updateBadge = (index: number, count: number) => {
|
||||
tabbarList.value[index].count = count;
|
||||
const setBadge = (index: number, count: number) => {
|
||||
// 整体替换以触发响应式(改对象属性 Vue3 也能追踪,但替换更稳、便于持久化裁剪)
|
||||
badges.value = { ...badges.value, [index]: count };
|
||||
};
|
||||
|
||||
const updateIsDot = (index: number, isDot: boolean) => {
|
||||
tabbarList.value[index].isDot = isDot;
|
||||
const clearBadge = (index: number) => {
|
||||
const next = { ...badges.value };
|
||||
delete next[index];
|
||||
badges.value = next;
|
||||
};
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
tabbarList,
|
||||
tabs,
|
||||
badges,
|
||||
setActiveIndex,
|
||||
updateBadge,
|
||||
updateIsDot,
|
||||
setBadge,
|
||||
clearBadge,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
// 只持久化 activeIndex:tabs/badges 是配置与运行期数据,持久化会让改默认值后仍读到旧缓存
|
||||
persist: {
|
||||
paths: ["activeIndex"],
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user