Files
Docker/uniapp/youjiala/src/pages/tab/mine.vue
T
2026-10-21 20:50:55 +08:00

648 lines
21 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, normalizeLanUrl, saveEndpoint, testEndpoint } 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";
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 lineModeDraft = ref<"wan" | "lan">("wan");
const lineLanDraft = ref("");
const lineTesting = ref(false);
const lineTestResult = ref<"" | "ok" | "fail">("");
/** 当前线路展示文案:公网 / 内网直连(附地址) */
const currentLineLabel = computed(() => {
const setting = lineSetting.value;
return setting.mode === "lan" ? `${t("MinePage.lineLan")} · ${setting.lanUrl}` : t("MinePage.lineWan");
});
/** 打开线路弹层:草稿回填当前设置,清掉上次测活结果 */
function openLinePopup(): void {
const setting = currentEndpoint();
lineModeDraft.value = setting.mode;
lineLanDraft.value = setting.lanUrl;
lineTestResult.value = "";
linePopupVisible.value = true;
}
/** 测试内网地址连通性:用 RSA 公钥接口探活(与登录首调同源) */
async function handleTestLine(): Promise<void> {
if (lineTesting.value) {
return;
}
const url = normalizeLanUrl(lineLanDraft.value);
if (!url) {
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
return;
}
lineTesting.value = true;
lineTestResult.value = "";
const ok = await testEndpoint(url);
lineTesting.value = false;
lineTestResult.value = ok ? "ok" : "fail";
if (!ok) {
uni.showToast({ title: t("MinePage.lineTestFail"), icon: "none" });
}
}
/** 保存线路:内网模式地址必填且合法;生效只影响后续请求,各页切回时自然按新线路拉数据 */
function handleSaveLine(): void {
if (lineModeDraft.value === "lan" && !normalizeLanUrl(lineLanDraft.value)) {
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
return;
}
saveEndpoint({ mode: lineModeDraft.value, lanUrl: lineLanDraft.value });
lineSetting.value = currentEndpoint();
linePopupVisible.value = false;
uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" });
}
/** 进入「安全管理」枢纽页:权限体系等基础设定的聚合入口 */
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 -->
<!-- 安全管理:进入权限体系基础设定聚合页(/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>
<!-- 访问线路弹层:公网/内网单选 + 内网地址手填 + 测连通(仅 App 有入口) -->
<!-- #ifdef APP-PLUS -->
<u-popup :model-value="linePopupVisible" mode="bottom" round="24" @close="linePopupVisible = false">
<view class="line-sheet">
<text class="line-sheet__title">
{{ t("MinePage.accessLine") }}
</text>
<text class="line-sheet__hint">
{{ t("MinePage.lineHint") }}
</text>
<!-- 公网:默认线路,随时随地可用 -->
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'wan' }"
@tap="lineModeDraft = 'wan'">
<view class="line-option__body">
<text class="line-option__name">
{{ t("MinePage.lineWan") }}
</text>
<text class="line-option__desc">
{{ t("MinePage.lineWanDesc") }}
</text>
</view>
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'wan' }" />
</view>
<!-- 内网直连:需与服务器同局域网,地址手填 -->
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'lan' }"
@tap="lineModeDraft = 'lan'">
<view class="line-option__body">
<text class="line-option__name">
{{ t("MinePage.lineLan") }}
</text>
<text class="line-option__desc">
{{ t("MinePage.lineLanDesc") }}
</text>
</view>
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'lan' }" />
</view>
<!-- 内网地址:选中内网才展开;测连通用免鉴权的 RSA 公钥接口 -->
<view v-if="lineModeDraft === 'lan'" class="line-lan">
<input v-model="lineLanDraft" class="line-lan__input" :placeholder="t('MinePage.lineLanPlaceholder')"
placeholder-class="line-lan__ph">
<view class="line-lan__test" @tap="handleTestLine">
<text class="line-lan__test-text">
{{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
</text>
</view>
</view>
<text v-if="lineModeDraft === 'lan' && lineTestResult === 'ok'" class="line-lan__result line-lan__result--ok">
{{ t("MinePage.lineTestOk") }}
</text>
<text v-else-if="lineModeDraft === 'lan' && lineTestResult === 'fail'"
class="line-lan__result line-lan__result--fail">
{{ t("MinePage.lineTestFail") }}
</text>
<view class="line-sheet__actions">
<view class="line-sheet__btn line-sheet__btn--ghost" @tap="linePopupVisible = false">
<text class="line-sheet__btn-text line-sheet__btn-text--ghost">
{{ t("MinePage.lineCancel") }}
</text>
</view>
<view class="line-sheet__btn line-sheet__btn--primary" @tap="handleSaveLine">
<text class="line-sheet__btn-text">
{{ t("MinePage.lineSave") }}
</text>
</view>
</view>
</view>
</u-popup>
<!-- #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;
}
/* ===== 访问线路弹层(底部抽屉,同 file-action-sheet 风格) ===== */
.line-sheet {
padding: 32rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #f5f6f7;
}
.line-sheet__title {
font-size: 32rpx;
font-weight: 600;
color: #1f2329;
}
.line-sheet__hint {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #9aa0a6;
}
/* 单选行:白卡;选中态用内阴影描边(不改变布局尺寸) */
.line-option {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 24rpx;
border-radius: 20rpx;
background-color: #ffffff;
}
.line-option--active {
box-shadow: 0 0 0 2rpx var(--fg-primary, #ff7d1f) inset;
}
.line-option__body {
display: flex;
flex: 1;
flex-direction: column;
gap: 6rpx;
}
.line-option__name {
font-size: 28rpx;
font-weight: 600;
color: #1f2329;
}
.line-option__desc {
font-size: 24rpx;
color: #9aa0a6;
}
.line-option__radio {
box-sizing: border-box;
width: 36rpx;
height: 36rpx;
margin-left: 16rpx;
border: 2rpx solid #d4d7dc;
border-radius: 50%;
}
.line-option__radio--on {
border-color: var(--fg-primary, #ff7d1f);
background: radial-gradient(circle, var(--fg-primary, #ff7d1f) 0 42%, transparent 48%);
}
/* 内网地址:输入框 + 测试连接一行 */
.line-lan {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 20rpx;
}
.line-lan__input {
flex: 1;
height: 76rpx;
padding: 0 20rpx;
border-radius: 16rpx;
font-size: 26rpx;
color: #1f2329;
background-color: #ffffff;
}
.line-lan__ph {
color: #c0c4cc;
}
.line-lan__test {
display: flex;
align-items: center;
justify-content: center;
height: 76rpx;
padding: 0 24rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
.line-lan__test-text {
font-size: 26rpx;
font-weight: 600;
color: var(--fg-primary, #ff7d1f);
}
.line-lan__result {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
}
.line-lan__result--ok {
color: #52c41a;
}
.line-lan__result--fail {
color: #fa3534;
}
/* 底部双按钮:取消(描边)/ 保存(主题渐变) */
.line-sheet__actions {
display: flex;
gap: 20rpx;
margin-top: 32rpx;
}
.line-sheet__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: 999rpx;
}
.line-sheet__btn--ghost {
border: 2rpx solid #d4d7dc;
background-color: #ffffff;
}
.line-sheet__btn--primary {
background: var(--fg-gradient, linear-gradient(135deg, #ff9a3d, #ff7d1f));
box-shadow: var(--fg-shadow, 0 6rpx 16rpx rgba(255, 125, 31, 0.35));
}
.line-sheet__btn-text {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
.line-sheet__btn-text--ghost {
color: #4e5969;
}
</style>