feat: 添加路由权限
自动部署 / build (push) Successful in 1m26s Details

master
luoer 2023-11-08 16:55:45 +08:00
parent 4adfe49747
commit 34b3a73f30
28 changed files with 375 additions and 247 deletions

2
.env
View File

@ -5,7 +5,7 @@
VITE_TITLE = 绝弹项目管理
# 网站副标题
VITE_SUBTITLE = 快速开发web应用的模板工具
# 部署路径
# 部署路径: 当为 ./ 时路由模式需为 hash
VITE_BASE = /
# 接口前缀:参见 axios 的 baseURL
VITE_API = /

View File

@ -14,6 +14,7 @@
- 图标/样式一个类名搞定
- 遵循 Conventional Changelog 规范, 自动生成版本记录文档
- 内置常用 VsCode 代码片段和推荐扩展,提升开发效率
- 支持路由动态打包、路由权限、路由缓存和动态首页
## 快速开始

View File

@ -1,13 +1,36 @@
<template>
<a-config-provider>
<router-view v-slot="{ Component }">
<page-403 v-if="Math.random() > 0.999"></page-403>
<component v-else :is="Component"></component>
<router-view v-slot="{ Component, route }">
<keep-alive :include="menuStore.cacheTopNames">
<component v-if="hasAuth(route, Component)" :is="Component"></component>
<page-403 v-else></page-403>
</keep-alive>
</router-view>
</a-config-provider>
</template>
<script setup lang="ts">
import { RouteLocationNormalizedLoaded } from "vue-router";
import { useUserStore } from "./store";
import { useMenuStore } from "./store/menu";
const userStore = useUserStore();
const menuStore = useMenuStore();
const hasAuth = (route: RouteLocationNormalizedLoaded, c: any) => {
const aAuth = route.meta.auth;
const uAuth = userStore.auth;
if (!aAuth?.length) {
return true;
}
if (aAuth.some((i) => i === "*")) {
return true;
}
if (uAuth.some((i) => aAuth.some((j) => j === i))) {
return true;
}
return false;
};
</script>
<style scoped></style>

View File

