353 lines
15 KiB
Vue
353 lines
15 KiB
Vue
<template>
|
||
<div class="desktop-workstation">
|
||
<el-form
|
||
ref="formRef"
|
||
class="ls-form"
|
||
:model="formData"
|
||
:rules="rules"
|
||
label-width="140px"
|
||
scroll-to-error
|
||
>
|
||
<el-card shadow="never" class="!border-none">
|
||
<el-alert type="info" :closable="false" show-icon class="mb-5">
|
||
<template #title>医生工作站升级包</template>
|
||
<div class="leading-6">
|
||
桌面端启动后会检测本页发布的版本。请上传或填写与
|
||
<code>一键打包</code>
|
||
产物一致的安装包,并填入打包目录中的 SHA-256。Windows 推荐使用
|
||
Setup.exe,用户点击“立即更新”后会自动安装并重启;macOS 继续使用 ZIP。
|
||
安装包通常超过 200MB,本页上传按钮会直传到已配置的腾讯云 COS;也可以
|
||
自行上传到其他对象存储 / CDN 后粘贴地址。
|
||
</div>
|
||
</el-alert>
|
||
<div class="text-xl font-medium mb-[20px]">升级策略</div>
|
||
<el-form-item label="自动检测" prop="enabled">
|
||
<div>
|
||
<el-switch
|
||
v-model="formData.enabled"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
/>
|
||
<span class="ml-2">{{ formData.enabled ? '开启' : '关闭' }}</span>
|
||
<div class="form-tips">关闭后,桌面端不会提示或强制升级。</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最新版本号" prop="latest_version">
|
||
<div class="w-80">
|
||
<el-input
|
||
v-model.trim="formData.latest_version"
|
||
placeholder="须与 pyproject.toml / 客户端版本一致,例如 0.2.0"
|
||
maxlength="20"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="强制升级" prop="force_update">
|
||
<div>
|
||
<el-switch
|
||
v-model="formData.force_update"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
/>
|
||
<span class="ml-2">{{ formData.force_update ? '开启' : '关闭' }}</span>
|
||
<div class="form-tips">
|
||
开启后,低于最新版本且已配置对应平台安装包的客户端必须升级后才能继续使用。
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="最低可用版本" prop="min_version">
|
||
<div class="w-80">
|
||
<el-input
|
||
v-model.trim="formData.min_version"
|
||
placeholder="可选,例如 0.1.0;低于该版本也会强制升级"
|
||
maxlength="20"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="更新标题" prop="title">
|
||
<div class="w-[480px]">
|
||
<el-input
|
||
v-model.trim="formData.title"
|
||
placeholder="例如:医生工作站 0.2.0"
|
||
maxlength="80"
|
||
show-word-limit
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="更新说明" prop="notes">
|
||
<div class="w-[640px]">
|
||
<el-input
|
||
v-model="formData.notes"
|
||
type="textarea"
|
||
:rows="6"
|
||
maxlength="4000"
|
||
show-word-limit
|
||
placeholder="医生端弹窗中展示的更新说明"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
</el-card>
|
||
|
||
<el-card
|
||
v-for="item in platforms"
|
||
:key="item.key"
|
||
shadow="never"
|
||
class="!border-none mt-4"
|
||
>
|
||
<div class="text-xl font-medium mb-[20px]">{{ item.label }}</div>
|
||
<el-form-item label="安装包类型">
|
||
<el-select v-model="formData.packages[item.key].type" class="w-80">
|
||
<el-option label="便携压缩包(ZIP)" value="archive" />
|
||
<el-option
|
||
v-if="item.key === 'windows_x64'"
|
||
label="Windows 安装程序(Inno Setup EXE)"
|
||
value="inno_setup"
|
||
/>
|
||
</el-select>
|
||
<div class="form-tips ml-3">
|
||
{{
|
||
formData.packages[item.key].type === 'inno_setup'
|
||
? '推荐:退出客户端后静默安装,必要时显示 Windows 权限确认。'
|
||
: '兼容旧版客户端的 ZIP 覆盖更新。'
|
||
}}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="安装包地址">
|
||
<div class="w-[640px]">
|
||
<el-input
|
||
v-model.trim="formData.packages[item.key].url"
|
||
:placeholder="
|
||
formData.packages[item.key].type === 'inno_setup'
|
||
? 'https://.../DoctorWorkstation-Setup-....exe'
|
||
: 'https://...zip 或站内 uploads/file/... 路径'
|
||
"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="上传安装包">
|
||
<div>
|
||
<upload
|
||
v-perms="['setting.desktop_workstation/setConfig']"
|
||
type="desktop_package"
|
||
direct
|
||
:limit="1"
|
||
:multiple="false"
|
||
:show-progress="true"
|
||
:data="{ cid: 0 }"
|
||
@success="(response: any) => handleUploadSuccess(item.key, response)"
|
||
@change="(file: any) => handleUploadFile(item.key, file)"
|
||
>
|
||
<el-button type="primary" plain>选择安装包并上传</el-button>
|
||
</upload>
|
||
<div class="form-tips">
|
||
安装包将分片直传腾讯云 COS,不经过业务服务器(支持 EXE / ZIP,最大
|
||
2GB)。Windows 自动安装程序必须使用 HTTPS 地址并开启证书校验。
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="SHA-256">
|
||
<div class="w-[640px]">
|
||
<el-input
|
||
v-model.trim="formData.packages[item.key].sha256"
|
||
placeholder="64 位十六进制,来自 SHA256SUMS.txt 或打包生成的 .sha256"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="文件名">
|
||
<div class="w-[480px]">
|
||
<el-input
|
||
v-model.trim="formData.packages[item.key].filename"
|
||
:placeholder="
|
||
formData.packages[item.key].type === 'inno_setup'
|
||
? 'DoctorWorkstation-Setup-Windows-x64-0.2.0.exe'
|
||
: 'DoctorWorkstation-Windows-x64-0.2.0.zip'
|
||
"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="文件大小">
|
||
<div class="flex items-center gap-3">
|
||
<el-input
|
||
v-model.number="formData.packages[item.key].size"
|
||
class="w-48"
|
||
placeholder="字节"
|
||
/>
|
||
<span class="text-tx-secondary text-sm">{{
|
||
formatSize(formData.packages[item.key].size)
|
||
}}</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-card>
|
||
</el-form>
|
||
<footer-btns v-perms="['setting.desktop_workstation/setConfig']">
|
||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||
</footer-btns>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup name="desktopWorkstation">
|
||
import type { FormInstance } from 'element-plus'
|
||
|
||
import {
|
||
getDesktopWorkstationConfig,
|
||
setDesktopWorkstationConfig,
|
||
type DesktopPackage,
|
||
type DesktopPackageType
|
||
} from '@/api/setting/desktop_workstation'
|
||
import Upload from '@/components/upload/index.vue'
|
||
import feedback from '@/utils/feedback'
|
||
|
||
type PlatformKey = 'windows_x64' | 'macos_arm64' | 'macos_x64'
|
||
|
||
const emptyPackage = (type: DesktopPackageType = 'archive'): DesktopPackage => ({
|
||
url: '',
|
||
sha256: '',
|
||
size: 0,
|
||
filename: '',
|
||
type
|
||
})
|
||
|
||
const formRef = shallowRef<FormInstance>()
|
||
const formData = reactive({
|
||
enabled: 1,
|
||
latest_version: '',
|
||
min_version: '',
|
||
force_update: 0,
|
||
title: '',
|
||
notes: '',
|
||
packages: {
|
||
windows_x64: emptyPackage('inno_setup'),
|
||
macos_arm64: emptyPackage(),
|
||
macos_x64: emptyPackage()
|
||
}
|
||
})
|
||
|
||
const platforms: { key: PlatformKey; label: string }[] = [
|
||
{ key: 'windows_x64', label: 'Windows 64 位安装包' },
|
||
{ key: 'macos_arm64', label: 'macOS Apple 芯片安装包' },
|
||
{ key: 'macos_x64', label: 'macOS Intel 安装包' }
|
||
]
|
||
|
||
const rules = {
|
||
latest_version: [
|
||
{
|
||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||
if (formData.enabled === 1 && !String(value || '').trim()) {
|
||
callback(new Error('启用自动检测时请填写最新版本号'))
|
||
return
|
||
}
|
||
if (value && !/^\d+(?:\.\d+){0,3}$/.test(value.trim())) {
|
||
callback(new Error('版本号须为 x.y.z 数字格式'))
|
||
return
|
||
}
|
||
callback()
|
||
},
|
||
trigger: ['blur', 'change']
|
||
}
|
||
],
|
||
min_version: [
|
||
{
|
||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||
if (value && !/^\d+(?:\.\d+){0,3}$/.test(value.trim())) {
|
||
callback(new Error('版本号须为 x.y.z 数字格式'))
|
||
return
|
||
}
|
||
callback()
|
||
},
|
||
trigger: ['blur']
|
||
}
|
||
]
|
||
}
|
||
|
||
const formatSize = (size: number) => {
|
||
const bytes = Number(size) || 0
|
||
if (bytes <= 0) return '未填写'
|
||
if (bytes < 1024) return `${bytes} B`
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||
}
|
||
|
||
const assignPackage = (key: PlatformKey, row: Partial<DesktopPackage> | undefined) => {
|
||
const current = formData.packages[key]
|
||
current.url = String(row?.url || '')
|
||
current.sha256 = String(row?.sha256 || '')
|
||
current.size = Number(row?.size || 0)
|
||
current.filename = String(row?.filename || '')
|
||
current.type =
|
||
row?.type === 'inno_setup' || (key === 'windows_x64' && !row?.url && !row?.filename)
|
||
? 'inno_setup'
|
||
: 'archive'
|
||
}
|
||
|
||
const getData = async () => {
|
||
const data = await getDesktopWorkstationConfig()
|
||
formData.enabled = Number(data.enabled ?? 0) ? 1 : 0
|
||
formData.latest_version = data.latest_version || ''
|
||
formData.min_version = data.min_version || ''
|
||
formData.force_update = Number(data.force_update ?? 0) ? 1 : 0
|
||
formData.title = data.title || ''
|
||
formData.notes = data.notes || ''
|
||
assignPackage('windows_x64', data.packages?.windows_x64)
|
||
assignPackage('macos_arm64', data.packages?.macos_arm64)
|
||
assignPackage('macos_x64', data.packages?.macos_x64)
|
||
}
|
||
|
||
const sha256File = async (file: File) => {
|
||
const buffer = await file.arrayBuffer()
|
||
const digest = await crypto.subtle.digest('SHA-256', buffer)
|
||
return Array.from(new Uint8Array(digest))
|
||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||
.join('')
|
||
}
|
||
|
||
const handleUploadFile = async (key: PlatformKey, file: any) => {
|
||
const raw = file?.raw as File | undefined
|
||
if (!raw) return
|
||
formData.packages[key].filename = raw.name || formData.packages[key].filename
|
||
formData.packages[key].size = Number(raw.size || 0)
|
||
if (key === 'windows_x64' && raw.name?.toLowerCase().endsWith('.exe')) {
|
||
formData.packages[key].type = 'inno_setup'
|
||
} else if (raw.name?.toLowerCase().endsWith('.zip')) {
|
||
formData.packages[key].type = 'archive'
|
||
}
|
||
try {
|
||
formData.packages[key].sha256 = await sha256File(raw)
|
||
} catch (error) {
|
||
console.warn(error)
|
||
feedback.msgWarning('浏览器计算 SHA-256 失败,请手动填写校验值')
|
||
}
|
||
}
|
||
|
||
const handleUploadSuccess = (key: PlatformKey, response: any) => {
|
||
const payload = response?.data || {}
|
||
if (payload.uri) {
|
||
formData.packages[key].url = payload.uri
|
||
} else if (payload.url) {
|
||
formData.packages[key].url = payload.url
|
||
}
|
||
if (payload.name && !formData.packages[key].filename) {
|
||
formData.packages[key].filename = payload.name
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
await formRef.value?.validate()
|
||
await setDesktopWorkstationConfig({
|
||
enabled: formData.enabled,
|
||
latest_version: formData.latest_version,
|
||
min_version: formData.min_version,
|
||
force_update: formData.force_update,
|
||
title: formData.title,
|
||
notes: formData.notes,
|
||
packages: {
|
||
windows_x64: { ...formData.packages.windows_x64 },
|
||
macos_arm64: { ...formData.packages.macos_arm64 },
|
||
macos_x64: { ...formData.packages.macos_x64 }
|
||
}
|
||
})
|
||
await getData()
|
||
}
|
||
|
||
getData()
|
||
</script>
|