diff --git a/CloudBuilder/CloudBuilder.Security/Policy/FileTypePolicy.cs b/CloudBuilder/CloudBuilder.Security/Policy/FileTypePolicy.cs index 0ce0057..a150525 100644 --- a/CloudBuilder/CloudBuilder.Security/Policy/FileTypePolicy.cs +++ b/CloudBuilder/CloudBuilder.Security/Policy/FileTypePolicy.cs @@ -4,6 +4,7 @@ using CloudBuilder.Core.DependencyInjection.Policy; using CloudBuilder.Core.DependencyInjection.Request; using CloudBuilder.Core.Extensions; using CloudBuilder.Core.Policy; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.DependencyInjection; using System; @@ -83,6 +84,30 @@ namespace CloudBuilder.Security.Policy if (!IsVideo(orderKey)) return null; return PackUrl(service, createdBy, orderKey, YesNoPolicy.YES, false); } + + /// + /// 资源地址基座:优先按当前请求的 Scheme+Host 拼(App 切内网直连后,缩略图/原图/视频/头像 + /// 返回的就是内网地址,资源自动跟随线路);无请求上下文(后台任务/启动阶段)回退 appsettings 的 ApplicationUrl。 + /// 公网反代场景(nginx → Kestrel 是 http,对外是 https)不能直接用 request.Scheme: + /// 优先取 X-Forwarded-Proto,其次 Host 等于配置域名时沿用配置 scheme,其余(内网 IP/自定义域名)用请求 scheme。 + /// + public static string BuildApplicationUrl(IApplicationService service) + { + string configured = service.Configuration[AppsettingsPolicy.ApplicationUrl] ?? ""; + IHttpContextAccessor accessor = service.ServiceProvider.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor; + HttpRequest request = accessor?.HttpContext?.Request; + if (request == null || !request.Host.HasValue) return configured; + + string scheme = request.Headers["X-Forwarded-Proto"].FirstOrDefault() ?? string.Empty; + if (string.IsNullOrEmpty(scheme)) + { + bool sameAsConfigured = configured.StartsWith("https", StringComparison.OrdinalIgnoreCase) + && Uri.TryCreate(configured, UriKind.Absolute, out Uri configuredUri) + && string.Equals(configuredUri.Host, request.Host.Host, StringComparison.OrdinalIgnoreCase); + scheme = sameAsConfigured ? "https" : request.Scheme; + } + return scheme + "://" + request.Host; + } public static string PackPicUrl(IApplicationService service, string createdBy, string orderKey) { return PackUrl(service, createdBy, orderKey, YesNoPolicy.YES, true); @@ -107,14 +132,14 @@ namespace CloudBuilder.Security.Policy var filePathRel = Path.Combine(backup, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + orderKey; + url = BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } else { var scale = (ServiceSettingsPolicy.GetDmsFileScaleDirectory(service) ?? "").Replace(phy ?? "", ""); var filePathRel = Path.Combine(scale, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + orderKey; + url = BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } } else if (GetType(orderKey!) == AUDIO) @@ -122,7 +147,7 @@ namespace CloudBuilder.Security.Policy var backup = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", ""); var filePathRel = Path.Combine(backup, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + orderKey; + url = BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } else if (GetType(orderKey!) == VIDEO) { @@ -134,14 +159,14 @@ namespace CloudBuilder.Security.Policy var filePathRel = Path.Combine(upload, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; // HH 才是 24 小时制:写成 hh 会让上午/下午的时间重复,缓存穿透失效 - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + JPG + "?v=" + DateTime.Now.ToString("yyyyMMddHHmmss"); + url = BuildApplicationUrl(service) + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + JPG + "?v=" + DateTime.Now.ToString("yyyyMMddHHmmss"); } else { var backup = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", ""); var filePathRel = Path.Combine(backup, createdBy, M3U8.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + M3U8; + url = BuildApplicationUrl(service) + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + M3U8; } } else @@ -149,7 +174,7 @@ namespace CloudBuilder.Security.Policy var backup = (ServiceSettingsPolicy.GetDmsFileBackupDirectory(service) ?? "").Replace(phy ?? "", ""); var filePathRel = Path.Combine(backup, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + orderKey; + url = BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } } else if (GetType(orderKey!) == LYRIC) @@ -157,7 +182,7 @@ namespace CloudBuilder.Security.Policy var upload = ServiceSettingsPolicy.GetDmsFileBackupDirectory(service).Replace(phy, ""); var filePathRel = Path.Combine(upload, createdBy, fileExtension.Replace(".", ""), ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - url = service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + orderKey; + url = BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } return url; } @@ -178,9 +203,9 @@ namespace CloudBuilder.Security.Policy var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; if (YesNoPolicy.IsYes(transcodeIndc)) { - return (service.Configuration[AppsettingsPolicy.ApplicationUrl] ?? "") + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + M3U8; + return BuildApplicationUrl(service) + webRootPath + filePath + Path.GetFileNameWithoutExtension(orderKey) + M3U8; } - return (service.Configuration[AppsettingsPolicy.ApplicationUrl] ?? "") + webRootPath + filePath + orderKey; + return BuildApplicationUrl(service) + webRootPath + filePath + orderKey; } public static string PackFilePath(IApplicationService service, string createdBy, string orderKey) @@ -208,7 +233,7 @@ namespace CloudBuilder.Security.Policy var filePathRel = Path.Combine(backup, folderName ?? "", fileExtension, ""); var filePath = filePathRel.Replace(Path.DirectorySeparatorChar, '/') + "/"; - return service.Configuration[AppsettingsPolicy.ApplicationUrl] + webRootPath + filePath + fileName; + return BuildApplicationUrl(service) + webRootPath + filePath + fileName; } } } diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/launchSettings.json b/CloudBuilder/CloudBuilder.Web.Entry/Properties/launchSettings.json index 57fef96..99c0e9b 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Properties/launchSettings.json +++ b/CloudBuilder/CloudBuilder.Web.Entry/Properties/launchSettings.json @@ -8,7 +8,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5235" + "applicationUrl": "http://172.16.253.53:5235" }, "IIS Express": { "commandName": "IISExpress", diff --git a/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json b/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json index bc50de4..6ace1e9 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json +++ b/CloudBuilder/CloudBuilder.Web.Entry/appsettings.json @@ -3,7 +3,7 @@ "LogFileDirectory": "D:\\Net8\\FileServer\\Log", "MaxFileSizeMB": 20 }, - "UrlOrigins": "http://192.168.195.68,http://localhost,http://localhost:8088,http://localhost:5173,http://localhost:5235", //这个参数是反向指向的地址,也是跨域访问的地址,包含Vue内WebSitePolicy设置WebSite + "UrlOrigins": "http://192.168.195.68,http://172.16.253.53:5235,http://localhost,http://localhost:8088,http://localhost:5173,http://localhost:5235", //这个参数是反向指向的地址,也是跨域访问的地址,包含Vue内WebSitePolicy设置WebSite "ApplicationUrl": "http://localhost:5235", //這個地址是最外层用户访问的,不是反向指向的地址https://uat.youjiala.com "CloudBuilder": { "Culture": "zh-cn", diff --git a/uniapp/youjiala/src/common/apiEndpoint.ts b/uniapp/youjiala/src/common/apiEndpoint.ts new file mode 100644 index 0000000..392d664 --- /dev/null +++ b/uniapp/youjiala/src/common/apiEndpoint.ts @@ -0,0 +1,105 @@ +import { http } from "uview-pro"; +import { WebSitePolicy } from "@/policy/custom/webSitePolicy"; + +/** + * 访问线路(公网 / 内网直连)——App 手动切换,运行时即时生效。 + * + * 背景:手机与服务器在同一局域网时,内网地址直连比公网域名快一个量级(飞牛 NAS 同款体验); + * 本项目只做「手动切换」:用户在「我的 → 访问线路」里选线路并填写内网地址,不做自动探测, + * 零探测开销、零误判。 + * + * 生效范围: + * - 接口请求:uview-pro 每次请求时读 http 实例的 config.baseUrl,切换后新请求立即走新线路; + * - 资源地址:缩略图/原图/视频/头像由后端按请求 Host 动态拼(FileTypePolicy.BuildApplicationUrl), + * 切到内网后后端返回的 URL 自动变成内网地址,前端无需改写; + * - 进行中的上传:URL 在发起时已固定,切换不影响在途任务,后续任务走新线路。 + */ + +const STORAGE_KEY = "api-endpoint-setting"; + +/** 线路设置:mode=wan 走公网(默认);lan 走内网直连(lanUrl 必须是合法 http(s) 地址) */ +export interface EndpointSetting { + mode: "wan" | "lan"; + lanUrl: string; +} + +// 公网地址沿用原 baseUrl 口径:开发构建区分 App 真机连不上宿主机 localhost 的问题,发布构建用正式域名 +let devBaseUrl = WebSitePolicy.WebApiRootDev; +// #ifdef APP-PLUS +devBaseUrl = WebSitePolicy.WebApiRootDevApp; +// #endif +/** 公网(默认)地址 */ +const WAN_BASE_URL = import.meta.env.DEV + ? devBaseUrl + : WebSitePolicy.WebApiRoot; + +/** 规范化内网地址:允许省略 http://,去掉末尾斜杠;非法返回空串(调用方据此判无效) */ +export function normalizeLanUrl(raw: string): string { + const text = String(raw ?? "") + .trim() + .replace(/\/+$/, ""); + if (!text) { + return ""; + } + const withScheme = /^https?:\/\//i.test(text) ? text : `http://${text}`; + return /^https?:\/\/[^\s/]+$/i.test(withScheme) ? withScheme : ""; +} + +function readSetting(): EndpointSetting { + try { + const raw = uni.getStorageSync(STORAGE_KEY) as + | Partial + | ""; + if (raw && typeof raw === "object") { + const lanUrl = normalizeLanUrl(String(raw.lanUrl ?? "")); + // 存储里有脏数据(lan 模式但地址非法)时按公网兜底,避免全站请求打到空地址 + return { mode: raw.mode === "lan" && lanUrl ? "lan" : "wan", lanUrl }; + } + } catch { + // 存储读取失败按默认公网处理,不阻塞启动 + } + return { mode: "wan", lanUrl: "" }; +} + +// 模块级缓存:请求层高频读地址,不每次都走同步 storage +let cached: EndpointSetting = readSetting(); + +/** 当前线路设置(只读快照) */ +export function currentEndpoint(): EndpointSetting { + return { ...cached }; +} + +/** 当前生效的后端地址:内网模式且地址合法时返回内网地址,否则公网 */ +export function apiBaseUrl(): string { + return cached.mode === "lan" ? cached.lanUrl : WAN_BASE_URL; +} + +/** 把线路写入请求层并落盘,返回生效后的地址 */ +export function saveEndpoint(setting: EndpointSetting): string { + const lanUrl = normalizeLanUrl(setting.lanUrl); + cached = { + mode: setting.mode === "lan" && lanUrl ? "lan" : "wan", + lanUrl, + }; + uni.setStorageSync(STORAGE_KEY, cached); + // uview-pro 的 http 实例每次请求读 config.baseUrl,改这里即对新请求生效 + http.setConfig({ baseUrl: apiBaseUrl() }); + return apiBaseUrl(); +} + +/** 测试内网地址连通性:用免鉴权的 RSA 公钥接口探活(与登录首调同源,最接近真实可用性) */ +export function testEndpoint(rawUrl: string): Promise { + const url = normalizeLanUrl(rawUrl); + if (!url) { + return Promise.resolve(false); + } + return new Promise((resolve) => { + uni.request({ + url: `${url}/Auth/GetRsaPublicKey`, + method: "GET", + timeout: 3000, + success: (res) => resolve(res.statusCode >= 200 && res.statusCode < 300), + fail: () => resolve(false), + }); + }); +} diff --git a/uniapp/youjiala/src/common/http.interceptor.ts b/uniapp/youjiala/src/common/http.interceptor.ts index 82a4ad8..3d1955b 100644 --- a/uniapp/youjiala/src/common/http.interceptor.ts +++ b/uniapp/youjiala/src/common/http.interceptor.ts @@ -6,6 +6,7 @@ import type { } from "uview-pro"; import type { JwtInfo } from "@/data/auto/jwtInfo"; import { http } from "uview-pro"; +import { apiBaseUrl } from "@/common/apiEndpoint"; import { markErrorNotified } from "@/common/global-error"; import { currentUiLocale, @@ -14,7 +15,6 @@ import { } 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, errorPageUrl, @@ -37,18 +37,10 @@ import { copyIntoAppUploadDir } from "@/utils/appUploadFile"; // ==================== 全局配置 ==================== -// 后端地址唯一来源:policy/custom/webSitePolicy(值可被项目根 .env 覆盖,缺失时回落该文件常量) -// 与旧系统 WebVue 同逻辑:开发构建用 WebApiRootDev,发布构建用 WebApiRoot -// App 端开发构建(真机/模拟器)连不上宿主机的 localhost,改用 WebApiRootDevApp -// 小程序端需在平台配置 https 合法域名;本地 H5 联调如遇 CORS 需后端放行开发端口 -// 换环境只改 .env(详见 src/md/environment.md),本文件不允许再写死地址 -let devBaseUrl = WebSitePolicy.WebApiRootDev; -// #ifdef APP-PLUS -devBaseUrl = WebSitePolicy.WebApiRootDevApp; -// #endif -export const baseUrl = import.meta.env.DEV - ? devBaseUrl - : WebSitePolicy.WebApiRoot; +// 后端地址唯一来源:common/apiEndpoint(公网/内网线路可运行时切换,见「我的 → 访问线路」)。 +// 原 baseUrl 常量已改为 apiBaseUrl():uview-pro 每次请求读 http 实例的 config.baseUrl, +// 切换线路后新请求立即生效;直接拼 URL 的调用方(登录页/上传队列)同步改用 apiBaseUrl()。 +// 换环境只改 .env(详见 src/md/environment.md),本文件不允许写死地址 // 登录页真实路径:getCurrentPages() 拿到的 route 永远是 pages.json 里的 path, // 不含 H5 运行时补的短地址别名,故路径比较只能用真实路径,跳转 URL 才走 pageUrl() const loginPage = toRealPath("login"); @@ -144,7 +136,7 @@ function doRefreshToken(): Promise { return new Promise((resolve, reject) => { uni.request({ - url: `${baseUrl}/Auth/RefreshToken`, + url: `${apiBaseUrl()}/Auth/RefreshToken`, method: "POST", data, timeout, @@ -293,7 +285,8 @@ export function extractBusinessError(data: any): string { // ==================== 全局配置(httpPlugin 注册用) ==================== const httpRequestConfig: RequestConfig = { - baseUrl, + // 启动时的当前线路;运行中切换由 apiEndpoint.saveEndpoint 调 http.setConfig 覆盖 + baseUrl: apiBaseUrl(), header: { "content-type": "application/json", }, @@ -531,7 +524,7 @@ function sendByUniUpload( * 协议物理名再交给 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 fullUrl = url.indexOf("http") === 0 ? url : apiBaseUrl() + url; const auth = bearerAuth(); // #ifdef H5 @@ -618,7 +611,7 @@ export async function requestBuffer(options: { data?: any; }): Promise { const { url, method = "POST", data } = options; - const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url; + const fullUrl = url.indexOf("http") === 0 ? url : apiBaseUrl() + url; const auth = bearerAuth(); const res = await new Promise( diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json index 7fba8c2..1b1328f 100644 --- a/uniapp/youjiala/src/locale/lang/en-US.json +++ b/uniapp/youjiala/src/locale/lang/en-US.json @@ -50,6 +50,21 @@ "logoutConfirmContent": "Are you sure you want to log out?", "logoutSuccess": "Logged out", "currentTheme": "Current Theme", + "accessLine": "Access Line", + "lineHint": "Choose LAN Direct when your device is on the same network as the server for faster transfers", + "lineWan": "WAN", + "lineWanDesc": "Access via the public domain, available anywhere", + "lineLan": "LAN Direct", + "lineLanDesc": "Connect directly over the same Wi-Fi for faster speed", + "lineLanPlaceholder": "LAN address, e.g. http://192.168.1.10:5235", + "lineTest": "Test", + "lineTesting": "Testing…", + "lineTestOk": "Connected", + "lineTestFail": "Connection failed. Check the address and network", + "lineInvalid": "Invalid address format", + "lineCancel": "Cancel", + "lineSave": "Save", + "lineSaved": "Line switched", "aboutTitle": "About", "appName": "App", "versionLabel": "Version", diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json index f0170d5..99c6297 100644 --- a/uniapp/youjiala/src/locale/lang/zh-CN.json +++ b/uniapp/youjiala/src/locale/lang/zh-CN.json @@ -50,6 +50,21 @@ "logoutConfirmContent": "确定要退出当前账号吗?", "logoutSuccess": "已退出登录", "currentTheme": "当前主题", + "accessLine": "访问线路", + "lineHint": "与服务器在同一局域网时选内网直连,上传更快速", + "lineWan": "公网", + "lineWanDesc": "通过公网域名访问,随时随地可用", + "lineLan": "内网直连", + "lineLanDesc": "同一 Wi-Fi 下直连服务器内网地址,速度更快", + "lineLanPlaceholder": "内网地址,如 http://192.168.1.10:5235", + "lineTest": "测试连接", + "lineTesting": "测试中…", + "lineTestOk": "连接成功", + "lineTestFail": "连接失败,请检查地址与网络", + "lineInvalid": "地址格式不正确", + "lineCancel": "取消", + "lineSave": "保存", + "lineSaved": "已切换线路", "aboutTitle": "关于", "appName": "应用名称", "versionLabel": "当前版本", diff --git a/uniapp/youjiala/src/pages/login/login.vue b/uniapp/youjiala/src/pages/login/login.vue index 10e1857..e7d1820 100644 --- a/uniapp/youjiala/src/pages/login/login.vue +++ b/uniapp/youjiala/src/pages/login/login.vue @@ -7,8 +7,9 @@ import type { LoginProfile } from "@/data/auto/loginProfile"; import { onLoad } from "@dcloudio/uni-app"; import { useLocale } from "uview-pro"; import { computed, ref } from "vue"; +import { apiBaseUrl } from "@/common/apiEndpoint"; import { brandGlyph, veinGlyph } from "@/common/brand"; -import { backendLocale, baseUrl } from "@/common/http.interceptor"; +import { backendLocale } from "@/common/http.interceptor"; import { DEFAULT_LOCALE, setCurrentUiLocale } from "@/common/locale-const"; import { encryptLoginPassword } from "@/common/pwd-crypto"; import { useThemeGradient, useThemeTokens } from "@/composables"; @@ -65,6 +66,8 @@ setCurrentUiLocale(currentLocale.value?.name ?? DEFAULT_LOCALE); /** 非发布构建才显示「当前环境 + 实际调用的后端地址」:发布版 import.meta.env.PROD 为 true,整行编译期即被剔除 */ const isProd = import.meta.env.PROD; const envMode = import.meta.env.MODE; +/** 当前后端地址:跟随「我的 → 访问线路」切换(内网直连时显示内网地址) */ +const currentApiUrl = apiBaseUrl(); function toggleLang() { const next = isZh.value ? "en-US" : "zh-CN"; setLocale(next); @@ -191,7 +194,7 @@ async function handleLogin() { diff --git a/uniapp/youjiala/src/pages/tab/mine.vue b/uniapp/youjiala/src/pages/tab/mine.vue index 46f4083..b9bdc96 100644 --- a/uniapp/youjiala/src/pages/tab/mine.vue +++ b/uniapp/youjiala/src/pages/tab/mine.vue @@ -6,7 +6,8 @@ * 版本号取 common/constant 的 APP_INFO(与 manifest/package 对齐);H5 无 plus.runtime.version。 */ import { useLocale } from "uview-pro"; -import { computed } from "vue"; +import { computed, ref } from "vue"; +import { currentEndpoint, normalizeLanUrl, saveEndpoint, testEndpoint } from "@/common/apiEndpoint"; import { APP_INFO } from "@/common/constant"; import { useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables"; import { usePermission } from "@/permission/permission-context"; @@ -46,6 +47,63 @@ function goTheme(): void { uni.navigateTo({ url: pageUrl("themePage") }); } +// ===== 访问线路(App 手动切换公网/内网直连;仅影响后续请求,不中断在途传输) ===== +const linePopupVisible = ref(false); +/** 当前生效线路(保存后刷新展示) */ +const lineSetting = ref(currentEndpoint()); +/** 弹层草稿:确认前不动真实线路,取消即作废 */ +const lineModeDraft = ref<"wan" | "lan">("wan"); +const lineLanDraft = ref(""); +const lineTesting = ref(false); +const lineTestResult = ref<"" | "ok" | "fail">(""); + +/** 当前线路展示文案:公网 / 内网直连(附地址) */ +const currentLineLabel = computed(() => { + const setting = lineSetting.value; + return setting.mode === "lan" ? `${t("MinePage.lineLan")} · ${setting.lanUrl}` : t("MinePage.lineWan"); +}); + +/** 打开线路弹层:草稿回填当前设置,清掉上次测活结果 */ +function openLinePopup(): void { + const setting = currentEndpoint(); + lineModeDraft.value = setting.mode; + lineLanDraft.value = setting.lanUrl; + lineTestResult.value = ""; + linePopupVisible.value = true; +} + +/** 测试内网地址连通性:用 RSA 公钥接口探活(与登录首调同源) */ +async function handleTestLine(): Promise { + if (lineTesting.value) { + return; + } + const url = normalizeLanUrl(lineLanDraft.value); + if (!url) { + uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" }); + return; + } + lineTesting.value = true; + lineTestResult.value = ""; + const ok = await testEndpoint(url); + lineTesting.value = false; + lineTestResult.value = ok ? "ok" : "fail"; + if (!ok) { + uni.showToast({ title: t("MinePage.lineTestFail"), icon: "none" }); + } +} + +/** 保存线路:内网模式地址必填且合法;生效只影响后续请求,各页切回时自然按新线路拉数据 */ +function handleSaveLine(): void { + if (lineModeDraft.value === "lan" && !normalizeLanUrl(lineLanDraft.value)) { + uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" }); + return; + } + saveEndpoint({ mode: lineModeDraft.value, lanUrl: lineLanDraft.value }); + lineSetting.value = currentEndpoint(); + linePopupVisible.value = false; + uni.showToast({ title: t("MinePage.lineSaved"), icon: "none" }); +} + /** 进入「安全管理」枢纽页:权限体系等基础设定的聚合入口 */ function goSecurity(): void { uni.navigateTo({ url: pageUrl("securityPage") }); @@ -126,6 +184,22 @@ async function handleLogout(): Promise { + + + + + + + + {{ t("MinePage.accessLine") }} + + + {{ currentLineLabel }} + + + + + @@ -240,6 +314,79 @@ async function handleLogout(): Promise { + + + + + + + {{ t("MinePage.accessLine") }} + + + {{ t("MinePage.lineHint") }} + + + + + + + {{ t("MinePage.lineWan") }} + + + {{ t("MinePage.lineWanDesc") }} + + + + + + + + + + {{ t("MinePage.lineLan") }} + + + {{ t("MinePage.lineLanDesc") }} + + + + + + + + + + + {{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }} + + + + + {{ t("MinePage.lineTestOk") }} + + + {{ t("MinePage.lineTestFail") }} + + + + + + {{ t("MinePage.lineCancel") }} + + + + + {{ t("MinePage.lineSave") }} + + + + + + @@ -356,4 +503,157 @@ async function handleLogout(): Promise { font-size: 26rpx; color: #1f2329; } + +/* ===== 访问线路弹层(底部抽屉,同 file-action-sheet 风格) ===== */ +.line-sheet { + padding: 32rpx 24rpx calc(24rpx + env(safe-area-inset-bottom)); + background-color: #f5f6f7; +} + +.line-sheet__title { + font-size: 32rpx; + font-weight: 600; + color: #1f2329; +} + +.line-sheet__hint { + display: block; + margin-top: 8rpx; + font-size: 24rpx; + color: #9aa0a6; +} + +/* 单选行:白卡;选中态用内阴影描边(不改变布局尺寸) */ +.line-option { + display: flex; + align-items: center; + margin-top: 20rpx; + padding: 24rpx; + border-radius: 20rpx; + background-color: #ffffff; +} + +.line-option--active { + box-shadow: 0 0 0 2rpx var(--fg-primary, #ff7d1f) inset; +} + +.line-option__body { + display: flex; + flex: 1; + flex-direction: column; + gap: 6rpx; +} + +.line-option__name { + font-size: 28rpx; + font-weight: 600; + color: #1f2329; +} + +.line-option__desc { + font-size: 24rpx; + color: #9aa0a6; +} + +.line-option__radio { + box-sizing: border-box; + width: 36rpx; + height: 36rpx; + margin-left: 16rpx; + border: 2rpx solid #d4d7dc; + border-radius: 50%; +} + +.line-option__radio--on { + border-color: var(--fg-primary, #ff7d1f); + background: radial-gradient(circle, var(--fg-primary, #ff7d1f) 0 42%, transparent 48%); +} + +/* 内网地址:输入框 + 测试连接一行 */ +.line-lan { + display: flex; + align-items: center; + gap: 16rpx; + margin-top: 20rpx; +} + +.line-lan__input { + flex: 1; + height: 76rpx; + padding: 0 20rpx; + border-radius: 16rpx; + font-size: 26rpx; + color: #1f2329; + background-color: #ffffff; +} + +.line-lan__ph { + color: #c0c4cc; +} + +.line-lan__test { + display: flex; + align-items: center; + justify-content: center; + height: 76rpx; + padding: 0 24rpx; + border-radius: 16rpx; + background-color: #ffffff; +} + +.line-lan__test-text { + font-size: 26rpx; + font-weight: 600; + color: var(--fg-primary, #ff7d1f); +} + +.line-lan__result { + display: block; + margin-top: 12rpx; + font-size: 24rpx; +} + +.line-lan__result--ok { + color: #52c41a; +} + +.line-lan__result--fail { + color: #fa3534; +} + +/* 底部双按钮:取消(描边)/ 保存(主题渐变) */ +.line-sheet__actions { + display: flex; + gap: 20rpx; + margin-top: 32rpx; +} + +.line-sheet__btn { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + height: 88rpx; + border-radius: 999rpx; +} + +.line-sheet__btn--ghost { + border: 2rpx solid #d4d7dc; + background-color: #ffffff; +} + +.line-sheet__btn--primary { + background: var(--fg-gradient, linear-gradient(135deg, #ff9a3d, #ff7d1f)); + box-shadow: var(--fg-shadow, 0 6rpx 16rpx rgba(255, 125, 31, 0.35)); +} + +.line-sheet__btn-text { + font-size: 28rpx; + font-weight: 600; + color: #ffffff; +} + +.line-sheet__btn-text--ghost { + color: #4e5969; +} diff --git a/uniapp/youjiala/src/stores/uploadQueue.ts b/uniapp/youjiala/src/stores/uploadQueue.ts index e7cc5d8..4b6a833 100644 --- a/uniapp/youjiala/src/stores/uploadQueue.ts +++ b/uniapp/youjiala/src/stores/uploadQueue.ts @@ -2,11 +2,8 @@ import type { PathNode } from "@/data/custom/fileBrowser"; import type { PickedFile } from "@/utils/chooseFiles"; import { defineStore } from "pinia"; import { computed, ref } from "vue"; -import { - baseUrl, - bearerAuth, - extractBusinessError, -} from "@/common/http.interceptor"; +import { apiBaseUrl } from "@/common/apiEndpoint"; +import { bearerAuth, extractBusinessError } from "@/common/http.interceptor"; import { copyIntoAppUploadDir } from "@/utils/appUploadFile"; /** @@ -87,7 +84,7 @@ function uniqueUploadTag(): string { /** 分片大小:内网 3MB / 公网 1MB(旧系统 FileUpload 同口径;开发构建视为内网) */ function resolveChunkSize(): number { - const publicNet = /localhost|127\.|192\.168\.|10\./.test(baseUrl); + const publicNet = /localhost|127\.|192\.168\.|10\./.test(apiBaseUrl()); const intranet = import.meta.env.DEV || publicNet; return (intranet ? 3 : 1) * 1024 * 1024; } @@ -233,7 +230,7 @@ export const useUploadQueueStore = defineStore( chunk, `${uid}-${index}-${total}${extOf(item.name)}`, ); - xhr.open("POST", `${baseUrl}/DmsFile/UploadDmsFile`); + xhr.open("POST", `${apiBaseUrl()}/DmsFile/UploadDmsFile`); xhr.setRequestHeader("Authorization", bearerAuth()); xhr.setRequestHeader("FilePath", item.targetPath); xhr.upload.onprogress = (e: ProgressEvent) => { @@ -293,7 +290,7 @@ export const useUploadQueueStore = defineStore( return; } task = uni.uploadFile({ - url: `${baseUrl}/DmsFile/UploadDmsFile`, + url: `${apiBaseUrl()}/DmsFile/UploadDmsFile`, filePath, // 后端约定:form 字段名 = 原始文件名 name: item.name,