ow
This commit is contained in:
1 parent
282d8ce583
commit
28dd3ffebd
229 files changed
+5765
-4786
No files matched your search
@@ -3,13 +3,13 @@
|
||||
// 已登录的管理员在「我的」页输入被代理号(目标用户手机号)+ 自己的密码,LoginAgent 成功后覆盖当前会话
|
||||
// 后端 LoginAgent 先 Login(data) 用「当前登录者自己的手机号 + 密码」认证,故手机号预填为本人、只读,杜绝填错他人号造成不一致
|
||||
// 加密与登录同算法:RsaUserPassword = RSA( guid + MD5(密码) )
|
||||
// 视觉与登录页共用一套品牌语言:暖米白底 + 叶脉树 Logo + 黛翠绿渐变按钮 + 图标内嵌白底胶囊输入框
|
||||
// 视觉与登录页共用一套语言:主题渐变底 + 叶脉树 Logo + 主题色渐变按钮 + 图标内嵌白底胶囊输入框(强调色全部跟随当前主题)
|
||||
import { useLocale } from "uview-pro";
|
||||
import { ref } from "vue";
|
||||
import { computed, 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 { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import authService from "@/service/security/authService";
|
||||
@@ -17,6 +17,16 @@ import { useLoginProfileStore } from "@/stores";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/* 主题接线:底渐变与强调色跟随当前主题(与登录/注册页同口径),不再写死品牌绿 */
|
||||
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,
|
||||
}));
|
||||
|
||||
useAuthGuard();
|
||||
|
||||
const loginProfileStore = useLoginProfileStore();
|
||||
@@ -89,7 +99,7 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-page" :style="[{ background: themeBackground }, themeVars]">
|
||||
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
|
||||
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
|
||||
|
||||
@@ -141,12 +151,9 @@ async function handleSubmit() {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,与登录页同一套变量 */
|
||||
$brand-green: #2f7d5c;
|
||||
$brand-green-dark: #1f5f47;
|
||||
$brand-green-light: #47a678;
|
||||
$brand-amber: #c8912f;
|
||||
$brand-ink: #1f3d2f;
|
||||
/* 强调色全部经 --fg-* 变量跟随主题(见 script 的 themeVars);此处仅留中性文字色与柔和投影 */
|
||||
$ink: #1f2329;
|
||||
$soft-shadow: 0 6rpx 20rpx rgba(31, 35, 41, 0.06);
|
||||
|
||||
.login-page {
|
||||
position: relative;
|
||||
@@ -157,7 +164,6 @@ $brand-ink: #1f3d2f;
|
||||
min-height: 100vh;
|
||||
padding: 120rpx 60rpx 60rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
|
||||
}
|
||||
|
||||
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
|
||||
@@ -188,8 +194,8 @@ $brand-ink: #1f3d2f;
|
||||
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);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 36rpx var(--fg-shadow, rgba(31, 95, 71, 0.32));
|
||||
}
|
||||
|
||||
.login-brand__glyph {
|
||||
@@ -201,14 +207,14 @@ $brand-ink: #1f3d2f;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
color: $brand-ink;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
.login-brand__sub {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 6rpx;
|
||||
color: $brand-amber;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
|
||||
/* ===== 表单 ===== */
|
||||
@@ -227,14 +233,14 @@ $brand-ink: #1f3d2f;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 20rpx rgba(31, 95, 71, 0.06);
|
||||
box-shadow: $soft-shadow;
|
||||
}
|
||||
|
||||
.login-field__input {
|
||||
flex: 1;
|
||||
height: 92rpx;
|
||||
font-size: 30rpx;
|
||||
color: $brand-ink;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
/* 只读手机号:压掉 disabled 默认灰白,保留可读的中性灰,表明「锁定但正常」 */
|
||||
@@ -264,14 +270,14 @@ $brand-ink: #1f3d2f;
|
||||
|
||||
.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);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 32rpx var(--fg-shadow, rgba(31, 95, 71, 0.3));
|
||||
}
|
||||
|
||||
.login-btn--ghost {
|
||||
margin-top: 24rpx;
|
||||
color: $brand-green;
|
||||
border: 2rpx solid $brand-green;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
border: 2rpx solid var(--fg-primary, #2f7d5c);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ 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 { useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
|
||||
import { reloadLocales } from "@/locale";
|
||||
import { decodeQueryPath, isAuthFree, pageUrl, resolvePageName } from "@/router/routes";
|
||||
@@ -22,6 +23,17 @@ const showPwd = ref(false);
|
||||
const loading = ref(false);
|
||||
const loginProfileStore = useLoginProfileStore();
|
||||
|
||||
/* 主题接线:页面底渐变与强调色全部跟随当前主题(与「我的」/媒体/共享页同一口径),
|
||||
不再写死品牌绿——切主题后登录/注册/代理登录三页即时联动 */
|
||||
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,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 登录成功后的落地地址:被守卫送去登录页时由 ?redirect= 带过来(见 nav-guard 的 goLoginWithRedirect)。
|
||||
* 只接受 PAGE_PATHS 里认得出的地址,避免 ?redirect= 把人送到站外或不存在的路径
|
||||
@@ -117,7 +129,7 @@ async function handleLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-page" :style="[{ background: themeBackground }, themeVars]">
|
||||
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
|
||||
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
|
||||
|
||||
@@ -138,17 +150,13 @@ async function handleLogin() {
|
||||
<view class="login-form">
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -185,174 +193,171 @@ async function handleLogin() {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,登录页品牌身份独立于应用内主题 */
|
||||
$brand-green: #2f7d5c;
|
||||
$brand-green-dark: #1f5f47;
|
||||
$brand-green-light: #47a678;
|
||||
$brand-amber: #c8912f;
|
||||
$brand-ink: #1f3d2f;
|
||||
/* 强调色(底渐变/按钮/Logo/链接)全部经 --fg-* 变量跟随主题(见 script 的 themeVars);
|
||||
此处仅留中性文字色与柔和投影,不再写死品牌绿 */
|
||||
$ink: #1f2329;
|
||||
$soft-shadow: 0 6rpx 20rpx rgba(31, 35, 41, 0.06);
|
||||
|
||||
.login-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 150rpx 60rpx 60rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 150rpx 60rpx 60rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
|
||||
.login-vein {
|
||||
position: absolute;
|
||||
top: -40rpx;
|
||||
right: -60rpx;
|
||||
width: 460rpx;
|
||||
height: 460rpx;
|
||||
opacity: 0.08;
|
||||
pointer-events: none;
|
||||
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;
|
||||
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);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
margin-bottom: 28rpx;
|
||||
border-radius: 36rpx;
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 36rpx var(--fg-shadow, rgba(31, 95, 71, 0.32));
|
||||
}
|
||||
|
||||
.login-brand__glyph {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
}
|
||||
|
||||
.login-brand__name {
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
color: $brand-ink;
|
||||
font-size: 52rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
.login-brand__slogan {
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 6rpx;
|
||||
color: $brand-amber;
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 6rpx;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
|
||||
/* ===== 表单 ===== */
|
||||
.login-form {
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
width: 100%;
|
||||
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);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: $soft-shadow;
|
||||
}
|
||||
|
||||
.login-field__input {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $brand-ink;
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
.login-field__ph {
|
||||
color: #b3b8bd;
|
||||
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;
|
||||
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);
|
||||
color: #ffffff;
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 32rpx var(--fg-shadow, rgba(31, 95, 71, 0.3));
|
||||
}
|
||||
|
||||
/* ===== 底部 ===== */
|
||||
.login-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding-top: 60rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding-top: 60rpx;
|
||||
}
|
||||
|
||||
.login-lang {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.login-lang__label {
|
||||
color: #9aa0a6;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
.login-lang__switch {
|
||||
font-weight: 600;
|
||||
color: $brand-green;
|
||||
font-weight: 600;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
|
||||
.login-register {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.login-register__tip {
|
||||
color: #9aa0a6;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
.login-register__link {
|
||||
font-weight: 600;
|
||||
color: $brand-amber;
|
||||
font-weight: 600;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
|
||||
/* 非发布版的环境 / 后端地址角标:低调灰字,仅开发或 UAT 构建出现 */
|
||||
.login-env {
|
||||
margin-top: 22rpx;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 0.5rpx;
|
||||
color: #b0a894;
|
||||
margin-top: 22rpx;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 0.5rpx;
|
||||
color: #b0a894;
|
||||
}
|
||||
</style>
|
||||
@@ -2,17 +2,28 @@
|
||||
// 注册页:复刻旧系统 WebVue views/login/register.vue 的注册流程
|
||||
// 流程:GetRsaPublicKey -> RsaUserPassword = RSA( guid + MD5(密码) ) -> Register -> 回登录页
|
||||
// 与旧系统一致:RegisterData.UserPassword 字段放 guid 作防重放前缀,密文放 RsaUserPassword
|
||||
// 视觉与登录页共用一套品牌语言:暖米白底 + 叶脉树 Logo + 黛翠绿渐变按钮 + 图标内嵌白底胶囊输入框
|
||||
// 视觉与登录页共用一套语言:主题渐变底 + 叶脉树 Logo + 主题色渐变按钮 + 图标内嵌白底胶囊输入框(强调色全部跟随当前主题)
|
||||
import { useLocale } from "uview-pro";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { brandGlyph, veinGlyph } from "@/common/brand";
|
||||
import { encryptLoginPassword, makeGuid } from "@/common/pwd-crypto";
|
||||
import { useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { RegisterData as RegisterDataClass } from "@/data/auto/registerData";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import authService from "@/service/security/authService";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/* 主题接线:底渐变与强调色跟随当前主题(与登录页同口径),不再写死品牌绿 */
|
||||
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,
|
||||
}));
|
||||
|
||||
const mobile = ref("");
|
||||
const password = ref("");
|
||||
const confirm = ref("");
|
||||
@@ -83,7 +94,7 @@ async function handleRegister() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-page" :style="[{ background: themeBackground }, themeVars]">
|
||||
<!-- 背景右上淡叶脉暗纹(纯装饰,低透明) -->
|
||||
<image class="login-vein" :src="veinGlyph" mode="aspectFit" />
|
||||
|
||||
@@ -154,12 +165,9 @@ async function handleRegister() {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 品牌配色:黛翠绿主色 + 琥珀金点缀 + 暖米白底,与登录页同一套变量 */
|
||||
$brand-green: #2f7d5c;
|
||||
$brand-green-dark: #1f5f47;
|
||||
$brand-green-light: #47a678;
|
||||
$brand-amber: #c8912f;
|
||||
$brand-ink: #1f3d2f;
|
||||
/* 强调色全部经 --fg-* 变量跟随主题(见 script 的 themeVars);此处仅留中性文字色与柔和投影 */
|
||||
$ink: #1f2329;
|
||||
$soft-shadow: 0 6rpx 20rpx rgba(31, 35, 41, 0.06);
|
||||
|
||||
.login-page {
|
||||
position: relative;
|
||||
@@ -170,7 +178,6 @@ $brand-ink: #1f3d2f;
|
||||
min-height: 100vh;
|
||||
padding: 120rpx 60rpx 60rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, #f7f0e3 0%, #f1e7d6 55%, #eaddc7 100%);
|
||||
}
|
||||
|
||||
/* 右上淡叶脉暗纹:半隐若现,不拦截点击 */
|
||||
@@ -201,8 +208,8 @@ $brand-ink: #1f3d2f;
|
||||
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);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 36rpx var(--fg-shadow, rgba(31, 95, 71, 0.32));
|
||||
}
|
||||
|
||||
.login-brand__glyph {
|
||||
@@ -214,14 +221,14 @@ $brand-ink: #1f3d2f;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4rpx;
|
||||
color: $brand-ink;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
.login-brand__sub {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 6rpx;
|
||||
color: $brand-amber;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
|
||||
/* ===== 表单 ===== */
|
||||
@@ -240,14 +247,14 @@ $brand-ink: #1f3d2f;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 6rpx 20rpx rgba(31, 95, 71, 0.06);
|
||||
box-shadow: $soft-shadow;
|
||||
}
|
||||
|
||||
.login-field__input {
|
||||
flex: 1;
|
||||
height: 92rpx;
|
||||
font-size: 30rpx;
|
||||
color: $brand-ink;
|
||||
color: $ink;
|
||||
}
|
||||
|
||||
.login-field__ph {
|
||||
@@ -270,14 +277,14 @@ $brand-ink: #1f3d2f;
|
||||
|
||||
.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);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #47a678, #1f5f47));
|
||||
box-shadow: 0 16rpx 32rpx var(--fg-shadow, rgba(31, 95, 71, 0.3));
|
||||
}
|
||||
|
||||
.login-btn--ghost {
|
||||
margin-top: 24rpx;
|
||||
color: $brand-green;
|
||||
border: 2rpx solid $brand-green;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
border: 2rpx solid var(--fg-primary, #2f7d5c);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -304,6 +311,6 @@ $brand-ink: #1f3d2f;
|
||||
}
|
||||
|
||||
.login-agreement__link {
|
||||
color: $brand-green;
|
||||
color: var(--fg-primary, #2f7d5c);
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,11 @@
|
||||
* uploadFile 直传 DmsFile/UploadFile,FilePath 走请求头、form 字段名=原始文件名、
|
||||
* filename=分片三段式物理名并同存 AvatarFilename);
|
||||
* - 性别是 GenderPolicy 枚举,admin-form 无 select,用 #extra 行点开 option-picker 回写;
|
||||
* - 身份证上传:#extra 里的身份证块,chooseImage 单图 → 单通道上传同源后端 DmsFile/UploadIdCardFile
|
||||
* (FilePath 头=idcard 目录、form 字段 side=auto)→ 后端先落盘再 server-to-server 调同 docker 的
|
||||
* OCR 服务 /api/v1/idcard/recognize,把识别结果回填进返回的 FamilyPersonDisplayData(前端不直连 OCR,避免浏览器跨域);
|
||||
* 前端拿到返回后:常用名/性别/出生日期 回填各自真实列;民族/住址/身份证号/签发机关/有效期 表内无列、不落库,
|
||||
* 故不显示为表单字段,而是格式化写入「简介」(introduction 列)随保存落库;
|
||||
* - 只读门控:!IsCanAmend 且 Privacy!=='Y' 时只显「基本资料」卡、隐藏其余卡与修改/删除;
|
||||
* - 保存按 CreatedBy 有无决定 Update / Insert。
|
||||
* 旧页的新增入口在查询页即被注释,本页只承担 view/amend 两态(create 走快速建员弹层)。
|
||||
@@ -32,6 +37,7 @@ const readonly = ref(true);
|
||||
const routeId = ref("");
|
||||
const genderPickerVisible = ref(false);
|
||||
const uploading = ref(false);
|
||||
const idcardUploading = ref(false);
|
||||
|
||||
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
|
||||
const genderLabel = computed(() => getPolicyText("GenderPolicy", person.value.Gender));
|
||||
@@ -216,6 +222,167 @@ function onClearAvatar(): void {
|
||||
person.value.AvatarUrl = "";
|
||||
}
|
||||
|
||||
/** 身份证 OCR 性别文本("男"/"女"/"male"/"female")反查 GenderPolicy 存储键:
|
||||
* 先按 key、再按 label 精确,最后按含「男/女」模糊匹配,全落空返回空串(键由后端语言包决定,不写死) */
|
||||
function matchGenderKey(text: string): string {
|
||||
const value = text.trim();
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
const options = getPolicyOptions("GenderPolicy");
|
||||
const exact = options.find(o => o.key === value || o.label === value);
|
||||
if (exact) {
|
||||
return exact.key;
|
||||
}
|
||||
const lower = value.toLowerCase();
|
||||
const wantMale = value.includes("男") || lower.includes("male");
|
||||
const wantFemale = value.includes("女") || lower.includes("female");
|
||||
if (!wantMale && !wantFemale) {
|
||||
return "";
|
||||
}
|
||||
const hit = options.find((o) => {
|
||||
const label = `${o.key} ${o.label}`.toLowerCase();
|
||||
return (wantMale && (o.label.includes("男") || label.includes("male")))
|
||||
|| (wantFemale && (o.label.includes("女") || label.includes("female")));
|
||||
});
|
||||
return hit?.key ?? "";
|
||||
}
|
||||
|
||||
/** 出生日期归一为 yyyy-MM-dd(OCR 常给 "19791111",也兼容带分隔符);不足 8 位数字返回空串 */
|
||||
function formatBirthToYmd(raw: string): string {
|
||||
const digits = String(raw ?? "").replace(/\D/g, "");
|
||||
if (digits.length < 8) {
|
||||
return "";
|
||||
}
|
||||
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
/** 把后端 UploadIdCardFile 返回的识别信息(FamilyPersonDisplayData)回填到表单:
|
||||
* 常用名/性别/出生日期有对应 family_person 列,直接回填(性别回传原始文本按 GenderPolicy 反查键,出生归一 yyyy-MM-dd);
|
||||
* 民族/住址/身份证号/签发机关/有效期限 表里没有列、存不进库,故不作为表单字段显示,
|
||||
* 而是格式化追加进「简介」(introduction 是真实列,能落库)。字段值兼容 PascalCase 与 camelCase;有任一写入返回 true */
|
||||
function applyIdCardOcr(info: any): boolean {
|
||||
if (!info || typeof info !== "object") {
|
||||
return false;
|
||||
}
|
||||
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
|
||||
let filled = 0;
|
||||
|
||||
const name = String(pick("CommonName", "commonName") ?? "").trim();
|
||||
if (name) {
|
||||
person.value.CommonName = name;
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
const genderKey = matchGenderKey(String(pick("Gender", "gender") ?? ""));
|
||||
if (genderKey) {
|
||||
person.value.Gender = genderKey;
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
const ymd = formatBirthToYmd(String(pick("BirthDate", "birthDate") ?? ""));
|
||||
if (ymd) {
|
||||
(person.value as Record<string, any>).BirthDate = ymd;
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
if (upsertIdCardIntro(info)) {
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
return filled > 0;
|
||||
}
|
||||
|
||||
/** 把无列的身份信息(民族/住址/身份证号/签发机关/有效期限)格式化成一个「身份证识别信息」块写入简介:
|
||||
* 该块恒追加在简介末尾,重新识别时先截掉旧块再追加,避免重复堆叠;无任何可写项返回 false(不动简介) */
|
||||
function upsertIdCardIntro(info: any): boolean {
|
||||
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
|
||||
const labelMap: Array<[string, string, string]> = [
|
||||
["nation", "Nation", "nation"],
|
||||
["address", "Address", "address"],
|
||||
["idNumber", "IdNumber", "idNumber"],
|
||||
["issuer", "Issuer", "issuer"],
|
||||
["validity", "Validity", "validity"],
|
||||
];
|
||||
const lines: string[] = [];
|
||||
for (const [labelKey, pascal, camel] of labelMap) {
|
||||
const val = String(pick(pascal, camel) ?? "").trim();
|
||||
if (val) {
|
||||
lines.push(`${t(`FamilyPersonPage.${labelKey}`)}: ${val}`);
|
||||
}
|
||||
}
|
||||
if (!lines.length) {
|
||||
return false;
|
||||
}
|
||||
const title = t("FamilyPersonPage.idcardIntroTitle");
|
||||
const block = [title, ...lines].join("\n");
|
||||
let intro = String(person.value.Introduction ?? "");
|
||||
const idx = intro.indexOf(title);
|
||||
if (idx >= 0) {
|
||||
// 旧识别块恒在末尾:截掉旧块(含其后内容)后再追加新块,避免重复
|
||||
intro = intro.slice(0, idx).replace(/\s+$/, "");
|
||||
}
|
||||
person.value.Introduction = intro ? `${intro}\n\n${block}` : block;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 上传身份证:单图 → 同源后端 DmsFile/UploadIdCardFile(后端落盘 idcard 目录 + server-to-server 调 OCR)
|
||||
* → 用返回的识别信息回填字段;前端不直连 OCR,避免浏览器跨域 */
|
||||
async function onUploadIdCard(): Promise<void> {
|
||||
if (idcardUploading.value) {
|
||||
return;
|
||||
}
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: async (res: any) => {
|
||||
const filePath = String(res?.tempFilePaths?.[0] ?? "");
|
||||
const fileName = String(res?.tempFiles?.[0]?.name || `idcard-${Date.now()}.jpg`);
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
idcardUploading.value = true;
|
||||
// 单通道:本地预览先填,物理名(分片三段式)与头像 AvatarFilename 同口径
|
||||
const physicalName = buildChunkFileName(fileName);
|
||||
person.value.IdCardFilename = physicalName;
|
||||
person.value.IdCardUrl = filePath;
|
||||
// 上传到后端:FilePath 头=idcard 目录,side=auto;后端落盘后调 OCR 并把识别结果放进返回体
|
||||
const result = await uploadFile({
|
||||
url: "/DmsFile/UploadIdCardFile",
|
||||
filePath,
|
||||
name: fileName,
|
||||
fileName: physicalName,
|
||||
header: { FilePath: "idcard" },
|
||||
formData: { side: "auto" },
|
||||
});
|
||||
// 后端返回 FamilyPersonDisplayData,业务响应封装体在 Data/data;容错取内层
|
||||
const recognized = result?.Data ?? result?.data ?? result;
|
||||
if (applyIdCardOcr(recognized)) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardUploadedTip"), icon: "none" });
|
||||
}
|
||||
else {
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardNoResult"), icon: "none" });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardUploadFailed"), icon: "none" });
|
||||
}
|
||||
finally {
|
||||
idcardUploading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onClearIdCard(): void {
|
||||
if (person.value.IdCardUrl.startsWith("blob:") && typeof URL !== "undefined") {
|
||||
URL.revokeObjectURL(person.value.IdCardUrl);
|
||||
}
|
||||
person.value.IdCardFilename = "";
|
||||
person.value.IdCardUrl = "";
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
if (!String(person.value.Surname ?? "").trim()) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
|
||||
@@ -339,6 +506,32 @@ useAuthGuard(() => {
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 身份证上传块(编辑态可传:后端落盘 idcard 目录并 OCR 后回填身份字段) -->
|
||||
<view class="group">
|
||||
<view class="group-card idcard-card">
|
||||
<view class="idcard-head">
|
||||
<text class="idcard-title">{{ t("FamilyPersonPage.idcardTitle") }}</text>
|
||||
<text v-if="!ro && (person.IdCardUrl || person.IdCardFilename)" class="idcard-clear"
|
||||
@tap="onClearIdCard">{{ t("FamilyPersonPage.idcardClear") }}</text>
|
||||
</view>
|
||||
<view class="idcard-thumb" :class="{ 'idcard-thumb--tap': !ro }" @tap="!ro && onUploadIdCard()">
|
||||
<image v-if="person.IdCardUrl" class="idcard-thumb__img" :src="person.IdCardUrl"
|
||||
mode="aspectFill" />
|
||||
<view v-else class="idcard-thumb__empty">
|
||||
<u-icon name="camera" size="40" color="#c4c8cf" />
|
||||
<text class="idcard-thumb__empty-text">{{ t("FamilyPersonPage.idcardUpload") }}</text>
|
||||
</view>
|
||||
<view v-if="!ro" class="idcard-thumb__mask">
|
||||
<text v-if="idcardUploading" class="idcard-thumb__mask-text">
|
||||
{{ t("FamilyPersonPage.uploadingTip") }}
|
||||
</text>
|
||||
<u-icon v-else-if="person.IdCardUrl" name="camera" size="30" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="idcard-hint">{{ t("FamilyPersonPage.idcardHint") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
|
||||
@@ -460,4 +653,86 @@ useAuthGuard(() => {
|
||||
.enum-value-empty {
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
/* 身份证上传块(编辑态可点,识别后回填身份字段) */
|
||||
.idcard-card {
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.idcard-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.idcard-title {
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.idcard-clear {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.idcard-thumb {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
overflow: hidden;
|
||||
background-color: #f5f6f8;
|
||||
border: 2rpx dashed #d6d9de;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.idcard-thumb--tap:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.idcard-thumb__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.idcard-thumb__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.idcard-thumb__empty-text {
|
||||
font-size: 26rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.idcard-thumb__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.idcard-thumb__mask-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.idcard-hint {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user