版本管理全面增强:状态、补丁更新、热更新
1. 自动状态管理 - 新增版本自动标记状态:最新版本为'有效',旧版本自动标记为'已失效' - 前端列表页新增状态列和状态筛选 2. 更新方式升级 - 更新方式从手动/自动改为:手动更新/热更新/完整更新 - 热更新:运行时增量替换文件,无需重启 - 完整更新:下载完整包替换,需重启 3. 补丁更新支持 - 新增 base_version_id 字段支持增量补丁 - 创建版本时可选择基础版本,自动识别为补丁模式 - 补丁模式仅上传变更文件 4. ZIP解析上传(已有功能完善) - 上传ZIP自动解析文件列表 - 支持选择入口文件 5. 国际化完善 - 中英文新增状态、更新方式等翻译
This commit is contained in:
@@ -232,12 +232,14 @@ type Version struct {
|
|||||||
Description string `gorm:"type:text" json:"description"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
Changelog string `gorm:"type:text" json:"changelog"`
|
Changelog string `gorm:"type:text" json:"changelog"`
|
||||||
Status string `gorm:"size:20;default:active" json:"status"`
|
Status string `gorm:"size:20;default:active" json:"status"`
|
||||||
|
BaseVersionID *uint `json:"base_version_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||||
|
BaseVersion *Version `gorm:"foreignKey:BaseVersionID" json:"base_version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionFile 版本文件模型
|
// VersionFile 版本文件模型
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func handleGetAllVersions(c *gin.Context) {
|
|||||||
applicationIDFilter := c.Query("application_id")
|
applicationIDFilter := c.Query("application_id")
|
||||||
updateStrategyFilter := c.Query("update_strategy")
|
updateStrategyFilter := c.Query("update_strategy")
|
||||||
updateMethodFilter := c.Query("update_method")
|
updateMethodFilter := c.Query("update_method")
|
||||||
|
statusFilter := c.Query("status")
|
||||||
searchFilter := c.Query("search")
|
searchFilter := c.Query("search")
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
@@ -85,6 +86,9 @@ func handleGetAllVersions(c *gin.Context) {
|
|||||||
if updateMethodFilter != "" {
|
if updateMethodFilter != "" {
|
||||||
countQuery = countQuery.Where("update_method = ?", updateMethodFilter)
|
countQuery = countQuery.Where("update_method = ?", updateMethodFilter)
|
||||||
}
|
}
|
||||||
|
if statusFilter != "" {
|
||||||
|
countQuery = countQuery.Where("status = ?", statusFilter)
|
||||||
|
}
|
||||||
if searchFilter != "" {
|
if searchFilter != "" {
|
||||||
searchLower := strings.ToLower(searchFilter)
|
searchLower := strings.ToLower(searchFilter)
|
||||||
countQuery = countQuery.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
countQuery = countQuery.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
||||||
@@ -101,6 +105,9 @@ func handleGetAllVersions(c *gin.Context) {
|
|||||||
if updateMethodFilter != "" {
|
if updateMethodFilter != "" {
|
||||||
query = query.Where("update_method = ?", updateMethodFilter)
|
query = query.Where("update_method = ?", updateMethodFilter)
|
||||||
}
|
}
|
||||||
|
if statusFilter != "" {
|
||||||
|
query = query.Where("status = ?", statusFilter)
|
||||||
|
}
|
||||||
if searchFilter != "" {
|
if searchFilter != "" {
|
||||||
searchLower := strings.ToLower(searchFilter)
|
searchLower := strings.ToLower(searchFilter)
|
||||||
query = query.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
query = query.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
||||||
@@ -186,6 +193,7 @@ func handleGetVersionByID(c *gin.Context) {
|
|||||||
"min_version": version.MinVersion,
|
"min_version": version.MinVersion,
|
||||||
"changelog": version.Changelog,
|
"changelog": version.Changelog,
|
||||||
"status": version.Status,
|
"status": version.Status,
|
||||||
|
"base_version_id": version.BaseVersionID,
|
||||||
"files": version.Files,
|
"files": version.Files,
|
||||||
"created_at": version.CreatedAt,
|
"created_at": version.CreatedAt,
|
||||||
"updated_at": version.UpdatedAt,
|
"updated_at": version.UpdatedAt,
|
||||||
@@ -205,6 +213,7 @@ type CreateVersionRequest struct {
|
|||||||
MinVersion string `json:"min_version"`
|
MinVersion string `json:"min_version"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Changelog string `json:"changelog"`
|
Changelog string `json:"changelog"`
|
||||||
|
BaseVersionID *uint `json:"base_version_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCreateVersionGlobal(c *gin.Context) {
|
func handleCreateVersionGlobal(c *gin.Context) {
|
||||||
@@ -247,6 +256,7 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
|||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Changelog: req.Changelog,
|
Changelog: req.Changelog,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
|
BaseVersionID: req.BaseVersionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if version.UpdateStrategy == "" {
|
if version.UpdateStrategy == "" {
|
||||||
@@ -261,6 +271,10 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.DB.Model(&model.Version{}).
|
||||||
|
Where("application_id = ? AND id != ? AND status = ?", req.ApplicationID, version.ID, "active").
|
||||||
|
Update("status", "superseded")
|
||||||
|
|
||||||
userIDPtr := &userID
|
userIDPtr := &userID
|
||||||
versionIDPtr := &version.ID
|
versionIDPtr := &version.ID
|
||||||
service.CreateLog(service.LogParams{
|
service.CreateLog(service.LogParams{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Check, CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Upload, UploadCloud, X } from 'lucide-vue-next'
|
import { Check, CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Upload, UploadCloud, X } from 'lucide-vue-next'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -44,10 +44,12 @@ const formData = ref({
|
|||||||
version: '',
|
version: '',
|
||||||
description: '',
|
description: '',
|
||||||
update_strategy: 'optional' as 'optional' | 'forced',
|
update_strategy: 'optional' as 'optional' | 'forced',
|
||||||
update_method: 'manual' as 'manual' | 'auto',
|
update_method: 'manual' as 'manual' | 'hot' | 'full',
|
||||||
|
status: 'active' as string,
|
||||||
min_version: '',
|
min_version: '',
|
||||||
changelog: '',
|
changelog: '',
|
||||||
entry_file: '',
|
entry_file: '',
|
||||||
|
base_version_id: '',
|
||||||
file: null as File | null,
|
file: null as File | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ async function fetchVersion() {
|
|||||||
formData.value.description = ver.description || ''
|
formData.value.description = ver.description || ''
|
||||||
formData.value.update_strategy = ver.update_strategy || 'optional'
|
formData.value.update_strategy = ver.update_strategy || 'optional'
|
||||||
formData.value.update_method = ver.update_method || 'manual'
|
formData.value.update_method = ver.update_method || 'manual'
|
||||||
|
formData.value.status = ver.status || 'active'
|
||||||
|
formData.value.base_version_id = ver.base_version_id ? String(ver.base_version_id) : ''
|
||||||
formData.value.min_version = ver.min_version || ''
|
formData.value.min_version = ver.min_version || ''
|
||||||
formData.value.changelog = ver.changelog || ''
|
formData.value.changelog = ver.changelog || ''
|
||||||
formData.value.entry_file = ver.entry_file || ''
|
formData.value.entry_file = ver.entry_file || ''
|
||||||
@@ -177,6 +181,7 @@ async function handleSubmit() {
|
|||||||
description: formData.value.description,
|
description: formData.value.description,
|
||||||
update_strategy: formData.value.update_strategy,
|
update_strategy: formData.value.update_strategy,
|
||||||
update_method: formData.value.update_method,
|
update_method: formData.value.update_method,
|
||||||
|
status: formData.value.status,
|
||||||
min_version: formData.value.min_version,
|
min_version: formData.value.min_version,
|
||||||
changelog: formData.value.changelog,
|
changelog: formData.value.changelog,
|
||||||
entry_file: formData.value.entry_file,
|
entry_file: formData.value.entry_file,
|
||||||
@@ -430,20 +435,41 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
<div class="space-y-2">
|
||||||
<div class="space-y-0.5">
|
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
|
||||||
<UiLabel class="text-base">
|
<UiSelect v-model="formData.update_method" :disabled="saving">
|
||||||
{{ t('admin.versions.createForm.autoUpdate') }}
|
<UiSelectTrigger>
|
||||||
</UiLabel>
|
<UiSelectValue />
|
||||||
<p class="text-sm text-muted-foreground">
|
</UiSelectTrigger>
|
||||||
{{ t('admin.versions.createForm.autoUpdateDesc') }}
|
<UiSelectContent>
|
||||||
</p>
|
<UiSelectItem value="full">
|
||||||
</div>
|
{{ t('admin.versions.methods.full') }}
|
||||||
<UiSwitch
|
</UiSelectItem>
|
||||||
:checked="formData.update_method === 'auto'"
|
<UiSelectItem value="hot">
|
||||||
:disabled="saving"
|
{{ t('admin.versions.methods.hot') }}
|
||||||
@update:checked="formData.update_method = $event ? 'auto' : 'manual'"
|
</UiSelectItem>
|
||||||
/>
|
<UiSelectItem value="manual">
|
||||||
|
{{ t('admin.versions.methods.manual') }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel>{{ t('admin.versions.createForm.status') }}</UiLabel>
|
||||||
|
<UiSelect v-model="formData.status" :disabled="saving">
|
||||||
|
<UiSelectTrigger>
|
||||||
|
<UiSelectValue />
|
||||||
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectItem value="active">
|
||||||
|
{{ t('admin.versions.status.active') }}
|
||||||
|
</UiSelectItem>
|
||||||
|
<UiSelectItem value="superseded">
|
||||||
|
{{ t('admin.versions.status.superseded') }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -493,8 +519,12 @@ onMounted(() => {
|
|||||||
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
|
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</span>
|
||||||
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
|
<span>{{
|
||||||
|
formData.update_method === 'full' ? t('admin.versions.methods.full')
|
||||||
|
: formData.update_method === 'hot' ? t('admin.versions.methods.hot')
|
||||||
|
: t('admin.versions.methods.manual')
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -75,15 +75,32 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const methodClasses: Record<string, string> = {
|
const methodClasses: Record<string, string> = {
|
||||||
manual: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
manual: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||||
auto: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
hot: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||||
|
full: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||||
}
|
}
|
||||||
const methodLabels: Record<string, string> = {
|
const methodLabels: Record<string, string> = {
|
||||||
manual: t('admin.versions.methods.manual'),
|
manual: t('admin.versions.methods.manual'),
|
||||||
auto: t('admin.versions.methods.auto'),
|
hot: t('admin.versions.methods.hot'),
|
||||||
|
full: t('admin.versions.methods.full'),
|
||||||
}
|
}
|
||||||
return h(Badge, { class: methodClasses[row.original.update_method] }, () => methodLabels[row.original.update_method])
|
return h(Badge, { class: methodClasses[row.original.update_method] }, () => methodLabels[row.original.update_method])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: () => t('admin.versions.columns.status'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const statusClasses: Record<string, string> = {
|
||||||
|
active: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||||
|
superseded: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||||
|
}
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
active: t('admin.versions.status.active'),
|
||||||
|
superseded: t('admin.versions.status.superseded'),
|
||||||
|
}
|
||||||
|
return h(Badge, { class: statusClasses[row.original.status] }, () => statusLabels[row.original.status])
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'created_at',
|
||||||
header: () => t('admin.versions.columns.createdAt'),
|
header: () => t('admin.versions.columns.createdAt'),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Rocket, Upload, UploadCloud, X } from 'lucide-vue-next'
|
import { CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Rocket, Upload, UploadCloud, X } from 'lucide-vue-next'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -15,6 +15,11 @@ interface Application {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VersionOption {
|
||||||
|
id: number
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
interface VersionFileInfo {
|
interface VersionFileInfo {
|
||||||
file_path: string
|
file_path: string
|
||||||
file_name: string
|
file_name: string
|
||||||
@@ -37,16 +42,18 @@ const loading = ref(false)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
const applications = ref<Application[]>([])
|
const applications = ref<Application[]>([])
|
||||||
|
const previousVersions = ref<VersionOption[]>([])
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
application_id: '',
|
application_id: '',
|
||||||
version: '',
|
version: '',
|
||||||
description: '',
|
description: '',
|
||||||
update_strategy: 'optional' as 'optional' | 'forced',
|
update_strategy: 'optional' as 'optional' | 'forced',
|
||||||
update_method: 'manual' as 'manual' | 'auto',
|
update_method: 'manual' as 'manual' | 'hot' | 'full',
|
||||||
min_version: '',
|
min_version: '',
|
||||||
changelog: '',
|
changelog: '',
|
||||||
entry_file: '',
|
entry_file: '',
|
||||||
|
base_version_id: '',
|
||||||
file: null as File | null,
|
file: null as File | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,6 +86,15 @@ function formatFileSize(size: number): string {
|
|||||||
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const methodLabel = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
full: t('admin.versions.methods.full'),
|
||||||
|
hot: t('admin.versions.methods.hot'),
|
||||||
|
manual: t('admin.versions.methods.manual'),
|
||||||
|
}
|
||||||
|
return map[formData.value.update_method] || formData.value.update_method
|
||||||
|
})
|
||||||
|
|
||||||
async function fetchApplications() {
|
async function fetchApplications() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -97,6 +113,21 @@ async function fetchApplications() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchPreviousVersions(appId: string) {
|
||||||
|
if (!appId)
|
||||||
|
return
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('application_id', appId)
|
||||||
|
params.append('page_size', '100')
|
||||||
|
const data = await api.get<{ versions: VersionOption[] }>(`/dev/versions?${params.toString()}`)
|
||||||
|
previousVersions.value = data?.versions || []
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取历史版本失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleFileSelect(event: Event) {
|
function handleFileSelect(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
@@ -167,6 +198,7 @@ async function handleSubmit() {
|
|||||||
min_version: formData.value.min_version,
|
min_version: formData.value.min_version,
|
||||||
changelog: formData.value.changelog,
|
changelog: formData.value.changelog,
|
||||||
entry_file: formData.value.entry_file,
|
entry_file: formData.value.entry_file,
|
||||||
|
base_version_id: formData.value.base_version_id ? Number(formData.value.base_version_id) : null,
|
||||||
file_path: uploadedData.value.file_path,
|
file_path: uploadedData.value.file_path,
|
||||||
file_size: uploadedData.value.file_size,
|
file_size: uploadedData.value.file_size,
|
||||||
file_hash: uploadedData.value.file_hash,
|
file_hash: uploadedData.value.file_hash,
|
||||||
@@ -186,6 +218,14 @@ async function handleSubmit() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchApplications()
|
fetchApplications()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(() => formData.value.application_id, (newAppId) => {
|
||||||
|
formData.value.base_version_id = ''
|
||||||
|
previousVersions.value = []
|
||||||
|
if (newAppId) {
|
||||||
|
fetchPreviousVersions(newAppId)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -410,20 +450,48 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
<div class="space-y-2">
|
||||||
<div class="space-y-0.5">
|
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
|
||||||
<UiLabel class="text-base">
|
<UiSelect v-model="formData.update_method" :disabled="saving">
|
||||||
{{ t('admin.versions.createForm.autoUpdate') }}
|
<UiSelectTrigger>
|
||||||
</UiLabel>
|
<UiSelectValue />
|
||||||
<p class="text-sm text-muted-foreground">
|
</UiSelectTrigger>
|
||||||
{{ t('admin.versions.createForm.autoUpdateDesc') }}
|
<UiSelectContent>
|
||||||
</p>
|
<UiSelectItem value="full">
|
||||||
</div>
|
{{ t('admin.versions.methods.full') }}
|
||||||
<UiSwitch
|
</UiSelectItem>
|
||||||
:checked="formData.update_method === 'auto'"
|
<UiSelectItem value="hot">
|
||||||
:disabled="saving"
|
{{ t('admin.versions.methods.hot') }}
|
||||||
@update:checked="formData.update_method = $event ? 'auto' : 'manual'"
|
</UiSelectItem>
|
||||||
/>
|
<UiSelectItem value="manual">
|
||||||
|
{{ t('admin.versions.methods.manual') }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }}</UiLabel>
|
||||||
|
<UiSelect v-model="formData.base_version_id" :disabled="saving || !formData.application_id">
|
||||||
|
<UiSelectTrigger>
|
||||||
|
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
|
||||||
|
</UiSelectTrigger>
|
||||||
|
<UiSelectContent>
|
||||||
|
<UiSelectItem value="">
|
||||||
|
{{ t('admin.versions.createForm.noBaseVersion') }}
|
||||||
|
</UiSelectItem>
|
||||||
|
<UiSelectItem
|
||||||
|
v-for="ver in previousVersions"
|
||||||
|
:key="ver.id"
|
||||||
|
:value="String(ver.id)"
|
||||||
|
>
|
||||||
|
v{{ ver.version }}
|
||||||
|
</UiSelectItem>
|
||||||
|
</UiSelectContent>
|
||||||
|
</UiSelect>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ t('admin.versions.createForm.baseVersionDesc') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -485,8 +553,8 @@ onMounted(() => {
|
|||||||
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
|
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</span>
|
||||||
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
|
<span>{{ methodLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const updateStrategySchema = z.enum(['optional', 'forced'])
|
export const updateStrategySchema = z.enum(['optional', 'forced'])
|
||||||
export const updateMethodSchema = z.enum(['manual', 'auto'])
|
export const updateMethodSchema = z.enum(['manual', 'hot', 'full'])
|
||||||
|
export const versionStatusSchema = z.enum(['active', 'superseded'])
|
||||||
|
|
||||||
export const versionSchema = z.object({
|
export const versionSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@@ -14,6 +15,8 @@ export const versionSchema = z.object({
|
|||||||
force_update: z.boolean(),
|
force_update: z.boolean(),
|
||||||
update_strategy: updateStrategySchema,
|
update_strategy: updateStrategySchema,
|
||||||
update_method: updateMethodSchema,
|
update_method: updateMethodSchema,
|
||||||
|
status: versionStatusSchema,
|
||||||
|
base_version_id: z.number().nullable(),
|
||||||
changelog: z.string(),
|
changelog: z.string(),
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const tableRef = ref()
|
|||||||
const appFilter = ref<string>('')
|
const appFilter = ref<string>('')
|
||||||
const strategyFilter = ref<string>('')
|
const strategyFilter = ref<string>('')
|
||||||
const methodFilter = ref<string>('')
|
const methodFilter = ref<string>('')
|
||||||
|
const statusFilter = ref<string>('')
|
||||||
const searchFilter = ref<string>('')
|
const searchFilter = ref<string>('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
@@ -58,7 +59,13 @@ const strategyOptions = computed(() => [
|
|||||||
|
|
||||||
const methodOptions = computed(() => [
|
const methodOptions = computed(() => [
|
||||||
{ label: t('admin.versions.methods.manual'), value: 'manual' },
|
{ label: t('admin.versions.methods.manual'), value: 'manual' },
|
||||||
{ label: t('admin.versions.methods.auto'), value: 'auto' },
|
{ label: t('admin.versions.methods.hot'), value: 'hot' },
|
||||||
|
{ label: t('admin.versions.methods.full'), value: 'full' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusOptions = computed(() => [
|
||||||
|
{ label: t('admin.versions.status.active'), value: 'active' },
|
||||||
|
{ label: t('admin.versions.status.superseded'), value: 'superseded' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const totalSize = computed(() => {
|
const totalSize = computed(() => {
|
||||||
@@ -115,6 +122,9 @@ async function fetchVersions() {
|
|||||||
if (methodFilter.value) {
|
if (methodFilter.value) {
|
||||||
params.append('update_method', methodFilter.value)
|
params.append('update_method', methodFilter.value)
|
||||||
}
|
}
|
||||||
|
if (statusFilter.value) {
|
||||||
|
params.append('status', statusFilter.value)
|
||||||
|
}
|
||||||
if (searchFilter.value) {
|
if (searchFilter.value) {
|
||||||
params.append('search', searchFilter.value)
|
params.append('search', searchFilter.value)
|
||||||
}
|
}
|
||||||
@@ -229,7 +239,7 @@ watch([currentPage, pageSize], () => {
|
|||||||
fetchVersions()
|
fetchVersions()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([appFilter, strategyFilter, methodFilter, searchFilter], () => {
|
watch([appFilter, strategyFilter, methodFilter, statusFilter, searchFilter], () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
fetchVersions()
|
fetchVersions()
|
||||||
})
|
})
|
||||||
@@ -340,6 +350,11 @@ watch([appFilter, strategyFilter, methodFilter, searchFilter], () => {
|
|||||||
:title="t('admin.versions.method')"
|
:title="t('admin.versions.method')"
|
||||||
:options="methodOptions"
|
:options="methodOptions"
|
||||||
/>
|
/>
|
||||||
|
<SingleFilter
|
||||||
|
v-model="statusFilter"
|
||||||
|
:title="t('admin.versions.columns.status')"
|
||||||
|
:options="statusOptions"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
|
|||||||
@@ -1418,6 +1418,7 @@
|
|||||||
"fileSize": "File Size",
|
"fileSize": "File Size",
|
||||||
"strategy": "Strategy",
|
"strategy": "Strategy",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
|
"status": "Status",
|
||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"actions": "Actions"
|
"actions": "Actions"
|
||||||
},
|
},
|
||||||
@@ -1427,7 +1428,12 @@
|
|||||||
},
|
},
|
||||||
"methods": {
|
"methods": {
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"auto": "Auto"
|
"hot": "Hot",
|
||||||
|
"full": "Full"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"active": "Active",
|
||||||
|
"superseded": "Superseded"
|
||||||
},
|
},
|
||||||
"createForm": {
|
"createForm": {
|
||||||
"title": "Create Version",
|
"title": "Create Version",
|
||||||
@@ -1461,6 +1467,12 @@
|
|||||||
"entryFilePlaceholder": "Select entry file (optional)",
|
"entryFilePlaceholder": "Select entry file (optional)",
|
||||||
"forcedUpdate": "Force Update",
|
"forcedUpdate": "Force Update",
|
||||||
"forcedUpdateDesc": "When enabled, users must update to this version to continue using",
|
"forcedUpdateDesc": "When enabled, users must update to this version to continue using",
|
||||||
|
"updateMethod": "Update Method",
|
||||||
|
"baseVersion": "Base Version",
|
||||||
|
"baseVersionPlaceholder": "Select base version (patch mode)",
|
||||||
|
"baseVersionDesc": "Select a base version to only upload changed files for incremental patch update",
|
||||||
|
"noBaseVersion": "None (Full version)",
|
||||||
|
"status": "Status",
|
||||||
"autoUpdate": "Auto Update",
|
"autoUpdate": "Auto Update",
|
||||||
"autoUpdateDesc": "When enabled, the application will automatically download and install updates",
|
"autoUpdateDesc": "When enabled, the application will automatically download and install updates",
|
||||||
"changelog": "Changelog",
|
"changelog": "Changelog",
|
||||||
@@ -1474,6 +1486,7 @@
|
|||||||
"previewFileCount": "File Count",
|
"previewFileCount": "File Count",
|
||||||
"previewEntryFile": "Entry File",
|
"previewEntryFile": "Entry File",
|
||||||
"previewForcedUpdate": "Force Update",
|
"previewForcedUpdate": "Force Update",
|
||||||
|
"previewUpdateMethod": "Update Method",
|
||||||
"previewAutoUpdate": "Auto Update",
|
"previewAutoUpdate": "Auto Update",
|
||||||
"changelogPreview": "Changelog Preview",
|
"changelogPreview": "Changelog Preview",
|
||||||
"noChangelog": "No changelog",
|
"noChangelog": "No changelog",
|
||||||
|
|||||||
@@ -1385,6 +1385,7 @@
|
|||||||
"fileSize": "文件大小",
|
"fileSize": "文件大小",
|
||||||
"strategy": "更新策略",
|
"strategy": "更新策略",
|
||||||
"method": "更新方式",
|
"method": "更新方式",
|
||||||
|
"status": "状态",
|
||||||
"createdAt": "创建时间",
|
"createdAt": "创建时间",
|
||||||
"actions": "操作"
|
"actions": "操作"
|
||||||
},
|
},
|
||||||
@@ -1394,7 +1395,12 @@
|
|||||||
},
|
},
|
||||||
"methods": {
|
"methods": {
|
||||||
"manual": "手动更新",
|
"manual": "手动更新",
|
||||||
"auto": "自动更新"
|
"hot": "热更新",
|
||||||
|
"full": "完整更新"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"active": "有效",
|
||||||
|
"superseded": "已失效"
|
||||||
},
|
},
|
||||||
"createForm": {
|
"createForm": {
|
||||||
"title": "创建版本",
|
"title": "创建版本",
|
||||||
@@ -1428,6 +1434,12 @@
|
|||||||
"entryFilePlaceholder": "选择入口文件(可选)",
|
"entryFilePlaceholder": "选择入口文件(可选)",
|
||||||
"forcedUpdate": "强制更新",
|
"forcedUpdate": "强制更新",
|
||||||
"forcedUpdateDesc": "启用后用户必须更新到此版本才能继续使用",
|
"forcedUpdateDesc": "启用后用户必须更新到此版本才能继续使用",
|
||||||
|
"updateMethod": "更新方式",
|
||||||
|
"baseVersion": "基础版本",
|
||||||
|
"baseVersionPlaceholder": "选择基础版本(补丁模式)",
|
||||||
|
"baseVersionDesc": "选择基础版本后,只会上传变更的文件,用于增量补丁更新",
|
||||||
|
"noBaseVersion": "无(完整版本)",
|
||||||
|
"status": "状态",
|
||||||
"autoUpdate": "自动更新",
|
"autoUpdate": "自动更新",
|
||||||
"autoUpdateDesc": "启用后应用将自动下载并安装更新",
|
"autoUpdateDesc": "启用后应用将自动下载并安装更新",
|
||||||
"changelog": "更新日志",
|
"changelog": "更新日志",
|
||||||
@@ -1441,6 +1453,7 @@
|
|||||||
"previewFileCount": "文件数量",
|
"previewFileCount": "文件数量",
|
||||||
"previewEntryFile": "入口文件",
|
"previewEntryFile": "入口文件",
|
||||||
"previewForcedUpdate": "强制更新",
|
"previewForcedUpdate": "强制更新",
|
||||||
|
"previewUpdateMethod": "更新方式",
|
||||||
"previewAutoUpdate": "自动更新",
|
"previewAutoUpdate": "自动更新",
|
||||||
"changelogPreview": "更新日志预览",
|
"changelogPreview": "更新日志预览",
|
||||||
"noChangelog": "暂无更新日志",
|
"noChangelog": "暂无更新日志",
|
||||||
|
|||||||
Reference in New Issue
Block a user