ow
This commit is contained in:
1 parent
b5d2a2ede0
commit
aff58692bb
17 files changed
+1851
-240
No files matched your search
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 品牌字形:登录域各页(登录 / 注册)共用的叶脉树 Logo 与背景暗纹。
|
||||
* uview 图标集无树形,故用内联 SVG data URI(跨端安全,套路同 family-tree-home 的 treeGlyph)。
|
||||
*/
|
||||
|
||||
/** 品牌标:叶脉树字形,白色描边,置于黛翠绿圆角方块内 */
|
||||
export const brandGlyph = `data:image/svg+xml,${encodeURIComponent(
|
||||
[
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='#ffffff' stroke-width='2.4' stroke-linecap='round'>",
|
||||
"<path d='M24 42V9'/>",
|
||||
"<path d='M24 16 16 10'/><path d='M24 16 32 10'/>",
|
||||
"<path d='M24 24 15 18'/><path d='M24 24 33 18'/>",
|
||||
"<path d='M24 32 16 26'/><path d='M24 32 32 26'/>",
|
||||
"<circle cx='24' cy='7' r='2.4' fill='#ffffff' stroke='none'/>",
|
||||
"</svg>",
|
||||
].join(""),
|
||||
)}`;
|
||||
|
||||
/** 背景右上淡叶脉暗纹:主脉分叉造型,绿色低透明,纯装饰 */
|
||||
export const veinGlyph = `data:image/svg+xml,${encodeURIComponent(
|
||||
[
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='#2f7d5c' stroke-width='2' stroke-linecap='round'>",
|
||||
"<path d='M150 10C120 60 110 120 120 190'/>",
|
||||
"<path d='M138 45 100 30'/><path d='M132 75 88 62'/>",
|
||||
"<path d='M127 105 82 95'/><path d='M124 135 84 128'/>",
|
||||
"<path d='M138 45 168 60'/><path d='M132 75 165 92'/>",
|
||||
"<path d='M127 105 160 124'/><path d='M124 135 154 156'/>",
|
||||
"</svg>",
|
||||
].join(""),
|
||||
)}`;
|
||||
@@ -141,14 +141,23 @@ function doRefreshToken(): Promise<JwtInfo> {
|
||||
data,
|
||||
timeout,
|
||||
success: (res) => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
|
||||
const jwt = res.data as JwtInfo;
|
||||
setLoginJwt(jwt);
|
||||
resolve(jwt);
|
||||
// 后端「无效的令牌」以 HTTP 200 + 异常体返回(.NET ServiceErrorException),
|
||||
// 只查状态码会把异常对象当新令牌存下去——刷新"成功"但令牌仍是坏的,
|
||||
// 之后每个请求都 401→刷新→401 死循环,永远回不到登录页(异地登录踢人后实测根因)。
|
||||
// 故必须验体:非 2xx、异常体、缺 AccessToken 一律 reject(带 status 供 goLogin 清态跳转)
|
||||
const jwt = res.data as JwtInfo | null;
|
||||
const ok = res.statusCode >= 200 && res.statusCode < 300
|
||||
&& !!jwt
|
||||
&& typeof jwt === "object"
|
||||
&& !(jwt as any).SourceCode
|
||||
&& !!jwt.AccessToken;
|
||||
if (ok) {
|
||||
setLoginJwt(jwt as JwtInfo);
|
||||
resolve(jwt as JwtInfo);
|
||||
}
|
||||
else {
|
||||
// 带上 status:goLogin 据此区分「服务端拒绝」与「网络抖动」
|
||||
const err: any = new Error("刷新令牌失败");
|
||||
const err: any = new Error(extractErrorMessage(res.data) || "刷新令牌失败");
|
||||
err.status = res.statusCode;
|
||||
reject(err);
|
||||
}
|
||||
@@ -343,6 +352,12 @@ const httpInterceptor: RequestInterceptor = {
|
||||
goForbidden();
|
||||
handled = true; // 已跳 403 页并列出原因,边界不再重复提示
|
||||
}
|
||||
// 刷新并重试后仍 401:新令牌也不被接受(如服务端会话被吊销),
|
||||
// 再留在原地只会让每个请求都 401 循环,清态回登录页是唯一出路
|
||||
if (statusCode === 401 && config._retry) {
|
||||
goLogin(true);
|
||||
handled = true;
|
||||
}
|
||||
// 异常体优先:.NET 后端常以 500 + JSON 异常返回业务错误
|
||||
const bizMsg = detectBusinessException(rawData);
|
||||
if (bizMsg)
|
||||
@@ -376,16 +391,95 @@ export interface UploadOptions {
|
||||
filePath: string;
|
||||
/** 文件对应的 form-key,后端按此名接收(默认 'file') */
|
||||
name?: string;
|
||||
/** 显式指定 multipart filename(后端物理落盘名):缺省自动生成单片三段式 uni-时间戳-1-1.ext */
|
||||
fileName?: string;
|
||||
formData?: Record<string, any>;
|
||||
header?: Record<string, any>;
|
||||
}
|
||||
|
||||
/** 上传文件(uni.uploadFile 封装,自动携带 token,失败 reject 可感知) */
|
||||
/** 上传响应统一裁决(两通道共用):非 2xx / 业务异常都走「已通知」reject */
|
||||
function settleUpload(statusCode: number, rawData: any, resolve: (v: any) => void, reject: (e: any) => void): void {
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
const message = `上传失败[${statusCode}]`;
|
||||
notifyError("", message);
|
||||
reject(markErrorNotified(new Error(message)));
|
||||
return;
|
||||
}
|
||||
let data: any = rawData;
|
||||
try {
|
||||
data = typeof rawData === "string" ? JSON.parse(rawData) : rawData;
|
||||
}
|
||||
catch {
|
||||
/* 非 JSON 响应保持原样 */
|
||||
}
|
||||
const bizMsg = detectBusinessException(data);
|
||||
if (bizMsg)
|
||||
reject(markErrorNotified(new Error(bizMsg)));
|
||||
else resolve(data);
|
||||
}
|
||||
|
||||
/** 后端分片协议的 multipart filename:单片固定 `-1-1`,保留原扩展名(无扩展给 .ext 兜底)。
|
||||
* 页面需要把物理名存库时(如头像 AvatarFilename)也用它生成,保证与后端落盘名一致 */
|
||||
export function buildChunkFileName(fileName: string): string {
|
||||
const dot = fileName.lastIndexOf(".");
|
||||
const ext = dot > 0 ? fileName.slice(dot) : ".ext";
|
||||
return `uni-${Date.now()}-1-1${ext}`;
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
/** H5 上传通道:XHR + 手写 FormData(与文件页上传队列同一协议口径) */
|
||||
function xhrUploadFile(
|
||||
fullUrl: string,
|
||||
filePath: string,
|
||||
name: string,
|
||||
physicalName: string,
|
||||
formData: Record<string, any> | undefined,
|
||||
header: Record<string, any>,
|
||||
auth: string,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onFail = (): void => {
|
||||
notifyError("", "上传失败");
|
||||
reject(markErrorNotified(new Error("上传失败")));
|
||||
};
|
||||
// chooseImage 的 tempFilePath 是 blob URL,先取回 File 再按协议名拼包
|
||||
fetch(filePath)
|
||||
.then(r => r.blob())
|
||||
.then((blob) => {
|
||||
const form = new FormData();
|
||||
form.append(name, blob, physicalName);
|
||||
Object.entries(formData ?? {}).forEach(([k, v]) => form.append(k, String(v)));
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", fullUrl);
|
||||
xhr.timeout = timeout;
|
||||
if (auth) {
|
||||
xhr.setRequestHeader("Authorization", auth);
|
||||
}
|
||||
Object.entries(header).forEach(([k, v]) => xhr.setRequestHeader(k, String(v)));
|
||||
xhr.onload = () => settleUpload(xhr.status, xhr.responseText, resolve, reject);
|
||||
xhr.onerror = onFail;
|
||||
xhr.ontimeout = onFail;
|
||||
xhr.send(form);
|
||||
})
|
||||
.catch(onFail);
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
/** 上传文件(自动携带 token,失败 reject 可感知)。
|
||||
* 后端 PackUploadDmsFile 对 multipart filename 执行 Split('-') 取下标,要求 `uid-序号-总数.ext`
|
||||
* 三段式(单片 -1-1),否则抛「Index was outside the bounds of the array」;而 uni.uploadFile
|
||||
* 无法定制 filename,故 H5 改走 XHR + 手写 FormData(与文件页上传队列同款),App 端仍用 uni.uploadFile。 */
|
||||
export function uploadFile(options: UploadOptions): Promise<any> {
|
||||
const { url, filePath, name = "file", formData, header = {} } = options;
|
||||
const { url, filePath, name = "file", fileName, formData, header = {} } = options;
|
||||
const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url;
|
||||
const auth = bearerAuth();
|
||||
|
||||
// #ifdef H5
|
||||
return xhrUploadFile(fullUrl, filePath, name, fileName || buildChunkFileName(name), formData, header, auth);
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: fullUrl,
|
||||
@@ -395,23 +489,7 @@ export function uploadFile(options: UploadOptions): Promise<any> {
|
||||
timeout,
|
||||
header: auth ? { Authorization: auth, ...header } : header,
|
||||
success: (res) => {
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const message = `上传失败[${res.statusCode}]`;
|
||||
notifyError("", message);
|
||||
reject(markErrorNotified(new Error(message)));
|
||||
return;
|
||||
}
|
||||
let data: any = res.data;
|
||||
try {
|
||||
data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
|
||||
}
|
||||
catch {
|
||||
/* 非 JSON 响应保持原样 */
|
||||
}
|
||||
const bizMsg = detectBusinessException(data);
|
||||
if (bizMsg)
|
||||
reject(markErrorNotified(new Error(bizMsg)));
|
||||
else resolve(data);
|
||||
settleUpload(res.statusCode, res.data, resolve, reject);
|
||||
},
|
||||
fail: (err: any) => {
|
||||
notifyError(err.errMsg ?? "", "上传失败");
|
||||
@@ -419,6 +497,7 @@ export function uploadFile(options: UploadOptions): Promise<any> {
|
||||
},
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
export interface DownloadOptions {
|
||||
|
||||
@@ -10,6 +10,18 @@ export function md5Hex(text: string): string {
|
||||
return CryptoJS.MD5(text).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 v4 风格 Guid(无需引入 uuid 包,与旧系统 uuidv4() 等效)
|
||||
* 登录/注册/改密都要它作防重放前缀,统一放这里避免各页重复实现
|
||||
*/
|
||||
export function makeGuid(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 公钥加密(PKCS#1 v1.5),返回 base64 密文
|
||||
* @param plaintext 待加密明文
|
||||
|
||||
@@ -127,14 +127,19 @@ function onTapAction(action: AdminFormAction): void {
|
||||
|
||||
<scroll-view class="form-body" scroll-y :show-scrollbar="false">
|
||||
<view class="hero">
|
||||
<text class="hero-title">
|
||||
{{ heroTitle || EMPTY_CELL_TEXT }}
|
||||
</text>
|
||||
<view v-if="heroChips.length" class="hero-chips">
|
||||
<text v-for="chip in heroChips" :key="chip" class="hero-chip">
|
||||
{{ chip }}
|
||||
<!-- 头部左右侧插槽:如成员详情的头像(hero-side)与清除头像(hero-end),不传则不占位 -->
|
||||
<slot name="hero-side" :readonly="readonly" />
|
||||
<view class="hero-main">
|
||||
<text class="hero-title">
|
||||
{{ heroTitle || EMPTY_CELL_TEXT }}
|
||||
</text>
|
||||
<view v-if="heroChips.length" class="hero-chips">
|
||||
<text v-for="chip in heroChips" :key="chip" class="hero-chip">
|
||||
{{ chip }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<slot name="hero-end" :readonly="readonly" />
|
||||
</view>
|
||||
|
||||
<view v-for="section in sections" :key="section.title" class="group">
|
||||
@@ -211,6 +216,10 @@ function onTapAction(action: AdminFormAction): void {
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
color: #ffffff;
|
||||
@@ -219,6 +228,12 @@ function onTapAction(action: AdminFormAction): void {
|
||||
box-shadow: 0 8rpx 24rpx rgba(5, 150, 105, 0.22);
|
||||
}
|
||||
|
||||
/* 标题+chips 主列:吃掉剩余宽度,两侧插槽各自贴边 */
|
||||
.hero-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -113,9 +113,53 @@ async function onRetry(): Promise<void> {
|
||||
const data = ref<FamilyTreeData>(new FamilyTreeData());
|
||||
const tree = computed(() => data.value.FamilyTree);
|
||||
const currentPerson = computed(() => data.value.CurrentPerson?.FamilyPerson);
|
||||
/** 当前族员显示名(旧系统同款:姓+谱名连写) */
|
||||
const currentPersonName = computed(() => {
|
||||
const p = currentPerson.value;
|
||||
return p ? `${p.Surname ?? ""}${p.GenealogyName ?? ""}` : "";
|
||||
});
|
||||
const spouseMaps = computed(() => data.value.CurrentPerson?.FamilyTreeSpouseMaps ?? []);
|
||||
const childMaps = computed(() => data.value.CurrentPerson?.FamilyTreeChildrenMaps ?? []);
|
||||
|
||||
/** 关系行头像查表:Spouse/Children 视图字段不带头像,回查全族人员档案拿 AvatarUrl(谱系图 getAvatar 同款口径) */
|
||||
function personAvatar(personId?: string): string {
|
||||
if (!personId) {
|
||||
return "";
|
||||
}
|
||||
const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[];
|
||||
return persons.find(p => p?.PersonId === personId)?.AvatarUrl ?? "";
|
||||
}
|
||||
|
||||
/** 谱系图同款性别判定(family-tree-chart isMale 字面一致):空性别按男兜底 */
|
||||
function genderIsMale(gender?: string): boolean {
|
||||
if (!gender) {
|
||||
return true;
|
||||
}
|
||||
const label = getPolicyText("GenderPolicy", gender).toLowerCase();
|
||||
return label.includes("男") || label.includes("male") || gender === "M" || gender === "1";
|
||||
}
|
||||
|
||||
/** 关系对方性别回查:关系视图的 Gender 是记录拥有者(丈夫/父亲)的字段(谱系图配色踩过的坑),优先拿对方档案真值 */
|
||||
function personGender(personId?: string, fallback?: string): string {
|
||||
if (!personId) {
|
||||
return fallback ?? "";
|
||||
}
|
||||
const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[];
|
||||
return persons.find(p => p?.PersonId === personId)?.Gender || fallback || "";
|
||||
}
|
||||
|
||||
/** 谱系图同款性别配色:男蓝 / 女粉 */
|
||||
function genderTheme(gender?: string): string {
|
||||
return genderIsMale(gender) ? "#1890ff" : "#eb2f96";
|
||||
}
|
||||
|
||||
/** 无头像时的占位圆样式:白底 + 性别同色描边(谱系图同款);有头像返回空不覆盖 */
|
||||
function avatarFallbackStyle(hasAvatar: boolean, gender?: string): Record<string, string> {
|
||||
return hasAvatar
|
||||
? {}
|
||||
: { backgroundColor: "#ffffff", border: `3rpx solid ${genderTheme(gender)}` };
|
||||
}
|
||||
|
||||
const treeId = ref("");
|
||||
const loaded = ref(false);
|
||||
const isEdit = ref(false);
|
||||
@@ -704,10 +748,10 @@ defineExpose({
|
||||
</view>
|
||||
<view class="tree-page__scroll-wrap">
|
||||
<scroll-view scroll-y class="tree-page__scroll">
|
||||
<!-- 头卡(与媒体/共享页同套):白底 + 主题浅底图标 + 编号灰字独立行 -->
|
||||
<!-- 头卡(与媒体/共享页同套):白底 + 族谱树图标(与列表卡同款渐变底白树形) + 编号灰字独立行 -->
|
||||
<view class="detail-hero">
|
||||
<view class="detail-hero__avatar">
|
||||
<u-icon name="account-fill" size="64" :color="primary" />
|
||||
<image class="detail-hero__glyph" :src="treeGlyph" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="detail-hero__main">
|
||||
<text class="detail-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
||||
@@ -768,11 +812,13 @@ defineExpose({
|
||||
<textarea v-model="tree.Brief" class="detail-field__textarea" :disabled="!isEdit" :maxlength="1000"
|
||||
:placeholder="t('FamilyTreePage.noBrief')" />
|
||||
</view>
|
||||
<!-- 封面文件暂时不用,先隐藏(保留代码便于日后恢复)
|
||||
<view class="detail-field">
|
||||
<text class="detail-field__label">{{ t("FamilyTreePage.treeCover") }}</text>
|
||||
<input v-model="tree.TreeCoverFilename" class="detail-field__input" :disabled="!isEdit"
|
||||
:maxlength="255">
|
||||
</view>
|
||||
-->
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">{{ t("FamilyTreePage.publicIndc") }}</text>
|
||||
<u-switch :model-value="tree.PublicIndc === 'Y'" :disabled="!isEdit" :active-color="primary"
|
||||
@@ -795,17 +841,28 @@ defineExpose({
|
||||
<u-icon :name="folded.person ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.person">
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">{{ t("FamilyTreePage.gender") }}</text>
|
||||
<text class="detail-field__value">{{ genderText(currentPerson?.Gender) || "-" }}</text>
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">{{ t("FamilyTreePage.genealogyName") }}</text>
|
||||
<text class="detail-field__value">{{ currentPerson?.GenealogyName || "-" }}</text>
|
||||
</view>
|
||||
<view class="detail-field detail-field--row">
|
||||
<text class="detail-field__label">{{ t("FamilyTreePage.commonName") }}</text>
|
||||
<text class="detail-field__value">{{ currentPerson?.CommonName || "-" }}</text>
|
||||
<!-- 头像行(旧系统同款):圆头像 + 名字带性别 + 编号芯片与常用名,无头像时灰底人形占位 -->
|
||||
<view class="person-hero">
|
||||
<view class="person-hero__avatar"
|
||||
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
|
||||
<image v-if="currentPerson?.AvatarUrl" class="person-hero__img" :src="currentPerson.AvatarUrl"
|
||||
mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="56" :color="genderTheme(currentPerson?.Gender)" />
|
||||
</view>
|
||||
<view class="person-hero__main">
|
||||
<view class="person-hero__line">
|
||||
<text class="person-hero__name">{{ currentPersonName || "-" }}</text>
|
||||
<text class="person-hero__gender">{{ genderText(currentPerson?.Gender) || "-" }}</text>
|
||||
</view>
|
||||
<view class="person-hero__chips">
|
||||
<text v-if="currentPerson?.PersonId" class="person-hero__chip">
|
||||
<text v-if="currentPerson?.CommonName" class="person-hero__chip-name">
|
||||
{{ currentPerson.CommonName }}
|
||||
</text>
|
||||
{{ `${t("FamilyPersonPage.personId")}: ${currentPerson.PersonId}` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="op-btns">
|
||||
<view class="op-btn" @tap="pickCurrentPerson">
|
||||
@@ -853,9 +910,28 @@ defineExpose({
|
||||
</view>
|
||||
<view v-for="(record, index) in spouseMaps" :key="record.SpouseId" class="rel-item">
|
||||
<view class="rel-head" @tap="expandedSpouses[record.SpouseId] = !expandedSpouses[record.SpouseId]">
|
||||
<!-- 当前族员同款:圆头像 + 名字带性别 + 芯片(常用名值+成员编号) -->
|
||||
<view class="rel-avatar"
|
||||
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
|
||||
<image v-if="personAvatar(record.SpouseId)" class="rel-avatar__img"
|
||||
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40"
|
||||
:color="genderTheme(personGender(record.SpouseId, record.Gender))" />
|
||||
</view>
|
||||
<view class="rel-text">
|
||||
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
|
||||
<text class="rel-sub">{{ genderText(record.Gender) }}</text>
|
||||
<view class="person-hero__line">
|
||||
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
|
||||
<text class="person-hero__gender">{{ genderText(personGender(record.SpouseId, record.Gender))
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="person-hero__chips">
|
||||
<text class="person-hero__chip">
|
||||
<text v-if="record.SpouseCommonName" class="person-hero__chip-name">
|
||||
{{ record.SpouseCommonName }}
|
||||
</text>
|
||||
{{ `${t("FamilyPersonPage.personId")}: ${record.SpouseId}` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rel-ops">
|
||||
<template v-if="isEdit">
|
||||
@@ -971,9 +1047,28 @@ defineExpose({
|
||||
<view v-for="(record, index) in childMaps" :key="record.ChildrenId" class="rel-item">
|
||||
<view class="rel-head"
|
||||
@tap="expandedChildren[record.ChildrenId] = !expandedChildren[record.ChildrenId]">
|
||||
<!-- 当前族员同款:圆头像 + 名字带性别 + 芯片(常用名值+成员编号) -->
|
||||
<view class="rel-avatar"
|
||||
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
|
||||
<image v-if="personAvatar(record.ChildrenId)" class="rel-avatar__img"
|
||||
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40"
|
||||
:color="genderTheme(personGender(record.ChildrenId, record.Gender))" />
|
||||
</view>
|
||||
<view class="rel-text">
|
||||
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
|
||||
<text class="rel-sub">{{ genderText(record.Gender) }}</text>
|
||||
<view class="person-hero__line">
|
||||
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
|
||||
<text class="person-hero__gender">{{ genderText(personGender(record.ChildrenId, record.Gender))
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="person-hero__chips">
|
||||
<text class="person-hero__chip">
|
||||
<text v-if="record.ChildrenCommonName" class="person-hero__chip-name">
|
||||
{{ record.ChildrenCommonName }}
|
||||
</text>
|
||||
{{ `${t("FamilyPersonPage.personId")}: ${record.ChildrenId}` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rel-ops">
|
||||
<template v-if="isEdit">
|
||||
@@ -1035,7 +1130,10 @@ defineExpose({
|
||||
<u-icon :name="folded.perms ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
|
||||
</view>
|
||||
<view v-show="!folded.perms">
|
||||
<text class="perm-section-label">{{ t("FamilyTreePage.permPending") }}</text>
|
||||
<view class="perm-section-head">
|
||||
<text class="perm-section-label">{{ t("FamilyTreePage.permPending") }}</text>
|
||||
<text class="perm-section-count">{{ pendingPermissions.length }}</text>
|
||||
</view>
|
||||
<view v-if="!pendingPermissions.length" class="detail-empty">
|
||||
<text>{{ t("FamilyTreePage.permEmpty") }}</text>
|
||||
</view>
|
||||
@@ -1057,7 +1155,11 @@ defineExpose({
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="perm-section-label">{{ t("FamilyTreePage.permManager") }}</text>
|
||||
<view class="perm-divider" />
|
||||
<view class="perm-section-head">
|
||||
<text class="perm-section-label">{{ t("FamilyTreePage.permManager") }}</text>
|
||||
<text class="perm-section-count">{{ managerPermissions.length }}</text>
|
||||
</view>
|
||||
<view v-if="!managerPermissions.length" class="detail-empty">
|
||||
<text>{{ t("FamilyTreePage.permEmpty") }}</text>
|
||||
</view>
|
||||
@@ -1358,10 +1460,95 @@ defineExpose({
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 32rpx;
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 树形字形(与列表卡同款内联 SVG data-URI) */
|
||||
.detail-hero__glyph {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
}
|
||||
|
||||
/* 当前族员头像行(旧系统 hero 同款):圆头像 + 名字 + 编号芯片 */
|
||||
.person-hero {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
|
||||
/* 上距标题 24rpx;下距分隔线由 .op-btns 的 margin-top(24rpx) 提供,两侧视觉等距 */
|
||||
padding: 24rpx 0 0;
|
||||
}
|
||||
|
||||
.person-hero__avatar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
overflow: hidden;
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
border: 4rpx solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 14rpx rgba(31, 35, 41, 0.12);
|
||||
}
|
||||
|
||||
.person-hero__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.person-hero__main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.person-hero__line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.person-hero__name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.person-hero__gender {
|
||||
font-size: 26rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
.person-hero__chips {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.person-hero__chip {
|
||||
padding: 4rpx 18rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
/* 芯片内的常用名值:排在编号前,同芯片浅底、深色字(旧系统同款合并位) */
|
||||
.person-hero__chip-name {
|
||||
margin-right: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.detail-hero__main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -1594,12 +1781,33 @@ defineExpose({
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 关系行圆头像(当前族员同款缩小一号):档案无头像时灰底人形占位 */
|
||||
.rel-avatar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
overflow: hidden;
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.rel-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rel-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.rel-name {
|
||||
@@ -1680,11 +1888,39 @@ defineExpose({
|
||||
}
|
||||
|
||||
/* ===== 权限维护行 ===== */
|
||||
/* 子标题分组头:加深字色 + 加粗 + 计数徽章,让分区标题清晰可辨(对齐参照图) */
|
||||
.perm-section-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 28rpx 0 8rpx;
|
||||
}
|
||||
|
||||
/* 第二分区(管理名单)上方的通栏分隔带:负外边距 bleed 到卡片左右边,色带用主题软底把白卡横向"切开";
|
||||
加 border-radius 让色带两端收圆角,圆角处透出白卡底,两分区过渡更柔和 */
|
||||
.perm-divider {
|
||||
height: 16rpx;
|
||||
margin: 28rpx -28rpx 0;
|
||||
background-color: var(--fg-soft, #f6efe8);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.perm-section-label {
|
||||
display: block;
|
||||
padding: 24rpx 0 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.perm-section-count {
|
||||
min-width: 34rpx;
|
||||
padding: 0 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 34rpx;
|
||||
color: var(--fg-primary, #2b8fff);
|
||||
text-align: center;
|
||||
background-color: var(--fg-soft, #eaf4ff);
|
||||
border-radius: 17rpx;
|
||||
}
|
||||
|
||||
.perm-row {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"name": "en-US",
|
||||
"LoginForm": {
|
||||
"userLabel": "User ID:",
|
||||
"passwordLabel": "Password:",
|
||||
"userLabel": "Username",
|
||||
"passwordLabel": "Password",
|
||||
"loginBut": "Login",
|
||||
"cancelBut": "Cancel",
|
||||
"cultureLabel": "Language:"
|
||||
"cultureLabel": "Language: ",
|
||||
"registerTip": "No account yet? ",
|
||||
"registerBut": "Sign up now",
|
||||
"brandName": "Family Tree",
|
||||
"slogan": "One tree, generations of us",
|
||||
"langZh": "简体中文",
|
||||
"langEn": "English"
|
||||
},
|
||||
"TabBar": {
|
||||
"home": "Home",
|
||||
@@ -20,12 +25,69 @@
|
||||
"mySecurity": "Security",
|
||||
"familyTree": "Family Trees",
|
||||
"familyPerson": "Family Members",
|
||||
"loginAgent": "Agent Login",
|
||||
"changePassword": "Change Password",
|
||||
"changeLocalName": "Change Nickname",
|
||||
"logout": "Log Out",
|
||||
"logoutConfirmTitle": "Log Out",
|
||||
"logoutConfirmContent": "Are you sure you want to log out?",
|
||||
"logoutSuccess": "Logged out",
|
||||
"currentTheme": "Current Theme",
|
||||
"aboutTitle": "About",
|
||||
"appName": "App",
|
||||
"versionLabel": "Version",
|
||||
"platformLabel": "Platform"
|
||||
},
|
||||
"RegisterPage": {
|
||||
"title": "Register",
|
||||
"mobileLabel": "Mobile",
|
||||
"passwordLabel": "Password",
|
||||
"confirmLabel": "Confirm password",
|
||||
"registerBut": "Register",
|
||||
"loginBut": "Back to login",
|
||||
"mobileRequired": "Please enter your mobile",
|
||||
"passwordRequired": "Please enter your password",
|
||||
"confirmRequired": "Please confirm your password",
|
||||
"passwordMismatch": "The two passwords do not match",
|
||||
"success": "Registered, please sign in",
|
||||
"agreementPrefix": "By registering you agree to the ",
|
||||
"agreementAnd": " and ",
|
||||
"userAgreement": "User Agreement",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"policyTodo": "Agreement content coming soon"
|
||||
},
|
||||
"LoginAgentPage": {
|
||||
"title": "Agent Login",
|
||||
"agentLabel": "Agent user ID",
|
||||
"mobileLabel": "Mobile",
|
||||
"passwordLabel": "Password",
|
||||
"submitBut": "Agent login",
|
||||
"backBut": "Back",
|
||||
"agentRequired": "Please enter the agent user ID",
|
||||
"mobileRequired": "Please enter your mobile",
|
||||
"passwordRequired": "Please enter your password",
|
||||
"success": "Signed in as agent"
|
||||
},
|
||||
"ChangePasswordPage": {
|
||||
"title": "Change Password",
|
||||
"oldLabel": "Current password",
|
||||
"newLabel": "New password",
|
||||
"confirmLabel": "Confirm new password",
|
||||
"submitBut": "Confirm",
|
||||
"oldRequired": "Please enter your current password",
|
||||
"newRequired": "Please enter a new password",
|
||||
"confirmRequired": "Please confirm the new password",
|
||||
"passwordMismatch": "The two passwords do not match",
|
||||
"success": "Password changed"
|
||||
},
|
||||
"ChangeLocalNamePage": {
|
||||
"title": "Change Nickname",
|
||||
"nameLabel": "User name",
|
||||
"submitBut": "Confirm",
|
||||
"nameRequired": "Please enter a user name",
|
||||
"nameTooShort": "User name must be at least 2 characters",
|
||||
"success": "Updated, please log in again"
|
||||
},
|
||||
"ThemePage": {
|
||||
"title": "My Theme",
|
||||
"themeTitle": "Theme Color",
|
||||
@@ -164,8 +226,6 @@
|
||||
"introduction": "Biography",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"avatarTitle": "Avatar",
|
||||
"uploadAvatar": "Upload Avatar",
|
||||
"clearAvatar": "Clear Avatar",
|
||||
"uploadingTip": "Uploading…",
|
||||
"avatarUploadedTip": "Avatar uploaded",
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"name": "zh-CN",
|
||||
"LoginForm": {
|
||||
"userLabel": "用户名:",
|
||||
"passwordLabel": "密码:",
|
||||
"userLabel": "用户名",
|
||||
"passwordLabel": "密码",
|
||||
"loginBut": "登录",
|
||||
"cancelBut": "取消",
|
||||
"cultureLabel": "语言:"
|
||||
"cultureLabel": "语言:",
|
||||
"registerTip": "还没有账号?",
|
||||
"registerBut": "立即注册",
|
||||
"brandName": "有家族谱",
|
||||
"slogan": "一棵树,几代人",
|
||||
"langZh": "简体中文",
|
||||
"langEn": "English"
|
||||
},
|
||||
"TabBar": {
|
||||
"home": "首页",
|
||||
@@ -20,12 +25,69 @@
|
||||
"mySecurity": "安全管理",
|
||||
"familyTree": "族谱管理",
|
||||
"familyPerson": "家族成员",
|
||||
"loginAgent": "代理登录",
|
||||
"changePassword": "修改密码",
|
||||
"changeLocalName": "修改昵称",
|
||||
"logout": "退出登录",
|
||||
"logoutConfirmTitle": "退出登录",
|
||||
"logoutConfirmContent": "确定要退出当前账号吗?",
|
||||
"logoutSuccess": "已退出登录",
|
||||
"currentTheme": "当前主题",
|
||||
"aboutTitle": "关于",
|
||||
"appName": "应用名称",
|
||||
"versionLabel": "当前版本",
|
||||
"platformLabel": "运行平台"
|
||||
},
|
||||
"RegisterPage": {
|
||||
"title": "注册",
|
||||
"mobileLabel": "手机号",
|
||||
"passwordLabel": "密码",
|
||||
"confirmLabel": "确认密码",
|
||||
"registerBut": "注册",
|
||||
"loginBut": "返回登录",
|
||||
"mobileRequired": "请输入手机号",
|
||||
"passwordRequired": "请输入密码",
|
||||
"confirmRequired": "请再次输入密码",
|
||||
"passwordMismatch": "两次输入的密码不一致",
|
||||
"success": "注册成功,请登录",
|
||||
"agreementPrefix": "注册即代表同意",
|
||||
"agreementAnd": "和",
|
||||
"userAgreement": "《用户协议》",
|
||||
"privacyPolicy": "《隐私政策》",
|
||||
"policyTodo": "协议内容整理中,敬请期待"
|
||||
},
|
||||
"LoginAgentPage": {
|
||||
"title": "代理登录",
|
||||
"agentLabel": "被代理号",
|
||||
"mobileLabel": "手机号",
|
||||
"passwordLabel": "密码",
|
||||
"submitBut": "代理登录",
|
||||
"backBut": "返回",
|
||||
"agentRequired": "请输入被代理号",
|
||||
"mobileRequired": "请输入手机号",
|
||||
"passwordRequired": "请输入密码",
|
||||
"success": "已以代理身份登录"
|
||||
},
|
||||
"ChangePasswordPage": {
|
||||
"title": "修改密码",
|
||||
"oldLabel": "当前密码",
|
||||
"newLabel": "新密码",
|
||||
"confirmLabel": "确认新密码",
|
||||
"submitBut": "确认修改",
|
||||
"oldRequired": "请输入当前密码",
|
||||
"newRequired": "请输入新密码",
|
||||
"confirmRequired": "请再次输入新密码",
|
||||
"passwordMismatch": "两次输入的密码不一致",
|
||||
"success": "密码已修改"
|
||||
},
|
||||
"ChangeLocalNamePage": {
|
||||
"title": "修改昵称",
|
||||
"nameLabel": "用户名称",
|
||||
"submitBut": "确认修改",
|
||||
"nameRequired": "请输入用户名称",
|
||||
"nameTooShort": "用户名称不能少于两个字",
|
||||
"success": "修改成功,请重新登录"
|
||||
},
|
||||
"ThemePage": {
|
||||
"title": "我的主题",
|
||||
"themeTitle": "主题颜色",
|
||||
@@ -164,8 +226,6 @@
|
||||
"introduction": "生平简介",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"avatarTitle": "头像",
|
||||
"uploadAvatar": "上传头像",
|
||||
"clearAvatar": "清除头像",
|
||||
"uploadingTip": "上传中…",
|
||||
"avatarUploadedTip": "头像上传成功",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"base": "/"
|
||||
},
|
||||
"devServer": {
|
||||
"host": "0.0.0.0",
|
||||
"host": "::",
|
||||
"port": 5173
|
||||
},
|
||||
"darkmode": true,
|
||||
|
||||
@@ -10,6 +10,20 @@
|
||||
{
|
||||
"path": "pages/login/login"
|
||||
},
|
||||
// 注册(/register,免登录)/ 代理登录(/login-agent)/ 修改密码(/change-password)/ 修改昵称(/change-local-name):
|
||||
// 后三者从「我的」页跳入,均需登录
|
||||
{
|
||||
"path": "pages/login/register"
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login-agent"
|
||||
},
|
||||
{
|
||||
"path": "pages/login/change-password"
|
||||
},
|
||||
{
|
||||
"path": "pages/login/change-local-name"
|
||||
},
|
||||
// 错误页:H5 短地址由 CLEAN_PATH_OVERRIDES 指定为 /error/page40x,与旧 WebVue 地址一致
|
||||
{
|
||||
"path": "pages/error/page401"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,14 @@ import type { PermissionNames } from "@/permission/permission-kernel";
|
||||
|
||||
export const PAGE_PATHS = {
|
||||
login: "pages/login/login",
|
||||
// 注册(短地址 /register):免登录页,从登录页「注册」入口跳入
|
||||
register: "pages/login/register",
|
||||
// 代理登录(短地址 /login-agent):需登录,从「我的」页跳入,成功后覆盖当前会话
|
||||
loginAgent: "pages/login/login-agent",
|
||||
// 修改密码(短地址 /change-password):需登录,从「我的」页跳入,AmendPassword 成功后更新档案
|
||||
changePassword: "pages/login/change-password",
|
||||
// 修改昵称(短地址 /change-local-name):需登录,从「我的」页跳入,成功后按旧系统约定退出重登刷新快照
|
||||
changeLocalName: "pages/login/change-local-name",
|
||||
dashboard: "pages/dashboard/dashboard",
|
||||
error401: "pages/error/page401",
|
||||
error403: "pages/error/page403",
|
||||
@@ -93,6 +101,7 @@ const CLEAN_PATH_OVERRIDES: Partial<Record<PageName, string>> = {
|
||||
*/
|
||||
export const AUTH_FREE_PAGES: PageName[] = [
|
||||
"login",
|
||||
"register",
|
||||
"error401",
|
||||
"error403",
|
||||
"error404",
|
||||
@@ -114,14 +123,13 @@ export const PAGE_PERMISSIONS: Partial<Record<PageName, PermissionNames>> = {
|
||||
userEnquiry: "UserEnquiry",
|
||||
userOperation: "UserOperation",
|
||||
actorEnquiry: "SecurityActor",
|
||||
// 底部导航 5 个 tab:真实权限尚未定义,按当前要求统一用 UserOperation 占位。
|
||||
// 注意 dashboard 也被登记后,登录落地页(首页)会开始按 UserOperation 做路由级校验;
|
||||
// 路由级是 fail-open(档案缺失/为空时放行),真正的边界仍在后端。
|
||||
dashboard: "DmsFile",
|
||||
fileTab: "DmsFile",
|
||||
mediaTab: "DmsFile",
|
||||
shareTab: "DmsFile",
|
||||
mineTab: "DmsFile",
|
||||
// 首页与底部 5 个 tab(dashboard/fileTab/mediaTab/shareTab/mineTab)刻意不绑权限:
|
||||
// 落地页一旦被权限门卡住,等于整个 App 进不去且无路可退(403 页「返回首页」会再被弹回一次)。
|
||||
// 而登录档案是本地快照、可能滞后于后端(clearLoginState 只清 token 不清档案,又没有按 token
|
||||
// 重拉权限的接口),一份「非空但过期」的旧快照就能把首页判成 403;路由级 fail-open 只在档案
|
||||
// 完全为空时救场,救不了非空旧档案。故按 permission.md §3.1 原设计:首页/tab 不做路由级校验,
|
||||
// 真正的边界仍在后端逐接口 [PermissionOperate]。下面是 tab 内的深层子页,仍绑 DmsFile 占位——
|
||||
// 它们 403 只是「点了才回登录」,不锁入口,可接受。
|
||||
transport: "DmsFile",
|
||||
mediaPlay: "DmsFile",
|
||||
photoShow: "DmsFile",
|
||||
|
||||
Reference in new issue
Block a user