diff --git a/uniapp/youjiala/src/common/brand.ts b/uniapp/youjiala/src/common/brand.ts
new file mode 100644
index 0000000..e2681fa
--- /dev/null
+++ b/uniapp/youjiala/src/common/brand.ts
@@ -0,0 +1,30 @@
+/**
+ * 品牌字形:登录域各页(登录 / 注册)共用的叶脉树 Logo 与背景暗纹。
+ * uview 图标集无树形,故用内联 SVG data URI(跨端安全,套路同 family-tree-home 的 treeGlyph)。
+ */
+
+/** 品牌标:叶脉树字形,白色描边,置于黛翠绿圆角方块内 */
+export const brandGlyph = `data:image/svg+xml,${encodeURIComponent(
+ [
+ "",
+ ].join(""),
+)}`;
+
+/** 背景右上淡叶脉暗纹:主脉分叉造型,绿色低透明,纯装饰 */
+export const veinGlyph = `data:image/svg+xml,${encodeURIComponent(
+ [
+ "",
+ ].join(""),
+)}`;
diff --git a/uniapp/youjiala/src/common/http.interceptor.ts b/uniapp/youjiala/src/common/http.interceptor.ts
index 5330810..1d22e02 100644
--- a/uniapp/youjiala/src/common/http.interceptor.ts
+++ b/uniapp/youjiala/src/common/http.interceptor.ts
@@ -141,14 +141,23 @@ function doRefreshToken(): Promise {
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;
header?: Record;
}
-/** 上传文件(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 | undefined,
+ header: Record,
+ auth: string,
+): Promise {
+ 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 {
- 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 {
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 {
},
});
});
+ // #endif
}
export interface DownloadOptions {
diff --git a/uniapp/youjiala/src/common/pwd-crypto.ts b/uniapp/youjiala/src/common/pwd-crypto.ts
index c915b4f..33c1e01 100644
--- a/uniapp/youjiala/src/common/pwd-crypto.ts
+++ b/uniapp/youjiala/src/common/pwd-crypto.ts
@@ -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 待加密明文
diff --git a/uniapp/youjiala/src/components/admin-form/admin-form.vue b/uniapp/youjiala/src/components/admin-form/admin-form.vue
index 19d2572..e1c3201 100644
--- a/uniapp/youjiala/src/components/admin-form/admin-form.vue
+++ b/uniapp/youjiala/src/components/admin-form/admin-form.vue
@@ -127,14 +127,19 @@ function onTapAction(action: AdminFormAction): void {
-
- {{ heroTitle || EMPTY_CELL_TEXT }}
-
-
-
- {{ chip }}
+
+
+
+
+ {{ heroTitle || EMPTY_CELL_TEXT }}
+
+
+ {{ chip }}
+
+
+
@@ -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;
diff --git a/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue
index 1606619..ceab2db 100644
--- a/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue
+++ b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue
@@ -113,9 +113,53 @@ async function onRetry(): Promise {
const data = ref(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 {
+ 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({
-
+
-
+
{{ tree.TreeName || t("FamilyTreePage.unnamed") }}
@@ -768,11 +812,13 @@ defineExpose({
+
{{ t("FamilyTreePage.publicIndc") }}
-
- {{ t("FamilyTreePage.gender") }}
- {{ genderText(currentPerson?.Gender) || "-" }}
-
-
- {{ t("FamilyTreePage.genealogyName") }}
- {{ currentPerson?.GenealogyName || "-" }}
-
-
- {{ t("FamilyTreePage.commonName") }}
- {{ currentPerson?.CommonName || "-" }}
+
+
+
+
+
+
+
+
+ {{ currentPersonName || "-" }}
+ {{ genderText(currentPerson?.Gender) || "-" }}
+
+
+
+
+ {{ currentPerson.CommonName }}
+
+ {{ `${t("FamilyPersonPage.personId")}: ${currentPerson.PersonId}` }}
+
+
+
@@ -853,9 +910,28 @@ defineExpose({
+
+
+
+
+
- {{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}
- {{ genderText(record.Gender) }}
+
+ {{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}
+ {{ genderText(personGender(record.SpouseId, record.Gender))
+ }}
+
+
+
+
+ {{ record.SpouseCommonName }}
+
+ {{ `${t("FamilyPersonPage.personId")}: ${record.SpouseId}` }}
+
+
@@ -971,9 +1047,28 @@ defineExpose({
+
+
+
+
+
- {{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}
- {{ genderText(record.Gender) }}
+
+ {{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}
+ {{ genderText(personGender(record.ChildrenId, record.Gender))
+ }}
+
+
+
+
+ {{ record.ChildrenCommonName }}
+
+ {{ `${t("FamilyPersonPage.personId")}: ${record.ChildrenId}` }}
+
+
@@ -1035,7 +1130,10 @@ defineExpose({
- {{ t("FamilyTreePage.permPending") }}
+
+ {{ t("FamilyTreePage.permPending") }}
+ {{ pendingPermissions.length }}
+
{{ t("FamilyTreePage.permEmpty") }}
@@ -1057,7 +1155,11 @@ defineExpose({
- {{ t("FamilyTreePage.permManager") }}
+
+
+ {{ t("FamilyTreePage.permManager") }}
+ {{ managerPermissions.length }}
+
{{ t("FamilyTreePage.permEmpty") }}
@@ -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 {
diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json
index 1fcf8f4..a2208b6 100644
--- a/uniapp/youjiala/src/locale/lang/en-US.json
+++ b/uniapp/youjiala/src/locale/lang/en-US.json
@@ -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",
diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json
index 3261aec..d4c18e0 100644
--- a/uniapp/youjiala/src/locale/lang/zh-CN.json
+++ b/uniapp/youjiala/src/locale/lang/zh-CN.json
@@ -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": "头像上传成功",
diff --git a/uniapp/youjiala/src/manifest.json b/uniapp/youjiala/src/manifest.json
index a34affa..f2b3f7b 100644
--- a/uniapp/youjiala/src/manifest.json
+++ b/uniapp/youjiala/src/manifest.json
@@ -56,7 +56,7 @@
"base": "/"
},
"devServer": {
- "host": "0.0.0.0",
+ "host": "::",
"port": 5173
},
"darkmode": true,
diff --git a/uniapp/youjiala/src/pages.json b/uniapp/youjiala/src/pages.json
index 026e7fc..f3c76ba 100644
--- a/uniapp/youjiala/src/pages.json
+++ b/uniapp/youjiala/src/pages.json
@@ -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"
diff --git a/uniapp/youjiala/src/pages/login/change-local-name.vue b/uniapp/youjiala/src/pages/login/change-local-name.vue
new file mode 100644
index 0000000..be69c31
--- /dev/null
+++ b/uniapp/youjiala/src/pages/login/change-local-name.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/login/change-password.vue b/uniapp/youjiala/src/pages/login/change-password.vue
new file mode 100644
index 0000000..129e1c0
--- /dev/null
+++ b/uniapp/youjiala/src/pages/login/change-password.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/login/login-agent.vue b/uniapp/youjiala/src/pages/login/login-agent.vue
new file mode 100644
index 0000000..1c4375d
--- /dev/null
+++ b/uniapp/youjiala/src/pages/login/login-agent.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("LoginForm.brandName") }}
+
+
+ {{ t("LoginAgentPage.title") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("LoginAgentPage.submitBut") }}
+
+
+ {{ t("LoginAgentPage.backBut") }}
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/login/login.vue b/uniapp/youjiala/src/pages/login/login.vue
index 407db62..e743237 100644
--- a/uniapp/youjiala/src/pages/login/login.vue
+++ b/uniapp/youjiala/src/pages/login/login.vue
@@ -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() {
-
- 有家相册
-
-
- {{ isDev ? `登录测试 · ${baseUrl}` : baseUrl }}
+
+
+
+
+
+
+
+
+
+ {{ t("LoginForm.brandName") }}
+
+
+ {{ t("LoginForm.slogan") }}
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
- {{ t('LoginForm.cultureLabel') }}
-
-
- {{ isZh ? 'English' : '中文' }}
-
+
+
+
+ {{ t("LoginForm.loginBut") }}
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/login/register.vue b/uniapp/youjiala/src/pages/login/register.vue
new file mode 100644
index 0000000..d82069e
--- /dev/null
+++ b/uniapp/youjiala/src/pages/login/register.vue
@@ -0,0 +1,309 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("LoginForm.brandName") }}
+
+
+ {{ t("RegisterPage.title") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("RegisterPage.registerBut") }}
+
+
+ {{ t("RegisterPage.loginBut") }}
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/security/family-person-operation.vue b/uniapp/youjiala/src/pages/security/family-person-operation.vue
index 791c915..dfe1f75 100644
--- a/uniapp/youjiala/src/pages/security/family-person-operation.vue
+++ b/uniapp/youjiala/src/pages/security/family-person-operation.vue
@@ -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 {
if (uploading.value) {
return;
@@ -181,13 +184,15 @@ async function onUploadAvatar(): Promise {
}
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(() => {
+
+
+
+
+
+
+
+ {{ t("FamilyPersonPage.uploadingTip") }}
+
+
+
+
+
+
+
+
+ {{ t("FamilyPersonPage.clearAvatar") }}
+
+
+
@@ -297,38 +322,7 @@ useAuthGuard(() => {
-
-
-
-
- {{ t("FamilyPersonPage.avatarTitle") }}
-
-
-
-
-
-
-
-
-
-
-
- {{ uploading ? t("FamilyPersonPage.uploadingTip") :
- t("FamilyPersonPage.uploadAvatar") }}
-
-
-
-
-
- {{ t("FamilyPersonPage.clearAvatar") }}
-
-
-
-
-
-
+
{
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 {
diff --git a/uniapp/youjiala/src/pages/tab/mine.vue b/uniapp/youjiala/src/pages/tab/mine.vue
index d1b53c4..2255da7 100644
--- a/uniapp/youjiala/src/pages/tab/mine.vue
+++ b/uniapp/youjiala/src/pages/tab/mine.vue
@@ -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 {
+ const confirmed = await new Promise((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);
+}
-
-
-
-
-
- {{ t("MinePage.myTheme") }}
-
-
-
-
-
-
-
- {{ t("MinePage.mySecurity") }}
-
-
-
-
-
-
-
- {{ t("MinePage.familyPerson") }}
-
-
-
-
-
-
-
- {{ t("MinePage.aboutTitle") }}
-
-
-
- {{ t("MinePage.appName") }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.myTheme") }}
-
- {{ APP_INFO.name }}
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.mySecurity") }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.familyPerson") }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.loginAgent") }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.changePassword") }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.changeLocalName") }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.logout") }}
-
-
- {{ t("MinePage.versionLabel") }}
-
-
- v{{ APP_INFO.version }}
-
-
-
-
- {{ t("MinePage.platformLabel") }}
-
-
- {{ APP_INFO.platform }}
+
+
+
+
+ {{ t("MinePage.aboutTitle") }}
+
+
+ {{ t("MinePage.appName") }}
+
+
+ {{ APP_INFO.name }}
+
+
+
+
+ {{ t("MinePage.versionLabel") }}
+
+
+ v{{ APP_INFO.version }}
+
+
+
+
+ {{ t("MinePage.platformLabel") }}
+
+
+ {{ APP_INFO.platform }}
+
+
@@ -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;
diff --git a/uniapp/youjiala/src/router/routes.ts b/uniapp/youjiala/src/router/routes.ts
index 811de9a..10a4df6 100644
--- a/uniapp/youjiala/src/router/routes.ts
+++ b/uniapp/youjiala/src/router/routes.ts
@@ -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> = {
*/
export const AUTH_FREE_PAGES: PageName[] = [
"login",
+ "register",
"error401",
"error403",
"error404",
@@ -114,14 +123,13 @@ export const PAGE_PERMISSIONS: Partial> = {
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",