Files
platform-vue/src/views/platform/softwareupgrade/components/edit.vue
T
2026-04-09 18:07:55 +08:00

463 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-drawer
v-model="visible"
:title="isAdd ? '新增软件产品' : '编辑软件产品'"
size="560px"
destroy-on-close
@closed="onClosed"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" v-loading="loading">
<el-form-item label="软件名称" prop="name">
<el-input v-model="form.name" placeholder="如:桌面客户端" />
</el-form-item>
<el-form-item label="产品 code" prop="code">
<el-input
v-model="form.code"
placeholder="客户端检查更新时传的 code,唯一"
:disabled="!isAdd"
/>
</el-form-item>
<el-form-item label="最新版本号" prop="latestVersion">
<el-input v-model="form.latestVersion" placeholder="如 1.2.0" />
</el-form-item>
<el-form-item label="安装包">
<el-upload
ref="uploadRef"
class="pkg-upload"
:limit="1"
:file-list="displayFileList"
:http-request="handlePackageUpload"
:on-remove="onRemovePackage"
@exceed="onExceedReplace"
accept=".zip,.exe,.dmg,.msi,.msix,.apk,.deb,.rpm,.7z,.pkg,.tar,.gz"
>
<el-button type="primary" :loading="uploading">
{{ form.fileId ? "重新上传安装包" : "上传安装包" }}
</el-button>
</el-upload>
<div v-if="uploadXHRActive" class="upload-progress-wrap">
<el-progress
:percentage="uploadPercent"
:indeterminate="uploadIndeterminate"
:stroke-width="10"
striped
striped-flow
/>
<div class="upload-progress-text">
<template v-if="!uploadIndeterminate">
<span>{{ uploadPercent }}%</span>
<span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template>
<template v-else>
<span>上传中</span>
<template v-if="uploadSpeedText && uploadSpeedText !== '—'">
<span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span>
</template>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template>
</div>
</div>
<div v-if="form.fileId" class="pkg-meta">
<span>文件 ID<code>{{ form.fileId }}</code></span>
</div>
</el-form-item>
<el-form-item label="下载/更新地址">
<el-input
v-model="form.downloadUrl"
type="textarea"
:rows="2"
placeholder="上传后自动填写;可改为 CDN 等完整 URL"
/>
</el-form-item>
<el-form-item label="强制更新">
<el-switch v-model="form.forceUpdate" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="更新说明">
<el-input v-model="form.releaseNotes" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio-button :value="1">启用</el-radio-button>
<el-radio-button :value="0">停用</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item v-if="!isAdd && resolvedUrl" label="当前解析地址">
<el-link :href="resolvedUrl" type="primary" target="_blank">{{ resolvedUrl }}</el-link>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, reactive, nextTick } from "vue";
import { ElMessage } from "element-plus";
import {
getSoftwareUpgradeDetail,
createSoftwareUpgrade,
updateSoftwareUpgrade,
} from "@/api/softwareUpgrade";
import { getUserCate, createFileCate, getFileById } from "@/api/file";
import { smartUpload } from "@/utils/qiniuUpload";
const emit = defineEmits(["saved"]);
const visible = ref(false);
const loading = ref(false);
const saving = ref(false);
const uploading = ref(false);
const uploadXHRActive = ref(false);
const uploadPercent = ref(0);
const uploadIndeterminate = ref(false);
const uploadSpeedText = ref("—");
const uploadSizeText = ref("");
const isAdd = ref(false);
const resolvedUrl = ref("");
const formRef = ref(null);
const uploadRef = ref(null);
const displayFileList = ref([]);
const uploadedLabel = ref("");
const appsUpgradeCateId = ref(null);
const form = reactive({
id: 0,
name: "",
code: "",
latestVersion: "0.0.1",
fileId: null,
downloadUrl: "",
forceUpdate: 0,
releaseNotes: "",
status: 1,
sort: 0,
});
const rules = {
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
code: [{ required: true, message: "请输入产品标识", trigger: "blur" }],
latestVersion: [{ required: true, message: "请输入版本号", trigger: "blur" }],
};
function formatBytes(n) {
if (n == null || !Number.isFinite(n) || n < 0) return "";
if (n < 1024) return `${Math.round(n)} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10240 ? 2 : 1)} KB`;
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`;
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
/** @param {number} bps bytes per second */
function formatSpeed(bps) {
if (bps == null || !Number.isFinite(bps) || bps <= 0) return "—";
return `${formatBytes(bps)}/s`;
}
function absoluteFromSrc(src) {
if (!src) return "";
const s = String(src).trim();
if (/^https?:\/\//i.test(s)) return s;
const base = (import.meta.env.VITE_API_BASE_URL || "").replace(/\/$/, "");
const path = s.startsWith("/") ? s : `/${s}`;
return base ? `${base}${path}` : path;
}
async function ensureAppsUpgradeCate() {
if (appsUpgradeCateId.value != null) {
return appsUpgradeCateId.value;
}
const res = await getUserCate();
if (res?.code !== 200 || !Array.isArray(res.data)) {
return null;
}
const found = res.data.find(
(c) => String(c.name || "").trim().toLowerCase() === "appsupgrade"
);
if (found && found.id) {
appsUpgradeCateId.value = Number(found.id);
return appsUpgradeCateId.value;
}
const cr = await createFileCate({ name: "appsupgrade" });
if (cr?.code === 200 && cr.data?.id != null) {
appsUpgradeCateId.value = Number(cr.data.id);
return appsUpgradeCateId.value;
}
ElMessage.error(cr?.msg || "创建文件分组 appsupgrade 失败");
return null;
}
function reset() {
form.id = 0;
form.name = "";
form.code = "";
form.latestVersion = "0.0.1";
form.fileId = null;
form.downloadUrl = "";
form.forceUpdate = 0;
form.releaseNotes = "";
form.status = 1;
form.sort = 0;
resolvedUrl.value = "";
displayFileList.value = [];
uploadedLabel.value = "";
uploadXHRActive.value = false;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
uploadRef.value?.clearFiles?.();
}
function onClosed() {
reset();
}
function onRemovePackage() {
form.fileId = null;
form.downloadUrl = "";
uploadedLabel.value = "";
displayFileList.value = [];
}
/** Element Plus:超出 limit 时传入待加入的文件列表(一般为 File[]) */
function onExceedReplace(files) {
const first = files[0];
const raw = first?.raw != null ? first.raw : first;
uploadRef.value?.clearFiles?.();
form.fileId = null;
form.downloadUrl = "";
uploadedLabel.value = "";
displayFileList.value = [];
if (raw) {
handlePackageUpload({ file: raw, onSuccess: () => {}, onError: () => {} });
}
}
async function handlePackageUpload(options) {
const file = options.file?.raw != null ? options.file.raw : options.file;
if (!file) {
options.onError?.(new Error("无文件"));
return;
}
uploading.value = true;
try {
const cateId = await ensureAppsUpgradeCate();
if (!cateId) {
options.onError?.(new Error("no category"));
return;
}
uploadXHRActive.value = true;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
const xhrStart = Date.now();
let lastLoaded = 0;
let lastTick = xhrStart;
let emaBps = 0;
// 使用智能上传(自动选择本地或七牛云)
const result = await smartUpload(file, {
cate: cateId,
onProgress: (progress) => {
const now = Date.now();
const loaded = progress.loaded;
const total = progress.total || 0;
const elapsedSec = (now - xhrStart) / 1000;
const dt = (now - lastTick) / 1000;
if (dt >= 0.07 && loaded >= lastLoaded) {
const inst = (loaded - lastLoaded) / dt;
emaBps = emaBps > 0 ? emaBps * 0.72 + inst * 0.28 : inst;
lastLoaded = loaded;
lastTick = now;
}
const avgBps = elapsedSec > 0.12 ? loaded / elapsedSec : 0;
const showBps = emaBps > 0 ? emaBps : avgBps;
uploadSpeedText.value = showBps > 0 ? formatSpeed(showBps) : elapsedSec > 0.05 ? formatSpeed(avgBps) : "—";
if (total > 0) {
uploadIndeterminate.value = false;
uploadPercent.value = Math.min(100, Math.round((loaded * 100) / total));
uploadSizeText.value = `${formatBytes(loaded)} / ${formatBytes(total)}`;
} else {
uploadIndeterminate.value = true;
uploadSizeText.value = loaded > 0 ? `已上传 ${formatBytes(loaded)}` : "";
}
},
});
// 上传成功
form.fileId = result.id;
form.downloadUrl = result.url;
uploadedLabel.value = result.name || file.name || "安装包";
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
ElMessage.success("上传成功");
options.onSuccess?.(result);
} catch (e) {
ElMessage.error(e?.message || "上传失败");
options.onError?.(e);
} finally {
uploading.value = false;
uploadXHRActive.value = false;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
uploadSpeedText.value = "—";
uploadSizeText.value = "";
}
}
async function open(id) {
reset();
isAdd.value = !id;
visible.value = true;
appsUpgradeCateId.value = null;
await nextTick();
formRef.value?.clearValidate?.();
if (!id) return;
loading.value = true;
try {
const res = await getSoftwareUpgradeDetail(id);
if (res?.code !== 200 || !res.data) {
ElMessage.error(res?.msg || "加载失败");
visible.value = false;
return;
}
const d = res.data;
form.id = d.id;
form.name = d.name || "";
form.code = d.code || "";
form.latestVersion = d.latestVersion || "0.0.1";
form.fileId = d.fileId != null ? Number(d.fileId) : null;
form.downloadUrl = d.downloadUrl || d.resolvedDownloadUrl || "";
form.forceUpdate = d.forceUpdate ? 1 : 0;
form.releaseNotes = d.releaseNotes || "";
form.status = d.status !== 0 ? 1 : 0;
form.sort = d.sort ?? 0;
resolvedUrl.value = d.resolvedDownloadUrl || "";
if (form.fileId) {
try {
const fr = await getFileById(form.fileId);
if (fr?.code === 200 && fr.data) {
uploadedLabel.value = fr.data.name || `文件 #${form.fileId}`;
} else {
uploadedLabel.value = `文件 #${form.fileId}`;
}
} catch {
uploadedLabel.value = `文件 #${form.fileId}`;
}
displayFileList.value = [
{ name: uploadedLabel.value, uid: `exist-${form.fileId}` },
];
}
} finally {
loading.value = false;
}
}
async function submit() {
try {
await formRef.value?.validate();
} catch {
return;
}
const payload = {
name: form.name.trim(),
code: form.code.trim(),
latestVersion: form.latestVersion.trim(),
downloadUrl: form.downloadUrl.trim() || undefined,
forceUpdate: form.forceUpdate,
releaseNotes: form.releaseNotes.trim() || undefined,
status: form.status,
sort: form.sort,
};
if (form.fileId != null && form.fileId !== "" && Number(form.fileId) > 0) {
payload.fileId = Number(form.fileId);
} else {
payload.fileId = 0;
}
saving.value = true;
try {
let res;
if (isAdd.value) {
res = await createSoftwareUpgrade(payload);
} else {
res = await updateSoftwareUpgrade(form.id, payload);
}
if (res?.code === 200) {
ElMessage.success("已保存");
visible.value = false;
emit("saved");
} else {
ElMessage.error(res?.msg || "失败");
}
} finally {
saving.value = false;
}
}
defineExpose({ open });
</script>
<style scoped>
.hint {
margin: 0 0 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.55;
}
.hint code {
padding: 0 4px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.pkg-upload {
width: 100%;
}
.upload-progress-wrap {
margin-top: 10px;
width: 100%;
}
.upload-progress-text {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
word-break: break-all;
}
.upload-progress-text .upload-sep {
margin: 0 0.35em;
opacity: 0.55;
}
.pkg-meta {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.pkg-meta code {
font-size: 12px;
}
</style>