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() {
- {{ envMode }} · {{ baseUrl }}
+ {{ envMode }} · {{ currentApiUrl }}
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,