This commit is contained in:
Your Name
2026-08-22 08:51:35 +08:00
parent 6c444a4a04
commit c06d293424
69 changed files with 11431 additions and 1601 deletions
@@ -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>