perf: improve dashboard page ui

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/581/head
Ryan Wang 2022-05-27 17:28:25 +08:00
parent 43945378d5
commit 486b4615e5
12 changed files with 634 additions and 31 deletions

View File

@ -21,9 +21,11 @@
"@halo-dev/admin-api": "^1.0.0",
"@vueuse/core": "^8.5.0",
"floating-vue": "2.0.0-beta.16",
"lodash.clonedeep": "^4.5.0",
"pinia": "^2.0.14",
"tippy.js": "^6.3.7",
"vue": "^3.2.36",
"vue-grid-layout": "3.0.0-beta1",
"vue-router": "^4.0.15"
},
"devDependencies": {
@ -31,6 +33,7 @@
"@rushstack/eslint-patch": "^1.1.3",
"@tailwindcss/aspect-ratio": "^0.4.0",
"@types/jsdom": "^16.2.14",
"@types/lodash.clonedeep": "4.5.0",
"@types/node": "^17.0.35",
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",

View File

@ -6,6 +6,7 @@ specifiers:
'@rushstack/eslint-patch': ^1.1.3
'@tailwindcss/aspect-ratio': ^0.4.0
'@types/jsdom': ^16.2.14
'@types/lodash.clonedeep': 4.5.0
'@types/node': ^17.0.35
'@vitejs/plugin-vue': ^2.3.3
'@vitejs/plugin-vue-jsx': ^1.3.10
@ -22,9 +23,11 @@ specifiers:
eslint-plugin-cypress: ^2.12.1
eslint-plugin-vue: ^8.7.1
floating-vue: 2.0.0-beta.16
github-profile-card: ^3.1.0
histoire: ^0.4.6
husky: ^8.0.1
jsdom: ^19.0.0
lodash.clonedeep: ^4.5.0
pinia: ^2.0.14
postcss: ^8.4.14
prettier: ^2.6.2
@ -42,6 +45,7 @@ specifiers:
vite-plugin-pwa: ^0.12.0
vitest: ^0.12.9
vue: ^3.2.36
vue-grid-layout: 3.0.0-beta1
vue-router: ^4.0.15
vue-tsc: ^0.34.16
@ -49,9 +53,12 @@ dependencies:
'@halo-dev/admin-api': 1.0.0
'@vueuse/core': 8.5.0_vue@3.2.36
floating-vue: 2.0.0-beta.16_vue@3.2.36
github-profile-card: 3.1.0
lodash.clonedeep: 4.5.0
pinia: 2.0.14_7gwbvmtszsbbeay4sidijbp26i
tippy.js: 6.3.7
vue: 3.2.36
vue-grid-layout: 3.0.0-beta1
vue-router: 4.0.15_vue@3.2.36
devDependencies:
@ -59,6 +66,7 @@ devDependencies:
'@rushstack/eslint-patch': 1.1.3
'@tailwindcss/aspect-ratio': 0.4.0_tailwindcss@3.0.24
'@types/jsdom': 16.2.14
'@types/lodash.clonedeep': 4.5.0
'@types/node': 17.0.35
'@vitejs/plugin-vue': 2.3.3_vite@2.9.9+vue@3.2.36
'@vitejs/plugin-vue-jsx': 1.3.10
@ -1503,6 +1511,215 @@ packages:
vue: 3.2.36
dev: true
/@interactjs/actions/1.10.11:
resolution: {integrity: sha512-P39zeefr4hkmKx+5nZ+mrH1s0l2YJ3gIHrthXmE81n6MlMa42m0WtHcTms4C5JTTNBP2EEDY+KGgGxSnmJKvUw==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/actions/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-P39zeefr4hkmKx+5nZ+mrH1s0l2YJ3gIHrthXmE81n6MlMa42m0WtHcTms4C5JTTNBP2EEDY+KGgGxSnmJKvUw==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/auto-scroll/1.10.11_@interactjs+utils@1.10.11:
resolution: {integrity: sha512-feHNjhi0EMNLV2nQcEgjYPz2mI54aeSW2RiaoNtFLyBvtXKp0b4DmluwDv6DvuXmUpDwD5g/Hk1gGM2rgl7iqQ==}
peerDependencies:
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/auto-start/1.10.11:
resolution: {integrity: sha512-cIg5CcalCPtC6AiGq6j/0hKUtL2MweEpvw12FuB19sz2Q9Dye0J4GliHKhOYvtumNinnvfVAZ4FZMqZEuX7YZA==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/auto-start/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-cIg5CcalCPtC6AiGq6j/0hKUtL2MweEpvw12FuB19sz2Q9Dye0J4GliHKhOYvtumNinnvfVAZ4FZMqZEuX7YZA==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/core/1.10.11_@interactjs+utils@1.10.11:
resolution: {integrity: sha512-aJ50ccVeszpJt7wPH7Yfqm7f1aG1SA94qd90P0NaESh5/QUXn4CESO6igobo4DFHQ5z+1Rfdl8aphP4JxlH4gw==}
peerDependencies:
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/utils': 1.10.11
dev: false
/@interactjs/dev-tools/1.10.11_6cfv4lzikfbo7rn4cjwgiucbha:
resolution: {integrity: sha512-BP2FNfMbF7zLuOAUGMkDhCo1e1B0fnqyb9ih/Y8yAIJuoLrZxP/9htbsS1vZOIVZ4UgtrId4cYOwfcAZBMQtmw==}
peerDependencies:
'@interactjs/modifiers': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/modifiers': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/dev-tools/1.10.11_ofnz4wzy252z7f2d24rqws4nkq:
resolution: {integrity: sha512-BP2FNfMbF7zLuOAUGMkDhCo1e1B0fnqyb9ih/Y8yAIJuoLrZxP/9htbsS1vZOIVZ4UgtrId4cYOwfcAZBMQtmw==}
peerDependencies:
'@interactjs/modifiers': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/modifiers': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/inertia/1.10.11_63jtl7zryfaftewohpjjqaczxm:
resolution: {integrity: sha512-h+sknCzRqBSyHy4ctPNsq56mxkAMMdwHWD6en7rDEw899gdGKYaXVDVdv1jMfiwNRw0eRFBNoCiol8r3a/a3Jw==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/modifiers': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/modifiers': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/offset': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/interact/1.10.11:
resolution: {integrity: sha512-0iZJ9l547JuBA/lKxK4ARGYVmMqRSsAdA8gXL1zWe51qEIQq8PyWmMipoi8JbDaL7exC2THKwkXu5uq5ndT+iA==}
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/types': 1.10.11
'@interactjs/utils': 1.10.11
dev: false
/@interactjs/interactjs/1.10.11:
resolution: {integrity: sha512-cGOxf6rp3Y8/sk88LhIT0XDn4gCiCzAnUG5Kkj9SAqiUO6BK/9+Wbp1IBkNaPgl/8uG8gNHh/dXBrlBBNcqJAg==}
dependencies:
'@interactjs/actions': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/auto-scroll': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/auto-start': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/dev-tools': 1.10.11_ofnz4wzy252z7f2d24rqws4nkq
'@interactjs/inertia': 1.10.11_63jtl7zryfaftewohpjjqaczxm
'@interactjs/interact': 1.10.11
'@interactjs/modifiers': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/offset': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/pointer-events': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/reflow': 1.10.11_tyfgkkxvwgresm6izb7s2h25im
'@interactjs/utils': 1.10.11
dev: false
/@interactjs/modifiers/1.10.11:
resolution: {integrity: sha512-ltqX1RSqeAIikixlQBlyEUdclT5+rbfIGi3sIdLLYaIZQnltYkWqL9MHKx/w5b+hV+Mc0p5MLUFWJbTdkSCZ9g==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/snappers': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/modifiers/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-ltqX1RSqeAIikixlQBlyEUdclT5+rbfIGi3sIdLLYaIZQnltYkWqL9MHKx/w5b+hV+Mc0p5MLUFWJbTdkSCZ9g==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/snappers': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/offset/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-mBT7eIfy5ivofECiv+VwtEwwIMLV54fT9ujSMWJPduxdSYIHepUWgEf/3zjJknFh6jQc7pqz9dtjvVvyzRCLlQ==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/pointer-events/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-yBT8JJVMZ+MgBay5l1WAHnL8ch/mZsRfaFahti+QFYeQyRloDtsWmEMDSYI/Onyy9+hS3gN/ge77ArGciZZ0Ow==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/reflow/1.10.11_tyfgkkxvwgresm6izb7s2h25im:
resolution: {integrity: sha512-NSCtcCkjImOYSbxzzv2kFqR9t49J8KlhEr9UoePc7GyLbNXsiv3WQ3n0ehZd7CgZXQDiVXnP2UnmIOv5Zd4HQg==}
peerDependencies:
'@interactjs/core': 1.10.11
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/core': 1.10.11_@interactjs+utils@1.10.11
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/snappers/1.10.11:
resolution: {integrity: sha512-yYtOMUZ7aFUZ1IYheq9Tj5hZ4J1r5dnaXhLF44WsI/awQ5L0DjZf07GPWof0B+7rZHEVudxyQNbPfFmb+1K94Q==}
peerDependencies:
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/snappers/1.10.11_@interactjs+utils@1.10.11:
resolution: {integrity: sha512-yYtOMUZ7aFUZ1IYheq9Tj5hZ4J1r5dnaXhLF44WsI/awQ5L0DjZf07GPWof0B+7rZHEVudxyQNbPfFmb+1K94Q==}
peerDependencies:
'@interactjs/utils': 1.10.11
dependencies:
'@interactjs/utils': 1.10.11
optionalDependencies:
'@interactjs/interact': 1.10.11
dev: false
/@interactjs/types/1.10.11:
resolution: {integrity: sha512-YRsVFWjL8Gkkvlx3qnjeaxW4fnibSJ9791g8BA7Pv5ANByI64WmtR1vU7A2rXcrOn8XvyCEfY0ss1s8NhZP+MA==}
dev: false
/@interactjs/utils/1.10.11:
resolution: {integrity: sha512-410ZoxKF+r1roeSelL+WHXfdryUMg5iykC1XwQ3l6XqNw43IMACzyvTH6k6Pwxj7w7x42nce0Qdn1GQ3Y8xyCw==}
dev: false
/@istanbuljs/schema/0.1.3:
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
@ -1790,6 +2007,16 @@ packages:
resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
dev: true
/@types/lodash.clonedeep/4.5.0:
resolution: {integrity: sha512-IHijjFVPJTvzvrNPz+6nQy5lZQb7uh807RfTIEaQBrZXrIGjZy0L2dEb3hju34J0eqbXLCY6Hub/g81Jl4pGCA==}
dependencies:
'@types/lodash': 4.14.182
dev: true
/@types/lodash/4.14.182:
resolution: {integrity: sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==}
dev: true
/@types/markdown-it/12.2.3:
resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
dependencies:
@ -2567,6 +2794,10 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
/batch-processor/1.0.0:
resolution: {integrity: sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=}
dev: false
/bcrypt-pbkdf/1.0.2:
resolution: {integrity: sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=}
dependencies:
@ -3227,6 +3458,12 @@ packages:
resolution: {integrity: sha512-oA7mzccefkvTNi9u7DXmT0LqvhnOiN2BhSrKerta7HeUC1cLoIwtbf2wL+Ah2ozh5KQd3/1njrGrwDBXx6d14Q==}
dev: true
/element-resize-detector/1.2.4:
resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==}
dependencies:
batch-processor: 1.0.0
dev: false
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true
@ -4082,6 +4319,10 @@ packages:
assert-plus: 1.0.0
dev: true
/github-profile-card/3.1.0:
resolution: {integrity: sha512-Ux26iya9zo9dH6HoqxJtztY4rTDBHoevuJESGHOsxAg5b0ucaEhpxpCT5iSIAv732g4fTOwfscr01qa+ngAB/A==}
dev: false
/glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -4842,6 +5083,10 @@ packages:
p-locate: 5.0.0
dev: true
/lodash.clonedeep/4.5.0:
resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=}
dev: false
/lodash.debounce/4.0.8:
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
dev: true
@ -5008,6 +5253,10 @@ packages:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true
/mitt/2.1.0:
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
dev: false
/mkdirp/1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@ -6566,6 +6815,21 @@ packages:
- supports-color
dev: true
/vue-grid-layout/3.0.0-beta1:
resolution: {integrity: sha512-MsW0yfYNtnAO/uDhfZvkP6effxSJxvhAFbIL37x6Rn3vW9xf0WHVefKaSbQMLpSq3mXnR6ut0pg2Cd5lqIIZzg==}
dependencies:
'@interactjs/actions': 1.10.11
'@interactjs/auto-start': 1.10.11
'@interactjs/dev-tools': 1.10.11_6cfv4lzikfbo7rn4cjwgiucbha
'@interactjs/interactjs': 1.10.11
'@interactjs/modifiers': 1.10.11
element-resize-detector: 1.2.4
mitt: 2.1.0
transitivePeerDependencies:
- '@interactjs/core'
- '@interactjs/utils'
dev: false
/vue-resize/2.0.0-alpha.1_vue@3.2.36:
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
peerDependencies:

View File

@ -6,11 +6,16 @@ import router from "./router";
import "@/styles/tailwind.css";
import { Dropdown, Menu, Tooltip, VClosePopper, VTooltip } from "floating-vue";
import "floating-vue/dist/style.css";
// @ts-ignore
import VueGridLayout from "vue-grid-layout";
import Widgets from "@/views/dashboard/widgets";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(VueGridLayout);
app.use(Widgets);
app.directive("tooltip", VTooltip);
app.directive("close-popper", VClosePopper);

View File

@ -4,43 +4,212 @@
<IconDashboard class="self-center mr-2" />
</template>
<template #actions>
<VButton type="secondary"> 设置</VButton>
<VSpace>
<VButton v-if="settings" @click="widgetsModal = true">
<template #icon>
<IconAddCircle class="w-full h-full" />
</template>
添加组件
</VButton>
<VButton type="secondary" @click="settings = !settings">
<template #icon>
<IconSettings v-if="!settings" class="w-full h-full" />
<IconSave v-else class="w-full h-full" />
</template>
{{ settings ? "完成" : "设置" }}
</VButton>
</VSpace>
</template>
</VPageHeader>
<div class="m-4">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div>
<dl class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<VCard>
<dt class="text-sm font-medium text-gray-500 truncate">文章</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">231</dd>
</VCard>
<VCard>
<dt class="text-sm font-medium text-gray-500 truncate">用户</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">10</dd>
</VCard>
<VCard>
<dt class="text-sm font-medium text-gray-500 truncate">评论</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">1423</dd>
</VCard>
<VCard>
<dt class="text-sm font-medium text-gray-500 truncate">访问量</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">1232</dd>
</VCard>
</dl>
</div>
<div>
<VCard title="快速操作">
<ul></ul>
</VCard>
</div>
</div>
<div class="m-4 dashboard">
<grid-layout
v-model:layout="layout"
:col-num="12"
:is-draggable="settings"
:is-resizable="settings"
:margin="[10, 10]"
:responsive="true"
:row-height="30"
:use-css-transforms="true"
:vertical-compact="true"
>
<grid-item
v-for="(item, index) in layout"
:key="index"
:h="item.h"
:i="item.i"
:w="item.w"
:x="item.x"
:y="item.y"
>
<component :is="item.widget" />
<div v-if="settings" class="absolute top-2 right-2">
<IconCloseCircle
class="text-gray-500 hover:text-gray-900 cursor-pointer text-lg"
@click="handleRemove(item)"
/>
</div>
</grid-item>
</grid-layout>
</div>
<VModal v-model:visible="widgetsModal" :width="1280" title="小组件">
<VTabbar
v-model:active-id="activeId"
:items="
widgetsGroup.map((group) => {
return { id: group.id, label: group.label };
})
"
type="outline"
></VTabbar>
<template v-for="(group, groupIndex) in widgetsGroup" :key="groupIndex">
<div v-if="activeId === group.id" class="mt-4">
<VAlert
v-if="group.notice"
:description="group.notice"
class="mb-4"
title="提示"
/>
<grid-layout
:col-num="12"
:is-draggable="false"
:is-resizable="false"
:layout="group.widgets"
:margin="[10, 10]"
:responsive="true"
:row-height="30"
:use-css-transforms="true"
:vertical-compact="true"
>
<grid-item
v-for="(item, index) in group.widgets"
:key="index"
:h="item.h"
:i="item.i"
:w="item.w"
:x="item.x"
:y="item.y"
class="cursor-pointer"
@click="handleAddWidget(item)"
>
<component :is="item.widget" />
</grid-item>
</grid-layout>
</div>
</template>
</VModal>
</template>
<script lang="ts" setup>
import { VSpace } from "@/components/base/space";
import { VButton } from "@/components/base/button";
import { VCard } from "@/components/base/card";
import { VModal } from "@/components/base/modal";
import { VPageHeader } from "@/components/base/header";
import { IconDashboard } from "@/core/icons";
import { VTabbar } from "@/components/base/tabs";
import { VAlert } from "@/components/base/alert";
import {
IconAddCircle,
IconCloseCircle,
IconDashboard,
IconSave,
IconSettings,
} from "@/core/icons";
import { ref } from "vue";
import { useStorage } from "@vueuse/core";
import cloneDeep from "lodash.clonedeep";
const settings = ref(false);
const widgetsModal = ref(false);
const activeId = ref("post");
const widgetsGroup = [
{
id: "post",
label: "文章",
widgets: [
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "PostStatsWidget" },
{ x: 3, y: 0, w: 6, h: 10, i: 1, widget: "RecentPublishedWidget" },
],
},
{
id: "comment",
label: "评论",
widgets: [{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "CommentStatsWidget" }],
},
{
id: "user",
label: "用户",
widgets: [
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "UserStatsWidget" },
{ x: 3, y: 0, w: 6, h: 10, i: 1, widget: "RecentLoginWidget" },
],
},
{
id: "plugin-journal",
label: "日志",
notice: "此组件由插件 plugin-journal 提供",
widgets: [{ x: 0, y: 0, w: 5, h: 8, i: 0, widget: "JournalPublishWidget" }],
},
{
id: "other",
label: "其他",
widgets: [{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "ViewsStatsWidget" }],
},
];
const layout = useStorage("widgets", [
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "PostStatsWidget" },
{ 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: 4,
y: 3,
w: 4,
h: 10,
i: 4,
widget: "RecentPublishedWidget",
},
{ x: 0, y: 3, w: 4, h: 10, i: 5, widget: "RecentLoginWidget" },
{
x: 8,
y: 3,
w: 4,
h: 8,
i: 6,
widget: "JournalPublishWidget",
},
]);
// eslint-disable-next-line
function handleAddWidget(widget: any) {
layout.value = [
...layout.value,
{
...widget,
i: layout.value.length,
},
];
}
// eslint-disable-next-line
function handleRemove(item: any) {
const cloneWidgets = cloneDeep(layout.value);
cloneWidgets.splice(item.i, 1);
// eslint-disable-next-line
layout.value = cloneWidgets.map((widget: any, index: number) => {
return {
...widget,
i: index,
};
});
}
</script>
<style>
.vue-grid-layout {
@apply -m-[10px];
}
</style>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
</script>
<template>
<VCard class="h-full">
<dt class="text-sm font-medium text-gray-500 truncate">评论</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">53</dd>
</VCard>
</template>

