stats appcard page

v3
veypi 6 months ago
parent 2c0326d75d
commit 127f5ed463

@ -9,4 +9,6 @@
### 依赖库 ### 依赖库
```bash ```bash
docker run -dit --name=tsdb -v /Users/veypi/test/vdb:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics -search.latencyOffset=1s
nats-server -c ./script/nats.cfg
``` ```

@ -3,10 +3,10 @@
<NuxtLayout> <NuxtLayout>
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
<div id='v-msg'></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
let app = useAppConfig() let app = useAppConfig()
onMounted(() => { onMounted(() => {

@ -0,0 +1,77 @@
<!--
* appcard.vue
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-06-07 16:48
* Distributed under terms of the MIT license.
-->
<template>
<div class="core rounded-2xl p-3">
<div class="grid gap-4 grid-cols-5">
<div class="col-span-2">
<img :src="core.icon">
</div>
<div class="col-span-3 grid grid-cols-1 items-center text-left">
<div class="truncate h-10 flex items-center text-xl italic font-bold">
{{ core.name }}
</div>
<span class="truncate">{{ }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import msg from "@veypi/msg";
const router = useRouter()
let props = withDefaults(defineProps<{
core: modelsApp,
is_part: boolean
}>(),
{}
)
const u = useUserStore()
function Go() {
if (props.is_part) {
router.push({ name: "app.home", params: { id: props.core.id } });
return
}
// $q.dialog({
// title: '',
// message: ' ' + props.core.name,
// cancel: true,
// }).onOk(() => {
api.app.user(props.core.id).add(u.id).then(e => {
switch (e.status) {
case AUStatus.OK:
msg.Info('加入成功')
router.push({ name: "app.home", params: { id: props.core.id } });
return;
case AUStatus.Applying:
msg.Info("请等待管理员审批进入");
return;
case AUStatus.Deny:
msg.Warn("进入申请未通过");
return;
case AUStatus.Disabled:
msg.Warn("已被禁止使用");
return;
}
}).catch(e => {
msg.Warn("加入失败" + e)
})
}
</script>
<style scoped>
.core {
width: 256px;
background: rgba(146, 145, 145, 0.1);
}
</style>

@ -7,7 +7,7 @@
<template> <template>
<div class="select-none items flex flex-col"> <div class="select-none items flex flex-col">
<template v-for="(v, i) in list"> <template v-for="(v, i) in list">
<div class="item flex items-center justify-center gap-2 py-4" :active='v.path === route.fullPath' <div class="item flex items-center justify-center gap-1 py-4" :active='v.path === route.fullPath'
@click="$router.push(v.path)"> @click="$router.push(v.path)">
<slot :name="'L' + i" @click='$router.push(v.path)'> <slot :name="'L' + i" @click='$router.push(v.path)'>
<div class='ico' v-if="show_ico"> <div class='ico' v-if="show_ico">

@ -0,0 +1,11 @@
/*
* index.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-22 05:11
* Distributed under terms of the MIT license.
*/
import tschart from './tschart.vue'
export default tschart

@ -0,0 +1,74 @@
/*
* params.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-22 05:13
* Distributed under terms of the MIT license.
*/
const use_params = () => {
let mode = ref(0)
let mode_label = ['近5分钟', '近1小时', '近24小时', '近7天', '近30天']
let change_mode = (m: number) => {
mode.value = m
let now = new Date()
switch (m) {
case 0: {
now.setMinutes(now.getMinutes() - 5)
params.value.start = now
params.value.step = "2s"
break
}
case 1: {
now.setHours(now.getHours() - 1)
params.value.start = now
params.value.step = "10s"
break
}
case 2: {
now.setHours(now.getHours() - 24)
params.value.start = now
params.value.step = "20s"
break
}
case 3: {
now.setHours(now.getHours() - 24 * 7)
params.value.start = now
params.value.step = "30s"
break
}
case 4: {
now.setHours(now.getHours() - 24 * 29)
params.value.start = now
params.value.step = "1h"
break
}
case 5: {
break
}
}
}
let params = ref<{ start: Date, end: Date, step: string }>({
start: new Date(),
end: new Date(),
step: '2s'
})
change_mode(0)
const set_delta = (start?: Date, end?: Date) => {
if (start) {
params.value.start = start
}
if (end) {
params.value.end = end
}
let delta = params.value.end.getTime() -
params.value.start.getTime()
console.log(delta)
}
return { params, change_mode, mode, mode_label }
}
export default use_params

@ -0,0 +1,216 @@
<!--
* tschart.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-20 22:50
* Distributed under terms of the MIT license.
-->
<template>
<div class="w-full h-full">
<div v-if="enable_mode" class="h-16 flex justify-start items-center">
<div :color="enable_sync ? 'primary' : ''" @click="enable_sync = !enable_sync">{{ enable_sync ? '关闭同步'
:
'开启同步' }}</div>
<div :color="mode === k ? 'primary' : ''" v-for="(v, k) in mode_label" :key="k" @click="change_mode(k)">{{
v }}</div>
</div>
<div class="v-chart w-full" :style="{
height:
enable_mode ? 'calc(100% - 4rem)' : '100%'
}" ref="chartdom"></div>
</div>
</template>
<script lang="ts" setup>
import * as echart from 'echarts'
import use_params from './params'
import { onMounted, onUnmounted, computed, ref, watch, markRaw } from 'vue';
let { params, mode, change_mode, mode_label } = use_params()
interface Item {
name: string
query: string | string[]
valueFormatter?: (s: number) => string
label?: string | string[] | ((s: any) => string)
}
let props = withDefaults(defineProps<{
item: Item,
sync?: boolean,
enable_zoom?: boolean,
time_mode?: number,
enable_mode?: boolean,
}>(),
{
}
)
let count = 0
let timer = ref<any[]>([])
let enable_sync = ref(false)
let chartdom = ref()
let options = ref<{ [key: string]: any }>({})
let chart: echart.ECharts = {} as any
let tooltip = {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {},
},
valueFormatter: (value: number) => value.toFixed(2),
className: 'v-echarts-tooltip',
}
const init_chart = () => {
count++
if (chart.clear) {
chart.clear()
}
timer.value.forEach(e => {
clearInterval(e)
})
options.value = {
title: { text: props.item.name, x: 'center' },
animationThreshold: 200,
tooltip: Object.assign({}, tooltip),
axisPointer: {
link: { xAxisIndex: 'all' },
label: {
backgroundColor: '#777'
}
},
xAxis: {
type: 'time',
},
yAxis: {},
series: []
}
if (props.enable_zoom) {
options.value.dataZoom = [
{
type: 'slider',
xAxisIndex: [0],
filterMode: 'filter'
},
]
}
if (props.item.valueFormatter) {
options.value.tooltip.valueFormatter = props.item.valueFormatter
}
let tmp = {
start: params.value.start.toISOString(),
step: params.value.step,
}
let querys: string[] = Array.isArray(props.item.query) ? props.item.query :
[props.item.query]
let labels = Array.isArray(props.item.label) ? props.item.label :
[props.item.label]
for (let q = 0; q < querys.length; q++) {
let query = querys[q]
api.tsdb.range(query, tmp).then(e => {
if (e.status == 'success') {
let data = e.data.result as any[]
if (data.length == 0) {
console.warn('not get data')
return
}
let idx = options.value.series.length || 0
data.forEach(d => {
let name = props.item.name
let label = labels[q]
if (typeof label === 'string') {
name = label
} else if (typeof label === 'function') {
name = label(d.metric)
}
options.value.series.push({
name: name,
data: d.values.map((e: any) =>
[e[0] * 1000, Number(e[1])]),
metric: d.metric,
metric_str: JSON.stringify(d.metric),
origin: query,
symbol: 'none',
smooth: true,
type: 'line',
})
})
chart.setOption(options.value)
let t = setInterval(() => {
sync_chart(idx, query, count)
}, 1000)
timer.value.push(t)
}
})
}
// let query = props.query
}
const sync_chart = (idx: number, query: string, c: number) => {
if (!enable_sync.value) {
return
}
api.tsdb.query(query).then(e => {
if (e.status == 'success') {
let data = e.data.result as any[]
if (data.length == 0) {
console.warn('not get data')
timer.value.forEach(e => {
clearInterval(e)
})
return
}
if (count === c) {
data.forEach((d, i) => {
let sidx = idx + i
if (d.metric) {
let ti = options.value.series.findIndex((s: any) =>
query === s.origin && JSON.stringify(d.metric) === s.metric_str)
if (ti >= 0) {
sidx = ti
}
}
options.value.series[sidx].data.push([d.value[0] * 1000,
Number(d.value[1])])
})
chart.setOption(options.value)
}
}
})
}
watch(computed(() => props.item), q => {
if (q) {
init_chart()
}
})
watch(mode, q => {
init_chart()
})
onMounted(() => {
enable_sync.value = props.sync
if (props.time_mode) {
change_mode(props.time_mode)
}
chart = markRaw(echart.init(chartdom.value, null, { renderer: 'svg' }))
init_chart()
})
onUnmounted(() => {
timer.value.forEach(e => {
clearInterval(e)
})
})
</script>
<style>
.v-chart {
min-width: 20rem;
min-height: 15rem;
}
.v-echarts-tooltip {
/* height: 5rem; */
/* width: 10rem; */
}
</style>

@ -17,18 +17,18 @@
<OneIcon class="mx-2" @click="toggle_theme"> <OneIcon class="mx-2" @click="toggle_theme">
{{ app.layout.theme === '' ? 'light' : 'dark' }} {{ app.layout.theme === '' ? 'light' : 'dark' }}
</OneIcon> </OneIcon>
<OAer v-if="user.ready" @logout="user.logout" :is-dark="app.layout.theme !== ''"> <OAer class="mx-2" v-if="user.ready" @logout="user.logout" :is-dark="app.layout.theme !== ''">
</OAer> </OAer>
</div> </div>
<div class="menu"> <div class="menu">
<Menu :show_name="menu_mode === 2" :list="menu"></Menu> <Menu :show_name="menu_mode === 2" :list="menu"></Menu>
</div> </div>
<div class="menu-hr"></div> <div class="menu-hr"></div>
<div class="main"> <div class="main px-8 py-6">
<slot /> <slot />
</div> </div>
<div class="footer flex justify-around items-center"> <div class="footer flex justify-around items-center">
<div @click="toggle_menu(0)">© 2024 veypi</div> <div @click="util.goto('https://veypi.com')">© 2024 veypi</div>
<div>使用说明</div> <div>使用说明</div>
<div>联系我们</div> <div>联系我们</div>
</div> </div>
@ -60,7 +60,8 @@ let menu = ref([
{ ico: 'home', name: '应用中心', path: '/' }, { ico: 'home', name: '应用中心', path: '/' },
{ ico: 'user', name: '用户设置', path: '/user' }, { ico: 'user', name: '用户设置', path: '/user' },
{ ico: 'file-exception', name: '文档中心', path: '/docs' }, { ico: 'file-exception', name: '文档中心', path: '/docs' },
{ ico: 'setting', name: '系统设置', path: '/settings' }, { ico: 'setting', name: '应用统计', path: '/stats' },
{ ico: 'setting', name: '系统设置', path: '/setting' },
]) ])
if (!util.checkLogin()) { if (!util.checkLogin()) {
router.push('/login') router.push('/login')
@ -75,9 +76,10 @@ let toggle_menu = (m: 0 | 1 | 2) => {
} else if (m == 1) { } else if (m == 1) {
app.layout.menu_width = 40 app.layout.menu_width = 40
} else { } else {
app.layout.menu_width = 100 app.layout.menu_width = 108
} }
} }
toggle_menu(2)
const toggle_fullscreen = () => { const toggle_fullscreen = () => {
app.layout.fullscreen = !app.layout.fullscreen app.layout.fullscreen = !app.layout.fullscreen

@ -14,6 +14,17 @@ export default defineNuxtConfig({
"/api": { "/api": {
target: "http://127.0.0.1:4001/api", target: "http://127.0.0.1:4001/api",
changeOrigin: true, changeOrigin: true,
ws: true,
},
'/fs': {
target: 'http://127.0.0.1:4001/fs',
changeOrigin: true,
ws: true,
},
'/media': {
target: 'http://127.0.0.1:4001/media',
changeOrigin: true,
ws: true,
}, },
}, },
}, },
@ -34,7 +45,7 @@ export default defineNuxtConfig({
script: [ script: [
{ src: '/icon.js' }, { src: '/icon.js' },
] ]
} },
}, },
postcss: { postcss: {
@ -45,4 +56,4 @@ export default defineNuxtConfig({
}, },
modules: ["@pinia/nuxt"] modules: ["@pinia/nuxt"]
}) })

