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 @@
+
+
+
+
+
+
+ {{ t("MinePage.accessLine") }}
+
+
+ {{ t("MinePage.lineHint") }}
+
+
+
+
+
+
+ {{ t("MinePage.lineWan") }}
+
+
+ {{ t("MinePage.lineWanDesc") }}
+
+
+
+
+
+
+
+
+
+ {{ t("MinePage.lineLan") }}
+
+
+ {{ t("MinePage.lineLanDesc") }}
+
+
+
+
+
+
+
+
+
+
+ {{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
+
+
+
+
+ {{ t("MinePage.lineTestOk") }}
+
+
+ {{ t("MinePage.lineTestFail") }}
+
+
+
+
+
+ {{ t("MinePage.lineCancel") }}
+
+
+
+
+ {{ t("MinePage.lineSave") }}
+
+
+
+
+
+
+
+
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("FamilyTreePage.chartEmpty") }}
-
-
-
-
-
-
- {{ row.TreeName || t("FamilyTreePage.unnamed") }}
- {{ row.Brief || t("FamilyTreePage.noBrief") }}
-
- {{ row.TreeId }}
-
- {{ t("FamilyTreePage.chipPublic") }}
-
- {{ t("FamilyTreePage.chipPrivate") }}
-
- {{ `${row.Surname}${row.GenealogyName}` }}
-
+
+
+ {{ displayName || "-" }}
+
+
+ {{ userTypeText }}
-
-
-
-
-
-
-
-
- {{ t("article.title") }}
-
-
-
-
-
-
-
-
-
+ {{ t("storage.welcome") }}
-
-
-
-
-
-
- {{ t("FilePage.loading") }}
-
-
- {{ articlesApi.errorText.value }}
- {{ t("FilePage.retryBut") }}
-
-
-
- {{ t("article.empty") }}
-
-
-
-
-
-
-
-
- {{ row.ArticleTitle || t("article.unnamed") }}
- {{
- t("article.tagTop") }}
- {{
- t("article.tagPending") }}
+
+
+
+
+
+
- {{ articlesApi.plainText(row.Brief, 100) || t("article.noBrief")
+ {{ t("storage.title") }}
+
+
+
+
+
+
+ {{ storagePercentText
}}
-
-
-
- {{ row.AuthorLocalName || t("article.unknownAuthor") }}
- {{ articlesApi.formatDate(row.CreatedDatetime) }}
-
-
-
-
+
+
+
+
+ {{ t("storage.used") }} {{ storageUsedText }}
+ {{ t("storage.total") }} {{ storageQuotaText }}
+
+
-
-
+
+
+ {{ t("FamilyTreePage.listTitle") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FilePage.loading") }}
+
+
+ {{ errorText }}
+ {{ t("FilePage.retryBut") }}
+
+
+
+ {{ t("FamilyTreePage.chartEmpty") }}
+
+
+
+
+
+
+
+
+ {{ row.TreeName || t("FamilyTreePage.unnamed") }}
+
+ {{ row.TreeId }}
+
+ {{ t("FamilyTreePage.chipPublic") }}
+
+ {{ t("FamilyTreePage.chipPrivate") }}
+
+
+
+
+ {{ briefLine(row.Brief) }}
+
+
+
+
+
+
+
+
+ {{ t("article.title") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FilePage.loading") }}
+
+
+ {{ articlesApi.errorText.value
+ }}
+ {{ t("FilePage.retryBut") }}
+
+
+
+ {{ t("article.empty") }}
+
+
+
+
+
+
+
+
+ {{ row.ArticleTitle || t("article.unnamed") }}
+ {{
+ t("article.tagTop") }}
+
+ {{ articlesApi.plainText(row.Brief, 100) || t("article.noBrief")
+ }}
+
+
+
+ {{ row.AuthorLocalName || t("article.unknownAuthor") }}
+
+ {{
+ t("article.tagPending") }}
+ {{ articlesApi.formatDate(row.CreatedDatetime) }}
+
+
+
+
+
+
+
+
+
+
+
@@ -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") }}
-
- {{ (tree.Brief || "").length }}/60
@@ -2979,7 +3052,7 @@ defineExpose({
@@ -3023,6 +3096,13 @@ defineExpose({
overflow: hidden;
}
+/* 列表态:外层 wrap 以 flex:1 + min-height:0 撑出确定视口高,内层 scroll-view 再 height:100%
+ 才能触发内部滚动——与详情态同一套写法(scroll-view 直接挂 flex:1 时其内部滚动层拿不到确定高)。
+ 列表 wrap 不留外边距,四周留白交给 scroll-view 的 padding,避免与状态栏 padding 叠加 */
+.tree-page__scroll-wrap--list {
+ margin: 0;
+}
+
/* 列表态整页滚动:分区面板按内容撑高(为日后首页并列多块面板留位)。
box-sizing 必加:uni 给 scroll-view 根设了 width:100%,默认 content-box 下
padding 会外溢成 100%+两侧留白,导致面板整体右移、两边不对称 */
@@ -3033,6 +3113,11 @@ defineExpose({
padding: calc(20rpx + var(--status-bar-height, 0px)) 20rpx 20rpx;
}
+/* 列表底部留白:首页带固定 tabbar,末张卡需能滚到 tabbar 之上才看得见 */
+.tree-page__list-space {
+ height: calc(140rpx + env(safe-area-inset-bottom, 0px));
+}
+
/* ===== 顶部两张卡片:用户身份 + 网盘用量(白底圆角,与下方族谱面板同宽同调) ===== */
.home-cards {
display: flex;
@@ -3485,13 +3570,14 @@ defineExpose({
background-color: var(--fg-soft, #eaf4ff);
}
-/* ===== 列表卡片(媒体页 album-card 同套) ===== */
+/* ===== 列表卡片(媒体页 album-card 同套,改版:上行 头像+名称+徽章+始祖,下行整行简介) ===== */
.tree-card {
+ position: relative;
display: flex;
- align-items: center;
- gap: 20rpx;
+ flex-direction: column;
+ gap: 16rpx;
margin: 0 0 20rpx;
- padding: 28rpx;
+ padding: 28rpx 56rpx 28rpx 28rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
@@ -3501,17 +3587,31 @@ defineExpose({
margin-bottom: 0;
}
+/* 首行:头像 + 名称/徽章 + 始祖,垂直居中 */
+.tree-card__top {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+}
+
.tree-card__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
+ overflow: hidden;
border-radius: 20rpx;
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
flex-shrink: 0;
}
+/* 封面图铺满头像位(与文章卡封面同口径,无封面才回退树形 glyph) */
+.tree-card__cover {
+ width: 100%;
+ height: 100%;
+}
+
/* 树形字形(内联 SVG data-URI) */
.tree-card__glyph {
width: 56rpx;
@@ -3535,12 +3635,12 @@ defineExpose({
text-overflow: ellipsis;
}
+/* 简介整行多行显示:省略号由 briefLine 按字数在末尾补(CSS 多行省略会在换行处也补) */
.tree-card__desc {
font-size: 24rpx;
+ line-height: 1.6;
color: #8f959e;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
+ word-break: break-word;
}
.tree-card__meta {
@@ -3564,10 +3664,12 @@ defineExpose({
}
}
-.tree-card__ancestor {
- margin-left: auto;
- font-size: 20rpx;
- color: #bfc3c9;
+/* 箭头改挂右上角,不再占首行一列(简介要整行铺开) */
+.tree-card__arrow {
+ position: absolute;
+ top: 50%;
+ right: 16rpx;
+ transform: translateY(-50%);
}
/* ===== 有家文化传承文章卡(复用 tree-panel 面板几何,卡体换横向图文) ===== */
@@ -3733,11 +3835,18 @@ defineExpose({
justify-content: center;
width: 140rpx;
height: 140rpx;
+ overflow: hidden;
border-radius: 32rpx;
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
flex-shrink: 0;
}
+/* 编辑态头卡同步显示封面(上传入口在浏览态族徽上) */
+.detail-hero__cover {
+ width: 100%;
+ height: 100%;
+}
+
/* 树形字形(与列表卡同款内联 SVG data-URI) */
.detail-hero__glyph {
width: 84rpx;
@@ -4259,20 +4368,44 @@ defineExpose({
}
.ft-hero__crest {
+ position: relative;
display: flex;
flex: none;
align-items: center;
justify-content: center;
- width: 112rpx;
- height: 112rpx;
- border-radius: 28rpx;
+ width: 168rpx;
+ height: 168rpx;
+ overflow: hidden;
+ border-radius: 36rpx;
background: var(--fg-gradient, linear-gradient(135deg, #ff9d45, #ff7d1f));
box-shadow: 0 12rpx 24rpx -12rpx var(--fg-shadow, rgba(255, 125, 31, 0.5));
}
+/* 封面图铺满族徽位(族徽已放大,无封面才回退树形 glyph) */
+.ft-hero__cover {
+ width: 100%;
+ height: 100%;
+}
+
+/* 相机角标:与首页头像角标同款(黑半透圆 + 白描边,不吃点击) */
+.ft-hero__crest-cam {
+ position: absolute;
+ right: 8rpx;
+ bottom: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 44rpx;
+ height: 44rpx;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.45);
+ border: 2rpx solid #ffffff;
+ pointer-events: none;
+}
+
.ft-hero__glyph {
- width: 64rpx;
- height: 64rpx;
+ width: 96rpx;
+ height: 96rpx;
}
.ft-hero__info {
@@ -5060,7 +5193,7 @@ defineExpose({
box-sizing: border-box;
width: 100%;
height: 180rpx;
- padding: 20rpx 24rpx 44rpx;
+ padding: 20rpx 24rpx;
font-size: 28rpx;
line-height: 1.5;
color: #1d2129;
@@ -5068,14 +5201,6 @@ defineExpose({
border-radius: 20rpx;
background-color: #f7f8fa;
}
-
- &__counter {
- position: absolute;
- right: 24rpx;
- bottom: 16rpx;
- font-size: 24rpx;
- color: #8f959e;
- }
}
/* ===== 交换文件弹层专属:说明小字/警示/文件行(选中态跟主题 soft 底) ===== */
diff --git a/uniapp/youjiala/src/data/auto/familyTreeViewEntity.ts b/uniapp/youjiala/src/data/auto/familyTreeViewEntity.ts
index ff9ac5e..948fe80 100644
--- a/uniapp/youjiala/src/data/auto/familyTreeViewEntity.ts
+++ b/uniapp/youjiala/src/data/auto/familyTreeViewEntity.ts
@@ -6,6 +6,7 @@ export class FamilyTreeViewEntity
public CommonName: string = "";
public GenealogyName: string = "";
public PublicIndc: string = "";
+ public TreeCoverFilename: string = "";
public Surname: string = "";
public TreeId: string = "";
public TreeName: string = "";
@@ -15,16 +16,18 @@ export class FamilyTreeViewEntity
static readonly COMMON_NAME : string ='CommonName';
static readonly GENEALOGY_NAME : string ='GenealogyName';
static readonly PUBLIC_INDC : string ='PublicIndc';
+ static readonly TREE_COVER_FILENAME : string ='TreeCoverFilename';
static readonly SURNAME : string ='Surname';
static readonly TREE_ID : string ='TreeId';
static readonly TREE_NAME : string ='TreeName';
static readonly DB_NAME_FAMILY_TREE_VIEW : string ='family_tree_view';
- static readonly DB_NAME_FIELDS : string ='ancestor_person_id,brief,common_name,genealogy_name,public_indc,surname,tree_id,tree_name';
+ static readonly DB_NAME_FIELDS : string ='ancestor_person_id,brief,common_name,genealogy_name,public_indc,tree_cover_filename,surname,tree_id,tree_name';
static readonly DB_ANCESTOR_PERSON_ID : string ='ancestor_person_id';
static readonly DB_BRIEF : string ='brief';
static readonly DB_COMMON_NAME : string ='common_name';
static readonly DB_GENEALOGY_NAME : string ='genealogy_name';
static readonly DB_PUBLIC_INDC : string ='public_indc';
+ static readonly DB_TREE_COVER_FILENAME : string ='tree_cover_filename';
static readonly DB_SURNAME : string ='surname';
static readonly DB_TREE_ID : string ='tree_id';
static readonly DB_TREE_NAME : string ='tree_name';
diff --git a/uniapp/youjiala/src/data/auto/securityUserPolicy.ts b/uniapp/youjiala/src/data/auto/securityUserPolicy.ts
index c22aebe..65aa2bf 100644
--- a/uniapp/youjiala/src/data/auto/securityUserPolicy.ts
+++ b/uniapp/youjiala/src/data/auto/securityUserPolicy.ts
@@ -3,5 +3,6 @@ export class SecurityUserPolicy
{
static readonly SECURITY_USER_BASE_ACTOR : string ='SecurityUserBaseActor';
+ static readonly SYSTEM_ACTOR : string ='system_actor';
static readonly ERROR_LOGIN_COUNT: number =5;
}
diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json
index 77bb779..e121a83 100644
--- a/uniapp/youjiala/src/locale/lang/en-US.json
+++ b/uniapp/youjiala/src/locale/lang/en-US.json
@@ -67,7 +67,9 @@
"slogan": "One tree, generations of us",
"langZh": "简体中文",
"langEn": "English",
- "loginFailed": "Incorrect account or password"
+ "loginFailed": "Incorrect account or password",
+ "netFailTitle": "Cannot reach the server",
+ "netFailContent": "The server for the current access line is unreachable. Check your network or reset the access line"
},
"TabBar": {
"home": "Home",
@@ -94,9 +96,10 @@
"logoutSuccess": "Logged out",
"currentTheme": "Current Theme",
"accessLine": "Access Line",
- "lineHint": "Choose LAN Direct when your device is on the same network as the server for faster transfers",
+ "lineHint": "Both public and LAN addresses are customizable; saving a server change signs you out for re-login",
"lineWan": "WAN",
"lineWanDesc": "Access via the public domain, available anywhere",
+ "lineWanPlaceholder": "Public address, leave blank for default ({default})",
"lineLan": "LAN Direct",
"lineLanDesc": "Connect directly over the same Wi-Fi for faster speed",
"lineLanPlaceholder": "LAN address, e.g. http://192.168.1.10:5235",
@@ -107,7 +110,8 @@
"lineInvalid": "Invalid address format",
"lineCancel": "Cancel",
"lineSave": "Save",
- "lineSaved": "Line switched",
+ "lineSaved": "Line saved",
+ "lineSavedRelogin": "Line updated, please sign in again",
"aboutTitle": "About",
"appName": "App",
"versionLabel": "Version",
@@ -373,7 +377,9 @@
"editInfoTitle": "Edit Family Tree Info",
"removeRelation": "Remove Relation",
"removeRelationConfirm": "Remove the relation with {name}? It takes effect immediately after saving.",
- "dupMemberTip": "This member is already linked, do not add again"
+ "dupMemberTip": "This member is already linked, do not add again",
+ "treeCoverPick": "Tap to upload cover",
+ "treeCoverUploaded": "Cover uploaded"
},
"FamilyPersonPage": {
"title": "Family Member Query",
diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json
index 0843171..3df3e1f 100644
--- a/uniapp/youjiala/src/locale/lang/zh-CN.json
+++ b/uniapp/youjiala/src/locale/lang/zh-CN.json
@@ -67,7 +67,9 @@
"slogan": "一棵树,几代人",
"langZh": "简体中文",
"langEn": "English",
- "loginFailed": "账号或密码错误"
+ "loginFailed": "账号或密码错误",
+ "netFailTitle": "无法连接服务器",
+ "netFailContent": "当前访问线路的服务器连不上,请检查网络或重新设置访问线路"
},
"TabBar": {
"home": "首页",
@@ -94,9 +96,10 @@
"logoutSuccess": "已退出登录",
"currentTheme": "当前主题",
"accessLine": "访问线路",
- "lineHint": "与服务器在同一局域网时选内网直连,上传更快速",
+ "lineHint": "公网与内网地址均可自定义;切换服务器后保存将自动退出,需重新登录",
"lineWan": "公网",
"lineWanDesc": "通过公网域名访问,随时随地可用",
+ "lineWanPlaceholder": "公网地址,留空使用默认({default})",
"lineLan": "内网直连",
"lineLanDesc": "同一 Wi-Fi 下直连服务器内网地址,速度更快",
"lineLanPlaceholder": "内网地址,如 http://192.168.1.10:5235",
@@ -107,7 +110,8 @@
"lineInvalid": "地址格式不正确",
"lineCancel": "取消",
"lineSave": "保存",
- "lineSaved": "已切换线路",
+ "lineSaved": "线路已保存",
+ "lineSavedRelogin": "线路已更新,请重新登录",
"aboutTitle": "关于",
"appName": "应用名称",
"versionLabel": "当前版本",
@@ -341,7 +345,7 @@
"statMembers": "位族人",
"statManagers": "管理员",
"statPending": "待审核",
- "statsTotal": "入树成员",
+ "statsTotal": "位族人",
"statsGeneration": "世代层数",
"statsMale": "男",
"statsFemale": "女",
@@ -373,7 +377,9 @@
"editInfoTitle": "编辑族谱信息",
"removeRelation": "解除关系",
"removeRelationConfirm": "确定解除与「{name}」的关系吗?解除后立即保存生效",
- "dupMemberTip": "该成员已在关系中,请勿重复添加"
+ "dupMemberTip": "该成员已在关系中,请勿重复添加",
+ "treeCoverPick": "点击上传封面",
+ "treeCoverUploaded": "封面已上传"
},
"FamilyPersonPage": {
"title": "家族成员查询",
@@ -1002,4 +1008,4 @@
"r404_3": "页面已下线或改名,收藏夹里还是旧地址",
"r404_4": "服务器刚重启、菜单还没加载出来,刷新一次即可"
}
-}
\ No newline at end of file
+}
diff --git a/uniapp/youjiala/src/pages/login/login.vue b/uniapp/youjiala/src/pages/login/login.vue
index e7d1820..3ea4a0b 100644
--- a/uniapp/youjiala/src/pages/login/login.vue
+++ b/uniapp/youjiala/src/pages/login/login.vue
@@ -9,7 +9,7 @@ import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { apiBaseUrl } from "@/common/apiEndpoint";
import { brandGlyph, veinGlyph } from "@/common/brand";
-import { backendLocale } from "@/common/http.interceptor";
+import { backendLocale, isNetworkError } from "@/common/http.interceptor";
import { DEFAULT_LOCALE, setCurrentUiLocale } from "@/common/locale-const";
import { encryptLoginPassword } from "@/common/pwd-crypto";
import { useThemeGradient, useThemeTokens } from "@/composables";
@@ -23,6 +23,8 @@ const userMobile = ref("");
const password = ref("");
const showPwd = ref(false);
const loading = ref(false);
+/** 「访问线路」弹层显隐:登录连不上服务器时自动弹出(组件与「我的」共用) */
+const endpointPopupVisible = ref(false);
const loginProfileStore = useLoginProfileStore();
/* 主题接线:页面底渐变与强调色全部跟随当前主题(与「我的」/媒体/共享页同一口径),
@@ -127,13 +129,33 @@ async function handleLogin() {
// 回到被拦前的页面(默认 dashboard):reLaunch 会清空页面栈,避免返回键退回登录页
setTimeout(() => uni.reLaunch({ url: redirectUrl.value }), 800);
}
- catch {
- // 错误提示已由 http.interceptor 统一处理
+ catch (error: any) {
+ // 连不上服务器(地址填错 / 服务器宕机):提示并自动弹出共用的「访问线路」让用户重设地址后原地重试;
+ // 其余错误(如账号密码错)的提示已由 http.interceptor 统一处理
+ // #ifdef APP-PLUS
+ if (isNetworkError(error)) {
+ uni.showModal({
+ title: t("LoginForm.netFailTitle"),
+ content: t("LoginForm.netFailContent"),
+ showCancel: false,
+ success: () => {
+ endpointPopupVisible.value = true;
+ },
+ });
+ }
+ // #endif
}
finally {
loading.value = false;
}
}
+
+/** 访问线路保存后:服务器真的变了就提示重试登录(本页本就未登录,无需再走登出) */
+function onEndpointSaved(payload: { changed: boolean }): void {
+ if (payload.changed) {
+ uni.showToast({ title: t("MinePage.lineSavedRelogin"), icon: "none" });
+ }
+}
@@ -197,6 +219,11 @@ async function handleLogin() {
{{ envMode }} · {{ currentApiUrl }}
+
+
+
+
+
diff --git a/uniapp/youjiala/src/pages/tab/mine.vue b/uniapp/youjiala/src/pages/tab/mine.vue
index b72fac9..ccffd61 100644
--- a/uniapp/youjiala/src/pages/tab/mine.vue
+++ b/uniapp/youjiala/src/pages/tab/mine.vue
@@ -7,7 +7,7 @@
*/
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
-import { currentEndpoint, normalizeLanUrl, saveEndpoint, testEndpoint } from "@/common/apiEndpoint";
+import { currentEndpoint } from "@/common/apiEndpoint";
import { APP_INFO } from "@/common/constant";
import { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
import { usePermission } from "@/permission/permission-context";
@@ -51,61 +51,36 @@ function goTheme(): void {
uni.navigateTo({ url: pageUrl("themePage") });
}
-// ===== 访问线路(App 手动切换公网/内网直连;仅影响后续请求,不中断在途传输) =====
+// ===== 访问线路(App 手动切换公网/内网直连;弹层组件与登录页共用) =====
const linePopupVisible = ref(false);
/** 当前生效线路(保存后刷新展示) */
const lineSetting = ref(currentEndpoint());
-/** 弹层草稿:确认前不动真实线路,取消即作废 */
-const lineModeDraft = ref<"wan" | "lan">("wan");
-const lineLanDraft = ref("");
-const lineTesting = ref(false);
-const lineTestResult = ref<"" | "ok" | "fail">("");
-/** 当前线路展示文案:公网 / 内网直连(附地址) */
+/** 当前线路展示文案:公网(附自定义地址)/ 内网直连(附地址) */
const currentLineLabel = computed(() => {
const setting = lineSetting.value;
- return setting.mode === "lan" ? `${t("MinePage.lineLan")} · ${setting.lanUrl}` : t("MinePage.lineWan");
+ if (setting.mode === "lan") {
+ return `${t("MinePage.lineLan")} · ${setting.lanUrl}`;
+ }
+ return setting.wanUrl ? `${t("MinePage.lineWan")} · ${setting.wanUrl}` : t("MinePage.lineWan");
});
-/** 打开线路弹层:草稿回填当前设置,清掉上次测活结果 */
+/** 打开线路弹层:草稿回填由组件内部完成,这里刷新展示快照并展开 */
function openLinePopup(): void {
- const setting = currentEndpoint();
- lineModeDraft.value = setting.mode;
- lineLanDraft.value = setting.lanUrl;
- lineTestResult.value = "";
+ lineSetting.value = currentEndpoint();
linePopupVisible.value = true;
}
-/** 测试内网地址连通性:用 RSA 公钥接口探活(与登录首调同源) */
-async function handleTestLine(): Promise {
- if (lineTesting.value) {
- return;
- }
- const url = normalizeLanUrl(lineLanDraft.value);
- if (!url) {
- uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
- return;
- }
- lineTesting.value = true;
- lineTestResult.value = "";
- const ok = await testEndpoint(url);
- lineTesting.value = false;
- lineTestResult.value = ok ? "ok" : "fail";
- if (!ok) {
- uni.showToast({ title: t("MinePage.lineTestFail"), icon: "none" });
- }
-}
-
-/** 保存线路:内网模式地址必填且合法;生效只影响后续请求,各页切回时自然按新线路拉数据 */
-function handleSaveLine(): void {
- if (lineModeDraft.value === "lan" && !normalizeLanUrl(lineLanDraft.value)) {
- uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
- return;
- }
- saveEndpoint({ mode: lineModeDraft.value, lanUrl: lineLanDraft.value });
+/** 弹层保存后回调:刷新展示;服务器真的变了 → 本地令牌对新服务器无意义,清登录态回登录页重新认证 */
+function onLineSaved(payload: { changed: boolean }): void {
lineSetting.value = currentEndpoint();
- linePopupVisible.value = false;
- uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" });
+ if (!payload.changed) {
+ uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" });
+ return;
+ }
+ loginProfileStore.clearStorage();
+ uni.showToast({ title: t("MinePage.lineSavedRelogin"), icon: "none" });
+ setTimeout(() => uni.reLaunch({ url: pageUrl("login") }), 1200);
}
/** 进入「安全管理」枢纽页:权限体系等基础设定的聚合入口 */
@@ -303,77 +278,9 @@ async function handleLogout(): Promise {
-
+
-
-
-
- {{ t("MinePage.accessLine") }}
-
-
- {{ t("MinePage.lineHint") }}
-
-
-
-
-
-
- {{ t("MinePage.lineWan") }}
-
-
- {{ t("MinePage.lineWanDesc") }}
-
-
-
-
-
-
-
-
-
- {{ t("MinePage.lineLan") }}
-
-
- {{ t("MinePage.lineLanDesc") }}
-
-
-
-
-
-
-
-
-
-
- {{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
-
-
-
-
- {{ t("MinePage.lineTestOk") }}
-
-
- {{ t("MinePage.lineTestFail") }}
-
-
-
-
-
- {{ t("MinePage.lineCancel") }}
-
-
-
-
- {{ t("MinePage.lineSave") }}
-
-
-
-
-
+
@@ -492,156 +399,5 @@ async function handleLogout(): Promise {
color: #1f2329;
}
-/* ===== 访问线路弹层(底部抽屉,同 file-action-sheet 风格) ===== */
-.line-sheet {
- padding: 32rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
- background-color: #f5f6f7;
-}
-
-.line-sheet__title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1f2329;
-}
-
-.line-sheet__hint {
- display: block;
- margin-top: 8rpx;
- font-size: 24rpx;
- color: #9aa0a6;
-}
-
-/* 单选行:白卡;选中态用内阴影描边(不改变布局尺寸) */
-.line-option {
- display: flex;
- align-items: center;
- margin-top: 20rpx;
- padding: 24rpx;
- border-radius: 20rpx;
- background-color: #ffffff;
-}
-
-.line-option--active {
- box-shadow: 0 0 0 2rpx var(--fg-primary, #ff7d1f) inset;
-}
-
-.line-option__body {
- display: flex;
- flex: 1;
- flex-direction: column;
- gap: 6rpx;
-}
-
-.line-option__name {
- font-size: 28rpx;
- font-weight: 600;
- color: #1f2329;
-}
-
-.line-option__desc {
- font-size: 24rpx;
- color: #9aa0a6;
-}
-
-.line-option__radio {
- box-sizing: border-box;
- width: 36rpx;
- height: 36rpx;
- margin-left: 16rpx;
- border: 2rpx solid #d4d7dc;
- border-radius: 50%;
-}
-
-.line-option__radio--on {
- border-color: var(--fg-primary, #ff7d1f);
- background: radial-gradient(circle, var(--fg-primary, #ff7d1f) 0 42%, transparent 48%);
-}
-
-/* 内网地址:输入框 + 测试连接一行 */
-.line-lan {
- display: flex;
- align-items: center;
- gap: 16rpx;
- margin-top: 20rpx;
-}
-
-.line-lan__input {
- flex: 1;
- height: 76rpx;
- padding: 0 20rpx;
- border-radius: 16rpx;
- font-size: 26rpx;
- color: #1f2329;
- background-color: #ffffff;
-}
-
-.line-lan__ph {
- color: #c0c4cc;
-}
-
-.line-lan__test {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 76rpx;
- padding: 0 24rpx;
- border-radius: 16rpx;
- background-color: #ffffff;
-}
-
-.line-lan__test-text {
- font-size: 26rpx;
- font-weight: 600;
- color: var(--fg-primary, #ff7d1f);
-}
-
-.line-lan__result {
- display: block;
- margin-top: 12rpx;
- font-size: 24rpx;
-}
-
-.line-lan__result--ok {
- color: #52c41a;
-}
-
-.line-lan__result--fail {
- color: #fa3534;
-}
-
-/* 底部双按钮:取消(描边)/ 保存(主题渐变) */
-.line-sheet__actions {
- display: flex;
- gap: 20rpx;
- margin-top: 32rpx;
-}
-
-.line-sheet__btn {
- display: flex;
- flex: 1;
- align-items: center;
- justify-content: center;
- height: 88rpx;
- border-radius: 999rpx;
-}
-
-.line-sheet__btn--ghost {
- border: 2rpx solid #d4d7dc;
- background-color: #ffffff;
-}
-
-.line-sheet__btn--primary {
- background: var(--fg-gradient, linear-gradient(135deg, #ff9a3d, #ff7d1f));
- box-shadow: var(--fg-shadow, 0 6rpx 16rpx rgba(255, 125, 31, 0.35));
-}
-
-.line-sheet__btn-text {
- font-size: 28rpx;
- font-weight: 600;
- color: #ffffff;
-}
-
-.line-sheet__btn-text--ghost {
- color: #4e5969;
-}
+/* 访问线路弹层样式已随组件迁至 components/endpoint-setting-popup/endpoint-setting-popup.vue */