View File

@ -0,0 +1,17 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
import { VTextarea } from "@/components/base/textarea";
import { VButton } from "@/components/base/button";
</script>
<template>
<VCard
:bodyClass="['h-full', 'overflow-y-auto']"
class="h-full"
title="日志发表"
>
<VTextarea :rows="6" />
<template #footer>
<VButton type="secondary">发布</VButton>
</template>
</VCard>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
</script>
<template>
<VCard class="h-full">
<dt class="text-sm font-medium text-gray-500 truncate">文章</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">123</dd>
</VCard>
</template>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
import { users } from "@/views/system/users/users-mock";
</script>
<template>
<VCard
:bodyClass="['h-full', '!p-0', 'overflow-y-auto']"
class="h-full"
title="最近登录"
>
<div class="p-4 h-full">
<ul class="divide-y divide-gray-200" role="list">
<li
v-for="(user, index) in users"
:key="index"
class="py-4 cursor-pointer hover:bg-gray-50"
>
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<img
:alt="user.name"
:src="user.avatar"
class="h-10 w-10 rounded"
/>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ user.name }}
</p>
<p class="text-sm text-gray-500 truncate">@{{ user.username }}</p>
</div>
<div>
<time class="text-sm text-gray-500" datetime="2020-01-07 20:00">
2020-01-07 20:00
</time>
</div>
</div>
</li>
</ul>
</div>
</VCard>
</template>

