更新软件升级
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
<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, uploadFile, getFileById } from "@/api/file";
|
||||
|
||||
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;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
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 res = await uploadFile(fd, {
|
||||
cate: cateId,
|
||||
onUploadProgress: (e) => {
|
||||
const now = Date.now();
|
||||
const loaded = e.loaded;
|
||||
const total = e.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)}` : "";
|
||||
}
|
||||
},
|
||||
});
|
||||
if (res?.code === 200 || res?.code === 201) {
|
||||
const d = res.data || {};
|
||||
form.fileId = d.id != null ? Number(d.id) : null;
|
||||
const src = d.url || "";
|
||||
form.downloadUrl = absoluteFromSrc(src);
|
||||
uploadedLabel.value = d.name || file.name || "安装包";
|
||||
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
|
||||
ElMessage.success(res.code === 201 ? "文件已存在,已关联" : "上传成功");
|
||||
options.onSuccess?.(res);
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "上传失败");
|
||||
options.onError?.(new Error(res?.msg));
|
||||
}
|
||||
} 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>
|
||||
Reference in New Issue
Block a user