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",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.0",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
|
@ -118,7 +119,6 @@
|
|||
"vite-plugin-html": "^3.2.0",
|
||||
"vite-plugin-pwa": "^0.13.3",
|
||||
"vite-plugin-static-copy": "^0.11.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vitest": "^0.25.3",
|
||||
"vue-tsc": "^1.0.9"
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export interface Plugin {
|
|||
/**
|
||||
* These components will be registered when plugin is activated.
|
||||
*/
|
||||
components?: Component[];
|
||||
components?: Record<string, Component>;
|
||||
|
||||
/**
|
||||
* Activate hook will be called when plugin is activated.
|
||||
|
|
|
@ -20,6 +20,7 @@ importers:
|
|||
'@iconify-json/vscode-icons': ^1.1.16
|
||||
'@rushstack/eslint-patch': ^1.2.0
|
||||
'@tailwindcss/aspect-ratio': ^0.4.2
|
||||
'@tailwindcss/container-queries': ^0.1.0
|
||||
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
||||
'@types/jsdom': ^20.0.1
|
||||
'@types/lodash.clonedeep': 4.5.7
|
||||
|
@ -90,7 +91,6 @@ importers:
|
|||
vite-plugin-html: ^3.2.0
|
||||
vite-plugin-pwa: ^0.13.3
|
||||
vite-plugin-static-copy: ^0.11.1
|
||||
vite-plugin-vue-setup-extend: ^0.4.0
|
||||
vitest: ^0.25.3
|
||||
vue: ^3.2.45
|
||||
vue-grid-layout: 3.0.0-beta1
|
||||
|
@ -150,6 +150,7 @@ importers:
|
|||
'@iconify-json/vscode-icons': 1.1.16
|
||||
'@rushstack/eslint-patch': 1.2.0
|
||||
'@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/lodash.clonedeep': 4.5.7
|
||||
'@types/lodash.debounce': 4.0.7
|
||||
|
@ -180,7 +181,7 @@ importers:
|
|||
randomstring: 1.2.3
|
||||
sass: 1.56.1
|
||||
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-themer: 2.0.2_tailwindcss@3.2.4
|
||||
typescript: 4.7.4
|
||||
|
@ -191,7 +192,6 @@ importers:
|
|||
vite-plugin-html: 3.2.0_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-vue-setup-extend: 0.4.0_vite@3.2.4
|
||||
vitest: 0.25.3_wqgykbafw5ps5nhnqfbxcvabhu
|
||||
vue-tsc: 1.0.9_typescript@4.7.4
|
||||
|
||||
|
@ -1931,7 +1931,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
'@formkit/core': 1.0.0-beta.12-e579559
|
||||
tailwindcss: 3.2.4
|
||||
tailwindcss: 3.2.4_postcss@8.4.19
|
||||
dev: false
|
||||
|
||||
/@formkit/utils/1.0.0-beta.12-e579559:
|
||||
|
@ -2675,7 +2675,15 @@ packages:
|
|||
peerDependencies:
|
||||
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||
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
|
||||
|
||||
/@tiptap/core/2.0.0-beta.202:
|
||||
|
@ -8532,13 +8540,15 @@ packages:
|
|||
just-unique: 4.1.1
|
||||
lodash.merge: 4.6.2
|
||||
lodash.mergewith: 4.6.2
|
||||
tailwindcss: 3.2.4
|
||||
tailwindcss: 3.2.4_postcss@8.4.19
|
||||
dev: true
|
||||
|
||||
/tailwindcss/3.2.4:
|
||||
/tailwindcss/3.2.4_postcss@8.4.19:
|
||||
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.0.9
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
chokidar: 3.5.3
|
||||
|
@ -9173,16 +9183,6 @@ packages:
|
|||
vite: 3.2.4_ajklay5k626t46b6fyghkbup3i
|
||||
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:
|
||||
resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==}
|
||||
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) {
|
||||
if (pluginModule.components) {
|
||||
if (!Array.isArray(pluginModule.components)) {
|
||||
console.error(`${pluginModule.name}: Plugin components must be an array`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const component of pluginModule.components) {
|
||||
component.name && app.component(component.name, component);
|
||||
}
|
||||
Object.keys(pluginModule.components).forEach((key) => {
|
||||
const component = pluginModule.components?.[key];
|
||||
if (component) {
|
||||
app.component(key, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pluginModule.routes) {
|
||||
|
|
|
@ -7,7 +7,9 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "attachmentModule",
|
||||
components: [AttachmentSelectorModal],
|
||||
components: {
|
||||
AttachmentSelectorModal,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: "/attachments",
|
||||
|
|
|
@ -2,11 +2,14 @@ import { definePlugin } from "@halo-dev/console-shared";
|
|||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||
import { IconMessage } from "@halo-dev/components";
|
||||
import CommentList from "./CommentList.vue";
|
||||
import CommentStatsWidget from "./widgets/CommentStatsWidget.vue";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "commentModule",
|
||||
components: [],
|
||||
components: {
|
||||
CommentStatsWidget,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
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 DeletedSinglePageList from "./DeletedSinglePageList.vue";
|
||||
import SinglePageEditor from "./SinglePageEditor.vue";
|
||||
import SinglePageStatsWidget from "./widgets/SinglePageStatsWidget.vue";
|
||||
import { IconPages } from "@halo-dev/components";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "pageModule",
|
||||
components: [],
|
||||
components: {
|
||||
SinglePageStatsWidget,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
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 CategoryList from "./categories/CategoryList.vue";
|
||||
import TagList from "./tags/TagList.vue";
|
||||
import PostStatsWidget from "./widgets/PostStatsWidget.vue";
|
||||
import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "postModule",
|
||||
components: [],
|
||||
components: {
|
||||
PostStatsWidget,
|
||||
RecentPublishedWidget,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
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,
|
||||
VTabbar,
|
||||
} from "@halo-dev/components";
|
||||
import { ref } from "vue";
|
||||
import { onMounted, provide, ref, type Ref } from "vue";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { DashboardStats } from "@halo-dev/api-client/index";
|
||||
|
||||
const widgetsGroup = [
|
||||
{
|
||||
|
@ -127,6 +129,13 @@ const widgetsGroup = [
|
|||
{ 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",
|
||||
label: "评论",
|
||||
|
@ -137,7 +146,7 @@ const widgetsGroup = [
|
|||
label: "用户",
|
||||
widgets: [
|
||||
{ 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: 6, y: 0, w: 3, h: 3, i: 2, widget: "CommentStatsWidget" },
|
||||
{ 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: 5, widget: "QuickLinkWidget" },
|
||||
{ x: 0, y: 3, w: 4, h: 10, i: 4, widget: "QuickLinkWidget" },
|
||||
{
|
||||
x: 4,
|
||||
y: 3,
|
||||
w: 4,
|
||||
h: 10,
|
||||
i: 6,
|
||||
i: 5,
|
||||
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>
|
||||
|
||||
<style>
|
||||
.vue-grid-layout {
|
||||
@apply -m-[10px];
|
||||
}
|
||||
|
||||
.vue-grid-item {
|
||||
transition: none !important;
|
||||
}
|
||||
|
|
|
@ -3,26 +3,16 @@ import BasicLayout from "@/layouts/BasicLayout.vue";
|
|||
import Dashboard from "./Dashboard.vue";
|
||||
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 RecentLoginWidget from "./widgets/RecentLoginWidget.vue";
|
||||
import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
||||
import UserStatsWidget from "./widgets/UserStatsWidget.vue";
|
||||
import ViewsStatsWidget from "./widgets/ViewsStatsWidget.vue";
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "dashboardModule",
|
||||
components: [
|
||||
CommentStatsWidget,
|
||||
PostStatsWidget,
|
||||
components: {
|
||||
QuickLinkWidget,
|
||||
RecentLoginWidget,
|
||||
RecentPublishedWidget,
|
||||
UserStatsWidget,
|
||||
ViewsStatsWidget,
|
||||
],
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
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 {
|
||||
IconArrowRight,
|
||||
IconBookRead,
|
||||
|
@ -117,16 +117,18 @@ const actions: Action[] = [
|
|||
</script>
|
||||
<template>
|
||||
<VCard
|
||||
:body-class="['h-full', 'overflow-y-auto', '!p-0']"
|
||||
:body-class="['h-full', 'overflow-y-auto', '@container']"
|
||||
class="h-full"
|
||||
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
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
|
@ -137,14 +139,13 @@ const actions: Action[] = [
|
|||
</span>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h3 class="text-base font-medium">
|
||||
<span aria-hidden="true" class="absolute inset-0"></span>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ action.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<span
|
||||
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 />
|
||||
</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>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
<script lang="ts" setup>
|
||||
import type { DashboardStats } from "@halo-dev/api-client";
|
||||
import { VCard } from "@halo-dev/components";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { VCard, IconEye } from "@halo-dev/components";
|
||||
import { inject, type Ref } from "vue";
|
||||
|
||||
const stats = ref<DashboardStats>({
|
||||
visits: 0,
|
||||
});
|
||||
|
||||
const handleFetchStats = async () => {
|
||||
const { data } = await apiClient.stats.getStats();
|
||||
stats.value = data;
|
||||
};
|
||||
|
||||
onMounted(handleFetchStats);
|
||||
const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
||||
</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">
|
||||
{{ stats.visits || 0 }}
|
||||
</dd>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "menuModule",
|
||||
components: [],
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/menus",
|
||||
|
|
|
@ -7,7 +7,7 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "themeModule",
|
||||
components: [],
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/theme",
|
||||
|
|
|
@ -10,7 +10,7 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "pluginModule",
|
||||
components: [],
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/plugins",
|
||||
|
|
|
@ -5,7 +5,7 @@ import RoleDetail from "./RoleDetail.vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "roleModule",
|
||||
components: [],
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/users",
|
||||
|
|
|
@ -6,7 +6,7 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "settingModule",
|
||||
components: [],
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
path: "/settings",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { definePlugin } from "@halo-dev/console-shared";
|
|||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||
import BlankLayout from "@/layouts/BlankLayout.vue";
|
||||
import UserProfileLayout from "./layouts/UserProfileLayout.vue";
|
||||
import UserStatsWidget from "./widgets/UserStatsWidget.vue";
|
||||
import UserList from "./UserList.vue";
|
||||
import UserDetail from "./UserDetail.vue";
|
||||
import PersonalAccessTokens from "./PersonalAccessTokens.vue";
|
||||
|
@ -11,7 +12,9 @@ import { markRaw } from "vue";
|
|||
|
||||
export default definePlugin({
|
||||
name: "userModule",
|
||||
components: [],
|
||||
components: {
|
||||
UserStatsWidget,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: "/login",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts" name="RecentLoginWidget" setup>
|
||||
<script lang="ts" setup>
|
||||
import { VCard, VAvatar } from "@halo-dev/components";
|
||||
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/aspect-ratio"),
|
||||
require("@formkit/themes/tailwindcss"),
|
||||
require("@tailwindcss/container-queries"),
|
||||
require("tailwindcss-themer")({
|
||||
defaultTheme: {
|
||||
extend: {
|
||||
|
|
|
@ -3,7 +3,6 @@ import fs from "fs";
|
|||
import { defineConfig, loadEnv } from "vite";
|
||||
import Vue from "@vitejs/plugin-vue";
|
||||
import VueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import VueSetupExtend from "vite-plugin-vue-setup-extend";
|
||||
import Compression from "vite-compression-plugin";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import Icons from "unplugin-icons/vite";
|
||||
|
@ -12,7 +11,6 @@ import { setupLibraryExternal } from "./src/build/library-external";
|
|||
export const sharedPlugins = [
|
||||
Vue(),
|
||||
VueJsx(),
|
||||
VueSetupExtend(),
|
||||
Compression(),
|
||||
Icons({
|
||||
compiler: "vue3",
|
||||
|
|
Loading…
Reference in New Issue