feat: show admin dashboard version
This commit is contained in:
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -68,6 +68,9 @@ export default {
|
||||
refreshing: '刷新中',
|
||||
lastUpdated: '最近更新:{time}',
|
||||
loadFailed: '仪表盘数据加载失败',
|
||||
footerProduct: 'FileCodeBox 管理后台',
|
||||
runtimeVersion: '运行版本',
|
||||
versionPending: '待同步',
|
||||
fileHealth: '文件健康',
|
||||
fileHealthDesc: '基于状态洞察统计可取件、风险与待处理队列。',
|
||||
activeFileRatio: '有效占比',
|
||||
|
||||
+29
-19
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user