Files
Docker/uniapp/youjiala/src/pages/tab/mine.vue
T
2026-10-23 15:48:08 +08:00

430 lines
16 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.
<script setup lang="ts">
/**
* 底部导航「我的」页:概览入口 +「关于」(应用名/当前版本/运行平台)。
* 主题配色与深色模式的具体设置已拆到 pages/theme/theme.vue(短地址 /theme),本页只做跳入入口。
* 权限沿用 DmsFile(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
* 版本号取 common/constant 的 APP_INFO(与 manifest/package 对齐);H5 无 plus.runtime.version。
*/
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { currentEndpoint } from "@/common/apiEndpoint";
import { APP_INFO } from "@/common/constant";
import { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
import { usePermission } from "@/permission/permission-context";
import { pageUrl } from "@/router/routes";
import authService from "@/service/security/authService";
import { useLoginProfileStore } from "@/stores";
// #ifdef APP-PLUS
import { checkAppUpdate } from "@/utils/appUpdate";
// #endif
const { t } = useLocale();
useAuthGuard();
const loginProfileStore = useLoginProfileStore();
/* 代理登录入口显隐,口径必须与后端 /Auth/LoginAgent 的 [PermissionOperate(name:"SecurityUser", operate:"LoginAgent")] 完全一致:
后端 PermissionHandler 对非标准 operate 走 default 分支 → CanNone("SecurityUser","LoginAgent"),
即查「数据级授权」里有没有一条 PermissionName=SecurityUser 且 DependentDetailName=LoginAgent 的行(只看挂没挂,不看 Y/N)。
前端镜像就是 can("none","SecurityUser","LoginAgent")。之前误用 canNone("LoginAgent") 查的是功能级同名行,口径错位——
会出现「入口看得见、一点就 403」。改成同口径后:见得到 == 用得了。 */
const { can, canNone } = usePermission();
const canSeeLoginAgent = computed(() => can("none", "SecurityUser", "LoginAgent"));
/* 「安全管理」入口显隐:需 SecurityActor 功能权限(与 PAGE_PERMISSIONS.actorEnquiry 同一权限名)。
没授这个权限的用户看不到入口,也就不存在“进得去但里面全 403”的空壳页 */
const canSeeSecurity = computed(() => canNone("SecurityActor"));
/* 「已认证」判定:与后端 UpdateSecurityUserIdCard 口径一致——UserType=NORMAL_USERS("N") 即视为已认证(身份验证成功后本地快照同步) */
const isVerified = computed(() => loginProfileStore.userType === "N");
/* 统一风格:主题渐变底 + 主题色变量(与 family-person-enquiry / 族谱首页同一接线口径) */
const { themeBackground } = useThemeGradient();
const { primary, accentGradient, accentSoft, accentShadow } = useThemeTokens();
const themeVars = computed(() => ({
"--fg-primary": primary.value,
"--fg-gradient": accentGradient.value,
"--fg-soft": accentSoft.value,
"--fg-shadow": accentShadow.value,
}));
/** 进入「我的主题」页:theme 非 tab 页,用 navigateTo 保留历史以便返回 */
function goTheme(): void {
uni.navigateTo({ url: pageUrl("themePage") });
}
// ===== 访问线路(App 手动切换公网/内网直连;弹层组件与登录页共用) =====
const linePopupVisible = ref(false);
/** 当前生效线路(保存后刷新展示) */
const lineSetting = ref(currentEndpoint());
/** 当前线路展示文案:公网(附自定义地址)/ 内网直连(附地址) */
const currentLineLabel = computed(() => {
const setting = lineSetting.value;
if (setting.mode === "lan") {
return `${t("MinePage.lineLan")} · ${setting.lanUrl}`;
}
return setting.wanUrl ? `${t("MinePage.lineWan")} · ${setting.wanUrl}` : t("MinePage.lineWan");
});
/** 打开线路弹层:草稿回填由组件内部完成,这里刷新展示快照并展开 */
function openLinePopup(): void {
lineSetting.value = currentEndpoint();
linePopupVisible.value = true;
}
/** 弹层保存后回调:刷新展示;服务器真的变了 → 本地令牌对新服务器无意义,清登录态回登录页重新认证 */
function onLineSaved(payload: { changed: boolean }): void {
lineSetting.value = currentEndpoint();
if (!payload.changed) {
uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" });
return;
}
loginProfileStore.clearStorage();
uni.showToast({ title: t("MinePage.lineSavedRelogin"), icon: "none" });
setTimeout(() => uni.reLaunch({ url: pageUrl("login") }), 1200);
}
// #ifdef APP-PLUS
/** 检查更新(仅 App):拉 FileServer 版本清单比对本机 versionCode,弹窗/下载/安装与失败提示都在 utils/appUpdate 里统一处理 */
function handleCheckUpdate(): void {
void checkAppUpdate(true);
}
// #endif
/** 进入「安全管理」枢纽页:权限体系等基础设定的聚合入口 */
function goSecurity(): void {
uni.navigateTo({ url: pageUrl("securityPage") });
}
/** 进入「代理登录」页(/login-agent):管理员以被代理号身份切换会话 */
function goLoginAgent(): void {
uni.navigateTo({ url: pageUrl("loginAgent") });
}
/** 进入「修改密码」页(/change-password):当前登录用户改密,成功后更新档案保持登录 */
function goChangePassword(): void {
uni.navigateTo({ url: pageUrl("changePassword") });
}
/** 进入「修改昵称」页(/change-local-name):改用户名,成功后按旧系统约定退出重登刷新快照;已认证用户昵称锁定为身份证姓名,单击无效(改由右侧直接展示昵称) */
function goChangeLocalName(): void {
if (isVerified.value) {
return;
}
uni.navigateTo({ url: pageUrl("changeLocalName") });
}
/** 进入「身份验证」页(/identity-verification):上传身份证 OCR 后把信息写入备注并置认证态;已认证后不再重复认证,单击无效 */
function goIdentityVerification(): void {
if (isVerified.value) {
return;
}
uni.navigateTo({ url: pageUrl("identityVerification") });
}
/** 退出登录:确认后调后端 Logout、清本地档案,回登录页 */
async function handleLogout(): Promise<void> {
const confirmed = await new Promise<boolean>((resolve) => {
uni.showModal({
title: t("MinePage.logoutConfirmTitle"),
content: t("MinePage.logoutConfirmContent"),
success: res => resolve(!!res.confirm),
fail: () => resolve(false),
});
});
if (!confirmed) {
return;
}
try {
await authService.Logout();
}
catch {
// 后端退出失败也要清本地态,避免卡在已登录的假象
}
loginProfileStore.clearStorage();
uni.showToast({ title: t("MinePage.logoutSuccess"), icon: "none" });
// mine 是 tab 页,用 reLaunch 清栈回登录页,避免返回键又回到「我的」
setTimeout(() => uni.reLaunch({ url: pageUrl("login") }), 800);
}
</script>
<template>
<!-- 统一风格:隐藏旧 u-navbar,改用与其他 tab 页同款透明 page-navbar 坐在主题渐变底上 -->
<app-page :hide-nav="true" :show-tabbar="true">
<view class="mine-page" :style="[{ background: themeBackground }, themeVars]">
<page-navbar :title="t('MinePage.title')" :show-back="false" :show-transfer="false" :show-quick-toggle="false"
:show-search-toggle="false" />
<view class="mine-page__body">
<!-- 我的主题:进入主题配色 / 深色模式设置页(/theme) -->
<view class="mine-cell" @tap="goTheme">
<view class="mine-cell__icon">
<u-icon name="setting-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.myTheme") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 访问线路:App 手动切换公网/内网直连(资源地址由后端按请求 Host 自适应,仅 App 展示) -->
<!-- #ifdef APP-PLUS -->
<view class="mine-cell" @tap="openLinePopup">
<view class="mine-cell__icon">
<u-icon name="server-man" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.accessLine") }}
</text>
<text class="mine-cell__value">
{{ currentLineLabel }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- #endif -->
<!-- 检查更新:仅 App;自检 FileServer 版本清单,发现新版走整包更新(逻辑见 utils/appUpdate.ts) -->
<!-- #ifdef APP-PLUS -->
<view class="mine-cell" @tap="handleCheckUpdate">
<view class="mine-cell__icon">
<u-icon name="download" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.checkUpdate") }}
</text>
<text class="mine-cell__value">
v{{ APP_INFO.version }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- #endif -->
<!-- 安全管理:进入权限体系基础设定聚合页(/security);仅授 SecurityActor 功能者可见 -->
<view v-if="canSeeSecurity" class="mine-cell" @tap="goSecurity">
<view class="mine-cell__icon">
<u-icon name="lock-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.mySecurity") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 代理登录:管理员以被代理号身份切换会话(/login-agent);仅授予 LoginAgent 功能者可见 -->
<view v-if="canSeeLoginAgent" class="mine-cell" @tap="goLoginAgent">
<view class="mine-cell__icon">
<u-icon name="account-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.loginAgent") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 修改密码:当前登录用户改密(/change-password) -->
<view class="mine-cell" @tap="goChangePassword">
<view class="mine-cell__icon">
<u-icon name="lock-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.changePassword") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 修改昵称:改用户名(/change-local-name);已认证则锁定、改名为「认证昵称」并直接展示昵称 -->
<view class="mine-cell" @tap="goChangeLocalName">
<view class="mine-cell__icon">
<u-icon name="edit-pen-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ isVerified ? t("MinePage.verifiedLocalName") : t("MinePage.changeLocalName") }}
</text>
<text v-if="isVerified" class="mine-cell__value">
{{ loginProfileStore.userLocalName }}
</text>
<u-icon v-else name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 身份验证:上传身份证 OCR 后写入备注并置认证态(/identity-verification);已认证则右侧换为徽标 -->
<view class="mine-cell" @tap="goIdentityVerification">
<view class="mine-cell__icon">
<u-icon name="checkmark-circle-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.identityVerification") }}
</text>
<text v-if="isVerified" class="mine-cell__tag">
{{ t("MinePage.verified") }}
</text>
<u-icon v-else name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 退出登录:确认后清会话回登录页 -->
<view class="mine-cell mine-cell--logout" @tap="handleLogout">
<view class="mine-cell__icon mine-cell__icon--logout">
<u-icon name="close-circle-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.logout") }}
</text>
</view>
<!-- 关于:应用名 + 当前版本 + 运行平台 -->
<view class="mine-card">
<text class="mine-card__title">
{{ t("MinePage.aboutTitle") }}
</text>
<view class="mine-about__row">
<text class="mine-about__label">
{{ t("MinePage.appName") }}
</text>
<text class="mine-about__value">
{{ APP_INFO.name }}
</text>
</view>
<view class="mine-about__row">
<text class="mine-about__label">
{{ t("MinePage.versionLabel") }}
</text>
<text class="mine-about__value">
v{{ APP_INFO.version }}
</text>
</view>
<view class="mine-about__row">
<text class="mine-about__label">
{{ t("MinePage.platformLabel") }}
</text>
<text class="mine-about__value">
{{ APP_INFO.platform }}
</text>
</view>
</view>
</view>
</view>
<!-- 访问线路弹层:与登录页共用的组件,改地址后由 onLineSaved 决定是否登出(仅 App 有入口) -->
<!-- #ifdef APP-PLUS -->
<endpoint-setting-popup v-model="linePopupVisible" @saved="onLineSaved" />
<!-- #endif -->
</app-page>
</template>
<style lang="scss" scoped>
.mine-page {
display: flex;
flex-direction: column;
box-sizing: border-box;
min-height: 100vh;
}
/* 内容区:page-navbar 之下,卡片间距与页边距统一 */
.mine-page__body {
display: flex;
flex: 1;
flex-direction: column;
gap: 24rpx;
padding: 0 24rpx 24rpx;
}
/* 白底圆角卡片:与文件页分区卡片同一风格语言(渐变底上轻阴影浮起) */
.mine-card {
padding: 32rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(31, 35, 41, 0.06);
}
.mine-card__title {
font-size: 30rpx;
font-weight: 600;
color: #1f2329;
}
/* 可点的列表行:主题渐变图标块 + 标题左、箭头右,整行白底卡片 */
.mine-cell {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(31, 35, 41, 0.06);
}
.mine-cell__icon {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 18rpx;
background: var(--fg-gradient, linear-gradient(135deg, #ff9a3d, #ff7d1f));
box-shadow: var(--fg-shadow, 0 6rpx 16rpx rgba(255, 125, 31, 0.35));
}
.mine-cell__title {
flex: 1;
margin-left: 24rpx;
font-size: 30rpx;
font-weight: 600;
color: #1f2329;
}
/* 已认证徽标:替换右箭头,绿色描边胶囊,与主题橙入口区分状态语义 */
.mine-cell__tag {
padding: 6rpx 20rpx;
border: 2rpx solid #52c41a;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 600;
color: #52c41a;
}
/* 已认证锁定的展示值(如昵称):右对齐灰字省略,替代不可点的箭头 */
.mine-cell__value {
max-width: 60%;
overflow: hidden;
font-size: 28rpx;
color: #8f959e;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 退出登录:降权为描边行,标题居中、危险色,与普通入口区分 */
.mine-cell--logout {
justify-content: center;
}
.mine-cell__icon--logout {
background: linear-gradient(135deg, #ff7a7a, #fa3534);
box-shadow: 0 6rpx 16rpx rgba(250, 53, 52, 0.35);
}
.mine-cell--logout .mine-cell__title {
flex: none;
margin-left: 24rpx;
color: #fa3534;
}
/* 关于页行:标签左灰、值右深,横向两端对齐 */
.mine-about__row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
}
.mine-about__label {
font-size: 26rpx;
color: #9aa0a6;
}
.mine-about__value {
font-size: 26rpx;
color: #1f2329;
}
/* 访问线路弹层样式已随组件迁至 components/endpoint-setting-popup/endpoint-setting-popup.vue */
</style>