版本管理全面增强:状态、补丁更新、热更新
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"`
|
||||
Changelog string `gorm:"type:text" json:"changelog"`
|
||||
Status string `gorm:"size:20;default:active" json:"status"`
|
||||
BaseVersionID *uint `json:"base_version_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Application Application `gorm:"foreignKey:ApplicationID" json:"application,omitempty"`
|
||||
Files []VersionFile `gorm:"foreignKey:VersionID" json:"files,omitempty"`
|
||||
BaseVersion *Version `gorm:"foreignKey:BaseVersionID" json:"base_version,omitempty"`
|
||||
}
|
||||
|
||||
// VersionFile 版本文件模型
|
||||
|
||||
@@ -43,6 +43,7 @@ func handleGetAllVersions(c *gin.Context) {
|
||||
applicationIDFilter := c.Query("application_id")
|
||||
updateStrategyFilter := c.Query("update_strategy")
|
||||
updateMethodFilter := c.Query("update_method")
|
||||
statusFilter := c.Query("status")
|
||||
searchFilter := c.Query("search")
|
||||
|
||||
var total int64
|
||||
@@ -85,6 +86,9 @@ func handleGetAllVersions(c *gin.Context) {
|
||||
if updateMethodFilter != "" {
|
||||
countQuery = countQuery.Where("update_method = ?", updateMethodFilter)
|
||||
}
|
||||
if statusFilter != "" {
|
||||
countQuery = countQuery.Where("status = ?", statusFilter)
|
||||
}
|
||||
if searchFilter != "" {
|
||||
searchLower := strings.ToLower(searchFilter)
|
||||
countQuery = countQuery.Where("LOWER(version) LIKE ? OR LOWER(description) LIKE ?", "%"+searchLower+"%", "%"+searchLower+"%")
|
||||
@@ -101,6 +105,9 @@ func handleGetAllVersions(c *gin.Context) {
|
||||
if updateMethodFilter != "" {
|
||||
query = query.Where("update_method = ?", updateMethodFilter)
|
||||
}
|
||||
if statusFilter != "" {
|
||||
query = query.Where("status = ?", statusFilter)
|
||||
}
|
||||
if searchFilter != "" {
|
||||
searchLower := strings.ToLower(searchFilter)
|
||||
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,
|
||||
"changelog": version.Changelog,
|
||||
"status": version.Status,
|
||||
"base_version_id": version.BaseVersionID,
|
||||
"files": version.Files,
|
||||
"created_at": version.CreatedAt,
|
||||
"updated_at": version.UpdatedAt,
|
||||
@@ -205,6 +213,7 @@ type CreateVersionRequest struct {
|
||||
MinVersion string `json:"min_version"`
|
||||
Description string `json:"description"`
|
||||
Changelog string `json:"changelog"`
|
||||
BaseVersionID *uint `json:"base_version_id"`
|
||||
}
|
||||
|
||||
func handleCreateVersionGlobal(c *gin.Context) {
|
||||
@@ -247,6 +256,7 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
||||
Description: req.Description,
|
||||
Changelog: req.Changelog,
|
||||
Status: "active",
|
||||
BaseVersionID: req.BaseVersionID,
|
||||
}
|
||||
|
||||
if version.UpdateStrategy == "" {
|
||||
@@ -261,6 +271,10 @@ func handleCreateVersionGlobal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
database.DB.Model(&model.Version{}).
|
||||
Where("application_id = ? AND id != ? AND status = ?", req.ApplicationID, version.ID, "active").
|
||||
Update("status", "superseded")
|
||||
|
||||
userIDPtr := &userID
|
||||
versionIDPtr := &version.ID
|
||||
service.CreateLog(service.LogParams{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
@@ -44,10 +44,12 @@ const formData = ref({
|
||||
version: '',
|
||||
description: '',
|
||||
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: '',
|
||||
changelog: '',
|
||||
entry_file: '',
|
||||
base_version_id: '',
|
||||
file: null as File | null,
|
||||
})
|
||||
|
||||
@@ -90,6 +92,8 @@ async function fetchVersion() {
|
||||
formData.value.description = ver.description || ''
|
||||
formData.value.update_strategy = ver.update_strategy || 'optional'
|
||||
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.changelog = ver.changelog || ''
|
||||
formData.value.entry_file = ver.entry_file || ''
|
||||
@@ -177,6 +181,7 @@ async function handleSubmit() {
|
||||
description: formData.value.description,
|
||||
update_strategy: formData.value.update_strategy,
|
||||
update_method: formData.value.update_method,
|
||||
status: formData.value.status,
|
||||
min_version: formData.value.min_version,
|
||||
changelog: formData.value.changelog,
|
||||
entry_file: formData.value.entry_file,
|
||||
@@ -430,20 +435,41 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel class="text-base">
|
||||
{{ t('admin.versions.createForm.autoUpdate') }}
|
||||
</UiLabel>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('admin.versions.createForm.autoUpdateDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch
|
||||
:checked="formData.update_method === 'auto'"
|
||||
:disabled="saving"
|
||||
@update:checked="formData.update_method = $event ? 'auto' : 'manual'"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
|
||||
<UiSelect v-model="formData.update_method" :disabled="saving">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="full">
|
||||
{{ t('admin.versions.methods.full') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="hot">
|
||||
{{ t('admin.versions.methods.hot') }}
|
||||
</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 class="space-y-2">
|
||||
@@ -493,8 +519,12 @@ onMounted(() => {
|
||||
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
|
||||
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</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>
|
||||
|
||||
|
||||
@@ -75,15 +75,32 @@ export function getColumns(options: ColumnOptions, t: (key: string) => string):
|
||||
cell: ({ row }) => {
|
||||
const methodClasses: Record<string, string> = {
|
||||
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> = {
|
||||
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])
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
header: () => t('admin.versions.columns.createdAt'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 { useRoute, useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
@@ -15,6 +15,11 @@ interface Application {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface VersionOption {
|
||||
id: number
|
||||
version: string
|
||||
}
|
||||
|
||||
interface VersionFileInfo {
|
||||
file_path: string
|
||||
file_name: string
|
||||
@@ -37,16 +42,18 @@ const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const applications = ref<Application[]>([])
|
||||
const previousVersions = ref<VersionOption[]>([])
|
||||
|
||||
const formData = ref({
|
||||
application_id: '',
|
||||
version: '',
|
||||
description: '',
|
||||
update_strategy: 'optional' as 'optional' | 'forced',
|
||||
update_method: 'manual' as 'manual' | 'auto',
|
||||
update_method: 'manual' as 'manual' | 'hot' | 'full',
|
||||
min_version: '',
|
||||
changelog: '',
|
||||
entry_file: '',
|
||||
base_version_id: '',
|
||||
file: null as File | null,
|
||||
})
|
||||
|
||||
@@ -79,6 +86,15 @@ function formatFileSize(size: number): string {
|
||||
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() {
|
||||
loading.value = true
|
||||
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) {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files && target.files.length > 0) {
|
||||
@@ -167,6 +198,7 @@ async function handleSubmit() {
|
||||
min_version: formData.value.min_version,
|
||||
changelog: formData.value.changelog,
|
||||
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_size: uploadedData.value.file_size,
|
||||
file_hash: uploadedData.value.file_hash,
|
||||
@@ -186,6 +218,14 @@ async function handleSubmit() {
|
||||
onMounted(() => {
|
||||
fetchApplications()
|
||||
})
|
||||
|
||||
watch(() => formData.value.application_id, (newAppId) => {
|
||||
formData.value.base_version_id = ''
|
||||
previousVersions.value = []
|
||||
if (newAppId) {
|
||||
fetchPreviousVersions(newAppId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -410,20 +450,48 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-0.5">
|
||||
<UiLabel class="text-base">
|
||||
{{ t('admin.versions.createForm.autoUpdate') }}
|
||||
</UiLabel>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('admin.versions.createForm.autoUpdateDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<UiSwitch
|
||||
:checked="formData.update_method === 'auto'"
|
||||
:disabled="saving"
|
||||
@update:checked="formData.update_method = $event ? 'auto' : 'manual'"
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
|
||||
<UiSelect v-model="formData.update_method" :disabled="saving">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="full">
|
||||
{{ t('admin.versions.methods.full') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="hot">
|
||||
{{ t('admin.versions.methods.hot') }}
|
||||
</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 class="space-y-2">
|
||||
@@ -485,8 +553,8 @@ onMounted(() => {
|
||||
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
|
||||
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
|
||||
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</span>
|
||||
<span>{{ methodLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
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({
|
||||
id: z.number(),
|
||||
@@ -14,6 +15,8 @@ export const versionSchema = z.object({
|
||||
force_update: z.boolean(),
|
||||
update_strategy: updateStrategySchema,
|
||||
update_method: updateMethodSchema,
|
||||
status: versionStatusSchema,
|
||||
base_version_id: z.number().nullable(),
|
||||
changelog: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
|
||||
@@ -31,6 +31,7 @@ const tableRef = ref()
|
||||
const appFilter = ref<string>('')
|
||||
const strategyFilter = ref<string>('')
|
||||
const methodFilter = ref<string>('')
|
||||
const statusFilter = ref<string>('')
|
||||
const searchFilter = ref<string>('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
@@ -58,7 +59,13 @@ const strategyOptions = computed(() => [
|
||||
|
||||
const methodOptions = computed(() => [
|
||||
{ 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(() => {
|
||||
@@ -115,6 +122,9 @@ async function fetchVersions() {
|
||||
if (methodFilter.value) {
|
||||
params.append('update_method', methodFilter.value)
|
||||
}
|
||||
if (statusFilter.value) {
|
||||
params.append('status', statusFilter.value)
|
||||
}
|
||||
if (searchFilter.value) {
|
||||
params.append('search', searchFilter.value)
|
||||
}
|
||||
@@ -229,7 +239,7 @@ watch([currentPage, pageSize], () => {
|
||||
fetchVersions()
|
||||
})
|
||||
|
||||
watch([appFilter, strategyFilter, methodFilter, searchFilter], () => {
|
||||
watch([appFilter, strategyFilter, methodFilter, statusFilter, searchFilter], () => {
|
||||
currentPage.value = 1
|
||||
fetchVersions()
|
||||
})
|
||||
@@ -340,6 +350,11 @@ watch([appFilter, strategyFilter, methodFilter, searchFilter], () => {
|
||||
:title="t('admin.versions.method')"
|
||||
:options="methodOptions"
|
||||
/>
|
||||
<SingleFilter
|
||||
v-model="statusFilter"
|
||||
:title="t('admin.versions.columns.status')"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</template>
|
||||
</DataTable>
|
||||
</UiCardContent>
|
||||
|
||||
@@ -1418,6 +1418,7 @@
|
||||
"fileSize": "File Size",
|
||||
"strategy": "Strategy",
|
||||
"method": "Method",
|
||||
"status": "Status",
|
||||
"createdAt": "Created At",
|
||||
"actions": "Actions"
|
||||
},
|
||||
@@ -1427,7 +1428,12 @@
|
||||
},
|
||||
"methods": {
|
||||
"manual": "Manual",
|
||||
"auto": "Auto"
|
||||
"hot": "Hot",
|
||||
"full": "Full"
|
||||
},
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"superseded": "Superseded"
|
||||
},
|
||||
"createForm": {
|
||||
"title": "Create Version",
|
||||
@@ -1461,6 +1467,12 @@
|
||||
"entryFilePlaceholder": "Select entry file (optional)",
|
||||
"forcedUpdate": "Force Update",
|
||||
"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",
|
||||
"autoUpdateDesc": "When enabled, the application will automatically download and install updates",
|
||||
"changelog": "Changelog",
|
||||
@@ -1474,6 +1486,7 @@
|
||||
"previewFileCount": "File Count",
|
||||
"previewEntryFile": "Entry File",
|
||||
"previewForcedUpdate": "Force Update",
|
||||
"previewUpdateMethod": "Update Method",
|
||||
"previewAutoUpdate": "Auto Update",
|
||||
"changelogPreview": "Changelog Preview",
|
||||
"noChangelog": "No changelog",
|
||||
|
||||
@@ -1385,6 +1385,7 @@
|
||||
"fileSize": "文件大小",
|
||||
"strategy": "更新策略",
|
||||
"method": "更新方式",
|
||||
"status": "状态",
|
||||
"createdAt": "创建时间",
|
||||
"actions": "操作"
|
||||
},
|
||||
@@ -1394,7 +1395,12 @@
|
||||
},
|
||||
"methods": {
|
||||
"manual": "手动更新",
|
||||
"auto": "自动更新"
|
||||
"hot": "热更新",
|
||||
"full": "完整更新"
|
||||
},
|
||||
"status": {
|
||||
"active": "有效",
|
||||
"superseded": "已失效"
|
||||
},
|
||||
"createForm": {
|
||||
"title": "创建版本",
|
||||
@@ -1428,6 +1434,12 @@
|
||||
"entryFilePlaceholder": "选择入口文件(可选)",
|
||||
"forcedUpdate": "强制更新",
|
||||
"forcedUpdateDesc": "启用后用户必须更新到此版本才能继续使用",
|
||||
"updateMethod": "更新方式",
|
||||
"baseVersion": "基础版本",
|
||||
"baseVersionPlaceholder": "选择基础版本(补丁模式)",
|
||||
"baseVersionDesc": "选择基础版本后,只会上传变更的文件,用于增量补丁更新",
|
||||
"noBaseVersion": "无(完整版本)",
|
||||
"status": "状态",
|
||||
"autoUpdate": "自动更新",
|
||||
"autoUpdateDesc": "启用后应用将自动下载并安装更新",
|
||||
"changelog": "更新日志",
|
||||
@@ -1441,6 +1453,7 @@
|
||||
"previewFileCount": "文件数量",
|
||||
"previewEntryFile": "入口文件",
|
||||
"previewForcedUpdate": "强制更新",
|
||||
"previewUpdateMethod": "更新方式",
|
||||
"previewAutoUpdate": "自动更新",
|
||||
"changelogPreview": "更新日志预览",
|
||||
"noChangelog": "暂无更新日志",
|
||||
|
||||
Reference in New Issue
Block a user