refactor: 删除版本更新功能 + 报表页自动刷新
删除版本与更新功能: - routes/admin_api/update.py: 删除整个文件 - routes/admin_api/__init__.py: 移除 update 模块注册 - admin-frontend/src/pages/SystemPage.vue: 移除版本更新UI区块 - admin-frontend/src/api/update.js: 删除整个文件 - 删除 static/admin/assets/update-*.js 报表页自动刷新: - admin-frontend/src/pages/ReportPage.vue: 添加 setInterval 每1秒刷新 - 在 onMounted 启动定时器,onUnmounted 清除 - 覆盖统计数据、运行中任务、系统信息等所有动态数据 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +0,0 @@
|
|||||||
import { api } from './client'
|
|
||||||
|
|
||||||
export async function fetchUpdateStatus() {
|
|
||||||
const { data } = await api.get('/update/status')
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchUpdateResult() {
|
|
||||||
const { data } = await api.get('/update/result')
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchUpdateLog(params = {}) {
|
|
||||||
const { data } = await api.get('/update/log', { params })
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestUpdateCheck() {
|
|
||||||
const { data } = await api.post('/update/check', {})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestUpdateRun(payload = {}) {
|
|
||||||
const { data } = await api.post('/update/run', payload)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
ChatLineSquare,
|
ChatLineSquare,
|
||||||
@@ -19,7 +19,6 @@ import { fetchFeedbackStats } from '../api/feedbacks'
|
|||||||
import { fetchEmailStats } from '../api/email'
|
import { fetchEmailStats } from '../api/email'
|
||||||
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
|
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
|
||||||
import { fetchSystemConfig } from '../api/system'
|
import { fetchSystemConfig } from '../api/system'
|
||||||
import { fetchUpdateResult, fetchUpdateStatus } from '../api/update'
|
|
||||||
|
|
||||||
const refreshStats = inject('refreshStats', null)
|
const refreshStats = inject('refreshStats', null)
|
||||||
const adminStats = inject('adminStats', null)
|
const adminStats = inject('adminStats', null)
|
||||||
@@ -34,9 +33,6 @@ const feedbackStats = ref(null)
|
|||||||
const serverInfo = ref(null)
|
const serverInfo = ref(null)
|
||||||
const dockerStats = ref(null)
|
const dockerStats = ref(null)
|
||||||
const systemConfig = ref(null)
|
const systemConfig = ref(null)
|
||||||
const updateStatus = ref(null)
|
|
||||||
const updateStatusError = ref('')
|
|
||||||
const updateResult = ref(null)
|
|
||||||
const queueTab = ref('running')
|
const queueTab = ref('running')
|
||||||
|
|
||||||
function recordUpdatedAt() {
|
function recordUpdatedAt() {
|
||||||
@@ -63,12 +59,6 @@ function parsePercent(value) {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortCommit(value) {
|
|
||||||
const text = String(value ?? '').trim()
|
|
||||||
if (!text) return '-'
|
|
||||||
return text.length > 12 ? `${text.slice(0, 12)}…` : text
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceLabel(source) {
|
function sourceLabel(source) {
|
||||||
const raw = String(source ?? '').trim()
|
const raw = String(source ?? '').trim()
|
||||||
if (!raw) return '手动'
|
if (!raw) return '手动'
|
||||||
@@ -155,9 +145,6 @@ const runningCountsLabel = computed(() => {
|
|||||||
return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}`
|
return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateAvailable = computed(() => Boolean(updateStatus.value?.update_available))
|
|
||||||
const updateRunning = computed(() => updateResult.value?.status === 'running')
|
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -170,8 +157,6 @@ async function refreshAll() {
|
|||||||
serverResult,
|
serverResult,
|
||||||
dockerResult,
|
dockerResult,
|
||||||
configResult,
|
configResult,
|
||||||
updateStatusResult,
|
|
||||||
updateResultResult,
|
|
||||||
] = await Promise.allSettled([
|
] = await Promise.allSettled([
|
||||||
fetchTaskStats(),
|
fetchTaskStats(),
|
||||||
fetchRunningTasks(),
|
fetchRunningTasks(),
|
||||||
@@ -180,8 +165,6 @@ async function refreshAll() {
|
|||||||
fetchServerInfo(),
|
fetchServerInfo(),
|
||||||
fetchDockerStats(),
|
fetchDockerStats(),
|
||||||
fetchSystemConfig(),
|
fetchSystemConfig(),
|
||||||
fetchUpdateStatus(),
|
|
||||||
fetchUpdateResult(),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
taskStats.value = taskResult.status === 'fulfilled' ? taskResult.value : null
|
taskStats.value = taskResult.status === 'fulfilled' ? taskResult.value : null
|
||||||
@@ -192,22 +175,6 @@ async function refreshAll() {
|
|||||||
dockerStats.value = dockerResult.status === 'fulfilled' ? dockerResult.value : null
|
dockerStats.value = dockerResult.status === 'fulfilled' ? dockerResult.value : null
|
||||||
systemConfig.value = configResult.status === 'fulfilled' ? configResult.value : null
|
systemConfig.value = configResult.status === 'fulfilled' ? configResult.value : null
|
||||||
|
|
||||||
if (updateStatusResult.status === 'fulfilled') {
|
|
||||||
const res = updateStatusResult.value
|
|
||||||
if (res?.ok) {
|
|
||||||
updateStatus.value = res.data || null
|
|
||||||
updateStatusError.value = ''
|
|
||||||
} else {
|
|
||||||
updateStatus.value = null
|
|
||||||
updateStatusError.value = res?.error || '未发现更新状态(Update-Agent 可能未运行)'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateStatus.value = null
|
|
||||||
updateStatusError.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
updateResult.value = updateResultResult.status === 'fulfilled' && updateResultResult.value?.ok ? updateResultResult.value.data : null
|
|
||||||
|
|
||||||
await refreshStats?.()
|
await refreshStats?.()
|
||||||
recordUpdatedAt()
|
recordUpdatedAt()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -215,7 +182,19 @@ async function refreshAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshAll)
|
let refreshTimer = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshAll()
|
||||||
|
refreshTimer = setInterval(refreshAll, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer)
|
||||||
|
refreshTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -225,10 +204,6 @@ onMounted(refreshAll)
|
|||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<div class="hero-title-row">
|
<div class="hero-title-row">
|
||||||
<h2>报表中心</h2>
|
<h2>报表中心</h2>
|
||||||
<el-tag v-if="updateStatusError" type="info" effect="dark">更新状态未知</el-tag>
|
|
||||||
<el-tag v-else-if="updateAvailable" type="warning" effect="dark">新版本可更新</el-tag>
|
|
||||||
<el-tag v-else type="success" effect="dark">已是最新</el-tag>
|
|
||||||
<el-tag v-if="updateRunning" type="warning" effect="plain">更新中</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta app-muted">
|
<div class="hero-meta app-muted">
|
||||||
<span v-if="lastUpdatedAt">更新时间:{{ lastUpdatedAt }}</span>
|
<span v-if="lastUpdatedAt">更新时间:{{ lastUpdatedAt }}</span>
|
||||||
@@ -584,21 +559,12 @@ onMounted(refreshAll)
|
|||||||
<el-icon><Tools /></el-icon>
|
<el-icon><Tools /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="head-text">
|
<div class="head-text">
|
||||||
<div class="panel-title">配置与更新</div>
|
<div class="panel-title">配置概览</div>
|
||||||
<div class="panel-sub app-muted">定时/代理/并发与版本</div>
|
<div class="panel-sub app-muted">定时 / 代理 / 并发</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag v-if="updateAvailable" effect="dark" type="warning">可更新</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
|
||||||
v-if="updateStatusError"
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
:title="updateStatusError"
|
|
||||||
style="margin-bottom: 12px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="config-grid">
|
<div class="config-grid">
|
||||||
<div class="config-item">
|
<div class="config-item">
|
||||||
<div class="config-k app-muted">定时任务</div>
|
<div class="config-k app-muted">定时任务</div>
|
||||||
@@ -631,18 +597,6 @@ onMounted(refreshAll)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="sub-title">版本信息</div>
|
|
||||||
<el-descriptions border :column="1" size="small">
|
|
||||||
<el-descriptions-item label="本地版本(commit)">{{ shortCommit(updateStatus?.local_commit) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="远端版本(commit)">{{ shortCommit(updateStatus?.remote_commit) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最近检查时间">{{ updateStatus?.checked_at || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="updateResult?.job_id" label="最近更新">
|
|
||||||
<span>job {{ updateResult.job_id }} / {{ updateResult?.status || '-' }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system'
|
import { fetchSystemConfig, updateSystemConfig, executeScheduleNow } from '../api/system'
|
||||||
import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy'
|
import { fetchProxyConfig, testProxy, updateProxyConfig } from '../api/proxy'
|
||||||
import { fetchUpdateLog, fetchUpdateResult, fetchUpdateStatus, requestUpdateCheck, requestUpdateRun } from '../api/update'
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -29,17 +28,6 @@ const autoApproveEnabled = ref(false)
|
|||||||
const autoApproveHourlyLimit = ref(10)
|
const autoApproveHourlyLimit = ref(10)
|
||||||
const autoApproveVipDays = ref(7)
|
const autoApproveVipDays = ref(7)
|
||||||
|
|
||||||
// 自动更新
|
|
||||||
const updateLoading = ref(false)
|
|
||||||
const updateActionLoading = ref(false)
|
|
||||||
const updateStatus = ref(null)
|
|
||||||
const updateStatusError = ref('')
|
|
||||||
const updateResult = ref(null)
|
|
||||||
const updateLog = ref('')
|
|
||||||
const updateLogTruncated = ref(false)
|
|
||||||
const updateBuildNoCache = ref(false)
|
|
||||||
let updatePollTimer = null
|
|
||||||
|
|
||||||
const weekdaysOptions = [
|
const weekdaysOptions = [
|
||||||
{ label: '周一', value: '1' },
|
{ label: '周一', value: '1' },
|
||||||
{ label: '周二', value: '2' },
|
{ label: '周二', value: '2' },
|
||||||
@@ -71,59 +59,6 @@ function normalizeBrowseType(value) {
|
|||||||
return '应读'
|
return '应读'
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortCommit(value) {
|
|
||||||
const text = String(value || '').trim()
|
|
||||||
if (!text) return '-'
|
|
||||||
return text.length > 12 ? `${text.slice(0, 12)}…` : text
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUpdateInfo({ withLog = true } = {}) {
|
|
||||||
updateLoading.value = true
|
|
||||||
updateStatusError.value = ''
|
|
||||||
try {
|
|
||||||
const [statusRes, resultRes] = await Promise.all([fetchUpdateStatus(), fetchUpdateResult()])
|
|
||||||
|
|
||||||
if (statusRes?.ok) {
|
|
||||||
updateStatus.value = statusRes.data || null
|
|
||||||
} else {
|
|
||||||
updateStatus.value = null
|
|
||||||
updateStatusError.value = statusRes?.error || '未发现更新状态(Update-Agent 可能未运行)'
|
|
||||||
}
|
|
||||||
|
|
||||||
updateResult.value = resultRes?.ok ? resultRes.data : null
|
|
||||||
|
|
||||||
const jobId = updateResult.value?.job_id
|
|
||||||
if (withLog && jobId) {
|
|
||||||
const logRes = await fetchUpdateLog({ job_id: jobId, max_bytes: 200000 })
|
|
||||||
updateLog.value = logRes?.log || ''
|
|
||||||
updateLogTruncated.value = !!logRes?.truncated
|
|
||||||
} else {
|
|
||||||
updateLog.value = ''
|
|
||||||
updateLogTruncated.value = false
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// handled by interceptor
|
|
||||||
} finally {
|
|
||||||
updateLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startUpdatePolling() {
|
|
||||||
if (updatePollTimer) return
|
|
||||||
updatePollTimer = setInterval(async () => {
|
|
||||||
if (updateResult.value?.status === 'running') {
|
|
||||||
await loadUpdateInfo()
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopUpdatePolling() {
|
|
||||||
if (updatePollTimer) {
|
|
||||||
clearInterval(updatePollTimer)
|
|
||||||
updatePollTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -150,9 +85,6 @@ async function loadAll() {
|
|||||||
proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1
|
proxyEnabled.value = (proxy.proxy_enabled ?? 0) === 1
|
||||||
proxyApiUrl.value = proxy.proxy_api_url || ''
|
proxyApiUrl.value = proxy.proxy_api_url || ''
|
||||||
proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3
|
proxyExpireMinutes.value = proxy.proxy_expire_minutes ?? 3
|
||||||
|
|
||||||
await loadUpdateInfo({ withLog: false })
|
|
||||||
startUpdatePolling()
|
|
||||||
} catch {
|
} catch {
|
||||||
// handled by interceptor
|
// handled by interceptor
|
||||||
} finally {
|
} finally {
|
||||||
@@ -301,49 +233,7 @@ async function saveAutoApprove() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCheckUpdate() {
|
|
||||||
updateActionLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await requestUpdateCheck()
|
|
||||||
ElMessage.success(res?.success ? '已触发检查更新' : '已提交检查请求')
|
|
||||||
setTimeout(() => loadUpdateInfo({ withLog: false }), 800)
|
|
||||||
} catch {
|
|
||||||
// handled by interceptor
|
|
||||||
} finally {
|
|
||||||
updateActionLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onRunUpdate() {
|
|
||||||
const status = updateStatus.value
|
|
||||||
const remote = status?.remote_commit ? shortCommit(status.remote_commit) : '-'
|
|
||||||
const buildFlags = updateBuildNoCache.value ? '\n\n构建选项: 强制重建(--no-cache)' : ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定开始“一键更新”吗?\n\n目标版本: ${remote}${buildFlags}\n\n更新将会重建并重启服务,页面可能短暂不可用;系统会先备份数据库。`,
|
|
||||||
'一键更新确认',
|
|
||||||
{ confirmButtonText: '开始更新', cancelButtonText: '取消', type: 'warning' },
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await requestUpdateRun({ build_no_cache: updateBuildNoCache.value ? 1 : 0 })
|
|
||||||
ElMessage.success(res?.message || '已提交更新请求')
|
|
||||||
startUpdatePolling()
|
|
||||||
setTimeout(() => loadUpdateInfo(), 800)
|
|
||||||
} catch {
|
|
||||||
// handled by interceptor
|
|
||||||
} finally {
|
|
||||||
updateActionLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadAll)
|
onMounted(loadAll)
|
||||||
onBeforeUnmount(stopUpdatePolling)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -457,89 +347,6 @@ onBeforeUnmount(stopUpdatePolling)
|
|||||||
|
|
||||||
<el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button>
|
<el-button type="primary" @click="saveAutoApprove">保存注册设置</el-button>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card" v-loading="updateLoading">
|
|
||||||
<h3 class="section-title">版本与更新</h3>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
v-if="updateStatus?.update_available"
|
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
title="检测到新版本:可以在此页面点击“一键更新”升级并自动重启服务。"
|
|
||||||
style="margin-bottom: 10px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
v-if="updateStatusError"
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
:title="updateStatusError"
|
|
||||||
style="margin-bottom: 10px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-descriptions border :column="1" size="small" style="margin-bottom: 10px">
|
|
||||||
<el-descriptions-item label="本地版本(commit)">
|
|
||||||
{{ shortCommit(updateStatus?.local_commit) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="远端版本(commit)">
|
|
||||||
{{ shortCommit(updateStatus?.remote_commit) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="是否有更新">
|
|
||||||
<el-tag v-if="updateStatus?.update_available" type="danger">有</el-tag>
|
|
||||||
<el-tag v-else type="success">无</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="工作区修改">
|
|
||||||
<el-tag v-if="updateStatus?.dirty" type="warning">有未提交修改</el-tag>
|
|
||||||
<el-tag v-else type="info">干净</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最近检查时间">
|
|
||||||
{{ updateStatus?.checked_at || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="updateStatus?.error" label="检查错误">
|
|
||||||
{{ updateStatus?.error }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<div class="row-actions" style="align-items: center">
|
|
||||||
<el-checkbox v-model="updateBuildNoCache">强制重建(--no-cache)</el-checkbox>
|
|
||||||
<div class="help" style="margin-top: 0">依赖变更或构建异常时建议开启(更新会更慢)。</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row-actions">
|
|
||||||
<el-button @click="loadUpdateInfo" :disabled="updateActionLoading">刷新更新信息</el-button>
|
|
||||||
<el-button @click="onCheckUpdate" :loading="updateActionLoading">检查更新</el-button>
|
|
||||||
<el-button type="danger" @click="onRunUpdate" :loading="updateActionLoading" :disabled="!updateStatus?.update_available">
|
|
||||||
一键更新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider content-position="left">最近一次更新结果</el-divider>
|
|
||||||
<el-descriptions v-if="updateResult" border :column="1" size="small" style="margin-bottom: 10px">
|
|
||||||
<el-descriptions-item label="job_id">{{ updateResult.job_id }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">
|
|
||||||
<el-tag v-if="updateResult.status === 'running'" type="warning">运行中</el-tag>
|
|
||||||
<el-tag v-else-if="updateResult.status === 'success'" type="success">成功</el-tag>
|
|
||||||
<el-tag v-else type="danger">失败</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="阶段">{{ updateResult.stage || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="开始时间">{{ updateResult.started_at || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="结束时间">{{ updateResult.finished_at || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="耗时(秒)">{{ updateResult.duration_seconds ?? '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="更新前(commit)">{{ shortCommit(updateResult.from_commit) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="更新后(commit)">{{ shortCommit(updateResult.to_commit) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="健康检查">
|
|
||||||
<span v-if="updateResult.health_ok === true">通过({{ updateResult.health_message }})</span>
|
|
||||||
<span v-else-if="updateResult.health_ok === false">失败({{ updateResult.health_message }})</span>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="updateResult.error" label="错误">{{ updateResult.error }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
<div v-else class="help">暂无更新记录。</div>
|
|
||||||
|
|
||||||
<el-divider content-position="left">更新日志</el-divider>
|
|
||||||
<div class="help" v-if="updateLogTruncated">日志过长,仅展示末尾内容。</div>
|
|
||||||
<el-input v-model="updateLog" type="textarea" :rows="10" readonly placeholder="暂无日志" />
|
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ admin_api_bp = Blueprint("admin_api", __name__, url_prefix="/yuyx/api")
|
|||||||
|
|
||||||
# Import side effects: register routes on blueprint
|
# Import side effects: register routes on blueprint
|
||||||
from routes.admin_api import core as _core # noqa: F401
|
from routes.admin_api import core as _core # noqa: F401
|
||||||
from routes.admin_api import update as _update # noqa: F401
|
|
||||||
|
|
||||||
# Export security blueprint for app registration
|
# Export security blueprint for app registration
|
||||||
from routes.admin_api.security import security_bp # noqa: F401
|
from routes.admin_api.security import security_bp # noqa: F401
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from flask import jsonify, request, session
|
|
||||||
|
|
||||||
from routes.admin_api import admin_api_bp
|
|
||||||
from routes.decorators import admin_required
|
|
||||||
from services.time_utils import get_beijing_now
|
|
||||||
from services.update_files import (
|
|
||||||
ensure_update_dirs,
|
|
||||||
get_update_job_log_path,
|
|
||||||
get_update_request_path,
|
|
||||||
get_update_result_path,
|
|
||||||
get_update_status_path,
|
|
||||||
load_json_file,
|
|
||||||
sanitize_job_id,
|
|
||||||
tail_text_file,
|
|
||||||
write_json_atomic,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _request_ip() -> str:
|
|
||||||
try:
|
|
||||||
return request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or request.remote_addr or ""
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _make_job_id(prefix: str = "upd") -> str:
|
|
||||||
now_str = get_beijing_now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
rand = uuid.uuid4().hex[:8]
|
|
||||||
return f"{prefix}_{now_str}_{rand}"
|
|
||||||
|
|
||||||
|
|
||||||
def _has_pending_request() -> bool:
|
|
||||||
try:
|
|
||||||
return os.path.exists(get_update_request_path())
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_bool_field(data: dict, key: str) -> bool | None:
|
|
||||||
if not isinstance(data, dict) or key not in data:
|
|
||||||
return None
|
|
||||||
value = data.get(key)
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return value
|
|
||||||
if isinstance(value, int):
|
|
||||||
if value in (0, 1):
|
|
||||||
return bool(value)
|
|
||||||
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
|
||||||
if isinstance(value, str):
|
|
||||||
text = value.strip().lower()
|
|
||||||
if text in ("1", "true", "yes", "y", "on"):
|
|
||||||
return True
|
|
||||||
if text in ("0", "false", "no", "n", "off", ""):
|
|
||||||
return False
|
|
||||||
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
raise ValueError(f"{key} 必须是 0/1 或 true/false")
|
|
||||||
|
|
||||||
|
|
||||||
def _admin_reauth_required() -> bool:
|
|
||||||
try:
|
|
||||||
return time.time() > float(session.get("admin_reauth_until", 0) or 0)
|
|
||||||
except Exception:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/status", methods=["GET"])
|
|
||||||
@admin_required
|
|
||||||
def get_update_status_api():
|
|
||||||
"""读取宿主机 Update-Agent 写入的 update/status.json。"""
|
|
||||||
ensure_update_dirs()
|
|
||||||
status_path = get_update_status_path()
|
|
||||||
data, err = load_json_file(status_path)
|
|
||||||
if err:
|
|
||||||
return jsonify({"ok": False, "error": f"读取 status 失败: {err}", "data": data}), 200
|
|
||||||
if not data:
|
|
||||||
return jsonify({"ok": False, "error": "未发现更新状态(Update-Agent 可能未运行)"}), 200
|
|
||||||
data.setdefault("update_available", False)
|
|
||||||
return jsonify({"ok": True, "data": data}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/result", methods=["GET"])
|
|
||||||
@admin_required
|
|
||||||
def get_update_result_api():
|
|
||||||
"""读取 update/result.json(最近一次更新执行结果)。"""
|
|
||||||
ensure_update_dirs()
|
|
||||||
result_path = get_update_result_path()
|
|
||||||
data, err = load_json_file(result_path)
|
|
||||||
if err:
|
|
||||||
return jsonify({"ok": False, "error": f"读取 result 失败: {err}", "data": data}), 200
|
|
||||||
if not data:
|
|
||||||
return jsonify({"ok": True, "data": None}), 200
|
|
||||||
return jsonify({"ok": True, "data": data}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/log", methods=["GET"])
|
|
||||||
@admin_required
|
|
||||||
def get_update_log_api():
|
|
||||||
"""读取 update/jobs/<job_id>.log 的末尾内容(用于后台展示进度)。"""
|
|
||||||
ensure_update_dirs()
|
|
||||||
|
|
||||||
job_id = sanitize_job_id(request.args.get("job_id"))
|
|
||||||
if not job_id:
|
|
||||||
# 若未指定,则尝试用 result.json 的 job_id
|
|
||||||
result_data, _ = load_json_file(get_update_result_path())
|
|
||||||
job_id = sanitize_job_id(result_data.get("job_id") if isinstance(result_data, dict) else None)
|
|
||||||
|
|
||||||
if not job_id:
|
|
||||||
return jsonify({"ok": True, "job_id": None, "log": "", "truncated": False}), 200
|
|
||||||
|
|
||||||
max_bytes = request.args.get("max_bytes", "200000")
|
|
||||||
try:
|
|
||||||
max_bytes_i = int(max_bytes)
|
|
||||||
except Exception:
|
|
||||||
max_bytes_i = 200_000
|
|
||||||
max_bytes_i = max(10_000, min(2_000_000, max_bytes_i))
|
|
||||||
|
|
||||||
log_path = get_update_job_log_path(job_id)
|
|
||||||
text, truncated = tail_text_file(log_path, max_bytes=max_bytes_i)
|
|
||||||
return jsonify({"ok": True, "job_id": job_id, "log": text, "truncated": truncated}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/check", methods=["POST"])
|
|
||||||
@admin_required
|
|
||||||
def request_update_check_api():
|
|
||||||
"""请求宿主机 Update-Agent 立刻执行一次检查更新。"""
|
|
||||||
ensure_update_dirs()
|
|
||||||
if _has_pending_request():
|
|
||||||
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
|
|
||||||
|
|
||||||
job_id = _make_job_id(prefix="chk")
|
|
||||||
payload = {
|
|
||||||
"job_id": job_id,
|
|
||||||
"action": "check",
|
|
||||||
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"requested_by": session.get("admin_username") or "",
|
|
||||||
"requested_ip": _request_ip(),
|
|
||||||
}
|
|
||||||
write_json_atomic(get_update_request_path(), payload)
|
|
||||||
return jsonify({"success": True, "job_id": job_id}), 200
|
|
||||||
|
|
||||||
|
|
||||||
@admin_api_bp.route("/update/run", methods=["POST"])
|
|
||||||
@admin_required
|
|
||||||
def request_update_run_api():
|
|
||||||
"""请求宿主机 Update-Agent 执行一键更新并重启服务。"""
|
|
||||||
ensure_update_dirs()
|
|
||||||
if _admin_reauth_required():
|
|
||||||
return jsonify({"error": "需要二次确认", "code": "reauth_required"}), 401
|
|
||||||
if _has_pending_request():
|
|
||||||
return jsonify({"error": "已有更新请求正在处理中,请稍后再试"}), 409
|
|
||||||
|
|
||||||
data = request.json or {}
|
|
||||||
try:
|
|
||||||
build_no_cache = _parse_bool_field(data, "build_no_cache")
|
|
||||||
if build_no_cache is None:
|
|
||||||
build_no_cache = _parse_bool_field(data, "no_cache")
|
|
||||||
build_pull = _parse_bool_field(data, "build_pull")
|
|
||||||
if build_pull is None:
|
|
||||||
build_pull = _parse_bool_field(data, "pull")
|
|
||||||
except ValueError as e:
|
|
||||||
return jsonify({"error": str(e)}), 400
|
|
||||||
|
|
||||||
job_id = _make_job_id(prefix="upd")
|
|
||||||
payload = {
|
|
||||||
"job_id": job_id,
|
|
||||||
"action": "update",
|
|
||||||
"requested_at": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"requested_by": session.get("admin_username") or "",
|
|
||||||
"requested_ip": _request_ip(),
|
|
||||||
"build_no_cache": bool(build_no_cache) if build_no_cache is not None else False,
|
|
||||||
"build_pull": bool(build_pull) if build_pull is not None else False,
|
|
||||||
}
|
|
||||||
write_json_atomic(get_update_request_path(), payload)
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"job_id": job_id,
|
|
||||||
"message": "已提交更新请求,服务将重启(页面可能短暂不可用),请等待1-2分钟后刷新",
|
|
||||||
}
|
|
||||||
), 200
|
|
||||||
@@ -1,34 +1,34 @@
|
|||||||
{
|
{
|
||||||
"_email-BghJNgj1.js": {
|
"_email-D-nWLD-A.js": {
|
||||||
"file": "assets/email-BghJNgj1.js",
|
"file": "assets/email-D-nWLD-A.js",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_tasks-Cx_Yf55V.js": {
|
"_system-CJ2QU_TO.js": {
|
||||||
"file": "assets/tasks-Cx_Yf55V.js",
|
"file": "assets/system-CJ2QU_TO.js",
|
||||||
|
"name": "system",
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_tasks-CEM7_KIT.js": {
|
||||||
|
"file": "assets/tasks-CEM7_KIT.js",
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_update-D34iQbO6.js": {
|
"_users-CA0gIT8G.js": {
|
||||||
"file": "assets/update-D34iQbO6.js",
|
"file": "assets/users-CA0gIT8G.js",
|
||||||
"name": "update",
|
|
||||||
"imports": [
|
|
||||||
"index.html"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_users-DCcrmSwH.js": {
|
|
||||||
"file": "assets/users-DCcrmSwH.js",
|
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-C9w-iZIr.js",
|
"file": "assets/index-akVRSJTL.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AnnouncementsPage.vue": {
|
"src/pages/AnnouncementsPage.vue": {
|
||||||
"file": "assets/AnnouncementsPage-DEX_yASt.js",
|
"file": "assets/AnnouncementsPage-7ij3KbUN.js",
|
||||||
"name": "AnnouncementsPage",
|
"name": "AnnouncementsPage",
|
||||||
"src": "src/pages/AnnouncementsPage.vue",
|
"src": "src/pages/AnnouncementsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -60,12 +60,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/EmailPage.vue": {
|
"src/pages/EmailPage.vue": {
|
||||||
"file": "assets/EmailPage-Cev_X_Ce.js",
|
"file": "assets/EmailPage-CuPwCZn-.js",
|
||||||
"name": "EmailPage",
|
"name": "EmailPage",
|
||||||
"src": "src/pages/EmailPage.vue",
|
"src": "src/pages/EmailPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_email-BghJNgj1.js",
|
"_email-D-nWLD-A.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/FeedbacksPage.vue": {
|
"src/pages/FeedbacksPage.vue": {
|
||||||
"file": "assets/FeedbacksPage-BKxylUkG.js",
|
"file": "assets/FeedbacksPage-CSDNvoUn.js",
|
||||||
"name": "FeedbacksPage",
|
"name": "FeedbacksPage",
|
||||||
"src": "src/pages/FeedbacksPage.vue",
|
"src": "src/pages/FeedbacksPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LogsPage.vue": {
|
"src/pages/LogsPage.vue": {
|
||||||
"file": "assets/LogsPage-CemQ-Y_T.js",
|
"file": "assets/LogsPage-DDxhRTa7.js",
|
||||||
"name": "LogsPage",
|
"name": "LogsPage",
|
||||||
"src": "src/pages/LogsPage.vue",
|
"src": "src/pages/LogsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-DCcrmSwH.js",
|
"_users-CA0gIT8G.js",
|
||||||
"_tasks-Cx_Yf55V.js",
|
"_tasks-CEM7_KIT.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -99,22 +99,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ReportPage.vue": {
|
"src/pages/ReportPage.vue": {
|
||||||
"file": "assets/ReportPage-D6vDD1zK.js",
|
"file": "assets/ReportPage-CoI2Nht-.js",
|
||||||
"name": "ReportPage",
|
"name": "ReportPage",
|
||||||
"src": "src/pages/ReportPage.vue",
|
"src": "src/pages/ReportPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_email-BghJNgj1.js",
|
"_email-D-nWLD-A.js",
|
||||||
"_tasks-Cx_Yf55V.js",
|
"_tasks-CEM7_KIT.js",
|
||||||
"_update-D34iQbO6.js"
|
"_system-CJ2QU_TO.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/ReportPage-CSbGJlZV.css"
|
"assets/ReportPage-CW7RwLmI.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SecurityPage.vue": {
|
"src/pages/SecurityPage.vue": {
|
||||||
"file": "assets/SecurityPage-DGvsGoGa.js",
|
"file": "assets/SecurityPage-CQQKpFcS.js",
|
||||||
"name": "SecurityPage",
|
"name": "SecurityPage",
|
||||||
"src": "src/pages/SecurityPage.vue",
|
"src": "src/pages/SecurityPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SettingsPage.vue": {
|
"src/pages/SettingsPage.vue": {
|
||||||
"file": "assets/SettingsPage-Bw1ItHlK.js",
|
"file": "assets/SettingsPage-BpSZamEk.js",
|
||||||
"name": "SettingsPage",
|
"name": "SettingsPage",
|
||||||
"src": "src/pages/SettingsPage.vue",
|
"src": "src/pages/SettingsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -138,25 +138,25 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SystemPage.vue": {
|
"src/pages/SystemPage.vue": {
|
||||||
"file": "assets/SystemPage-RgAQwtHu.js",
|
"file": "assets/SystemPage-DUY6QC8Y.js",
|
||||||
"name": "SystemPage",
|
"name": "SystemPage",
|
||||||
"src": "src/pages/SystemPage.vue",
|
"src": "src/pages/SystemPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_update-D34iQbO6.js",
|
"_system-CJ2QU_TO.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/SystemPage-BjTkcmTG.css"
|
"assets/SystemPage-DhVR0HeO.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/UsersPage.vue": {
|
"src/pages/UsersPage.vue": {
|
||||||
"file": "assets/UsersPage-CFbr6Y3k.js",
|
"file": "assets/UsersPage-hj_Nb-9c.js",
|
||||||
"name": "UsersPage",
|
"name": "UsersPage",
|
||||||
"src": "src/pages/UsersPage.vue",
|
"src": "src/pages/UsersPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-DCcrmSwH.js",
|
"_users-CA0gIT8G.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
|
|||||||
1
static/admin/assets/AnnouncementsPage-7ij3KbUN.js
Normal file
1
static/admin/assets/AnnouncementsPage-7ij3KbUN.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/FeedbacksPage-CSDNvoUn.js
Normal file
1
static/admin/assets/FeedbacksPage-CSDNvoUn.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/LogsPage-DDxhRTa7.js
Normal file
1
static/admin/assets/LogsPage-DDxhRTa7.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/ReportPage-CW7RwLmI.css
Normal file
1
static/admin/assets/ReportPage-CW7RwLmI.css
Normal file
File diff suppressed because one or more lines are too long
1
static/admin/assets/ReportPage-CoI2Nht-.js
Normal file
1
static/admin/assets/ReportPage-CoI2Nht-.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{O as m,_ as T,r as p,d as u,e as h,f as k,g as r,h as a,w as s,n as x,J as d,I as b}from"./index-C9w-iZIr.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),d.success("密码修改成功,请重新登录"),i.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},I=T(N,[["__scopeId","data-v-2f4b840f"]]);export{I as default};
|
import{P as m,_ as T,r as p,e as u,f as h,g as k,h as r,j as a,w as s,p as x,K as i,J as b}from"./index-akVRSJTL.js";async function P(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function C(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};
|
||||||
@@ -1 +0,0 @@
|
|||||||
.page-stack[data-v-d88590f1]{display:flex;flex-direction:column;gap:12px}.card[data-v-d88590f1]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-d88590f1]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-d88590f1]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-d88590f1]{display:flex;flex-wrap:wrap;gap:10px}
|
|
||||||
16
static/admin/assets/SystemPage-DUY6QC8Y.js
Normal file
16
static/admin/assets/SystemPage-DUY6QC8Y.js
Normal file
File diff suppressed because one or more lines are too long
1
static/admin/assets/SystemPage-DhVR0HeO.css
Normal file
1
static/admin/assets/SystemPage-DhVR0HeO.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.page-stack[data-v-bb187149]{display:flex;flex-direction:column;gap:12px}.card[data-v-bb187149]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-bb187149]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-bb187149]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-bb187149]{display:flex;flex-wrap:wrap;gap:10px}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{O as n}from"./index-C9w-iZIr.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
import{P as n}from"./index-akVRSJTL.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
||||||
File diff suppressed because one or more lines are too long
1
static/admin/assets/system-CJ2QU_TO.js
Normal file
1
static/admin/assets/system-CJ2QU_TO.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{P as e}from"./index-akVRSJTL.js";async function s(){const{data:t}=await e.get("/system/config");return t}async function c(t){const{data:a}=await e.post("/system/config",t);return a}async function o(){const{data:t}=await e.post("/schedule/execute",{});return t}export{o as e,s as f,c as u};
|
||||||
@@ -1 +1 @@
|
|||||||
import{O as a}from"./index-C9w-iZIr.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
import{P as a}from"./index-akVRSJTL.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{O as a}from"./index-C9w-iZIr.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
|
|
||||||
@@ -1 +1 @@
|
|||||||
import{O as t}from"./index-C9w-iZIr.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
import{P as t}from"./index-akVRSJTL.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>后台管理 - 知识管理平台</title>
|
<title>后台管理 - 知识管理平台</title>
|
||||||
<script type="module" crossorigin src="./assets/index-C9w-iZIr.js"></script>
|
<script type="module" crossorigin src="./assets/index-akVRSJTL.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-_5Ec1Hmd.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-_5Ec1Hmd.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user