feat: 调整路由以文件夹为单位

master
luoer 2024-01-11 17:36:46 +08:00
parent 3ae0869386
commit 21de506907
55 changed files with 2819 additions and 2870 deletions

View File

@ -18,16 +18,12 @@
## 快速开始
1. 确保本地安装有如下软件,推荐最新版本
1. 确保本地安装有如下软件(推荐最新版本)。提示Pnpm 在 NodeJS v16+ 版本可通过 corepack enable 命令开启,低版本请通过 npm install pnpm 命令安装
```bash
# 官网https://git-scm.com/
git
# 官网https://nodejs.org/en
node + pnpm
git # 地址https://git-scm.com/
node + pnpm # 地址https://nodejs.org/en
```
备注Pnpm 在 NodeJS v16+ 版本可通过 corepack enable 命令开启,低版本请通过 npm install pnpm 命令安装。
2. 拉取模板
@ -53,16 +49,20 @@ pnpm dev
### 路由菜单
基于 [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) 插件。本项目使用 src/pages 作为路由目录,最终生成的路由仅有 2 级,主要是出于 keepalive 缓存的需要,其中:
基于 [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) 插件。根据 src/pages 目录生成路由数组,然后根据路由数组自动生成菜单数组,导航时根据菜单层级自动生成导航面包屑。
| 说明 |
| ----------------------------------------------------------------- |
| src/pages 目录下以 _ 开头的文件名/目录名为一级路由,如登陆页面。 |
| src/pages 其他子目录或 .vue 文件为二级路由,如应用首页。 |
根据 src/pages 目录生成路由数组,包含以下以下规则:
左侧菜单,将根据上面的二级路由自动生成,如需生成层级只需在对应目录下的 index.vue 文件中定义如下路由配置:
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
- components 目录会被忽视
- xxx.xx.xx 文件会被忽视,例如 index.my.vue
```
对应目录下的 index.vue 文件中定义如下路由配置:
```jsonc
<route lang="json">
{
"parentMeta": {
@ -72,9 +72,9 @@ pnpm dev
</route>
```
### 文件后缀
### 条件加载
在 scripts/vite/plugin.ts 文件中,内置有一个 VITE 插件,主要用于输出编译信息以及根据不同文件后缀进行打包。在项目根目录下的 .env 配置文件中,可指定以下属性:
基于 [plugin](./scripts/vite/plugin.ts) 内置 VITE 插件,主要用于输出编译信息以及根据不同文件后缀进行打包。在项目根目录下的 .env 配置文件中,可指定以下属性:
```
VITE_EXTENSION = my

View File

@ -12,41 +12,41 @@
"release": "release-it --config ./scripts/release/index.cjs"
},
"devDependencies": {
"@arco-design/web-vue": "^2.51.1",
"@iconify-json/icon-park-outline": "^1.1.12",
"@release-it/conventional-changelog": "^5.1.1",
"@types/ejs": "^3.1.2",
"@types/lodash-es": "^4.17.9",
"@types/nprogress": "^0.2.0",
"@vitejs/plugin-vue": "^4.3.4",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"@vueuse/core": "^9.13.0",
"axios": "^1.5.0",
"dayjs": "^1.11.9",
"@arco-design/web-vue": "^2.54.1",
"@iconify-json/icon-park-outline": "^1.1.15",
"@release-it/conventional-changelog": "^8.0.1",
"@types/ejs": "^3.1.5",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vueuse/core": "^10.7.1",
"axios": "^1.6.5",
"dayjs": "^1.11.10",
"dplayer": "^1.27.1",
"ejs": "^3.1.9",
"less": "^4.2.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.44.0",
"monaco-editor": "^0.45.0",
"nprogress": "^0.2.0",
"numeral": "^2.0.6",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",
"plop": "^3.1.2",
"release-it": "^15.11.0",
"rollup-plugin-visualizer": "^5.9.2",
"swagger-typescript-api": "^12.0.4",
"tsx": "^3.12.9",
"typescript": "^4.9.5",
"unocss": "^0.49.8",
"unplugin-auto-import": "^0.13.0",
"unplugin-vue-components": "^0.23.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"plop": "^4.0.1",
"release-it": "^17.0.1",
"rollup-plugin-visualizer": "^5.12.0",
"swagger-typescript-api": "^13.0.3",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"unocss": "^0.58.3",
"unplugin-auto-import": "^0.17.3",
"unplugin-vue-components": "^0.26.0",
"unplugin-vue-router": "^0.7.0",
"vite": "^4.4.9",
"vite-plugin-pages": "^0.28.0",
"vite": "^5.0.11",
"vite-plugin-pages": "^0.32.0",
"vite-plugin-style-import": "^2.0.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-tsc": "^1.8.11"
"vue": "^3.4.8",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.27"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<template>
<a-config-provider>
<router-view v-slot="{ Component, route }">
<keep-alive :include="menuStore.cacheTopNames">
<keep-alive :include="menuStore.caches">
<component v-if="hasAuth(route)" :is="Component"></component>
<AnForbidden v-else></AnForbidden>
</keep-alive>
@ -11,22 +11,22 @@
<script setup lang="ts">
import { RouteLocationNormalizedLoaded } from 'vue-router';
import { useUserStore } from './store';
import { useMenuStore } from './store/menu';
import { useUserStore } from '@/store/user';
import { useMenuStore } from '@/store/menu';
const userStore = useUserStore();
const menuStore = useMenuStore();
const hasAuth = (route: RouteLocationNormalizedLoaded) => {
const aAuth = route.meta.auth;
const uAuth = userStore.auth;
if (!aAuth?.length) {
const neddAuth = route.meta.auth;
const userAuth = userStore.auth;
if (!neddAuth?.length) {
return true;
}
if (aAuth.some(i => i === '*')) {
if (neddAuth.some(i => i === '*')) {
return true;
}
if (uAuth.some(i => aAuth.some(j => j === i))) {
if (userAuth.some(i => neddAuth.some(j => j === i))) {
return true;
}
return false;

View File

@ -7,7 +7,6 @@ const expiredCodes = [4050, 4051];
const resMessageTip = `响应异常,请检查参数或稍后重试!`;
const resGetMessage = `数据获取失败,请检查网络或稍后重试!`;
const reqMessageTip = `请求失败,请检查网络或稍后重试!`;
let logoutTipShowing = false;
/**
@ -15,6 +14,10 @@ let logoutTipShowing = false;
* @param axios Axios
*/
export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (...args: any[]) => any) {
/**
*
* ()
*/
axios.interceptors.request.use(null, error => {
const msg = error.response?.data?.message;
Notification.error({
@ -24,7 +27,13 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
return Promise.reject(error);
});
/**
*
*/
axios.interceptors.response.use(
/**
*
*/
res => {
const code = res.data?.code;
if (code && !successCodes.includes(code)) {
@ -33,8 +42,12 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
return res;
},
error => {
/**
*
*/
if (error.response) {
const code = error.response.data?.code;
if (expiredCodes.includes(code)) {
if (!logoutTipShowing) {
logoutTipShowing = true;
@ -47,6 +60,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
}
return Promise.reject(error);
}
let message: string | null = resMessageTip;
if (error.config?.method === 'get') {
message = resGetMessage;
@ -70,7 +84,9 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
});
}
return Promise.reject(error);
} else if (error.request) {
}
if (error.request) {
const resMsg = error.response?.message;
let message: string | null = resMsg ?? reqMessageTip;
if (has(error.config, 'reqErrorTip')) {

View File

@ -10,14 +10,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
axios.interceptors.request.use(
config => {
if (config.toast) {
let options: AnToastOptions = {};
if (typeof config.toast === 'string') {
options = { message: config.toast };
}
if (typeof config.toast === 'object') {
options = config.toast;
}
config.closeToast = toast(options);
config.closeToast = toast(config.toast);
}
return config;
},

View File

@ -1,9 +1,18 @@
import setterMap from '../setters';
/**
*
*/
export type SetterMap = typeof setterMap;
/**
*
*/
export type SetterType = keyof SetterMap;
/**
*
*/
export type SetterItemMap = {
[key in SetterType]: {
/**
@ -33,6 +42,11 @@ export type SetterItemMap = {
};
};
export type SetterItem = SetterItemMap[SetterType] | { setter?: undefined; setterProps?: undefined; setterSlots?: undefined };
/**
*
*/
export type SetterItem =
| SetterItemMap[SetterType]
| { setter?: undefined; setterProps?: undefined; setterSlots?: undefined };
export { setterMap };

View File

@ -0,0 +1,34 @@
<template>
<div class="grid grid-rows-[auto_1fr]">
<div class="h-10 bg-white flex items-center gap-2 px-4">
<router-link
v-for="menu in menus"
:key="menu.path"
:to="menu.path"
:class="route.path === menu.path ? `bg-blue-500! text-white` : null"
class="px-2 text-gray-500 leading-[24px] rounded-sm hover:bg-gray-100"
>
<i :class="`${menu.icon}`"></i>
{{ menu.title }}
</router-link>
</div>
<div class="p-4">
<div class="bg-white py-4 px-5">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { useMenuStore } from '@/store/menu';
const route = useRoute();
const menuStore = useMenuStore();
const menus = computed(() => {
const parentPath = route.path.split('/').slice(0, -1).join('/');
const item = menuStore.find(parentPath);
return item?.children ?? [];
});
</script>

View File

@ -85,6 +85,7 @@ export function useSearchForm(
};
const items: AnFormItemProps[] = [];
for (const _item of _items) {
const { searchable, enterable, field, extend, ...itemRest } = _item;
if ((field || extend) === 'submit' && hideSearch) {

View File

@ -0,0 +1,62 @@
<template>
<div class="audio-player flex items-center gap-4 bg-[rgba(255,255,255,.1)] text-white px-4 py-4">
<div
@click="playing = !playing"
class="hover:bg-[rgba(255,255,255,.1)] h-8 px-1.5 flex items-center justify-center rounded"
>
<i v-if="playing" class="text-xl icon-park-outline-pause-one"></i>
<i v-else class="text-xl icon-park-outline-play"></i>
</div>
<div>
{{ currentFormated }}
</div>
<div class="w-96">
<audio ref="audioRef" src="" class="hidden" @timeupdate="onTimeUpdate"></audio>
<a-slider class="block!"></a-slider>
</div>
<div>
{{ durationFormated }}
</div>
<div class="dd">
<a-popover>
<div
@click="onMuteToggle"
class="text-xl hover:bg-[rgba(255,255,255,.1)] h-8 px-1.5 flex items-center justify-center rounded"
>
<i :class="volumeIcon"></i>
</div>
<template #content>
<div class="flex flex-col items-center">
<div class="w-6 text-center">
{{ volume }}
</div>
<a-slider class="min-w-auto!" v-model="volume" direction="vertical"></a-slider>
</div>
</template>
</a-popover>
</div>
</div>
</template>
<script setup lang="ts">
const playing = ref(true);
const volume = ref(50);
const volumeLast = ref(50);
const current = ref(18);
const duration = ref(120);
const currentFormated = computed(() => numeral(current.value).format('0:00'));
const durationFormated = computed(() => numeral(duration.value).format('0:00'));
const volumeIcon = computed(() => {
if (volume.value <= 0) {
return 'icon-park-outline-volume-mute';
} else if (volume.value <= 50) {
return 'icon-park-outline-volume-small';
} else {
return 'icon-park-outline-volume-notice';
}
});
</script>
<style scoped></style>

View File

@ -232,3 +232,4 @@ watch(
</script>
<style scoped></style>
@/pages/content/material/util

View File

@ -5,6 +5,11 @@
<BreadCrumb></BreadCrumb>
<div>
<a-link>需要帮助</a-link>
<a-link @click="router.push({ path: route.path, query: { s: Math.random() }, force: true })">
<template #icon>
<i class="icon-park-outline-refresh"></i>
</template>
</a-link>
</div>
</div>
</div>
@ -21,6 +26,8 @@
<script setup lang="ts">
import BreadCrumb from './bread-crumb.vue';
const route = useRoute();
const router = useRouter();
defineProps({
contentPadding: {
type: Boolean,

View File

@ -0,0 +1,64 @@
<script lang="tsx">
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.path);
},
{ immediate: true }
);
function goto(route: MenuItem) {
menuStore.current = route;
if (route.link) {
window.open(route.link, '_blank');
return;
}
router.push(route.path);
}
function renderItem(routes: MenuItem[], level = 1) {
return routes.map((route): any => {
const icon = route.icon ? () => <i class={route.icon} /> : null;
if (level < 3 && route.children?.some(i => !i.hide)) {
return (
<>
<a-divider margin={6} class="!border-slate-100 px-2"></a-divider>
{renderItem(route?.children, level + 1)}
</>
);
}
return (
<>
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => goto(route)}>
<div class="flex items-center justify-between gap-2">
<div>{route.title}</div>
<div class="text-xs text-gray-400">
{/* <a-badge count={8}>8</a-badge> */}
{route.hide === 'prod' ? <a-tag color="blue">{'开发'}</a-tag> : null}
</div>
</div>
</a-menu-item>
</>
);
});
}
return () => (
<a-menu style={{ width: '100%' }} selectedKeys={selectedKeys.value} autoOpenSelected={true} levelIndent={0}>
{renderItem(menuStore.menus)}
</a-menu>
);
},
});
</script>

View File

@ -1,61 +0,0 @@
<script lang="tsx">
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.path);
},
{ immediate: true }
);
function goto(route: MenuItem) {
if (route.external) {
window.open(route.path, '_blank');
return;
}
router.push(route.path);
}
function renderItem(routes: MenuItem[]) {
return routes.map(route => {
const icon = route.icon ? () => <i class={route.icon} /> : null;
const node: any = route.children?.length ? (
<>
<div class="px-2">
<a-divider margin={6} class="!border-slate-100"></a-divider>
</div>
{renderItem(route?.children)}
</>
) : (
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => goto(route)}>
<div class="flex items-center justify-between gap-2 pr-4">
<div>{route.title}</div>
<div class="text-xs text-gray-400">
{/* <a-badge count={8}>8</a-badge> */}
{ route.only === 'dev' ? '仅开发' : null }
</div>
</div>
</a-menu-item>
);
return node;
});
}
return () => (
<a-menu style={{ width: '100%' }} selectedKeys={selectedKeys.value} autoOpenSelected={true} levelIndent={0}>
{renderItem(menuStore.menus)}
</a-menu>
);
},
});
</script>

View File

@ -11,7 +11,7 @@
</a-button> -->
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-normal">
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-semibold">
{{ appStore.title }}
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
</h1>
@ -70,8 +70,8 @@
<IconSync></IconSync>
</template>
<router-view v-slot="{ Component }">
<keep-alive :include="menuStore.cacheAppNames">
<component v-if="!appStore.pageLoding" :is="Component"></component>
<keep-alive :include="menuStore.caches">
<component :is="Component"></component>
</keep-alive>
</router-view>
</a-spin>
@ -86,11 +86,12 @@ import { useAppStore } from '@/store';
import { useMenuStore } from '@/store/menu';
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 Menu from './Menu.vue';
import userDropdown from './UserDropdown.vue';
defineOptions({ name: 'LayoutPage' });
const route = useRoute()
const appStore = useAppStore();
const menuStore = useMenuStore();
const isCollapsed = ref(false);

View File

@ -1,9 +1,7 @@
<template>
<div></div>
</template>
<script></script>
<route lang="json">
{
"component": null,
"meta": {
"sort": 10300,
"title": "内容管理",

View File

@ -0,0 +1,112 @@
<template>
<bread-page>
<CategoryTable />
</bread-page>
</template>
<script setup lang="tsx">
import { api } from '@/api';
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { listToTree } from '@/utils/listToTree';
const { component: CategoryTable } = useTable({
columns: [
{
title: '分类名称',
dataIndex: 'title',
render: ({ record }) => (
<div class="flex flex-col overflow-hidden">
<span>
{record.name}
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
</span>
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
</div>
),
},
useCreateColumn(),
useUpdateColumn(),
{
type: 'button',
title: '操作',
width: 120,
buttons: [
{
type: 'modify',
text: '修改',
},
{
type: 'delete',
text: '删除',
onClick({ record }) {
return api.category.delCategory(record.id);
},
},
],
},
],
source: async model => {
const res = await api.fileCategory.getFileCategorys(model);
const data = listToTree(res.data.data ?? []);
return { data: { data, total: (res.data as any).total } };
},
search: [
{
field: 'name',
label: '分类名称',
setter: 'search',
enterable: true,
searchable: true,
},
],
create: {
title: '添加分类',
width: 580,
items: [
{
field: 'name',
label: '名称',
setter: 'input',
required: true,
},
{
field: 'code',
label: '别名',
setter: 'input',
required: true,
setterProps: {
placeholder: '只包含字母、小数和连字符',
},
},
{
field: 'description',
label: '备注',
setter: 'textarea',
required: false,
},
],
submit: model => {
return api.fileCategory.addFileCategory(model as any);
},
},
modify: {
extend: true,
title: '修改分类',
submit: model => {
return api.fileCategory.setFileCategory(model.id, model as any);
},
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10300,
"title": "素材分类",
"icon": "icon-park-outline-category-management"
}
}
</route>

View File

@ -25,15 +25,15 @@
</template>
<script setup lang="tsx">
import numeral from 'numeral';
import AnCategory from './AnCategory.vue';
import AnPreview from './AnPreview.vue';
import AnUpload from './AnUpload.vue';
import { FileCategory, api } from '@/api';
import { useCreateColumn, useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
import { FileTypes } from '@/constants/file';
import { Message } from '@arco-design/web-vue';
import numeral from 'numeral';
import AnCategory from './components/AnCategory.vue';
import AnPreview from './components/AnPreview.vue';
import AnUpload from './components/AnUpload.vue';
import { getIcon } from './components/util';
import { getIcon } from './util';
const current = ref<FileCategory>();
const viewer = reactive({ visible: false, url: undefined, type: undefined });
@ -157,7 +157,7 @@ const {
field: 'type',
label: '类型',
setter: 'select',
options: FileTypes,
options: FileTypes.raw,
setterProps: {
style: {
width: '100px',
@ -231,3 +231,4 @@ const {
}
}
</route>
./util

View File

@ -4,9 +4,10 @@
<route lang="json">
{
"only": "dev",
"component": null,
"meta": {
"sort": 120010,
"hide": "prod",
"title": "开发相关",
"icon": "icon-park-outline-home"
}

View File

@ -0,0 +1,17 @@
<template></template>
<template>
<div></div>
</template>
<route lang="json">
{
"component": null,
"meta": {
"sort": 120012,
"title": "前端导航",
"link": "https://nav.juetan.cn",
"icon": "icon-park-outline-mail"
}
}
</route>

View File

@ -16,9 +16,10 @@
<route lang="json">
{
"only": "dev",
"meta": {
"sort": 120010,
"hide": "prod",
"cache": true,
"title": "接口文档",
"icon": "icon-park-outline-api"
}

View File

@ -57,9 +57,10 @@
<div class="flex justify-between gap-4 mt-4">
<ul class="list-none w-full m-0 p-0">
<li v-for="i in 8" class="w-full h-6 items-center overflow-hidden justify-between flex gap-2 mb-2">
<span class="flex-1 truncate hover:underline underline-offset-2 cursor-pointer"
>但是预测已加载的数据不足以</span
>
<a-tag>{{ i }}</a-tag>
<span class="flex-1 truncate hover:underline underline-offset-2 cursor-pointer">
但是预测已加载的数据不足以
</span>
<span class="text-gray-400">3天前</span>
</li>
</ul>
@ -109,7 +110,7 @@ const stat = {
{
"meta": {
"sort": 1000,
"title": "概览",
"title": "首页",
"icon": "icon-park-outline-home"
}
}

View File

@ -4,6 +4,7 @@
<route lang="json">
{
"component": null,
"meta": {
"title": "日志管理",
"icon": "icon-park-outline-log",

View File

@ -0,0 +1,61 @@
<template>
<bread-page>
<a-form :model="{}" :label-col-props="{ span: 3 }" label-align="left" layout="vertical">
<a-form-item label="站点LOGO">
<a-avatar :size="64">
<img :src="appStore.logo" alt="" />
<template #trigger-icon>
<i class="icon-park-outline-edit"></i>
</template>
</a-avatar>
<template #help>提示仅支持 5MB 以内大小, png jpg 格式的图片 </template>
</a-form-item>
<a-form-item label="站点名称">
<a-input
v-model="appStore.title"
placeholder="请输入"
class="!w-[432px]"
allow-clear
:max-length="24"
:show-word-limit="true"
></a-input>
<template #help> 用作系统内显示的名称可在后台修改 </template>
</a-form-item>
<a-form-item label="站点描述">
<a-textarea
v-model="appStore.subtitle"
placeholder="请输入"
class="!w-[432px] h-24"
:max-length="140"
:show-word-limit="true"
></a-textarea>
<template #help> 启用后消息通知将在左上角进行提示. </template>
</a-form-item>
<a-form-item label="站点URL">
<a-input v-model="appStore.title" placeholder="请输入" class="!w-[432px]" allow-clear></a-input>
<template #help> 示例https://www.juetan.cn </template>
</a-form-item>
<a-form-item>
<a-button type="primary">保存修改</a-button>
</a-form-item>
</a-form>
</bread-page>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/app';
const appStore = useAppStore();
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 30400,
"title": "常规设置",
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -0,0 +1,132 @@
<template>
<bread-page>
<!-- <div>
<div class="bg-white">
<div v-for="t1 in types" :key="t1.label" class="flex items-center">
{{ t1.label }}
<div class="flex gap-2">
<a-tag
v-for="t2 in t1.children"
:key="t2.value"
:checked="search.bk === t2.value"
color="blue"
:bordered="true"
checkable
@check="search.bk = t2.value"
>
{{ t2.label }}
</a-tag>
</div>
</div>
</div>
</div> -->
<div class="grid">
<div class="mb-3">功能列表</div>
<div v-for="i in 3" class="border-t py-4 flex justify-between items-center gap-4">
<div class="flex gap-3 items-center">
<div class="p-2 bg-slate-100 rounded">
<i class="icon-park-outline-mail"></i>
</div>
<div>
<div class="text-gray-900 text-base">支付功能</div>
<div class="text-gray-400 mt-2">通知管理员由企业互联的管理员来设置拥有通知业务的最大权限</div>
</div>
</div>
<div>
<a-switch checked-color="#3c9">
<template #checked> 已启用 </template>
<template #unchecked> 未启用 </template>
</a-switch>
</div>
</div>
</div>
</bread-page>
</template>
<script setup lang="ts">
const search = reactive({ bk: undefined as string | undefined });
const types = [
{
label: '板块',
children: [
{
label: '全部',
value: undefined,
},
{
label: '电影',
value: 'fild',
},
{
label: '电视剧',
value: 'vs',
},
{
label: '综艺',
value: 'zy',
},
{
label: '动漫',
value: 'dm',
},
{
label: '短剧',
value: 'dj',
},
{
label: '体育',
value: 'ty',
},
{
label: '纪录片',
value: 'jlp',
},
{
label: '游戏',
value: 'yx',
},
{
label: '新闻',
value: 'xw',
},
{
label: '娱乐',
value: 'yl',
},
{
label: '生活',
value: 'sh',
},
{
label: '音乐',
value: 'yinyue',
},
{
label: '时尚',
value: 'shishang',
},
{
label: '科技',
value: 'keji',
},
{
label: '发现',
value: 'faxian',
},
],
},
];
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 30401,
"title": "插件设置",
"loading": false,
"icon": "icon-park-outline-lightning"
}
}
</route>

View File

@ -0,0 +1,20 @@
<template>
<AnPage></AnPage>
</template>
<script setup lang="tsx"></script>
<style lang="less"></style>
<route lang="json">
{
"redirect": "/setting/common",
"component": null,
"meta": {
"sort": 30401,
"title": "系统设置",
"loading": false,
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -0,0 +1,101 @@
<template>
<bread-page>
<div class="flex">
<a-form
:model="{}"
:label-col-props="{ span: 3 }"
:disabled="!mail.enable"
layout="vertical"
label-align="left"
class="w-[580px]! divide-y divide-gray-100"
>
<a-form-item label="是否启用" :disabled="false">
<a-radio-group v-model="mail.enable">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="服务器和端口">
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
<span class="inline-block px-2">:</span>
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
<template #help>
示例: smtp.163.com:25国内常见有
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>
</template>
</a-form-item>
<a-form-item label="发信人地址">
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
<template #help> 示例: example@mail.com仅作为发送邮件时的发送人标识与登陆无关</template>
</a-form-item>
<a-form-item label="是否需要验证">
<a-radio-group v-model="mail.smtpAuth">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="验证账号">
<a-input
:disabled="!mail.enable || !mail.smtpAuth"
v-model="mail.smtpUser"
placeholder="请输入"
class="!w-[432px]"
></a-input>
<template #help> 示例: example@mail.com企业邮箱请使用企业域名后缀</template>
</a-form-item>
<a-form-item label="验证密码">
<a-input
:disabled="!mail.enable || !mail.smtpAuth"
v-model="mail.smtpPass"
placeholder="请输入"
class="!w-[432px]"
></a-input>
<template #help> 示例AATOLARFABJKYWUY具体请在对应邮箱设置面板进行生成 </template>
</a-form-item>
<a-form-item :disabled="false">
<a-button type="primary"> 保存修改 </a-button>
</a-form-item>
</a-form>
<a-divider direction="vertical" :margin="32"></a-divider>
<div class="flex-1">
<div>
<div class="text-base font-semibold">配置测试</div>
<div class="text-gray-400 mt-1">发送一封测试邮件检测邮件设置是否能正常工作</div>
<div class="mt-6">
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
</div>
<a-textarea placeholder="写点什么..." class="w-[432px]! h-24 mt-2"></a-textarea>
<div class="mt-2">
<a-button type="primary" :disabled="!mail.enable">发送邮件</a-button>
</div>
</div>
</div>
</div>
</bread-page>
</template>
<script setup lang="ts">
const mail = reactive({
enable: true,
smtpHost: '10.10.10.30',
smtpPort: 25,
smtpFrom: 'no-reply@juetan.cn',
smtpAuth: true,
smtpUser: '952222@163.com',
smtpPass: 'FenZyealdsa@s92.',
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 30401,
"title": "邮件设置",
"icon": "icon-park-outline-mail"
}
}
</route>

View File

@ -0,0 +1,182 @@
<template>
<BreadPage>
<UserTable />
<PasswordModal></PasswordModal>
</BreadPage>
</template>
<script setup lang="tsx">
import { api } from '@/api';
import { useFormModal } from '@/components/AnForm';
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
defineOptions({ name: 'SystemDepartmentPage' });
const { component: PasswordModal, open } = useFormModal({
title: '重置密码',
trigger: false,
width: 432,
model: {
id: undefined,
nickname: undefined,
},
items: [
{
field: 'password',
label: '新密码',
setter: 'input',
},
],
submit: model => api.user.setUser(model.id, model as any),
});
const usernameRender: TableColumnRender = ({ record }) => (
<div class="flex items-center gap-4 w-full overflow-hidden">
<a-avatar size={32} class="!bg-brand-500">
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
</a-avatar>
<div class="w-full flex-1 overflow-hidden">
<div>
<span class="cursor-pointer hover:text-brand-500">{record.nickname}</span>
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
</div>
<div class="w-full text-gray-400 space-x-4 text-xs">
<span>
<i class="icon-park-outline-mail mr-1 align-[-4px]"></i>
contact@juetan.cn
</span>
<span>
<i class="icon-park-outline-phone-telephone mr-1"></i>
1591234568
</span>
</div>
</div>
</div>
);
const { component: UserTable } = useTable({
columns: [
{
title: '用户昵称',
dataIndex: 'username',
render: usernameRender,
},
{
...useCreateColumn(),
},
{
...useUpdateColumn(),
},
{
title: '操作',
type: 'button',
width: 200,
align: 'right',
buttons: [
{
text: '重置密码',
onClick: ({ record }) => open(record),
},
{
type: 'modify',
text: '修改',
},
{
type: 'delete',
text: '删除',
onClick: async ({ record }) => {
return api.user.delUser(record.id, { toast: true });
},
},
],
},
],
source: model => {
return api.user.getUsers(model);
},
search: [
{
field: 'nickname',
label: '用户昵称',
setter: 'input',
},
],
create: {
title: '新建用户',
width: 820,
formClass: '!grid grid-cols-2 gap-x-6',
items: [
{
field: 'avatar',
label: '用户头像',
setter: 'input',
setterProps: {
class: 'col-span-2',
},
},
{
field: 'username',
label: '登录账号',
setter: 'input',
required: true,
placeholder: '英文字母+数组组成5~10位',
},
// {
// field: 'password',
// label: '',
// setter: 'input',
// placeholder: '6 ~ 12',
// },
{
field: 'nickname',
label: '用户昵称',
setter: 'input',
},
{
field: 'roleIds',
label: '关联角色',
setter: 'select',
options: () => api.role.getRoles() as any,
setterProps: {
multiple: true,
},
},
{
field: 'description',
label: '个人描述',
setter: 'textarea',
itemProps: {
class: 'col-span-2',
},
setterProps: {
class: 'h-[96px]',
},
},
],
submit: model => {
return api.user.addUser(model as any);
},
},
modify: {
extend: true,
title: '修改用户',
submit: model => {
return api.user.setUser(model.id, model as any);
},
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"name": "SystemDepartmentPage",
"keepAlive": true,
"sort": 10301,
"title": "部门管理",
"icon": "icon-park-outline-group"
}
}
</route>

View File

@ -5,7 +5,7 @@
</div>
<div class="grid grid-cols-[auto_1fr] gap-2 overflow-hidden m-4 rounded">
<div class="bg-white p-4">
<ani-group :current="current" @change="onTypeChange"></ani-group>
<an-group :current="current" @change="onTypeChange"></an-group>
</div>
<div class="bg-white p-4">
<div :show-icon="false" class="rounded mb-3 bg-gray-200 px-4 py-3">
@ -13,9 +13,7 @@
<i class="icon-park-outline-folder-close"></i>
{{ current?.name }}
</span>
<div class="mt-1.5 text-gray-500">
描述{{ current?.description }}
</div>
<div class="mt-1.5 text-gray-500">描述{{ current?.description }}</div>
</div>
<dict-table></dict-table>
</div>
@ -26,7 +24,7 @@
<script setup lang="tsx">
import { DictType, api } from '@/api';
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import aniGroup from './components/group.vue';
import AnGroup from './Group.vue';
defineOptions({ name: 'SystemDictPage' });
const current = ref<DictType>();

View File

@ -4,10 +4,11 @@
<route lang="json">
{
"component": null,
"meta": {
"sort": 20000,
"title": "系统管理",
"icon": "icon-park-outline-config",
"sort": 20000
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -1,6 +1,6 @@
<template>
<bread-page class="">
<menu-table> </menu-table>
<MenuTable> </MenuTable>
</bread-page>
</template>
@ -56,7 +56,8 @@ const { component: MenuTable, tableRef } = useTable({
},
{
title: '类型',
width: 200,
width: 100,
align: 'center',
render: ({ record }) => (
<a-tag bordered color={MenuTypes.fmt(record.type, 'color')}>
{{
@ -96,11 +97,10 @@ const { component: MenuTable, tableRef } = useTable({
source: search => api.menu.getMenus({ ...search, tree: true, size: 0 }),
search: [
{
extend: 'name',
field: 'name',
label: '菜单名称',
required: false,
setterProps: {
placeholder: '菜单名称',
},
setter: 'search',
},
],
create: {

View File

@ -173,7 +173,7 @@ const { component: UserTable } = useTable({
{
"meta": {
"name": "SystemUserPage",
"keepAlive": true,
"cache": true,
"sort": 10301,
"title": "用户管理",
"icon": "icon-park-outline-user"

View File

@ -1,64 +0,0 @@
<template>
<a-form
:model="{}"
:label-col-props="{ span: 3 }"
label-align="left"
layout="vertical"
class="divide-y divide-gray-100"
>
<a-form-item label="站点LOGO">
<a-avatar :size="64">
<img :src="appStore.logo" alt="" />
<template #trigger-icon>
<i class="icon-park-outline-edit"></i>
</template>
</a-avatar>
<template #help>提示仅支持 5MB 以内大小, png jpg 格式的图片 </template>
</a-form-item>
<a-form-item label="站点名称">
<a-input
v-model="appStore.title"
placeholder="请输入"
class="!w-[432px]"
allow-clear
:max-length="24"
:show-word-limit="true"
></a-input>
<template #help> 用作系统内显示的名称可在后台修改 </template>
</a-form-item>
<a-form-item label="站点描述">
<a-textarea
v-model="appStore.subtitle"
placeholder="请输入"
class="!w-[432px] h-24"
:max-length="140"
:show-word-limit="true"
></a-textarea>
<template #help> 启用后消息通知将在左上角进行提示. </template>
</a-form-item>
<a-form-item label="站点URL">
<a-input v-model="appStore.title" placeholder="请输入" class="!w-[432px]" allow-clear></a-input>
<template #help> 示例https://www.juetan.cn </template>
</a-form-item>
<a-form-item>
<a-button type="primary">保存修改</a-button>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/app';
const appStore = useAppStore();
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 30401,
"title": "个人设置",
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -1,99 +0,0 @@
<template>
<div>
<!-- <div class="bg-white ">
<div v-for="t1 in types" :key="t1.label" class="flex items-center">
{{ t1.label }}
<div class="flex gap-2">
<a-tag
v-for="t2 in t1.children"
:key="t2.value"
:checked="search.bk === t2.value"
color="blue"
:bordered="true"
checkable
@check="search.bk = t2.value"
>
{{ t2.label }}
</a-tag>
</div>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
const search = reactive({ bk: undefined });
const types = [
{
label: '板块',
children: [
{
label: '全部',
value: undefined,
},
{
label: '电影',
value: 'fild',
},
{
label: '电视剧',
value: 'vs',
},
{
label: '综艺',
value: 'zy',
},
{
label: '动漫',
value: 'dm',
},
{
label: '短剧',
value: 'dj',
},
{
label: '体育',
value: 'ty',
},
{
label: '纪录片',
value: 'jlp',
},
{
label: '游戏',
value: 'yx',
},
{
label: '新闻',
value: 'xw',
},
{
label: '娱乐',
value: 'yl',
},
{
label: '生活',
value: 'sh',
},
{
label: '音乐',
value: 'yinyue',
},
{
label: '时尚',
value: 'shishang',
},
{
label: '科技',
value: 'keji',
},
{
label: '发现',
value: 'faxian',
},
],
},
];
</script>
<style scoped></style>

View File

@ -1,100 +0,0 @@
<template>
<div class="flex">
<a-form
:model="{}"
:label-col-props="{ span: 3 }"
:disabled="!mail.enable"
layout="vertical"
label-align="left"
class="w-[580px]! divide-y divide-gray-100"
>
<a-form-item label="是否启用" :disabled="false">
<a-radio-group v-model="mail.enable">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="服务器和端口">
<a-input v-model="mail.smtpHost" allow-clear placeholder="请输入" class="!w-[314px]"></a-input>
<span class="inline-block px-2">:</span>
<a-input-number v-model="mail.smtpPort" :min="0" :max="65535" class="w-24!"></a-input-number>
<template #help>
示例: smtp.163.com:25国内常见有
<a target="_blank" class="mr-2" href="https://mail.163.com">网易邮箱</a>
<a target="_blank" class="mr-2" href="http://mail.aliyun.com/">阿里邮箱</a>
<a target="_blank" class="mr-2" href="https://mail.qq.com">QQ邮箱</a>
</template>
</a-form-item>
<a-form-item label="发信人地址">
<a-input v-model="mail.smtpFrom" placeholder="请输入" class="!w-[432px]"></a-input>
<template #help> 示例: example@mail.com仅作为发送邮件时的发送人标识与登陆无关</template>
</a-form-item>
<a-form-item label="是否需要验证">
<a-radio-group v-model="mail.smtpAuth">
<a-radio :value="true"></a-radio>
<a-radio :value="false"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="验证账号">
<a-input
:disabled="!mail.enable || !mail.smtpAuth"
v-model="mail.smtpUser"
placeholder="请输入"
class="!w-[432px]"
></a-input>
<template #help> 示例: example@mail.com企业邮箱请使用企业域名后缀</template>
</a-form-item>
<a-form-item label="验证密码">
<a-input
:disabled="!mail.enable || !mail.smtpAuth"
v-model="mail.smtpPass"
placeholder="请输入"
class="!w-[432px]"
></a-input>
<template #help> 示例AATOLARFABJKYWUY具体请在对应邮箱设置面板进行生成 </template>
</a-form-item>
<a-form-item :disabled="false">
<a-button type="primary"> 保存修改 </a-button>
</a-form-item>
</a-form>
<a-divider direction="vertical" :margin="32"></a-divider>
<div class="flex-1">
<div>
<div class="text-base font-semibold">配置测试</div>
<div class="text-gray-400 mt-1">
发送一封测试邮件检测邮件设置是否能正常工作
</div>
<div class="mt-6">
<a-input placeholder="接收人邮箱" class="w-[432px]!"></a-input>
</div>
<a-textarea placeholder="写点什么..." class="w-[432px]! h-24 mt-2"></a-textarea>
<div class="mt-2">
<a-button type="primary" :disabled="!mail.enable">发送邮件</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const mail = reactive({
enable: true,
smtpHost: '10.10.10.30',
smtpPort: 25,
smtpFrom: 'no-reply@juetan.cn',
smtpAuth: true,
smtpUser: '952222@163.com',
smtpPass: 'FenZyealdsa@s92.',
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 30401,
"title": "个人设置",
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -1,81 +0,0 @@
<template>
<a-tabs
size="large"
class="tabs-page my-page"
:default-active-key="($route.query.tab as string)"
@change="onTabChange"
>
<a-tab-pane key="common" title="常规设置">
<div class="m-4 mt-0 py-4 px-5 bg-white rounded-sm">
<TabCommon></TabCommon>
</div>
</a-tab-pane>
<a-tab-pane key="mail" title="邮件设置">
<div class="m-4 mt-0 py-4 px-5 bg-white rounded-sm">
<TabMail></TabMail>
</div>
</a-tab-pane>
<a-tab-pane key="extra" title="额外功能">
<div class="m-4 mt-0 py-4 px-5 bg-white rounded-sm flex-1 grid px-6">
<div class="mb-3">功能列表</div>
<div v-for="i in 3" class="border-t py-4 flex justify-between items-center gap-4">
<div class="flex gap-3 items-center">
<div class="p-2 bg-slate-100 rounded">
<i class="icon-park-outline-mail"></i>
</div>
<div>
<div class="text-gray-900 text-base">支付功能</div>
<div class="text-gray-400 mt-2">通知管理员由企业互联的管理员来设置拥有通知业务的最大权限</div>
</div>
</div>
<div>
<a-switch checked-color="#3c9">
<template #checked> 已启用 </template>
<template #unchecked> 未启用 </template>
</a-switch>
</div>
</div>
</div>
</a-tab-pane>
</a-tabs>
</template>
<script setup lang="tsx">
import TabCommon from './TabCommon.vue';
import TabMail from './TabMail.vue';
const route = useRoute();
const router = useRouter();
const onTabChange = (val: string | number) => {
router.replace({ query: { tab: val } });
};
</script>
<style lang="less">
.my-page {
.arco-form-item.arco-form-item-error,
.arco-form-item.arco-form-item-has-help {
margin-bottom: 20px;
}
.arco-form-item-message {
margin-top: 4px;
}
.arco-checkbox-icon-hover,
.arco-radio-icon-hover {
margin-top: 4px;
}
.arco-form-item:not(:first-child) {
padding: 20px 0 0;
}
}
</style>
<route lang="json">
{
"meta": {
"sort": 30401,
"title": "个人设置",
"icon": "icon-park-outline-config"
}
}
</route>

View File

@ -88,20 +88,12 @@ export function useAuthGuard(router: Router) {
}
// 缓存处理
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);
}
const { cache, name } = item.meta ?? {};
if (cache && name) {
menuStore.caches.push(name);
}
});
menuStore.setCacheTopNames(topNames);
menuStore.setCacheAppNames(appNames);
// 首页处理
const home = treeFind(routes, i => i.path === menuStore.home);

View File

@ -1,6 +1,6 @@
import { NProgress } from "@/libs/nprogress";
import { useAppStore } from "@/store";
import { Router } from "vue-router";
import { NProgress } from '@/libs/nprogress';
import { useAppStore } from '@/store';
import { Router } from 'vue-router';
const routeMap = new Map<string, boolean>();
@ -10,6 +10,9 @@ export function useProgressGard(router: Router) {
if (routeMap.get(to.fullPath)) {
return true;
}
if (to.meta.loading === false) {
return true;
}
const appStore = useAppStore();
appStore.setPageLoading(true);
});

View File

@ -14,7 +14,9 @@ export interface MenuItem {
external?: boolean;
name?: string;
only?: undefined | 'none' | 'dev';
keepAlive: boolean;
cache?: boolean;
hide?: any;
link?: string;
children?: MenuItem[];
}
@ -26,32 +28,15 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
const items: MenuItem[] = [];
for (const route of routes) {
const { meta = {}, parentMeta, only, path } = route as any;
const { title, sort, icon, keepAlive = false, name } = meta;
let id = path;
let paths = route.path.split('/');
let parentId = paths.slice(0, -1).join('/');
if (parentMeta) {
const { title, icon, sort, only } = parentMeta;
id = `${path}/index`;
parentId = path;
items.push({
title,
icon,
sort,
path,
only,
id: path,
keepAlive: false,
parentId: paths.slice(0, -1).join('/'),
});
} else {
const p = paths.slice(0, -1).join('/');
if (routes.some(i => i.path === p) && parentMeta) {
parentId = p;
}
const { path, meta = {} } = route;
if (meta.hide === true || meta.hide === 'menu') {
continue;
}
items.push({ id, title, parentId, path, icon, sort, only, keepAlive, name });
let parentId = route.path.split('/').slice(0, -1).join('/');
if (!routes.some(i => i.path === parentId)) {
parentId = '';
}
items.push({ ...meta, id: path, parentId, path });
}
return items;
@ -98,6 +83,21 @@ function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key
});
}
function routeToMenus(routes: RouteRecordRaw[]) {
const items: MenuItem[] = [];
for (const route of routes) {
const { path, meta = {} } = route;
const item = { ...meta };
if (route.children) {
item.children = routeToMenus(route.children);
}
items.push({ ...item, id: path, parentId: (route as any).parentPath, path });
}
return items;
}
/**
*
*/
@ -111,4 +111,4 @@ export const treeMenus = listToTree(flatMenus);
/**
*
*/
export const menus = sort(treeMenus);
export const menus = routeToMenus(appRoutes);

View File

@ -2,11 +2,47 @@ import generatedRoutes from 'virtual:generated-pages';
import { RouteRecordRaw } from 'vue-router';
import { routes as vroutes } from 'vue-router/auto/routes';
console.log({vroutes, generatedRoutes});
console.log({ vroutes, generatedRoutes });
export const TOP_ROUTE_PREF = '_';
export const APP_ROUTE_NAME = '_layout';
function treeRoutes(list: RouteRecordRaw[]) {
const map: Record<string, RouteRecordRaw> = {};
const tree: RouteRecordRaw[] = [];
for (const item of list) {
map[item.path] = item;
}
for (const item of list) {
const parentPath = item.path.split('/').slice(0, -1).join('/');
const parent = map[parentPath];
if (parent) {
(item as any).parentPath = parentPath;
(parent.children || (parent.children = [])).push(item);
} else {
tree.push(item);
}
}
return tree;
}
function sortRoutes(routes: RouteRecordRaw[]) {
return routes.sort((prev, next) => {
if (prev.children) {
prev.children = sortRoutes(prev.children);
}
if (next.children) {
next.children = sortRoutes(next.children);
}
const x = prev.meta?.sort ?? 0;
const y = next.meta?.sort ?? 0;
return x - y;
});
}
/**
*
* @description _
@ -27,7 +63,7 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
appRoutes.push(route);
}
return [topRoutes, appRoutes];
return [topRoutes, sortRoutes(treeRoutes(appRoutes))];
};
export const [routes, appRoutes] = transformRoutes(generatedRoutes);

View File

@ -1,14 +1,16 @@
import { MenuItem } from "@/router";
import { defineStore } from "pinia";
import { MenuItem } from '@/router';
import { defineStore } from 'pinia';
export const useMenuStore = defineStore({
id: "menu",
id: 'menu',
state: (): MenuStore => {
return {
menus: [],
cacheAppNames: [],
cacheTopNames: [],
home: "",
current: null,
caches: [],
home: '',
};
},
actions: {
@ -42,6 +44,20 @@ export const useMenuStore = defineStore({
setCacheAppNames(names: string[]) {
this.cacheAppNames = names;
},
find(path: string, menus?: MenuItem[]): MenuItem | null {
let item: MenuItem | null = null;
for (const menu of menus ?? this.menus) {
if (menu.path === path) {
item = menu;
break;
}
if (menu.children) {
item = this.find(path, menu.children);
}
}
return item;
},
},
});
@ -62,4 +78,6 @@ export interface MenuStore {
*
*/
home: string;
current: MenuItem | null;
caches: string[];
}

View File

@ -1,11 +1,11 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
declare module 'vue' {
export interface GlobalComponents {
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
@ -40,6 +40,7 @@ declare module '@vue/runtime-core' {
AnAudio: typeof import('./../components/AnViewer/AnAudio.vue')['default']
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
AnPage: typeof import('./../components/AnPage/AnPage.vue')['default']
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
AnViewer: typeof import('./../components/AnViewer/AnViewer.vue')['default']
APagination: typeof import('@arco-design/web-vue')['Pagination']

View File

@ -1,4 +1,8 @@
// Generated by 'unplugin-auto-import'
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
@ -45,6 +49,7 @@ declare global {
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
@ -59,3 +64,9 @@ declare global {
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -46,23 +46,26 @@ declare module 'vue-router/auto/routes' {
'/content/category/': RouteRecordInfo<'/content/category/', '/content/category', Record<never, never>, Record<never, never>>,
'/content/comment/': RouteRecordInfo<'/content/comment/', '/content/comment', Record<never, never>, Record<never, never>>,
'/content/material/': RouteRecordInfo<'/content/material/', '/content/material', Record<never, never>, Record<never, never>>,
'/content/material-category/': RouteRecordInfo<'/content/material-category/', '/content/material-category', Record<never, never>, Record<never, never>>,
'/content/post/': RouteRecordInfo<'/content/post/', '/content/post', Record<never, never>, Record<never, never>>,
'/dev/': RouteRecordInfo<'/dev/', '/dev', Record<never, never>, Record<never, never>>,
'/dev/editor/': RouteRecordInfo<'/dev/editor/', '/dev/editor', Record<never, never>, Record<never, never>>,
'/dev/nav/': RouteRecordInfo<'/dev/nav/', '/dev/nav', Record<never, never>, Record<never, never>>,
'/dev/openapi/': RouteRecordInfo<'/dev/openapi/', '/dev/openapi', Record<never, never>, Record<never, never>>,
'/home/': RouteRecordInfo<'/home/', '/home', Record<never, never>, Record<never, never>>,
'/log/': RouteRecordInfo<'/log/', '/log', Record<never, never>, Record<never, never>>,
'/log/login/': RouteRecordInfo<'/log/login/', '/log/login', Record<never, never>, Record<never, never>>,
'/log/operation/': RouteRecordInfo<'/log/operation/', '/log/operation', Record<never, never>, Record<never, never>>,
'/setting/': RouteRecordInfo<'/setting/', '/setting', Record<never, never>, Record<never, never>>,
'/setting/common/': RouteRecordInfo<'/setting/common/', '/setting/common', Record<never, never>, Record<never, never>>,
'/setting/function/': RouteRecordInfo<'/setting/function/', '/setting/function', Record<never, never>, Record<never, never>>,
'/setting/mail/': RouteRecordInfo<'/setting/mail/', '/setting/mail', Record<never, never>, Record<never, never>>,
'/system/': RouteRecordInfo<'/system/', '/system', Record<never, never>, Record<never, never>>,
'/system/department/': RouteRecordInfo<'/system/department/', '/system/department', Record<never, never>, Record<never, never>>,
'/system/dict/': RouteRecordInfo<'/system/dict/', '/system/dict', Record<never, never>, Record<never, never>>,
'/system/menu/': RouteRecordInfo<'/system/menu/', '/system/menu', Record<never, never>, Record<never, never>>,
'/system/role/': RouteRecordInfo<'/system/role/', '/system/role', Record<never, never>, Record<never, never>>,
'/system/user/': RouteRecordInfo<'/system/user/', '/system/user', Record<never, never>, Record<never, never>>,
'/user/': RouteRecordInfo<'/user/', '/user', Record<never, never>, Record<never, never>>,
'/user/TabCommon': RouteRecordInfo<'/user/TabCommon', '/user/TabCommon', Record<never, never>, Record<never, never>>,
'/user/TabDemo': RouteRecordInfo<'/user/TabDemo', '/user/TabDemo', Record<never, never>, Record<never, never>>,
'/user/TabMail': RouteRecordInfo<'/user/TabMail', '/user/TabMail', Record<never, never>, Record<never, never>>,
}
}

View File

@ -1,59 +1,68 @@
import "vue-router";
declare module "vue-router" {
interface RouteRecordSingleView {
parentMeta: {
/**
*
*/
title?: string;
/**
*
*/
icon?: string;
/**
*
*/
sort?: number;
};
}
interface RouteMeta {
/**
*
* @description
*
*/
title?: string;
/**
*
* @description
* 使 icon-park-outline
*/
icon?: string;
/**
*
*
* @description
*
*/
sort?: number;
/**
*
* @description
* - false // 不隐藏(默认)
* - true // 在路由和菜单中隐藏,即忽略且不打包
* - 'menu' // 在菜单中隐藏,通过其他方式访问
* - 'prod' // 在生产环境下隐藏
*/
hide?: boolean;
hide?: boolean | 'menu' | 'prod';
/**
*
* @example
* ```js
* ['system:user']
* ```
*/
auth?: string[];
/**
*
*
* @description
* 使 keep-alive
*/
breadcrumb?: boolean;
cache?: boolean;
/**
*
*/
keepAlive?: boolean;
/**
* (keepAlivetrue)
*
* @description
* cachetrue
*/
name?: string;
/**
* loading
* @description
* loading
*/
loading?: boolean | string;
/**
*
* @description
* ```js
* 'https://juetan.cn'
* ```
*/
link?: string;
parentPath?: string;
}
}

View File

@ -12,6 +12,7 @@
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"noImplicitAny": false,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]

View File

@ -1,19 +1,19 @@
import Vue from '@vitejs/plugin-vue';
import VueJsx from '@vitejs/plugin-vue-jsx';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { presetIcons, presetUno } from 'unocss';
import Unocss from 'unocss/vite';
import AutoImport from 'unplugin-auto-import/vite';
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
import AutoComponent from 'unplugin-vue-components/vite';
import router from 'unplugin-vue-router/vite';
import { defineConfig, loadEnv } from 'vite';
import Page from 'vite-plugin-pages';
import { arcoToUnoColor } from './scripts/vite/color';
import iconFile from './scripts/vite/file.json';
import iconFmt from './scripts/vite/fmt.json';
import plugin from './scripts/vite/plugin';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { presetIcons, presetUno } from 'unocss';
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
import { defineConfig, loadEnv } from 'vite';
import { arcoToUnoColor } from './scripts/vite/color';
/**
* vite
@ -24,6 +24,7 @@ export default defineConfig(({ mode }) => {
const base = env.VITE_BASE ?? '/';
const host = env.VITE_HOST ?? '0.0.0.0';
const port = Number(env.VITE_PORT ?? 3020);
return {
base,
plugins: [
@ -33,7 +34,7 @@ export default defineConfig(({ mode }) => {
*/
router({
dts: 'src/types/auto-router.d.ts',
exclude: ['**/components/*'],
exclude: ['**/components/*', '**/*.*.*', '**/!(index).*'],
}),
/**
@ -76,13 +77,26 @@ export default defineConfig(({ mode }) => {
* @see https://github.com/hannoeru/vite-plugin-pages
*/
Page({
exclude: ['**/components/*', '**/*.*.*'],
exclude: ['**/components/*', '**/*.*.*', '**/!(index).*'],
importMode: 'async',
extensions: ['vue'],
onRoutesGenerated(routes) {
if (mode === 'development') {
return routes.filter(route => route.only !== 'none');
const isProd = mode !== 'development';
const result = [];
for (const route of routes) {
const { hide } = route.meta ?? {};
if (!route.meta) {
continue;
}
if (hide === true) {
continue;
}
if (isProd && hide === 'prod') {
continue;
}
result.push(route);
}
return routes.filter(route => !['none', 'dev'].includes(route.only));
return result;
},
}),
@ -155,5 +169,8 @@ export default defineConfig(({ mode }) => {
},
},
},
build: {
chunkSizeWarningLimit: 2000,
},
};
});