feat: 调整路由以文件夹为单位
parent
3ae0869386
commit
21de506907
32
README.md
32
README.md
|
|
@ -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
|
||||
|
|
|
|||
56
package.json
56
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3989
pnpm-lock.yaml
3989
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
16
src/App.vue
16
src/App.vue
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -232,3 +232,4 @@ watch(
|
|||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@/pages/content/material/util
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script></script>
|
||||
<route lang="json">
|
||||
{
|
||||
"component": null,
|
||||
"meta": {
|
||||
"sort": 10300,
|
||||
"title": "内容管理",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"only": "dev",
|
||||
"component": null,
|
||||
"meta": {
|
||||
"sort": 120010,
|
||||
"hide": "prod",
|
||||
"title": "开发相关",
|
||||
"icon": "icon-park-outline-home"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -16,9 +16,10 @@
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"only": "dev",
|
||||
"meta": {
|
||||
"sort": 120010,
|
||||
"hide": "prod",
|
||||
"cache": true,
|
||||
"title": "接口文档",
|
||||
"icon": "icon-park-outline-api"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<route lang="json">
|
||||
{
|
||||
"component": null,
|
||||
"meta": {
|
||||
"title": "日志管理",
|
||||
"icon": "icon-park-outline-log",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ const { component: UserTable } = useTable({
|
|||
{
|
||||
"meta": {
|
||||
"name": "SystemUserPage",
|
||||
"keepAlive": true,
|
||||
"cache": true,
|
||||
"sort": 10301,
|
||||
"title": "用户管理",
|
||||
"icon": "icon-park-outline-user"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
/**
|
||||
* 组件名字(keepAlive为true时必须)
|
||||
* 组件名字
|
||||
* @description
|
||||
* 组件名字,当 cache为true 时必须
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* 是否显示loading
|
||||
* @description
|
||||
* 可以自定义 loading 文本
|
||||
*/
|
||||
loading?: boolean | string;
|
||||
/**
|
||||
* 链接
|
||||
* @description
|
||||
* ```js
|
||||
* 'https://juetan.cn'
|
||||
* ```
|
||||
*/
|
||||
link?: string;
|
||||
parentPath?: string;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue