仪表板组件支持拖动排序

This commit is contained in:
jxxghp
2024-05-09 14:45:12 +08:00
parent 74e96980e6
commit 2065992b17
18 changed files with 437 additions and 464 deletions
+2 -1
View File
@@ -55,6 +55,7 @@
"vue3-ace-editor": "^2.2.4", "vue3-ace-editor": "^2.2.4",
"vue3-apexcharts": "^1.4.1", "vue3-apexcharts": "^1.4.1",
"vue3-perfect-scrollbar": "^2.0.0", "vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "3.5.14", "vuetify": "3.5.14",
"vuetify-use-dialog": "^0.6.11", "vuetify-use-dialog": "^0.6.11",
"vuex": "^4.1.0", "vuex": "^4.1.0",
@@ -114,4 +115,4 @@
"resolutions": { "resolutions": {
"postcss": "8" "postcss": "8"
} }
} }
-20
View File
@@ -34,30 +34,10 @@ function startSSEMessager() {
} }
} }
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem('MP_DASHBOARD')) {
localStorage.setItem('MP_DASHBOARD', data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem('MP_DASHBOARD')) {
return
}
await loadDashboardConfig()
}
// 页面加载时,加载当前用户数据 // 页面加载时,加载当前用户数据
onBeforeMount(async () => { onBeforeMount(async () => {
setTheme() setTheme()
startSSEMessager() startSSEMessager()
await tryLoadDashboardConfig()
}) })
</script> </script>
+4 -4
View File
@@ -445,11 +445,11 @@ export interface Plugin {
add_time?: number add_time?: number
} }
// 插件仪表板 // 仪表板组件
export interface PluginDashboard { export interface DashboardItem {
// 插件ID // ID
id: string id: string
// 插件名称 // 名称
name: string name: string
// 全局配置 // 全局配置
attrs: { [key: string]: any } attrs: { [key: string]: any }
+44
View File
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { DashboardItem } from '@/api/types'
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue'
import { isNullOrEmptyObject } from '@/@core/utils'
// 输入参数
const props = defineProps({
// 仪表板配置
config: Object as PropType<DashboardItem>,
})
console.log(props.config)
</script>
<template>
<!-- 系统内置的仪表板 -->
<AnalyticsStorage v-if="config?.id === 'storage'" />
<AnalyticsMediaStatistic v-else-if="config?.id === 'mediaStatistic'" />
<AnalyticsWeeklyOverview v-else-if="config?.id === 'weeklyOverview'" />
<AnalyticsSpeed v-else-if="config?.id === 'speed'" />
<AnalyticsScheduler v-else-if="config?.id === 'scheduler'" />
<AnalyticsCpu v-else-if="config?.id === 'cpu'" />
<AnalyticsMemory v-else-if="config?.id === 'memory'" />
<MediaServerLibrary v-else-if="config?.id === 'library'" />
<MediaServerPlaying v-else-if="config?.id === 'playing'" />
<MediaServerLatest v-else-if="config?.id === 'latest'" />
<!-- 插件仪表板 -->
<VCard v-else-if="!isNullOrEmptyObject(props.config)">
<VCardItem>
<VCardTitle class="cursor-move">{{ props.config?.name }}</VCardTitle>
</VCardItem>
<VCardItem>
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardItem>
</VCard>
</template>
+165 -97
View File
@@ -1,20 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue' import draggable from 'vuedraggable'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
import DashboardRender from '@/components/render/DashboardRender.vue'
import api from '@/api' import api from '@/api'
import { isNullOrEmptyObject } from '@/@core/utils' import { isNullOrEmptyObject } from '@/@core/utils'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { PluginDashboard } from '@/api/types' import { DashboardItem } from '@/api/types'
import store from '@/store' import store from '@/store'
import DashboardElement from '@/components/misc/DashboardElement.vue'
// 显示器宽度 // 显示器宽度
const display = useDisplay() const display = useDisplay()
@@ -22,8 +13,8 @@ const display = useDisplay()
// 从Vuex Store中获取superuser信息 // 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser const superUser = store.state.auth.superUser
// 从localStorage中获取数据 // 仪表板启用配置
const default_config = { const enableConfig = ref<{ [key: string]: boolean }>({
mediaStatistic: true, mediaStatistic: true,
scheduler: false, scheduler: false,
speed: false, speed: false,
@@ -34,47 +25,149 @@ const default_config = {
library: true, library: true,
playing: true, playing: true,
latest: true, latest: true,
}
// 仪表盘字典
const dashboard_names = ref<{ [key: string]: string }>({
storage: '存储空间',
mediaStatistic: '媒体统计',
weeklyOverview: '最近入库',
speed: '实时速率',
scheduler: '后台任务',
cpu: 'CPU',
memory: '内存',
library: '我的媒体库',
playing: '继续观看',
latest: '最近添加',
}) })
// 仪表板的插件 // 仪表板配置
const dashboard_plugins = ref<any[]>([]) const dashboardConfigs = ref<DashboardItem[]>([
{
id: 'storage',
name: '存储空间',
attrs: {},
cols: { cols: 12, md: 4 },
elements: [],
},
{
id: 'mediaStatistic',
name: '媒体统计',
attrs: {},
cols: { cols: 12, md: 8 },
elements: [],
},
{
id: 'weeklyOverview',
name: '最近入库',
attrs: {},
cols: { cols: 12, md: 4 },
elements: [],
},
{
id: 'speed',
name: '实时速率',
attrs: {},
cols: { cols: 12, md: 4 },
elements: [],
},
{
id: 'scheduler',
name: '后台任务',
attrs: {},
cols: { cols: 12, md: 4 },
elements: [],
},
{
id: 'cpu',
name: 'CPU',
attrs: {},
cols: { cols: 12, md: 6 },
elements: [],
},
{
id: 'memory',
name: '内存',
attrs: {},
cols: { cols: 12, md: 6 },
elements: [],
},
{
id: 'library',
name: '我的媒体库',
attrs: {},
cols: { cols: 12 },
elements: [],
},
{
id: 'playing',
name: '继续观看',
attrs: {},
cols: { cols: 12 },
elements: [],
},
{
id: 'latest',
name: '最近添加',
attrs: {},
cols: { cols: 12 },
elements: [],
},
])
// 插件仪表板配置 // 仪表板的插件
const plugin_dashboards = ref<PluginDashboard[]>([]) const dashboardPlugins = ref<any[]>([])
// 弹窗 // 弹窗
const dialog = ref(false) const dialog = ref(false)
// 初始化默认值 // 加载用户监控面板配置(本地无配置时才加载)
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}')) async function loadDashboardConfig() {
if (isNullOrEmptyObject(config.value)) { // 显示配置
config.value = default_config const local_enable = localStorage.getItem('MP_DASHBOARD')
if (local_enable) {
enableConfig.value = JSON.parse(local_enable)
} else {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
enableConfig.value = response.data.value
localStorage.setItem('MP_DASHBOARD', JSON.stringify(response.data.value))
}
}
// 顺序配置
const local_order = localStorage.getItem('MP_DASHBOARD_ORDER')
let order = null
if (local_order) {
order = JSON.parse(local_order)
} else {
const response2 = await api.get('/user/config/DashboardOrder')
if (response2 && response2.data && response2.data.value) {
order = response2.data.value
localStorage.setItem('MP_DASHBOARD_ORDER', JSON.stringify(order))
}
}
// 按order的顺序对dashboardConfigs进行排序
if (order) {
const newConfigs: DashboardItem[] = []
order.forEach((item: { id: string }) => {
const config = dashboardConfigs.value.find(c => c.id === item.id)
if (config) {
newConfigs.push(config)
}
})
dashboardConfigs.value = newConfigs
}
} }
// 设置项目 // 设置项目
function setDashboardConfig() { function setDashboardConfig() {
const data = JSON.stringify(config.value) // 启用配置
const data = JSON.stringify(enableConfig.value)
localStorage.setItem('MP_DASHBOARD', data) localStorage.setItem('MP_DASHBOARD', data)
// 顺序配置,从dashboardConfigs中提取
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id })))
localStorage.setItem('MP_DASHBOARD_ORDER', order)
// 保存到服务端 // 保存到服务端
api.post('/user/config/Dashboard', data, { try {
headers: { api.post('/user/config/Dashboard', data, {
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
}) },
})
api.post('/user/config/DashboardOrder', order, {
headers: {
'Content-Type': 'application/json',
},
})
} catch (error) {
console.error(error)
}
dialog.value = false dialog.value = false
} }
@@ -83,14 +176,10 @@ async function getDashboardPlugins() {
// 只有超级用户才能获取插件仪表板 // 只有超级用户才能获取插件仪表板
if (!superUser) return if (!superUser) return
try { try {
dashboard_plugins.value = await api.get('/plugin/dashboards') dashboardPlugins.value = await api.get('/plugin/dashboards')
if (!isNullOrEmptyObject(dashboard_plugins.value)) { if (!isNullOrEmptyObject(dashboardPlugins.value)) {
// 获取id和name补充到 dashboard_names 中
dashboard_plugins.value.forEach((plugin: { [key: string]: string }) => {
dashboard_names.value[plugin.id] = plugin.name
})
// 下载插件仪表板配置 // 下载插件仪表板配置
dashboard_plugins.value.forEach(async (plugin: { id: string }) => { dashboardPlugins.value.forEach(async (plugin: { id: string }) => {
await getPluginDashboard(plugin.id) await getPluginDashboard(plugin.id)
}) })
} }
@@ -104,9 +193,10 @@ async function getPluginDashboard(id: string) {
try { try {
api.get(`/plugin/dashboard/${id}`).then((res: any) => { api.get(`/plugin/dashboard/${id}`).then((res: any) => {
if (res) { if (res) {
plugin_dashboards.value.push(res) // 保存到仪表板配置中
dashboardConfigs.value.push(res)
// 定时刷新
if (res.attrs?.refresh) { if (res.attrs?.refresh) {
// 定时刷新
setTimeout(() => { setTimeout(() => {
getPluginDashboard(id) getPluginDashboard(id)
}, res.attrs.refresh * 1000) }, res.attrs.refresh * 1000)
@@ -118,58 +208,36 @@ async function getPluginDashboard(id: string) {
} }
} }
onMounted(() => { // 拖动排序结束
function dragOrderEnd() {
// 保存数据
setDashboardConfig()
}
onBeforeMount(async () => {
await loadDashboardConfig()
getDashboardPlugins() getDashboardPlugins()
}) })
</script> </script>
<template> <template>
<!-- 底部操作按钮 -->
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
<!-- 仪表板 --> <!-- 仪表板 -->
<VRow class="match-height"> <draggable
<!-- 系统内置的仪表板 --> v-model="dashboardConfigs"
<VCol v-if="config.storage" cols="12" md="4"> @end="dragOrderEnd"
<AnalyticsStorage /> item-key="id"
</VCol> tag="VRow"
<VCol v-if="config.mediaStatistic" cols="12" md="8"> :component-data="{ 'class': 'match-height' }"
<AnalyticsMediaStatistic /> >
</VCol> <template #item="{ element }">
<VCol v-if="config.weeklyOverview" cols="12" md="4"> <VCol v-if="enableConfig[element.id]" v-bind:="element.cols">
<AnalyticsWeeklyOverview /> <DashboardElement :config="element" />
</VCol>
<VCol v-if="config.speed" cols="12" md="4">
<AnalyticsSpeed />
</VCol>
<VCol v-if="config.scheduler" cols="12" md="4">
<AnalyticsScheduler />
</VCol>
<VCol v-if="config.cpu" cols="12" md="6">
<AnalyticsCpu />
</VCol>
<VCol v-if="config.memory" cols="12" md="6">
<AnalyticsMemory />
</VCol>
<VCol v-if="config.library" cols="12">
<MediaServerLibrary />
</VCol>
<VCol v-if="config.playing" cols="12">
<MediaServerPlaying />
</VCol>
<VCol v-if="config.latest" cols="12">
<MediaServerLatest />
</VCol>
<!-- 插件仪表板 -->
<template v-for="plugin in plugin_dashboards" :key="plugin.id">
<VCol v-if="config[plugin.id]" v-bind="plugin.cols">
<VCard :title="plugin.name">
<VCardItem>
<DashboardRender v-for="(item, index) in plugin.elements" :key="index" :config="item" />
</VCardItem>
</VCard>
</VCol> </VCol>
</template> </template>
</VRow> </draggable>
<!-- 底部操作按钮 -->
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
<!-- 弹窗根据配置生成选项 --> <!-- 弹窗根据配置生成选项 -->
<VDialog v-model="dialog" max-width="35rem" scrollable :fullscreen="!display.mdAndUp.value"> <VDialog v-model="dialog" max-width="35rem" scrollable :fullscreen="!display.mdAndUp.value">
@@ -180,8 +248,8 @@ onMounted(() => {
<VDivider /> <VDivider />
<VCardText> <VCardText>
<VRow> <VRow>
<VCol v-for="(name, key) in dashboard_names" :key="key" cols="12" md="4" sm="4"> <VCol v-for="item in dashboardConfigs" :key="item.id" cols="12" md="4" sm="4">
<VCheckbox v-model="config[key]" :label="name" /> <VCheckbox v-model="enableConfig[item.id]" :label="item.name" />
</VCol> </VCol>
</VRow> </VRow>
</VCardText> </VCardText>
-21
View File
@@ -70,25 +70,6 @@ const fetchOTP = debounce(async () => {
}) })
}, 500) }, 500)
// 加载用户监控面板配置
async function loadDashboardConfig() {
const response = await api.get('/user/config/Dashboard')
if (response && response.data && response.data.value) {
const data = JSON.stringify(response.data.value)
if (data != localStorage.getItem('MP_DASHBOARD')) {
localStorage.setItem('MP_DASHBOARD', data)
}
}
}
// 尝试加载用户监控面板配置(本地无配置时才加载)
async function tryLoadDashboardConfig() {
if (localStorage.getItem('MP_DASHBOARD')) {
return
}
await loadDashboardConfig()
}
// 获取用户主题配置 // 获取用户主题配置
async function fetchThemeConfig() { async function fetchThemeConfig() {
const response = await api.get('/user/config/theme') const response = await api.get('/user/config/theme')
@@ -111,8 +92,6 @@ async function setTheme() {
async function afterLogin() { async function afterLogin() {
// 生效主题配置 // 生效主题配置
await setTheme() await setTheme()
// 尝试加载用户监控面板配置(本地无配置时才加载)
await tryLoadDashboardConfig()
// 跳转到首页或回原始页面 // 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/') router.push(store.state.auth.originalPath ?? '/')
} }
+76 -74
View File
@@ -6,8 +6,14 @@ import api from '@/api'
const vuetifyTheme = useTheme() const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors) const currentTheme = controlledComputed(
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables) () => vuetifyTheme.name.value,
() => vuetifyTheme.current.value.colors,
)
const variableTheme = controlledComputed(
() => vuetifyTheme.name.value,
() => vuetifyTheme.current.value.variables,
)
// 定时器 // 定时器
let refreshTimer: NodeJS.Timer | null = null let refreshTimer: NodeJS.Timer | null = null
@@ -22,83 +28,86 @@ const series = ref([
// 当前值 // 当前值
const current = ref(0) const current = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => { const chartOptions = controlledComputed(
return { () => vuetifyTheme.name.value,
chart: { () => {
parentHeightOffset: 0, return {
toolbar: { show: false }, chart: {
animations: { enabled: false }, parentHeightOffset: 0,
}, toolbar: { show: false },
tooltip: { enabled: false }, animations: { enabled: false },
grid: { },
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`, tooltip: { enabled: false },
strokeDashArray: 6, grid: {
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
variableTheme.value['border-opacity']
})`,
strokeDashArray: 6,
xaxis: {
lines: { show: false },
},
yaxis: {
lines: { show: true },
},
padding: {
top: -10,
left: -7,
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
xaxis: { xaxis: {
lines: { show: false }, labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
}, },
yaxis: { yaxis: {
lines: { show: true }, labels: { show: false },
max: 100,
}, },
padding: { }
top: -10, },
left: -7, )
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: {
labels: { show: false },
max: 100,
},
}
})
// 调用API接口获取最新CPU使用率 // 调用API接口获取最新CPU使用率
async function getCpuUsage() { async function getCpuUsage() {
try { try {
// 请求数据 // 请求数据
current.value = await api.get('dashboard/cpu') ?? 0 current.value = (await api.get('dashboard/cpu')) ?? 0
// 添加到序列 // 添加到序列
series.value[0].data.push(current.value) series.value[0].data.push(current.value)
// 序列超过30条记录时,清掉前面的 // 序列超过30条记录时,清掉前面的
if (series.value[0].data.length > 30) if (series.value[0].data.length > 30) series.value[0].data.shift()
series.value[0].data.shift() } catch (e) {
}
catch (e) {
console.log(e) console.log(e)
} }
} }
onMounted(() => { onMounted(() => {
getCpuUsage()// 启动定时器 getCpuUsage() // 启动定时器
refreshTimer = setInterval(() => { refreshTimer = setInterval(() => {
getCpuUsage() getCpuUsage()
}, 2000) }, 2000)
@@ -115,20 +124,13 @@ onUnmounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem>
<VCardTitle class="cursor-move">CPU</VCardTitle>
</VCardItem>
<VCardText> <VCardText>
<h6 class="text-h6"> <VueApexCharts type="line" :options="chartOptions" :series="series" :height="150" />
CPU
</h6>
<VueApexCharts
type="line"
:options="chartOptions"
:series="series"
:height="150"
/>
<p class="text-center font-weight-medium mb-0"> <p class="text-center font-weight-medium mb-0">当前{{ current }}%</p>
当前{{ current }}%
</p>
</VCardText> </VCardText>
</VCard> </VCard>
</template> </template>
@@ -42,8 +42,7 @@ async function loadMediaStatistic() {
color: 'info', color: 'info',
}, },
] ]
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -56,29 +55,16 @@ onMounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>媒体统计</VCardTitle> <VCardTitle class="cursor-move">媒体统计</VCardTitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<VRow> <VRow>
<VCol <VCol v-for="item in statistics" :key="item.title" cols="6" sm="3">
v-for="item in statistics"
:key="item.title"
cols="6"
sm="3"
>
<div class="d-flex align-center"> <div class="d-flex align-center">
<div class="me-3"> <div class="me-3">
<VAvatar <VAvatar :color="item.color" rounded size="42" class="elevation-1">
:color="item.color" <VIcon size="24" :icon="item.icon" />
rounded
size="42"
class="elevation-1"
>
<VIcon
size="24"
:icon="item.icon"
/>
</VAvatar> </VAvatar>
</div> </div>
+78 -76
View File
@@ -7,8 +7,14 @@ import { formatBytes } from '@/@core/utils/formatters'
const vuetifyTheme = useTheme() const vuetifyTheme = useTheme()
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors) const currentTheme = controlledComputed(
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables) () => vuetifyTheme.name.value,
() => vuetifyTheme.current.value.colors,
)
const variableTheme = controlledComputed(
() => vuetifyTheme.name.value,
() => vuetifyTheme.current.value.variables,
)
// 定时器 // 定时器
let refreshTimer: NodeJS.Timer | null = null let refreshTimer: NodeJS.Timer | null = null
@@ -25,79 +31,82 @@ const usedMemory = ref(0)
// 内存使用百分比 // 内存使用百分比
const memoryUsage = ref(0) const memoryUsage = ref(0)
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => { const chartOptions = controlledComputed(
return { () => vuetifyTheme.name.value,
chart: { () => {
parentHeightOffset: 0, return {
toolbar: { show: false }, chart: {
animations: { enabled: false }, parentHeightOffset: 0,
}, toolbar: { show: false },
tooltip: { enabled: false }, animations: { enabled: false },
grid: { },
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`, tooltip: { enabled: false },
strokeDashArray: 6, grid: {
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${
variableTheme.value['border-opacity']
})`,
strokeDashArray: 6,
xaxis: {
lines: { show: false },
},
yaxis: {
lines: { show: true },
},
padding: {
top: -10,
left: -7,
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
dataLabels: {
enabled: false,
},
xaxis: { xaxis: {
lines: { show: false }, labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
}, },
yaxis: { yaxis: {
lines: { show: true }, labels: { show: false },
max: 100,
}, },
padding: { }
top: -10, },
left: -7, )
right: 5,
bottom: 5,
},
},
stroke: {
width: 3,
lineCap: 'butt',
curve: 'smooth',
},
colors: [currentTheme.value.primary],
markers: {
size: 6,
offsetY: 4,
offsetX: -2,
strokeWidth: 3,
colors: ['transparent'],
strokeColors: 'transparent',
discrete: [
{
size: 5.5,
seriesIndex: 0,
strokeColor: currentTheme.value.primary,
fillColor: currentTheme.value.surface,
},
],
hover: { size: 7 },
},
dataLabels: {
enabled: false,
},
xaxis: {
labels: { show: false },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: {
labels: { show: false },
max: 100,
},
}
})
// 调用API接口获取最新内存使用量 // 调用API接口获取最新内存使用量
async function getMemorgUsage() { async function getMemorgUsage() {
try { try {
// 请求数据 // 请求数据
[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory') ;[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
series.value[0].data.push(memoryUsage.value) series.value[0].data.push(memoryUsage.value)
// 序列超过30条记录时,清掉前面的 // 序列超过30条记录时,清掉前面的
if (series.value[0].data.length > 30) if (series.value[0].data.length > 30) series.value[0].data.shift()
series.value[0].data.shift() } catch (e) {
}
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -121,20 +130,13 @@ onUnmounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem>
<VCardTitle class="cursor-move">内存</VCardTitle>
</VCardItem>
<VCardText> <VCardText>
<h6 class="text-h6"> <VueApexCharts type="area" :options="chartOptions" :series="series" :height="150" />
内存
</h6>
<VueApexCharts
type="area"
:options="chartOptions"
:series="series"
:height="150"
/>
<p class="text-center font-weight-medium mb-0"> <p class="text-center font-weight-medium mb-0">当前{{ formatBytes(usedMemory) }}</p>
当前{{ formatBytes(usedMemory) }}
</p>
</VCardText> </VCardText>
</VCard> </VCard>
</template> </template>
+11 -30
View File
@@ -18,8 +18,7 @@ async function loadProcessList() {
const res: Process[] = await api.get('dashboard/processes') const res: Process[] = await api.get('dashboard/processes')
processList.value = res processList.value = res
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -43,47 +42,29 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<VCard title="系统进程"> <VCard>
<VTable <VCardItem>
item-key="fullName" <VCardTitle class="cursor-move">系统进程</VCardTitle>
class="table-rounded" </VCardItem>
hide-default-footer <VTable item-key="fullName" class="table-rounded" hide-default-footer disable-sort>
disable-sort
>
<thead> <thead>
<tr> <tr>
<th <th v-for="header in headers" :id="header" :key="header">
v-for="header in headers"
:id="header"
:key="header"
>
{{ header }} {{ header }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="row in processList" :key="row.pid">
v-for="row in processList" <td class="text-sm" v-text="row.pid" />
:key="row.pid"
>
<td
class="text-sm"
v-text="row.pid"
/>
<!-- name --> <!-- name -->
<td> <td>
<h6 class="text-sm font-weight-medium"> <h6 class="text-sm font-weight-medium">
{{ row.name }} {{ row.name }}
</h6> </h6>
</td> </td>
<td <td class="text-sm" v-text="formatSeconds(row.run_time)" />
class="text-sm" <td class="text-sm" v-text="`${row.memory} MB`" />
v-text="formatSeconds(row.run_time)"
/>
<td
class="text-sm"
v-text="`${row.memory} MB`"
/>
</tr> </tr>
</tbody> </tbody>
</VTable> </VTable>
+6 -20
View File
@@ -14,8 +14,7 @@ async function loadSchedulerList() {
const res: ScheduleInfo[] = await api.get('dashboard/schedule') const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res schedulerList.value = res
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -41,25 +40,14 @@ onUnmounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>后台任务</VCardTitle> <VCardTitle class="cursor-move">后台任务</VCardTitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<VList <VList class="card-list" height="250">
class="card-list" <VListItem v-for="item in schedulerList" :key="item.id">
height="250"
>
<VListItem
v-for="item in schedulerList"
:key="item.id"
>
<template #prepend> <template #prepend>
<VAvatar <VAvatar size="40" variant="tonal" color="" class="me-3">
size="40"
variant="tonal"
color=""
class="me-3"
>
{{ item.name[0] }} {{ item.name[0] }}
</VAvatar> </VAvatar>
</template> </template>
@@ -81,9 +69,7 @@ onUnmounted(() => {
</template> </template>
</VListItem> </VListItem>
<VListItem v-if="schedulerList.length === 0"> <VListItem v-if="schedulerList.length === 0">
<VListItemTitle class="text-center"> <VListItemTitle class="text-center"> 没有后台服务 </VListItemTitle>
没有后台服务
</VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VCardText> </VCardText>
+6 -17
View File
@@ -56,8 +56,7 @@ async function loadDownloaderInfo() {
amount: formatFileSize(res.free_space), amount: formatFileSize(res.free_space),
}, },
] ]
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -83,28 +82,18 @@ onUnmounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>实时速率</VCardTitle> <VCardTitle class="cursor-move">实时速率</VCardTitle>
</VCardItem> </VCardItem>
<VCardText class="pt-4"> <VCardText class="pt-4">
<div> <div>
<p class="text-h5 me-2"> <p class="text-h5 me-2">{{ formatFileSize(downloadInfo.upload_speed) }}/s</p>
{{ formatFileSize(downloadInfo.upload_speed) }}/s <p class="text-h4 me-2">{{ formatFileSize(downloadInfo.download_speed) }}/s</p>
</p>
<p class="text-h4 me-2">
{{ formatFileSize(downloadInfo.download_speed) }}/s
</p>
</div> </div>
<VList class="card-list mt-9"> <VList class="card-list mt-9">
<VListItem <VListItem v-for="item in infoItems" :key="item.title">
v-for="item in infoItems"
:key="item.title"
>
<template #prepend> <template #prepend>
<VIcon <VIcon rounded :icon="item.avatar" />
rounded
:icon="item.avatar"
/>
</template> </template>
<VListItemTitle class="text-sm font-weight-medium mb-1"> <VListItemTitle class="text-sm font-weight-medium mb-1">
+11 -26
View File
@@ -8,9 +8,7 @@ import triangleLight from '@images/misc/triangle-light.png'
const { global } = useTheme() const { global } = useTheme()
const triangleBg = computed(() => const triangleBg = computed(() => (global.name.value === 'light' ? triangleLight : triangleDark))
global.name.value === 'light' ? triangleLight : triangleDark,
)
// 总存储空间 // 总存储空间
const storage = ref(0) const storage = ref(0)
@@ -30,8 +28,7 @@ async function getStorage() {
storage.value = res.total_storage storage.value = res.total_storage
used.value = res.used_storage used.value = res.used_storage
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -42,42 +39,30 @@ onMounted(() => {
</script> </script>
<template> <template>
<VCard <VCard>
title="存储空间" <VCardItem>
subtitle="" <VCardTitle class="cursor-move">存储空间</VCardTitle>
class="position-relative" </VCardItem>
>
<VCardText> <VCardText>
<h5 class="text-2xl font-weight-medium text-primary"> <h5 class="text-2xl font-weight-medium text-primary">
{{ formatFileSize(storage) }} {{ formatFileSize(storage) }}
</h5> </h5>
<p class="mt-2"> <p class="mt-2">已使用 {{ usedPercent }}% 🚀</p>
已使用 {{ usedPercent }}% 🚀
</p>
<p class="mt-1"> <p class="mt-1">
<VProgressLinear <VProgressLinear :model-value="usedPercent" color="primary" />
:model-value="usedPercent"
color="primary"
/>
</p> </p>
</VCardText> </VCardText>
<!-- Triangle Background --> <!-- Triangle Background -->
<VImg <VImg :src="triangleBg" class="triangle-bg flip-in-rtl" />
:src="triangleBg"
class="triangle-bg flip-in-rtl"
/>
<!-- Trophy --> <!-- Trophy -->
<VImg <VImg :src="trophy" class="trophy" />
:src="trophy"
class="trophy"
/>
</VCard> </VCard>
</template> </template>
<style lang="scss"> <style lang="scss">
@use "@layouts/styles/mixins" as layoutsMixins; @use '@layouts/styles/mixins' as layoutsMixins;
.v-card .triangle-bg { .v-card .triangle-bg {
position: absolute; position: absolute;
@@ -80,8 +80,7 @@ const options = controlledComputed(
fontSize: '12px', fontSize: '12px',
}, },
formatter: (value: number) => formatter: (value: number) => (value > 999 ? (value / 1000).toFixed(0) : value),
value > 999 ? (value / 1000).toFixed(0) : value,
}, },
}, },
} }
@@ -100,8 +99,7 @@ async function getWeeklyData() {
const res: number[] = await api.get('dashboard/transfer') const res: number[] = await api.get('dashboard/transfer')
series.value = [{ data: res }] series.value = [{ data: res }]
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -114,16 +112,11 @@ onMounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>最近入库</VCardTitle> <VCardTitle class="cursor-move">最近入库</VCardTitle>
</VCardItem> </VCardItem>
<VCardText> <VCardText>
<VueApexCharts <VueApexCharts type="bar" :options="options" :series="series" :height="160" />
type="bar"
:options="options"
:series="series"
:height="160"
/>
<div class="d-flex align-center mb-3"> <div class="d-flex align-center mb-3">
<h5 class="text-h5 me-4"> <h5 class="text-h5 me-4">
@@ -132,13 +125,7 @@ onMounted(() => {
<p>最近一周入库了 {{ totalCount }} 部影片 😎</p> <p>最近一周入库了 {{ totalCount }} 部影片 😎</p>
</div> </div>
<VBtn <VBtn v-if="superUser" block to="/history"> 查看详情 </VBtn>
v-if="superUser"
block
to="/history"
>
查看详情
</VBtn>
</VCardText> </VCardText>
</VCard> </VCard>
</template> </template>
+4 -13
View File
@@ -10,8 +10,7 @@ const latestList = ref<MediaServerPlayItem[]>([])
async function loadLatest() { async function loadLatest() {
try { try {
latestList.value = await api.get('mediaserver/latest') latestList.value = await api.get('mediaserver/latest')
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -24,19 +23,11 @@ onMounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>最近添加</VCardTitle> <VCardTitle class="cursor-move">最近添加</VCardTitle>
</VCardItem> </VCardItem>
<div <div v-if="latestList.length > 0" class="grid gap-4 grid-media-card mx-3 mb-3" tabindex="0">
v-if="latestList.length > 0" <PosterCard v-for="data in latestList" :key="data.id" :media="data" />
class="grid gap-4 grid-media-card mx-3 mb-3"
tabindex="0"
>
<PosterCard
v-for="data in latestList"
:key="data.id"
:media="data"
/>
</div> </div>
</VCard> </VCard>
</template> </template>
+4 -14
View File
@@ -10,8 +10,7 @@ const libraryList = ref<MediaServerPlayItem[]>([])
async function loadLibrary() { async function loadLibrary() {
try { try {
libraryList.value = await api.get('mediaserver/library') libraryList.value = await api.get('mediaserver/library')
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -24,20 +23,11 @@ onMounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>我的媒体库</VCardTitle> <VCardTitle class="cursor-move">我的媒体库</VCardTitle>
</VCardItem> </VCardItem>
<div <div v-if="libraryList.length > 0" class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
v-if="libraryList.length > 0" <LibraryCard v-for="data in libraryList" :key="data.id" :media="data" height="10rem" />
class="grid gap-4 grid-backdrop-card mx-3"
tabindex="0"
>
<LibraryCard
v-for="data in libraryList"
:key="data.id"
:media="data"
height="10rem"
/>
</div> </div>
</VCard> </VCard>
</template> </template>
+4 -14
View File
@@ -10,8 +10,7 @@ const playingList = ref<MediaServerPlayItem[]>([])
async function loadPlayingList() { async function loadPlayingList() {
try { try {
playingList.value = await api.get('mediaserver/playing') playingList.value = await api.get('mediaserver/playing')
} } catch (e) {
catch (e) {
console.log(e) console.log(e)
} }
} }
@@ -24,20 +23,11 @@ onMounted(() => {
<template> <template>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle>继续观看</VCardTitle> <VCardTitle class="cursor-move">继续观看</VCardTitle>
</VCardItem> </VCardItem>
<div <div v-if="playingList.length > 0" class="grid gap-4 grid-backdrop-card mx-3" tabindex="0">
v-if="playingList.length > 0" <BackdropCard v-for="data in playingList" :key="data.id" :media="data" height="10rem" />
class="grid gap-4 grid-backdrop-card mx-3"
tabindex="0"
>
<BackdropCard
v-for="data in playingList"
:key="data.id"
:media="data"
height="10rem"
/>
</div> </div>
</VCard> </VCard>
</template> </template>
+12
View File
@@ -6407,6 +6407,11 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0" astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
sortablejs@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0: "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
@@ -7496,6 +7501,13 @@ vue@^3.3.2:
"@vue/server-renderer" "3.4.21" "@vue/server-renderer" "3.4.21"
"@vue/shared" "3.4.21" "@vue/shared" "3.4.21"
vuedraggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
dependencies:
sortablejs "1.14.0"
vuetify-use-dialog@^0.6.11: vuetify-use-dialog@^0.6.11:
version "0.6.11" version "0.6.11"
resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.11.tgz#8800cc56b234dae1dfa44a7f06a6bb1a33ad4b39" resolved "https://registry.yarnpkg.com/vuetify-use-dialog/-/vuetify-use-dialog-0.6.11.tgz#8800cc56b234dae1dfa44a7f06a6bb1a33ad4b39"