feat: show admin dashboard version
This commit is contained in:
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user