diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user index 09a78d5..7e78ab0 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-17T12:49:21.0401213Z||;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-20T07:45:39.3393674Z||;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/src/androidPrivacy.json b/uniapp/youjiala/src/androidPrivacy.json new file mode 100644 index 0000000..05153b5 --- /dev/null +++ b/uniapp/youjiala/src/androidPrivacy.json @@ -0,0 +1,35 @@ +{ + "version": "1", + "prompt": "template", + "title": "服务协议和隐私政策", + "message": "感谢你使用“有家相册”。我们非常重视你的个人信息保护,请你务必审慎阅读、充分理解《服务协议》和《隐私政策》各条款,包括但不限于:为向你提供账号注册登录、家庭相册、家族谱系与文件共享服务,我们会在你授权同意后收集你的手机号、头像、身份证照片(仅实名核验用)、你主动上传的照片/视频/文件以及必要的设备标识与操作日志等信息。
你可阅读《服务协议》和《隐私政策》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", + "buttonAccept": "同意并接受", + "buttonRefuse": "暂不同意", + "hrefLoader": "default", + "backToExit": "false", + "second": { + "title": "确认提示", + "message": "进入应用前,你需先同意《服务协议》和《隐私政策》,否则将退出应用。", + "buttonAccept": "同意并继续", + "buttonRefuse": "退出应用" + }, + "disagreeMode": { + "support": false, + "loadNativePlugins": false, + "visitorEntry": false, + "showAlways": false + }, + "styles": { + "backgroundColor": "#ffffff", + "borderRadius": "12px", + "title": { + "color": "#1f2937" + }, + "buttonAccept": { + "color": "#ff6b00" + }, + "buttonRefuse": { + "color": "#6b7280" + } + } +} diff --git a/uniapp/youjiala/src/common/http.interceptor.ts b/uniapp/youjiala/src/common/http.interceptor.ts index 1d22e02..82a4ad8 100644 --- a/uniapp/youjiala/src/common/http.interceptor.ts +++ b/uniapp/youjiala/src/common/http.interceptor.ts @@ -7,7 +7,13 @@ import type { import type { JwtInfo } from "@/data/auto/jwtInfo"; import { http } from "uview-pro"; import { markErrorNotified } from "@/common/global-error"; -import { toBackendLocale } from "@/common/locale-const"; +import { + currentUiLocale, + toBackendLocale, + UI_LOCALE_EN, +} from "@/common/locale-const"; +import enUS from "@/locale/lang/en-US.json"; +import zhCN from "@/locale/lang/zh-CN.json"; import { WebSitePolicy } from "@/policy/custom/webSitePolicy"; import { currentRealPath, @@ -16,6 +22,8 @@ import { resolvePageName, toRealPath, } from "@/router/routes"; +import { writeDownloadFile } from "@/utils/appFileWrite"; +import { copyIntoAppUploadDir } from "@/utils/appUploadFile"; /** * 全项目唯一请求层(基于 uview-pro httpPlugin,main.ts 已注册) @@ -225,6 +233,28 @@ function throwRequestError(message: string, handled: boolean): never { throw handled ? markErrorNotified(err) : err; } +/** + * 后端固定英文短语 → 按界面语言重写。 + * 登录失败后端只下发一句 certification(不区分账号不存在/密码错,防撞库探测), + * 但它的语言取决于请求体 Culture,而 Culture 取的是系统语言—— + * 中文界面 + 英文系统的手机上会弹出英文正文,故在展示前统一本地化。 + */ +const BACKEND_MESSAGE_LOCALE: Record = { + "The certification was not approved.": { + zh: zhCN.LoginForm.loginFailed, + en: enUS.LoginForm.loginFailed, + }, +}; + +/** 把后端异常消息换成当前界面语言(未命中映射原样返回) */ +function localizeBusinessMessage(message: string): string { + const hit = BACKEND_MESSAGE_LOCALE[message.trim()]; + if (!hit) { + return message; + } + return currentUiLocale() === UI_LOCALE_EN ? hit.en : hit.zh; +} + /** * 业务异常识别:HTTP 成功但 body 是 .NET 异常对象时命中 * @returns 命中返回异常消息(调用方需抛出),未命中返回空串 @@ -232,10 +262,10 @@ function throwRequestError(message: string, handled: boolean): never { function detectBusinessException(data: any): string { if (!data || typeof data !== "object" || !data.SourceCode) return ""; - const msg = extractErrorMessage(data) || "业务处理失败"; + const msg = localizeBusinessMessage(extractErrorMessage(data) || "业务处理失败"); if (data.SourceCode === "CloudBuilder.Core.ValidatedException") { uni.showModal({ - title: "业务校验失败", + title: currentUiLocale() === UI_LOCALE_EN ? "Validation failed" : "业务校验失败", content: msg.slice(0, 200), showCancel: false, }); @@ -466,20 +496,15 @@ function xhrUploadFile( } // #endif -/** 上传文件(自动携带 token,失败 reject 可感知)。 - * 后端 PackUploadDmsFile 对 multipart filename 执行 Split('-') 取下标,要求 `uid-序号-总数.ext` - * 三段式(单片 -1-1),否则抛「Index was outside the bounds of the array」;而 uni.uploadFile - * 无法定制 filename,故 H5 改走 XHR + 手写 FormData(与文件页上传队列同款),App 端仍用 uni.uploadFile。 */ -export function uploadFile(options: UploadOptions): Promise { - const { url, filePath, name = "file", fileName, formData, header = {} } = options; - const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url; - const auth = bearerAuth(); - - // #ifdef H5 - return xhrUploadFile(fullUrl, filePath, name, fileName || buildChunkFileName(name), formData, header, auth); - // #endif - - // #ifndef H5 +/** 非 H5 通道:uni.uploadFile 直发(App 侧进门前已把 filePath 准备成协议物理名) */ +function sendByUniUpload( + fullUrl: string, + filePath: string, + name: string, + formData: Record | undefined, + header: Record, + auth: string, +): Promise { return new Promise((resolve, reject) => { uni.uploadFile({ url: fullUrl, @@ -497,6 +522,32 @@ export function uploadFile(options: UploadOptions): Promise { }, }); }); +} + +/** 上传文件(自动携带 token,失败 reject 可感知)。 + * 后端 PackUploadDmsFile 对 multipart filename 执行 Split('-') 取下标,要求 `uid-序号-总数.ext` + * 三段式(单片 -1-1),否则抛「Index was outside the bounds of the array」;而 uni.uploadFile + * 无法定制 filename:H5 改走 XHR + 手写 FormData(与文件页上传队列同款),App 端先把文件复制成 + * 协议物理名再交给 uni.uploadFile(basename 即 filename,头像/身份证/网盘共用同一落盘口径)。 */ +export function uploadFile(options: UploadOptions): Promise { + const { url, filePath, name = "file", fileName, formData, header = {} } = options; + const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url; + const auth = bearerAuth(); + + // #ifdef H5 + return xhrUploadFile(fullUrl, filePath, name, fileName || buildChunkFileName(name), formData, header, auth); + // #endif + + // #ifndef H5 + // #ifdef APP-PLUS + return copyIntoAppUploadDir(filePath, fileName || buildChunkFileName(name)) + .then(targetPath => sendByUniUpload(fullUrl, targetPath, name, formData, header, auth)) + // 复制失败仍按原路径发一次:至少把后端真实报错带回来,而不是前端吞掉 + .catch(() => sendByUniUpload(fullUrl, filePath, name, formData, header, auth)); + // #endif + // #ifndef APP-PLUS + return sendByUniUpload(fullUrl, filePath, name, formData, header, auth); + // #endif // #endif } @@ -525,7 +576,7 @@ function abToString(buffer: ArrayBuffer): string { result += String.fromCharCode(((c & 0x1F) << 6) | (bytes[i + 1] & 0x3F)); i += 2; } - else { + else if (c < 0xF0) { result += String.fromCharCode( ((c & 0x0F) << 12) | ((bytes[i + 1] & 0x3F) << 6) @@ -533,6 +584,18 @@ function abToString(buffer: ArrayBuffer): string { ); i += 3; } + else { + // 4 字节码点(生僻字/emoji)要拆成代理对,否则提示消息乱码 + const point = ((c & 0x07) << 18) + | ((bytes[i + 1] & 0x3F) << 12) + | ((bytes[i + 2] & 0x3F) << 6) + | (bytes[i + 3] & 0x3F); + result += String.fromCharCode( + 0xD800 + ((point - 0x10000) >> 10), + 0xDC00 + ((point - 0x10000) & 0x3FF), + ); + i += 4; + } } return result; } @@ -600,7 +663,7 @@ export async function requestBuffer(options: { return res.data as ArrayBuffer; } -/** 字节按指定文件名落盘:H5 走 blob+;小程序/App 写本地目录后调起系统文档预览 */ +/** 字节按指定文件名落盘:H5 走 blob+;小程序写本地目录、App 走 Native.js 落盘后调起系统文档预览 */ export async function saveArrayBuffer(bytes: ArrayBuffer, fileName: string): Promise { // #ifdef H5 // H5:blob + 隐藏 触发浏览器下载 @@ -618,17 +681,26 @@ export async function saveArrayBuffer(bytes: ArrayBuffer, fileName: string): Pro // #endif // #ifndef H5 - // 小程序/App:写入本地用户目录后调起系统文档预览 - const fs = uni.getFileSystemManager(); - // 写入目录按平台区分:微信用 uni.env.USER_DATA_PATH(官方类型未声明故转 any); - // App 端无 uni.env,写 plus 私有目录 _doc/(openDocument 同样支持该相对路径) - let userDataPath = ""; - // #ifdef MP-WEIXIN - userDataPath = (uni as any).env?.USER_DATA_PATH ?? ""; - // #endif // #ifdef APP-PLUS - userDataPath = "_doc"; + // App 没有 uni.getFileSystemManager(仅小程序可用): + // Android 落公共「下载/有家啦」目录(文件管理器可见),iOS/降级落应用私有目录; + // 落盘后再 openDocument 调起系统预览,打不开不算失败——文件已经保存好了 + const saved = writeDownloadFile(bytes, fileName); + await new Promise((resolve) => { + uni.openDocument({ + filePath: saved.path, + showMenu: true, + success: () => resolve(), + fail: () => resolve(), + }); + }); + return; // #endif + // #ifndef APP-PLUS + // 小程序:写入本地用户目录后调起系统文档预览 + const fs = uni.getFileSystemManager(); + // 写入目录用 uni.env.USER_DATA_PATH(官方类型未声明故转 any) + const userDataPath = (uni as any).env?.USER_DATA_PATH ?? ""; const filePath = `${userDataPath}/${fileName}`; await new Promise((resolve, reject) => { fs.writeFile({ @@ -647,6 +719,7 @@ export async function saveArrayBuffer(bytes: ArrayBuffer, fileName: string): Pro }); }); // #endif + // #endif } /** diff --git a/uniapp/youjiala/src/common/locale-const.ts b/uniapp/youjiala/src/common/locale-const.ts index eb9e3b2..dcfb373 100644 --- a/uniapp/youjiala/src/common/locale-const.ts +++ b/uniapp/youjiala/src/common/locale-const.ts @@ -31,3 +31,20 @@ export function toUiLocale(value: string): string { const l = String(value || "").toLowerCase(); return l.startsWith("en") ? UI_LOCALE_EN : UI_LOCALE_ZH; } + +/** + * 当前界面语言包 name(uView 切换结果)。 + * 给非组件上下文用(拦截器、工具函数):那些地方拿不到 useLocale(), + * 也不能用 uni.getLocale()——它读的是系统语言,界面切了中文它仍可能是 en。 + */ +let currentUiLocaleName = DEFAULT_LOCALE; + +/** 切换界面语言时同步(目前入口在登录页 toggleLang) */ +export function setCurrentUiLocale(value: string): void { + currentUiLocaleName = toUiLocale(value); +} + +/** 取当前界面语言包 name(默认中文) */ +export function currentUiLocale(): string { + return currentUiLocaleName; +} diff --git a/uniapp/youjiala/src/components/app-page/app-page.vue b/uniapp/youjiala/src/components/app-page/app-page.vue index ba3abe3..ee61821 100644 --- a/uniapp/youjiala/src/components/app-page/app-page.vue +++ b/uniapp/youjiala/src/components/app-page/app-page.vue @@ -40,6 +40,20 @@ const background = reactive({ // 渐变色 backgroundImage: "linear-gradient(90deg, var(--u-type-primary-dark), var(--u-type-primary-disabled))", }); + +/** + * 页面入场动画名:App 端改用淡入 fade,不用 slide-left。 + * slide-left 的入场关键帧带 translate3d(40rpx, 0, 0): + * - 本项目多数 tab 页根节点是 position:fixed,而 fixed 遇到 transform 祖先会改以该祖先为包含块, + * 于是整页内容被这 40rpx 位移瞬时推出可视区右缘; + * - App 的 webview 会为此立刻显出页面级横向滚动条(贴在屏幕最底缘的一条细线,形似进度条), + * 动画结束又消失——这就是切 tab 时「底部闪一下」的来源;H5 由 CSS 控制滚动条、小程序无此机制,故只有 App 闪。 + * H5/小程序保留原来的侧滑观感。 + */ +let enterTransition = "slide-left"; +// #ifdef APP-PLUS || APP-HARMONY +enterTransition = "fade"; +// #endif