From 31de6c4cd66f0fc0c6197cd363ef5eb30b773751 Mon Sep 17 00:00:00 2001 From: owenchen Date: Fri, 23 Oct 2026 15:15:53 +0800 Subject: [PATCH] ow --- .../Entity/FamilyTreeViewEntity.auto.cs | 301 ++++----- .../Policy/UploadFolderPolicy.cs | 3 + .../InsertSecurityUserActorSubscriber.cs | 10 +- .../Policy/SecurityUserPolicy.cs | 1 + .../Data/Init-Furion-202610221752.sql | 3 +- uniapp/youjiala/src/common/apiEndpoint.ts | 53 +- .../youjiala/src/common/http.interceptor.ts | 19 +- .../endpoint-setting-popup.vue | 351 +++++++++++ .../family-tree-home/family-tree-home.vue | 569 +++++++++++------- .../src/data/auto/familyTreeViewEntity.ts | 5 +- .../src/data/auto/securityUserPolicy.ts | 1 + uniapp/youjiala/src/locale/lang/en-US.json | 14 +- uniapp/youjiala/src/locale/lang/zh-CN.json | 18 +- uniapp/youjiala/src/pages/login/login.vue | 33 +- uniapp/youjiala/src/pages/tab/mine.vue | 286 +-------- 15 files changed, 1001 insertions(+), 666 deletions(-) create mode 100644 uniapp/youjiala/src/components/endpoint-setting-popup/endpoint-setting-popup.vue diff --git a/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeViewEntity.auto.cs b/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeViewEntity.auto.cs index 6632a95..3c16bfd 100644 --- a/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeViewEntity.auto.cs +++ b/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeViewEntity.auto.cs @@ -4,175 +4,180 @@ using CloudBuilder.Core.DatabaseAccessor.Entity; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace CloudBuilder.Master.Entity - { +{ [TypeScript] [Table("family_tree_view")] public class FamilyTreeViewEntity : ViewEntityBase { - public FamilyTreeViewEntity() - { - } - - [Column("ancestor_person_id")] - public string AncestorPersonId { get; set; } - [Column("brief")] - public string? Brief { get; set; } - [Column("common_name")] - public string? CommonName { get; set; } - [Column("genealogy_name")] - public string? GenealogyName { get; set; } - [Column("public_indc")] - public string PublicIndc { get; set; } - [Column("surname")] - public string Surname { get; set; } - [Column("tree_id")] - public string TreeId { get; set; } - [Column("tree_name")] - public string TreeName { get; set; } - - public const string ANCESTOR_PERSON_ID = "AncestorPersonId"; - public const string BRIEF = "Brief"; - public const string COMMON_NAME = "CommonName"; - public const string GENEALOGY_NAME = "GenealogyName"; - public const string PUBLIC_INDC = "PublicIndc"; - public const string SURNAME = "Surname"; - public const string TREE_ID = "TreeId"; - public const string TREE_NAME = "TreeName"; - - public const string DB_NAME_FAMILY_TREE_VIEW = "family_tree_view"; - public const string DB_NAME_FIELDS="ancestor_person_id,brief,common_name,genealogy_name,public_indc,surname,tree_id,tree_name"; - - public const string DB_ANCESTOR_PERSON_ID = "ancestor_person_id"; - public const string DB_BRIEF = "brief"; - public const string DB_COMMON_NAME = "common_name"; - public const string DB_GENEALOGY_NAME = "genealogy_name"; - public const string DB_PUBLIC_INDC = "public_indc"; - public const string DB_SURNAME = "surname"; - public const string DB_TREE_ID = "tree_id"; - public const string DB_TREE_NAME = "tree_name"; - - private string ProviderName { get; set; } - - public static FamilyTreeViewEntity GetInstance(string providerName) - { - return new FamilyTreeViewEntity() { ProviderName = providerName }; - } - - public virtual void CopyFrom(FamilyTreeViewEntity source) - { - CopyFrom(source, true); - } - - public virtual void CopyFrom(FamilyTreeViewEntity source, bool includeSystemFields) - { - if (source == null) - { - throw new NullReferenceException("Source entity is null."); - } - - this.AncestorPersonId = source.AncestorPersonId; - this.Brief = source.Brief; - this.CommonName = source.CommonName; - this.GenealogyName = source.GenealogyName; - this.PublicIndc = source.PublicIndc; - this.Surname = source.Surname; - this.TreeId = source.TreeId; - this.TreeName = source.TreeName; - - if (includeSystemFields) + public FamilyTreeViewEntity() { } - } - public override string GetSelectFrom() - { - if (ProviderName.Contains("MySql")) - { - return GetSelectFromByMySql(); - } - else if (ProviderName.Contains("SqlServer")) - { - return GetSelectFromByBySqlServer(); - } - else - { - throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); - } - } - public string GetSelectFromByMySql() - { - return $" select {DB_NAME_FIELDS} from { DB_NAME_FAMILY_TREE_VIEW} with(nolock) "; - } - public string GetSelectFromByBySqlServer() - { - return $" select {DB_NAME_FIELDS} from { DB_NAME_FAMILY_TREE_VIEW} "; - } + [Column("ancestor_person_id")] + public string AncestorPersonId { get; set; } + [Column("brief")] + public string? Brief { get; set; } + [Column("common_name")] + public string? CommonName { get; set; } + [Column("genealogy_name")] + public string? GenealogyName { get; set; } + [Column("public_indc")] + public string PublicIndc { get; set; } + [Column("tree_cover_filename")] + public string? TreeCoverFilename { get; set; } + [Column("surname")] + public string Surname { get; set; } + [Column("tree_id")] + public string TreeId { get; set; } + [Column("tree_name")] + public string TreeName { get; set; } - public override string GetSelectCountFrom(string sqlWhere) - { - if (ProviderName.Contains("MySql")) - { - return GetSelectCountFromByMySql(sqlWhere); - } - else if (ProviderName.Contains("SqlServer")) - { - return GetSelectCountFromBySqlServer(sqlWhere); - } - else - { - throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); - } - } + public const string ANCESTOR_PERSON_ID = "AncestorPersonId"; + public const string BRIEF = "Brief"; + public const string COMMON_NAME = "CommonName"; + public const string GENEALOGY_NAME = "GenealogyName"; + public const string PUBLIC_INDC = "PublicIndc"; + public const string TREE_COVER_FILENAME = "TreeCoverFilename"; + public const string SURNAME = "Surname"; + public const string TREE_ID = "TreeId"; + public const string TREE_NAME = "TreeName"; - public string GetSelectCountFromByMySql(string sqlWhere) - { - return $" select count(*) from { DB_NAME_FAMILY_TREE_VIEW} where 1=1 {sqlWhere}"; - } - public string GetSelectCountFromBySqlServer(string sqlWhere) - { - return $" select count(*) from { DB_NAME_FAMILY_TREE_VIEW} with(nolock) where 1=1 {sqlWhere}"; - } + public const string DB_NAME_FAMILY_TREE_VIEW = "family_tree_view"; + public const string DB_NAME_FIELDS = "ancestor_person_id,brief,common_name,genealogy_name,public_indc,tree_cover_filename,surname,tree_id,tree_name"; - public override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere) - { - if (ProviderName.Contains("MySql")) + public const string DB_ANCESTOR_PERSON_ID = "ancestor_person_id"; + public const string DB_BRIEF = "brief"; + public const string DB_COMMON_NAME = "common_name"; + public const string DB_GENEALOGY_NAME = "genealogy_name"; + public const string DB_PUBLIC_INDC = "public_indc"; + public const string DB_TREE_COVER_FILENAME = "tree_cover_filename"; + public const string DB_SURNAME = "surname"; + public const string DB_TREE_ID = "tree_id"; + public const string DB_TREE_NAME = "tree_name"; + + private string ProviderName { get; set; } + + public static FamilyTreeViewEntity GetInstance(string providerName) { - return GetSelectTopFromByMySql(limit, start, orderby, orderbytag, sqlWhere); + return new FamilyTreeViewEntity() { ProviderName = providerName }; } - else if (ProviderName.Contains("SqlServer")) + + public virtual void CopyFrom(FamilyTreeViewEntity source) { - return GetSelectTopFromBySqlServer(limit, start, orderby, orderbytag, sqlWhere); + CopyFrom(source, true); } - else + + public virtual void CopyFrom(FamilyTreeViewEntity source, bool includeSystemFields) { - throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); + if (source == null) + { + throw new NullReferenceException("Source entity is null."); + } + + this.AncestorPersonId = source.AncestorPersonId; + this.Brief = source.Brief; + this.CommonName = source.CommonName; + this.GenealogyName = source.GenealogyName; + this.PublicIndc = source.PublicIndc; + this.TreeCoverFilename = source.TreeCoverFilename; + this.Surname = source.Surname; + this.TreeId = source.TreeId; + this.TreeName = source.TreeName; + + if (includeSystemFields) + { + } } - } - public string GetSelectTopFromByMySql(int limit, int start,string orderby,string orderbytag, string sqlWhere) - { - if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc"; - - if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_ANCESTOR_PERSON_ID ; int offset = limit * (start - 1); - return $" select {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_FAMILY_TREE_VIEW} where 1=1 {sqlWhere} ) a where num> {offset} LIMIT {limit} "; - } + public override string GetSelectFrom() + { + if (ProviderName.Contains("MySql")) + { + return GetSelectFromByMySql(); + } + else if (ProviderName.Contains("SqlServer")) + { + return GetSelectFromByBySqlServer(); + } + else + { + throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); + } + } + public string GetSelectFromByMySql() + { + return $" select {DB_NAME_FIELDS} from {DB_NAME_FAMILY_TREE_VIEW} with(nolock) "; + } + public string GetSelectFromByBySqlServer() + { + return $" select {DB_NAME_FIELDS} from {DB_NAME_FAMILY_TREE_VIEW} "; + } - public string GetSelectTopFromBySqlServer(int limit, int start,string orderby,string orderbytag, string sqlWhere) - { - if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc"; - - if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_ANCESTOR_PERSON_ID ; string top = limit > 0 ? $"top {limit}" : ""; - return $" select {top} {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_FAMILY_TREE_VIEW} with(nolock) where 1=1 {sqlWhere} ) a where num> {limit * (start - 1)}"; - } + public override string GetSelectCountFrom(string sqlWhere) + { + if (ProviderName.Contains("MySql")) + { + return GetSelectCountFromByMySql(sqlWhere); + } + else if (ProviderName.Contains("SqlServer")) + { + return GetSelectCountFromBySqlServer(sqlWhere); + } + else + { + throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); + } + } - public FamilyTreeViewEntity Clone() - { - FamilyTreeViewEntity result = new FamilyTreeViewEntity(); - result.CopyFrom(this); + public string GetSelectCountFromByMySql(string sqlWhere) + { + return $" select count(*) from {DB_NAME_FAMILY_TREE_VIEW} where 1=1 {sqlWhere}"; + } + public string GetSelectCountFromBySqlServer(string sqlWhere) + { + return $" select count(*) from {DB_NAME_FAMILY_TREE_VIEW} with(nolock) where 1=1 {sqlWhere}"; + } - return result; - } + public override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere) + { + if (ProviderName.Contains("MySql")) + { + return GetSelectTopFromByMySql(limit, start, orderby, orderbytag, sqlWhere); + } + else if (ProviderName.Contains("SqlServer")) + { + return GetSelectTopFromBySqlServer(limit, start, orderby, orderbytag, sqlWhere); + } + else + { + throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}"); + } + } + + public string GetSelectTopFromByMySql(int limit, int start, string orderby, string orderbytag, string sqlWhere) + { + if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc"; + + if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_ANCESTOR_PERSON_ID; int offset = limit * (start - 1); + return $" select {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from {DB_NAME_FAMILY_TREE_VIEW} where 1=1 {sqlWhere} ) a where num> {offset} LIMIT {limit} "; + } + + public string GetSelectTopFromBySqlServer(int limit, int start, string orderby, string orderbytag, string sqlWhere) + { + if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc"; + + if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_ANCESTOR_PERSON_ID; string top = limit > 0 ? $"top {limit}" : ""; + return $" select {top} {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from {DB_NAME_FAMILY_TREE_VIEW} with(nolock) where 1=1 {sqlWhere} ) a where num> {limit * (start - 1)}"; + } + + public FamilyTreeViewEntity Clone() + { + FamilyTreeViewEntity result = new FamilyTreeViewEntity(); + result.CopyFrom(this); + + return result; + } } } diff --git a/CloudBuilder/CloudBuilder.Master/Policy/UploadFolderPolicy.cs b/CloudBuilder/CloudBuilder.Master/Policy/UploadFolderPolicy.cs index 6ec697b..c93f635 100644 --- a/CloudBuilder/CloudBuilder.Master/Policy/UploadFolderPolicy.cs +++ b/CloudBuilder/CloudBuilder.Master/Policy/UploadFolderPolicy.cs @@ -24,5 +24,8 @@ namespace CloudBuilder.Master.Policy /// 文章正文/配图目录(article_filename.file_name 对应,前端上传 FilePath 头须一致) public const string ARTICLE_IMAGE = "ArticleImage"; + + /// 族谱封面目录(family_tree.tree_cover_filename 对应,前端上传 FilePath 头须一致) + public const string TREE_COVER = "TreeCover"; } } diff --git a/CloudBuilder/CloudBuilder.Security.Service/Subscriber/InsertSecurityUserActorSubscriber.cs b/CloudBuilder/CloudBuilder.Security.Service/Subscriber/InsertSecurityUserActorSubscriber.cs index 6251a58..b55f4eb 100644 --- a/CloudBuilder/CloudBuilder.Security.Service/Subscriber/InsertSecurityUserActorSubscriber.cs +++ b/CloudBuilder/CloudBuilder.Security.Service/Subscriber/InsertSecurityUserActorSubscriber.cs @@ -22,8 +22,16 @@ namespace CloudBuilder.Security.Service.Subscriber SecurityUserActorEntity ent = new SecurityUserActorEntity(); ent.UserName = ((SecurityUserEntity)eventData.EventSource).UserName; ent.ActorName = SecurityUserPolicy.SECURITY_USER_BASE_ACTOR; - repository.InsertNow(ent); + + //第一个注册用户就是管理员 + if (ent.UserName == SecurityPrefixPolicy.SECURITY_USER_FORMAT.Substring(0, SecurityPrefixPolicy.SECURITY_USER_FORMAT.Length - 1) + "1") + { + ent = new SecurityUserActorEntity(); + ent.UserName = ((SecurityUserEntity)eventData.EventSource).UserName; + ent.ActorName = SecurityUserPolicy.SYSTEM_ACTOR; + repository.InsertNow(ent); + } } } } diff --git a/CloudBuilder/CloudBuilder.Security/Policy/SecurityUserPolicy.cs b/CloudBuilder/CloudBuilder.Security/Policy/SecurityUserPolicy.cs index 7c050f4..43586dd 100644 --- a/CloudBuilder/CloudBuilder.Security/Policy/SecurityUserPolicy.cs +++ b/CloudBuilder/CloudBuilder.Security/Policy/SecurityUserPolicy.cs @@ -14,6 +14,7 @@ namespace CloudBuilder.Security.Policy /// 普通用户首次注册给予的角色 /// public const string SECURITY_USER_BASE_ACTOR = "SecurityUserBaseActor"; + public const string SYSTEM_ACTOR = "system_actor"; /// /// 用户登录可以出错的次数 diff --git a/CloudBuilder/Data/Init-Furion-202610221752.sql b/CloudBuilder/Data/Init-Furion-202610221752.sql index 18fc59b..db1d74c 100644 --- a/CloudBuilder/Data/Init-Furion-202610221752.sql +++ b/CloudBuilder/Data/Init-Furion-202610221752.sql @@ -1017,6 +1017,7 @@ SET @saved_cs_client = @@character_set_client; 1 AS `tree_id`, 1 AS `public_indc`, 1 AS `brief`, + 1 AS `tree_cover_filename`, 1 AS `ancestor_person_id`, 1 AS `surname`, 1 AS `genealogy_name`, @@ -2601,7 +2602,7 @@ SET character_set_client = @saved_cs_client; /*!50001 SET collation_connection = utf8mb4_0900_ai_ci */; /*!50001 CREATE ALGORITHM=UNDEFINED */ /*!50013 SQL SECURITY DEFINER */ -/*!50001 VIEW `family_tree_view` AS select `tree`.`tree_name` AS `tree_name`,`tree`.`tree_id` AS `tree_id`,`tree`.`public_indc` AS `public_indc`,`tree`.`brief` AS `brief`,`tree`.`ancestor_person_id` AS `ancestor_person_id`,`p`.`surname` AS `surname`,`p`.`genealogy_name` AS `genealogy_name`,`p`.`common_name` AS `common_name` from (`family_tree` `tree` join `family_person` `p` on((`p`.`person_id` = `tree`.`ancestor_person_id`))) */; +/*!50001 VIEW `family_tree_view` AS select `tree`.`tree_name` AS `tree_name`,`tree`.`tree_id` AS `tree_id`,`tree`.`public_indc` AS `public_indc`,`tree`.`brief` AS `brief`,`tree`.`tree_cover_filename` AS `tree_cover_filename`,`tree`.`ancestor_person_id` AS `ancestor_person_id`,`p`.`surname` AS `surname`,`p`.`genealogy_name` AS `genealogy_name`,`p`.`common_name` AS `common_name` from (`family_tree` `tree` join `family_person` `p` on((`p`.`person_id` = `tree`.`ancestor_person_id`))) */; /*!50001 SET character_set_client = @saved_cs_client */; /*!50001 SET character_set_results = @saved_cs_results */; /*!50001 SET collation_connection = @saved_col_connection */; diff --git a/uniapp/youjiala/src/common/apiEndpoint.ts b/uniapp/youjiala/src/common/apiEndpoint.ts index 392d664..b157d4a 100644 --- a/uniapp/youjiala/src/common/apiEndpoint.ts +++ b/uniapp/youjiala/src/common/apiEndpoint.ts @@ -17,9 +17,13 @@ import { WebSitePolicy } from "@/policy/custom/webSitePolicy"; const STORAGE_KEY = "api-endpoint-setting"; -/** 线路设置:mode=wan 走公网(默认);lan 走内网直连(lanUrl 必须是合法 http(s) 地址) */ +/** 线路设置:mode=wan 走公网、lan 走内网直连;两条线路的地址都可自定义。 + * wanUrl 留空表示沿用内置默认公网地址(WebSitePolicy),填了则覆盖;lanUrl 在 lan 模式下必填合法。 */ export interface EndpointSetting { mode: "wan" | "lan"; + /** 自定义公网地址(可空=用内置默认) */ + wanUrl: string; + /** 内网直连地址(lan 模式必填) */ lanUrl: string; } @@ -33,15 +37,20 @@ const WAN_BASE_URL = import.meta.env.DEV ? devBaseUrl : WebSitePolicy.WebApiRoot; -/** 规范化内网地址:允许省略 http://,去掉末尾斜杠;非法返回空串(调用方据此判无效) */ -export function normalizeLanUrl(raw: string): string { +/** 规范化地址:省略协议时补默认方案(内网 http、公网 https),去末尾斜杠;非法返回空串(调用方据此判无效) */ +export function normalizeEndpointUrl( + raw: string, + defaultScheme: "http" | "https" = "http", +): string { const text = String(raw ?? "") .trim() .replace(/\/+$/, ""); if (!text) { return ""; } - const withScheme = /^https?:\/\//i.test(text) ? text : `http://${text}`; + const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(text) + ? text + : `${defaultScheme}://${text}`; return /^https?:\/\/[^\s/]+$/i.test(withScheme) ? withScheme : ""; } @@ -51,14 +60,19 @@ function readSetting(): EndpointSetting { | Partial | ""; if (raw && typeof raw === "object") { - const lanUrl = normalizeLanUrl(String(raw.lanUrl ?? "")); + const wanUrl = normalizeEndpointUrl(String(raw.wanUrl ?? ""), "https"); + const lanUrl = normalizeEndpointUrl(String(raw.lanUrl ?? ""), "http"); // 存储里有脏数据(lan 模式但地址非法)时按公网兜底,避免全站请求打到空地址 - return { mode: raw.mode === "lan" && lanUrl ? "lan" : "wan", lanUrl }; + return { + mode: raw.mode === "lan" && lanUrl ? "lan" : "wan", + wanUrl, + lanUrl, + }; } } catch { // 存储读取失败按默认公网处理,不阻塞启动 } - return { mode: "wan", lanUrl: "" }; + return { mode: "wan", wanUrl: "", lanUrl: "" }; } // 模块级缓存:请求层高频读地址,不每次都走同步 storage @@ -69,16 +83,26 @@ export function currentEndpoint(): EndpointSetting { return { ...cached }; } -/** 当前生效的后端地址:内网模式且地址合法时返回内网地址,否则公网 */ +/** 内置默认公网地址(未自定义公网时沿用;供设置页展示占位) */ +export function defaultWanUrl(): string { + return WAN_BASE_URL; +} + +/** 当前生效的后端地址:内网模式取内网地址,公网模式取自定义公网地址(留空回落内置默认) */ export function apiBaseUrl(): string { - return cached.mode === "lan" ? cached.lanUrl : WAN_BASE_URL; + if (cached.mode === "lan") { + return cached.lanUrl; + } + return cached.wanUrl || WAN_BASE_URL; } /** 把线路写入请求层并落盘,返回生效后的地址 */ export function saveEndpoint(setting: EndpointSetting): string { - const lanUrl = normalizeLanUrl(setting.lanUrl); + const wanUrl = normalizeEndpointUrl(setting.wanUrl, "https"); + const lanUrl = normalizeEndpointUrl(setting.lanUrl, "http"); cached = { mode: setting.mode === "lan" && lanUrl ? "lan" : "wan", + wanUrl, lanUrl, }; uni.setStorageSync(STORAGE_KEY, cached); @@ -87,9 +111,12 @@ export function saveEndpoint(setting: EndpointSetting): string { return apiBaseUrl(); } -/** 测试内网地址连通性:用免鉴权的 RSA 公钥接口探活(与登录首调同源,最接近真实可用性) */ -export function testEndpoint(rawUrl: string): Promise { - const url = normalizeLanUrl(rawUrl); +/** 测试地址连通性:用免鉴权的 RSA 公钥接口探活(与登录首调同源,最接近真实可用性) */ +export function testEndpoint( + rawUrl: string, + defaultScheme: "http" | "https" = "http", +): Promise { + const url = normalizeEndpointUrl(rawUrl, defaultScheme); if (!url) { return Promise.resolve(false); } diff --git a/uniapp/youjiala/src/common/http.interceptor.ts b/uniapp/youjiala/src/common/http.interceptor.ts index 213130a..5613a29 100644 --- a/uniapp/youjiala/src/common/http.interceptor.ts +++ b/uniapp/youjiala/src/common/http.interceptor.ts @@ -229,6 +229,21 @@ function throwRequestError(message: string, handled: boolean): never { throw handled ? markErrorNotified(err) : err; } +/** + * 标记「网络层不可达」错误(连不上 / 请求失败 / 超时): + * 供登录页区分「服务器不通」与「账号密码错」——前者引导重设访问线路,后者正常提示重填。 + */ +export function isNetworkError(error: any): boolean { + return !!(error && error.isNetworkError === true); +} + +/** 抛出网络层错误:在 throwRequestError 基础上打 isNetworkError 标记 */ +function throwNetworkError(message: string, handled: boolean): never { + const err: any = new Error(message); + err.isNetworkError = true; + throw handled ? markErrorNotified(err) : err; +} + /** * 后端固定英文短语 → 按界面语言重写。 * 登录失败后端只下发一句 certification(不区分账号不存在/密码错,防撞库探测), @@ -335,11 +350,11 @@ const httpInterceptor: RequestInterceptor = { // 网络层错误 if (errMsg && errMsg.includes("Failed to connect")) { meta.toast && notifyError("", "网络错误"); - throwRequestError("网络错误", !!meta.toast); + throwNetworkError("网络错误", !!meta.toast); } if (errMsg && errMsg.includes("request:fail")) { meta.toast && notifyError("", "请求错误:未知"); - throwRequestError("请求错误:未知", !!meta.toast); + throwNetworkError("请求错误:未知", !!meta.toast); } // 401 且未重试过:共用刷新 + 重试 diff --git a/uniapp/youjiala/src/components/endpoint-setting-popup/endpoint-setting-popup.vue b/uniapp/youjiala/src/components/endpoint-setting-popup/endpoint-setting-popup.vue new file mode 100644 index 0000000..83a3987 --- /dev/null +++ b/uniapp/youjiala/src/components/endpoint-setting-popup/endpoint-setting-popup.vue @@ -0,0 +1,351 @@ + + + + + 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 143bc07..bef92f2 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 @@ -77,6 +77,24 @@ const treeGlyph = `data:image/svg+xml,${encodeURIComponent([ "", ].join(""))}`; +/** 族谱封面落盘目录:与后端 UploadFolderPolicy.TREE_COVER 同名(上传 FilePath 头 / 展示拼直链共用一个值) */ +const TREE_COVER_FOLDER = "TreeCover"; + +/** 封面展示地址:库里只存物理名,按落盘口径 /FileServer/Backup/TreeCover/{ext}/{name} 拼直链(与文章封面同法) */ +function treeCoverUrl(fileName?: string | null): string { + return backupFileUrl(TREE_COVER_FOLDER, fileName); +} + +/** 列表卡简介截断:CSS 多行省略在换行处也会补省略号,改按字数在末尾截断,完整简介只在详情看 */ +const BRIEF_MAX_LEN = 60; +function briefLine(text?: string | null): string { + const value = String(text ?? "").trim(); + if (!value) { + return t("FamilyTreePage.noBrief"); + } + return value.length > BRIEF_MAX_LEN ? `${value.slice(0, BRIEF_MAX_LEN)}…` : value; +} + const trees = ref([]); const loading = ref(false); const errorText = ref(""); @@ -531,9 +549,9 @@ const memberFilter = ref("all"); /** 邀请族人弹层已随底部分享入口取消(无后端接口占位),相关函数/样式/词条已同步清理,待邀请 service 就绪再重建 */ -/** 编辑族谱信息弹层:只含 名称/始祖谱名/简介/对外公开 四字段,关系与成员在其他入口分开维护 */ +/** 编辑族谱信息弹层:只含 名称/简介/对外公开 三字段(始祖谱名不展示,仍随快照守护),关系与成员在其他入口分开维护 */ const treeEditVisible = ref(false); -/** 打开弹层时的四字段快照:取消时本地还原直改,免去重载带来的“处理中” */ +/** 打开弹层时的字段快照(含不展示的始祖谱名):取消时本地还原直改,免去重载带来的“处理中” */ const treeEditSnapshot = ref({ TreeName: "", AncestorPersonGenealogyName: "", Brief: "", PublicIndc: "N" }); /** hero 统计「位族人」:全谱总人数(FindByCurrentPerson 只下发直系小集合,切当前族员会跳变,改走搜索链 Total 全量口径) */ @@ -645,7 +663,52 @@ async function onPublicToggle(value: boolean): Promise { }); } -/** 打开「编辑族谱信息」弹层:浏览态四字段快改入口(无修改权限挡回),先记快照供取消还原 */ +/** 点封面上传族谱封面(与首页头像同款交互):单图 -> /DmsFile/UploadFile(FilePath 头 TreeCover,上传即落盘) + * -> 物理名写回 TreeCoverFilename 后马上 Update 落库,不需再点保存 */ +const coverUploading = ref(false); +function onUploadTreeCover(): void { + if (!loaded.value) { + return; + } + if (!data.value.IsCanAmend) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } + if (coverUploading.value) { + return; + } + uni.chooseImage({ + count: 1, + success: async (res: any) => { + const filePath = String(res?.tempFilePaths?.[0] ?? ""); + const fileName = String(res?.tempFiles?.[0]?.name || `cover-${Date.now()}.png`); + if (!filePath) { + return; + } + try { + coverUploading.value = true; + const physicalName = buildChunkFileName(fileName); + await uploadFile({ + url: "/DmsFile/UploadFile", + filePath, + name: fileName, + fileName: physicalName, + header: { FilePath: TREE_COVER_FOLDER }, + }); + tree.value.TreeCoverFilename = physicalName; + sanitizeTreePayload(); + await familyTreeService.Update(data.value); + toast(t("FamilyTreePage.treeCoverUploaded")); + } + catch { /* uploadFile 已统一弹错,这里只补一个失败落点 */ } + finally { + coverUploading.value = false; + } + }, + }); +} + +/** 打开「编辑族谱信息」弹层:浏览态快改入口(无修改权限挡回),先记快照供取消还原 */ function openTreeEditor(): void { if (!isCreate.value && !data.value.IsCanAmend) { toast(t("FamilyTreePage.noAmendTip")); @@ -1693,177 +1756,185 @@ defineExpose({ - - - - - - - - - - {{ displayName ? displayName.slice(0, 1) : "?" }} - - - - - - - {{ displayName || "-" }} - - - {{ userTypeText }} + + + + + + + + + + + {{ displayName ? displayName.slice(0, 1) : "?" }} + + - {{ t("storage.welcome") }} - - - - - - - - - - {{ t("storage.title") }} - - - - - - - {{ storagePercentText - }} - - - - - - {{ t("storage.used") }} {{ storageUsedText }} - {{ t("storage.total") }} {{ storageQuotaText }} - - - - - - {{ t("FamilyTreePage.listTitle") }} - - - - - - - - - - - - - - - - - - {{ t("FilePage.loading") }} - - - {{ errorText }} - {{ t("FilePage.retryBut") }} - - - - - - - - {{ t("article.title") }} - - - - - - - - - + {{ t("storage.welcome") }} - - - - - - - {{ t("FilePage.loading") }} - - - {{ articlesApi.errorText.value }} - {{ t("FilePage.retryBut") }} - - + + + + + {{ t("storage.used") }} {{ storageUsedText }} + {{ t("storage.total") }} {{ storageQuotaText }} + + - - + + + {{ t("FamilyTreePage.listTitle") }} + + + + + + + + + + + + + + + + + + {{ t("FilePage.loading") }} + + + {{ errorText }} + {{ t("FilePage.retryBut") }} + + + + + + + + {{ t("article.title") }} + + + + + + + + + + + + + + + + + + {{ t("FilePage.loading") }} + + + {{ articlesApi.errorText.value + }} + {{ t("FilePage.retryBut") }} + + + + + + + + @@ -1877,8 +1948,14 @@ defineExpose({ - - + + + + + + + {{ tree.TreeName || t("FamilyTreePage.unnamed") }} @@ -1894,7 +1971,7 @@ defineExpose({ {{ t("FamilyTreePage.managerTag") - }} + }} @@ -2006,7 +2083,7 @@ defineExpose({ {{ t("FamilyTreePage.currentPerson") - }} + }} @@ -2021,7 +2098,7 @@ defineExpose({ {{ personChipText(currentPerson) - }} + }} @@ -2064,12 +2141,12 @@ defineExpose({ {{ t("FamilyTreePage.spouseTitle") - }}{{ spouseMaps.length }} + }}{{ spouseMaps.length }} {{ t("FamilyTreePage.addSpouse") }} {{ t("FamilyTreePage.spouseEmpty") - }} + }} {{ t("FamilyTreePage.childTitle") - }}{{ childMaps.length }} + }}{{ childMaps.length }} {{ t("FamilyTreePage.addChildBtn") }} {{ t("FamilyTreePage.childEmpty") - }} + }} {{ t("FamilyTreePage.permPending") - }}{{ pendingPermissions.length }} + }}{{ pendingPermissions.length }} {{ record.UserLocalName || @@ -2177,10 +2254,10 @@ defineExpose({ {{ t("FamilyTreePage.permManager") - }}{{ managerPermissions.length }} + }}{{ managerPermissions.length }} {{ t("FamilyTreePage.permEmpty") - }} + }} {{ record.UserLocalName || record.UserName @@ -2250,17 +2327,17 @@ defineExpose({ {{ t("FamilyTreePage.statsColGeneration") + }} + {{ t("FamilyTreePage.statsColCount") }} - {{ t("FamilyTreePage.statsColCount") }} {{ gen.Generation }}{{ t("FamilyTreePage.statsGenerationUnit") - }} + }} {{ gen.Count }} - + @@ -2293,7 +2370,9 @@ defineExpose({ - + + {{ tree.TreeName || t("FamilyTreePage.unnamed") }} @@ -2465,7 +2544,7 @@ defineExpose({ {{ record.SpouseSurname }}{{ record.SpouseGenealogyName }} {{ genderText(personGender(record.SpouseId, record.Gender)) - }} + }} @@ -2502,7 +2581,7 @@ defineExpose({ {{ t("FamilyTreePage.marryDate") }} {{ datePart(record.MarryDate) || t("AdminPage.notSetTip") - }} + }} @@ -2518,7 +2597,7 @@ defineExpose({ {{ datePart(record.DivorceDate) || t("AdminPage.notSetTip") - }} + }} @@ -2604,7 +2683,7 @@ defineExpose({ {{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }} {{ genderText(personGender(record.ChildrenId, record.Gender)) - }} + }} @@ -2715,7 +2794,7 @@ defineExpose({ {{ record.UserLocalName || record.UserName }} {{ record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag") - }} + }} @@ -2804,7 +2883,7 @@ defineExpose({ - + @@ -2892,7 +2971,7 @@ defineExpose({ {{ t("FamilyTreePage.exchangeFileLoading") - }} + }} {{ t(imScope === "SUB" ? "FamilyTreePage.exchangeFileEmptySub" : "FamilyTreePage.exchangeFileEmpty") }} {{ t("FamilyTreePage.exchangeFileLoading") }} {{ t("FamilyTreePage.exchangeBackupEmpty") - }} + }} @@ -2955,17 +3034,11 @@ defineExpose({ - - {{ t("FamilyTreePage.ancestorGenealogyName") }} - - {{ t("FamilyTreePage.brief") }} -