ow
This commit is contained in:
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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user