可切換內網訪問

This commit is contained in:
owenchen committed 2026-10-21 10:51:07 +08:00
1 parent 1ff69f3cb3
commit fd67fa8f63
10 files changed
+493 -40

No files matched your search

@@ -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);
}
/// <summary>
/// 资源地址基座:优先按当前请求的 Scheme+Host 拼(App 切内网直连后,缩略图/原图/视频/头像
/// 返回的就是内网地址,资源自动跟随线路);无请求上下文(后台任务/启动阶段)回退 appsettings 的 ApplicationUrl。
/// 公网反代场景(nginx → Kestrel 是 http,对外是 https)不能直接用 request.Scheme:
/// 优先取 X-Forwarded-Proto,其次 Host 等于配置域名时沿用配置 scheme,其余(内网 IP/自定义域名)用请求 scheme。
/// </summary>
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;
}
}
}
@@ -8,7 +8,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5235"
"applicationUrl": "http://172.16.253.53:5235"
},
"IIS Express": {
"commandName": "IISExpress",
@@ -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",
+105
View File
@@ -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<EndpointSetting>
| "";
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<boolean> {
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),
});
});
}
+10 -17
View File
@@ -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<JwtInfo> {
return new Promise<JwtInfo>((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<any> {
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<ArrayBuffer> {
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<UniApp.RequestSuccessCallbackResult>(
@@ -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",
@@ -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": "当前版本",
+5 -2
View File
@@ -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() {
</text>
</view>
<view v-if="!isProd" class="login-env">
{{ envMode }} · {{ baseUrl }}
{{ envMode }} · {{ currentApiUrl }}
</view>
</view>
</view>
+301 -1
View File
@@ -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<void> {
if (lineTesting.value) {
return;
}
const url = normalizeLanUrl(lineLanDraft.value);
if (!url) {
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
return;
}
lineTesting.value = true;
lineTestResult.value = "";
const ok = await testEndpoint(url);
lineTesting.value = false;
lineTestResult.value = ok ? "ok" : "fail";
if (!ok) {
uni.showToast({ title: t("MinePage.lineTestFail"), icon: "none" });
}
}
/** 保存线路:内网模式地址必填且合法;生效只影响后续请求,各页切回时自然按新线路拉数据 */
function handleSaveLine(): void {
if (lineModeDraft.value === "lan" && !normalizeLanUrl(lineLanDraft.value)) {
uni.showToast({ title: t("MinePage.lineInvalid"), icon: "none" });
return;
}
saveEndpoint({ mode: lineModeDraft.value, lanUrl: lineLanDraft.value });
lineSetting.value = currentEndpoint();
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<void> {
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- 访问线路:App 手动切换公网/内网直连(资源地址由后端按请求 Host 自适应,仅 App 展示) -->
<!-- #ifdef APP-PLUS -->
<view class="mine-cell" @tap="openLinePopup">
<view class="mine-cell__icon">
<u-icon name="server-man" size="30" color="#ffffff" />
</view>
<text class="mine-cell__title">
{{ t("MinePage.accessLine") }}
</text>
<text class="mine-cell__value">
{{ currentLineLabel }}
</text>
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
</view>
<!-- #endif -->
<!-- 安全管理:进入权限体系基础设定聚合页(/security) -->
<view class="mine-cell" @tap="goSecurity">
<view class="mine-cell__icon">
@@ -240,6 +314,79 @@ async function handleLogout(): Promise<void> {
</view>
</view>
</view>
<!-- 访问线路弹层:公网/内网单选 + 内网地址手填 + 测连通(仅 App 有入口) -->
<!-- #ifdef APP-PLUS -->
<u-popup :model-value="linePopupVisible" mode="bottom" round="24" @close="linePopupVisible = false">
<view class="line-sheet">
<text class="line-sheet__title">
{{ t("MinePage.accessLine") }}
</text>
<text class="line-sheet__hint">
{{ t("MinePage.lineHint") }}
</text>
<!-- 公网:默认线路,随时随地可用 -->
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'wan' }"
@tap="lineModeDraft = 'wan'">
<view class="line-option__body">
<text class="line-option__name">
{{ t("MinePage.lineWan") }}
</text>
<text class="line-option__desc">
{{ t("MinePage.lineWanDesc") }}
</text>
</view>
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'wan' }" />
</view>
<!-- 内网直连:需与服务器同局域网,地址手填 -->
<view class="line-option" :class="{ 'line-option--active': lineModeDraft === 'lan' }"
@tap="lineModeDraft = 'lan'">
<view class="line-option__body">
<text class="line-option__name">
{{ t("MinePage.lineLan") }}
</text>
<text class="line-option__desc">
{{ t("MinePage.lineLanDesc") }}
</text>
</view>
<view class="line-option__radio" :class="{ 'line-option__radio--on': lineModeDraft === 'lan' }" />
</view>
<!-- 内网地址:选中内网才展开;测连通用免鉴权的 RSA 公钥接口 -->
<view v-if="lineModeDraft === 'lan'" class="line-lan">
<input v-model="lineLanDraft" class="line-lan__input" :placeholder="t('MinePage.lineLanPlaceholder')"
placeholder-class="line-lan__ph">
<view class="line-lan__test" @tap="handleTestLine">
<text class="line-lan__test-text">
{{ lineTesting ? t("MinePage.lineTesting") : t("MinePage.lineTest") }}
</text>
</view>
</view>
<text v-if="lineModeDraft === 'lan' && lineTestResult === 'ok'" class="line-lan__result line-lan__result--ok">
{{ t("MinePage.lineTestOk") }}
</text>
<text v-else-if="lineModeDraft === 'lan' && lineTestResult === 'fail'"
class="line-lan__result line-lan__result--fail">
{{ t("MinePage.lineTestFail") }}
</text>
<view class="line-sheet__actions">
<view class="line-sheet__btn line-sheet__btn--ghost" @tap="linePopupVisible = false">
<text class="line-sheet__btn-text line-sheet__btn-text--ghost">
{{ t("MinePage.lineCancel") }}
</text>
</view>
<view class="line-sheet__btn line-sheet__btn--primary" @tap="handleSaveLine">
<text class="line-sheet__btn-text">
{{ t("MinePage.lineSave") }}
</text>
</view>
</view>
</view>
</u-popup>
<!-- #endif -->
</app-page>
</template>
@@ -356,4 +503,157 @@ async function handleLogout(): Promise<void> {
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;
}
</style>
+5 -8
View File
@@ -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,