ow
This commit is contained in:
1 parent
6c86c59bdc
commit
31de6c4cd6
15 files changed
+988
-653
No files matched your search
@@ -4,175 +4,180 @@ using CloudBuilder.Core.DatabaseAccessor.Entity;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
namespace CloudBuilder.Master.Entity
|
namespace CloudBuilder.Master.Entity
|
||||||
{
|
{
|
||||||
[TypeScript]
|
[TypeScript]
|
||||||
[Table("family_tree_view")]
|
[Table("family_tree_view")]
|
||||||
public class FamilyTreeViewEntity : ViewEntityBase
|
public class FamilyTreeViewEntity : ViewEntityBase
|
||||||
{
|
{
|
||||||
|
|
||||||
public FamilyTreeViewEntity()
|
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 override string GetSelectFrom()
|
[Column("ancestor_person_id")]
|
||||||
{
|
public string AncestorPersonId { get; set; }
|
||||||
if (ProviderName.Contains("MySql"))
|
[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 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 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 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 GetSelectFromByMySql();
|
return new FamilyTreeViewEntity() { ProviderName = providerName };
|
||||||
}
|
}
|
||||||
else if (ProviderName.Contains("SqlServer"))
|
|
||||||
|
public virtual void CopyFrom(FamilyTreeViewEntity source)
|
||||||
{
|
{
|
||||||
return GetSelectFromByBySqlServer();
|
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.");
|
||||||
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 override string GetSelectCountFrom(string sqlWhere)
|
this.AncestorPersonId = source.AncestorPersonId;
|
||||||
{
|
this.Brief = source.Brief;
|
||||||
if (ProviderName.Contains("MySql"))
|
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 override string GetSelectFrom()
|
||||||
{
|
{
|
||||||
return GetSelectCountFromByMySql(sqlWhere);
|
if (ProviderName.Contains("MySql"))
|
||||||
|
{
|
||||||
|
return GetSelectFromByMySql();
|
||||||
|
}
|
||||||
|
else if (ProviderName.Contains("SqlServer"))
|
||||||
|
{
|
||||||
|
return GetSelectFromByBySqlServer();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (ProviderName.Contains("SqlServer"))
|
public string GetSelectFromByMySql()
|
||||||
{
|
{
|
||||||
return GetSelectCountFromBySqlServer(sqlWhere);
|
return $" select {DB_NAME_FIELDS} from {DB_NAME_FAMILY_TREE_VIEW} with(nolock) ";
|
||||||
}
|
}
|
||||||
else
|
public string GetSelectFromByBySqlServer()
|
||||||
{
|
{
|
||||||
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
|
return $" select {DB_NAME_FIELDS} from {DB_NAME_FAMILY_TREE_VIEW} ";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public string GetSelectCountFromByMySql(string sqlWhere)
|
public override string GetSelectCountFrom(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 override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere)
|
|
||||||
{
|
|
||||||
if (ProviderName.Contains("MySql"))
|
|
||||||
{
|
{
|
||||||
return GetSelectTopFromByMySql(limit, start, orderby, orderbytag, sqlWhere);
|
if (ProviderName.Contains("MySql"))
|
||||||
|
{
|
||||||
|
return GetSelectCountFromByMySql(sqlWhere);
|
||||||
|
}
|
||||||
|
else if (ProviderName.Contains("SqlServer"))
|
||||||
|
{
|
||||||
|
return GetSelectCountFromBySqlServer(sqlWhere);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (ProviderName.Contains("SqlServer"))
|
|
||||||
|
public string GetSelectCountFromByMySql(string sqlWhere)
|
||||||
{
|
{
|
||||||
return GetSelectTopFromBySqlServer(limit, start, orderby, orderbytag, sqlWhere);
|
return $" select count(*) from {DB_NAME_FAMILY_TREE_VIEW} where 1=1 {sqlWhere}";
|
||||||
}
|
}
|
||||||
else
|
public string GetSelectCountFromBySqlServer(string sqlWhere)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
|
return $" select count(*) from {DB_NAME_FAMILY_TREE_VIEW} with(nolock) where 1=1 {sqlWhere}";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public string GetSelectTopFromByMySql(int limit, int start,string orderby,string orderbytag, string sqlWhere)
|
public override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_ANCESTOR_PERSON_ID ; int offset = limit * (start - 1);
|
public string GetSelectTopFromByMySql(int limit, int start, string orderby, string orderbytag, string sqlWhere)
|
||||||
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} ";
|
{
|
||||||
}
|
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
|
||||||
|
|
||||||
public string GetSelectTopFromBySqlServer(int limit, int start,string orderby,string orderbytag, string sqlWhere)
|
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} ";
|
||||||
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}" : "";
|
public string GetSelectTopFromBySqlServer(int limit, int start, string orderby, string orderbytag, string sqlWhere)
|
||||||
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)}";
|
{
|
||||||
}
|
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
|
||||||
|
|
||||||
public FamilyTreeViewEntity Clone()
|
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)}";
|
||||||
FamilyTreeViewEntity result = new FamilyTreeViewEntity();
|
}
|
||||||
result.CopyFrom(this);
|
|
||||||
|
|
||||||
return result;
|
public FamilyTreeViewEntity Clone()
|
||||||
}
|
{
|
||||||
|
FamilyTreeViewEntity result = new FamilyTreeViewEntity();
|
||||||
|
result.CopyFrom(this);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,8 @@ namespace CloudBuilder.Master.Policy
|
|||||||
|
|
||||||
/// <summary>文章正文/配图目录(article_filename.file_name 对应,前端上传 FilePath 头须一致)</summary>
|
/// <summary>文章正文/配图目录(article_filename.file_name 对应,前端上传 FilePath 头须一致)</summary>
|
||||||
public const string ARTICLE_IMAGE = "ArticleImage";
|
public const string ARTICLE_IMAGE = "ArticleImage";
|
||||||
|
|
||||||
|
/// <summary>族谱封面目录(family_tree.tree_cover_filename 对应,前端上传 FilePath 头须一致)</summary>
|
||||||
|
public const string TREE_COVER = "TreeCover";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-1
@@ -22,8 +22,16 @@ namespace CloudBuilder.Security.Service.Subscriber
|
|||||||
SecurityUserActorEntity ent = new SecurityUserActorEntity();
|
SecurityUserActorEntity ent = new SecurityUserActorEntity();
|
||||||
ent.UserName = ((SecurityUserEntity)eventData.EventSource).UserName;
|
ent.UserName = ((SecurityUserEntity)eventData.EventSource).UserName;
|
||||||
ent.ActorName = SecurityUserPolicy.SECURITY_USER_BASE_ACTOR;
|
ent.ActorName = SecurityUserPolicy.SECURITY_USER_BASE_ACTOR;
|
||||||
|
|
||||||
repository.InsertNow(ent);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ namespace CloudBuilder.Security.Policy
|
|||||||
/// 普通用户首次注册给予的角色
|
/// 普通用户首次注册给予的角色
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SECURITY_USER_BASE_ACTOR = "SecurityUserBaseActor";
|
public const string SECURITY_USER_BASE_ACTOR = "SecurityUserBaseActor";
|
||||||
|
public const string SYSTEM_ACTOR = "system_actor";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户登录可以出错的次数
|
/// 用户登录可以出错的次数
|
||||||
|
|||||||
@@ -1017,6 +1017,7 @@ SET @saved_cs_client = @@character_set_client;
|
|||||||
1 AS `tree_id`,
|
1 AS `tree_id`,
|
||||||
1 AS `public_indc`,
|
1 AS `public_indc`,
|
||||||
1 AS `brief`,
|
1 AS `brief`,
|
||||||
|
1 AS `tree_cover_filename`,
|
||||||
1 AS `ancestor_person_id`,
|
1 AS `ancestor_person_id`,
|
||||||
1 AS `surname`,
|
1 AS `surname`,
|
||||||
1 AS `genealogy_name`,
|
1 AS `genealogy_name`,
|
||||||
@@ -2601,7 +2602,7 @@ SET character_set_client = @saved_cs_client;
|
|||||||
/*!50001 SET collation_connection = utf8mb4_0900_ai_ci */;
|
/*!50001 SET collation_connection = utf8mb4_0900_ai_ci */;
|
||||||
/*!50001 CREATE ALGORITHM=UNDEFINED */
|
/*!50001 CREATE ALGORITHM=UNDEFINED */
|
||||||
/*!50013 SQL SECURITY DEFINER */
|
/*!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_client = @saved_cs_client */;
|
||||||
/*!50001 SET character_set_results = @saved_cs_results */;
|
/*!50001 SET character_set_results = @saved_cs_results */;
|
||||||
/*!50001 SET collation_connection = @saved_col_connection */;
|
/*!50001 SET collation_connection = @saved_col_connection */;
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
|
|||||||
|
|
||||||
const STORAGE_KEY = "api-endpoint-setting";
|
const STORAGE_KEY = "api-endpoint-setting";
|
||||||
|
|
||||||
/** 线路设置:mode=wan 走公网(默认);lan 走内网直连(lanUrl 必须是合法 http(s) 地址) */
|
/** 线路设置:mode=wan 走公网、lan 走内网直连;两条线路的地址都可自定义。
|
||||||
|
* wanUrl 留空表示沿用内置默认公网地址(WebSitePolicy),填了则覆盖;lanUrl 在 lan 模式下必填合法。 */
|
||||||
export interface EndpointSetting {
|
export interface EndpointSetting {
|
||||||
mode: "wan" | "lan";
|
mode: "wan" | "lan";
|
||||||
|
/** 自定义公网地址(可空=用内置默认) */
|
||||||
|
wanUrl: string;
|
||||||
|
/** 内网直连地址(lan 模式必填) */
|
||||||
lanUrl: string;
|
lanUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,15 +37,20 @@ const WAN_BASE_URL = import.meta.env.DEV
|
|||||||
? devBaseUrl
|
? devBaseUrl
|
||||||
: WebSitePolicy.WebApiRoot;
|
: WebSitePolicy.WebApiRoot;
|
||||||
|
|
||||||
/** 规范化内网地址:允许省略 http://,去掉末尾斜杠;非法返回空串(调用方据此判无效) */
|
/** 规范化地址:省略协议时补默认方案(内网 http、公网 https),去末尾斜杠;非法返回空串(调用方据此判无效) */
|
||||||
export function normalizeLanUrl(raw: string): string {
|
export function normalizeEndpointUrl(
|
||||||
|
raw: string,
|
||||||
|
defaultScheme: "http" | "https" = "http",
|
||||||
|
): string {
|
||||||
const text = String(raw ?? "")
|
const text = String(raw ?? "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/\/+$/, "");
|
.replace(/\/+$/, "");
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return "";
|
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 : "";
|
return /^https?:\/\/[^\s/]+$/i.test(withScheme) ? withScheme : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,14 +60,19 @@ function readSetting(): EndpointSetting {
|
|||||||
| Partial<EndpointSetting>
|
| Partial<EndpointSetting>
|
||||||
| "";
|
| "";
|
||||||
if (raw && typeof raw === "object") {
|
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 模式但地址非法)时按公网兜底,避免全站请求打到空地址
|
// 存储里有脏数据(lan 模式但地址非法)时按公网兜底,避免全站请求打到空地址
|
||||||
return { mode: raw.mode === "lan" && lanUrl ? "lan" : "wan", lanUrl };
|
return {
|
||||||
|
mode: raw.mode === "lan" && lanUrl ? "lan" : "wan",
|
||||||
|
wanUrl,
|
||||||
|
lanUrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 存储读取失败按默认公网处理,不阻塞启动
|
// 存储读取失败按默认公网处理,不阻塞启动
|
||||||
}
|
}
|
||||||
return { mode: "wan", lanUrl: "" };
|
return { mode: "wan", wanUrl: "", lanUrl: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模块级缓存:请求层高频读地址,不每次都走同步 storage
|
// 模块级缓存:请求层高频读地址,不每次都走同步 storage
|
||||||
@@ -69,16 +83,26 @@ export function currentEndpoint(): EndpointSetting {
|
|||||||
return { ...cached };
|
return { ...cached };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前生效的后端地址:内网模式且地址合法时返回内网地址,否则公网 */
|
/** 内置默认公网地址(未自定义公网时沿用;供设置页展示占位) */
|
||||||
|
export function defaultWanUrl(): string {
|
||||||
|
return WAN_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前生效的后端地址:内网模式取内网地址,公网模式取自定义公网地址(留空回落内置默认) */
|
||||||
export function apiBaseUrl(): string {
|
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 {
|
export function saveEndpoint(setting: EndpointSetting): string {
|
||||||
const lanUrl = normalizeLanUrl(setting.lanUrl);
|
const wanUrl = normalizeEndpointUrl(setting.wanUrl, "https");
|
||||||
|
const lanUrl = normalizeEndpointUrl(setting.lanUrl, "http");
|
||||||
cached = {
|
cached = {
|
||||||
mode: setting.mode === "lan" && lanUrl ? "lan" : "wan",
|
mode: setting.mode === "lan" && lanUrl ? "lan" : "wan",
|
||||||
|
wanUrl,
|
||||||
lanUrl,
|
lanUrl,
|
||||||
};
|
};
|
||||||
uni.setStorageSync(STORAGE_KEY, cached);
|
uni.setStorageSync(STORAGE_KEY, cached);
|
||||||
@@ -87,9 +111,12 @@ export function saveEndpoint(setting: EndpointSetting): string {
|
|||||||
return apiBaseUrl();
|
return apiBaseUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 测试内网地址连通性:用免鉴权的 RSA 公钥接口探活(与登录首调同源,最接近真实可用性) */
|
/** 测试地址连通性:用免鉴权的 RSA 公钥接口探活(与登录首调同源,最接近真实可用性) */
|
||||||
export function testEndpoint(rawUrl: string): Promise<boolean> {
|
export function testEndpoint(
|
||||||
const url = normalizeLanUrl(rawUrl);
|
rawUrl: string,
|
||||||
|
defaultScheme: "http" | "https" = "http",
|
||||||
|
): Promise<boolean> {
|
||||||
|
const url = normalizeEndpointUrl(rawUrl, defaultScheme);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,21 @@ function throwRequestError(message: string, handled: boolean): never {
|
|||||||
throw handled ? markErrorNotified(err) : err;
|
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(不区分账号不存在/密码错,防撞库探测),
|
* 登录失败后端只下发一句 certification(不区分账号不存在/密码错,防撞库探测),
|
||||||
@@ -335,11 +350,11 @@ const httpInterceptor: RequestInterceptor = {
|
|||||||
// 网络层错误
|
// 网络层错误
|
||||||
if (errMsg && errMsg.includes("Failed to connect")) {
|
if (errMsg && errMsg.includes("Failed to connect")) {
|
||||||
meta.toast && notifyError("", "网络错误");
|
meta.toast && notifyError("", "网络错误");
|
||||||
throwRequestError("网络错误", !!meta.toast);
|
throwNetworkError("网络错误", !!meta.toast);
|
||||||
}
|
}
|
||||||
if (errMsg && errMsg.includes("request:fail")) {
|
if (errMsg && errMsg.includes("request:fail")) {
|
||||||
meta.toast && notifyError("", "请求错误:未知");
|
meta.toast && notifyError("", "请求错误:未知");
|
||||||
throwRequestError("请求错误:未知", !!meta.toast);
|
throwNetworkError("请求错误:未知", !!meta.toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 且未重试过:共用刷新 + 重试
|
// 401 且未重试过:共用刷新 + 重试
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 「访问线路」底部弹层(App 专用,公网 / 内网直连地址均可自定义)。
|
||||||
|
*
|
||||||
|
* 由「我的 → 访问线路」与登录页共用:
|
||||||
|
* - 「我的」页:保存后若服务器(生效地址)真的变了,父页负责清登录态回登录页重新认证;
|
||||||
|
* - 登录页:登录遇到「服务器不可达」时自动弹出,让用户改地址后原地重试登录(本就未登录,无需登出)。
|
||||||
|
*
|
||||||
|
* 组件只负责:草稿编辑 / 连通性测试 / 校验 / saveEndpoint 落盘,
|
||||||
|
* 并通过 saved 事件把「是否真的换了服务器(changed)+ 前后地址」抛给父页决策后续动作。
|
||||||
|
* 地址留空语义:公网留空=沿用内置默认(WebSitePolicy),内网留空非法。
|
||||||
|
*/
|
||||||
|
import { useLocale } from "uview-pro";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { apiBaseUrl, currentEndpoint, defaultWanUrl, normalizeEndpointUrl, saveEndpoint, testEndpoint } from "@/common/apiEndpoint";
|
||||||
|
import { useThemeTokens } from "@/composables";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 弹层显隐(v-model) */
|
||||||
|
modelValue: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: boolean): void;
|
||||||
|
(e: "saved", payload: { changed: boolean; previousUrl: string; currentUrl: string }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { primary, accentGradient, accentShadow } = useThemeTokens();
|
||||||
|
const themeVars = computed(() => ({
|
||||||
|
"--fg-primary": primary.value,
|
||||||
|
"--fg-gradient": accentGradient.value,
|
||||||
|
"--fg-shadow": accentShadow.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 弹层草稿:确认前不动真实线路,取消即作废 */
|
||||||
|
const lineModeDraft = ref<"wan" | "lan">("wan");
|
||||||
|
const lineLanDraft = ref("");
|
||||||
|
const lineWanDraft = ref("");
|
||||||
|
const lineTesting = ref(false);
|
||||||
|
const lineTestResult = ref<"" | "ok" | "fail">("");
|
||||||
|
|
||||||
|
// 每次打开都从当前生效设置回填草稿,并清掉上次测活结果
|
||||||
|
watch(() => props.modelValue, (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const setting = currentEndpoint();
|
||||||
|
lineModeDraft.value = setting.mode;
|
||||||
|
lineLanDraft.value = setting.lanUrl;
|
||||||
|
lineWanDraft.value = setting.wanUrl;
|
||||||
|
lineTestResult.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 当前选中线路对应的地址草稿(公网/内网共用一个输入框,读写各自草稿) */
|
||||||
|
const lineUrlDraft = computed({
|
||||||
|
get: () => (lineModeDraft.value === "lan" ? lineLanDraft.value : lineWanDraft.value),
|
||||||
|
set: (value: string) => {
|
||||||
|
if (lineModeDraft.value === "lan") {
|
||||||
|
lineLanDraft.value = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lineWanDraft.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 输入框占位:内网给示例,公网提示留空回落内置默认地址 */
|
||||||
|
const lineUrlPlaceholder = computed(() => {
|
||||||
|
if (lineModeDraft.value === "lan") {
|
||||||
|
return t("MinePage.lineLanPlaceholder");
|
||||||
|
}
|
||||||
|
return t("MinePage.lineWanPlaceholder").replace("{default}", defaultWanUrl());
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 当前草稿规范化后的地址(非法或空返回空串):内网补 http、公网补 https */
|
||||||
|
function draftUrlOrEmpty(): string {
|
||||||
|
return normalizeEndpointUrl(lineUrlDraft.value, lineModeDraft.value === "lan" ? "http" : "https");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测试地址连通性:用 RSA 公钥接口探活(与登录首调同源);公网留空时探内置默认 */
|
||||||
|
async function handleTestLine(): Promise<void> {
|
||||||
|
if (lineTesting.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scheme = lineModeDraft.value === "lan" ? "http" : "https";
|
||||||
|
const url = draftUrlOrEmpty() || (lineModeDraft.value === "wan" ? defaultWanUrl() : "");
|
||||||
|
if (!url) {
|
||||||
|
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lineTesting.value = true;
|
||||||
|
lineTestResult.value = "";
|
||||||
|
const ok = await testEndpoint(url, scheme);
|
||||||
|
lineTesting.value = false;
|
||||||
|
lineTestResult.value = ok ? "ok" : "fail";
|
||||||
|
if (!ok) {
|
||||||
|
uni.showToast({ title: t("MinePage.lineTestFail"), icon: "none" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): void {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存线路:内网必填合法、公网可留空(用默认)。落盘后把「是否真的换了服务器」抛给父页 */
|
||||||
|
function handleSaveLine(): void {
|
||||||
|
const mode = lineModeDraft.value;
|
||||||
|
const normalized = draftUrlOrEmpty();
|
||||||
|
// 内网:地址必填合法;公网:填了才校验(留空合法=用默认)
|
||||||
|
if (mode === "lan" && !normalized) {
|
||||||
|
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === "wan" && lineWanDraft.value.trim() && !normalized) {
|
||||||
|
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousUrl = apiBaseUrl();
|
||||||
|
saveEndpoint({ mode, wanUrl: lineWanDraft.value, lanUrl: lineLanDraft.value });
|
||||||
|
const currentUrl = apiBaseUrl();
|
||||||
|
close();
|
||||||
|
emit("saved", { changed: currentUrl !== previousUrl, previousUrl, currentUrl });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<u-popup :model-value="modelValue" mode="bottom" round="24" @close="close">
|
||||||
|
<view class="line-sheet" :style="themeVars">
|
||||||
|
<text class="line-sheet__title">
|
||||||
|
{{ t("MinePage.accessLine") }}
|
||||||
|
</text>
|
||||||
|
<text class="line-sheet__hint">
|
||||||
|
{{ t("MinePage.lineHint") }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 公网:默认线路,地址可自定义(留空用内置默认) -->
|
||||||
|
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'wan' }"
|
||||||
|
@tap="lineModeDraft = 'wan'">
|
||||||
|
<view class="line-option__body">
|
||||||
|
<text class="line-option__name">
|
||||||
|
{{ t("MinePage.lineWan") }}
|
||||||
|
</text>
|
||||||
|
<text class="line-option__desc">
|
||||||
|
{{ t("MinePage.lineWanDesc") }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'wan' }" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内网直连:需与服务器同局域网,地址手填 -->
|
||||||
|
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'lan' }"
|
||||||
|
@tap="lineModeDraft = 'lan'">
|
||||||
|
<view class="line-option__body">
|
||||||
|
<text class="line-option__name">
|
||||||
|
{{ t("MinePage.lineLan") }}
|
||||||
|
</text>
|
||||||
|
<text class="line-option__desc">
|
||||||
|
{{ t("MinePage.lineLanDesc") }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'lan' }" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 地址输入:公网/内网都按当前选中线路展开;测连通用免鉴权的 RSA 公钥接口 -->
|
||||||
|
<view class="line-lan">
|
||||||
|
<input v-model="lineUrlDraft" class="line-lan__input" :placeholder="lineUrlPlaceholder"
|
||||||
|
placeholder-class="line-lan__ph">
|
||||||
|
<view class="line-lan__test" @tap="handleTestLine">
|
||||||
|
<text class="line-lan__test-text">
|
||||||
|
{{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-if="lineTestResult === 'ok'" class="line-lan__result line-lan__result--ok">
|
||||||
|
{{ t("MinePage.lineTestOk") }}
|
||||||
|
</text>
|
||||||
|
<text v-else-if="lineTestResult === 'fail'" class="line-lan__result line-lan__result--fail">
|
||||||
|
{{ t("MinePage.lineTestFail") }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<view class="line-sheet__actions">
|
||||||
|
<view class="line-sheet__btn line-sheet__btn--ghost" @tap="close">
|
||||||
|
<text class="line-sheet__btn-text line-sheet__btn-text--ghost">
|
||||||
|
{{ t("MinePage.lineCancel") }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="line-sheet__btn line-sheet__btn--primary" @tap="handleSaveLine">
|
||||||
|
<text class="line-sheet__btn-text">
|
||||||
|
{{ t("MinePage.lineSave") }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* ===== 访问线路弹层(底部抽屉,同 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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -77,6 +77,24 @@ const treeGlyph = `data:image/svg+xml,${encodeURIComponent([
|
|||||||
"</svg>",
|
"</svg>",
|
||||||
].join(""))}`;
|
].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<FamilyTreeViewEntity[]>([]);
|
const trees = ref<FamilyTreeViewEntity[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const errorText = ref("");
|
const errorText = ref("");
|
||||||
@@ -531,9 +549,9 @@ const memberFilter = ref<MemberFilter>("all");
|
|||||||
|
|
||||||
/** 邀请族人弹层已随底部分享入口取消(无后端接口占位),相关函数/样式/词条已同步清理,待邀请 service 就绪再重建 */
|
/** 邀请族人弹层已随底部分享入口取消(无后端接口占位),相关函数/样式/词条已同步清理,待邀请 service 就绪再重建 */
|
||||||
|
|
||||||
/** 编辑族谱信息弹层:只含 名称/始祖谱名/简介/对外公开 四字段,关系与成员在其他入口分开维护 */
|
/** 编辑族谱信息弹层:只含 名称/简介/对外公开 三字段(始祖谱名不展示,仍随快照守护),关系与成员在其他入口分开维护 */
|
||||||
const treeEditVisible = ref(false);
|
const treeEditVisible = ref(false);
|
||||||
/** 打开弹层时的四字段快照:取消时本地还原直改,免去重载带来的“处理中” */
|
/** 打开弹层时的字段快照(含不展示的始祖谱名):取消时本地还原直改,免去重载带来的“处理中” */
|
||||||
const treeEditSnapshot = ref({ TreeName: "", AncestorPersonGenealogyName: "", Brief: "", PublicIndc: "N" });
|
const treeEditSnapshot = ref({ TreeName: "", AncestorPersonGenealogyName: "", Brief: "", PublicIndc: "N" });
|
||||||
|
|
||||||
/** hero 统计「位族人」:全谱总人数(FindByCurrentPerson 只下发直系小集合,切当前族员会跳变,改走搜索链 Total 全量口径) */
|
/** hero 统计「位族人」:全谱总人数(FindByCurrentPerson 只下发直系小集合,切当前族员会跳变,改走搜索链 Total 全量口径) */
|
||||||
@@ -645,7 +663,52 @@ async function onPublicToggle(value: boolean): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开「编辑族谱信息」弹层:浏览态四字段快改入口(无修改权限挡回),先记快照供取消还原 */
|
/** 点封面上传族谱封面(与首页头像同款交互):单图 -> /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 {
|
function openTreeEditor(): void {
|
||||||
if (!isCreate.value && !data.value.IsCanAmend) {
|
if (!isCreate.value && !data.value.IsCanAmend) {
|
||||||
toast(t("FamilyTreePage.noAmendTip"));
|
toast(t("FamilyTreePage.noAmendTip"));
|
||||||
@@ -1693,177 +1756,185 @@ defineExpose({
|
|||||||
<view class="tree-page" :style="[{ background: themeBackground }, themeVars]">
|
<view class="tree-page" :style="[{ background: themeBackground }, themeVars]">
|
||||||
<!-- ===== 列表态:软底分区面板(标题 + 搜索/新建/伸缩圆钮组 + 白底族谱卡) ===== -->
|
<!-- ===== 列表态:软底分区面板(标题 + 搜索/新建/伸缩圆钮组 + 白底族谱卡) ===== -->
|
||||||
<!-- 隐藏原生滚动条:否则 Chrome 滚动条占右侧 ~17px,面板左右留白不对称 -->
|
<!-- 隐藏原生滚动条:否则 Chrome 滚动条占右侧 ~17px,面板左右留白不对称 -->
|
||||||
<scroll-view v-if="!inDetail" scroll-y :show-scrollbar="false" class="tree-page__body tree-page__body--list">
|
<view v-if="!inDetail" class="tree-page__scroll-wrap tree-page__scroll-wrap--list">
|
||||||
<!-- ===== 顶部两张卡片:用户身份 + 网盘用量 ===== -->
|
<scroll-view scroll-y :show-scrollbar="false" class="tree-page__scroll tree-page__body--list">
|
||||||
<view class="home-cards">
|
<!-- ===== 顶部两张卡片:用户身份 + 网盘用量 ===== -->
|
||||||
<view class="home-card home-card--user">
|
<view class="home-cards">
|
||||||
<view class="home-card__sheen" />
|
<view class="home-card home-card--user">
|
||||||
<view class="home-card__glow" />
|
<view class="home-card__sheen" />
|
||||||
<image class="home-card__mark" :src="brandGlyph" mode="aspectFit" />
|
<view class="home-card__glow" />
|
||||||
<view class="home-card__avatar" @tap="onUploadAvatar">
|
<image class="home-card__mark" :src="brandGlyph" mode="aspectFit" />
|
||||||
<image v-if="storage.AvatarUrl" class="home-card__avatar-img" :src="absoluteFileUrl(storage.AvatarUrl)"
|
<view class="home-card__avatar" @tap="onUploadAvatar">
|
||||||
mode="aspectFill" />
|
<image v-if="storage.AvatarUrl" class="home-card__avatar-img" :src="absoluteFileUrl(storage.AvatarUrl)"
|
||||||
<text v-else class="home-card__avatar-ph">{{ displayName ? displayName.slice(0, 1) : "?" }}</text>
|
mode="aspectFill" />
|
||||||
<view class="home-card__avatar-cam">
|
<text v-else class="home-card__avatar-ph">{{ displayName ? displayName.slice(0, 1) : "?" }}</text>
|
||||||
<u-icon name="camera-fill" size="24" color="#ffffff" />
|
<view class="home-card__avatar-cam">
|
||||||
</view>
|
<u-icon name="camera-fill" size="24" color="#ffffff" />
|
||||||
</view>
|
|
||||||
<view class="home-card__uinfo">
|
|
||||||
<view class="home-card__uname-row">
|
|
||||||
<text class="home-card__uname">{{ displayName || "-" }}</text>
|
|
||||||
<view class="home-card__utype" :class="{ 'home-card__utype--tap': currentType === 'U' }"
|
|
||||||
:style="{ background: userTypeBg }" @tap="onTapUserType">
|
|
||||||
<image class="home-card__utype-icon" :src="userTypeIcon" mode="aspectFit" />
|
|
||||||
<text class="home-card__utype-text">{{ userTypeText }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="home-card__welcome">{{ t("storage.welcome") }}</text>
|
<view class="home-card__uinfo">
|
||||||
</view>
|
<view class="home-card__uname-row">
|
||||||
</view>
|
<text class="home-card__uname">{{ displayName || "-" }}</text>
|
||||||
<view class="home-card home-card--storage">
|
<view class="home-card__utype" :class="{ 'home-card__utype--tap': currentType === 'U' }"
|
||||||
<view class="home-card__sglow" />
|
:style="{ background: userTypeBg }" @tap="onTapUserType">
|
||||||
<view class="home-card__srow">
|
<image class="home-card__utype-icon" :src="userTypeIcon" mode="aspectFit" />
|
||||||
<view class="home-card__stitle-wrap">
|
<text class="home-card__utype-text">{{ userTypeText }}</text>
|
||||||
<view class="home-card__sicon">
|
|
||||||
<image class="home-card__sicon-img" :src="cloudGlyph" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
<text class="home-card__stitle">{{ t("storage.title") }}</text>
|
|
||||||
<!-- #ifdef H5 -->
|
|
||||||
<view class="home-card__dlapp" @tap="onDownloadApp">
|
|
||||||
<image class="home-card__dlapp-icon" :src="downloadGlyph" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
<!-- #endif -->
|
|
||||||
</view>
|
|
||||||
<text class="home-card__spct" :class="{ 'home-card__spct--over': storageOver }">{{ storagePercentText
|
|
||||||
}}</text>
|
|
||||||
</view>
|
|
||||||
<view class="home-card__bar">
|
|
||||||
<view class="home-card__bar-fill" :class="{ 'home-card__bar-fill--over': storageOver }"
|
|
||||||
:style="{ width: storageBarWidth }" />
|
|
||||||
</view>
|
|
||||||
<view class="home-card__snums">
|
|
||||||
<text class="home-card__snum">{{ t("storage.used") }} {{ storageUsedText }}</text>
|
|
||||||
<text class="home-card__snum">{{ t("storage.total") }} {{ storageQuotaText }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="tree-panel">
|
|
||||||
<view class="tree-panel__head">
|
|
||||||
<text class="tree-panel__title">{{ t("FamilyTreePage.listTitle") }}</text>
|
|
||||||
<view class="tree-panel__acts">
|
|
||||||
<view class="tree-panel__act" @tap="onToggleSearch">
|
|
||||||
<u-icon name="search" size="34" :color="primary" />
|
|
||||||
</view>
|
|
||||||
<view class="tree-panel__act tree-panel__act--add" @tap="onCreateTree">
|
|
||||||
<u-icon name="plus" size="30" color="#ffffff" />
|
|
||||||
</view>
|
|
||||||
<view class="tree-panel__act" @tap="listCollapsed = !listCollapsed">
|
|
||||||
<u-icon :name="listCollapsed ? 'arrow-up' : 'arrow-down'" size="34" :color="primary" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="showSearch" class="tree-panel__search">
|
|
||||||
<search-input v-model="keyword" :placeholder="t('FamilyTreePage.searchPlaceholder')" @search="onSearch" />
|
|
||||||
</view>
|
|
||||||
<view v-if="!listCollapsed" class="tree-panel__list">
|
|
||||||
<view v-if="loading" class="tree-page__state">
|
|
||||||
<text class="tree-page__state-text">{{ t("FilePage.loading") }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-else-if="errorText" class="tree-page__state">
|
|
||||||
<text class="tree-page__state-text tree-page__state-text--error">{{ errorText }}</text>
|
|
||||||
<text class="tree-page__retry" @click="onRetry">{{ t("FilePage.retryBut") }}</text>
|
|
||||||
</view>
|
|
||||||
<template v-else>
|
|
||||||
<view v-if="!filteredTrees.length" class="tree-page__empty">
|
|
||||||
<text class="tree-page__state-text">{{ t("FamilyTreePage.chartEmpty") }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-for="row in filteredTrees" :key="row.TreeId" class="tree-card" @tap="onOpenTree(row)">
|
|
||||||
<view class="tree-card__avatar">
|
|
||||||
<image class="tree-card__glyph" :src="treeGlyph" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
<view class="tree-card__main">
|
|
||||||
<text class="tree-card__name">{{ row.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
|
||||||
<text class="tree-card__desc">{{ row.Brief || t("FamilyTreePage.noBrief") }}</text>
|
|
||||||
<view class="tree-card__meta">
|
|
||||||
<text class="tree-card__chip tree-card__chip--code">{{ row.TreeId }}</text>
|
|
||||||
<text v-if="row.PublicIndc === 'Y'" class="tree-card__chip tree-card__chip--public">
|
|
||||||
{{ t("FamilyTreePage.chipPublic") }}
|
|
||||||
</text>
|
|
||||||
<text v-else class="tree-card__chip">{{ t("FamilyTreePage.chipPrivate") }}</text>
|
|
||||||
<text v-if="row.Surname || row.GenealogyName" class="tree-card__ancestor">
|
|
||||||
{{ `${row.Surname}${row.GenealogyName}` }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<u-icon name="arrow-right" size="30" color="#c0c4cc" />
|
<text class="home-card__welcome">{{ t("storage.welcome") }}</text>
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- ===== 有家文化传承:与族谱面板并列的文章卡 ===== -->
|
|
||||||
<view class="tree-panel article-panel">
|
|
||||||
<view class="tree-panel__head">
|
|
||||||
<text class="tree-panel__title">{{ t("article.title") }}</text>
|
|
||||||
<view class="tree-panel__acts">
|
|
||||||
<view class="tree-panel__act" @tap="onToggleArticleSearch">
|
|
||||||
<u-icon name="search" size="34" :color="primary" />
|
|
||||||
</view>
|
|
||||||
<view class="tree-panel__act tree-panel__act--add" @tap="onCreateArticle">
|
|
||||||
<u-icon name="plus" size="30" color="#ffffff" />
|
|
||||||
</view>
|
|
||||||
<view class="tree-panel__act" @tap="articleCollapsed = !articleCollapsed">
|
|
||||||
<u-icon :name="articleCollapsed ? 'arrow-up' : 'arrow-down'" size="34" :color="primary" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="home-card home-card--storage">
|
||||||
<view v-if="showArticleSearch" class="tree-panel__search">
|
<view class="home-card__sglow" />
|
||||||
<search-input v-model="articleKeyword" :placeholder="t('article.searchPlaceholder')"
|
<view class="home-card__srow">
|
||||||
@search="onArticleSearch" />
|
<view class="home-card__stitle-wrap">
|
||||||
</view>
|
<view class="home-card__sicon">
|
||||||
<view v-if="!articleCollapsed" class="tree-panel__list">
|
<image class="home-card__sicon-img" :src="cloudGlyph" mode="aspectFit" />
|
||||||
<view v-if="articlesApi.loading.value" class="tree-page__state">
|
|
||||||
<text class="tree-page__state-text">{{ t("FilePage.loading") }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-else-if="articlesApi.errorText.value" class="tree-page__state">
|
|
||||||
<text class="tree-page__state-text tree-page__state-text--error">{{ articlesApi.errorText.value }}</text>
|
|
||||||
<text class="tree-page__retry" @click="loadArticles">{{ t("FilePage.retryBut") }}</text>
|
|
||||||
</view>
|
|
||||||
<template v-else>
|
|
||||||
<view v-if="!homeArticles.length" class="tree-page__empty">
|
|
||||||
<text class="tree-page__state-text">{{ t("article.empty") }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-for="row in homeArticles" :key="row.ArticleName" class="article-card"
|
|
||||||
@tap="onOpenArticle(row.ArticleName)">
|
|
||||||
<view class="article-card__cover">
|
|
||||||
<image v-if="row.CoverUrl || row.ArticleCoverFilename" class="article-card__cover-img"
|
|
||||||
:src="absoluteFileUrl(row.CoverUrl) || backupFileUrl('ArticleCover', row.ArticleCoverFilename)"
|
|
||||||
mode="aspectFill" />
|
|
||||||
<image v-else class="article-card__glyph" :src="articleGlyph" mode="aspectFit" />
|
|
||||||
</view>
|
|
||||||
<view class="article-card__main">
|
|
||||||
<view class="article-card__titlerow">
|
|
||||||
<text class="article-card__name">{{ row.ArticleTitle || t("article.unnamed") }}</text>
|
|
||||||
<text v-if="row.TopIndc === 'Y'" class="article-card__chip article-card__chip--top">{{
|
|
||||||
t("article.tagTop") }}</text>
|
|
||||||
<text v-if="row.ApprovedIndc !== 'Y'" class="article-card__chip article-card__chip--pending">{{
|
|
||||||
t("article.tagPending") }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<text class="article-card__desc">{{ articlesApi.plainText(row.Brief, 100) || t("article.noBrief")
|
<text class="home-card__stitle">{{ t("storage.title") }}</text>
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<view class="home-card__dlapp" @tap="onDownloadApp">
|
||||||
|
<image class="home-card__dlapp-icon" :src="downloadGlyph" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
|
<text class="home-card__spct" :class="{ 'home-card__spct--over': storageOver }">{{ storagePercentText
|
||||||
}}</text>
|
}}</text>
|
||||||
<view class="article-card__meta">
|
|
||||||
<image v-if="row.AuthorAvatarUrl" class="article-card__avatar"
|
|
||||||
:src="absoluteFileUrl(row.AuthorAvatarUrl)" mode="aspectFill" />
|
|
||||||
<view v-else class="article-card__avatar article-card__avatar--empty" />
|
|
||||||
<text class="article-card__author">{{ row.AuthorLocalName || t("article.unknownAuthor") }}</text>
|
|
||||||
<text class="article-card__date">{{ articlesApi.formatDate(row.CreatedDatetime) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<u-icon name="arrow-right" size="30" color="#c0c4cc" />
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
<view class="home-card__bar">
|
||||||
|
<view class="home-card__bar-fill" :class="{ 'home-card__bar-fill--over': storageOver }"
|
||||||
|
:style="{ width: storageBarWidth }" />
|
||||||
|
</view>
|
||||||
|
<view class="home-card__snums">
|
||||||
|
<text class="home-card__snum">{{ t("storage.used") }} {{ storageUsedText }}</text>
|
||||||
|
<text class="home-card__snum">{{ t("storage.total") }} {{ storageQuotaText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="tree-panel">
|
||||||
</scroll-view>
|
<view class="tree-panel__head">
|
||||||
|
<text class="tree-panel__title">{{ t("FamilyTreePage.listTitle") }}</text>
|
||||||
|
<view class="tree-panel__acts">
|
||||||
|
<view class="tree-panel__act" @tap="onToggleSearch">
|
||||||
|
<u-icon name="search" size="34" :color="primary" />
|
||||||
|
</view>
|
||||||
|
<view class="tree-panel__act tree-panel__act--add" @tap="onCreateTree">
|
||||||
|
<u-icon name="plus" size="30" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
<view class="tree-panel__act" @tap="listCollapsed = !listCollapsed">
|
||||||
|
<u-icon :name="listCollapsed ? 'arrow-up' : 'arrow-down'" size="34" :color="primary" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="showSearch" class="tree-panel__search">
|
||||||
|
<search-input v-model="keyword" :placeholder="t('FamilyTreePage.searchPlaceholder')" @search="onSearch" />
|
||||||
|
</view>
|
||||||
|
<view v-if="!listCollapsed" class="tree-panel__list">
|
||||||
|
<view v-if="loading" class="tree-page__state">
|
||||||
|
<text class="tree-page__state-text">{{ t("FilePage.loading") }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorText" class="tree-page__state">
|
||||||
|
<text class="tree-page__state-text tree-page__state-text--error">{{ errorText }}</text>
|
||||||
|
<text class="tree-page__retry" @click="onRetry">{{ t("FilePage.retryBut") }}</text>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="!filteredTrees.length" class="tree-page__empty">
|
||||||
|
<text class="tree-page__state-text">{{ t("FamilyTreePage.chartEmpty") }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="row in filteredTrees" :key="row.TreeId" class="tree-card" @tap="onOpenTree(row)">
|
||||||
|
<view class="tree-card__top">
|
||||||
|
<view class="tree-card__avatar">
|
||||||
|
<image v-if="row.TreeCoverFilename" class="tree-card__cover"
|
||||||
|
:src="treeCoverUrl(row.TreeCoverFilename)" mode="aspectFill" />
|
||||||
|
<image v-else class="tree-card__glyph" :src="treeGlyph" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="tree-card__main">
|
||||||
|
<text class="tree-card__name">{{ row.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
||||||
|
<view class="tree-card__meta">
|
||||||
|
<text class="tree-card__chip tree-card__chip--code">{{ row.TreeId }}</text>
|
||||||
|
<text v-if="row.PublicIndc === 'Y'" class="tree-card__chip tree-card__chip--public">
|
||||||
|
{{ t("FamilyTreePage.chipPublic") }}
|
||||||
|
</text>
|
||||||
|
<text v-else class="tree-card__chip">{{ t("FamilyTreePage.chipPrivate") }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 简介整行在下:按字数在末尾截断,完整内容进详情看 -->
|
||||||
|
<text class="tree-card__desc">{{ briefLine(row.Brief) }}</text>
|
||||||
|
<u-icon class="tree-card__arrow" name="arrow-right" size="30" color="#c0c4cc" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- ===== 有家文化传承:与族谱面板并列的文章卡 ===== -->
|
||||||
|
<view class="tree-panel article-panel">
|
||||||
|
<view class="tree-panel__head">
|
||||||
|
<text class="tree-panel__title">{{ t("article.title") }}</text>
|
||||||
|
<view class="tree-panel__acts">
|
||||||
|
<view class="tree-panel__act" @tap="onToggleArticleSearch">
|
||||||
|
<u-icon name="search" size="34" :color="primary" />
|
||||||
|
</view>
|
||||||
|
<view class="tree-panel__act tree-panel__act--add" @tap="onCreateArticle">
|
||||||
|
<u-icon name="plus" size="30" color="#ffffff" />
|
||||||
|
</view>
|
||||||
|
<view class="tree-panel__act" @tap="articleCollapsed = !articleCollapsed">
|
||||||
|
<u-icon :name="articleCollapsed ? 'arrow-up' : 'arrow-down'" size="34" :color="primary" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="showArticleSearch" class="tree-panel__search">
|
||||||
|
<search-input v-model="articleKeyword" :placeholder="t('article.searchPlaceholder')"
|
||||||
|
@search="onArticleSearch" />
|
||||||
|
</view>
|
||||||
|
<view v-if="!articleCollapsed" class="tree-panel__list">
|
||||||
|
<view v-if="articlesApi.loading.value" class="tree-page__state">
|
||||||
|
<text class="tree-page__state-text">{{ t("FilePage.loading") }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="articlesApi.errorText.value" class="tree-page__state">
|
||||||
|
<text class="tree-page__state-text tree-page__state-text--error">{{ articlesApi.errorText.value
|
||||||
|
}}</text>
|
||||||
|
<text class="tree-page__retry" @click="loadArticles">{{ t("FilePage.retryBut") }}</text>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="!homeArticles.length" class="tree-page__empty">
|
||||||
|
<text class="tree-page__state-text">{{ t("article.empty") }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="row in homeArticles" :key="row.ArticleName" class="article-card"
|
||||||
|
@tap="onOpenArticle(row.ArticleName)">
|
||||||
|
<view class="article-card__cover">
|
||||||
|
<image v-if="row.CoverUrl || row.ArticleCoverFilename" class="article-card__cover-img"
|
||||||
|
:src="absoluteFileUrl(row.CoverUrl) || backupFileUrl('ArticleCover', row.ArticleCoverFilename)"
|
||||||
|
mode="aspectFill" />
|
||||||
|
<image v-else class="article-card__glyph" :src="articleGlyph" mode="aspectFit" />
|
||||||
|
</view>
|
||||||
|
<view class="article-card__main">
|
||||||
|
<view class="article-card__titlerow">
|
||||||
|
<text class="article-card__name">{{ row.ArticleTitle || t("article.unnamed") }}</text>
|
||||||
|
<text v-if="row.TopIndc === 'Y'" class="article-card__chip article-card__chip--top">{{
|
||||||
|
t("article.tagTop") }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="article-card__desc">{{ articlesApi.plainText(row.Brief, 100) || t("article.noBrief")
|
||||||
|
}}</text>
|
||||||
|
<view class="article-card__meta">
|
||||||
|
<image v-if="row.AuthorAvatarUrl" class="article-card__avatar"
|
||||||
|
:src="absoluteFileUrl(row.AuthorAvatarUrl)" mode="aspectFill" />
|
||||||
|
<view v-else class="article-card__avatar article-card__avatar--empty" />
|
||||||
|
<text class="article-card__author">{{ row.AuthorLocalName || t("article.unknownAuthor") }}</text>
|
||||||
|
<!-- 待审核从标题行下移到这里:腾出标题整行,标题可显示更多内容 -->
|
||||||
|
<text v-if="row.ApprovedIndc !== 'Y'" class="article-card__chip article-card__chip--pending">{{
|
||||||
|
t("article.tagPending") }}</text>
|
||||||
|
<text class="article-card__date">{{ articlesApi.formatDate(row.CreatedDatetime) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" size="30" color="#c0c4cc" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 底部留白:让最后一张卡能滚到底部固定 tabbar 之上 -->
|
||||||
|
<view class="tree-page__list-space" />
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- ===== 详情态:族谱管理 ===== -->
|
<!-- ===== 详情态:族谱管理 ===== -->
|
||||||
<view v-else class="tree-page__body">
|
<view v-else class="tree-page__body">
|
||||||
@@ -1877,8 +1948,14 @@ defineExpose({
|
|||||||
<!-- Hero 族谱卡 -->
|
<!-- Hero 族谱卡 -->
|
||||||
<view class="ft-hero">
|
<view class="ft-hero">
|
||||||
<view class="ft-hero__top">
|
<view class="ft-hero__top">
|
||||||
<view class="ft-hero__crest">
|
<view class="ft-hero__crest" @tap="onUploadTreeCover">
|
||||||
<image class="ft-hero__glyph" :src="treeGlyph" mode="aspectFit" />
|
<image v-if="tree.TreeCoverFilename" class="ft-hero__cover"
|
||||||
|
:src="treeCoverUrl(tree.TreeCoverFilename)" mode="aspectFill" />
|
||||||
|
<image v-else class="ft-hero__glyph" :src="treeGlyph" mode="aspectFit" />
|
||||||
|
<!-- 相机角标:与首页头像同款,可改才显示(整块可点) -->
|
||||||
|
<view v-if="data.IsCanAmend" class="ft-hero__crest-cam">
|
||||||
|
<u-icon name="camera-fill" size="24" color="#ffffff" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="ft-hero__info">
|
<view class="ft-hero__info">
|
||||||
<text class="ft-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
<text class="ft-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
||||||
@@ -1894,7 +1971,7 @@ defineExpose({
|
|||||||
<u-switch v-if="data.IsManager" :model-value="tree.PublicIndc === 'Y'" size="40"
|
<u-switch v-if="data.IsManager" :model-value="tree.PublicIndc === 'Y'" size="40"
|
||||||
:active-color="primary" @change="onPublicToggle" />
|
:active-color="primary" @change="onPublicToggle" />
|
||||||
<text v-if="data.IsManager" class="ft-badge ft-badge--admin">{{ t("FamilyTreePage.managerTag")
|
<text v-if="data.IsManager" class="ft-badge ft-badge--admin">{{ t("FamilyTreePage.managerTag")
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -2006,7 +2083,7 @@ defineExpose({
|
|||||||
<!-- 当前族员:从概览移入,夫妻/子女列表都是相对此人 -->
|
<!-- 当前族员:从概览移入,夫妻/子女列表都是相对此人 -->
|
||||||
<view v-if="loaded && currentPerson" class="ft-card">
|
<view v-if="loaded && currentPerson" class="ft-card">
|
||||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.currentPerson")
|
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.currentPerson")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view class="person-hero">
|
<view class="person-hero">
|
||||||
<view class="person-hero__avatar"
|
<view class="person-hero__avatar"
|
||||||
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
|
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
|
||||||
@@ -2021,7 +2098,7 @@ defineExpose({
|
|||||||
</view>
|
</view>
|
||||||
<view class="person-hero__chips">
|
<view class="person-hero__chips">
|
||||||
<text v-if="currentPerson?.PersonId" class="person-hero__chip">{{ personChipText(currentPerson)
|
<text v-if="currentPerson?.PersonId" class="person-hero__chip">{{ personChipText(currentPerson)
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -2064,12 +2141,12 @@ defineExpose({
|
|||||||
<!-- 新增入口与行尾操作同一把闸:当前锚点不可编(未绑定/出支系)时隐藏 -->
|
<!-- 新增入口与行尾操作同一把闸:当前锚点不可编(未绑定/出支系)时隐藏 -->
|
||||||
<view class="ft-card__head">
|
<view class="ft-card__head">
|
||||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.spouseTitle")
|
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.spouseTitle")
|
||||||
}}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view>
|
}}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view>
|
||||||
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('spouse')"><u-icon
|
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('spouse')"><u-icon
|
||||||
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addSpouse") }}</text></view>
|
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addSpouse") }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
|
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
|
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
|
||||||
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
|
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
|
||||||
@tap.stop="setCurrentPerson(record.SpouseId)"
|
@tap.stop="setCurrentPerson(record.SpouseId)"
|
||||||
@@ -2106,13 +2183,13 @@ defineExpose({
|
|||||||
<view class="ft-card">
|
<view class="ft-card">
|
||||||
<view class="ft-card__head">
|
<view class="ft-card__head">
|
||||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.childTitle")
|
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.childTitle")
|
||||||
}}</text><text class="ft-card__count">{{ childMaps.length }}</text></view>
|
}}</text><text class="ft-card__count">{{ childMaps.length }}</text></view>
|
||||||
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('child')"><u-icon
|
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('child')"><u-icon
|
||||||
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addChildBtn") }}</text>
|
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addChildBtn") }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
|
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
|
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
|
||||||
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
|
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
|
||||||
@tap.stop="setCurrentPerson(record.ChildrenId)"
|
@tap.stop="setCurrentPerson(record.ChildrenId)"
|
||||||
@@ -2153,7 +2230,7 @@ defineExpose({
|
|||||||
<view v-if="data.IsManager && pendingPermissions.length" class="ft-card">
|
<view v-if="data.IsManager && pendingPermissions.length" class="ft-card">
|
||||||
<view class="ft-card__head">
|
<view class="ft-card__head">
|
||||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permPending")
|
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permPending")
|
||||||
}}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view>
|
}}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-for="record in pendingPermissions" :key="record.UserName" class="ft-perm-row">
|
<view v-for="record in pendingPermissions" :key="record.UserName" class="ft-perm-row">
|
||||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
||||||
@@ -2177,10 +2254,10 @@ defineExpose({
|
|||||||
<view v-if="data.IsManager" class="ft-card">
|
<view v-if="data.IsManager" class="ft-card">
|
||||||
<view class="ft-card__head">
|
<view class="ft-card__head">
|
||||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permManager")
|
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permManager")
|
||||||
}}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view>
|
}}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!managerPermissions.length" class="ft-empty"><text>{{ t("FamilyTreePage.permEmpty")
|
<view v-if="!managerPermissions.length" class="ft-empty"><text>{{ t("FamilyTreePage.permEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="record in managerPermissions" :key="record.UserName" class="ft-perm-row">
|
<view v-for="record in managerPermissions" :key="record.UserName" class="ft-perm-row">
|
||||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
||||||
record.UserName
|
record.UserName
|
||||||
@@ -2250,17 +2327,17 @@ defineExpose({
|
|||||||
<block v-else>
|
<block v-else>
|
||||||
<view class="ft-gen-head">
|
<view class="ft-gen-head">
|
||||||
<text class="ft-gen-head__col ft-gen-head__col--gen">{{ t("FamilyTreePage.statsColGeneration")
|
<text class="ft-gen-head__col ft-gen-head__col--gen">{{ t("FamilyTreePage.statsColGeneration")
|
||||||
|
}}</text>
|
||||||
|
<text class="ft-gen-head__col ft-gen-head__col--cnt">{{ t("FamilyTreePage.statsColCount")
|
||||||
}}</text>
|
}}</text>
|
||||||
<text class="ft-gen-head__col ft-gen-head__col--cnt">{{ t("FamilyTreePage.statsColCount") }}</text>
|
|
||||||
<text class="ft-gen-head__col ft-gen-head__col--bar" />
|
<text class="ft-gen-head__col ft-gen-head__col--bar" />
|
||||||
</view>
|
</view>
|
||||||
<view v-for="gen in statsData?.PerGeneration ?? []" :key="gen.Generation" class="ft-gen-row">
|
<view v-for="gen in statsData?.PerGeneration ?? []" :key="gen.Generation" class="ft-gen-row">
|
||||||
<text class="ft-gen-row__gen">{{ gen.Generation }}{{ t("FamilyTreePage.statsGenerationUnit")
|
<text class="ft-gen-row__gen">{{ gen.Generation }}{{ t("FamilyTreePage.statsGenerationUnit")
|
||||||
}}</text>
|
}}</text>
|
||||||
<text class="ft-gen-row__cnt">{{ gen.Count }}</text>
|
<text class="ft-gen-row__cnt">{{ gen.Count }}</text>
|
||||||
<view class="ft-gen-bar">
|
<view class="ft-gen-bar">
|
||||||
<view class="ft-gen-bar__fill"
|
<view class="ft-gen-bar__fill" :style="{ width: genBarWidth(gen.Count) }" />
|
||||||
:style="{ width: genBarWidth(gen.Count) }" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
@@ -2293,7 +2370,9 @@ defineExpose({
|
|||||||
<!-- 头卡(与媒体/共享页同套):白底 + 族谱树图标(与列表卡同款渐变底白树形) + 编号灰字独立行 -->
|
<!-- 头卡(与媒体/共享页同套):白底 + 族谱树图标(与列表卡同款渐变底白树形) + 编号灰字独立行 -->
|
||||||
<view class="detail-hero">
|
<view class="detail-hero">
|
||||||
<view class="detail-hero__avatar">
|
<view class="detail-hero__avatar">
|
||||||
<image class="detail-hero__glyph" :src="treeGlyph" mode="aspectFit" />
|
<image v-if="tree.TreeCoverFilename" class="detail-hero__cover"
|
||||||
|
:src="treeCoverUrl(tree.TreeCoverFilename)" mode="aspectFill" />
|
||||||
|
<image v-else class="detail-hero__glyph" :src="treeGlyph" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-hero__main">
|
<view class="detail-hero__main">
|
||||||
<text class="detail-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
<text class="detail-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
|
||||||
@@ -2465,7 +2544,7 @@ defineExpose({
|
|||||||
<view class="person-hero__line">
|
<view class="person-hero__line">
|
||||||
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
|
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
|
||||||
<text class="person-hero__gender">{{ genderText(personGender(record.SpouseId, record.Gender))
|
<text class="person-hero__gender">{{ genderText(personGender(record.SpouseId, record.Gender))
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="person-hero__chips">
|
<view class="person-hero__chips">
|
||||||
<text class="person-hero__chip">
|
<text class="person-hero__chip">
|
||||||
@@ -2502,7 +2581,7 @@ defineExpose({
|
|||||||
<text class="sub-label">{{ t("FamilyTreePage.marryDate") }}</text>
|
<text class="sub-label">{{ t("FamilyTreePage.marryDate") }}</text>
|
||||||
<view class="sub-picker">
|
<view class="sub-picker">
|
||||||
<text class="sub-picker-text">{{ datePart(record.MarryDate) || t("AdminPage.notSetTip")
|
<text class="sub-picker-text">{{ datePart(record.MarryDate) || t("AdminPage.notSetTip")
|
||||||
}}</text>
|
}}</text>
|
||||||
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -2518,7 +2597,7 @@ defineExpose({
|
|||||||
<view class="sub-picker">
|
<view class="sub-picker">
|
||||||
<text class="sub-picker-text">{{
|
<text class="sub-picker-text">{{
|
||||||
datePart(record.DivorceDate) || t("AdminPage.notSetTip")
|
datePart(record.DivorceDate) || t("AdminPage.notSetTip")
|
||||||
}}</text>
|
}}</text>
|
||||||
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -2604,7 +2683,7 @@ defineExpose({
|
|||||||
<view class="person-hero__line">
|
<view class="person-hero__line">
|
||||||
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
|
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
|
||||||
<text class="person-hero__gender">{{ genderText(personGender(record.ChildrenId, record.Gender))
|
<text class="person-hero__gender">{{ genderText(personGender(record.ChildrenId, record.Gender))
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="person-hero__chips">
|
<view class="person-hero__chips">
|
||||||
<text class="person-hero__chip">
|
<text class="person-hero__chip">
|
||||||
@@ -2715,7 +2794,7 @@ defineExpose({
|
|||||||
<text class="rel-name">{{ record.UserLocalName || record.UserName }}</text>
|
<text class="rel-name">{{ record.UserLocalName || record.UserName }}</text>
|
||||||
<text class="rel-sub">{{
|
<text class="rel-sub">{{
|
||||||
record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag")
|
record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag")
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="perm-ops">
|
<view class="perm-ops">
|
||||||
<view class="perm-op perm-op--danger" @tap="rejectPermission(record)">
|
<view class="perm-op perm-op--danger" @tap="rejectPermission(record)">
|
||||||
@@ -2804,7 +2883,7 @@ defineExpose({
|
|||||||
</view>
|
</view>
|
||||||
</u-popup>
|
</u-popup>
|
||||||
|
|
||||||
<!-- ===== 编辑族谱信息弹层(仅名称/始祖谱名/简介/公开四字段,关系与成员分开维护) ===== -->
|
<!-- ===== 编辑族谱信息弹层(仅名称/简介/公开三字段,始祖谱名不展示,关系与成员分开维护) ===== -->
|
||||||
<!-- ===== 交换文件导出弹层(信封加密在前端,密码只当密钥材料、后端不校验)===== -->
|
<!-- ===== 交换文件导出弹层(信封加密在前端,密码只当密钥材料、后端不校验)===== -->
|
||||||
<u-popup :model-value="exportVisible" mode="bottom" round="24" @close="exportVisible = false">
|
<u-popup :model-value="exportVisible" mode="bottom" round="24" @close="exportVisible = false">
|
||||||
<view class="ft-sheet">
|
<view class="ft-sheet">
|
||||||
@@ -2892,7 +2971,7 @@ defineExpose({
|
|||||||
<!-- 服务器文件列表限高滚动:文件多了不挤掉密码框/警告/按钮 -->
|
<!-- 服务器文件列表限高滚动:文件多了不挤掉密码框/警告/按钮 -->
|
||||||
<scroll-view v-if="!localFileName" scroll-y class="ft-exchange__list">
|
<scroll-view v-if="!localFileName" scroll-y class="ft-exchange__list">
|
||||||
<view v-if="importFilesLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading")
|
<view v-if="importFilesLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-else-if="!importFiles.length" class="ft-empty"><text>{{ t(imScope === "SUB" ?
|
<view v-else-if="!importFiles.length" class="ft-empty"><text>{{ t(imScope === "SUB" ?
|
||||||
"FamilyTreePage.exchangeFileEmptySub" : "FamilyTreePage.exchangeFileEmpty") }}</text></view>
|
"FamilyTreePage.exchangeFileEmptySub" : "FamilyTreePage.exchangeFileEmpty") }}</text></view>
|
||||||
<view v-for="file in importFiles" :key="file.FileName" class="ft-exchange__row"
|
<view v-for="file in importFiles" :key="file.FileName" class="ft-exchange__row"
|
||||||
@@ -2913,7 +2992,7 @@ defineExpose({
|
|||||||
<view v-if="backupLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading") }}</text>
|
<view v-if="backupLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading") }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="!backupFiles.length" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeBackupEmpty")
|
<view v-else-if="!backupFiles.length" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeBackupEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="bk in backupFiles" :key="bk.FileName" class="ft-exchange__row"
|
<view v-for="bk in backupFiles" :key="bk.FileName" class="ft-exchange__row"
|
||||||
:class="{ 'ft-exchange__row--on': pickedBackup === bk.FileName }" @tap="pickedBackup = bk.FileName">
|
:class="{ 'ft-exchange__row--on': pickedBackup === bk.FileName }" @tap="pickedBackup = bk.FileName">
|
||||||
<view class="ft-exchange__row-main">
|
<view class="ft-exchange__row-main">
|
||||||
@@ -2955,17 +3034,11 @@ defineExpose({
|
|||||||
<input v-model="tree.TreeName" class="ft-field__input" :maxlength="50"
|
<input v-model="tree.TreeName" class="ft-field__input" :maxlength="50"
|
||||||
:placeholder="t('FamilyTreePage.treeNameRequired')">
|
:placeholder="t('FamilyTreePage.treeNameRequired')">
|
||||||
</view>
|
</view>
|
||||||
<view class="ft-field">
|
|
||||||
<text class="ft-field__label">{{ t("FamilyTreePage.ancestorGenealogyName") }}</text>
|
|
||||||
<input v-model="tree.AncestorPersonGenealogyName" class="ft-field__input" :maxlength="50"
|
|
||||||
:placeholder="t('FamilyTreePage.ancestorGenealogyName')">
|
|
||||||
</view>
|
|
||||||
<view class="ft-field">
|
<view class="ft-field">
|
||||||
<text class="ft-field__label">{{ t("FamilyTreePage.brief") }}</text>
|
<text class="ft-field__label">{{ t("FamilyTreePage.brief") }}</text>
|
||||||
<view class="ft-field__area">
|
<view class="ft-field__area">
|
||||||
<textarea v-model="tree.Brief" class="ft-field__textarea" :maxlength="60"
|
<textarea v-model="tree.Brief" class="ft-field__textarea" :maxlength="-1"
|
||||||
:placeholder="t('FamilyTreePage.noBrief')" />
|
:placeholder="t('FamilyTreePage.noBrief')" />
|
||||||
<text class="ft-field__counter">{{ (tree.Brief || "").length }}/60</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="ft-switch-card">
|
<view class="ft-switch-card">
|
||||||
@@ -2979,7 +3052,7 @@ defineExpose({
|
|||||||
<view class="ft-sheet__foot">
|
<view class="ft-sheet__foot">
|
||||||
<view class="ft-sheet__btn" @tap="cancelTreeEdit"><text>{{ t("AdminPage.cancelBut") }}</text></view>
|
<view class="ft-sheet__btn" @tap="cancelTreeEdit"><text>{{ t("AdminPage.cancelBut") }}</text></view>
|
||||||
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="saveTreeEdit"><text>{{ t("AdminPage.saveBut")
|
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="saveTreeEdit"><text>{{ t("AdminPage.saveBut")
|
||||||
}}</text>
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -3023,6 +3096,13 @@ defineExpose({
|
|||||||
overflow: hidden;
|
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 下
|
box-sizing 必加:uni 给 scroll-view 根设了 width:100%,默认 content-box 下
|
||||||
padding 会外溢成 100%+两侧留白,导致面板整体右移、两边不对称 */
|
padding 会外溢成 100%+两侧留白,导致面板整体右移、两边不对称 */
|
||||||
@@ -3033,6 +3113,11 @@ defineExpose({
|
|||||||
padding: calc(20rpx + var(--status-bar-height, 0px)) 20rpx 20rpx;
|
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 {
|
.home-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -3485,13 +3570,14 @@ defineExpose({
|
|||||||
background-color: var(--fg-soft, #eaf4ff);
|
background-color: var(--fg-soft, #eaf4ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 列表卡片(媒体页 album-card 同套) ===== */
|
/* ===== 列表卡片(媒体页 album-card 同套,改版:上行 头像+名称+徽章+始祖,下行整行简介) ===== */
|
||||||
.tree-card {
|
.tree-card {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 20rpx;
|
gap: 16rpx;
|
||||||
margin: 0 0 20rpx;
|
margin: 0 0 20rpx;
|
||||||
padding: 28rpx;
|
padding: 28rpx 56rpx 28rpx 28rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
|
||||||
@@ -3501,17 +3587,31 @@ defineExpose({
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 首行:头像 + 名称/徽章 + 始祖,垂直居中 */
|
||||||
|
.tree-card__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-card__avatar {
|
.tree-card__avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 88rpx;
|
width: 88rpx;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
|
overflow: hidden;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 封面图铺满头像位(与文章卡封面同口径,无封面才回退树形 glyph) */
|
||||||
|
.tree-card__cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* 树形字形(内联 SVG data-URI) */
|
/* 树形字形(内联 SVG data-URI) */
|
||||||
.tree-card__glyph {
|
.tree-card__glyph {
|
||||||
width: 56rpx;
|
width: 56rpx;
|
||||||
@@ -3535,12 +3635,12 @@ defineExpose({
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 简介整行多行显示:省略号由 briefLine 按字数在末尾补(CSS 多行省略会在换行处也补) */
|
||||||
.tree-card__desc {
|
.tree-card__desc {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
color: #8f959e;
|
color: #8f959e;
|
||||||
overflow: hidden;
|
word-break: break-word;
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-card__meta {
|
.tree-card__meta {
|
||||||
@@ -3564,10 +3664,12 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-card__ancestor {
|
/* 箭头改挂右上角,不再占首行一列(简介要整行铺开) */
|
||||||
margin-left: auto;
|
.tree-card__arrow {
|
||||||
font-size: 20rpx;
|
position: absolute;
|
||||||
color: #bfc3c9;
|
top: 50%;
|
||||||
|
right: 16rpx;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 有家文化传承文章卡(复用 tree-panel 面板几何,卡体换横向图文) ===== */
|
/* ===== 有家文化传承文章卡(复用 tree-panel 面板几何,卡体换横向图文) ===== */
|
||||||
@@ -3733,11 +3835,18 @@ defineExpose({
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 140rpx;
|
width: 140rpx;
|
||||||
height: 140rpx;
|
height: 140rpx;
|
||||||
|
overflow: hidden;
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑态头卡同步显示封面(上传入口在浏览态族徽上) */
|
||||||
|
.detail-hero__cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* 树形字形(与列表卡同款内联 SVG data-URI) */
|
/* 树形字形(与列表卡同款内联 SVG data-URI) */
|
||||||
.detail-hero__glyph {
|
.detail-hero__glyph {
|
||||||
width: 84rpx;
|
width: 84rpx;
|
||||||
@@ -4259,20 +4368,44 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ft-hero__crest {
|
.ft-hero__crest {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 112rpx;
|
width: 168rpx;
|
||||||
height: 112rpx;
|
height: 168rpx;
|
||||||
border-radius: 28rpx;
|
overflow: hidden;
|
||||||
|
border-radius: 36rpx;
|
||||||
background: var(--fg-gradient, linear-gradient(135deg, #ff9d45, #ff7d1f));
|
background: var(--fg-gradient, linear-gradient(135deg, #ff9d45, #ff7d1f));
|
||||||
box-shadow: 0 12rpx 24rpx -12rpx var(--fg-shadow, rgba(255, 125, 31, 0.5));
|
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 {
|
.ft-hero__glyph {
|
||||||
width: 64rpx;
|
width: 96rpx;
|
||||||
height: 64rpx;
|
height: 96rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ft-hero__info {
|
.ft-hero__info {
|
||||||
@@ -5060,7 +5193,7 @@ defineExpose({
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180rpx;
|
height: 180rpx;
|
||||||
padding: 20rpx 24rpx 44rpx;
|
padding: 20rpx 24rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #1d2129;
|
color: #1d2129;
|
||||||
@@ -5068,14 +5201,6 @@ defineExpose({
|
|||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
background-color: #f7f8fa;
|
background-color: #f7f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__counter {
|
|
||||||
position: absolute;
|
|
||||||
right: 24rpx;
|
|
||||||
bottom: 16rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #8f959e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 交换文件弹层专属:说明小字/警示/文件行(选中态跟主题 soft 底) ===== */
|
/* ===== 交换文件弹层专属:说明小字/警示/文件行(选中态跟主题 soft 底) ===== */
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export class FamilyTreeViewEntity
|
|||||||
public CommonName: string = "";
|
public CommonName: string = "";
|
||||||
public GenealogyName: string = "";
|
public GenealogyName: string = "";
|
||||||
public PublicIndc: string = "";
|
public PublicIndc: string = "";
|
||||||
|
public TreeCoverFilename: string = "";
|
||||||
public Surname: string = "";
|
public Surname: string = "";
|
||||||
public TreeId: string = "";
|
public TreeId: string = "";
|
||||||
public TreeName: string = "";
|
public TreeName: string = "";
|
||||||
@@ -15,16 +16,18 @@ export class FamilyTreeViewEntity
|
|||||||
static readonly COMMON_NAME : string ='CommonName';
|
static readonly COMMON_NAME : string ='CommonName';
|
||||||
static readonly GENEALOGY_NAME : string ='GenealogyName';
|
static readonly GENEALOGY_NAME : string ='GenealogyName';
|
||||||
static readonly PUBLIC_INDC : string ='PublicIndc';
|
static readonly PUBLIC_INDC : string ='PublicIndc';
|
||||||
|
static readonly TREE_COVER_FILENAME : string ='TreeCoverFilename';
|
||||||
static readonly SURNAME : string ='Surname';
|
static readonly SURNAME : string ='Surname';
|
||||||
static readonly TREE_ID : string ='TreeId';
|
static readonly TREE_ID : string ='TreeId';
|
||||||
static readonly TREE_NAME : string ='TreeName';
|
static readonly TREE_NAME : string ='TreeName';
|
||||||
static readonly DB_NAME_FAMILY_TREE_VIEW : string ='family_tree_view';
|
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_ANCESTOR_PERSON_ID : string ='ancestor_person_id';
|
||||||
static readonly DB_BRIEF : string ='brief';
|
static readonly DB_BRIEF : string ='brief';
|
||||||
static readonly DB_COMMON_NAME : string ='common_name';
|
static readonly DB_COMMON_NAME : string ='common_name';
|
||||||
static readonly DB_GENEALOGY_NAME : string ='genealogy_name';
|
static readonly DB_GENEALOGY_NAME : string ='genealogy_name';
|
||||||
static readonly DB_PUBLIC_INDC : string ='public_indc';
|
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_SURNAME : string ='surname';
|
||||||
static readonly DB_TREE_ID : string ='tree_id';
|
static readonly DB_TREE_ID : string ='tree_id';
|
||||||
static readonly DB_TREE_NAME : string ='tree_name';
|
static readonly DB_TREE_NAME : string ='tree_name';
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export class SecurityUserPolicy
|
|||||||
{
|
{
|
||||||
|
|
||||||
static readonly SECURITY_USER_BASE_ACTOR : string ='SecurityUserBaseActor';
|
static readonly SECURITY_USER_BASE_ACTOR : string ='SecurityUserBaseActor';
|
||||||
|
static readonly SYSTEM_ACTOR : string ='system_actor';
|
||||||
static readonly ERROR_LOGIN_COUNT: number =5;
|
static readonly ERROR_LOGIN_COUNT: number =5;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,9 @@
|
|||||||
"slogan": "One tree, generations of us",
|
"slogan": "One tree, generations of us",
|
||||||
"langZh": "简体中文",
|
"langZh": "简体中文",
|
||||||
"langEn": "English",
|
"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": {
|
"TabBar": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@@ -94,9 +96,10 @@
|
|||||||
"logoutSuccess": "Logged out",
|
"logoutSuccess": "Logged out",
|
||||||
"currentTheme": "Current Theme",
|
"currentTheme": "Current Theme",
|
||||||
"accessLine": "Access Line",
|
"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",
|
"lineWan": "WAN",
|
||||||
"lineWanDesc": "Access via the public domain, available anywhere",
|
"lineWanDesc": "Access via the public domain, available anywhere",
|
||||||
|
"lineWanPlaceholder": "Public address, leave blank for default ({default})",
|
||||||
"lineLan": "LAN Direct",
|
"lineLan": "LAN Direct",
|
||||||
"lineLanDesc": "Connect directly over the same Wi-Fi for faster speed",
|
"lineLanDesc": "Connect directly over the same Wi-Fi for faster speed",
|
||||||
"lineLanPlaceholder": "LAN address, e.g. http://192.168.1.10:5235",
|
"lineLanPlaceholder": "LAN address, e.g. http://192.168.1.10:5235",
|
||||||
@@ -107,7 +110,8 @@
|
|||||||
"lineInvalid": "Invalid address format",
|
"lineInvalid": "Invalid address format",
|
||||||
"lineCancel": "Cancel",
|
"lineCancel": "Cancel",
|
||||||
"lineSave": "Save",
|
"lineSave": "Save",
|
||||||
"lineSaved": "Line switched",
|
"lineSaved": "Line saved",
|
||||||
|
"lineSavedRelogin": "Line updated, please sign in again",
|
||||||
"aboutTitle": "About",
|
"aboutTitle": "About",
|
||||||
"appName": "App",
|
"appName": "App",
|
||||||
"versionLabel": "Version",
|
"versionLabel": "Version",
|
||||||
@@ -373,7 +377,9 @@
|
|||||||
"editInfoTitle": "Edit Family Tree Info",
|
"editInfoTitle": "Edit Family Tree Info",
|
||||||
"removeRelation": "Remove Relation",
|
"removeRelation": "Remove Relation",
|
||||||
"removeRelationConfirm": "Remove the relation with {name}? It takes effect immediately after saving.",
|
"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": {
|
"FamilyPersonPage": {
|
||||||
"title": "Family Member Query",
|
"title": "Family Member Query",
|
||||||
|
|||||||
@@ -67,7 +67,9 @@
|
|||||||
"slogan": "一棵树,几代人",
|
"slogan": "一棵树,几代人",
|
||||||
"langZh": "简体中文",
|
"langZh": "简体中文",
|
||||||
"langEn": "English",
|
"langEn": "English",
|
||||||
"loginFailed": "账号或密码错误"
|
"loginFailed": "账号或密码错误",
|
||||||
|
"netFailTitle": "无法连接服务器",
|
||||||
|
"netFailContent": "当前访问线路的服务器连不上,请检查网络或重新设置访问线路"
|
||||||
},
|
},
|
||||||
"TabBar": {
|
"TabBar": {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
@@ -94,9 +96,10 @@
|
|||||||
"logoutSuccess": "已退出登录",
|
"logoutSuccess": "已退出登录",
|
||||||
"currentTheme": "当前主题",
|
"currentTheme": "当前主题",
|
||||||
"accessLine": "访问线路",
|
"accessLine": "访问线路",
|
||||||
"lineHint": "与服务器在同一局域网时选内网直连,上传更快速",
|
"lineHint": "公网与内网地址均可自定义;切换服务器后保存将自动退出,需重新登录",
|
||||||
"lineWan": "公网",
|
"lineWan": "公网",
|
||||||
"lineWanDesc": "通过公网域名访问,随时随地可用",
|
"lineWanDesc": "通过公网域名访问,随时随地可用",
|
||||||
|
"lineWanPlaceholder": "公网地址,留空使用默认({default})",
|
||||||
"lineLan": "内网直连",
|
"lineLan": "内网直连",
|
||||||
"lineLanDesc": "同一 Wi-Fi 下直连服务器内网地址,速度更快",
|
"lineLanDesc": "同一 Wi-Fi 下直连服务器内网地址,速度更快",
|
||||||
"lineLanPlaceholder": "内网地址,如 http://192.168.1.10:5235",
|
"lineLanPlaceholder": "内网地址,如 http://192.168.1.10:5235",
|
||||||
@@ -107,7 +110,8 @@
|
|||||||
"lineInvalid": "地址格式不正确",
|
"lineInvalid": "地址格式不正确",
|
||||||
"lineCancel": "取消",
|
"lineCancel": "取消",
|
||||||
"lineSave": "保存",
|
"lineSave": "保存",
|
||||||
"lineSaved": "已切换线路",
|
"lineSaved": "线路已保存",
|
||||||
|
"lineSavedRelogin": "线路已更新,请重新登录",
|
||||||
"aboutTitle": "关于",
|
"aboutTitle": "关于",
|
||||||
"appName": "应用名称",
|
"appName": "应用名称",
|
||||||
"versionLabel": "当前版本",
|
"versionLabel": "当前版本",
|
||||||
@@ -341,7 +345,7 @@
|
|||||||
"statMembers": "位族人",
|
"statMembers": "位族人",
|
||||||
"statManagers": "管理员",
|
"statManagers": "管理员",
|
||||||
"statPending": "待审核",
|
"statPending": "待审核",
|
||||||
"statsTotal": "入树成员",
|
"statsTotal": "位族人",
|
||||||
"statsGeneration": "世代层数",
|
"statsGeneration": "世代层数",
|
||||||
"statsMale": "男",
|
"statsMale": "男",
|
||||||
"statsFemale": "女",
|
"statsFemale": "女",
|
||||||
@@ -373,7 +377,9 @@
|
|||||||
"editInfoTitle": "编辑族谱信息",
|
"editInfoTitle": "编辑族谱信息",
|
||||||
"removeRelation": "解除关系",
|
"removeRelation": "解除关系",
|
||||||
"removeRelationConfirm": "确定解除与「{name}」的关系吗?解除后立即保存生效",
|
"removeRelationConfirm": "确定解除与「{name}」的关系吗?解除后立即保存生效",
|
||||||
"dupMemberTip": "该成员已在关系中,请勿重复添加"
|
"dupMemberTip": "该成员已在关系中,请勿重复添加",
|
||||||
|
"treeCoverPick": "点击上传封面",
|
||||||
|
"treeCoverUploaded": "封面已上传"
|
||||||
},
|
},
|
||||||
"FamilyPersonPage": {
|
"FamilyPersonPage": {
|
||||||
"title": "家族成员查询",
|
"title": "家族成员查询",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useLocale } from "uview-pro";
|
|||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { apiBaseUrl } from "@/common/apiEndpoint";
|
import { apiBaseUrl } from "@/common/apiEndpoint";
|
||||||
import { brandGlyph, veinGlyph } from "@/common/brand";
|
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 { DEFAULT_LOCALE, setCurrentUiLocale } from "@/common/locale-const";
|
||||||
import { encryptLoginPassword } from "@/common/pwd-crypto";
|
import { encryptLoginPassword } from "@/common/pwd-crypto";
|
||||||
import { useThemeGradient, useThemeTokens } from "@/composables";
|
import { useThemeGradient, useThemeTokens } from "@/composables";
|
||||||
@@ -23,6 +23,8 @@ const userMobile = ref("");
|
|||||||
const password = ref("");
|
const password = ref("");
|
||||||
const showPwd = ref(false);
|
const showPwd = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
/** 「访问线路」弹层显隐:登录连不上服务器时自动弹出(组件与「我的」共用) */
|
||||||
|
const endpointPopupVisible = ref(false);
|
||||||
const loginProfileStore = useLoginProfileStore();
|
const loginProfileStore = useLoginProfileStore();
|
||||||
|
|
||||||
/* 主题接线:页面底渐变与强调色全部跟随当前主题(与「我的」/媒体/共享页同一口径),
|
/* 主题接线:页面底渐变与强调色全部跟随当前主题(与「我的」/媒体/共享页同一口径),
|
||||||
@@ -127,13 +129,33 @@ async function handleLogin() {
|
|||||||
// 回到被拦前的页面(默认 dashboard):reLaunch 会清空页面栈,避免返回键退回登录页
|
// 回到被拦前的页面(默认 dashboard):reLaunch 会清空页面栈,避免返回键退回登录页
|
||||||
setTimeout(() => uni.reLaunch({ url: redirectUrl.value }), 800);
|
setTimeout(() => uni.reLaunch({ url: redirectUrl.value }), 800);
|
||||||
}
|
}
|
||||||
catch {
|
catch (error: any) {
|
||||||
// 错误提示已由 http.interceptor 统一处理
|
// 连不上服务器(地址填错 / 服务器宕机):提示并自动弹出共用的「访问线路」让用户重设地址后原地重试;
|
||||||
|
// 其余错误(如账号密码错)的提示已由 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 {
|
finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 访问线路保存后:服务器真的变了就提示重试登录(本页本就未登录,无需再走登出) */
|
||||||
|
function onEndpointSaved(payload: { changed: boolean }): void {
|
||||||
|
if (payload.changed) {
|
||||||
|
uni.showToast({ title: t("MinePage.lineSavedRelogin"), icon: "none" });
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -197,6 +219,11 @@ async function handleLogin() {
|
|||||||
{{ envMode }} · {{ currentApiUrl }}
|
{{ envMode }} · {{ currentApiUrl }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 访问线路弹层(与「我的」共用组件):登录连不上服务器时自动弹出,改地址后原地重试 -->
|
||||||
|
<!-- #ifdef APP-PLUS -->
|
||||||
|
<endpoint-setting-popup v-model="endpointPopupVisible" @saved="onEndpointSaved" />
|
||||||
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useLocale } from "uview-pro";
|
import { useLocale } from "uview-pro";
|
||||||
import { computed, ref } from "vue";
|
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 { APP_INFO } from "@/common/constant";
|
||||||
import { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
|
import { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
|
||||||
import { usePermission } from "@/permission/permission-context";
|
import { usePermission } from "@/permission/permission-context";
|
||||||
@@ -51,61 +51,36 @@ function goTheme(): void {
|
|||||||
uni.navigateTo({ url: pageUrl("themePage") });
|
uni.navigateTo({ url: pageUrl("themePage") });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 访问线路(App 手动切换公网/内网直连;仅影响后续请求,不中断在途传输) =====
|
// ===== 访问线路(App 手动切换公网/内网直连;弹层组件与登录页共用) =====
|
||||||
const linePopupVisible = ref(false);
|
const linePopupVisible = ref(false);
|
||||||
/** 当前生效线路(保存后刷新展示) */
|
/** 当前生效线路(保存后刷新展示) */
|
||||||
const lineSetting = ref(currentEndpoint());
|
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 currentLineLabel = computed(() => {
|
||||||
const setting = lineSetting.value;
|
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 {
|
function openLinePopup(): void {
|
||||||
const setting = currentEndpoint();
|
lineSetting.value = currentEndpoint();
|
||||||
lineModeDraft.value = setting.mode;
|
|
||||||
lineLanDraft.value = setting.lanUrl;
|
|
||||||
lineTestResult.value = "";
|
|
||||||
linePopupVisible.value = true;
|
linePopupVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 测试内网地址连通性:用 RSA 公钥接口探活(与登录首调同源) */
|
/** 弹层保存后回调:刷新展示;服务器真的变了 → 本地令牌对新服务器无意义,清登录态回登录页重新认证 */
|
||||||
async function handleTestLine(): Promise<void> {
|
function onLineSaved(payload: { changed: boolean }): void {
|
||||||
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 });
|
|
||||||
lineSetting.value = currentEndpoint();
|
lineSetting.value = currentEndpoint();
|
||||||
linePopupVisible.value = false;
|
if (!payload.changed) {
|
||||||
uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" });
|
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<void> {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 访问线路弹层:公网/内网单选 + 内网地址手填 + 测连通(仅 App 有入口) -->
|
<!-- 访问线路弹层:与登录页共用的组件,改地址后由 onLineSaved 决定是否登出(仅 App 有入口) -->
|
||||||
<!-- #ifdef APP-PLUS -->
|
<!-- #ifdef APP-PLUS -->
|
||||||
<u-popup :model-value="linePopupVisible" mode="bottom" round="24" @close="linePopupVisible = false">
|
<endpoint-setting-popup v-model="linePopupVisible" @saved="onLineSaved" />
|
||||||
<view class="line-sheet">
|
|
||||||
<text class="line-sheet__title">
|
|
||||||
{{ t("MinePage.accessLine") }}
|
|
||||||
</text>
|
|
||||||
<text class="line-sheet__hint">
|
|
||||||
{{ t("MinePage.lineHint") }}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<!-- 公网:默认线路,随时随地可用 -->
|
|
||||||
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'wan' }"
|
|
||||||
@tap="lineModeDraft = 'wan'">
|
|
||||||
<view class="line-option__body">
|
|
||||||
<text class="line-option__name">
|
|
||||||
{{ t("MinePage.lineWan") }}
|
|
||||||
</text>
|
|
||||||
<text class="line-option__desc">
|
|
||||||
{{ t("MinePage.lineWanDesc") }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'wan' }" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 内网直连:需与服务器同局域网,地址手填 -->
|
|
||||||
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'lan' }"
|
|
||||||
@tap="lineModeDraft = 'lan'">
|
|
||||||
<view class="line-option__body">
|
|
||||||
<text class="line-option__name">
|
|
||||||
{{ t("MinePage.lineLan") }}
|
|
||||||
</text>
|
|
||||||
<text class="line-option__desc">
|
|
||||||
{{ t("MinePage.lineLanDesc") }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'lan' }" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 内网地址:选中内网才展开;测连通用免鉴权的 RSA 公钥接口 -->
|
|
||||||
<view v-if="lineModeDraft === 'lan'" class="line-lan">
|
|
||||||
<input v-model="lineLanDraft" class="line-lan__input" :placeholder="t('MinePage.lineLanPlaceholder')"
|
|
||||||
placeholder-class="line-lan__ph">
|
|
||||||
<view class="line-lan__test" @tap="handleTestLine">
|
|
||||||
<text class="line-lan__test-text">
|
|
||||||
{{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text v-if="lineModeDraft === 'lan' && lineTestResult === 'ok'" class="line-lan__result line-lan__result--ok">
|
|
||||||
{{ t("MinePage.lineTestOk") }}
|
|
||||||
</text>
|
|
||||||
<text v-else-if="lineModeDraft === 'lan' && lineTestResult === 'fail'"
|
|
||||||
class="line-lan__result line-lan__result--fail">
|
|
||||||
{{ t("MinePage.lineTestFail") }}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<view class="line-sheet__actions">
|
|
||||||
<view class="line-sheet__btn line-sheet__btn--ghost" @tap="linePopupVisible = false">
|
|
||||||
<text class="line-sheet__btn-text line-sheet__btn-text--ghost">
|
|
||||||
{{ t("MinePage.lineCancel") }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
<view class="line-sheet__btn line-sheet__btn--primary" @tap="handleSaveLine">
|
|
||||||
<text class="line-sheet__btn-text">
|
|
||||||
{{ t("MinePage.lineSave") }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</u-popup>
|
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
</app-page>
|
</app-page>
|
||||||
</template>
|
</template>
|
||||||
@@ -492,156 +399,5 @@ async function handleLogout(): Promise<void> {
|
|||||||
color: #1f2329;
|
color: #1f2329;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 访问线路弹层(底部抽屉,同 file-action-sheet 风格) ===== */
|
/* 访问线路弹层样式已随组件迁至 components/endpoint-setting-popup/endpoint-setting-popup.vue */
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user