10692
oaweb/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -11,10 +11,11 @@
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@veypi/msg": "^0.1.1", "@veypi/msg": "^0.1.4",
"@veypi/oaer": "^0.2.2", "@veypi/oaer": "^0.2.4",
"@veypi/one-icon": "^2.0.6", "@veypi/one-icon": "^2.0.6",
"axios": "^1.7.2", "axios": "^1.7.2",
"echarts": "^5.5.0",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"nuxt": "^3.11.2", "nuxt": "^3.11.2",
"vue": "^3.4.27", "vue": "^3.4.27",

@ -18,7 +18,7 @@
</div> </div>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k"> <div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k">
<!-- <AppCard :core="item" :is_part="true"></AppCard> --> <Appcard :core="item" :is_part="true"></Appcard>
</div> </div>
</div> </div>
</div> </div>
@ -26,7 +26,7 @@
<h1 class="page-h1">应用中心</h1> <h1 class="page-h1">应用中心</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k"> <div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
<!-- <AppCard :core="item" :is_part="false"></AppCard> --> <Appcard :core="item" :is_part="false"></Appcard>
</div> </div>
</div> </div>
</div> </div>
@ -62,7 +62,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue';
import msg from '@veypi/msg'; import msg from '@veypi/msg';
let user = useUserStore() let user = useUserStore()

