diff --git a/mateclaw-ui/src/components/dashboard/OperationalExport.vue b/mateclaw-ui/src/components/dashboard/OperationalExport.vue
new file mode 100644
index 00000000..cca0c38b
--- /dev/null
+++ b/mateclaw-ui/src/components/dashboard/OperationalExport.vue
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('dashboard.exportDescription') }}
+
+
+
+
{{ t('common.selectRange') }}
+
+
+
+
+
+
+
+
+ {{ t('dashboard.exportIncludes', { count: total }) }}
+
+
+
+
+ {{ t('dashboard.generateFailed') }}
+
+
+
+
+
+
+
+
+ {{ step }}/{{ total }}
+ {{ stepLabel }}
+
+
+
+ {{ status === 'locked' ? t('dashboard.exportInProgress') : t('dashboard.generating') }}
+
+
+
+
+
+
+
{{ t('dashboard.reportReady') }}
+
{{ t('dashboard.expiredHint') }}
+
+
+
+
+ {{ t('common.close') }}
+
+ {{ status === 'failed' ? t('dashboard.regenerating') : t('dashboard.generateReport') }}
+
+
+ {{ status === 'locked' ? t('dashboard.exportInProgress') : t('dashboard.generating') }}
+
+
+
+ {{ t('dashboard.downloadReport') }}
+
+
+
+
+
+
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 9d824d6e..dac2005c 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -25,6 +25,10 @@ export default {
copyFailed: 'Copy failed',
confirm: 'Confirm',
close: 'Close',
+ selectRange: 'Select date range',
+ startDate: 'Start date',
+ endDate: 'End date',
+ lastDays: 'Last {n} days',
add: 'Add',
search: 'Search',
noResults: 'No matches',
@@ -3963,6 +3967,8 @@ export default {
exportInProgress: 'Export in progress...',
expiredHint: 'Report is ready, please download soon',
generatingProgress: 'Generating... ({step}/{total})',
+ reportReady: 'Report is ready',
+ exportIncludes: 'The report contains {count} data sheets',
},
memory: {
kicker: 'Memory',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 74c47a9b..41a297db 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -25,6 +25,10 @@ export default {
copyFailed: '复制失败',
confirm: '确认',
close: '关闭',
+ selectRange: '选择时间范围',
+ startDate: '开始日期',
+ endDate: '结束日期',
+ lastDays: '近 {n} 天',
add: '添加',
search: '搜索',
noResults: '没有匹配项',
@@ -4055,6 +4059,8 @@ export default {
exportInProgress: '后台生成中...',
expiredHint: '数据已生成,请尽快下载',
generatingProgress: '生成中... ({step}/{total})',
+ reportReady: '报告已就绪',
+ exportIncludes: '报告包含 {count} 张数据表',
},
memory: {
kicker: '记忆',
diff --git a/mateclaw-ui/src/views/Dashboard.vue b/mateclaw-ui/src/views/Dashboard.vue
index 0cb5c7f2..39a94a5e 100644
--- a/mateclaw-ui/src/views/Dashboard.vue
+++ b/mateclaw-ui/src/views/Dashboard.vue
@@ -7,10 +7,14 @@
{{ t('dashboard.kicker') }}
{{ t('dashboard.title') }}
{{ t('dashboard.desc') }}
-
+
-
-
-
-
{{ t('dashboard.exportDescription') }}
-
-
-
-
-
-
-
- {{ exportStep }}/{{ exportTotal }}
- {{ exportStepLabel }}
-
-
-
-
-
{{ t('dashboard.expiredHint') }}
-
生成失败,请重试
-
-
- 关闭
-
- {{ exportStatus === 'failed' ? t('dashboard.regenerating') : t('dashboard.generateReport') }}
-
-
- {{ exportStatus === 'locked' ? t('dashboard.exportInProgress') : t('dashboard.generating') }}
-
-
- {{ t('dashboard.downloadReport') }}
-
-
-
@@ -261,9 +201,9 @@ import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { ArrowRight, ChatDotRound, DataLine, Document, Tools } from '@element-plus/icons-vue'
-import { dashboardApi, modelApi, operationalApi } from '@/api'
+import { dashboardApi, modelApi, http } from '@/api'
import { getProviderIcon, onProviderIconError } from '@/utils/providerIcons'
-import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
+import OperationalExport from '@/components/dashboard/OperationalExport.vue'
import * as echarts from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
@@ -273,8 +213,10 @@ echarts.use([LineChart, GridComponent, TooltipComponent, LegendComponent, Canvas
const { t, locale } = useI18n()
const router = useRouter()
-const workspaceStore = useWorkspaceStore()
-const isGlobalAdmin = computed(() => workspaceStore.isGlobalAdmin)
+
+// Connected database product name (e.g. "MySQL" / "H2" / "PostgreSQL"), shown as
+// a subtle chip in the page header. Empty string hides it when unavailable.
+const dbLabel = ref('')
const overview = ref>({})
const recentRuns = ref([])
@@ -359,6 +301,15 @@ onMounted(async () => {
} catch {
// Non-critical
}
+
+ // Connected database label — independent and non-critical. Reuses the existing
+ // system health endpoint, which already reports the product name.
+ try {
+ const healthRes: any = await http.get('/system/health')
+ dbLabel.value = (healthRes?.data || healthRes)?.database || ''
+ } catch {
+ dbLabel.value = ''
+ }
})
onUnmounted(() => {
@@ -461,121 +412,6 @@ function calcDuration(run: any): string {
return (ms / 1000).toFixed(1) + 's'
}
-// ── Operational Data Export ──
-const exportDialogVisible = ref(false)
-const exportDateRange = ref<[string, string] | null>(null)
-const exportStatus = ref<'idle' | 'generating' | 'locked' | 'completed' | 'failed'>('idle')
-const exportStep = ref(0)
-const exportTotal = ref(9)
-const exportTaskId = ref('')
-const exportDownloadToken = ref('')
-let exportPollTimer: ReturnType | null = null
-
-const stepLabels = ['概览汇总', 'Token用量', '技能统计', '用户统计', '用户对话', '安全与审计', '渠道统计', '模型配置', '定时任务']
-
-const exportStepLabel = computed(() => {
- const i = exportStep.value - 1
- return stepLabels[i] || ''
-})
-
-const circumference = 2 * Math.PI * 52 // r=52
-
-const progressOffset = computed(() => {
- const pct = Math.min(exportStep.value / exportTotal.value, 1)
- return circumference * (1 - pct)
-})
-
-const disabledExportDate = (date: Date) => {
- // Max 90 days, not in the future
- const now = new Date()
- now.setHours(23, 59, 59, 0)
- return date > now
-}
-
-function openExportDialog() {
- // Reconnect to in-progress or completed task
- if (exportStatus.value === 'completed') {
- // Keep completed state — user can still download
- } else if (exportStatus.value === 'generating' || exportStatus.value === 'locked') {
- // Resume polling for in-progress task
- startPolling()
- } else {
- // Fresh start
- const end = new Date()
- const start = new Date()
- start.setDate(start.getDate() - 30)
- exportDateRange.value = [formatDateStr(start), formatDateStr(end)]
- exportStatus.value = 'idle'
- exportStep.value = 0
- }
- exportDialogVisible.value = true
-}
-
-watch(exportDialogVisible, (v) => {
- if (!v && exportPollTimer) {
- clearInterval(exportPollTimer)
- exportPollTimer = null
- }
-})
-
-function formatDateStr(d: Date): string {
- return d.toISOString().slice(0, 10)
-}
-
-async function doGenerate() {
- if (!exportDateRange.value) return
- try {
- exportStatus.value = 'generating'
- const [start, end] = exportDateRange.value
- const res: any = await operationalApi.generate(start, end)
- exportTaskId.value = res.data?.taskId || res.taskId || ''
- if (!exportTaskId.value) throw new Error('No taskId')
- startPolling()
- } catch (e: any) {
- const msg = e?.response?.data?.msg || e?.message || 'Unknown error'
- if (e?.response?.status === 409) {
- exportStatus.value = 'locked'
- } else {
- exportStatus.value = 'failed'
- console.error('Export generate failed:', msg)
- }
- }
-}
-
-function startPolling() {
- if (exportPollTimer) clearInterval(exportPollTimer)
- exportPollTimer = setInterval(async () => {
- try {
- const res: any = await operationalApi.progress(exportTaskId.value)
- const data = res.data || res
- exportStep.value = data.step || 0
- exportTotal.value = data.total || 9
- if (data.status === 'completed') {
- exportStatus.value = 'completed'
- exportDownloadToken.value = data.downloadToken || ''
- if (exportPollTimer) { clearInterval(exportPollTimer); exportPollTimer = null }
- } else if (data.status === 'failed') {
- exportStatus.value = 'failed'
- if (exportPollTimer) { clearInterval(exportPollTimer); exportPollTimer = null }
- }
- } catch {
- exportStatus.value = 'failed'
- if (exportPollTimer) { clearInterval(exportPollTimer); exportPollTimer = null }
- }
- }, 1000)
-}
-
-async function doDownload() {
- try {
- await operationalApi.download(exportTaskId.value, exportDownloadToken.value)
- exportDialogVisible.value = false
- exportStatus.value = 'idle'
- exportTaskId.value = ''
- exportDownloadToken.value = ''
- } catch (e) {
- console.error('Download failed:', e)
- }
-}