Initial commit
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Frontend development
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.cache/
|
||||||
|
.vite/
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Frontend build
|
||||||
|
/static/
|
||||||
|
|
||||||
|
# Backend development
|
||||||
|
/lib/vendor/
|
||||||
|
coverage/
|
||||||
|
phpunit.xml.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.phpstan.cache
|
||||||
|
.phpactor/
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
9
composer.json
Normal file
9
composer.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "ktxm/dashboard",
|
||||||
|
"type": "project",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"KTXM\\Dashboard\\": "lib/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/Module.php
Normal file
65
lib/Module.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXM\Dashboard;
|
||||||
|
|
||||||
|
use KTXF\Module\ModuleBrowserInterface;
|
||||||
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard Module
|
||||||
|
*/
|
||||||
|
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): string
|
||||||
|
{
|
||||||
|
return 'Ktrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'User dashboard module for Ktrix - provides widgets, user management, and system overview';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function version(): string
|
||||||
|
{
|
||||||
|
return '1.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dashboard' => [
|
||||||
|
'label' => 'Access Dashboard',
|
||||||
|
'description' => 'View and access the dashboard module',
|
||||||
|
'group' => 'Dashboard'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerBI(): array {
|
||||||
|
return [
|
||||||
|
'handle' => $this->handle(),
|
||||||
|
'namespace' => 'Dashboard',
|
||||||
|
'version' => $this->version(),
|
||||||
|
'label' => $this->label(),
|
||||||
|
'author' => $this->author(),
|
||||||
|
'description' => $this->description(),
|
||||||
|
'boot' => 'static/module.mjs',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1800
package-lock.json
generated
Normal file
1800
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"description": "Ktrix Dashboard Module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"author": "Sebastian Krupinski",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --mode production --config vite.config.ts",
|
||||||
|
"dev": "vite build --mode development --config vite.config.ts",
|
||||||
|
"watch": "vite build --mode development --watch --config vite.config.ts",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
|
"pinia": "^2.3.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-echarts": "^8.0.0",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"vuetify": "^3.7.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"sass": "^1.77.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6",
|
||||||
|
"vite-plugin-vuetify": "2.0.4",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/components/ChartBar.vue
Normal file
90
src/components/ChartBar.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
|
import type { ComposeOption } from 'echarts/core'
|
||||||
|
import type { BarSeriesOption } from 'echarts/charts'
|
||||||
|
import type { GridComponentOption, TooltipComponentOption, TitleComponentOption } from 'echarts/components'
|
||||||
|
|
||||||
|
type ECOption = ComposeOption<
|
||||||
|
BarSeriesOption | GridComponentOption | TooltipComponentOption | TitleComponentOption
|
||||||
|
>
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const option: ECOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 10,
|
||||||
|
left: '2%',
|
||||||
|
right: '2%',
|
||||||
|
bottom: '3%',
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'pageA',
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'vistors',
|
||||||
|
barWidth: '60%',
|
||||||
|
data: [79, 52, 200, 334, 390, 330, 220],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pageB',
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'vistors',
|
||||||
|
barWidth: '60%',
|
||||||
|
data: [80, 52, 200, 334, 390, 330, 220],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pageC',
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'vistors',
|
||||||
|
barWidth: '60%',
|
||||||
|
data: [30, 52, 200, 334, 390, 330, 220],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Simple delay to let DOM settle, then render
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('ChartBar: rendering chart')
|
||||||
|
isReady.value = true
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="chart-container">
|
||||||
|
<v-chart
|
||||||
|
v-if="isReady"
|
||||||
|
:option="option"
|
||||||
|
:style="{ width: '100%', height: '100%' }"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
123
src/components/ChartLine.vue
Normal file
123
src/components/ChartLine.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
|
import type { ComposeOption } from 'echarts/core'
|
||||||
|
import type { LineSeriesOption } from 'echarts/charts'
|
||||||
|
import type { GridComponentOption, TooltipComponentOption, VisualMapComponentOption } from 'echarts/components'
|
||||||
|
|
||||||
|
type ECOption = ComposeOption<
|
||||||
|
LineSeriesOption | GridComponentOption | TooltipComponentOption | VisualMapComponentOption
|
||||||
|
>
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
['2022-06-05', 116],
|
||||||
|
['2022-06-06', 129],
|
||||||
|
['2022-06-07', 135],
|
||||||
|
['2022-06-08', 86],
|
||||||
|
['2022-06-09', 73],
|
||||||
|
['2022-06-10', 85],
|
||||||
|
['2022-06-11', 73],
|
||||||
|
['2022-06-12', 68],
|
||||||
|
['2022-06-13', 92],
|
||||||
|
['2022-06-14', 130],
|
||||||
|
['2022-06-15', 245],
|
||||||
|
['2022-06-16', 139],
|
||||||
|
['2022-06-17', 115],
|
||||||
|
['2022-06-18', 111],
|
||||||
|
['2022-06-19', 309],
|
||||||
|
['2022-06-20', 206],
|
||||||
|
['2022-06-21', 137],
|
||||||
|
['2022-06-22', 128],
|
||||||
|
['2022-06-23', 85],
|
||||||
|
['2022-06-24', 94],
|
||||||
|
['2022-06-25', 71],
|
||||||
|
['2022-06-26', 106],
|
||||||
|
['2022-06-27', 84],
|
||||||
|
['2022-06-28', 93],
|
||||||
|
['2022-06-29', 85],
|
||||||
|
['2022-06-30', 73],
|
||||||
|
['2022-07-01', 83],
|
||||||
|
['2022-07-02', 125],
|
||||||
|
['2022-07-03', 107],
|
||||||
|
['2022-07-04', 82],
|
||||||
|
['2022-07-05', 44],
|
||||||
|
['2022-07-06', 72],
|
||||||
|
['2022-07-07', 106],
|
||||||
|
['2022-07-08', 107],
|
||||||
|
['2022-07-09', 66],
|
||||||
|
['2022-07-10', 91],
|
||||||
|
['2022-07-11', 92],
|
||||||
|
['2022-07-12', 113],
|
||||||
|
['2022-07-13', 107],
|
||||||
|
['2022-07-14', 131],
|
||||||
|
['2022-07-15', 111],
|
||||||
|
['2022-07-16', 64],
|
||||||
|
['2022-07-17', 69],
|
||||||
|
['2022-07-18', 88],
|
||||||
|
['2022-07-19', 77],
|
||||||
|
['2022-07-20', 83],
|
||||||
|
['2022-07-21', 111],
|
||||||
|
['2022-07-22', 57],
|
||||||
|
['2022-07-23', 55],
|
||||||
|
['2022-07-24', 60],
|
||||||
|
]
|
||||||
|
|
||||||
|
const option: ECOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
dataset: { source: data },
|
||||||
|
visualMap: {
|
||||||
|
show: false,
|
||||||
|
type: 'continuous',
|
||||||
|
min: 0,
|
||||||
|
max: 400,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 10,
|
||||||
|
left: '2%',
|
||||||
|
right: '2%',
|
||||||
|
bottom: '3%',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'line',
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: {
|
||||||
|
width: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Simple delay to let DOM settle, then render
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('ChartLine: rendering chart')
|
||||||
|
isReady.value = true
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="chart-container">
|
||||||
|
<v-chart
|
||||||
|
v-if="isReady"
|
||||||
|
:option="option"
|
||||||
|
:style="{ width: '100%', height: '100%' }"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
63
src/components/ChartPie.vue
Normal file
63
src/components/ChartPie.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
|
import type { ComposeOption } from 'echarts/core'
|
||||||
|
import type { PieSeriesOption } from 'echarts/charts'
|
||||||
|
import type { LegendComponentOption, TitleComponentOption } from 'echarts/components'
|
||||||
|
|
||||||
|
type ECOption = ComposeOption<
|
||||||
|
PieSeriesOption | LegendComponentOption | TitleComponentOption
|
||||||
|
>
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const option: ECOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
left: 'center',
|
||||||
|
bottom: '10',
|
||||||
|
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts'],
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'WEEKLY WRITE ARTICLES',
|
||||||
|
type: 'pie',
|
||||||
|
roseType: 'radius',
|
||||||
|
radius: [15, 95],
|
||||||
|
center: ['50%', '38%'],
|
||||||
|
data: [
|
||||||
|
{ value: 320, name: 'Industries' },
|
||||||
|
{ value: 240, name: 'Technology' },
|
||||||
|
{ value: 149, name: 'Forex' },
|
||||||
|
{ value: 100, name: 'Gold' },
|
||||||
|
{ value: 59, name: 'Forecasts' },
|
||||||
|
],
|
||||||
|
animationEasing: 'cubicInOut',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Simple delay to let DOM settle, then render
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('ChartPie: rendering chart')
|
||||||
|
isReady.value = true
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="chart-container">
|
||||||
|
<v-chart
|
||||||
|
v-if="isReady"
|
||||||
|
:option="option"
|
||||||
|
:style="{ width: '100%', height: '100%' }"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
93
src/components/ChartRadar.vue
Normal file
93
src/components/ChartRadar.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
|
import type { ComposeOption } from 'echarts/core'
|
||||||
|
import type { RadarSeriesOption } from 'echarts/charts'
|
||||||
|
import type { LegendComponentOption, TitleComponentOption } from 'echarts/components'
|
||||||
|
|
||||||
|
type ECOption = ComposeOption<
|
||||||
|
RadarSeriesOption | LegendComponentOption | TitleComponentOption
|
||||||
|
>
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const option: ECOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
radar: {
|
||||||
|
radius: '66%',
|
||||||
|
center: ['50%', '42%'],
|
||||||
|
splitNumber: 8,
|
||||||
|
splitArea: {
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(127,95,132,.3)',
|
||||||
|
opacity: 1,
|
||||||
|
shadowBlur: 45,
|
||||||
|
shadowColor: 'rgba(0,0,0,.5)',
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
indicator: [
|
||||||
|
{ name: 'Sales' },
|
||||||
|
{ name: 'Administration' },
|
||||||
|
{ name: 'Technology' },
|
||||||
|
{ name: 'Customer Support' },
|
||||||
|
{ name: 'Development' },
|
||||||
|
{ name: 'Marketing' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
left: 'center',
|
||||||
|
bottom: '10',
|
||||||
|
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending'],
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'radar',
|
||||||
|
symbolSize: 0,
|
||||||
|
areaStyle: {
|
||||||
|
shadowBlur: 13,
|
||||||
|
shadowColor: 'rgba(0,0,0,.2)',
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowOffsetY: 10,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: [5000, 7000, 12000, 11000, 15000, 14000],
|
||||||
|
name: 'Allocated Budget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: [4000, 9000, 15000, 15000, 13000, 11000],
|
||||||
|
name: 'Expected Spending',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: [5500, 5000, 12000, 15000, 8000, 6000],
|
||||||
|
name: 'Actual Spending',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// Simple delay to let DOM settle, then render
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('ChartRadar: rendering chart')
|
||||||
|
isReady.value = true
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="chart-container">
|
||||||
|
<v-chart
|
||||||
|
v-if="isReady"
|
||||||
|
:option="option"
|
||||||
|
:style="{ width: '100%', height: '100%' }"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
82
src/components/StatsCard.vue
Normal file
82
src/components/StatsCard.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
icon: string
|
||||||
|
iconClass?: string
|
||||||
|
color: string
|
||||||
|
title: string
|
||||||
|
value: number | null
|
||||||
|
unit?: string
|
||||||
|
formatter?: (v: number) => string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
iconClass: '',
|
||||||
|
value: null,
|
||||||
|
unit: '',
|
||||||
|
formatter: (v: number) => v.toString(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card class="stats-card" v-bind="$attrs">
|
||||||
|
<v-icon class="stats-icon" :color="color" :class="iconClass" :icon="icon" />
|
||||||
|
<div class="card-title ml-auto text-right">
|
||||||
|
<span
|
||||||
|
class="card-title--name font-weight-bold text--darken-2"
|
||||||
|
:class="`${color}--text`"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
<h3
|
||||||
|
class="font-weight-regular text--primary d-inline-block ml-2"
|
||||||
|
style="font-size: 18px"
|
||||||
|
>
|
||||||
|
{{ value != null ? formatter(value) : '' }}
|
||||||
|
<small v-if="unit">{{ unit }}</small>
|
||||||
|
</h3>
|
||||||
|
<v-divider />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="v-alert__border v-alert__border--top v-alert__border--has-color"
|
||||||
|
:class="color"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="$slots.footer"
|
||||||
|
class="grey--text text-right stats-footer text-caption"
|
||||||
|
>
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.stats-card {
|
||||||
|
padding: 5px;
|
||||||
|
padding-top: 10px;
|
||||||
|
.card-title {
|
||||||
|
width: fit-content;
|
||||||
|
.card-title--name {
|
||||||
|
display: inline-block;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.caption {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.stats-icon {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.3;
|
||||||
|
:deep(svg) {
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stats-footer {
|
||||||
|
:deep(span) {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px !important;
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/integrations.ts
Normal file
15
src/integrations.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||||
|
|
||||||
|
const integrations: ModuleIntegrations = {
|
||||||
|
app_menu: [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
label: 'Dashboard',
|
||||||
|
path: '/',
|
||||||
|
icon: 'mdi-view-dashboard-outline',
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default integrations;
|
||||||
56
src/main.ts
Normal file
56
src/main.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import routes from '@/routes'
|
||||||
|
import integrations from '@/integrations'
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import type { App as Vue } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
RadarChart,
|
||||||
|
EffectScatterChart,
|
||||||
|
ScatterChart,
|
||||||
|
} from 'echarts/charts'
|
||||||
|
|
||||||
|
import {
|
||||||
|
LegendComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
TitleComponent,
|
||||||
|
VisualMapComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
MarkPointComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
} from 'echarts/components'
|
||||||
|
|
||||||
|
// CSS filename is injected by the vite plugin at build time
|
||||||
|
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
|
export { routes, integrations }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(app: Vue) {
|
||||||
|
use([
|
||||||
|
LineChart,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
RadarChart,
|
||||||
|
EffectScatterChart,
|
||||||
|
ScatterChart,
|
||||||
|
CanvasRenderer,
|
||||||
|
LegendComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
TitleComponent,
|
||||||
|
VisualMapComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
MarkPointComponent,
|
||||||
|
DatasetComponent,
|
||||||
|
])
|
||||||
|
app.component('VChart', VChart)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/routes.ts
Normal file
9
src/routes.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
component: () => import('@/views/Main.vue'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
1
src/style.css
Normal file
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* dashboard module styles */
|
||||||
131
src/views/Main.vue
Normal file
131
src/views/Main.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ChartRadar from '@/components/ChartRadar.vue'
|
||||||
|
import ChartLine from '@/components/ChartLine.vue'
|
||||||
|
import ChartBar from '@/components/ChartBar.vue'
|
||||||
|
import ChartPie from '@/components/ChartPie.vue'
|
||||||
|
import StatsCard from '@/components/StatsCard.vue'
|
||||||
|
|
||||||
|
// Import MDI icons
|
||||||
|
import { mdiWeb, mdiRss, mdiSend, mdiBell, mdiGithub, mdiCurrencyCny } from '@mdi/js'
|
||||||
|
|
||||||
|
const stats = ref([
|
||||||
|
{
|
||||||
|
icon: mdiWeb,
|
||||||
|
title: 'Bandwidth',
|
||||||
|
value: 230,
|
||||||
|
unit: 'GB',
|
||||||
|
color: 'primary',
|
||||||
|
caption: 'Up: 100, Down: 130',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiRss,
|
||||||
|
title: 'Submissions',
|
||||||
|
value: 108,
|
||||||
|
color: 'primary',
|
||||||
|
caption: 'Too young, too naive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiSend,
|
||||||
|
title: 'Requests',
|
||||||
|
value: 1238,
|
||||||
|
color: 'warning',
|
||||||
|
caption: 'Limit: 1320',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiBell,
|
||||||
|
title: 'Messages',
|
||||||
|
value: 9042,
|
||||||
|
color: 'primary',
|
||||||
|
caption: 'Warnings: 300, erros: 47',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiGithub,
|
||||||
|
title: 'Github Stars',
|
||||||
|
value: NaN,
|
||||||
|
color: 'grey',
|
||||||
|
caption: 'API has no response',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiCurrencyCny,
|
||||||
|
title: 'Total Fee',
|
||||||
|
value: 2300,
|
||||||
|
unit: '¥',
|
||||||
|
color: 'error',
|
||||||
|
caption: 'Upper Limit: 2000 ¥',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="stat in stats"
|
||||||
|
:key="stat.title"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="2"
|
||||||
|
>
|
||||||
|
<StatsCard
|
||||||
|
:title="stat.title"
|
||||||
|
:unit="stat.unit"
|
||||||
|
:color="stat.color"
|
||||||
|
:icon="stat.icon"
|
||||||
|
:value="stat.value"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
{{ stat.caption }}
|
||||||
|
</template>
|
||||||
|
</StatsCard>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6" lg="12">
|
||||||
|
<v-card class="pa-2 chart-card">
|
||||||
|
<ChartLine />
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6" lg="4">
|
||||||
|
<v-card class="pa-2 chart-card">
|
||||||
|
<ChartRadar />
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6" lg="4">
|
||||||
|
<v-card class="pa-2 chart-card">
|
||||||
|
<ChartPie />
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6" lg="4">
|
||||||
|
<v-card class="pa-2 chart-card">
|
||||||
|
<ChartBar />
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.chart-card {
|
||||||
|
height: 350px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.chart-container) {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chart) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
19
tsconfig.app.json
Normal file
19
tsconfig.app.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@KTXC/*": ["../../core/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
57
vite.config.ts
Normal file
57
vite.config.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
{
|
||||||
|
name: 'inject-css-filename',
|
||||||
|
enforce: 'post',
|
||||||
|
generateBundle(_options, bundle) {
|
||||||
|
const cssFile = Object.keys(bundle).find(name => name.endsWith('.css'))
|
||||||
|
if (!cssFile) return
|
||||||
|
|
||||||
|
for (const fileName of Object.keys(bundle)) {
|
||||||
|
const chunk = bundle[fileName]
|
||||||
|
if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) {
|
||||||
|
chunk.code = chunk.code.replace(/__CSS_FILENAME_PLACEHOLDER__/g, `static/${cssFile}`)
|
||||||
|
console.log(`Injected CSS filename "static/${cssFile}" into ${fileName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'@KTXC': path.resolve(__dirname, '../../core/src')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
'process': undefined,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'static',
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/main.ts'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => 'module.mjs',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue', 'vue-router', 'pinia'],
|
||||||
|
output: {
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
if (assetInfo.name?.endsWith('.css')) {
|
||||||
|
return 'dashboard-[hash].css'
|
||||||
|
}
|
||||||
|
return '[name]-[hash][extname]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user