更新
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export type DesktopPackage = {
|
||||
url: string
|
||||
sha256: string
|
||||
size: number
|
||||
filename: string
|
||||
}
|
||||
|
||||
export type DesktopWorkstationConfig = {
|
||||
enabled: number
|
||||
latest_version: string
|
||||
min_version: string
|
||||
force_update: number
|
||||
title: string
|
||||
notes: string
|
||||
packages: {
|
||||
windows_x64: DesktopPackage
|
||||
macos_arm64: DesktopPackage
|
||||
macos_x64: DesktopPackage
|
||||
}
|
||||
}
|
||||
|
||||
export function getDesktopWorkstationConfig() {
|
||||
return request.get({ url: '/setting.desktop_workstation/getConfig' }) as Promise<DesktopWorkstationConfig>
|
||||
}
|
||||
|
||||
export function setDesktopWorkstationConfig(params: DesktopWorkstationConfig) {
|
||||
return request.post({ url: '/setting.desktop_workstation/setConfig', params })
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<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>
|
||||
产物一致的 ZIP,并填入打包目录中的 SHA-256。安装包通常超过
|
||||
200MB,优先传到对象存储 / 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="安装包地址">
|
||||
<div class="w-[640px]">
|
||||
<el-input
|
||||
v-model.trim="formData.packages[item.key].url"
|
||||
placeholder="https://...zip 或站内 uploads/file/... 路径"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传安装包">
|
||||
<div>
|
||||
<upload
|
||||
type="file"
|
||||
: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>选择 ZIP 并上传</el-button>
|
||||
</upload>
|
||||
<div class="form-tips">
|
||||
仅建议上传较小的包。大文件请先传到对象存储,再把地址和 SHA-256
|
||||
填到上方。客户端只会安装 ZIP。
|
||||
</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="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
|
||||
} 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 = (): DesktopPackage => ({
|
||||
url: '',
|
||||
sha256: '',
|
||||
size: 0,
|
||||
filename: ''
|
||||
})
|
||||
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
const formData = reactive({
|
||||
enabled: 1,
|
||||
latest_version: '',
|
||||
min_version: '',
|
||||
force_update: 0,
|
||||
title: '',
|
||||
notes: '',
|
||||
packages: {
|
||||
windows_x64: emptyPackage(),
|
||||
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 || '')
|
||||
}
|
||||
|
||||
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)
|
||||
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>
|
||||
Reference in New Issue
Block a user