@ -0,0 +1,89 @@
<!--
* stats.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-20 22:56
* Distributed under terms of the MIT license.
-->
<template>
<div>
<div class="page-h1">
服务
</div>
<div class="w-40 text-center py-4 start_card">
<div class="text-3xl"> 已运行 </div>
<div class="text-2xl mt-2">
{{ start_time }}
</div>
</div>
<div class="flex flex-nowrap" style="">
<div class="w-1/2">
<Tschart :item="querys[0]" :time_mode="1"></Tschart>
</div>
<div class="w-1/2">
<Tschart :item="querys[1]" :time_mode="1"></Tschart>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, onMounted } from 'vue';
// import tschart from 'src/components/tschart';
const start_time = ref('')
const timer = ref()
const querys = ref<{
name: string, query: string[] | string, label?: any,
valueFormatter?: any
}[]>([
{
name: 'cpu',
query: `srv_cpu{i='oa'}`,
label: 'cpu',
valueFormatter: (value: number) => value.toFixed(2) + "%",
},
{
name: '内存',
query: `srv_mem{i='oa'} / 1048576`,
label: '内存',
valueFormatter: (value: number) => value.toFixed(2) + "MB",
},
])
onMounted(() => {
api.tsdb.query('srv_start{i="oa"}').then(e => {
if (e.data.result.length) {
let s = Number(e.data.result[0].value[1])
if (s < 60) {
start_time.value = s + ' 秒'
} else if (s < 3600) {
start_time.value = (s / 60).toFixed(1) + ' 分钟'
} else if (s < 3600 * 24) {
start_time.value = (s / 60 / 60).toFixed(1) + ' 小时'
} else {
start_time.value = (s / 60 / 60 / 24).toFixed(1) + ' 天'
}
}
})
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
<style lang="scss" scoped>
.start_card {
border: 1px solid var(--color-primary);
:first-child {
color: var(--color-primary)
}
:nth-child(2) {}
}
</style>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save