@ -5,6 +5,7 @@ import { has, isString } from "lodash-es";
const successCodes = [2000];
const expiredCodes = [4050, 4051];
const resMessageTip = `响应异常,请检查参数或稍后重试!`;
const resGetMessage = `数据获取失败,请检查网络或稍后重试!`;
const reqMessageTip = `请求失败,请检查网络或稍后重试!`;
let logoutTipShowing = false;
@ -49,6 +50,9 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
}
const resMsg = error.response?.data?.message;
let message: string | null = resMsg ?? resMessageTip;
if (error.config?.method === "get") {
message = resGetMessage;
}
if (has(error.config, "resErrorTip")) {
const tip = error.config.resErrorTip;
if (tip) {

View File

@ -0,0 +1,106 @@
<template>
<div class="h-full flex items-center">
<a-empty>
<template #image>
<svg
height="104"
node-id="1"
template-height="104"
template-width="122"
version="1.1"
viewBox="0 0 122 104"
width="122"
xmlns="http://www.w3.org/2000/svg"
>
<defs node-id="20"></defs>
<g node-id="22">
<g node-id="23">
<g node-id="24">
<g node-id="25">
<g node-id="27">
<g node-id="29">
<path
d="M 15.00 82.30 L 14.43 82.07 L 14.20 81.50 L 14.43 80.93 L 15.00 80.70 L 85.00 80.70 L 85.57 80.93 L 85.80 81.50 L 85.57 82.07 L 85.00 82.30 L 15.00 82.30 Z M 89.00 82.30 L 88.43 82.07 L 88.20 81.50 L 88.43 80.93 L 89.00 80.70 L 91.50 80.70 L 92.07 80.93 L 92.30 81.50 L 92.07 82.07 L 91.50 82.30 L 89.00 82.30 Z M 98.00 82.30 L 97.43 82.07 L 97.20 81.50 L 97.43 80.93 L 98.00 80.70 L 107.00 80.70 L 107.57 80.93 L 107.80 81.50 L 107.57 82.07 L 107.00 82.30 L 98.00 82.30 Z M 38.00 89.80 L 37.43 89.57 L 37.20 89.00 L 37.43 88.43 L 38.00 88.20 L 45.00 88.20 L 45.57 88.43 L 45.80 89.00 L 45.57 89.57 L 45.00 89.80 L 38.00 89.80 Z M 49.50 89.80 L 48.93 89.57 L 48.70 89.00 L 48.93 88.43 L 49.50 88.20 L 80.00 88.20 L 80.57 88.43 L 80.80 89.00 L 80.57 89.57 L 80.00 89.80 L 49.50 89.80 Z M 94.20 62.00 L 94.46 61.39 L 95.00 61.20 L 95.54 61.39 L 95.80 62.00 L 95.80 65.00 L 95.57 65.57 L 95.00 65.80 L 92.00 65.80 L 91.39 65.54 L 91.20 65.00 L 91.39 64.46 L 92.00 64.20 L 94.20 64.20 L 94.20 62.00 Z M 95.80 68.00 L 95.54 68.61 L 95.00 68.80 L 94.46 68.61 L 94.20 68.00 L 94.20 65.00 L 94.43 64.43 L 95.00 64.20 L 98.00 64.20 L 98.61 64.46 L 98.80 65.00 L 98.61 65.54 L 98.00 65.80 L 95.80 65.80 L 95.80 68.00 Z M 18.20 38.00 L 18.46 37.39 L 19.00 37.20 L 19.54 37.39 L 19.80 38.00 L 19.80 41.00 L 19.57 41.57 L 19.00 41.80 L 16.00 41.80 L 15.39 41.54 L 15.20 41.00 L 15.39 40.46 L 16.00 40.20 L 18.20 40.20 L 18.20 38.00 Z M 92.30 12.70 L 95.00 12.70 L 95.61 12.96 L 95.80 13.50 L 95.61 14.04 L 95.00 14.30 L 92.30 14.30 L 92.30 17.00 L 92.04 17.61 L 91.50 17.80 L 90.96 17.61 L 90.70 17.00 L 90.70 14.30 L 88.00 14.30 L 87.39 14.04 L 87.20 13.50 L 87.39 12.96 L 88.00 12.70 L 90.70 12.70 L 90.70 10.00 L 90.96 9.39 L 91.50 9.20 L 92.04 9.39 L 92.30 10.00 L 92.30 12.70 Z M 19.80 44.00 L 19.54 44.61 L 19.00 44.80 L 18.46 44.61 L 18.20 44.00 L 18.20 41.00 L 18.43 40.43 L 19.00 40.20 L 22.00 40.20 L 22.61 40.46 L 22.80 41.00 L 22.61 41.54 L 22.00 41.80 L 19.80 41.80 L 19.80 44.00 Z"
fill="#c3cbd6"
fill-rule="nonzero"
group-id="1,2,3,4,6,8"
id="Path-2"
node-id="13"
stroke="none"
target-height="80.6"
target-width="93.6"
target-x="14.2"
target-y="9.2"
/>
<path
d="M 28.29 70.34 L 28.68 70.19 L 29.00 70.34 L 29.15 70.67 L 29.00 71.05 L 27.94 72.11 L 27.59 72.26 L 27.23 72.11 L 26.17 71.05 L 26.02 70.67 L 26.17 70.34 L 26.50 70.19 L 26.88 70.34 L 27.59 71.05 L 28.29 70.34 Z M 26.88 73.17 L 26.50 73.33 L 26.17 73.17 L 26.02 72.85 L 26.17 72.46 L 27.23 71.40 L 27.59 71.26 L 27.94 71.40 L 29.00 72.46 L 29.15 72.85 L 29.00 73.17 L 28.68 73.33 L 28.29 73.17 L 27.59 72.46 L 26.88 73.17 Z M 37.12 18.00 L 37.50 17.85 L 37.83 18.00 L 37.98 18.32 L 37.83 18.71 L 36.77 19.77 L 36.41 19.91 L 36.06 19.77 L 35.00 18.71 L 34.85 18.32 L 35.00 18.00 L 35.32 17.85 L 35.71 18.00 L 36.41 18.71 L 37.12 18.00 Z M 35.71 20.83 L 35.32 20.98 L 35.00 20.83 L 34.85 20.50 L 35.00 20.12 L 36.06 19.06 L 36.41 18.91 L 36.77 19.06 L 37.83 20.12 L 37.98 20.50 L 37.83 20.83 L 37.50 20.98 L 37.12 20.83 L 36.41 20.12 L 35.71 20.83 Z"
fill="#c3cbd6"
fill-rule="nonzero"
group-id="1,2,3,4,6,8"
id="Path复制"
node-id="14"
stroke="none"
target-height="55.480774"
target-width="11.966061"
target-x="26.016972"
target-y="17.845398"
/>
</g>
</g>
</g>
<g node-id="26">
<path
d="M 45.00 35.00 L 77.00 35.00 L 78.18 35.24 L 79.12 35.88 L 79.76 36.82 L 80.00 38.00 L 80.00 71.00 L 79.76 72.18 L 79.12 73.12 L 78.18 73.76 L 77.00 74.00 L 45.00 74.00 L 43.82 73.76 L 42.88 73.12 L 42.24 72.18 L 42.00 71.00 L 42.00 38.00 L 42.24 36.82 L 42.88 35.88 L 43.82 35.24 L 45.00 35.00 Z"
fill="#ffffff"
fill-rule="evenodd"
group-id="1,2,3,5"
id="矩形"
node-id="16"
stroke="#c3cbd6"
stroke-linecap="butt"
stroke-width="1.6"
target-height="39"
target-width="38"
target-x="42"
target-y="35"
/>
<path
d="M 57.00 33.00 L 57.64 32.85 L 58.16 32.48 L 59.05 31.52 L 59.50 31.14 L 60.00 31.00 L 62.00 31.00 L 62.51 31.14 L 62.99 31.52 L 63.91 32.48 L 64.42 32.85 L 65.00 33.00 L 68.00 33.00 L 68.78 33.16 L 69.41 33.59 L 69.84 34.22 L 70.00 35.00 L 70.00 36.00 L 69.84 36.78 L 69.41 37.41 L 68.78 37.84 L 68.00 38.00 L 54.00 38.00 L 53.22 37.84 L 52.59 37.41 L 52.16 36.78 L 52.00 36.00 L 52.00 35.00 L 52.16 34.22 L 52.59 33.59 L 53.22 33.16 L 54.00 33.00 L 57.00 33.00 Z"
fill="#f5f7f9"
fill-rule="evenodd"
group-id="1,2,3,5"
id="路径"
node-id="17"
stroke="#c3cbd6"
stroke-linecap="butt"
stroke-width="1.6"
target-height="7"
target-width="18"
target-x="52"
target-y="31"
/>
<g node-id="28">
<path
d="M 50.83 52.09 L 54.72 55.13 L 50.83 52.09 Z M 60.61 48.15 L 60.63 53.16 L 60.61 48.15 Z M 70.41 51.75 L 66.50 54.95 L 70.41 51.75 Z"
fill="none"
group-id="1,2,3,5,7"
id="路径-7"
node-id="18"
stroke="#c3cad7"
stroke-linecap="round"
stroke-width="2"
target-height="6.9805374"
target-width="19.58184"
target-x="50.827675"
target-y="48.147778"
/>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>
</a-empty>
</div>
</template>

View File

@ -1,106 +0,0 @@
<template>
<a-empty>
<template #image>
<svg
height="104"
node-id="1"
template-height="104"
template-width="122"
version="1.1"
viewBox="0 0 122 104"
width="122"
xmlns="http://www.w3.org/2000/svg"
>
<defs node-id="20"></defs>
<g node-id="22">
<g node-id="23">
<g node-id="24">
<g node-id="25">
<g node-id="27">
<g node-id="29">
<path
d="M 15.00 82.30 L 14.43 82.07 L 14.20 81.50 L 14.43 80.93 L 15.00 80.70 L 85.00 80.70 L 85.57 80.93 L 85.80 81.50 L 85.57 82.07 L 85.00 82.30 L 15.00 82.30 Z M 89.00 82.30 L 88.43 82.07 L 88.20 81.50 L 88.43 80.93 L 89.00 80.70 L 91.50 80.70 L 92.07 80.93 L 92.30 81.50 L 92.07 82.07 L 91.50 82.30 L 89.00 82.30 Z M 98.00 82.30 L 97.43 82.07 L 97.20 81.50 L 97.43 80.93 L 98.00 80.70 L 107.00 80.70 L 107.57 80.93 L 107.80 81.50 L 107.57 82.07 L 107.00 82.30 L 98.00 82.30 Z M 38.00 89.80 L 37.43 89.57 L 37.20 89.00 L 37.43 88.43 L 38.00 88.20 L 45.00 88.20 L 45.57 88.43 L 45.80 89.00 L 45.57 89.57 L 45.00 89.80 L 38.00 89.80 Z M 49.50 89.80 L 48.93 89.57 L 48.70 89.00 L 48.93 88.43 L 49.50 88.20 L 80.00 88.20 L 80.57 88.43 L 80.80 89.00 L 80.57 89.57 L 80.00 89.80 L 49.50 89.80 Z M 94.20 62.00 L 94.46 61.39 L 95.00 61.20 L 95.54 61.39 L 95.80 62.00 L 95.80 65.00 L 95.57 65.57 L 95.00 65.80 L 92.00 65.80 L 91.39 65.54 L 91.20 65.00 L 91.39 64.46 L 92.00 64.20 L 94.20 64.20 L 94.20 62.00 Z M 95.80 68.00 L 95.54 68.61 L 95.00 68.80 L 94.46 68.61 L 94.20 68.00 L 94.20 65.00 L 94.43 64.43 L 95.00 64.20 L 98.00 64.20 L 98.61 64.46 L 98.80 65.00 L 98.61 65.54 L 98.00 65.80 L 95.80 65.80 L 95.80 68.00 Z M 18.20 38.00 L 18.46 37.39 L 19.00 37.20 L 19.54 37.39 L 19.80 38.00 L 19.80 41.00 L 19.57 41.57 L 19.00 41.80 L 16.00 41.80 L 15.39 41.54 L 15.20 41.00 L 15.39 40.46 L 16.00 40.20 L 18.20 40.20 L 18.20 38.00 Z M 92.30 12.70 L 95.00 12.70 L 95.61 12.96 L 95.80 13.50 L 95.61 14.04 L 95.00 14.30 L 92.30 14.30 L 92.30 17.00 L 92.04 17.61 L 91.50 17.80 L 90.96 17.61 L 90.70 17.00 L 90.70 14.30 L 88.00 14.30 L 87.39 14.04 L 87.20 13.50 L 87.39 12.96 L 88.00 12.70 L 90.70 12.70 L 90.70 10.00 L 90.96 9.39 L 91.50 9.20 L 92.04 9.39 L 92.30 10.00 L 92.30 12.70 Z M 19.80 44.00 L 19.54 44.61 L 19.00 44.80 L 18.46 44.61 L 18.20 44.00 L 18.20 41.00 L 18.43 40.43 L 19.00 40.20 L 22.00 40.20 L 22.61 40.46 L 22.80 41.00 L 22.61 41.54 L 22.00 41.80 L 19.80 41.80 L 19.80 44.00 Z"
fill="#c3cbd6"
fill-rule="nonzero"
group-id="1,2,3,4,6,8"
id="Path-2"
node-id="13"
stroke="none"
target-height="80.6"
target-width="93.6"
target-x="14.2"
target-y="9.2"
/>
<path
d="M 28.29 70.34 L 28.68 70.19 L 29.00 70.34 L 29.15 70.67 L 29.00 71.05 L 27.94 72.11 L 27.59 72.26 L 27.23 72.11 L 26.17 71.05 L 26.02 70.67 L 26.17 70.34 L 26.50 70.19 L 26.88 70.34 L 27.59 71.05 L 28.29 70.34 Z M 26.88 73.17 L 26.50 73.33 L 26.17 73.17 L 26.02 72.85 L 26.17 72.46 L 27.23 71.40 L 27.59 71.26 L 27.94 71.40 L 29.00 72.46 L 29.15 72.85 L 29.00 73.17 L 28.68 73.33 L 28.29 73.17 L 27.59 72.46 L 26.88 73.17 Z M 37.12 18.00 L 37.50 17.85 L 37.83 18.00 L 37.98 18.32 L 37.83 18.71 L 36.77 19.77 L 36.41 19.91 L 36.06 19.77 L 35.00 18.71 L 34.85 18.32 L 35.00 18.00 L 35.32 17.85 L 35.71 18.00 L 36.41 18.71 L 37.12 18.00 Z M 35.71 20.83 L 35.32 20.98 L 35.00 20.83 L 34.85 20.50 L 35.00 20.12 L 36.06 19.06 L 36.41 18.91 L 36.77 19.06 L 37.83 20.12 L 37.98 20.50 L 37.83 20.83 L 37.50 20.98 L 37.12 20.83 L 36.41 20.12 L 35.71 20.83 Z"
fill="#c3cbd6"
fill-rule="nonzero"
group-id="1,2,3,4,6,8"
id="Path复制"
node-id="14"
stroke="none"
target-height="55.480774"
target-width="11.966061"
target-x="26.016972"
target-y="17.845398"
/>
</g>
</g>
</g>
<g node-id="26">
<path
d="M 45.00 35.00 L 77.00 35.00 L 78.18 35.24 L 79.12 35.88 L 79.76 36.82 L 80.00 38.00 L 80.00 71.00 L 79.76 72.18 L 79.12 73.12 L 78.18 73.76 L 77.00 74.00 L 45.00 74.00 L 43.82 73.76 L 42.88 73.12 L 42.24 72.18 L 42.00 71.00 L 42.00 38.00 L 42.24 36.82 L 42.88 35.88 L 43.82 35.24 L 45.00 35.00 Z"
fill="#ffffff"
fill-rule="evenodd"
group-id="1,2,3,5"
id="矩形"
node-id="16"
stroke="#c3cbd6"
stroke-linecap="butt"
stroke-width="1.6"
target-height="39"
target-width="38"
target-x="42"
target-y="35"
/>
<path
d="M 57.00 33.00 L 57.64 32.85 L 58.16 32.48 L 59.05 31.52 L 59.50 31.14 L 60.00 31.00 L 62.00 31.00 L 62.51 31.14 L 62.99 31.52 L 63.91 32.48 L 64.42 32.85 L 65.00 33.00 L 68.00 33.00 L 68.78 33.16 L 69.41 33.59 L 69.84 34.22 L 70.00 35.00 L 70.00 36.00 L 69.84 36.78 L 69.41 37.41 L 68.78 37.84 L 68.00 38.00 L 54.00 38.00 L 53.22 37.84 L 52.59 37.41 L 52.16 36.78 L 52.00 36.00 L 52.00 35.00 L 52.16 34.22 L 52.59 33.59 L 53.22 33.16 L 54.00 33.00 L 57.00 33.00 Z"
fill="#f5f7f9"
fill-rule="evenodd"
group-id="1,2,3,5"
id="路径"
node-id="17"
stroke="#c3cbd6"
stroke-linecap="butt"
stroke-width="1.6"
target-height="7"
target-width="18"
target-x="52"
target-y="31"
/>
<g node-id="28">
<path
d="M 50.83 52.09 L 54.72 55.13 L 50.83 52.09 Z M 60.61 48.15 L 60.63 53.16 L 60.61 48.15 Z M 70.41 51.75 L 66.50 54.95 L 70.41 51.75 Z"
fill="none"
group-id="1,2,3,5,7"
id="路径-7"
node-id="18"
stroke="#c3cad7"
stroke-linecap="round"
stroke-width="2"
target-height="6.9805374"
target-width="19.58184"
target-x="50.827675"
target-y="48.147778"
/>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>
</a-empty>
</template>
<script setup lang="ts"></script>

View File

@ -1,7 +1,7 @@
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
import { merge } from "lodash-es";
import { PropType, computed, defineComponent, reactive, ref } from "vue";
import AniEmpty from "../empty/index.vue";
import AniEmpty from "../empty/AniEmpty.vue";
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
import { config } from "./table.config";

View File

@ -1,9 +1,7 @@
<template>
<div class="w-full h-full flex justify-center items-center p-4">
<div class="flex flex-col md:flex-row items-center">
<div v-html="Image404">
</div>
<div v-html="Image404"></div>
<div class="slide-in-bottom">
<h1 class="text-3xl font-bold my-0">404</h1>
<p class="mt-2">页面不存在请检查地址或联系管理员!</p>
@ -28,7 +26,10 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import Image404 from './image-404.svg?raw';
import Image404 from "./image-404.svg?raw";
defineOptions({ name: "AllUncatchedPage" });
const router = useRouter();
</script>

View File

@ -1,32 +1,32 @@
<script lang="tsx">
import { MenuItem, menus } from "@/router";
import { MenuItem } from "@/router";
import { useMenuStore } from "@/store/menu";
export default defineComponent({
name: "LayoutMenu",
setup() {
const selectedKeys = ref<string[]>([]);
const route = useRoute();
const router = useRouter();
const menuStore = useMenuStore();
watch(
() => route.path,
() => {
selectedKeys.value = route.matched.map((i) => i.aliasOf?.path ?? i.path);
selectedKeys.value = route.matched.map((i) => i.path);
},
{ immediate: true }
);
return { selectedKeys };
},
methods: {
goto(route: MenuItem) {
function goto(route: MenuItem) {
if (route.external) {
window.open(route.path, "_blank");
return;
}
this.$router.push(route);
},
router.push(route.path);
}
renderItem(routes: MenuItem[], isTop = false) {
function renderItem(routes: MenuItem[]) {
return routes.map((route) => {
const icon = route.icon ? () => <i class={route.icon} /> : null;
const node: any = route.children?.length ? (
@ -34,30 +34,21 @@ export default defineComponent({
<div class="px-2">
<a-divider margin={6} class="!border-slate-100"></a-divider>
</div>
{this.renderItem(route?.children)}
{renderItem(route?.children)}
</>
) : (
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => this.goto(route)}>
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => goto(route)}>
{route.title}
{false && <span class="text-xs text-slate-400 ml-2">({route.sort})</span>}
</a-menu-item>
);
return node;
});
},
},
}
render() {
return (
<a-menu
style={{ width: "100%" }}
breakpoint="xl"
selectedKeys={this.selectedKeys}
autoOpenSelected={true}
levelIndent={0}
>
{this.renderItem(menus, true)}
return () => (
<a-menu style={{ width: "100%" }} selectedKeys={selectedKeys.value} autoOpenSelected={true} levelIndent={0}>
{renderItem(menuStore.menus)}
</a-menu>
);
},

View File

@ -41,13 +41,16 @@
:collapsible="true"
:collapsed="isCollapsed"
:hide-trigger="false"
@collapse="onCollapse"
@collapse="(val) => (isCollapsed = val)"
>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-1">
<Menu />
</a-scrollbar>
<template #trigger="{ collapsed }">
<i :class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'" class="text-gray-400 text-base hover:text-gray-700"></i>
<i
:class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'"
class="text-gray-400 text-base hover:text-gray-700"
></i>
</template>
</a-layout-sider>
<a-layout class="layout-content flex-1">
@ -57,7 +60,9 @@
<IconSync></IconSync>
</template>
<router-view v-slot="{ Component }">
<component :is="Component"></component>
<keep-alive :include="menuStore.cacheAppNames">
<component :is="Component"></component>
</keep-alive>
</router-view>
</a-spin>
</a-layout-content>
@ -67,23 +72,19 @@
</template>
<script lang="ts" setup>
import { useAppStore, useUserStore } from "@/store";
import { useAppStore } from "@/store";
import { Message } from "@arco-design/web-vue";
import { IconSync } from "@arco-design/web-vue/es/icon";
import Menu from "./components/menu.vue";
import userDropdown from "./components/userDropdown.vue";
import { useMenuStore } from "@/store/menu";
defineOptions({ name: "LayoutPage" });
const appStore = useAppStore();
const userStore = useUserStore();
const menuStore = useMenuStore();
const isCollapsed = ref(false);
const route = useRoute();
const router = useRouter();
const themeConfig = ref({ visible: false });
const isDev = import.meta.env.DEV;
const onCollapse = (val: boolean) => {
isCollapsed.value = val;
};
const buttons = [
{
@ -108,34 +109,6 @@ const buttons = [
},
},
];
const tabButtons = [
{
icon: "icon-park-outline-refresh",
text: "刷新页面",
},
{
icon: "icon-park-outline-full-screen",
text: "全屏显示",
},
{
icon: "icon-park-outline-more",
text: "更多",
},
];
const tagItems = [
{
active: true,
text: "首页",
showClose: false,
},
{
active: false,
text: "评论管理",
showClose: true,
},
];
</script>
<style scoped lang="less">
@ -205,9 +178,11 @@ const tagItems = [
<route lang="json">
{
"meta": {
"name": "LayoutPage",
"sort": 101,
"title": "概览",
"icon": "icon-park-outline-home"
"icon": "icon-park-outline-home",
"keepAlive": true
}
}
</route>

View File

@ -61,6 +61,8 @@ import { useAppStore, useUserStore } from "@/store";
import { FieldRule, Form, Message, Modal, Notification } from "@arco-design/web-vue";
import { reactive } from "vue";
defineOptions({ name: "LoginPage" });
const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
const appStore = useAppStore();
const userStore = useUserStore();
@ -131,6 +133,7 @@ const onSubmitForm = async () => {
<route lang="json">
{
"meta": {
"name": "LoginPage",
"sort": 101,
"title": "登录",
"icon": "icon-park-outline-home"

View File

@ -1,7 +1,7 @@
<template>
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="flex gap-2">
<a-input-search allow-clear placeholder="文件分类" class="mb-2"></a-input-search>
<a-input-search allow-clear placeholder="文件分类" class="mb-2" @search="updateFileCategories"></a-input-search>
<a-button @click="formCtx.open">
<template #icon>
<i class="icon-park-outline-add"></i>
@ -10,42 +10,45 @@
<form-modal></form-modal>
</div>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
<ul class="pl-0 mt-0">
<li
v-for="item in list"
:key="item.code"
:class="{ active: item.id === current?.id }"
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
>
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', item)">
<i class="icon-park-outline-folder-close align-[-2px]"></i>
<span class="flex-1 truncate">{{ item.name }}</span>
</div>
<div class="">
<a-dropdown>
<a-button size="small" type="text">
<template #icon>
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
<a-spin :loading="loading" class="w-full h-full">
<ul v-if="list.length" class="pl-0 mt-0">
<li
v-for="item in list"
:key="item.code"
:class="{ active: item.id === current?.id }"
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
>
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', item)">
<i class="icon-park-outline-folder-close align-[-2px]"></i>
<span class="flex-1 truncate">{{ item.name }}</span>
</div>
<div class="">
<a-dropdown>
<a-button size="small" type="text">
<template #icon>
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
</template>
</a-button>
<template #content>
<a-doption @click="formCtx.open(item)">
<template #icon>
<i class="icon-park-outline-edit"></i>
</template>
修改
</a-doption>
<a-doption class="!text-red-500" @click="onDeleteRow(item)">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</a-doption>
</template>
</a-button>
<template #content>
<a-doption @click="formCtx.open(item)">
<template #icon>
<i class="icon-park-outline-edit"></i>
</template>
修改
</a-doption>
<a-doption class="!text-red-500" @click="onDeleteRow(item)">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</a-doption>
</template>
</a-dropdown>
</div>
</li>
</ul>
</a-dropdown>
</div>
</li>
</ul>
<ani-empty v-else></ani-empty>
</a-spin>
</a-scrollbar>
</div>
</template>
@ -65,12 +68,20 @@ defineProps({
const emit = defineEmits(["change"]);
const list = ref<FileCategory[]>([]);
const loading = ref(false);
const updateFileCategories = async () => {
const res = await api.fileCategory.getFileCategorys({ size: 0 });
list.value = res.data.data ?? [];
list.value.unshift({ id: undefined, name: '全部' } as any)
list.value.length && emit("change", list.value[0]);
try {
loading.value = true;
const res = await api.fileCategory.getFileCategorys({ size: 0 });
list.value = res.data.data ?? [];
list.value.unshift({ id: undefined, name: "全部" } as any);
list.value.length && emit("change", list.value[0]);
} catch {
// nothing to do
} finally {
loading.value = false;
}
};
onMounted(updateFileCategories);

View File

@ -57,7 +57,7 @@
</span>
<span v-else-if="item.status === 'done'" class="text-green-600">
完成(
耗时{{ fileMap.get(item.uid)?.cost || 0 }},
耗时{{ fileMap.get(item.uid)?.cost || 0 }},
平均{{ numeral(fileMap.get(item.uid)?.aspeed || 0).format("0 b") }}/s)
</span>
<span v-else="item.status === 'error'" class="text-red-500">
@ -77,7 +77,7 @@
</ul>
<div v-else class="h-[424px] flex items-center justify-center">
<a-empty description="选择文件后显示"></a-empty>
<ani-empty></ani-empty>
</div>
<template #footer>

View File

@ -10,7 +10,7 @@
<form-modal></form-modal>
</div>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
<ul class="pl-0 mt-0">
<ul v-if="list.length" class="pl-0 mt-0">
<li
v-for="item in list"
:key="item.code"
@ -46,6 +46,7 @@
</div>
</li>
</ul>
<ani-empty v-else></ani-empty>
</a-scrollbar>
</div>
</template>

View File

@ -23,6 +23,8 @@ import { DictType, api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components";
import aniGroup from "./components/group.vue";
defineOptions({ name: "SystemDictPage" })
const current = ref<DictType>();
const onTypeChange = (item: DictType) => {
current.value = item;
@ -129,6 +131,7 @@ const [dictTable, dict] = useAniTable({
<route lang="json">
{
"meta": {
"name": "SystemDictPage",
"sort": 20010,
"title": "字典管理",
"icon": "icon-park-outline-spanner"

View File

@ -15,6 +15,8 @@ import { Table, useTable } from "@/components";
import { Editor as aniEditor } from "@/components/editor";
import dayjs from "dayjs";
defineOptions({ name: "SystemLoglPage" })
const visible = ref(false);
const table = useTable({
data: async (model, paging) => {
@ -132,6 +134,7 @@ const table = useTable({
<route lang="json">
{
"meta": {
"name": "SystemLoglPage",
"sort": 10303,
"title": "登陆日志",
"icon": "icon-park-outline-log"

View File

@ -10,6 +10,8 @@ import { Table, useTable } from "@/components";
import { dayjs } from "@/libs/dayjs";
import { Tag } from "@arco-design/web-vue";
defineOptions({ name: "SystemLogoPage" })
const table = useTable({
data: async (model, paging) => {
return api.log.getLoginLogs({ ...model, ...paging });
@ -81,6 +83,7 @@ const table = useTable({
<route lang="json">
{
"meta": {
"name": "SystemLogoPage",
"sort": 10304,
"title": "操作日志",
"icon": "icon-park-outline-doc-detail"

View File

@ -11,8 +11,9 @@ import { MenuType, MenuTypes } from "@/constants/menu";
import { flatMenus } from "@/router";
import { listToTree } from "@/utils/listToTree";
const menuArr = flatMenus.map((i) => ({ label: i.title, value: i.id }));
defineOptions({ name: 'SystemMenuPage' })
const menuArr = flatMenus.map((i) => ({ label: i.title, value: i.id }));
const expanded = ref(false);
const toggleExpand = () => {
expanded.value = !expanded.value;
@ -234,6 +235,7 @@ const [menuTable, menu] = useAniTable({
<route lang="json">
{
"meta": {
"name": "SystemMenuPage",
"sort": 10302,
"title": "菜单管理",
"icon": "icon-park-outline-add-subtract"

View File

@ -8,6 +8,8 @@
import { api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components";
defineOptions({ name: 'SystemRolePage' })
const [roleTable, roleCtx] = useAniTable({
data: async () => {
return api.role.getRoles();
@ -126,6 +128,7 @@ const [roleTable, roleCtx] = useAniTable({
<route lang="json">
{
"meta": {
"name": "SystemRolePage",
"sort": 10302,
"title": "角色管理",
"icon": "icon-park-outline-key"

View File

@ -11,6 +11,7 @@ import { Table, createColumn, updateColumn, useTable } from "@/components";
import InputAvatar from "./components/avatar.vue";
import { usePassworModal } from "./components/password";
defineOptions({ name: "SystemUserPage" });
const [passModal, passCtx] = usePassworModal();
const table = useTable({
@ -68,17 +69,6 @@ const table = useTable({
search: {
button: true,
items: [
// {
// field: "nickname",
// label: "",
// type: "input",
// nodeProps: {
// placeholder: ''
// },
// itemProps: {
// hideLabel: true
// }
// },
{
field: "nickname",
label: "用户昵称",
@ -196,6 +186,8 @@ const table = useTable({
<route lang="json">
{
"meta": {
"name": "SystemUserPage",
"keepAlive": true,
"sort": 10301,
"title": "用户管理",
"icon": "icon-park-outline-user"

View File

@ -216,7 +216,8 @@ const user = reactive({
"meta": {
"sort": 30401,
"title": "个人设置",
"icon": "icon-park-outline-config"
"icon": "icon-park-outline-config",
"auth": ["1"]
}
}
</route>

View File

@ -1,10 +1,10 @@
import { api } from "@/api";
import { store, useUserStore } from "@/store";
import { useMenuStore } from "@/store/menu";
import { treeFind } from "@/utils/listToTree";
import { treeEach, treeFilter, treeFind } from "@/utils/listToTree";
import { Notification } from "@arco-design/web-vue";
import { Router } from "vue-router";
import { menus } from "../menus";
import { MenuItem, menus } from "../menus";
import { APP_HOME_NAME } from "../routes/base";
import { APP_ROUTE_NAME, routes } from "../routes/page";
import { env } from "@/config/env";
@ -26,34 +26,81 @@ export function useAuthGuard(router: Router) {
router.push({ path: "/login", query: { redirect } });
};
router.beforeEach(async function (to) {
router.beforeEach(async function (to, from) {
const userStore = useUserStore(store);
const menuStore = useMenuStore(store);
// 手动指定直接通过
if (to.meta.auth?.some((i) => i === "*")) {
return true;
}
// 在白名单内直接通过
if (WHITE_LIST.includes(to.path)) {
return true;
}
// 未登陆才能访问的页面
if (UNSIGNIN_LIST.includes(to.path)) {
// 未登陆则允许通过
if (!userStore.accessToken) {
return true;
}
// 已登陆进行提示
Notification.warning({
title: "跳转提示",
content: `您已登陆,如需重新登陆请退出后再操作!`,
});
// 不是从路由跳转的,跳转回首页
if (!from.matched.length) {
return "/";
}
// 已登陆不允许
return false;
}
// 未登录跳转到登陆页面
if (!userStore.accessToken) {
return { path: "/login", query: { redirect: to.path } };
}
// 未获取菜单进行获取
if (!menuStore.menus.length) {
menuStore.setMenus(menus);
// 菜单处理
const authMenus = treeFilter(menus, (item) => {
if (item.path === env.homePath) {
item.path = "/";
}
return true;
});
menuStore.setMenus(authMenus);
menuStore.setHome(env.homePath);
// 路由处理
for (const route of routes) {
router.addRoute(route);
}
// 缓存处理
const topNames: string[] = [];
const appNames: string[] = [];
treeEach(routes, (item, level) => {
const { keepAlive, name } = item.meta ?? {};
if (keepAlive && name) {
if (level === 1) {
topNames.push(name);
} else {
appNames.push(name);
}
}
});
menuStore.setCacheTopNames(topNames);
menuStore.setCacheAppNames(appNames);
// 首页处理
const home = treeFind(routes, (i) => i.path === menuStore.home);
if (home) {
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
@ -62,6 +109,8 @@ export function useAuthGuard(router: Router) {
return router.replace(to.path);
}
}
// 兜底处理
return true;
});

View File

@ -12,6 +12,8 @@ export interface MenuItem {
title?: string;
icon?: string;
external?: boolean;
name?: string;
keepAlive: boolean;
children?: MenuItem[];
}
@ -25,7 +27,7 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
for (const route of routes) {
const { meta = {}, parentMeta, path } = route as any;
const { title, sort, icon } = meta;
const { title, sort, icon, keepAlive = false, name } = meta;
let id = path;
let paths = route.path.split("/");
let parentId = paths.slice(0, -1).join("/");
@ -39,6 +41,7 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
sort,
path,
id: path,
keepAlive: false,
parentId: paths.slice(0, -1).join("/"),
});
} else {
@ -47,7 +50,7 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
parentId = p;
}
}
items.push({ id, title, parentId, path, icon, sort });
items.push({ id, title, parentId, path, icon, sort, keepAlive, name });
}
return items;

View File

@ -6,6 +6,8 @@ export const useMenuStore = defineStore({
state: (): MenuStore => {
return {
menus: [],
cacheAppNames: [],
cacheTopNames: [],
home: "",
};
},
@ -23,7 +25,23 @@ export const useMenuStore = defineStore({
*/
setHome(path: string) {
this.home = path;
}
},
/**
*
* @param names
*/
setCacheTopNames(names: string[]) {
this.cacheTopNames = names;
},
/**
*
* @param names
*/
setCacheAppNames(names: string[]) {
this.cacheAppNames = names;
},
},
});
@ -32,6 +50,14 @@ export interface MenuStore {
*
*/
menus: MenuItem[];
/**
* KeepAlive
*/
cacheTopNames: string[];
/**
* KeepAlive
*/
cacheAppNames: string[];
/**
*
*/

View File

@ -10,6 +10,7 @@ export const useUserStore = defineStore({
avatar: "https://github.com/juetan.png",
accessToken: "",
refreshToken: undefined,
auth: []
};
},
actions: {
@ -75,4 +76,8 @@ export interface UserStore {
*
*/
refreshToken?: string;
/**
*
*/
auth: string[];
}

View File

@ -43,6 +43,7 @@ declare module '@vue/runtime-core' {
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']

View File

@ -47,6 +47,10 @@ declare module "vue-router" {
*
*/
keepAlive?: boolean;
/**
* (keepAlivetrue)
*/
name?: string;
/**
* loading
*/

View File

@ -24,13 +24,18 @@ export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "chil
* @param fn
* @param before 广
*/
export function treeEach(tree: any[], fn: (item: any) => void, before = true) {
export function treeEach<T extends { children?: T[]; [key: string]: any } = any>(
tree: T[],
fn: (item: T, level: number) => void,
before = true,
level = 1
) {
for (const item of tree) {
before && fn(item);
before && fn(item, level);
if (item.children) {
treeEach(item.children, fn);
treeEach(item.children, fn, before, level + 1);
}
!before && fn(item);
!before && fn(item, level);
}
}
@ -59,3 +64,21 @@ export function treeFind<T extends { children?: T[]; [key: string]: any } = any>
}
return data;
}
/**
*
* @param tree
* @param fn
* @returns
*/
export function treeFilter<T extends { children?: T[]; [key: string]: any } = any>(
tree: T[],
fn: (item: T) => boolean
) {
return tree.filter((item) => {
if (item.children) {
item.children = treeFilter(item.children, fn);
}
return fn(item);
});
}