feat: show admin dashboard version

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