refactor: 统一静态资源地址拼接为 utils/asset-url,解除更新模块对应用模块的反向依赖
This commit is contained in:
@@ -117,16 +117,3 @@ const AppPackageAPI = {
|
||||
};
|
||||
|
||||
export default AppPackageAPI;
|
||||
|
||||
/**
|
||||
* 拼接静态资源完整访问地址(img / el-image 的 src 不经过 axios,需手动补 baseURL)
|
||||
* 说明:后端现在直接返回 OSS 绝对地址(https://xxx.aliyuncs.com/...),不再需要拼前缀,
|
||||
* 这里同时兼容历史相对路径(/20261019/xxx.png)。
|
||||
*/
|
||||
export function withBase(url?: string): string {
|
||||
if (!url) return "";
|
||||
// 已是绝对地址(http(s)://、协议相对 //、base64)时原样返回,避免拼出 /dev-apihttps://... 这类错误地址
|
||||
if (/^(https?:)?\/\//i.test(url) || url.startsWith("data:")) return url;
|
||||
const baseApi = import.meta.env.VITE_APP_BASE_API ?? "";
|
||||
return `${baseApi.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
<el-carousel-item v-for="(item, index) in screenshots" :key="item.id">
|
||||
<div class="screenshot-item">
|
||||
<el-image
|
||||
:src="resolveIconUrl(item.url)"
|
||||
:src="withBase(item.url)"
|
||||
fit="contain"
|
||||
class="screenshot-image"
|
||||
@click="openPreview(index)"
|
||||
@@ -148,7 +148,7 @@
|
||||
<!-- 全屏预览单张截图 -->
|
||||
<el-image-viewer
|
||||
v-if="previewVisible"
|
||||
:url-list="screenshots.map((s) => resolveIconUrl(s.url) as string)"
|
||||
:url-list="screenshots.map((s) => withBase(s.url) as string)"
|
||||
:initial-index="previewIndex"
|
||||
teleported
|
||||
@close="previewVisible = false"
|
||||
@@ -564,10 +564,10 @@
|
||||
<el-table-column label="图标" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="resolveIconUrl(row.iconUrl)"
|
||||
:src="withBase(row.iconUrl)"
|
||||
fit="cover"
|
||||
class="apk-icon"
|
||||
:preview-src-list="row.iconUrl ? [resolveIconUrl(row.iconUrl)] : []"
|
||||
:preview-src-list="row.iconUrl ? [withBase(row.iconUrl)] : []"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
@@ -822,6 +822,7 @@ import {
|
||||
Select,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
|
||||
// 接收传入的 props
|
||||
interface Props {
|
||||
@@ -851,7 +852,7 @@ watch(
|
||||
loadAll(newSerialno);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const activeNames = ref(["basic", "hardware", "network", "security", "other"]);
|
||||
@@ -957,22 +958,6 @@ const policyItems: { label: string; field: string }[] = [
|
||||
{ label: "深色模式", field: "darkMode" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 拼接应用图标完整访问地址
|
||||
*
|
||||
* 后端返回的是相对路径(如 /static/app_icon/{md5}.png)。
|
||||
* img/el-image 的 src 是浏览器原生请求,不经过 axios,拿不到 baseURL,
|
||||
* 因此必须手动补上代理前缀,否则请求会打到前端 dev server 上导致 404。
|
||||
*/
|
||||
function resolveIconUrl(url?: string | null): string {
|
||||
if (!url) return "";
|
||||
// 已是绝对地址(http/https)或 base64,直接返回
|
||||
if (/^(https?:)?\/\//.test(url) || url.startsWith("data:")) return url;
|
||||
const baseApi = import.meta.env.VITE_APP_BASE_API ?? "";
|
||||
// 避免 baseApi 结尾与 url 开头的斜杠重复
|
||||
return `${baseApi.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
/** 空值统一显示占位符 */
|
||||
function formatText(value?: string | number | null): string {
|
||||
return value === undefined || value === null || value === "" ? "-" : String(value);
|
||||
@@ -1016,7 +1001,7 @@ function dateValue(value?: Date | string | null): number {
|
||||
|
||||
function assignTo<T extends object>(reactiveObj: T, data?: object) {
|
||||
Object.keys(reactiveObj as object).forEach(
|
||||
(key) => delete (reactiveObj as Record<string, unknown>)[key]
|
||||
(key) => delete (reactiveObj as Record<string, unknown>)[key],
|
||||
);
|
||||
if (data) {
|
||||
Object.assign(reactiveObj, data);
|
||||
@@ -1440,7 +1425,7 @@ const handleClearAppData = (row: ApkInstallInfo) => {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
DeviceAPI.clearAppData(props.serialno, row.packageName ?? "").then(() => {
|
||||
@@ -1459,7 +1444,7 @@ const handleUninstallApp = (row: ApkInstallInfo) => {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "error",
|
||||
}
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
DeviceAPI.uninstallApp(props.serialno, row.packageName ?? "").then(() => {
|
||||
@@ -1513,7 +1498,7 @@ onMounted(() => {
|
||||
const sse = useSse();
|
||||
unsubscribeDeviceOnlineStatus = sse.on<DeviceOnlineVO>(
|
||||
"device-online-status",
|
||||
handleDeviceOnlineStatus
|
||||
handleDeviceOnlineStatus,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
13
src/utils/asset-url.ts
Normal file
13
src/utils/asset-url.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 拼接静态资源完整访问地址
|
||||
*
|
||||
* img / el-image 的 src 不经过 axios,拿不到请求实例的 baseURL,
|
||||
* 这里按当前环境补上代理前缀,兼容后端返回的相对路径。
|
||||
*/
|
||||
export function withBase(url?: string | null): string {
|
||||
if (!url) return "";
|
||||
// 绝对地址(http(s)://、协议相对 //、base64)原样返回
|
||||
if (/^(https?:)?\/\//i.test(url) || url.startsWith("data:")) return url;
|
||||
const baseApi = import.meta.env.VITE_APP_BASE_API ?? "";
|
||||
return `${baseApi.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
@@ -159,7 +159,8 @@ import { onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, type FormInstance } from "element-plus";
|
||||
import { Box, Delete, Document, Picture, UploadFilled } from "@element-plus/icons-vue";
|
||||
import { parseApkMeta } from "apk-meta-parser";
|
||||
import AppPackageAPI, { withBase, type AppPackage } from "@/api/app/package";
|
||||
import AppPackageAPI, { type AppPackage } from "@/api/app/package";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
import AppCategoryAPI, { type AppCategory } from "@/api/app/category";
|
||||
|
||||
const visible = defineModel<boolean>("visible", { default: false });
|
||||
|
||||
@@ -239,7 +239,8 @@ import {
|
||||
View,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import AppPackageAPI, { withBase, type AppPackage, type PackageType } from "@/api/app/package";
|
||||
import AppPackageAPI, { type AppPackage, type PackageType } from "@/api/app/package";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
import AppCategoryAPI, { type AppCategory } from "@/api/app/category";
|
||||
import UploadDialog from "./components/UploadDialog.vue";
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<el-table-column label="图标" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:src="resolveIconUrl(scope.row.iconUrl)"
|
||||
:src="withBase(scope.row.iconUrl)"
|
||||
fit="contain"
|
||||
style="width: 48px; height: 48px; border-radius: 6px"
|
||||
:preview-src-list="previewList"
|
||||
@@ -236,6 +236,7 @@ import {
|
||||
import { usePageTable } from "@/composables";
|
||||
import AppIconAPI from "@/api/device/appIcon";
|
||||
import type { AppIconQuery, AppIconVO } from "@/api/device/types";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
|
||||
defineOptions({
|
||||
name: "AppIcon",
|
||||
@@ -275,25 +276,12 @@ function handleCurrentChange(row: AppIconVO | null) {
|
||||
selectedRow.value = row;
|
||||
}
|
||||
|
||||
const previewList = computed(() => list.value.map((item) => resolveIconUrl(item.iconUrl)));
|
||||
const previewList = computed(() => list.value.map((item) => withBase(item.iconUrl)));
|
||||
const previewIndex = (row: AppIconVO) => {
|
||||
const idx = list.value.findIndex((item) => item.id === row.id);
|
||||
return idx < 0 ? 0 : idx;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼接图标完整访问地址
|
||||
* 后端返回的是相对路径(如 /static/app_icon/{md5}.png)。
|
||||
* el-image 的 src 是浏览器原生请求,不经过 axios,拿不到 baseURL,
|
||||
* 因此必须手动补上代理前缀(VITE_APP_BASE_API),否则请求会打到前端 dev server 上导致 404。
|
||||
*/
|
||||
function resolveIconUrl(url?: string | null): string {
|
||||
if (!url) return "";
|
||||
if (/^(https?:)?\/\//.test(url) || url.startsWith("data:")) return url;
|
||||
const baseApi = import.meta.env.VITE_APP_BASE_API ?? "";
|
||||
return `${baseApi.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
function validateUploadFile(file: UploadRawFile) {
|
||||
const allowExt = ["jpg", "jpeg", "png", "webp"];
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
|
||||
@@ -386,7 +386,7 @@ import {
|
||||
View,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { withBase } from "@/api/app/package";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
import UpdateChannelAPI, { type UpdateChannel } from "@/api/update/channel";
|
||||
import AppUpdateReleaseAPI, {
|
||||
type AppUpdateRelease,
|
||||
@@ -459,7 +459,7 @@ watch(
|
||||
() => form.listed,
|
||||
(listed) => {
|
||||
if (!listed) form.forceUpdate = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const detailVisible = ref(false);
|
||||
@@ -697,7 +697,7 @@ async function handlePublish(row: AppUpdateRelease) {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将「${row.appLabel || row.packageName} ${row.versionName || row.versionCode}」发布给全部设备?同渠道同应用的其余已发布版本会被自动停止。`,
|
||||
"发布确认",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
try {
|
||||
await AppUpdateReleaseAPI.publish(row.id);
|
||||
@@ -713,7 +713,7 @@ async function handleStop(row: AppUpdateRelease) {
|
||||
await ElMessageBox.confirm(
|
||||
`停止后设备将不再检查到「${row.appLabel || row.packageName} ${row.versionName || row.versionCode}」,确认停止?`,
|
||||
"停止确认",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
try {
|
||||
await AppUpdateReleaseAPI.stop(row.id);
|
||||
|
||||
@@ -439,7 +439,7 @@ import {
|
||||
View,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { withBase } from "@/api/app/package";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
import UpdateChannelAPI, { type UpdateChannel } from "@/api/update/channel";
|
||||
import type { UpdateReleaseStatus } from "@/api/update/global";
|
||||
import AppUpdateGrayAPI, {
|
||||
@@ -531,7 +531,7 @@ watch(
|
||||
() => form.listed,
|
||||
(listed) => {
|
||||
if (!listed) form.forceUpdate = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
@@ -543,7 +543,7 @@ watch(
|
||||
form.channelCode = "";
|
||||
}
|
||||
formRef.value?.clearValidate(["channelCode", "snList"]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const detailVisible = ref(false);
|
||||
@@ -784,8 +784,8 @@ async function readSnFile(file: File): Promise<string[]> {
|
||||
}
|
||||
return Array.from(
|
||||
new Set(
|
||||
cells.map((cell) => cell.trim()).filter((cell) => cell && !SN_HEADER_PATTERN.test(cell))
|
||||
)
|
||||
cells.map((cell) => cell.trim()).filter((cell) => cell && !SN_HEADER_PATTERN.test(cell)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -844,7 +844,7 @@ async function handlePublish(row: AppUpdateGrayRelease) {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将「${row.appLabel || row.packageName} ${row.versionName || row.versionCode}」灰度发布给${targetText(row)}?目标重叠的已发布灰度会被自动停止。`,
|
||||
"发布确认",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
try {
|
||||
await AppUpdateGrayAPI.publish(row.id);
|
||||
@@ -860,7 +860,7 @@ async function handleStop(row: AppUpdateGrayRelease) {
|
||||
await ElMessageBox.confirm(
|
||||
`停止后${targetText(row)}将不再检查到该灰度版本,确认停止?`,
|
||||
"停止确认",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
try {
|
||||
await AppUpdateGrayAPI.stop(row.id);
|
||||
|
||||
@@ -330,7 +330,7 @@ import {
|
||||
WarningFilled,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox, ElNotification } from "element-plus";
|
||||
import { withBase } from "@/api/app/package";
|
||||
import { withBase } from "@/utils/asset-url";
|
||||
import UpdateChannelAPI, { type UpdateChannel } from "@/api/update/channel";
|
||||
import AppUpdateReleaseAPI, { type AppUpdateRelease } from "@/api/update/global";
|
||||
import AppUpdatePatchAPI, { type AppUpdatePatch, type PatchStatus } from "@/api/update/save-flow";
|
||||
@@ -405,7 +405,7 @@ function shortHash(value: string | null) {
|
||||
/** 轮询发现生成中的记录出结果时提示,一次提交多个任务时合并成一条 */
|
||||
function notifyOutcomes(before: Map<string, PatchStatus>, rows: AppUpdatePatch[]) {
|
||||
const finished = rows.filter(
|
||||
(row) => before.get(row.id) === "GENERATING" && row.status !== "GENERATING"
|
||||
(row) => before.get(row.id) === "GENERATING" && row.status !== "GENERATING",
|
||||
);
|
||||
if (finished.length === 0) {
|
||||
return;
|
||||
@@ -448,14 +448,14 @@ function notifyOutcomes(before: Map<string, PatchStatus>, rows: AppUpdatePatch[]
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(row) =>
|
||||
`${row.baseVersionName} → ${row.targetVersionName}:${row.errorMessage || row.advice}`
|
||||
`${row.baseVersionName} → ${row.targetVersionName}:${row.errorMessage || row.advice}`,
|
||||
)
|
||||
.join(";")
|
||||
: finished
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(row) =>
|
||||
`${row.baseVersionName} → ${row.targetVersionName}:${ratioText(row.patchRatio)}`
|
||||
`${row.baseVersionName} → ${row.targetVersionName}:${ratioText(row.patchRatio)}`,
|
||||
)
|
||||
.join(";"),
|
||||
duration: 8000,
|
||||
@@ -573,7 +573,7 @@ async function handleRegenerate(row: AppUpdatePatch) {
|
||||
await ElMessageBox.confirm(
|
||||
`将重新生成 ${row.baseVersionName} → ${row.targetVersionName} 的差分补丁,原有补丁会被覆盖。`,
|
||||
"重新生成",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
@@ -593,7 +593,7 @@ async function handleDelete(row: AppUpdatePatch) {
|
||||
await ElMessageBox.confirm(
|
||||
`删除 ${row.baseVersionName} → ${row.targetVersionName} 的差分补丁?`,
|
||||
"删除确认",
|
||||
{ type: "warning" }
|
||||
{ type: "warning" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user