View File

@ -0,0 +1,46 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
import { VSpace } from "@/components/base/space";
import { posts } from "@/views/contents/posts/posts-mock";
</script>
<template>
<VCard
:bodyClass="['h-full', '!p-0', 'overflow-y-auto']"
class="h-full"
title="最近发布"
>
<div class="p-4 h-full">
<ul class="divide-y divide-gray-200" role="list">
<li
v-for="(post, index) in posts"
:key="index"
class="py-4 cursor-pointer hover:bg-gray-50"
>
<div class="flex items-center space-x-4">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ post.title }}
</p>
<div class="flex mt-1">
<VSpace>
<span class="text-xs text-gray-500">
阅读 {{ post.visits }}
</span>
<span class="text-xs text-gray-500">
评论 {{ post.commentCount }}
</span>
</VSpace>
</div>
</div>
<div>
<time class="text-sm text-gray-500" datetime="2020-01-07 20:00">
2020-01-07 20:00
</time>
</div>
</div>
</li>
</ul>
</div>
</VCard>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
</script>
<template>
<VCard class="h-full">
<dt class="text-sm font-medium text-gray-500 truncate">用户</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">5</dd>
</VCard>
</template>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
import { VCard } from "@/components/base/card";
</script>
<template>
<VCard class="h-full">
<dt class="text-sm font-medium text-gray-500 truncate">浏览量</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">123,321</dd>
</VCard>
</template>

View File

@ -0,0 +1,21 @@
import type { App } from "vue";
import PostStatsWidget from "./PostStatsWidget.vue";
import UserStatsWidget from "./UserStatsWidget.vue";
import CommentStatsWidget from "./CommentStatsWidget.vue";
import ViewsStatsWidget from "./ViewsStatsWidget.vue";
import RecentLoginWidget from "./RecentLoginWidget.vue";
import RecentPublishedWidget from "./RecentPublishedWidget.vue";
import JournalPublishWidget from "./JournalPublishWidget.vue";
const install = (app: App) => {
app.component("PostStatsWidget", PostStatsWidget);
app.component("UserStatsWidget", UserStatsWidget);
app.component("CommentStatsWidget", CommentStatsWidget);
app.component("ViewsStatsWidget", ViewsStatsWidget);
app.component("RecentLoginWidget", RecentLoginWidget);
app.component("RecentPublishedWidget", RecentPublishedWidget);
app.component("JournalPublishWidget", JournalPublishWidget);
};
export default install;