From d97a8884aa1e28b513e7cd0a967f7b8f5bd1e8ba Mon Sep 17 00:00:00 2001 From: owenchen Date: Mon, 26 Oct 2026 10:46:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=AE=89=E8=A3=85=E5=8C=85?= =?UTF-8?q?=EF=BC=8C=E6=9C=89=E8=BF=9B=E5=BA=A6=E6=9D=A1=EF=BC=8C=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FamilyTreeService.auto.cs | 38 +++ .../FolderProfile3.pubxml.user | 2 +- uniapp/youjiala/package.json | 2 +- uniapp/youjiala/src/App.root.vue | 139 +++++++- uniapp/youjiala/src/common/constant.ts | 2 +- uniapp/youjiala/src/common/global-error.ts | 24 ++ .../family-tree-chart/family-tree-chart.vue | 217 +++++++++++- .../family-tree-chart/family-tree-node.vue | 4 + .../src/components/family-tree-chart/types.ts | 2 + uniapp/youjiala/src/locale/lang/en-US.json | 2 + uniapp/youjiala/src/locale/lang/zh-CN.json | 2 + uniapp/youjiala/src/manifest.json | 308 +++++++++--------- uniapp/youjiala/src/utils/appUpdate.ts | 141 +++++--- uniapp/youjiala/src/utils/request.ts | 2 + 14 files changed, 665 insertions(+), 220 deletions(-) diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs index 14bb9a1..45a71ba 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs @@ -48,6 +48,38 @@ namespace CloudBuilder.Master.Service } } + /// + /// 全系图下发瘦身(人员):只保留卡片渲染所需列(PersonId/Surname/GenealogyName/CommonName/Gender/AvatarUrl), + /// 其余可空列(履历/学历/O/OCR 临时字段等)连同已生成 AvatarUrl 后不再需要的 AvatarFilename 一律置空, + /// 由全局 NullValueHandling.Ignore 丢弃——大谱(数千族员)整谱响应体量得以大幅压缩,避免 H5 请求超时。 + /// + private static void SlimPersonForChart(FamilyPersonDisplayData p) + { + if (p == null) return; + p.Alias = null; p.BindUserId = null; p.BirthDate = null; p.BirthOrder = null; p.BirthText = null; + p.DeathDate = null; p.DeathText = null; p.Duty = null; p.Education = null; p.FatherId = null; + p.GraduationStatus = null; p.Hao = null; p.Introduction = null; p.IsBindUser = null; p.Major = null; + p.MaritalStatus = null; p.MotherName = null; p.Privacy = null; p.School = null; p.Title = null; p.Zi = null; + p.Nation = null; p.Address = null; p.IdNumber = null; p.Issuer = null; p.Validity = null; + p.IdCardFilename = null; p.IdCardUrl = null; + p.AvatarFilename = null; p.IsCanAmend = null; + } + + /// 全系图下发瘦身(配偶视图行):图只用双方姓名/序号,婚嫁/离异/备注等列置空丢弃(Gender 是拥有者本人性别,前端不用) + private static void SlimSpouseMapForChart(FamilyTreeSpouseMapViewEntity s) + { + if (s == null) return; + s.Gender = null; s.MarryDate = null; s.MarryDateText = null; + s.DivorceDate = null; s.DivorceDateText = null; s.MarryType = null; s.IsCurrent = null; s.Remark = null; + } + + /// 全系图下发瘦身(亲子视图行):图只用于「父→子」拓扑与子女姓名,拥有者姓名/Gender/配偶 id/备注置空丢弃 + private static void SlimChildMapForChart(FamilyTreeChildrenMapViewEntity c) + { + if (c == null) return; + c.Gender = null; c.CommonName = null; c.GenealogyName = null; c.SpouseId = null; c.Remark = null; + } + [PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)] public FamilyTreeData FindAllByTreeId(string treeId) { @@ -79,6 +111,10 @@ namespace CloudBuilder.Master.Service data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray(); + // 全系图一次性下发整谱两套关系视图,大谱体量惊人:只保留卡片渲染所需列,其余置空丢弃 + foreach (var s in data.CurrentPerson.FamilyTreeSpouseMaps) SlimSpouseMapForChart(s); + foreach (var c in data.CurrentPerson.FamilyTreeChildrenMaps) SlimChildMapForChart(c); + List ps = new List(); ps.Add(d.AncestorPersonId); if (data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0) @@ -96,6 +132,8 @@ namespace CloudBuilder.Master.Service { data.CurrentPerson.FamilyTreePersons = families.Select(x => new FamilyPersonDisplayData(x)).ToArray(); FillAvatarUrl(data.CurrentPerson.FamilyTreePersons); + // 头像 URL 已生成,整谱人员只留卡片渲染所需列,压掉其余可空列(在 FillAvatarUrl 之后做,勿丢 AvatarFilename 依赖) + foreach (var person in data.CurrentPerson.FamilyTreePersons) SlimPersonForChart(person); } return data; diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user index 77e90f6..26429fd 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user +++ b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2 - True|2026-10-23T07:18:36.1507056Z||;True|2026-10-23T09:45:02.7814696+08:00||;True|2026-10-22T14:27:02.4892588+08:00||;True|2026-10-21T21:22:01.1456473+08:00||;True|2026-10-20T15:45:39.3393674+08:00||;True|2026-10-17T20:49:21.0401213+08:00||;True|2026-09-23T13:19:13.0148919+08:00||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; + True|2026-10-26T01:48:34.0694643Z||;True|2026-10-23T16:26:07.0320450+08:00||;True|2026-10-23T15:18:36.1507056+08:00||;True|2026-10-23T09:45:02.7814696+08:00||;True|2026-10-22T14:27:02.4892588+08:00||;True|2026-10-21T21:22:01.1456473+08:00||;True|2026-10-20T15:45:39.3393674+08:00||;True|2026-10-17T20:49:21.0401213+08:00||;True|2026-09-23T13:19:13.0148919+08:00||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; \ No newline at end of file diff --git a/uniapp/youjiala/package.json b/uniapp/youjiala/package.json index 64b2f42..7c494db 100644 --- a/uniapp/youjiala/package.json +++ b/uniapp/youjiala/package.json @@ -1,7 +1,7 @@ { "name": "youjiala", "type": "module", - "version": "1.0.1", + "version": "1.0.2", "private": true, "overrides": { "unconfig": "7.3.2" diff --git a/uniapp/youjiala/src/App.root.vue b/uniapp/youjiala/src/App.root.vue index e4eb6f2..0cc3287 100644 --- a/uniapp/youjiala/src/App.root.vue +++ b/uniapp/youjiala/src/App.root.vue @@ -1,14 +1,145 @@ + + - + diff --git a/uniapp/youjiala/src/common/constant.ts b/uniapp/youjiala/src/common/constant.ts index 3a499b0..fc74f5c 100644 --- a/uniapp/youjiala/src/common/constant.ts +++ b/uniapp/youjiala/src/common/constant.ts @@ -5,7 +5,7 @@ export const APP_INFO : { } = { // 版本号需与 src/manifest.json 的 versionName / package.json 的 version 保持一致(改版本时三处同步) name: "有家相册", - version: "1.0.1", + version: "1.0.2", platform: getPlatform(), }; diff --git a/uniapp/youjiala/src/common/global-error.ts b/uniapp/youjiala/src/common/global-error.ts index 25b0f27..81df5a3 100644 --- a/uniapp/youjiala/src/common/global-error.ts +++ b/uniapp/youjiala/src/common/global-error.ts @@ -111,6 +111,26 @@ function shouldNotify(text: string): boolean { /** 错误来源:运行时异常 / 未处理的 Promise 拒绝 */ export type GlobalErrorSource = "error" | "rejection"; +/** + * vue-router 导航竞态的可预期拒绝(非应用故障):H5 下 uni.reLaunch/navigateTo/redirectTo + * 返回的 promise,在「新导航取消旧导航 / 重复导航 / 重定向中止」时会 reject。 + * 冷启动地址栏停在 "/" 时,守卫把它 reLaunch 去 /login 与首屏路由解析撞车最常见—— + * 导航其实成功(人已落在登录页),只是被后一条导航抢先,前一条留下一个"cancelled"拒绝。 + * 这类拒绝不该打扰用户:全局边界对它只留 console 痕迹、不弹「未捕获错误」。 + * 文案取 uni 前缀后的 vue-router 原话,命中任一即判为良性。 + */ +const BENIGN_ROUTER_PATTERNS = [ + "Navigation cancelled", + "Navigation aborted", + "NavigationAborted", + "duplicated navigation", + "Redirected when going", +]; + +function isBenignRouterRejection(text: string): boolean { + return BENIGN_ROUTER_PATTERNS.some(pattern => text.includes(pattern)); +} + /** 开发期弹窗最长占用时间:过了这个点就允许下一条弹窗顶上 */ const MODAL_MAX_HOLD_MS = 5000; @@ -146,6 +166,10 @@ export function reportGlobalError( const text = describe(error); // 全局边界的职责就是留痕:console.error 在 no-console 配置里本就放行,无需 disable 注释 console.error(`[global:${source}]`, error); + // vue-router 导航竞态的可预期拒绝:留了 console 痕迹即可,不打扰用户(详见 BENIGN_ROUTER_PATTERNS 注释) + if (source === "rejection" && isBenignRouterRejection(text)) { + return; + } if (isNotified(error)) { return; } diff --git a/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue b/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue index 3d7de79..8ecccac 100644 --- a/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue +++ b/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue @@ -15,9 +15,9 @@ import type { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChil import type { FamilyTreeData } from "@/data/auto/familyTreeData"; import type { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity"; import { useLocale } from "uview-pro"; -import { computed, provide, ref, watch } from "vue"; +import { computed, onBeforeUnmount, provide, ref, watch } from "vue"; import { usePolicyOptions } from "@/composables/usePolicyOptions"; -import familyTreeService from "@/service/master/familyTreeService"; +import { request } from "@/utils/request"; import { FAMILY_TREE_CHART_API } from "./types"; const props = defineProps({ @@ -56,6 +56,10 @@ interface FullIndex { currentId: string; /** 主系链:父 PersonId -> 该父子女中位于「祖→当前族人」链上的那一个,排序时置顶(旧图主系居首同款) */ lineageChildOf: Map; + /** 主系脊(始祖→当前族人的直连祖先链,含两端):默认展开恒开,保证当前族人可达可见 */ + spine: Set; + /** 各人世代号(始祖=0,按亲子视图自顶向下 BFS):±5 代窗口据此判定 */ + genOf: Map; } const fullIndex = ref(null); @@ -250,6 +254,41 @@ function toggle(personId: string): void { expanded.value = { ...expanded.value, [personId]: !expanded.value[personId] }; } +// ===== 全系图默认展开口径:主系脊恒开 + 当前族人 ±5 代窗口展开,其余折叠待点击懒展开 ===== +const MAIN_LINE_UP = 5; +const MAIN_LINE_DOWN = 5; +/** 一键展开全部(大谱会变卡,由用户自担);关闭回到 ±5 代窗口默认 */ +const expandAll = ref(false); + +/** 依当前口径算展开集:expandAll=全展开;否则主系脊 ∪ 世代窗口内节点 */ +function computeExpanded(): Record { + const index = fullIndex.value; + const next: Record = {}; + if (!index) { + return next; + } + if (index.rootId) { + next[index.rootId] = true; + } + const curGen = index.genOf.get(index.currentId); + index.childrenOf.forEach((_list: any[], parentId: string) => { + if (expandAll.value || index.spine.has(parentId)) { + next[parentId] = true; + return; + } + const g = index.genOf.get(parentId); + if (curGen !== undefined && g !== undefined && g >= curGen - MAIN_LINE_UP && g <= curGen + MAIN_LINE_DOWN) { + next[parentId] = true; + } + }); + return next; +} + +function toggleExpandAll(): void { + expandAll.value = !expandAll.value; + expanded.value = computeExpanded(); +} + // ===== 缩放与布局方向(弹窗右上控制条)===== const ZOOM_MIN = 0.5; const ZOOM_MAX = 2; @@ -295,7 +334,9 @@ function onCtrlTouchMove(e: TouchEvent): void { } const sys = uni.getSystemInfoSync(); const barW = uni.upx2px(88); - const barH = uni.upx2px(384); // 握把 + 4 动作钮各 76 + 分隔线 + // 握把 + 动作钮各 76 + 分隔线;全系图多一枚「展开全部」钮,高度随之 +76 + const btnCount = props.mode === "full" ? 5 : 4; + const barH = uni.upx2px(76 + btnCount * 76 + 4); const edge = uni.upx2px(16); // 默认锚点(top/right 定位)换算成 left/top 基准,再按屏幕边界夹取 const baseLeft = sys.windowWidth - uni.upx2px(24) - barW; @@ -350,6 +391,93 @@ function onChartTouchEnd(e: TouchEvent): void { } } +// ===== H5 鼠标拖拽平移:scroll-view 桌面端只能滚轮/触控板滚,按住拖动不动,补一套 mousedown→改 scroll-left/top ===== +// 走受控 scroll-left/scroll-top(uni 官方属性,跨版本稳):拖动中改这两个 ref 让 scroll-view 滚, +// 非拖动的滚轮/触摸由 @scroll 回填 ref(panning 时不回填,避免和手打架)。 +const scrollLeft = ref(0); +const scrollTop = ref(0); +const isPanning = ref(false); +let panning = false; +let panMoved = false; +let panStartX = 0; +let panStartY = 0; +let panStartLeft = 0; +let panStartTop = 0; +// 拖拽刚结束的时刻:node 用它丢弃「拖完松手那一下」误触发的 tap +let panEndedAt = 0; + +function onPanStart(e: MouseEvent): void { + if (typeof window === "undefined") { + return; + } + // 只认鼠标左键;右键/中键交回浏览器,触摸平移走原生滚动 + if (e.button !== 0) { + return; + } + panning = true; + panMoved = false; + isPanning.value = true; + panStartX = e.clientX; + panStartY = e.clientY; + panStartLeft = scrollLeft.value; + panStartTop = scrollTop.value; + // 不能 preventDefault:它会取消后续原生 click,卡片 @tap 再也收不到(float-add-btn 同款教训); + // 文本选中与图片拖影改由 CSS user-select:none + -webkit-user-drag:none 拦 + window.addEventListener("mousemove", onPanMove); + window.addEventListener("mouseup", onPanEnd); +} + +function onPanMove(e: MouseEvent): void { + if (!panning) { + return; + } + const dx = e.clientX - panStartX; + const dy = e.clientY - panStartY; + if (Math.abs(dx) > 4 || Math.abs(dy) > 4) { + panMoved = true; + } + // 抓住内容反向拖:鼠标右移→视口左移(scrollLeft 减小) + scrollLeft.value = Math.max(0, panStartLeft - dx); + scrollTop.value = Math.max(0, panStartTop - dy); +} + +function onPanEnd(): void { + if (!panning) { + return; + } + panning = false; + isPanning.value = false; + if (typeof window !== "undefined") { + window.removeEventListener("mousemove", onPanMove); + window.removeEventListener("mouseup", onPanEnd); + } + if (panMoved) { + panEndedAt = Date.now(); + } +} + +/** 滚轮/触摸滚动时把真实偏移同步回受控值(拖动中跳过,避免覆盖刚算出的目标位置) */ +function onChartScroll(e: any): void { + if (panning) { + return; + } + scrollLeft.value = e?.detail?.scrollLeft ?? scrollLeft.value; + scrollTop.value = e?.detail?.scrollTop ?? scrollTop.value; +} + +/** 拖拽平移刚结束的一瞬为真:node 用它丢弃误触 tap */ +function didPanDrag(): boolean { + return Date.now() - panEndedAt < 250; +} + +// 拖拽中途弹窗被关(组件卸载)也要摘掉 window 监听,避免泄漏与卸载后仍改 ref +onBeforeUnmount(() => { + if (typeof window !== "undefined") { + window.removeEventListener("mousemove", onPanMove); + window.removeEventListener("mouseup", onPanEnd); + } +}); + function requestLoad(personId: string): void { emit("select-child", personId); } @@ -366,6 +494,7 @@ provide(FAMILY_TREE_CHART_API, { requestPick, isMale, vertical, + didPanDrag, }); /** 全树:从 FamilyTreeData 拍平三张索引表(照抄旧 buildIndex 的字段映射) */ @@ -450,7 +579,26 @@ function buildFullIndex(resp: FamilyTreeData): FullIndex { lineageChildOf.set(parentId, cursor); cursor = parentId; } - return { rootId, personMap, spousesOf, childrenOf, parentsOf, currentId, lineageChildOf }; + // 主系脊:visited 即「当前族人 + 其历代父辈直至始祖」,默认展开据此恒开 + const spine = new Set(visited); + + // 世代号:自始祖按亲子视图 BFS(一人多父取首父世代,图以主承担父渲染子树,够用且稳) + const genOf = new Map(); + if (rootId) { + genOf.set(rootId, 0); + const queue: string[] = [rootId]; + for (let i = 0; i < queue.length; i++) { + const id = queue[i]; + const g = genOf.get(id) as number; + (childrenOf.get(id) ?? []).forEach((c: any) => { + if (!genOf.has(c.id)) { + genOf.set(c.id, g + 1); + queue.push(c.id); + } + }); + } + } + return { rootId, personMap, spousesOf, childrenOf, parentsOf, currentId, lineageChildOf, spine, genOf }; } async function loadFull(): Promise { @@ -461,7 +609,14 @@ async function loadFull(): Promise { } try { loading.value = true; - const resp = await familyTreeService.FindAllByTreeId(treeId); + // 整谱一次性下发(大谱数千族员),体量远超常规接口,走生成 service 拿不到单次超时会被全局 20s 掐断 + // 报「请求错误:未知」,故这里直连请求层并放宽到 90s(后端已同步瘦身,只下发卡片渲染所需列) + const resp = await request({ + url: "/FamilyTree/FindAllByTreeId?treeId=" + treeId, + method: "post", + data: treeId, + timeout: 90000, + }); if (resp?.CurrentPerson) { fullIndex.value = buildFullIndex(resp); } @@ -478,7 +633,7 @@ async function loadFull(): Promise { } } -/** 弹窗打开:重置展开态到根节点;full 默认全树展开(旧祖谱系图同款),sub 仅展开根待懒加载 */ +/** 弹窗打开:重置展开态;full 默认只铺主系脊+当前族人±5代窗口(旁系/更深世代折叠懒展开,可一键全展开),sub 仅展开根待懒加载 */ watch( () => props.visible, (on) => { @@ -487,16 +642,10 @@ watch( } expanded.value = {}; if (props.mode === "full") { + expandAll.value = false; loadFull().then(() => { - const next: Record = {}; - if (rootPersonId.value) { - next[rootPersonId.value] = true; - } - // 所有有子女的族员一律展开;非主承担父由 node 的 expandable 门控,不会重复渲染子树 - fullIndex.value?.childrenOf.forEach((_list: any[], parentId: string) => { - next[parentId] = true; - }); - expanded.value = next; + // 默认只铺主系脊与当前族人 ±5 代窗口,旁系/更深世代折叠,靠 node 点击懒展开(避免整谱全展开卡死 H5) + expanded.value = computeExpanded(); }); } else if (rootPersonId.value) { @@ -556,10 +705,17 @@ function close(): void { ⇅ + + + {{ expandAll ? '⊟' : '⊞' }} + - - + + @@ -664,6 +820,16 @@ function close(): void { border-bottom-color: #f2f3f5; } +/* 展开全部态:青底白字,与默认「±5 代窗口」折叠态区分(对齐 node 动作钮青配色) */ +.ftc__ctrl-btn--active { + .ftc__ctrl-glyph { + color: #ffffff; + } + + background-color: #13c2c2; + border-bottom-color: #13c2c2; +} + .ftc__ctrl-glyph { font-size: 34rpx; color: #1f2329; @@ -678,6 +844,21 @@ function close(): void { flex: 1; min-height: 0; white-space: nowrap; + + /* H5 鼠标拖拽平移:手型光标 + 禁选(图是示意不是文本,拖动时不选中卡内文字/不拖出图片幽灵) */ + cursor: grab; + user-select: none; + -webkit-user-select: none; +} + +.ftc__scroll--panning { + cursor: grabbing; +} + +/* 拖拽平移时不把头像拖出原生图片幽灵(preventDefault 会误伤 @tap,改由 CSS 拦) */ +.ftc__scroll :deep(img) { + -webkit-user-drag: none; + user-select: none; } .ftc__tree { diff --git a/uniapp/youjiala/src/components/family-tree-chart/family-tree-node.vue b/uniapp/youjiala/src/components/family-tree-chart/family-tree-node.vue index 9790d84..5297387 100644 --- a/uniapp/youjiala/src/components/family-tree-chart/family-tree-node.vue +++ b/uniapp/youjiala/src/components/family-tree-chart/family-tree-node.vue @@ -62,6 +62,10 @@ const actionText = computed(() => { const actionClass = computed(() => (loaded.value ? "ftn__action--toggle" : "ftn__action--load")); function onCardTap(): void { + // 刚做过鼠标拖拽平移:松手这一下是拖拽副产物,不是点击,丢弃以免误展开/误选人 + if (api.didPanDrag()) { + return; + } if (!loaded.value) { api.requestLoad(props.personId); return; diff --git a/uniapp/youjiala/src/components/family-tree-chart/types.ts b/uniapp/youjiala/src/components/family-tree-chart/types.ts index 5714f35..5bc72e7 100644 --- a/uniapp/youjiala/src/components/family-tree-chart/types.ts +++ b/uniapp/youjiala/src/components/family-tree-chart/types.ts @@ -57,6 +57,8 @@ export interface FamilyTreeChartApi { isMale: (gender?: string) => boolean; /** 布局方向:false=世代向右(横向阶梯),true=世代向下(纵向排列),由容器 ⇅ 按钮切换 */ vertical: Ref; + /** H5 鼠标拖拽平移刚结束的一瞬返回 true:node 据此丢弃拖拽误触发的 tap,避免拖完手滑点到卡片 */ + didPanDrag: () => boolean; } /** sub 模式懒加载缓存的一项:某人的原始「人 + 配偶 + 子女」记录 */ diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json index 8647d48..a009136 100644 --- a/uniapp/youjiala/src/locale/lang/en-US.json +++ b/uniapp/youjiala/src/locale/lang/en-US.json @@ -130,6 +130,8 @@ "checkFailed": "Update check failed. Check your network or access line and retry", "downloading": "Downloading installer…", "downloadFailed": "Download failed, please retry later", + "preparing": "Preparing installation…", + "installFailed": "Install failed. Allow installing unknown apps in Settings and retry", "installHint": "Please finish installing in the system dialog, then reopen the app", "iosTip": "In-app install is not available on iOS. Please download the latest package from the website", "gotIt": "Got it" diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json index c77ede2..835d70f 100644 --- a/uniapp/youjiala/src/locale/lang/zh-CN.json +++ b/uniapp/youjiala/src/locale/lang/zh-CN.json @@ -130,6 +130,8 @@ "checkFailed": "检查更新失败,请检查网络或访问线路后重试", "downloading": "下载安装包…", "downloadFailed": "下载失败,请稍后重试", + "preparing": "正在准备安装…", + "installFailed": "安装失败,请在系统设置中允许「安装未知应用」后重试", "installHint": "请在弹出的安装界面完成安装,装好后重开即为新版", "iosTip": "iOS 暂不支持应用内安装,请前往网页版下载最新安装包", "gotIt": "知道了" diff --git a/uniapp/youjiala/src/manifest.json b/uniapp/youjiala/src/manifest.json index 622a811..4f48f1b 100644 --- a/uniapp/youjiala/src/manifest.json +++ b/uniapp/youjiala/src/manifest.json @@ -1,160 +1,160 @@ { - "name": "有家相册", - "appid": "__UNI__F0B0BDF", - "description": "有家相册:家庭相册、家族树与文档共享的跨端应用", - "versionName": "1.0.1", - "versionCode": 2, - "transformPx": false, - "uniStatistics": { - "enable": false - }, - "app-plus": { - "usingComponents": true, - "nvueStyleCompiler": "uni-app", - "compilerVersion": 3, - "splashscreen": { - "alwaysShowBeforeRender": true, - "waiting": true, - "autoclose": true, - "delay": 0 + "name" : "有家相册", + "appid" : "__UNI__F0B0BDF", + "description" : "有家相册:家庭相册、家族树与文档共享的跨端应用", + "versionName" : "1.0.2", + "versionCode" : 3, + "transformPx" : false, + "uniStatistics" : { + "enable" : false }, - "safearea": { - "background": "#ffffff", - "backgroundDark": "#2f0508", - "bottom": { - "offset": "none" - } - }, - "modules": { - "VideoPlayer": {} - }, - "privacy": { - "prompt": "template", - "template": { - "title": "服务协议和隐私政策", - "message": "感谢你使用“有家相册”。我们非常重视你的个人信息保护,请你务必审慎阅读、充分理解《服务协议》和《隐私政策》各条款,包括但不限于:为向你提供账号注册登录、家庭相册、家族谱系与文件共享服务,我们会在你授权同意后收集你的手机号、头像、身份证照片(仅实名核验用)、你主动上传的照片/视频/文件以及必要的设备标识与操作日志等信息。
你可阅读《服务协议》和《隐私政策》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", - "buttonAccept": "同意并接受", - "buttonRefuse": "暂不同意", - "second": { - "title": "确认提示", - "message": "进入应用前,你需先同意《服务协议》和《隐私政策》,否则将退出应用。", - "buttonAccept": "同意并继续", - "buttonRefuse": "退出应用" - } - } - }, - "distribute": { - "android": { - "permissions": [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ] - }, - "ios": { - "dSYMs": false, - "privacyDescription": { - "NSCameraUsageDescription": "用于拍摄头像、身份证照片及上传家庭相册中的照片和视频", - "NSPhotoLibraryUsageDescription": "用于从相册选择头像、身份证照片及上传照片和视频", - "NSPhotoLibraryAddUsageDescription": "用于将照片或视频保存到系统相册" - } - }, - "sdkConfigs": {}, - "icons": { - "android": { - "hdpi": "unpackage/res/icons/72x72.png", - "xhdpi": "unpackage/res/icons/96x96.png", - "xxhdpi": "unpackage/res/icons/144x144.png", - "xxxhdpi": "unpackage/res/icons/192x192.png" + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 }, - "ios": { - "appstore": "unpackage/res/icons/1024x1024.png", - "ipad": { - "app": "unpackage/res/icons/76x76.png", - "app@2x": "unpackage/res/icons/152x152.png", - "notification": "unpackage/res/icons/20x20.png", - "notification@2x": "unpackage/res/icons/40x40.png", - "proapp@2x": "unpackage/res/icons/167x167.png", - "settings": "unpackage/res/icons/29x29.png", - "settings@2x": "unpackage/res/icons/58x58.png", - "spotlight": "unpackage/res/icons/40x40.png", - "spotlight@2x": "unpackage/res/icons/80x80.png" - }, - "iphone": { - "app@2x": "unpackage/res/icons/120x120.png", - "app@3x": "unpackage/res/icons/180x180.png", - "notification@2x": "unpackage/res/icons/40x40.png", - "notification@3x": "unpackage/res/icons/60x60.png", - "settings@2x": "unpackage/res/icons/58x58.png", - "settings@3x": "unpackage/res/icons/87x87.png", - "spotlight@2x": "unpackage/res/icons/80x80.png", - "spotlight@3x": "unpackage/res/icons/120x120.png" - } + "safearea" : { + "background" : "#ffffff", + "backgroundDark" : "#2f0508", + "bottom" : { + "offset" : "none" + } + }, + "modules" : { + "VideoPlayer" : {} + }, + "privacy" : { + "prompt" : "template", + "template" : { + "title" : "服务协议和隐私政策", + "message" : "感谢你使用“有家相册”。我们非常重视你的个人信息保护,请你务必审慎阅读、充分理解《服务协议》和《隐私政策》各条款,包括但不限于:为向你提供账号注册登录、家庭相册、家族谱系与文件共享服务,我们会在你授权同意后收集你的手机号、头像、身份证照片(仅实名核验用)、你主动上传的照片/视频/文件以及必要的设备标识与操作日志等信息。
你可阅读《服务协议》和《隐私政策》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", + "buttonAccept" : "同意并接受", + "buttonRefuse" : "暂不同意", + "second" : { + "title" : "确认提示", + "message" : "进入应用前,你需先同意《服务协议》和《隐私政策》,否则将退出应用。", + "buttonAccept" : "同意并继续", + "buttonRefuse" : "退出应用" + } + } + }, + "distribute" : { + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "ios" : { + "dSYMs" : false, + "privacyDescription" : { + "NSCameraUsageDescription" : "用于拍摄头像、身份证照片及上传家庭相册中的照片和视频", + "NSPhotoLibraryUsageDescription" : "用于从相册选择头像、身份证照片及上传照片和视频", + "NSPhotoLibraryAddUsageDescription" : "用于将照片或视频保存到系统相册" + } + }, + "sdkConfigs" : {}, + "icons" : { + "android" : { + "hdpi" : "unpackage/res/icons/72x72.png", + "xhdpi" : "unpackage/res/icons/96x96.png", + "xxhdpi" : "unpackage/res/icons/144x144.png", + "xxxhdpi" : "unpackage/res/icons/192x192.png" + }, + "ios" : { + "appstore" : "unpackage/res/icons/1024x1024.png", + "ipad" : { + "app" : "unpackage/res/icons/76x76.png", + "app@2x" : "unpackage/res/icons/152x152.png", + "notification" : "unpackage/res/icons/20x20.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "proapp@2x" : "unpackage/res/icons/167x167.png", + "settings" : "unpackage/res/icons/29x29.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "spotlight" : "unpackage/res/icons/40x40.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png" + }, + "iphone" : { + "app@2x" : "unpackage/res/icons/120x120.png", + "app@3x" : "unpackage/res/icons/180x180.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "notification@3x" : "unpackage/res/icons/60x60.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "settings@3x" : "unpackage/res/icons/87x87.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png", + "spotlight@3x" : "unpackage/res/icons/120x120.png" + } + } + } + } + }, + "h5" : { + "router" : { + "mode" : "history", + "base" : "/" + }, + "devServer" : { + "host" : "::", + "port" : 5173 + }, + "darkmode" : true, + "themeLocation" : "theme.json" + }, + "quickapp" : {}, + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true, + "darkmode" : true, + "themeLocation" : "theme.json" + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "vueVersion" : "3", + "app-harmony" : { + "darkmode" : true, + "themeLocation" : "theme.json", + "safearea" : { + "background" : "#ffffff", + "backgroundDark" : "#2f0508", + "bottom" : { + "offset" : "none" + } + }, + "distribute" : { + "bundleName" : "com.youjiala.app", + "icons" : { + "foreground" : "unpackage/res/icons/1024x1024.png", + "background" : "unpackage/res/icons/1024x1024.png" + }, + "splashScreens" : { + "startWindowBackground" : "#FFFFFF", + "startWindowIcon" : "unpackage/res/icons/1024x1024.png" + }, + "signingConfigs" : {} } - } } - }, - "h5": { - "router": { - "mode": "history", - "base": "/" - }, - "devServer": { - "host": "::", - "port": 5173 - }, - "darkmode": true, - "themeLocation": "theme.json" - }, - "quickapp": {}, - "mp-weixin": { - "appid": "", - "setting": { - "urlCheck": false - }, - "usingComponents": true, - "darkmode": true, - "themeLocation": "theme.json" - }, - "mp-alipay": { - "usingComponents": true - }, - "mp-baidu": { - "usingComponents": true - }, - "mp-toutiao": { - "usingComponents": true - }, - "vueVersion": "3", - "app-harmony": { - "darkmode": true, - "themeLocation": "theme.json", - "safearea": { - "background": "#ffffff", - "backgroundDark": "#2f0508", - "bottom": { - "offset": "none" - } - }, - "distribute": { - "bundleName": "com.youjiala.app", - "icons": { - "foreground": "unpackage/res/icons/1024x1024.png", - "background": "unpackage/res/icons/1024x1024.png" - }, - "splashScreens": { - "startWindowBackground": "#FFFFFF", - "startWindowIcon": "unpackage/res/icons/1024x1024.png" - }, - "signingConfigs": {} - } - } } diff --git a/uniapp/youjiala/src/utils/appUpdate.ts b/uniapp/youjiala/src/utils/appUpdate.ts index 1c0fa9c..5aae7e8 100644 --- a/uniapp/youjiala/src/utils/appUpdate.ts +++ b/uniapp/youjiala/src/utils/appUpdate.ts @@ -1,3 +1,4 @@ +import { reactive } from "vue"; import { currentUiLocale, UI_LOCALE_EN } from "@/common/locale-const"; import { enUS, zhCN } from "@/locale"; import { absoluteFileUrl } from "@/utils/fileUrl"; @@ -45,6 +46,21 @@ const MANIFEST_PATH = "/FileServer/app-update.json"; /** 「稍后再说」记账:记下被跳过的 versionCode,同一版本自动检查不再弹 */ const SKIP_KEY = "app-update-skipped"; +/** + * 下载/安装进度(供 App.root.vue 的全局遮罩进度条渲染)。 + * percent<0 表示服务器未回报总大小(无 Content-Length)→ 走不定量动画; + * 文案 title 在本模块用 msg() 取好词(非组件上下文拿不到 useLocale),页面只读不算。 + */ +export const updateProgress = reactive({ + visible: false, + phase: "download" as "download" | "install", + title: "", + /** 0-100 已下载百分比;<0=总量未知的不确定态 */ + percent: 0, + downloaded: 0, + total: 0, +}); + /** 非组件上下文的取词:AppUpdate.xxx → 当前语言包词条,缺失回落另一语言 */ function msg(key: string, vars?: Record): string { const primary = currentUiLocale() === UI_LOCALE_EN ? enUS : zhCN; @@ -133,55 +149,56 @@ function promptUpdate(info: AppUpdateInfo): Promise { }); } -/** 下整包并拉起系统安装器:showLoading 反复调用即刷进度文案;安装器弹出后由用户完成安装 */ -function downloadAndInstall(info: AppUpdateInfo): Promise { +/** 下整包:plus.downloader 拉 apk,statechange 里刷进度到 updateProgress;返回本地路径 */ +function downloadApk(info: AppUpdateInfo): Promise { return new Promise((resolve, reject) => { const url = absoluteFileUrl(`/FileServer/${info.apk ?? "youjiala.apk"}`); - uni.showLoading({ title: msg("AppUpdate.downloading"), mask: true }); + // 保存路径自己定死并作为兜底:plus.downloader 成功后回的是 download.filename(不是 localPath) + const savePath = `_downloads/update-${info.versionCode}.apk`; + updateProgress.visible = true; + updateProgress.phase = "download"; + updateProgress.title = msg("AppUpdate.downloading"); + updateProgress.percent = 0; + updateProgress.downloaded = 0; + updateProgress.total = 0; const task = plus.downloader.createDownload( url, { - filename: `_downloads/update-${info.versionCode}.apk`, - retry: 1, - timeout: 300, + filename: savePath, + retry: 2, + timeout: 600, }, - (download: { localPath?: string }, status: number) => { - uni.hideLoading(); - if (status !== 200 || !download?.localPath) { - reject(new Error(`download HTTP ${status}`)); + (download: { filename?: string }, status: number) => { + // 判成功只看 HTTP 200;路径取 filename,缺失回落到我们指定的 savePath(旧代码错读 localPath 导致 200 也判失败) + if (status !== 200) { + reject(new Error(`下载未完成(HTTP ${status})\n${url}`)); return; } - plus.runtime.install( - plus.io.convertLocalFileSystemURL(download.localPath), - {}, - () => { - // apk 分支走系统安装界面,装完重开即是新版;这里只兜底提示 - uni.showToast({ - title: msg("AppUpdate.installHint"), - icon: "none", - duration: 3000, - }); - resolve(); - }, - (error: unknown) => reject(error), - ); + updateProgress.percent = 100; + resolve(download?.filename || savePath); }, ); + // 事件名是 statechanged(带 d),且进度字段挂在下载对象上(部分版本在 event.target),旧代码写错名/取错层→一直 0% task.addEventListener( - "statechange", - (event: { - state: number; - downloadedSize?: number; - totalSize?: number; - }) => { - const total = Number(event.totalSize ?? 0); - const done = Number(event.downloadedSize ?? 0); - // state=4 下载中:apk 十几 MB,按百分比刷新比转圈更安心 - if (event.state === 4 && total > 0) { - uni.showLoading({ - title: `${msg("AppUpdate.downloading")} ${Math.min(99, Math.floor((done / total) * 100))}%`, - mask: true, - }); + "statechanged", + (event: any) => { + const t = event?.target ?? event; + updateProgress.downloaded = Number(t?.downloadedSize ?? 0); + updateProgress.total = Number(t?.totalSize ?? 0); + const state = Number(t?.state ?? 0); + // state:3 开始接收数据 / 4 下载中 / 5 完成 / 6 失败 + if (state === 4 || state === 3) { + updateProgress.percent = + updateProgress.total > 0 + ? Math.min( + 99, + Math.floor( + (updateProgress.downloaded / updateProgress.total) * 100, + ), + ) + : -1; // 无 Content-Length:不确定态动画 + } else if (state === 6) { + reject(new Error(`下载中断\n${url}`)); } }, ); @@ -189,6 +206,26 @@ function downloadAndInstall(info: AppUpdateInfo): Promise { }); } +/** 拉起系统安装器:装完由用户在系统界面确认;成功即 resolve,失败带出原因 */ +function installApk(localPath: string): Promise { + return new Promise((resolve, reject) => { + updateProgress.phase = "install"; + updateProgress.title = msg("AppUpdate.preparing"); + updateProgress.percent = 100; + plus.runtime.install( + plus.io.convertLocalFileSystemURL(localPath), + {}, + () => resolve(), + (error: any) => + reject( + new Error( + `拉起安装失败(${error?.message ?? error?.code ?? error})`, + ), + ), + ); + }); +} + function readSkippedCode(): number { try { return Number(uni.getStorageSync(SKIP_KEY) ?? 0); @@ -262,9 +299,31 @@ export async function checkAppUpdate( return "updated"; } try { - await downloadAndInstall(info); - } catch { - uni.showToast({ title: msg("AppUpdate.downloadFailed"), icon: "none" }); + const localPath = await downloadApk(info); + await installApk(localPath); + // 安装器已拉起,遮罩交回系统界面;提示用户完成安装 + updateProgress.visible = false; + uni.showToast({ + title: msg("AppUpdate.installHint"), + icon: "none", + duration: 3000, + }); + return "updated"; + } catch (error: any) { + // 下载/安装失败:区分阶段,把真实原因(HTTP 码/中断/安装被拒)摆到弹窗与日志,别再笼统报「下载失败」 + const failedInstall = updateProgress.phase === "install"; + updateProgress.visible = false; + const reason = error instanceof Error ? error.message : String(error); + console.error("[app-update] 下载/安装失败", { + versionCode: info.versionCode, + reason, + }); + uni.showModal({ + title: msg(failedInstall ? "AppUpdate.installFailed" : "AppUpdate.downloadFailed"), + content: reason, + showCancel: false, + confirmText: msg("AppUpdate.gotIt"), + }); return "failed"; } return "updated"; diff --git a/uniapp/youjiala/src/utils/request.ts b/uniapp/youjiala/src/utils/request.ts index dce6fc9..dea8776 100644 --- a/uniapp/youjiala/src/utils/request.ts +++ b/uniapp/youjiala/src/utils/request.ts @@ -72,6 +72,8 @@ export async function request(config: RequestConfigLite): Promise { data: config.data ?? {}, header: config.header ?? {}, meta: config.meta, + // 整谱类重接口(如全系图 FindAllByTreeId)体量远超常规请求,允许调用方按单次覆盖全局 20s 超时 + timeout: config.timeout, }); }