10th_fix status
This commit is contained in:
@@ -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: '連接失敗' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user