This commit is contained in:
owenchen committed 2026-10-17 13:22:10 +08:00
1 parent b5d2a2ede0
commit aff58692bb
17 files changed
+1851 -240

No files matched your search

@@ -0,0 +1,90 @@
<script setup lang="ts">
// 修改昵称页:复刻旧系统 WebVue views/login/changeLocalName.vue(旧为弹窗,本页改作「我的」跳入的独立页)
// 旧系统约定:昵称写在登录快照(LoginProfile.UserLocalName)里,改后无法即时刷新,
// 故 UpdateLocalName 成功后照旧退出登录并清档案,回登录页重登以拉取最新昵称
import { useLocale } from "uview-pro";
import { ref } from "vue";
import { useAuthGuard } from "@/composables";
import { pageUrl } from "@/router/routes";
import authService from "@/service/security/authService";
import securityUserService from "@/service/security/securityUserService";
import { useLoginProfileStore } from "@/stores";
const { t } = useLocale();
useAuthGuard();
const loginProfileStore = useLoginProfileStore();
// 预填当前昵称,便于在原值上微调
const localName = ref(loginProfileStore.userLocalName || "");
const loading = ref(false);
/** 退出登录并回登录页:改昵称 / 主动退出共用 */
async function doLogout(): Promise<void> {
try {
await authService.Logout();
}
catch {
// 后端退出失败也要清本地态,避免卡在已登录的假象
}
loginProfileStore.clearStorage();
uni.reLaunch({ url: pageUrl("login") });
}
async function handleSubmit() {
const name = localName.value.trim();
if (!name) {
uni.showToast({ title: t("ChangeLocalNamePage.nameRequired"), icon: "none" });
return;
}
if (name.length < 2) {
uni.showToast({ title: t("ChangeLocalNamePage.nameTooShort"), icon: "none" });
return;
}
loading.value = true;
try {
await securityUserService.UpdateLocalName(name);
uni.showToast({ title: t("ChangeLocalNamePage.success"), icon: "none" });
// 旧系统约定:改昵称后退出重登,刷新登录快照里的昵称
setTimeout(doLogout, 800);
}
catch {
// 错误提示已由 http.interceptor 统一处理
}
finally {
loading.value = false;
}
}
</script>
<template>
<app-page :nav-title="t('ChangeLocalNamePage.title')">
<view class="name-page">
<view class="name-card">
<u-input v-model="localName" :placeholder="t('ChangeLocalNamePage.nameLabel')" border clearable
maxlength="20" @confirm="handleSubmit" />
<u-button type="primary" shape="circle" :loading="loading" :text="t('ChangeLocalNamePage.submitBut')"
@click="handleSubmit" />
</view>
</view>
</app-page>
</template>
<style lang="scss" scoped>
.name-page {
box-sizing: border-box;
min-height: 100%;
padding: 40rpx 24rpx;
}
.name-card {
display: flex;
flex-direction: column;
gap: 40rpx;
padding: 48rpx 32rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(31, 35, 41, 0.06);
}
</style>
@@ -0,0 +1,108 @@
<script setup lang="ts">
// 修改密码页:复刻旧系统 WebVue views/login/changePassword.vue(旧为弹窗,本页改作「我的」跳入的独立页)
// 加密沿用旧系统约定:UserName 字段放 guid 作前缀,旧密文与新密文都用同一前缀
// RsaUserPassword = RSA( guid + MD5(旧密码) ),UserPassword = RSA( guid + MD5(新密码) )
// AmendPassword 成功后返回最新 LoginProfile,直接覆盖档案,保持登录态无需重登
import { useLocale } from "uview-pro";
import { ref } from "vue";
import { backendLocale } from "@/common/http.interceptor";
import { encryptLoginPassword, makeGuid } from "@/common/pwd-crypto";
import { useAuthGuard } from "@/composables";
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
import authService from "@/service/security/authService";
import { useLoginProfileStore } from "@/stores";
const { t } = useLocale();
useAuthGuard();
const loginProfileStore = useLoginProfileStore();
const oldPassword = ref("");
const newPassword = ref("");
const confirmNew = ref("");
const loading = ref(false);
async function handleSubmit() {
if (!oldPassword.value) {
uni.showToast({ title: t("ChangePasswordPage.oldRequired"), icon: "none" });
return;
}
if (!newPassword.value) {
uni.showToast({ title: t("ChangePasswordPage.newRequired"), icon: "none" });
return;
}
if (confirmNew.value !== newPassword.value) {
uni.showToast({ title: t("ChangePasswordPage.passwordMismatch"), icon: "none" });
return;
}
loading.value = true;
try {
const guid = makeGuid();
// 1. 取后端 RSA 公钥
const key = await authService.GetRsaPublicKey();
// 2. 旧、新密码分别用同一 guid 前缀加密
const rsaOld = encryptLoginPassword(oldPassword.value, guid, String(key));
const rsaNew = encryptLoginPassword(newPassword.value, guid, String(key));
if (!rsaOld || !rsaNew) {
uni.showToast({ title: "密码加密失败,请重试", icon: "none" });
return;
}
// 3. 修改密码(UserName 字段放 guid 作前缀,与旧系统一致)
const data = new LoginDataClass();
data.UserName = guid;
data.RsaUserPassword = rsaOld;
data.UserPassword = rsaNew;
data.Culture = backendLocale();
const profile = await authService.AmendPassword(data);
loginProfileStore.updateLoginProfile(profile);
uni.showToast({ title: t("ChangePasswordPage.success"), icon: "none" });
setTimeout(() => uni.navigateBack(), 800);
}
catch {
// 错误提示已由 http.interceptor 统一处理
}
finally {
loading.value = false;
}
}
</script>
<template>
<app-page :nav-title="t('ChangePasswordPage.title')">
<view class="pwd-page">
<view class="pwd-card">
<u-input v-model="oldPassword" type="password" :placeholder="t('ChangePasswordPage.oldLabel')" border
:password-icon="true" />
<u-input v-model="newPassword" type="password" :placeholder="t('ChangePasswordPage.newLabel')" border
:password-icon="true" />
<u-input v-model="confirmNew" type="password" :placeholder="t('ChangePasswordPage.confirmLabel')" border
:password-icon="true" @confirm="handleSubmit" />
<u-button type="primary" shape="circle" :loading="loading" :text="t('ChangePasswordPage.submitBut')"
@click="handleSubmit" />
</view>
</view>
</app-page>
</template>
<style lang="scss" scoped>
.pwd-page {
box-sizing: border-box;
min-height: 100%;
padding: 40rpx 24rpx;
}
.pwd-card {
display: flex;
flex-direction: column;
gap: 40rpx;
padding: 48rpx 32rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(31, 35, 41, 0.06);
}
</style>
@@ -0,0 +1,267 @@
<script setup lang="ts">
// 代理登录页:复刻旧系统 WebVue views/login/loginAgent.vue
// 已登录的管理员在「我的」页输入被代理号 + 对方手机号 + 密码,LoginAgent 成功后覆盖当前会话
// 加密与登录同算法:RsaUserPassword = RSA( guid + MD5(密码) )
// 视觉与登录页共用一套品牌语言:暖米白底 + 叶脉树 Logo + 黛翠绿渐变按钮 + 图标内嵌白底胶囊输入框
import { useLocale } from "uview-pro";
import { ref } from "vue";
import { brandGlyph, veinGlyph } from "@/common/brand";
import { backendLocale } from "@/common/http.interceptor";
import { encryptLoginPassword, makeGuid } from "@/common/pwd-crypto";
import { useAuthGuard } from "@/composables";
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
import { pageUrl } from "@/router/routes";
import authService from "@/service/security/authService";
import { useLoginProfileStore } from "@/stores";
const { t } = useLocale();
useAuthGuard();
const loginProfileStore = useLoginProfileStore();
const agentUserName = ref("");
const mobile = ref("");
const password = ref("");
const showPwd = ref(false);
const loading = ref(false);
/** 返回「我的」:代理登录从「我的」跳入,优先回退;深链直达无栈时重进「我的」 */
function goBack(): void {
if (getCurrentPages().length > 1) {
uni.navigateBack();
}
else {
uni.reLaunch({ url: pageUrl("mineTab") });
}
}
async function handleSubmit() {
if (!agentUserName.value) {
uni.showToast({ title: t("LoginAgentPage.agentRequired"), icon: "none" });
return;
}
if (!mobile.value) {
uni.showToast({ title: t("LoginAgentPage.mobileRequired"), icon: "none" });
return;
}
if (!password.value) {
uni.showToast({ title: t("LoginAgentPage.passwordRequired"), icon: "none" });
return;
}
loading.value = true;
try {
const guid = makeGuid();
// 1. 取后端 RSA 公钥
const key = await authService.GetRsaPublicKey();
// 2. RSA( guid + MD5(密码) )
const rsaUserPassword = encryptLoginPassword(password.value, guid, String(key));
if (!rsaUserPassword) {
uni.showToast({ title: "密码加密失败,请重试", icon: "none" });
return;
}
// 3. 代理登录(成功后覆盖当前会话档案与令牌)
const data = new LoginDataClass();
data.AgentUserName = agentUserName.value;
data.UserMobile = mobile.value;
data.Guid = guid;
data.RsaUserPassword = rsaUserPassword;
data.Culture = backendLocale();
const profile = await authService.LoginAgent(data);
loginProfileStore.updateLoginProfile(profile);
uni.showToast({ title: t("LoginAgentPage.success"), icon: "none" });
// reLaunch 清空页面栈,避免返回键退回代理登录页
setTimeout(() => uni.reLaunch({ url: pageUrl("dashboard") }), 800);
}
catch {
// 错误提示已由 http.interceptor 统一处理
}
finally {
loading.value = false;
}
}
</script>
<template>
<view class="login-page">
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
<!-- 品牌位:叶脉树 Logo + 名称 + 「代理登录」副标题 -->
<view class="login-brand">
<view class="login-brand__logo">
<image class="login-brand__glyph" :src="brandGlyph" mode="aspectFit" />
</view>
<view class="login-brand__name">
{{ t("LoginForm.brandName") }}
</view>
<view class="login-brand__sub">
{{ t("LoginAgentPage.title") }}
</view>
</view>
<!-- 表单:图标内嵌白底圆角胶囊 -->
<view class="login-form">
<view class="login-field">
<u-icon name="account" size="34" color="#9aa0a6" />
<input v-model="agentUserName" class="login-field__input" :placeholder="t('LoginAgentPage.agentLabel')"
placeholder-class="login-field__ph">
</view>
<view class="login-field">
<u-icon name="phone" size="34" color="#9aa0a6" />
<input v-model="mobile" class="login-field__input" type="number"
:placeholder="t('LoginAgentPage.mobileLabel')" placeholder-class="login-field__ph">
</view>
<view class="login-field">
<u-icon name="lock" size="34" color="#9aa0a6" />
<input v-model="password" class="login-field__input" :password="!showPwd"
:placeholder="t('LoginAgentPage.passwordLabel')" placeholder-class="login-field__ph"
@confirm="handleSubmit">
<u-icon :name="showPwd ? 'eye-fill' : 'eye-off'" size="34" color="#9aa0a6"
@click="showPwd = !showPwd" />
</view>
</view>
<!-- 按钮:代理登录黛翠绿渐变实心,返回白底绿描边 -->
<view class="login-btn login-btn--submit" @click="handleSubmit">
<u-loading v-if="loading" mode="circle" color="#ffffff" :size="34" />
<text v-else>{{ t("LoginAgentPage.submitBut") }}</text>
</view>
<view class="login-btn login-btn--ghost" @click="goBack">
<text>{{ t("LoginAgentPage.backBut") }}</text>
</view>
</view>
</template>
<style lang="scss" scoped>
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,与登录页同一套变量 */
$brand-green: #2f7d5c;
$brand-green-dark: #1f5f47;
$brand-green-light: #47a678;
$brand-amber: #c8912f;
$brand-ink: #1f3d2f;
.login-page {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
min-height: 100vh;
padding: 120rpx 60rpx 60rpx;
overflow: hidden;
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
}
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
.login-vein {
position: absolute;
top: -40rpx;
right: -60rpx;
width: 460rpx;
height: 460rpx;
opacity: 0.08;
pointer-events: none;
}
/* ===== 品牌位 ===== */
.login-brand {
display: flex;
z-index: 1;
flex-direction: column;
align-items: center;
margin-bottom: 70rpx;
}
.login-brand__logo {
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
border-radius: 34rpx;
background: linear-gradient(150deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 36rpx rgba(31, 95, 71, 0.32);
}
.login-brand__glyph {
width: 70rpx;
height: 70rpx;
}
.login-brand__name {
font-size: 48rpx;
font-weight: 700;
letter-spacing: 4rpx;
color: $brand-ink;
}
.login-brand__sub {
margin-top: 10rpx;
font-size: 26rpx;
letter-spacing: 6rpx;
color: $brand-amber;
}
/* ===== 表单 ===== */
.login-form {
display: flex;
z-index: 1;
flex-direction: column;
gap: 28rpx;
width: 100%;
}
.login-field {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 32rpx;
border-radius: 20rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(31, 95, 71, 0.06);
}
.login-field__input {
flex: 1;
height: 92rpx;
font-size: 30rpx;
color: $brand-ink;
}
.login-field__ph {
color: #b3b8bd;
}
/* ===== 按钮 ===== */
.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 92rpx;
margin-top: 32rpx;
border-radius: 46rpx;
font-size: 32rpx;
font-weight: 600;
letter-spacing: 8rpx;
}
.login-btn--submit {
color: #ffffff;
background: linear-gradient(135deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 32rpx rgba(31, 95, 71, 0.3);
}
.login-btn--ghost {
margin-top: 24rpx;
color: $brand-green;
border: 2rpx solid $brand-green;
background-color: #ffffff;
}
</style>
+207 -41
View File
@@ -7,6 +7,7 @@ import type { LoginProfile } from "@/data/auto/loginProfile";
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { brandGlyph, veinGlyph } from "@/common/brand";
import { backendLocale, baseUrl } from "@/common/http.interceptor";
import { encryptLoginPassword } from "@/common/pwd-crypto";
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
@@ -17,6 +18,7 @@ import { useLoginProfileStore } from "@/stores";
const userMobile = ref("");
const password = ref("");
const showPwd = ref(false);
const loading = ref(false);
const loginProfileStore = useLoginProfileStore();
@@ -44,6 +46,10 @@ onLoad((options) => {
// 登录页中英文切换:走 uView Pro 语言包(t / setLocale),语言包 name 为 zh-CN / en-US
const { t, setLocale, currentLocale } = useLocale();
const isZh = computed(() => (currentLocale.value?.name ?? "zh-CN") !== "en-US");
/** 非发布构建才显示「当前环境 + 实际调用的后端地址」:发布版 import.meta.env.PROD 为 true,整行编译期即被剔除 */
const isProd = import.meta.env.PROD;
const envMode = import.meta.env.MODE;
function toggleLang() {
setLocale(isZh.value ? "en-US" : "zh-CN");
// 开发构建顺手重拉数据库翻译:改完 DB 词条点一下切换就能看到新值,无需整页刷新
@@ -53,15 +59,11 @@ function toggleLang() {
}
}
/** 取消:清空输入 */
function handleCancel() {
userMobile.value = "";
password.value = "";
/** 进入注册页(/register,免登录):navigateTo 保留历史以便返回登录 */
function goRegister() {
uni.navigateTo({ url: pageUrl("register") });
}
// 开发构建显示「登录测试」标记,发布构建只显示后端地址(import.meta.env.DEV 编译期确定)
const isDev = import.meta.env.DEV;
/** 生成 v4 风格 Guid(无需引入 uuid 包,与旧系统 uuidv4() 等效) */
function makeGuid(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -116,77 +118,241 @@ async function handleLogin() {
<template>
<view class="login-page">
<view class="login-title">
有家相册
</view>
<view class="login-subtitle">
{{ isDev ? `登录测试 · ${baseUrl}` : baseUrl }}
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
<!-- 品牌位:叶脉树 Logo + 名称 + 标语 -->
<view class="login-brand">
<view class="login-brand__logo">
<image class="login-brand__glyph" :src="brandGlyph" mode="aspectFit" />
</view>
<view class="login-brand__name">
{{ t("LoginForm.brandName") }}
</view>
<view class="login-brand__slogan">
{{ t("LoginForm.slogan") }}
</view>
</view>
<!-- 表单:图标内嵌白底圆角胶囊 -->
<view class="login-form">
<u-input v-model="userMobile" :placeholder="t('LoginForm.userLabel')" border clearable />
<u-input
v-model="password" type="password" :placeholder="t('LoginForm.passwordLabel')" border :password-icon="true"
@confirm="handleLogin"
/>
<u-button type="primary" shape="circle" :loading="loading" :text="t('LoginForm.loginBut')" @click="handleLogin" />
<u-button shape="circle" :text="t('LoginForm.cancelBut')" @click="handleCancel" />
<view class="login-field">
<u-icon name="account" size="34" color="#9aa0a6" />
<input
v-model="userMobile" class="login-field__input" :placeholder="t('LoginForm.userLabel')"
placeholder-class="login-field__ph" @confirm="handleLogin"
>
</view>
<view class="login-field">
<u-icon name="lock" size="34" color="#9aa0a6" />
<input
v-model="password" class="login-field__input" :password="!showPwd" :placeholder="t('LoginForm.passwordLabel')"
placeholder-class="login-field__ph" @confirm="handleLogin"
>
<u-icon :name="showPwd ? 'eye-fill' : 'eye-off'" size="34" color="#9aa0a6" @click="showPwd = !showPwd" />
</view>
</view>
<view class="login-lang" @click="toggleLang">
<text class="login-lang__label">
{{ t('LoginForm.cultureLabel') }}
</text>
<text class="login-lang__switch">
{{ isZh ? 'English' : '中文' }}
</text>
<!-- 按钮:登录黛翠绿渐变实心 -->
<view class="login-btn login-btn--submit" @click="handleLogin">
<u-loading v-if="loading" mode="circle" color="#ffffff" :size="34" />
<text v-else>{{ t("LoginForm.loginBut") }}</text>
</view>
<!-- 底部:语言切换 + 注册入口(非发布版另显示当前环境与后端地址) -->
<view class="login-footer">
<view class="login-lang" @click="toggleLang">
<text class="login-lang__label">
{{ t("LoginForm.cultureLabel") }}
</text>
<text class="login-lang__switch">
{{ isZh ? t("LoginForm.langZh") : t("LoginForm.langEn") }}
</text>
</view>
<view class="login-register" @click="goRegister">
<text class="login-register__tip">
{{ t("LoginForm.registerTip") }}
</text>
<text class="login-register__link">
{{ t("LoginForm.registerBut") }}
</text>
</view>
<view v-if="!isProd" class="login-env">
{{ envMode }} · {{ baseUrl }}
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,登录页品牌身份独立于应用内主题 */
$brand-green: #2f7d5c;
$brand-green-dark: #1f5f47;
$brand-green-light: #47a678;
$brand-amber: #c8912f;
$brand-ink: #1f3d2f;
.login-page {
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 200rpx 60rpx 0;
background: linear-gradient(160deg, rgba(41, 121, 255, 0.08), rgba(25, 190, 107, 0.06));
box-sizing: border-box;
min-height: 100vh;
padding: 150rpx 60rpx 60rpx;
overflow: hidden;
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
}
.login-title {
font-size: 56rpx;
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
.login-vein {
position: absolute;
top: -40rpx;
right: -60rpx;
width: 460rpx;
height: 460rpx;
opacity: 0.08;
pointer-events: none;
}
/* ===== 品牌位 ===== */
.login-brand {
display: flex;
z-index: 1;
flex-direction: column;
align-items: center;
margin-bottom: 80rpx;
}
.login-brand__logo {
display: flex;
align-items: center;
justify-content: center;
width: 132rpx;
height: 132rpx;
margin-bottom: 28rpx;
border-radius: 36rpx;
background: linear-gradient(150deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 36rpx rgba(31, 95, 71, 0.32);
}
.login-brand__glyph {
width: 76rpx;
height: 76rpx;
}
.login-brand__name {
font-size: 52rpx;
font-weight: 700;
color: var(--u-type-primary);
letter-spacing: 4rpx;
color: $brand-ink;
}
.login-subtitle {
margin: 16rpx 0 80rpx;
font-size: 24rpx;
color: $u-tips-color;
.login-brand__slogan {
margin-top: 14rpx;
font-size: 26rpx;
letter-spacing: 6rpx;
color: $brand-amber;
}
/* ===== 表单 ===== */
.login-form {
display: flex;
z-index: 1;
flex-direction: column;
gap: 32rpx;
width: 100%;
}
.login-field {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 32rpx;
border-radius: 20rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(31, 95, 71, 0.06);
}
.login-field__input {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $brand-ink;
}
.login-field__ph {
color: #b3b8bd;
}
/* ===== 按钮 ===== */
.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-top: 40rpx;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 600;
letter-spacing: 8rpx;
}
.login-btn--submit {
color: #ffffff;
background: linear-gradient(135deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 32rpx rgba(31, 95, 71, 0.3);
}
/* ===== 底部 ===== */
.login-footer {
display: flex;
flex-direction: column;
gap: 40rpx;
align-items: center;
width: 100%;
margin-top: auto;
padding-top: 60rpx;
}
.login-lang {
margin-top: 48rpx;
display: flex;
align-items: center;
gap: 8rpx;
gap: 6rpx;
font-size: 26rpx;
}
.login-lang__label {
color: $u-tips-color;
color: #9aa0a6;
}
.login-lang__switch {
color: var(--u-type-primary);
font-weight: 600;
color: $brand-green;
}
.login-register {
display: flex;
align-items: center;
gap: 6rpx;
margin-top: 24rpx;
font-size: 26rpx;
}
.login-register__tip {
color: #9aa0a6;
}
.login-register__link {
font-weight: 600;
color: $brand-amber;
}
/* 非发布版的环境 / 后端地址角标:低调灰字,仅开发或 UAT 构建出现 */
.login-env {
margin-top: 22rpx;
font-size: 22rpx;
letter-spacing: 0.5rpx;
color: #b0a894;
}
</style>
@@ -0,0 +1,309 @@
<script setup lang="ts">
// 注册页:复刻旧系统 WebVue views/login/register.vue 的注册流程
// 流程:GetRsaPublicKey -> RsaUserPassword = RSA( guid + MD5(密码) ) -> Register -> 回登录页
// 与旧系统一致:RegisterData.UserPassword 字段放 guid 作防重放前缀,密文放 RsaUserPassword
// 视觉与登录页共用一套品牌语言:暖米白底 + 叶脉树 Logo + 黛翠绿渐变按钮 + 图标内嵌白底胶囊输入框
import { useLocale } from "uview-pro";
import { ref } from "vue";
import { brandGlyph, veinGlyph } from "@/common/brand";
import { encryptLoginPassword, makeGuid } from "@/common/pwd-crypto";
import { RegisterData as RegisterDataClass } from "@/data/auto/registerData";
import { pageUrl } from "@/router/routes";
import authService from "@/service/security/authService";
const { t } = useLocale();
const mobile = ref("");
const password = ref("");
const confirm = ref("");
const showPwd = ref(false);
const showConfirmPwd = ref(false);
const loading = ref(false);
/** 返回登录:注册从登录页跳入,优先回退;深链直达无栈时直接重进登录页 */
function goLogin() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
}
else {
uni.reLaunch({ url: pageUrl("login") });
}
}
/** 用户协议 / 隐私政策:暂无独立页面,先以提示占位 */
function openPolicy(): void {
uni.showToast({ title: t("RegisterPage.policyTodo"), icon: "none" });
}
async function handleRegister() {
if (!mobile.value) {
uni.showToast({ title: t("RegisterPage.mobileRequired"), icon: "none" });
return;
}
if (!password.value) {
uni.showToast({ title: t("RegisterPage.passwordRequired"), icon: "none" });
return;
}
if (confirm.value !== password.value) {
uni.showToast({ title: t("RegisterPage.passwordMismatch"), icon: "none" });
return;
}
loading.value = true;
try {
const guid = makeGuid();
// 1. 取后端 RSA 公钥(免鉴权)
const key = await authService.GetRsaPublicKey();
// 2. 密码先 MD5 再与 Guid 拼接后 RSA 加密(与登录同一算法)
const rsaUserPassword = encryptLoginPassword(password.value, guid, String(key));
if (!rsaUserPassword) {
uni.showToast({ title: "密码加密失败,请重试", icon: "none" });
return;
}
// 3. 注册(失败时拦截器已统一弹窗提示)
const data = new RegisterDataClass();
data.UserMobile = mobile.value;
data.UserPassword = guid; // 旧系统约定:此字段放 guid 作前缀
data.RsaUserPassword = rsaUserPassword;
data.ConfirmUserPassword = "";
await authService.Register(data);
uni.showToast({ title: t("RegisterPage.success"), icon: "none" });
setTimeout(goLogin, 800);
}
catch {
// 错误提示已由 http.interceptor 统一处理
}
finally {
loading.value = false;
}
}
</script>
<template>
<view class="login-page">
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
<!-- 品牌位:叶脉树 Logo + 名称 + 「注册」副标题 -->
<view class="login-brand">
<view class="login-brand__logo">
<image class="login-brand__glyph" :src="brandGlyph" mode="aspectFit" />
</view>
<view class="login-brand__name">
{{ t("LoginForm.brandName") }}
</view>
<view class="login-brand__sub">
{{ t("RegisterPage.title") }}
</view>
</view>
<!-- 表单:图标内嵌白底圆角胶囊 -->
<view class="login-form">
<view class="login-field">
<u-icon name="phone" size="34" color="#9aa0a6" />
<input v-model="mobile" class="login-field__input" type="number"
:placeholder="t('RegisterPage.mobileLabel')" placeholder-class="login-field__ph">
</view>
<view class="login-field">
<u-icon name="lock" size="34" color="#9aa0a6" />
<input v-model="password" class="login-field__input" :password="!showPwd"
:placeholder="t('RegisterPage.passwordLabel')" placeholder-class="login-field__ph">
<u-icon :name="showPwd ? 'eye-fill' : 'eye-off'" size="34" color="#9aa0a6"
@click="showPwd = !showPwd" />
</view>
<view class="login-field">
<u-icon name="checkmark-circle" size="34" color="#9aa0a6" />
<input v-model="confirm" class="login-field__input" :password="!showConfirmPwd"
:placeholder="t('RegisterPage.confirmLabel')" placeholder-class="login-field__ph"
@confirm="handleRegister">
<u-icon :name="showConfirmPwd ? 'eye-fill' : 'eye-off'" size="34" color="#9aa0a6"
@click="showConfirmPwd = !showConfirmPwd" />
</view>
</view>
<!-- 按钮:注册黛翠绿渐变实心,返回登录白底绿描边 -->
<view class="login-btn login-btn--submit" @click="handleRegister">
<u-loading v-if="loading" mode="circle" color="#ffffff" :size="34" />
<text v-else>{{ t("RegisterPage.registerBut") }}</text>
</view>
<view class="login-btn login-btn--ghost" @click="goLogin">
<text>{{ t("RegisterPage.loginBut") }}</text>
</view>
<!-- 底部:协议声明 -->
<view class="login-footer">
<view class="login-agreement">
<text class="login-agreement__tip">
{{ t("RegisterPage.agreementPrefix") }}
</text>
<text class="login-agreement__link" @click="openPolicy">
{{ t("RegisterPage.userAgreement") }}
</text>
<text class="login-agreement__tip">
{{ t("RegisterPage.agreementAnd") }}
</text>
<text class="login-agreement__link" @click="openPolicy">
{{ t("RegisterPage.privacyPolicy") }}
</text>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,与登录页同一套变量 */
$brand-green: #2f7d5c;
$brand-green-dark: #1f5f47;
$brand-green-light: #47a678;
$brand-amber: #c8912f;
$brand-ink: #1f3d2f;
.login-page {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
min-height: 100vh;
padding: 120rpx 60rpx 60rpx;
overflow: hidden;
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
}
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
.login-vein {
position: absolute;
top: -40rpx;
right: -60rpx;
width: 460rpx;
height: 460rpx;
opacity: 0.08;
pointer-events: none;
}
/* ===== 品牌位 ===== */
.login-brand {
display: flex;
z-index: 1;
flex-direction: column;
align-items: center;
margin-bottom: 70rpx;
}
.login-brand__logo {
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
border-radius: 34rpx;
background: linear-gradient(150deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 36rpx rgba(31, 95, 71, 0.32);
}
.login-brand__glyph {
width: 70rpx;
height: 70rpx;
}
.login-brand__name {
font-size: 48rpx;
font-weight: 700;
letter-spacing: 4rpx;
color: $brand-ink;
}
.login-brand__sub {
margin-top: 10rpx;
font-size: 26rpx;
letter-spacing: 6rpx;
color: $brand-amber;
}
/* ===== 表单 ===== */
.login-form {
display: flex;
z-index: 1;
flex-direction: column;
gap: 28rpx;
width: 100%;
}
.login-field {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 32rpx;
border-radius: 20rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(31, 95, 71, 0.06);
}
.login-field__input {
flex: 1;
height: 92rpx;
font-size: 30rpx;
color: $brand-ink;
}
.login-field__ph {
color: #b3b8bd;
}
/* ===== 按钮 ===== */
.login-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 92rpx;
margin-top: 32rpx;
border-radius: 46rpx;
font-size: 32rpx;
font-weight: 600;
letter-spacing: 8rpx;
}
.login-btn--submit {
color: #ffffff;
background: linear-gradient(135deg, $brand-green-light, $brand-green-dark);
box-shadow: 0 16rpx 32rpx rgba(31, 95, 71, 0.3);
}
.login-btn--ghost {
margin-top: 24rpx;
color: $brand-green;
border: 2rpx solid $brand-green;
background-color: #ffffff;
}
/* ===== 底部 ===== */
.login-footer {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: auto;
padding-top: 60rpx;
}
.login-agreement {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
font-size: 24rpx;
}
.login-agreement__tip {
color: #9aa0a6;
}
.login-agreement__link {
color: $brand-green;
}
</style>
@@ -5,7 +5,8 @@
* 对齐旧逻辑:
* - FindByKey(PersonId) 取 FamilyPersonData,主对象 FamilyPerson;
* - 头像上传 file-path=FamilyPersonAvatar(旧 FileUpload 的移动端等价:chooseImage 单图 +
* uploadFile 直传 DmsFile/UploadDmsFile,FilePath 走请求头、form 字段名=原始文件名);
* uploadFile 直传 DmsFile/UploadFile,FilePath 走请求头、form 字段名=原始文件名、
* filename=分片三段式物理名并同存 AvatarFilename);
* - 性别是 GenderPolicy 枚举,admin-form 无 select,用 #extra 行点开 option-picker 回写;
* - 只读门控:!IsCanAmend 且 Privacy!=='Y' 时只显「基本资料」卡、隐藏其余卡与修改/删除;
* - 保存按 CreatedBy 有无决定 Update / Insert。
@@ -15,7 +16,7 @@ import type { AdminFormAction, AdminFormField, AdminFormSection } from "@/admin/
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { uploadFile } from "@/common/http.interceptor";
import { buildChunkFileName, uploadFile } from "@/common/http.interceptor";
import { useAuthGuard } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonData } from "@/data/auto/familyPersonData";
@@ -166,7 +167,9 @@ function onGenderConfirm(key: string): void {
genderPickerVisible.value = false;
}
/** 头像上传:单图 -> DmsFile/UploadDmsFile(FilePath=FamilyPersonAvatar),成功后本地预览 */
/** 头像上传:单图 -> DmsFile/UploadFile(旧 FileUpload 默认通道,FilePath 头指定 FamilyPersonAvatar)。
* 后端按 {Backup}/{FilePath}/{ext}/{分片名} 落盘,FindByKey 的 AvatarUrl 也按此拼 URL,
* 故 AvatarFilename 必须存分片物理名(buildChunkFileName),不能存原始文件名 */
async function onUploadAvatar(): Promise<void> {
if (uploading.value) {
return;
@@ -181,13 +184,15 @@ async function onUploadAvatar(): Promise<void> {
}
try {
uploading.value = true;
const physicalName = buildChunkFileName(fileName);
await uploadFile({
url: "/DmsFile/UploadDmsFile",
url: "/DmsFile/UploadFile",
filePath,
name: fileName,
fileName: physicalName,
header: { FilePath: "FamilyPersonAvatar" },
});
person.value.AvatarFilename = fileName;
person.value.AvatarFilename = physicalName;
person.value.AvatarUrl = filePath;
uni.showToast({ title: t("FamilyPersonPage.avatarUploadedTip"), icon: "none" });
}
@@ -288,6 +293,26 @@ useAuthGuard(() => {
<admin-form :nav-title="t('FamilyPersonPage.detailTitle')" :show-nav-back="true" :hero-title="heroTitle"
:hero-chips="heroChips" :sections="sections" :model="person" :readonly="readonly" :actions="actions"
@back="onBack" @action="onAction" @field-change="onFieldChange">
<!-- 旧系统同款:头像就在 hero 卡左侧,显示与上传同一处;编辑态右侧出「清除头像」 -->
<template #hero-side="{ readonly: ro }">
<view class="hero-avatar" @tap="!ro && onUploadAvatar()">
<image v-if="person.AvatarUrl" class="hero-avatar__img" :src="person.AvatarUrl" mode="aspectFill" />
<u-icon v-else name="account" size="56" color="#ffffff" />
<view v-if="!ro" class="hero-avatar__mask">
<text v-if="uploading" class="hero-avatar__mask-text">
{{ t("FamilyPersonPage.uploadingTip") }}
</text>
<u-icon v-else name="camera" size="28" color="#ffffff" />
</view>
</view>
</template>
<template #hero-end="{ readonly: ro }">
<view v-if="!ro && (person.AvatarUrl || person.AvatarFilename)" class="hero-clear" @tap="onClearAvatar">
<text class="hero-clear-text">
{{ t("FamilyPersonPage.clearAvatar") }}
</text>
</view>
</template>
<template #extra="{ readonly: ro }">
<!-- 只读门控提示:无修改权限且对方未公开详细资料 -->
<view v-if="readonlyBasicOnly" class="ro-tip">
@@ -297,38 +322,7 @@ useAuthGuard(() => {
</view>
<template v-else>
<!-- 头像 + 性别(admin-form 无 select,枚举行在这里点开的 option-picker 里回写) -->
<view class="group">
<view class="group-head">
<text class="group-title">
{{ t("FamilyPersonPage.avatarTitle") }}
</text>
</view>
<view class="group-card avatar-card">
<view class="avatar-box" :class="{ 'avatar-box--tap': !ro }" @tap="!ro && onUploadAvatar()">
<image v-if="person.AvatarUrl" class="avatar-img" :src="person.AvatarUrl"
mode="aspectFill" />
<u-icon v-else name="account-fill" size="72" color="#c4c8cf" />
</view>
<view v-if="!ro" class="avatar-ops">
<view class="avatar-op" @tap="onUploadAvatar">
<u-icon name="camera" size="30" color="#059669" />
<text class="avatar-op-text">
{{ uploading ? t("FamilyPersonPage.uploadingTip") :
t("FamilyPersonPage.uploadAvatar") }}
</text>
</view>
<view v-if="person.AvatarUrl || person.AvatarFilename" class="avatar-op"
@tap="onClearAvatar">
<u-icon name="close-circle" size="30" color="#c4c8cf" />
<text class="avatar-op-text">
{{ t("FamilyPersonPage.clearAvatar") }}
</text>
</view>
</view>
</view>
</view>
<!-- 性别(admin-form 无 select,枚举行在这里点开的 option-picker 里回写) -->
<view class="group">
<view class="group-card">
<view class="enum-row" :class="{ 'enum-row--tap': !ro }"
@@ -388,51 +382,54 @@ useAuthGuard(() => {
border-radius: 20rpx;
}
.avatar-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 28rpx;
}
.avatar-box {
/* hero 左侧头像(旧系统 ft-hero-avatar 同款):显示与上传同一处,编辑态覆相机遮罩 */
.hero-avatar {
position: relative;
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
width: 120rpx;
height: 120rpx;
overflow: hidden;
background-color: $u-bg-color;
border: 2rpx solid $u-border-color;
background-color: rgba(255, 255, 255, 0.18);
border: 4rpx solid rgba(255, 255, 255, 0.55);
border-radius: 50%;
}
.avatar-box--tap {
border-style: dashed;
}
.avatar-img {
.hero-avatar__img {
width: 100%;
height: 100%;
}
.avatar-ops {
.hero-avatar__mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.avatar-op {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.35);
}
.avatar-op-text {
margin-left: 8rpx;
font-size: 26rpx;
color: $u-content-color;
.hero-avatar__mask-text {
font-size: 20rpx;
color: #ffffff;
}
/* hero 右侧「清除头像」白胶囊(旧系统同款位) */
.hero-clear {
flex: none;
padding: 10rpx 24rpx;
background-color: rgba(255, 255, 255, 0.22);
border-radius: 999rpx;
}
.hero-clear-text {
font-size: 24rpx;
color: #ffffff;
}
.enum-row {
+217 -57
View File
@@ -6,14 +6,38 @@
* 版本号取 common/constant 的 APP_INFO(与 manifest/package 对齐);H5 无 plus.runtime.version。
*/
import { useLocale } from "uview-pro";
import { computed } from "vue";
import { APP_INFO } from "@/common/constant";
import { useAuthGuard } from "@/composables";
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 } = usePermission();
const canSeeLoginAgent = computed(() => can("none", "SecurityUser", "LoginAgent"));
/* 统一风格:主题渐变底 + 主题色变量(与 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") });
@@ -28,63 +52,160 @@ function goSecurity(): void {
function goFamilyPerson(): void {
uni.navigateTo({ url: pageUrl("familyPersonEnquiry") });
}
/** 进入「代理登录」页(/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 {
uni.navigateTo({ url: pageUrl("changeLocalName") });
}
/** 退出登录:确认后调后端 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>
<app-page :nav-title="t('MinePage.title')" :show-nav-back="false" :show-tabbar="true">
<view class="mine-page">
<!-- 我的主题:进入主题配色 / 深色模式设置页(/theme) -->
<view class="mine-cell" @tap="goTheme">
<text class="mine-cell__title">
{{ t("MinePage.myTheme") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 安全管理:进入权限体系基础设定聚合页(/security) -->
<view class="mine-cell" @tap="goSecurity">
<text class="mine-cell__title">
{{ t("MinePage.mySecurity") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 家族成员:族人档案检索(/family-person-enquiry) -->
<view class="mine-cell" @tap="goFamilyPerson">
<text class="mine-cell__title">
{{ t("MinePage.familyPerson") }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</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") }}
<!-- 统一风格:隐藏旧 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>
<text class="mine-about__value">
{{ APP_INFO.name }}
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 安全管理:进入权限体系基础设定聚合页(/security) -->
<view 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>
<!-- 家族成员:族人档案检索(/family-person-enquiry) -->
<view class="mine-cell" @tap="goFamilyPerson">
<view class="mine-cell__icon">
<u-icon name="account-fill" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.familyPerson") }}
</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">
{{ t("MinePage.changeLocalName") }}
</text>
<u-icon 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-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 }}
<!-- 关于:应用名 + 当前版本 + 运行平台 -->
<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>
@@ -95,15 +216,25 @@ function goFamilyPerson(): void {
.mine-page {
display: flex;
flex-direction: column;
gap: 24rpx;
padding: 24rpx;
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 {
@@ -112,22 +243,51 @@ function goFamilyPerson(): void {
color: #1f2329;
}
/* 可点的列表行:标题左、箭头右,整行白底卡片 */
/* 可点的列表行:主题渐变图标块 + 标题左、箭头右,整行白底卡片 */
.mine-cell {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
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--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;