10th_fix status

This commit is contained in:
beabigegg
2025-09-04 07:57:06 +08:00
parent e0e3a55e36
commit aba50891ef
6 changed files with 400 additions and 173 deletions

View File

@@ -38,7 +38,10 @@ export const useAdminStore = defineStore('admin', {
totalCost: (state) => state.stats?.overview?.total_cost || 0,
// 系統是否健康
isSystemHealthy: (state) => state.systemHealth?.status === 'healthy'
isSystemHealthy: (state) => {
const status = state.systemHealth?.status
return status === 'healthy' || status === 'warning'
}
},
actions: {
@@ -220,12 +223,37 @@ export const useAdminStore = defineStore('admin', {
*/
async fetchSystemHealth() {
try {
console.log('開始獲取系統健康狀態...')
const response = await adminAPI.getSystemHealth()
this.systemHealth = response
console.log('健康檢查響應:', response)
// 處理響應數據格式
if (response.success && response.data) {
this.systemHealth = response.data
console.log('健康狀態設定為:', this.systemHealth)
} else if (response.status) {
// 直接返回狀態數據
this.systemHealth = response
console.log('直接設定健康狀態為:', this.systemHealth)
} else {
// 預設為異常狀態
this.systemHealth = { status: 'unhealthy', error: 'Invalid response format' }
console.log('設定預設異常狀態')
}
return response
} catch (error) {
console.error('取得系統健康狀態失敗:', error)
this.systemHealth = { status: 'unhealthy' }
console.error('錯誤詳情:', error.response?.data || error.message)
// 根據錯誤類型設定不同狀態
if (error.response?.status === 403) {
this.systemHealth = { status: 'unhealthy', error: '權限不足' }
} else if (error.response?.status === 401) {
this.systemHealth = { status: 'unhealthy', error: '需要登入' }
} else {
this.systemHealth = { status: 'unhealthy', error: '連接失敗' }
}
}
},

View File

@@ -175,17 +175,42 @@
<div class="card-body">
<div class="system-health">
<div class="health-item">
<div class="health-label">系統狀態</div>
<div class="health-label">整體狀態</div>
<div class="health-value">
<el-tag
:type="isSystemHealthy ? 'success' : 'danger'"
:type="getSystemStatusType"
size="small"
>
{{ isSystemHealthy ? '正常' : '異常' }}
{{ getSystemStatusText }}
</el-tag>
</div>
</div>
<!-- 各組件狀態詳情 -->
<div v-if="systemHealthDetails" class="health-details">
<div
v-for="(service, serviceName) in systemHealthDetails.services"
:key="serviceName"
class="service-item"
>
<div class="service-name">{{ getServiceDisplayName(serviceName) }}</div>
<div class="service-status">
<el-tag
:type="getServiceTagType(service.status)"
size="mini"
>
{{ getServiceStatusText(service.status) }}
</el-tag>
</div>
<div v-if="service.error || service.message" class="service-message">
{{ service.error || service.message }}
</div>
<div v-if="service.active_workers !== undefined" class="service-extra">
工作者: {{ service.active_workers }}
</div>
</div>
</div>
<div class="health-item" v-if="systemMetrics">
<div class="health-label">排隊任務</div>
<div class="health-value">
@@ -334,6 +359,26 @@ const dailyStats = computed(() => adminStore.dailyStats)
const userRankings = computed(() => adminStore.userRankings.slice(0, 10))
const isSystemHealthy = computed(() => adminStore.isSystemHealthy)
const systemMetrics = computed(() => adminStore.systemMetrics)
const systemHealthDetails = computed(() => adminStore.systemHealth)
// 系統狀態相關的計算屬性
const getSystemStatusType = computed(() => {
const status = systemHealthDetails.value?.status
if (status === 'healthy') return 'success'
if (status === 'warning') return 'warning'
if (status === 'unhealthy') return 'danger'
return 'info' // 檢查中狀態
})
const getSystemStatusText = computed(() => {
const status = systemHealthDetails.value?.status
switch (status) {
case 'healthy': return '正常'
case 'warning': return '警告'
case 'unhealthy': return '異常'
default: return '檢查中...'
}
})
const recentJobs = computed(() => {
return adminStore.allJobs.slice(0, 10)
@@ -349,6 +394,35 @@ const languageMap = {
'vi': '越文'
}
// 服務狀態相關方法
const getServiceDisplayName = (serviceName) => {
const nameMap = {
'database': '資料庫',
'translation_service': '翻譯服務',
'celery': 'Celery 工作者',
'file_system': '檔案系統'
}
return nameMap[serviceName] || serviceName
}
const getServiceTagType = (status) => {
switch (status) {
case 'healthy': return 'success'
case 'warning': return 'warning'
case 'unhealthy': return 'danger'
default: return 'info'
}
}
const getServiceStatusText = (status) => {
switch (status) {
case 'healthy': return '正常'
case 'warning': return '警告'
case 'unhealthy': return '異常'
default: return '未知'
}
}
// 方法
const refreshData = async () => {
loading.value = true
@@ -405,18 +479,37 @@ const initCharts = () => {
}
const initDailyChart = () => {
if (!dailyChartRef.value || dailyStats.value.length === 0) return
if (dailyChart.value) {
dailyChart.value.dispose()
if (!dailyChartRef.value) {
console.warn('Daily chart ref not available')
return
}
dailyChart.value = echarts.init(dailyChartRef.value)
if (!dailyStats.value || dailyStats.value.length === 0) {
console.warn('Daily stats data not available or empty')
return
}
const dates = dailyStats.value.map(stat => stat.date)
const jobs = dailyStats.value.map(stat => stat.jobs)
const completed = dailyStats.value.map(stat => stat.completed)
const failed = dailyStats.value.map(stat => stat.failed)
try {
if (dailyChart.value) {
dailyChart.value.dispose()
}
dailyChart.value = echarts.init(dailyChartRef.value)
console.log('Daily stats data:', dailyStats.value)
// 確保數據格式正確並提供預設值
const dates = dailyStats.value.map(stat => stat?.date || 'N/A')
const jobs = dailyStats.value.map(stat => stat?.jobs || 0)
const completed = dailyStats.value.map(stat => stat?.completed || 0)
// 注意:後端可能沒有提供 failed 數據,所以計算或預設為 0
const failed = dailyStats.value.map(stat => {
if (stat?.failed !== undefined) {
return stat.failed
}
// 如果沒有 failed 數據,可以計算為 total - completed或預設為 0
return Math.max(0, (stat?.jobs || 0) - (stat?.completed || 0))
})
const option = {
title: {
@@ -471,11 +564,34 @@ const initDailyChart = () => {
]
}
dailyChart.value.setOption(option)
dailyChart.value.setOption(option)
console.log('Daily chart initialized successfully')
} catch (error) {
console.error('Error initializing daily chart:', error)
// 清理可能損壞的圖表實例
if (dailyChart.value) {
try {
dailyChart.value.dispose()
} catch (e) {
console.error('Error disposing chart:', e)
}
dailyChart.value = null
}
}
}
const initCostChart = () => {
if (!costChartRef.value || dailyStats.value.length === 0) return
if (!costChartRef.value) {
console.warn('Cost chart ref not available')
return
}
if (!dailyStats.value || dailyStats.value.length === 0) {
console.warn('Daily stats data not available for cost chart')
return
}
try {
if (costChart.value) {
costChart.value.dispose()
@@ -483,8 +599,8 @@ const initCostChart = () => {
costChart.value = echarts.init(costChartRef.value)
const dates = dailyStats.value.map(stat => stat.date)
const costs = dailyStats.value.map(stat => stat.cost)
const dates = dailyStats.value.map(stat => stat?.date || 'N/A')
const costs = dailyStats.value.map(stat => stat?.cost || 0)
const option = {
title: {
@@ -537,7 +653,20 @@ const initCostChart = () => {
]
}
costChart.value.setOption(option)
costChart.value.setOption(option)
console.log('Cost chart initialized successfully')
} catch (error) {
console.error('Error initializing cost chart:', error)
// 清理可能損壞的圖表實例
if (costChart.value) {
try {
costChart.value.dispose()
} catch (e) {
console.error('Error disposing cost chart:', e)
}
costChart.value = null
}
}
}
const getFileExtension = (filename) => {
@@ -755,6 +884,49 @@ onUnmounted(() => {
color: var(--el-text-color-primary);
}
}
.health-details {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
.service-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
border-bottom: 1px solid var(--el-border-color-extra-light);
&:last-child {
border-bottom: none;
}
.service-name {
font-weight: 500;
color: var(--el-text-color-primary);
font-size: 13px;
}
.service-status {
display: flex;
align-items: center;
}
.service-message {
font-size: 12px;
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
padding: 4px 8px;
border-radius: 4px;
word-break: break-word;
}
.service-extra {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
}

View File

@@ -174,40 +174,6 @@
</div>
</div>
<!-- 系統公告 -->
<div class="announcements-section" v-if="announcements.length > 0">
<div class="content-card">
<div class="card-header">
<h3 class="card-title">系統公告</h3>
</div>
<div class="card-body">
<div class="announcements-list">
<div
v-for="announcement in announcements"
:key="announcement.id"
class="announcement-item"
:class="announcement.type"
>
<div class="announcement-icon">
<el-icon>
<component :is="getAnnouncementIcon(announcement.type)" />
</el-icon>
</div>
<div class="announcement-content">
<div class="announcement-title">{{ announcement.title }}</div>
<div class="announcement-message">{{ announcement.message }}</div>
<div class="announcement-time">{{ formatTime(announcement.created_at) }}</div>
</div>
<div class="announcement-actions" v-if="announcement.actionText">
<el-button type="text" size="small" @click="handleAnnouncementAction(announcement)">
{{ announcement.actionText }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -219,7 +185,7 @@ import { useJobsStore } from '@/stores/jobs'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Upload, List, Refresh, Files, Clock, SuccessFilled, CircleCloseFilled,
ArrowRight, Document, More, InfoFilled, WarningFilled, CircleCheckFilled
ArrowRight, Document, More
} from '@element-plus/icons-vue'
// Router 和 Stores
@@ -229,23 +195,6 @@ const jobsStore = useJobsStore()
// 響應式數據
const loading = ref(false)
const announcements = ref([
{
id: 1,
type: 'info',
title: '系統更新通知',
message: '系統已更新至最新版本,新增了批量下載功能。',
created_at: new Date().toISOString(),
actionText: '了解更多'
},
{
id: 2,
type: 'warning',
title: '維護通知',
message: '系統將於本週日凌晨 2:00-4:00 進行定期維護。',
created_at: new Date(Date.now() - 86400000).toISOString()
}
])
// 計算屬性
const jobStats = computed(() => jobsStore.jobStats)
@@ -320,13 +269,6 @@ const handleJobAction = async (action, job) => {
}
}
const handleAnnouncementAction = (announcement) => {
if (announcement.actionUrl) {
window.open(announcement.actionUrl, '_blank')
} else {
ElMessage.info('功能開發中,敬請期待')
}
}
const getFileExtension = (filename) => {
return filename.split('.').pop().toLowerCase()
@@ -366,15 +308,6 @@ const formatTime = (timestamp) => {
return time.toLocaleDateString('zh-TW')
}
const getAnnouncementIcon = (type) => {
const iconMap = {
info: 'InfoFilled',
warning: 'WarningFilled',
success: 'CircleCheckFilled',
error: 'CircleCloseFilled'
}
return iconMap[type] || 'InfoFilled'
}
// 生命週期
onMounted(async () => {
@@ -578,72 +511,6 @@ onMounted(async () => {
}
}
.announcements-section {
.announcements-list {
.announcement-item {
display: flex;
align-items: flex-start;
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
border-left: 4px solid;
&.info {
background-color: var(--el-color-info-light-9);
border-left-color: var(--el-color-info);
}
&.warning {
background-color: var(--el-color-warning-light-9);
border-left-color: var(--el-color-warning);
}
&.success {
background-color: var(--el-color-success-light-9);
border-left-color: var(--el-color-success);
}
&.error {
background-color: var(--el-color-danger-light-9);
border-left-color: var(--el-color-danger);
}
&:last-child {
margin-bottom: 0;
}
.announcement-icon {
margin-right: 12px;
margin-top: 2px;
}
.announcement-content {
flex: 1;
.announcement-title {
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 4px;
}
.announcement-message {
color: var(--el-text-color-regular);
line-height: 1.5;
margin-bottom: 8px;
}
.announcement-time {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
.announcement-actions {
margin-left: 12px;
}
}
}
}
}
.loading-state {