mirror of https://github.com/halo-dev/halo-admin
refactor: widgets of dashboard page (#736)
#### What type of PR is this? /kind improvement /milestone 2.0 #### What this PR does / why we need it: 优化仪表盘小部件的样式和数据获取方式。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2649 #### Screenshots: <img width="1664" alt="image" src="https://user-images.githubusercontent.com/21301288/204808668-29d65e42-eabc-4598-9a9e-03597d6a0344.png"> #### Special notes for your reviewer: None #### Does this PR introduce a user-facing change? ```release-note None ```pull/734/head^2
parent
c20767a30a
commit
c26e438420
|
@ -77,6 +77,7 @@
|
||||||
"@iconify-json/vscode-icons": "^1.1.16",
|
"@iconify-json/vscode-icons": "^1.1.16",
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.0",
|
||||||
"@types/jsdom": "^20.0.1",
|
"@types/jsdom": "^20.0.1",
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"@types/lodash.clonedeep": "4.5.7",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
|
@ -118,7 +119,6 @@
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-pwa": "^0.13.3",
|
"vite-plugin-pwa": "^0.13.3",
|
||||||
"vite-plugin-static-copy": "^0.11.1",
|
"vite-plugin-static-copy": "^0.11.1",
|
||||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
|
||||||
"vitest": "^0.25.3",
|
"vitest": "^0.25.3",
|
||||||
"vue-tsc": "^1.0.9"
|
"vue-tsc": "^1.0.9"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export interface Plugin {
|
||||||
/**
|
/**
|
||||||
* These components will be registered when plugin is activated.
|
* These components will be registered when plugin is activated.
|
||||||
*/
|
*/
|
||||||
components?: Component[];
|
components?: Record<string, Component>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate hook will be called when plugin is activated.
|
* Activate hook will be called when plugin is activated.
|
||||||
|
|
|
@ -20,6 +20,7 @@ importers:
|
||||||
'@iconify-json/vscode-icons': ^1.1.16
|
'@iconify-json/vscode-icons': ^1.1.16
|
||||||
'@rushstack/eslint-patch': ^1.2.0
|
'@rushstack/eslint-patch': ^1.2.0
|
||||||
'@tailwindcss/aspect-ratio': ^0.4.2
|
'@tailwindcss/aspect-ratio': ^0.4.2
|
||||||
|
'@tailwindcss/container-queries': ^0.1.0
|
||||||
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
||||||
'@types/jsdom': ^20.0.1
|
'@types/jsdom': ^20.0.1
|
||||||
'@types/lodash.clonedeep': 4.5.7
|
'@types/lodash.clonedeep': 4.5.7
|
||||||
|
@ -90,7 +91,6 @@ importers:
|
||||||
vite-plugin-html: ^3.2.0
|
vite-plugin-html: ^3.2.0
|
||||||
vite-plugin-pwa: ^0.13.3
|
vite-plugin-pwa: ^0.13.3
|
||||||
vite-plugin-static-copy: ^0.11.1
|
vite-plugin-static-copy: ^0.11.1
|
||||||
vite-plugin-vue-setup-extend: ^0.4.0
|
|
||||||
vitest: ^0.25.3
|
vitest: ^0.25.3
|
||||||
vue: ^3.2.45
|
vue: ^3.2.45
|
||||||
vue-grid-layout: 3.0.0-beta1
|
vue-grid-layout: 3.0.0-beta1
|
||||||
|
@ -150,6 +150,7 @@ importers:
|
||||||
'@iconify-json/vscode-icons': 1.1.16
|
'@iconify-json/vscode-icons': 1.1.16
|
||||||
'@rushstack/eslint-patch': 1.2.0
|
'@rushstack/eslint-patch': 1.2.0
|
||||||
'@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4
|
'@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4
|
||||||
|
'@tailwindcss/container-queries': 0.1.0_tailwindcss@3.2.4
|
||||||
'@types/jsdom': 20.0.1
|
'@types/jsdom': 20.0.1
|
||||||
'@types/lodash.clonedeep': 4.5.7
|
'@types/lodash.clonedeep': 4.5.7
|
||||||
'@types/lodash.debounce': 4.0.7
|
'@types/lodash.debounce': 4.0.7
|
||||||
|
@ -180,7 +181,7 @@ importers:
|
||||||
randomstring: 1.2.3
|
randomstring: 1.2.3
|
||||||
sass: 1.56.1
|
sass: 1.56.1
|
||||||
start-server-and-test: 1.14.0
|
start-server-and-test: 1.14.0
|
||||||
tailwindcss: 3.2.4
|
tailwindcss: 3.2.4_postcss@8.4.19
|
||||||
tailwindcss-safe-area: 0.2.2
|
tailwindcss-safe-area: 0.2.2
|
||||||
tailwindcss-themer: 2.0.2_tailwindcss@3.2.4
|
tailwindcss-themer: 2.0.2_tailwindcss@3.2.4
|
||||||
typescript: 4.7.4
|
typescript: 4.7.4
|
||||||
|
@ -191,7 +192,6 @@ importers:
|
||||||
vite-plugin-html: 3.2.0_vite@3.2.4
|
vite-plugin-html: 3.2.0_vite@3.2.4
|
||||||
vite-plugin-pwa: 0.13.3_vite@3.2.4
|
vite-plugin-pwa: 0.13.3_vite@3.2.4
|
||||||
vite-plugin-static-copy: 0.11.1_vite@3.2.4
|
vite-plugin-static-copy: 0.11.1_vite@3.2.4
|
||||||
vite-plugin-vue-setup-extend: 0.4.0_vite@3.2.4
|
|
||||||
vitest: 0.25.3_wqgykbafw5ps5nhnqfbxcvabhu
|
vitest: 0.25.3_wqgykbafw5ps5nhnqfbxcvabhu
|
||||||
vue-tsc: 1.0.9_typescript@4.7.4
|
vue-tsc: 1.0.9_typescript@4.7.4
|
||||||
|
|
||||||
|
@ -1931,7 +1931,7 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formkit/core': 1.0.0-beta.12-e579559
|
'@formkit/core': 1.0.0-beta.12-e579559
|
||||||
tailwindcss: 3.2.4
|
tailwindcss: 3.2.4_postcss@8.4.19
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@formkit/utils/1.0.0-beta.12-e579559:
|
/@formkit/utils/1.0.0-beta.12-e579559:
|
||||||
|
@ -2675,7 +2675,15 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.2.4
|
tailwindcss: 3.2.4_postcss@8.4.19
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@tailwindcss/container-queries/0.1.0_tailwindcss@3.2.4:
|
||||||
|
resolution: {integrity: sha512-t1GeJ9P8ual160BvKy6Y1sG7bjChArMaK6iRXm3ZYjZGN2FTzmqb5ztsTDb9AsTSJD4NMHtsnaI2ielrXEk+hw==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.2.0'
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 3.2.4_postcss@8.4.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@tiptap/core/2.0.0-beta.202:
|
/@tiptap/core/2.0.0-beta.202:
|
||||||
|
@ -8532,13 +8540,15 @@ packages:
|
||||||
just-unique: 4.1.1
|
just-unique: 4.1.1
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
lodash.mergewith: 4.6.2
|
lodash.mergewith: 4.6.2
|
||||||
tailwindcss: 3.2.4
|
tailwindcss: 3.2.4_postcss@8.4.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/tailwindcss/3.2.4:
|
/tailwindcss/3.2.4_postcss@8.4.19:
|
||||||
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
|
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
|
||||||
engines: {node: '>=12.13.0'}
|
engines: {node: '>=12.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
postcss: ^8.0.9
|
||||||
dependencies:
|
dependencies:
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
chokidar: 3.5.3
|
chokidar: 3.5.3
|
||||||
|
@ -9173,16 +9183,6 @@ packages:
|
||||||
vite: 3.2.4_ajklay5k626t46b6fyghkbup3i
|
vite: 3.2.4_ajklay5k626t46b6fyghkbup3i
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-vue-setup-extend/0.4.0_vite@3.2.4:
|
|
||||||
resolution: {integrity: sha512-WMbjPCui75fboFoUTHhdbXzu4Y/bJMv5N9QT9a7do3wNMNHHqrk+Tn2jrSJU0LS5fGl/EG+FEDBYVUeWIkDqXQ==}
|
|
||||||
peerDependencies:
|
|
||||||
vite: '>=2.0.0'
|
|
||||||
dependencies:
|
|
||||||
'@vue/compiler-sfc': 3.2.45
|
|
||||||
magic-string: 0.25.9
|
|
||||||
vite: 3.2.4_ajklay5k626t46b6fyghkbup3i
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite/3.2.4:
|
/vite/3.2.4:
|
||||||
resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==}
|
resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
14
src/main.ts
14
src/main.ts
|
@ -28,14 +28,12 @@ app.use(createPinia());
|
||||||
|
|
||||||
function registerModule(pluginModule: Plugin, core: boolean) {
|
function registerModule(pluginModule: Plugin, core: boolean) {
|
||||||
if (pluginModule.components) {
|
if (pluginModule.components) {
|
||||||
if (!Array.isArray(pluginModule.components)) {
|
Object.keys(pluginModule.components).forEach((key) => {
|
||||||
console.error(`${pluginModule.name}: Plugin components must be an array`);
|
const component = pluginModule.components?.[key];
|
||||||
return;
|
if (component) {
|
||||||
}
|
app.component(key, component);
|
||||||
|
}
|
||||||
for (const component of pluginModule.components) {
|
});
|
||||||
component.name && app.component(component.name, component);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginModule.routes) {
|
if (pluginModule.routes) {
|
||||||
|
|
|
@ -7,7 +7,9 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "attachmentModule",
|
name: "attachmentModule",
|
||||||
components: [AttachmentSelectorModal],
|
components: {
|
||||||
|
AttachmentSelectorModal,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/attachments",
|
path: "/attachments",
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||||
import { IconMessage } from "@halo-dev/components";
|
import { IconMessage } from "@halo-dev/components";
|
||||||
import CommentList from "./CommentList.vue";
|
import CommentList from "./CommentList.vue";
|
||||||
|
import CommentStatsWidget from "./widgets/CommentStatsWidget.vue";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "commentModule",
|
name: "commentModule",
|
||||||
components: [],
|
components: {
|
||||||
|
CommentStatsWidget,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/comments",
|
path: "/comments",
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DashboardStats } from "@halo-dev/api-client";
|
||||||
|
import { VCard, IconMessage } from "@halo-dev/components";
|
||||||
|
import { inject, type Ref } from "vue";
|
||||||
|
|
||||||
|
const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard class="h-full" :body-class="['h-full']">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span
|
||||||
|
class="hidden rounded-full bg-gray-100 p-2.5 text-gray-600 sm:block"
|
||||||
|
>
|
||||||
|
<IconMessage class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-sm text-gray-500">评论</span>
|
||||||
|
<p class="text-2xl font-medium text-gray-900">
|
||||||
|
{{ dashboardStats?.approvedComments }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -6,12 +6,15 @@ import FunctionalPageList from "./FunctionalPageList.vue";
|
||||||
import SinglePageList from "./SinglePageList.vue";
|
import SinglePageList from "./SinglePageList.vue";
|
||||||
import DeletedSinglePageList from "./DeletedSinglePageList.vue";
|
import DeletedSinglePageList from "./DeletedSinglePageList.vue";
|
||||||
import SinglePageEditor from "./SinglePageEditor.vue";
|
import SinglePageEditor from "./SinglePageEditor.vue";
|
||||||
|
import SinglePageStatsWidget from "./widgets/SinglePageStatsWidget.vue";
|
||||||
import { IconPages } from "@halo-dev/components";
|
import { IconPages } from "@halo-dev/components";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "pageModule",
|
name: "pageModule",
|
||||||
components: [],
|
components: {
|
||||||
|
SinglePageStatsWidget,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/pages",
|
path: "/pages",
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VCard, IconPages } from "@halo-dev/components";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
|
|
||||||
|
const singlePageTotal = ref<number>(0);
|
||||||
|
|
||||||
|
const handleFetchSinglePages = async () => {
|
||||||
|
const { data } = await apiClient.singlePage.listSinglePages({
|
||||||
|
labelSelector: [
|
||||||
|
`${singlePageLabels.DELETED}=false`,
|
||||||
|
`${singlePageLabels.PUBLISHED}=true`,
|
||||||
|
],
|
||||||
|
page: 0,
|
||||||
|
size: 0,
|
||||||
|
});
|
||||||
|
singlePageTotal.value = data.total;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(handleFetchSinglePages);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard class="h-full" :body-class="['h-full']">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span
|
||||||
|
class="hidden rounded-full bg-gray-100 p-2.5 text-gray-600 sm:block"
|
||||||
|
>
|
||||||
|
<IconPages class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-sm text-gray-500">页面</span>
|
||||||
|
<p class="text-2xl font-medium text-gray-900">
|
||||||
|
{{ singlePageTotal || 0 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -7,11 +7,16 @@ import DeletedPostList from "./DeletedPostList.vue";
|
||||||
import PostEditor from "./PostEditor.vue";
|
import PostEditor from "./PostEditor.vue";
|
||||||
import CategoryList from "./categories/CategoryList.vue";
|
import CategoryList from "./categories/CategoryList.vue";
|
||||||
import TagList from "./tags/TagList.vue";
|
import TagList from "./tags/TagList.vue";
|
||||||
|
import PostStatsWidget from "./widgets/PostStatsWidget.vue";
|
||||||
|
import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "postModule",
|
name: "postModule",
|
||||||
components: [],
|
components: {
|
||||||
|
PostStatsWidget,
|
||||||
|
RecentPublishedWidget,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/posts",
|
path: "/posts",
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VCard, IconBookRead } from "@halo-dev/components";
|
||||||
|
import { inject, type Ref } from "vue";
|
||||||
|
import type { DashboardStats } from "@halo-dev/api-client/index";
|
||||||
|
|
||||||
|
const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard class="h-full" :body-class="['h-full']">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span
|
||||||
|
class="hidden rounded-full bg-gray-100 p-2.5 text-gray-600 sm:block"
|
||||||
|
>
|
||||||
|
<IconBookRead class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-sm text-gray-500">文章</span>
|
||||||
|
<p class="text-2xl font-medium text-gray-900">
|
||||||
|
{{ dashboardStats?.posts || 0 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -0,0 +1,90 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
VCard,
|
||||||
|
VSpace,
|
||||||
|
VEntity,
|
||||||
|
VEntityField,
|
||||||
|
IconExternalLinkLine,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
|
||||||
|
const posts = ref<ListedPost[]>([] as ListedPost[]);
|
||||||
|
|
||||||
|
const handleFetchPosts = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.post.listPosts({
|
||||||
|
labelSelector: [
|
||||||
|
`${postLabels.DELETED}=false`,
|
||||||
|
`${postLabels.PUBLISHED}=true`,
|
||||||
|
],
|
||||||
|
sort: "PUBLISH_TIME",
|
||||||
|
sortOrder: false,
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
});
|
||||||
|
posts.value = data.items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch posts", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(handleFetchPosts);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard
|
||||||
|
:body-class="['h-full', '!p-0', 'overflow-y-auto']"
|
||||||
|
class="h-full"
|
||||||
|
title="最近文章"
|
||||||
|
>
|
||||||
|
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||||
|
<li v-for="(post, index) in posts" :key="index">
|
||||||
|
<VEntity>
|
||||||
|
<template #start>
|
||||||
|
<VEntityField
|
||||||
|
:title="post.post.spec.title"
|
||||||
|
:route="{
|
||||||
|
name: 'PostEditor',
|
||||||
|
query: { name: post.post.metadata.name },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<VSpace>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
访问量 {{ post.stats.visit || 0 }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
评论 {{ post.stats.totalComment || 0 }}
|
||||||
|
</span>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a
|
||||||
|
v-if="post.post.status?.permalink"
|
||||||
|
target="_blank"
|
||||||
|
:href="post.post.status?.permalink"
|
||||||
|
:title="post.post.status?.permalink"
|
||||||
|
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||||
|
>
|
||||||
|
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
||||||
|
<template #end>
|
||||||
|
<VEntityField>
|
||||||
|
<template #description>
|
||||||
|
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||||
|
{{ formatDatetime(post.post.spec.publishTime) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
||||||
|
</VEntity>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -114,9 +114,11 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
VTabbar,
|
VTabbar,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { ref } from "vue";
|
import { onMounted, provide, ref, type Ref } from "vue";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import type { DashboardStats } from "@halo-dev/api-client/index";
|
||||||
|
|
||||||
const widgetsGroup = [
|
const widgetsGroup = [
|
||||||
{
|
{
|
||||||
|
@ -127,6 +129,13 @@ const widgetsGroup = [
|
||||||
{ x: 0, y: 0, w: 6, h: 10, i: 1, widget: "RecentPublishedWidget" },
|
{ x: 0, y: 0, w: 6, h: 10, i: 1, widget: "RecentPublishedWidget" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "page",
|
||||||
|
label: "页面",
|
||||||
|
widgets: [
|
||||||
|
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "SinglePageStatsWidget" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "comment",
|
id: "comment",
|
||||||
label: "评论",
|
label: "评论",
|
||||||
|
@ -137,7 +146,7 @@ const widgetsGroup = [
|
||||||
label: "用户",
|
label: "用户",
|
||||||
widgets: [
|
widgets: [
|
||||||
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "UserStatsWidget" },
|
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "UserStatsWidget" },
|
||||||
{ x: 0, y: 0, w: 6, h: 10, i: 1, widget: "RecentLoginWidget" },
|
{ x: 0, y: 0, w: 3, h: 3, i: 1, widget: "UserProfileWidget" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -159,14 +168,13 @@ const layout = useStorage("widgets", [
|
||||||
{ x: 3, y: 0, w: 3, h: 3, i: 1, widget: "UserStatsWidget" },
|
{ x: 3, y: 0, w: 3, h: 3, i: 1, widget: "UserStatsWidget" },
|
||||||
{ x: 6, y: 0, w: 3, h: 3, i: 2, widget: "CommentStatsWidget" },
|
{ x: 6, y: 0, w: 3, h: 3, i: 2, widget: "CommentStatsWidget" },
|
||||||
{ x: 9, y: 0, w: 3, h: 3, i: 3, widget: "ViewsStatsWidget" },
|
{ x: 9, y: 0, w: 3, h: 3, i: 3, widget: "ViewsStatsWidget" },
|
||||||
{ x: 8, y: 3, w: 4, h: 10, i: 4, widget: "RecentLoginWidget" },
|
{ x: 0, y: 3, w: 4, h: 10, i: 4, widget: "QuickLinkWidget" },
|
||||||
{ x: 0, y: 3, w: 4, h: 10, i: 5, widget: "QuickLinkWidget" },
|
|
||||||
{
|
{
|
||||||
x: 4,
|
x: 4,
|
||||||
y: 3,
|
y: 3,
|
||||||
w: 4,
|
w: 4,
|
||||||
h: 10,
|
h: 10,
|
||||||
i: 6,
|
i: 5,
|
||||||
widget: "RecentPublishedWidget",
|
widget: "RecentPublishedWidget",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -194,12 +202,32 @@ function handleRemove(item: any) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dashboard basic stats
|
||||||
|
|
||||||
|
const dashboardStats = ref<DashboardStats>({
|
||||||
|
posts: 0,
|
||||||
|
comments: 0,
|
||||||
|
approvedComments: 0,
|
||||||
|
users: 0,
|
||||||
|
visits: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
provide<Ref<DashboardStats>>("dashboardStats", dashboardStats);
|
||||||
|
|
||||||
|
const handleFetchStats = async () => {
|
||||||
|
const { data } = await apiClient.stats.getStats();
|
||||||
|
dashboardStats.value = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(handleFetchStats);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.vue-grid-layout {
|
.vue-grid-layout {
|
||||||
@apply -m-[10px];
|
@apply -m-[10px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-grid-item {
|
.vue-grid-item {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,26 +3,16 @@ import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||||
import Dashboard from "./Dashboard.vue";
|
import Dashboard from "./Dashboard.vue";
|
||||||
import { IconDashboard } from "@halo-dev/components";
|
import { IconDashboard } from "@halo-dev/components";
|
||||||
|
|
||||||
import CommentStatsWidget from "./widgets/CommentStatsWidget.vue";
|
|
||||||
import PostStatsWidget from "./widgets/PostStatsWidget.vue";
|
|
||||||
import QuickLinkWidget from "./widgets/QuickLinkWidget.vue";
|
import QuickLinkWidget from "./widgets/QuickLinkWidget.vue";
|
||||||
import RecentLoginWidget from "./widgets/RecentLoginWidget.vue";
|
|
||||||
import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
|
||||||
import UserStatsWidget from "./widgets/UserStatsWidget.vue";
|
|
||||||
import ViewsStatsWidget from "./widgets/ViewsStatsWidget.vue";
|
import ViewsStatsWidget from "./widgets/ViewsStatsWidget.vue";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "dashboardModule",
|
name: "dashboardModule",
|
||||||
components: [
|
components: {
|
||||||
CommentStatsWidget,
|
|
||||||
PostStatsWidget,
|
|
||||||
QuickLinkWidget,
|
QuickLinkWidget,
|
||||||
RecentLoginWidget,
|
|
||||||
RecentPublishedWidget,
|
|
||||||
UserStatsWidget,
|
|
||||||
ViewsStatsWidget,
|
ViewsStatsWidget,
|
||||||
],
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<script lang="ts" name="CommentStatsWidget" setup>
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import { VCard } from "@halo-dev/components";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
const commentTotal = ref<number>(0);
|
|
||||||
|
|
||||||
const handleFetchComments = async () => {
|
|
||||||
const { data } =
|
|
||||||
await apiClient.extension.comment.listcontentHaloRunV1alpha1Comment();
|
|
||||||
commentTotal.value = data.total;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(handleFetchComments);
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard class="h-full">
|
|
||||||
<dt class="truncate text-sm font-medium text-gray-500">评论</dt>
|
|
||||||
<dd class="mt-1 text-3xl font-semibold text-gray-900">
|
|
||||||
{{ commentTotal }}
|
|
||||||
</dd>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script lang="ts" name="PostStatsWidget" setup>
|
|
||||||
import { VCard } from "@halo-dev/components";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
|
|
||||||
const postTotal = ref<number>(0);
|
|
||||||
|
|
||||||
const handleFetchPosts = async () => {
|
|
||||||
const { data } =
|
|
||||||
await apiClient.extension.post.listcontentHaloRunV1alpha1Post();
|
|
||||||
postTotal.value = data.total;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(handleFetchPosts);
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard class="h-full">
|
|
||||||
<dt class="truncate text-sm font-medium text-gray-500">文章</dt>
|
|
||||||
<dd class="mt-1 text-3xl font-semibold text-gray-900">{{ postTotal }}</dd>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script lang="ts" name="QuickLinkWidget" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconBookRead,
|
IconBookRead,
|
||||||
|
@ -117,16 +117,18 @@ const actions: Action[] = [
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VCard
|
<VCard
|
||||||
:body-class="['h-full', 'overflow-y-auto', '!p-0']"
|
:body-class="['h-full', 'overflow-y-auto', '@container']"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
title="快捷访问"
|
title="快捷访问"
|
||||||
>
|
>
|
||||||
<div class="overflow-hidden sm:grid sm:grid-cols-3 sm:gap-px">
|
<div
|
||||||
|
class="grid grid-cols-1 gap-2 overflow-hidden @sm:grid-cols-2 @md:grid-cols-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(action, index) in actions"
|
v-for="(action, index) in actions"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-permission="action.permissions"
|
v-permission="action.permissions"
|
||||||
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
|
class="group relative cursor-pointer rounded-lg bg-gray-50 p-4 transition-all hover:bg-gray-100"
|
||||||
@click="action.action"
|
@click="action.action"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -137,14 +139,13 @@ const actions: Action[] = [
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h3 class="text-base font-medium">
|
<h3 class="text-sm font-semibold">
|
||||||
<span aria-hidden="true" class="absolute inset-0"></span>
|
|
||||||
{{ action.title }}
|
{{ action.title }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="pointer-events-none absolute top-6 right-6 text-gray-300 group-hover:text-gray-400"
|
class="pointer-events-none absolute top-6 right-6 text-gray-300 transition-all group-hover:translate-x-1 group-hover:text-gray-400"
|
||||||
>
|
>
|
||||||
<IconArrowRight />
|
<IconArrowRight />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
<script lang="ts" name="RecentPublishedWidget" setup>
|
|
||||||
import { VCard, VSpace } from "@halo-dev/components";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import { formatDatetime } from "@/utils/date";
|
|
||||||
import { postLabels } from "@/constants/labels";
|
|
||||||
|
|
||||||
const posts = ref<ListedPost[]>([] as ListedPost[]);
|
|
||||||
|
|
||||||
const handleFetchPosts = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await apiClient.post.listPosts({
|
|
||||||
labelSelector: [`${postLabels.PUBLISHED}=true`],
|
|
||||||
sort: "PUBLISH_TIME",
|
|
||||||
sortOrder: false,
|
|
||||||
page: 1,
|
|
||||||
size: 10,
|
|
||||||
});
|
|
||||||
posts.value = data.items;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch posts", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(handleFetchPosts);
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard
|
|
||||||
:body-class="['h-full', '!p-0', 'overflow-y-auto']"
|
|
||||||
class="h-full"
|
|
||||||
title="最近文章"
|
|
||||||
>
|
|
||||||
<div class="h-full">
|
|
||||||
<ul class="divide-y divide-gray-200" role="list">
|
|
||||||
<li
|
|
||||||
v-for="(post, index) in posts"
|
|
||||||
:key="index"
|
|
||||||
class="cursor-pointer p-4 hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<p class="truncate text-sm font-medium text-gray-900">
|
|
||||||
{{ post.post.spec.title }}
|
|
||||||
</p>
|
|
||||||
<div class="mt-1 flex">
|
|
||||||
<VSpace>
|
|
||||||
<span class="text-xs text-gray-500">
|
|
||||||
阅读 {{ post.stats.visit }}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-500">
|
|
||||||
评论 {{ post.stats.totalComment }}
|
|
||||||
</span>
|
|
||||||
</VSpace>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<time class="text-sm tabular-nums text-gray-500">
|
|
||||||
{{ formatDatetime(post.post.spec.publishTime) }}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script lang="ts" name="UserStatsWidget" setup>
|
|
||||||
import { VCard } from "@halo-dev/components";
|
|
||||||
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
|
||||||
const { users } = useUserFetch({ fetchOnMounted: true });
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard class="h-full">
|
|
||||||
<dt class="truncate text-sm font-medium text-gray-500">用户</dt>
|
|
||||||
<dd class="mt-1 text-3xl font-semibold text-gray-900">
|
|
||||||
{{ users.length }}
|
|
||||||
</dd>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
|
@ -1,25 +1,27 @@
|
||||||
<script lang="ts" name="ViewsStatsWidget" setup>
|
<script lang="ts" setup>
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import type { DashboardStats } from "@halo-dev/api-client";
|
import type { DashboardStats } from "@halo-dev/api-client";
|
||||||
import { VCard } from "@halo-dev/components";
|
import { VCard, IconEye } from "@halo-dev/components";
|
||||||
import { onMounted, ref } from "vue";
|
import { inject, type Ref } from "vue";
|
||||||
|
|
||||||
const stats = ref<DashboardStats>({
|
const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
||||||
visits: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFetchStats = async () => {
|
|
||||||
const { data } = await apiClient.stats.getStats();
|
|
||||||
stats.value = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(handleFetchStats);
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VCard class="h-full">
|
<VCard class="h-full" :body-class="['h-full']">
|
||||||
<dt class="truncate text-sm font-medium text-gray-500">浏览量</dt>
|
<div class="flex h-full">
|
||||||
<dd class="mt-1 text-3xl font-semibold text-gray-900">
|
<div class="flex items-center gap-4">
|
||||||
{{ stats.visits || 0 }}
|
<span
|
||||||
</dd>
|
class="hidden rounded-full bg-gray-100 p-2.5 text-gray-600 sm:block"
|
||||||
|
>
|
||||||
|
<IconEye class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-sm text-gray-500">浏览量</span>
|
||||||
|
<p class="text-2xl font-medium text-gray-900">
|
||||||
|
{{ dashboardStats?.visits || 0 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "menuModule",
|
name: "menuModule",
|
||||||
components: [],
|
components: {},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/menus",
|
path: "/menus",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "themeModule",
|
name: "themeModule",
|
||||||
components: [],
|
components: {},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/theme",
|
path: "/theme",
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "pluginModule",
|
name: "pluginModule",
|
||||||
components: [],
|
components: {},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/plugins",
|
path: "/plugins",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import RoleDetail from "./RoleDetail.vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "roleModule",
|
name: "roleModule",
|
||||||
components: [],
|
components: {},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/users",
|
path: "/users",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "settingModule",
|
name: "settingModule",
|
||||||
components: [],
|
components: {},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||||
import BlankLayout from "@/layouts/BlankLayout.vue";
|
import BlankLayout from "@/layouts/BlankLayout.vue";
|
||||||
import UserProfileLayout from "./layouts/UserProfileLayout.vue";
|
import UserProfileLayout from "./layouts/UserProfileLayout.vue";
|
||||||
|
import UserStatsWidget from "./widgets/UserStatsWidget.vue";
|
||||||
import UserList from "./UserList.vue";
|
import UserList from "./UserList.vue";
|
||||||
import UserDetail from "./UserDetail.vue";
|
import UserDetail from "./UserDetail.vue";
|
||||||
import PersonalAccessTokens from "./PersonalAccessTokens.vue";
|
import PersonalAccessTokens from "./PersonalAccessTokens.vue";
|
||||||
|
@ -11,7 +12,9 @@ import { markRaw } from "vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "userModule",
|
name: "userModule",
|
||||||
components: [],
|
components: {
|
||||||
|
UserStatsWidget,
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script lang="ts" name="RecentLoginWidget" setup>
|
<script lang="ts" setup>
|
||||||
import { VCard, VAvatar } from "@halo-dev/components";
|
import { VCard, VAvatar } from "@halo-dev/components";
|
||||||
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VCard, IconUserSettings } from "@halo-dev/components";
|
||||||
|
import { inject, type Ref } from "vue";
|
||||||
|
import type { DashboardStats } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard class="h-full" :body-class="['h-full']">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span
|
||||||
|
class="hidden rounded-full bg-gray-100 p-2.5 text-gray-600 sm:block"
|
||||||
|
>
|
||||||
|
<IconUserSettings class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-sm text-gray-500">用户</span>
|
||||||
|
<p class="text-2xl font-medium text-gray-900">
|
||||||
|
{{ dashboardStats?.users }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -23,6 +23,7 @@ module.exports = {
|
||||||
require("tailwindcss-safe-area"),
|
require("tailwindcss-safe-area"),
|
||||||
require("@tailwindcss/aspect-ratio"),
|
require("@tailwindcss/aspect-ratio"),
|
||||||
require("@formkit/themes/tailwindcss"),
|
require("@formkit/themes/tailwindcss"),
|
||||||
|
require("@tailwindcss/container-queries"),
|
||||||
require("tailwindcss-themer")({
|
require("tailwindcss-themer")({
|
||||||
defaultTheme: {
|
defaultTheme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import fs from "fs";
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import Vue from "@vitejs/plugin-vue";
|
import Vue from "@vitejs/plugin-vue";
|
||||||
import VueJsx from "@vitejs/plugin-vue-jsx";
|
import VueJsx from "@vitejs/plugin-vue-jsx";
|
||||||
import VueSetupExtend from "vite-plugin-vue-setup-extend";
|
|
||||||
import Compression from "vite-compression-plugin";
|
import Compression from "vite-compression-plugin";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import Icons from "unplugin-icons/vite";
|
import Icons from "unplugin-icons/vite";
|
||||||
|
@ -12,7 +11,6 @@ import { setupLibraryExternal } from "./src/build/library-external";
|
||||||
export const sharedPlugins = [
|
export const sharedPlugins = [
|
||||||
Vue(),
|
Vue(),
|
||||||
VueJsx(),
|
VueJsx(),
|
||||||
VueSetupExtend(),
|
|
||||||
Compression(),
|
Compression(),
|
||||||
Icons({
|
Icons({
|
||||||
compiler: "vue3",
|
compiler: "vue3",
|
||||||
|
|
Loading…
Reference in New Issue