feat: show admin dashboard version

This commit is contained in:
Lan
2026-06-03 12:21:44 +08:00
parent 2428409953
commit 04a0498923
7 changed files with 89 additions and 21 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ export function usePublicConfigBootstrap() {
return
}
const notifyMessage = configStore.applyRemoteConfig(res.detail)
configStore.applyPublicMeta(res.detail.meta)
const notifyMessage = configStore.applyRemoteConfig(res.detail.config)
if (notifyMessage) {
alertStore.showAlert(notifyMessage, 'success')
}
+3
View File
@@ -68,6 +68,9 @@ export default {
refreshing: 'Refreshing',
lastUpdated: 'Last updated: {time}',
loadFailed: 'Failed to load dashboard data',
footerProduct: 'FileCodeBox Admin Console',
runtimeVersion: 'Runtime Version',
versionPending: 'Pending sync',
fileHealth: 'File Health',
fileHealthDesc: 'Status insights for available, risky, and pending file queues.',
activeFileRatio: 'Active Ratio',
+3
View File
@@ -68,6 +68,9 @@ export default {
refreshing: '刷新中',
lastUpdated: '最近更新:{time}',
loadFailed: '仪表盘数据加载失败',
footerProduct: 'FileCodeBox 管理后台',
runtimeVersion: '运行版本',
versionPending: '待同步',
fileHealth: '文件健康',
fileHealthDesc: '基于状态洞察统计可取件、风险与待处理队列。',
activeFileRatio: '有效占比',
+29 -19
View File
@@ -1,38 +1,48 @@
import api from './client'
import type { ApiResponse, ConfigState } from '@/types'
type PublicConfigEnvelope = {
config?: Partial<ConfigState>
meta?: unknown
}
import type { ApiResponse, ConfigState, PublicConfigPayload } from '@/types'
const isPublicConfigEnvelope = (
detail: ConfigState | PublicConfigEnvelope | null | undefined
): detail is PublicConfigEnvelope => {
detail: ConfigState | PublicConfigPayload | null | undefined
): detail is PublicConfigPayload => {
return !!detail && typeof detail === 'object' && 'config' in detail
}
const normalizeUserConfigResponse = (
response: ApiResponse<ConfigState | PublicConfigPayload>
): ApiResponse<PublicConfigPayload> => {
if (isPublicConfigEnvelope(response.detail)) {
return {
...response,
detail: {
config: response.detail.config,
meta: response.detail.meta
}
}
}
return {
...response,
detail: {
config: response.detail ?? {}
}
}
}
export class ConfigService {
static async getConfig(): Promise<ApiResponse<ConfigState>> {
return api.get('/admin/config/get')
}
static async getUserConfig(): Promise<ApiResponse<ConfigState>> {
static async getUserConfig(): Promise<ApiResponse<PublicConfigPayload>> {
try {
const response = (await api.get('/api/v1/config')) as ApiResponse<
ConfigState | PublicConfigEnvelope
ConfigState | PublicConfigPayload
>
if (isPublicConfigEnvelope(response.detail) && response.detail.config) {
return {
...response,
detail: response.detail.config as ConfigState
}
}
return response as ApiResponse<ConfigState>
return normalizeUserConfigResponse(response)
} catch {
return api.post('/')
const response = (await api.post('/')) as ApiResponse<ConfigState>
return normalizeUserConfigResponse(response)
}
}
+15 -1
View File
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { ConfigState } from '@/types'
import type { ConfigState, PublicConfigMeta } from '@/types'
import {
DEFAULT_PUBLIC_CONFIG,
readNotifyKey,
@@ -16,8 +16,10 @@ export const useConfigStore = defineStore('config', () => {
...DEFAULT_PUBLIC_CONFIG,
...toPublicConfig(readStoredConfig<Partial<ConfigState>>())
})
const publicMeta = ref<PublicConfigMeta>({})
const uploadSizeLimit = computed(() => config.value.uploadSize)
const appVersion = computed(() => publicMeta.value.version || '')
const updateConfig = (nextConfig: Partial<ConfigState>) => {
config.value = {
@@ -45,6 +47,15 @@ export const useConfigStore = defineStore('config', () => {
return `${notifyTitle}: ${notifyContent}`
}
const applyPublicMeta = (nextMeta: PublicConfigMeta | undefined) => {
if (!nextMeta) return
publicMeta.value = {
...publicMeta.value,
...nextMeta
}
}
const reloadStoredConfig = () => {
config.value = {
...DEFAULT_PUBLIC_CONFIG,
@@ -53,8 +64,11 @@ export const useConfigStore = defineStore('config', () => {
}
return {
appVersion,
config,
publicMeta,
uploadSizeLimit,
applyPublicMeta,
applyRemoteConfig,
updateConfig,
reloadStoredConfig
+12
View File
@@ -15,6 +15,18 @@ export interface ThemeChoice {
version: string
}
export interface PublicConfigMeta {
version?: string
api?: Record<string, string>
features?: Record<string, unknown>
limits?: Record<string, unknown>
}
export interface PublicConfigPayload {
config: Partial<ConfigState>
meta?: PublicConfigMeta
}
export interface ConfigState {
name: string
description: string
+25
View File
@@ -221,12 +221,33 @@
</div>
</section>
</div>
<footer
class="mt-6 flex flex-col gap-2 border-t pt-4 text-xs sm:flex-row sm:items-center sm:justify-between"
:class="[isDarkMode ? 'border-gray-800 text-gray-500' : 'border-gray-200 text-gray-500']"
>
<span>{{ t('admin.dashboard.footerProduct') }}</span>
<span class="inline-flex items-center gap-2">
<span>{{ t('admin.dashboard.runtimeVersion') }}</span>
<span
class="rounded-md border px-2 py-0.5 font-medium"
:class="[
isDarkMode
? 'border-gray-700 bg-gray-800/70 text-gray-300'
: 'border-gray-200 bg-white text-gray-700'
]"
>
{{ versionText }}
</span>
</span>
</footer>
</div>
</template>
<script setup lang="ts">
import { computed, defineComponent, h, onMounted } from 'vue'
import type { Component, PropType } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import {
ActivityIcon,
@@ -244,11 +265,14 @@ import StatCard from '@/components/common/StatCard.vue'
import { useDashboardStats, useInjectedDarkMode } from '@/composables'
import { useI18n } from 'vue-i18n'
import { ROUTES } from '@/constants'
import { useConfigStore } from '@/stores/configStore'
import type { DashboardHealthAction } from '@/types'
const isDarkMode = useInjectedDarkMode()
const { t } = useI18n()
const router = useRouter()
const configStore = useConfigStore()
const { appVersion } = storeToRefs(configStore)
const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedText } =
useDashboardStats({
loadFailedMessage: t('admin.dashboard.loadFailed')
@@ -256,6 +280,7 @@ const { dashboardData, errorMessage, fetchDashboardData, isLoading, lastUpdatedT
const primaryTextClass = computed(() => (isDarkMode.value ? 'text-white' : 'text-gray-900'))
const mutedTextClass = computed(() => (isDarkMode.value ? 'text-gray-400' : 'text-gray-500'))
const versionText = computed(() => appVersion.value || t('admin.dashboard.versionPending'))
const panelClass = computed(() =>
isDarkMode.value ? 'bg-gray-800/80 border border-gray-700' : 'bg-white border border-gray-100'
)