This commit is contained in:
@@ -189,9 +189,8 @@ export const oaApi = {
|
||||
// 统一预警(全平台期限/异常聚合)
|
||||
listAlerts: alertsApi.listAlerts,
|
||||
// 系统更新
|
||||
getSystemUpdateConfig: updateApi.getSystemUpdateConfig,
|
||||
saveSystemUpdateConfig: updateApi.saveSystemUpdateConfig,
|
||||
getSystemUpdateStatus: updateApi.getSystemUpdateStatus,
|
||||
getSystemUpdateReleases: updateApi.getSystemUpdateReleases,
|
||||
checkSystemUpdate: updateApi.checkSystemUpdate,
|
||||
installSystemUpdate: updateApi.installSystemUpdate
|
||||
}
|
||||
@@ -219,7 +218,7 @@ export type {
|
||||
} from './reportdefs'
|
||||
export type { Alert } from './alerts'
|
||||
export type {
|
||||
SystemUpdateStatus, SystemUpdateConfig, SystemUpdateConfigInput, UpdatePhase, ReleaseAsset
|
||||
SystemUpdateStatus, ReleaseHistoryItem, UpdatePhase, ReleaseAsset
|
||||
} from './update'
|
||||
export type {
|
||||
CompanySubject, Contract, Supplier, Customer, BankAccount, Seal, Invoice, ContractMilestone
|
||||
|
||||
@@ -36,37 +36,22 @@ export interface SystemUpdateStatus {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface SystemUpdateConfig {
|
||||
enabled: boolean
|
||||
giteaBaseUrl: string
|
||||
repository: string
|
||||
channel: 'stable' | 'preview'
|
||||
tokenConfigured: boolean
|
||||
allowInsecureHttp: boolean
|
||||
}
|
||||
|
||||
export interface SystemUpdateConfigInput {
|
||||
enabled: boolean
|
||||
giteaBaseUrl: string
|
||||
repository: string
|
||||
channel: 'stable' | 'preview'
|
||||
token: string
|
||||
clearToken: boolean
|
||||
allowInsecureHttp: boolean
|
||||
}
|
||||
|
||||
export function getSystemUpdateConfig() {
|
||||
return http.get<SystemUpdateConfig>('/system-update/config')
|
||||
}
|
||||
|
||||
export function saveSystemUpdateConfig(input: SystemUpdateConfigInput) {
|
||||
return http.put<SystemUpdateConfig>('/system-update/config', input)
|
||||
export interface ReleaseHistoryItem {
|
||||
version: string
|
||||
publishedAt: string | null
|
||||
releaseNotes: string
|
||||
current: boolean
|
||||
latest: boolean
|
||||
}
|
||||
|
||||
export function getSystemUpdateStatus() {
|
||||
return http.get<SystemUpdateStatus>('/system-update/status')
|
||||
}
|
||||
|
||||
export function getSystemUpdateReleases() {
|
||||
return http.get<ReleaseHistoryItem[]>('/system-update/releases', undefined, { timeoutMs: 30000 })
|
||||
}
|
||||
|
||||
export function checkSystemUpdate() {
|
||||
return http.post<SystemUpdateStatus>('/system-update/check', undefined, { timeoutMs: 30000 })
|
||||
}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Check,
|
||||
Connection,
|
||||
Download,
|
||||
Refresh,
|
||||
Setting,
|
||||
Warning
|
||||
} from '@element-plus/icons-vue'
|
||||
import { Check, Download, Refresh, Warning } from '@element-plus/icons-vue'
|
||||
import ErpPageHeader from '../../../components/erp/ErpPageHeader.vue'
|
||||
import {
|
||||
oaApi,
|
||||
OaApiError,
|
||||
type SystemUpdateConfig,
|
||||
type SystemUpdateConfigInput,
|
||||
type ReleaseHistoryItem,
|
||||
type SystemUpdateStatus,
|
||||
type UpdatePhase
|
||||
} from '../../api'
|
||||
@@ -26,19 +18,10 @@ const session = useSession()
|
||||
const isAdmin = computed(() => session.user.value?.roles?.includes('ADMIN') === true)
|
||||
|
||||
const status = ref<SystemUpdateStatus | null>(null)
|
||||
const config = ref<SystemUpdateConfig | null>(null)
|
||||
const sourceForm = reactive<SystemUpdateConfigInput>({
|
||||
enabled: true,
|
||||
giteaBaseUrl: '',
|
||||
repository: 'awaioi/ERP',
|
||||
channel: 'stable',
|
||||
token: '',
|
||||
clearToken: false,
|
||||
allowInsecureHttp: false
|
||||
})
|
||||
const releases = ref<ReleaseHistoryItem[]>([])
|
||||
const loading = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const checking = ref(false)
|
||||
const saving = ref(false)
|
||||
const installing = ref(false)
|
||||
const reconnecting = ref(false)
|
||||
const pollFailures = ref(0)
|
||||
@@ -66,13 +49,33 @@ const phaseLabels: Record<UpdatePhase, string> = {
|
||||
}
|
||||
|
||||
const busy = computed(() => !!status.value && activePhases.has(status.value.phase))
|
||||
const configLocked = computed(() => busy.value || reconnecting.value)
|
||||
const canCheck = computed(() => Boolean(
|
||||
sourceForm.enabled && sourceForm.giteaBaseUrl && !configLocked.value
|
||||
status.value?.configured
|
||||
&& !busy.value
|
||||
&& !reconnecting.value
|
||||
&& !checking.value
|
||||
&& !installing.value
|
||||
))
|
||||
const canInstall = computed(() => Boolean(
|
||||
status.value?.configured && status.value.updateAvailable && status.value.latestVersion && !busy.value
|
||||
status.value?.configured
|
||||
&& status.value.updateAvailable
|
||||
&& status.value.latestVersion
|
||||
&& !busy.value
|
||||
&& !reconnecting.value
|
||||
&& !checking.value
|
||||
&& !installing.value
|
||||
))
|
||||
const remoteLatest = computed(() => releases.value.find(release => release.latest) || releases.value[0] || null)
|
||||
const latestVersion = computed(() => status.value?.latestVersion || remoteLatest.value?.version || null)
|
||||
const latestRelease = computed(() => {
|
||||
const checkedVersion = status.value?.latestVersion
|
||||
if (checkedVersion) {
|
||||
return releases.value.find(release => release.version === checkedVersion) || remoteLatest.value
|
||||
}
|
||||
return remoteLatest.value
|
||||
})
|
||||
const latestPublishedAt = computed(() => status.value?.publishedAt || latestRelease.value?.publishedAt || null)
|
||||
const latestReleaseNotes = computed(() => status.value?.releaseNotes || latestRelease.value?.releaseNotes || '')
|
||||
const phaseTone = computed<'success' | 'warning' | 'danger' | 'info'>(() => {
|
||||
const phase = status.value?.phase
|
||||
if (phase === 'FAILED' || phase === 'ROLLED_BACK') return 'danger'
|
||||
@@ -96,36 +99,9 @@ function formatDate(value: string | null | undefined) {
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
function formatBytes(size: number) {
|
||||
if (size <= 0) return '-'
|
||||
if (size < 1024 * 1024) return `${Math.ceil(size / 1024)} KB`
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function verificationLabel(name: string) {
|
||||
if (name === 'SHA256SUMS.sig') return 'Ed25519 签名'
|
||||
if (name === 'SHA256SUMS') return 'SHA-256 清单'
|
||||
if (name.endsWith('.tar.gz') || name.endsWith('.jar')) return '签名清单校验'
|
||||
return '-'
|
||||
}
|
||||
|
||||
function applyConfig(value: SystemUpdateConfig) {
|
||||
config.value = value
|
||||
sourceForm.enabled = value.enabled
|
||||
sourceForm.giteaBaseUrl = value.giteaBaseUrl
|
||||
sourceForm.repository = value.repository
|
||||
sourceForm.channel = value.channel
|
||||
sourceForm.token = ''
|
||||
sourceForm.clearToken = false
|
||||
sourceForm.allowInsecureHttp = value.allowInsecureHttp
|
||||
}
|
||||
|
||||
async function loadConfig(silent = false) {
|
||||
try {
|
||||
applyConfig(await oaApi.getSystemUpdateConfig())
|
||||
} catch (error) {
|
||||
if (!silent) ElMessage.error(apiMessage(error, '更新源加载失败'))
|
||||
}
|
||||
function formatVersion(value: string | null | undefined) {
|
||||
if (!value) return '-'
|
||||
return /^v/i.test(value) ? value : `v${value}`
|
||||
}
|
||||
|
||||
async function loadStatus(silent = false) {
|
||||
@@ -146,66 +122,33 @@ async function loadStatus(silent = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReleases(silent = false) {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
releases.value = await oaApi.getSystemUpdateReleases()
|
||||
} catch (error) {
|
||||
if (!silent) ElMessage.error(apiMessage(error, '历史版本加载失败'))
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPage() {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([loadConfig(), loadStatus()])
|
||||
await loadStatus()
|
||||
if (status.value?.configured) await loadReleases()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function validateSource() {
|
||||
const baseUrl = sourceForm.giteaBaseUrl.trim()
|
||||
const repository = sourceForm.repository.trim()
|
||||
if (sourceForm.enabled && !baseUrl) {
|
||||
ElMessage.warning('请填写 Gitea 地址')
|
||||
return false
|
||||
}
|
||||
if (baseUrl && !/^https?:\/\//i.test(baseUrl)) {
|
||||
ElMessage.warning('Gitea 地址必须以 https:// 或 http:// 开头')
|
||||
return false
|
||||
}
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(repository)) {
|
||||
ElMessage.warning('仓库格式必须为 owner/repository')
|
||||
return false
|
||||
}
|
||||
if (baseUrl.startsWith('http://') && !sourceForm.allowInsecureHttp) {
|
||||
ElMessage.warning('HTTP 更新源必须显式开启允许 HTTP')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function saveSource(checkAfterSave: boolean) {
|
||||
if (configLocked.value) {
|
||||
ElMessage.warning('更新任务执行期间不能修改更新源')
|
||||
return
|
||||
}
|
||||
if (!validateSource()) return
|
||||
saving.value = true
|
||||
try {
|
||||
const saved = await oaApi.saveSystemUpdateConfig({
|
||||
...sourceForm,
|
||||
giteaBaseUrl: sourceForm.giteaBaseUrl.trim(),
|
||||
repository: sourceForm.repository.trim(),
|
||||
token: sourceForm.token.trim()
|
||||
})
|
||||
applyConfig(saved)
|
||||
ElMessage.success('更新源已保存')
|
||||
await loadStatus(true)
|
||||
if (checkAfterSave && saved.enabled) await checkUpdate()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiMessage(error, '更新源保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
if (!canCheck.value) return
|
||||
checking.value = true
|
||||
try {
|
||||
status.value = await oaApi.checkSystemUpdate()
|
||||
await loadReleases(true)
|
||||
ElMessage.success(status.value.updateAvailable ? '发现新版本' : '当前已是最新版本')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiMessage(error, '检查更新失败'))
|
||||
@@ -216,18 +159,20 @@ async function checkUpdate() {
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!canInstall.value) return
|
||||
const version = status.value?.latestVersion
|
||||
if (!version) return
|
||||
installing.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认安装 ${version}?服务将自动重启,健康检查失败会切回上一版本。`,
|
||||
`确认安装 ${formatVersion(version)}?服务将自动重启,健康检查失败会切回上一版本。`,
|
||||
'安装正式更新',
|
||||
{ type: 'warning', confirmButtonText: '安装并重启', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
installing.value = false
|
||||
return
|
||||
}
|
||||
installing.value = true
|
||||
try {
|
||||
status.value = await oaApi.installSystemUpdate(version)
|
||||
reconnecting.value = false
|
||||
@@ -278,13 +223,13 @@ onBeforeUnmount(stopPolling)
|
||||
<ErpPageHeader
|
||||
title="系统更新"
|
||||
:crumbs="['应用定制平台', '系统更新']"
|
||||
:description="`当前版本 ${status?.currentVersion || '-'}`"
|
||||
:description="`当前版本 ${formatVersion(status?.currentVersion)}`"
|
||||
>
|
||||
<template #actions>
|
||||
<el-button
|
||||
:icon="Refresh"
|
||||
:loading="checking"
|
||||
:disabled="!canCheck || saving"
|
||||
:disabled="!canCheck"
|
||||
@click="checkUpdate"
|
||||
>
|
||||
检查更新
|
||||
@@ -302,7 +247,16 @@ onBeforeUnmount(stopPolling)
|
||||
</ErpPageHeader>
|
||||
|
||||
<el-alert
|
||||
v-if="reconnecting"
|
||||
v-if="status && !status.configured"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="服务器尚未启用在线更新"
|
||||
description="请通过安装器完成正式服务配置。"
|
||||
class="update-alert"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="reconnecting"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@@ -331,11 +285,11 @@ onBeforeUnmount(stopPolling)
|
||||
<section class="update-summary" aria-label="版本状态">
|
||||
<div class="version-block">
|
||||
<span class="field-label">当前版本</span>
|
||||
<strong>{{ status?.currentVersion || '-' }}</strong>
|
||||
<strong>{{ formatVersion(status?.currentVersion) }}</strong>
|
||||
</div>
|
||||
<div class="version-block">
|
||||
<span class="field-label">最新版本</span>
|
||||
<strong>{{ status?.latestVersion || '-' }}</strong>
|
||||
<span class="field-label">在线最新版本</span>
|
||||
<strong>{{ formatVersion(latestVersion) }}</strong>
|
||||
</div>
|
||||
<div class="version-block">
|
||||
<span class="field-label">更新状态</span>
|
||||
@@ -344,77 +298,20 @@ onBeforeUnmount(stopPolling)
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="version-block">
|
||||
<span class="field-label">检查时间</span>
|
||||
<span class="field-label">最后检查时间</span>
|
||||
<span>{{ formatDate(status?.checkedAt) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="source-section">
|
||||
<div class="section-heading">
|
||||
<div class="heading-title"><el-icon><Setting /></el-icon><h2>更新源设置</h2></div>
|
||||
<el-tag v-if="config?.tokenConfigured" type="success" effect="plain" size="small">Token 已配置</el-tag>
|
||||
<el-tag v-else type="info" effect="plain" size="small">公开仓库</el-tag>
|
||||
</div>
|
||||
|
||||
<el-form :disabled="configLocked" label-position="top" class="source-form" @submit.prevent>
|
||||
<el-form-item label="在线更新">
|
||||
<el-switch v-model="sourceForm.enabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Gitea 地址" class="source-form__wide">
|
||||
<el-input v-model="sourceForm.giteaBaseUrl" placeholder="https://git.example.com" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库">
|
||||
<el-input v-model="sourceForm.repository" placeholder="owner/repository" />
|
||||
</el-form-item>
|
||||
<el-form-item label="更新通道">
|
||||
<el-select v-model="sourceForm.channel" style="width: 100%">
|
||||
<el-option label="正式版" value="stable" />
|
||||
<el-option label="预览版" value="preview" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Gitea Token" class="source-form__wide">
|
||||
<el-input
|
||||
v-model="sourceForm.token"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
:disabled="configLocked || sourceForm.clearToken"
|
||||
:placeholder="config?.tokenConfigured ? '已配置,留空保持不变' : '公开仓库可留空'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP 更新源">
|
||||
<el-switch v-model="sourceForm.allowInsecureHttp" active-text="允许" inactive-text="禁止" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="config?.tokenConfigured" label="凭据操作">
|
||||
<el-checkbox
|
||||
v-model="sourceForm.clearToken"
|
||||
:disabled="configLocked || Boolean(sourceForm.token)"
|
||||
>
|
||||
清除现有 Token
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="source-actions">
|
||||
<el-button :loading="saving" :disabled="configLocked || checking" @click="saveSource(false)">保存设置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Connection"
|
||||
:loading="saving || checking"
|
||||
:disabled="configLocked"
|
||||
@click="saveSource(true)"
|
||||
>
|
||||
保存并检查
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="status && (busy || status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' || status.phase === 'SUCCEEDED')" class="update-progress">
|
||||
<section
|
||||
v-if="status && (busy || status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' || status.phase === 'SUCCEEDED')"
|
||||
class="update-section"
|
||||
>
|
||||
<div class="section-heading">
|
||||
<div class="heading-title">
|
||||
<el-icon v-if="status.phase === 'FAILED' || status.phase === 'ROLLED_BACK'"><Warning /></el-icon>
|
||||
<el-icon v-else><Check /></el-icon>
|
||||
<h2>执行结果</h2>
|
||||
<h2>更新进度</h2>
|
||||
</div>
|
||||
<span>{{ status.progress }}%</span>
|
||||
</div>
|
||||
@@ -427,35 +324,36 @@ onBeforeUnmount(stopPolling)
|
||||
<p v-if="status.error" class="error-line"><el-icon><Warning /></el-icon>{{ status.error }}</p>
|
||||
</section>
|
||||
|
||||
<section class="release-section">
|
||||
<section class="update-section">
|
||||
<div class="section-heading">
|
||||
<div class="heading-title"><h2>版本更新日志</h2></div>
|
||||
<span>发布日期 {{ formatDate(status?.publishedAt) }}</span>
|
||||
<div class="heading-title"><h2>最新版本更新日志</h2></div>
|
||||
<span>发布日期 {{ formatDate(latestPublishedAt) }}</span>
|
||||
</div>
|
||||
<div class="release-notes">{{ status?.releaseNotes || '暂无发布说明' }}</div>
|
||||
<div class="release-notes">{{ latestReleaseNotes || '暂无发布说明' }}</div>
|
||||
</section>
|
||||
|
||||
<section class="release-section">
|
||||
<div class="section-heading"><div class="heading-title"><h2>发布文件与校验</h2></div></div>
|
||||
<el-table :data="status?.assets || []" size="small" border empty-text="暂无发布文件">
|
||||
<el-table-column prop="name" label="文件" min-width="280" />
|
||||
<el-table-column label="大小" width="120">
|
||||
<template #default="{ row }">{{ formatBytes(row.size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="校验方式" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="verificationLabel(row.name) !== '-'"
|
||||
:type="row.name === 'SHA256SUMS.sig' ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
size="small"
|
||||
>
|
||||
{{ verificationLabel(row.name) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<section class="update-section history-section">
|
||||
<div class="section-heading">
|
||||
<div class="heading-title"><h2>历史版本</h2></div>
|
||||
<span>{{ releases.length ? `共 ${releases.length} 个正式版本` : '暂无记录' }}</span>
|
||||
</div>
|
||||
<el-skeleton v-if="historyLoading" :rows="4" animated />
|
||||
<div v-else-if="releases.length" class="release-history">
|
||||
<article v-for="release in releases" :key="release.version" class="release-entry">
|
||||
<div class="release-version">
|
||||
<strong>{{ formatVersion(release.version) }}</strong>
|
||||
<div class="release-tags">
|
||||
<el-tag v-if="release.current" type="primary" effect="plain" size="small">当前版本</el-tag>
|
||||
<el-tag v-if="release.latest" type="success" effect="plain" size="small">最新版本</el-tag>
|
||||
</div>
|
||||
<time>{{ formatDate(release.publishedAt) }}</time>
|
||||
</div>
|
||||
<div class="release-notes release-notes--history">
|
||||
{{ release.releaseNotes || '暂无发布说明' }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-else description="暂无历史版本" :image-size="72" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -504,16 +402,14 @@ onBeforeUnmount(stopPolling)
|
||||
font-size: var(--erp-font-size-xs);
|
||||
}
|
||||
|
||||
.source-section,
|
||||
.update-progress,
|
||||
.release-section {
|
||||
.update-section {
|
||||
padding: var(--erp-space-5) 0;
|
||||
border-bottom: 1px solid var(--erp-color-border-soft);
|
||||
}
|
||||
|
||||
.section-heading,
|
||||
.heading-title,
|
||||
.source-actions {
|
||||
.release-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -540,24 +436,8 @@ onBeforeUnmount(stopPolling)
|
||||
font-size: var(--erp-font-size-xs);
|
||||
}
|
||||
|
||||
.source-form {
|
||||
display: grid;
|
||||
grid-template-columns: 140px minmax(260px, 2fr) minmax(200px, 1fr) 160px;
|
||||
gap: 0 var(--erp-space-4);
|
||||
}
|
||||
|
||||
.source-form__wide {
|
||||
min-width: 0;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.source-actions {
|
||||
justify-content: flex-end;
|
||||
gap: var(--erp-space-2);
|
||||
}
|
||||
|
||||
.release-notes {
|
||||
min-height: 96px;
|
||||
min-height: 72px;
|
||||
color: var(--erp-color-text-muted);
|
||||
font-size: var(--erp-font-size-sm);
|
||||
line-height: 1.75;
|
||||
@@ -580,14 +460,47 @@ onBeforeUnmount(stopPolling)
|
||||
font-size: var(--erp-font-size-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.source-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.release-history {
|
||||
border-top: 1px solid var(--erp-color-border-soft);
|
||||
}
|
||||
|
||||
.source-form__wide {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.release-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 190px minmax(0, 1fr);
|
||||
gap: var(--erp-space-5);
|
||||
padding: var(--erp-space-4) 0;
|
||||
border-bottom: 1px solid var(--erp-color-border-soft);
|
||||
}
|
||||
|
||||
.release-entry:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.release-version {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-version > strong {
|
||||
display: block;
|
||||
color: var(--erp-color-text);
|
||||
font-size: var(--erp-font-size-base);
|
||||
}
|
||||
|
||||
.release-tags {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--erp-space-1);
|
||||
margin-top: var(--erp-space-2);
|
||||
}
|
||||
|
||||
.release-version time {
|
||||
display: block;
|
||||
margin-top: var(--erp-space-2);
|
||||
color: var(--erp-color-text-subtle);
|
||||
font-size: var(--erp-font-size-xs);
|
||||
}
|
||||
|
||||
.release-notes--history {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -605,8 +518,7 @@ onBeforeUnmount(stopPolling)
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.update-summary,
|
||||
.source-form {
|
||||
.update-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -625,14 +537,9 @@ onBeforeUnmount(stopPolling)
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.source-actions .el-button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
.release-entry {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--erp-space-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user