init project
This commit is contained in:
18
src/App.vue
Normal file
18
src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import { legacyLogicalPropertiesTransformer, StyleProvider } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN" :transformers="[legacyLogicalPropertiesTransformer]">
|
||||
<StyleProvider hash-priority="high">
|
||||
<router-view />
|
||||
</StyleProvider>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
22
src/api/common.ts
Normal file
22
src/api/common.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { get } from '@/utils/request';
|
||||
|
||||
export interface UserInfoType {
|
||||
alias: string;
|
||||
all_dept_name: string;
|
||||
avatar: string;
|
||||
dept: string;
|
||||
dept_id: number;
|
||||
email: string;
|
||||
job: string;
|
||||
job_name: string;
|
||||
job_type: string;
|
||||
mobile: string;
|
||||
user_id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export const getUserInfo = () => get<UserInfoType>({
|
||||
url: '/api/home/grade',
|
||||
});
|
||||
|
||||
export const logout = () => get({ url: '/api/common/logout' });
|
||||
BIN
src/assets/avatar.png
Normal file
BIN
src/assets/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
44
src/global.less
Normal file
44
src/global.less
Normal file
@@ -0,0 +1,44 @@
|
||||
@primary-bg-color: #f8f8f8;
|
||||
|
||||
html, body {
|
||||
background-color: @primary-bg-color;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 选择滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块悬停 */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.normal-container {
|
||||
padding: 16px 24px;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
94
src/layout/components/Header.vue
Normal file
94
src/layout/components/Header.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { HomeOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import avatar from '@/assets/avatar.png';
|
||||
import { OA_BASEURL } from '@/utils/request';
|
||||
import { logout } from '@/api/common';
|
||||
|
||||
const emits = defineEmits(['requestFullscreen']);
|
||||
|
||||
const route = useRoute();
|
||||
const userInfoStore = useUserInfoStore();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout().then(() => {
|
||||
window.location.href = `${OA_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item v-for="item in route.matched" :key="item.path">
|
||||
<home-outlined v-if="item.path === '/' && item.name === 'layout'" />
|
||||
<span v-else>{{ item.meta.title || item.path }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<div class="user-area">
|
||||
<div class="fullscreen-icon-area" @click="emits('requestFullscreen')">
|
||||
<FullscreenOutlined class="fullscreen-icon" />
|
||||
</div>
|
||||
<a-dropdown
|
||||
placement="bottom"
|
||||
>
|
||||
<div style="display: flex;align-items: center;cursor: pointer;">
|
||||
<img :src="userInfoStore.userInfo?.avatar || avatar" class="avatar" />
|
||||
<div>{{ userInfoStore.userInfo?.alias || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="handleLogout">
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a class="back-oa" :href="`${OA_BASEURL}/front/`">
|
||||
返回OA
|
||||
</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 12px 0 rgba(0,0,0,.08);
|
||||
.user-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(51, 51, 51);
|
||||
.fullscreen-icon-area {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #ecf4fe;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
.fullscreen-icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
src/layout/components/Sider.vue
Normal file
59
src/layout/components/Sider.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
function formatMemu(list: RouteType[], path: string = ''): ItemType[] {
|
||||
return list.filter((i) => i.isMenu).map((item) => {
|
||||
const key = item.path.startsWith('/') ? item.path : `${path}/${item.path}`;
|
||||
return {
|
||||
key,
|
||||
icon: item.icon,
|
||||
children: item.children.length ? formatMemu(item.children, key) : void 0,
|
||||
label: item.meta.title || '-',
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ItemType } from 'ant-design-vue';
|
||||
import routeList, { type RouteType } from '@/router/routes';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const selectedKeys = ref<string[]>(['1']);
|
||||
const openKeys = ref<string[]>(['sub1']);
|
||||
const menuList = computed(() => formatMemu(routeList[0].children));
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
watch(() => route.path, (val) => {
|
||||
if (!selectedKeys.value.includes(val)) {
|
||||
selectedKeys.value = [val];
|
||||
openKeys.value = route.matched.slice(1).map((i) => i.path);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const handleClick = (config: { key: string }) => {
|
||||
router.push(config.key);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-menu
|
||||
v-model:openKeys="openKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
:items="menuList"
|
||||
class="sider-root"
|
||||
@click="handleClick"
|
||||
v-bind="$attrs"
|
||||
></a-menu>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sider-root {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
border-inline-end: none !important;
|
||||
}
|
||||
</style>
|
||||
112
src/layout/index.vue
Normal file
112
src/layout/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserInfoStore } from '@/stores/useUserInfoStore';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import Header from './components/Header.vue';
|
||||
import Sider from './components/Sider.vue';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined, FullscreenExitOutlined } from '@ant-design/icons-vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const userInfoStore = useUserInfoStore();
|
||||
const isCollapsed = ref(false);
|
||||
const isFullscreen = ref(false);
|
||||
const container = ref<HTMLDivElement>();
|
||||
|
||||
onMounted(() => {
|
||||
userInfoStore.fetchUserInfo();
|
||||
});
|
||||
|
||||
useEventListener(window, 'fullscreenchange', () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
});
|
||||
|
||||
const handleFullscreen = () => {
|
||||
if (container.value) {
|
||||
container.value.requestFullscreen();
|
||||
}
|
||||
};
|
||||
const handleExitFullscreen = () => {
|
||||
document.exitFullscreen?.();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="root">
|
||||
<header class="header">
|
||||
<Header @requestFullscreen="handleFullscreen" />
|
||||
</header>
|
||||
<section class="left-aside" :class="{ 'left-aside-collapsed': isCollapsed }">
|
||||
<Sider :inlineCollapsed="isCollapsed" />
|
||||
<div class="collapsed-icon">
|
||||
<component :is="isCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined" @click="isCollapsed = !isCollapsed" />
|
||||
</div>
|
||||
</section>
|
||||
<section class="container" :class="{ 'container-fullscreen': isFullscreen, 'container-collapsed': isCollapsed }" ref="container">
|
||||
<router-view />
|
||||
<a-float-button @click="handleExitFullscreen" v-if="isFullscreen">
|
||||
<template #icon>
|
||||
<FullscreenExitOutlined />
|
||||
</template>
|
||||
</a-float-button>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@header-height: 60px;
|
||||
@aside-width: 220px;
|
||||
@aside-width-collapsed: 60px;
|
||||
@header-margin: 12px;
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
.header {
|
||||
height: @header-height;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
}
|
||||
.left-aside {
|
||||
width: @aside-width;
|
||||
position: fixed;
|
||||
top: @header-height;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: calc(100% - @header-height - @header-margin);
|
||||
overflow-y: hidden;
|
||||
margin-top: @header-margin;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 12px 0 rgba(0,0,0,.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
transition: width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
|
||||
.collapsed-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
padding-left: @aside-width + 8px;
|
||||
padding-top: @header-height + @header-margin;
|
||||
height: calc(100% - @header-height - @header-margin);
|
||||
position: relative;
|
||||
background-color: #f8f8f8;
|
||||
transition: padding 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.container-collapsed {
|
||||
padding-left: @aside-width-collapsed + 8px;
|
||||
}
|
||||
.container-fullscreen {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(:where(.css-dev-only-do-not-override-1hsjdkk).ant-menu-inline-collapsed), .left-aside-collapsed {
|
||||
width: @aside-width-collapsed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import './global.less';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app');
|
||||
8
src/router/guards.ts
Normal file
8
src/router/guards.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NavigationGuardWithThis } from 'vue-router';
|
||||
|
||||
const titleGuard: NavigationGuardWithThis<undefined> = (to, from, next) => {
|
||||
next();
|
||||
document.title = to.meta.title ? `${to.meta.title} | 机制系统` : '机制系统';
|
||||
};
|
||||
|
||||
export { titleGuard };
|
||||
13
src/router/index.ts
Normal file
13
src/router/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { titleGuard } from './guards';
|
||||
import routeList from './routes';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(''),
|
||||
routes: routeList,
|
||||
});
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach(titleGuard);
|
||||
|
||||
export default router;
|
||||
71
src/router/routes.ts
Normal file
71
src/router/routes.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import Layout from '@/layout/index.vue';
|
||||
import { HomeOutlined, BarChartOutlined } from '@ant-design/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import type { VNode, RendererNode, RendererElement } from 'vue';
|
||||
|
||||
export interface RouteType {
|
||||
path: string;
|
||||
name: string;
|
||||
component?: any;
|
||||
meta: { title?: string };
|
||||
isMenu?: boolean;
|
||||
redirect?: string;
|
||||
children: RouteType[];
|
||||
icon?: () => VNode<RendererNode, RendererElement, {
|
||||
[key: string]: any;
|
||||
}>
|
||||
}
|
||||
|
||||
const routeList: RouteType[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'layout',
|
||||
component: Layout,
|
||||
meta: { title: '首页' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '-',
|
||||
meta: {},
|
||||
children: [],
|
||||
redirect: '/flow-manager/list',
|
||||
},
|
||||
{
|
||||
path: '/flow-manager',
|
||||
name: 'flow-manager',
|
||||
isMenu: true,
|
||||
meta: { title: '机制管理' },
|
||||
icon: () => h(HomeOutlined),
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'list',
|
||||
component: () => import('@/views/flow-manager/list/index.vue'),
|
||||
meta: { title: '机制列表' },
|
||||
isMenu: true,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'create',
|
||||
component: () => import('@/views/flow-manager/create/index.vue'),
|
||||
meta: { title: '机制创建' },
|
||||
isMenu: true,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/data-overview',
|
||||
name: 'data-overview',
|
||||
isMenu: true,
|
||||
meta: { title: '数据总览' },
|
||||
children: [],
|
||||
icon: () => h(BarChartOutlined),
|
||||
component: () => import('@/views/data-overview/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routeList;
|
||||
16
src/stores/useUserInfoStore.ts
Normal file
16
src/stores/useUserInfoStore.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { readonly, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { getUserInfo } from '@/api/common';
|
||||
import type { UserInfoType } from '@/api/common';
|
||||
|
||||
export const useUserInfoStore = defineStore('userInfoStore', () => {
|
||||
const userInfo = ref<UserInfoType>();
|
||||
|
||||
const fetchUserInfo = () => {
|
||||
getUserInfo().then((res) => {
|
||||
userInfo.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
return { userInfo: readonly(userInfo), fetchUserInfo };
|
||||
});
|
||||
102
src/utils/request.ts
Normal file
102
src/utils/request.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
export interface ResopnseType<T> {
|
||||
reason: string
|
||||
message: string
|
||||
data: T
|
||||
ts: string
|
||||
}
|
||||
export const OA_BASEURL: string = import.meta.env.VITE_OA_BASEURL;
|
||||
|
||||
const requestType = {
|
||||
base: OA_BASEURL,
|
||||
};
|
||||
|
||||
const baseAxios: AxiosInstance = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 100000,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
const errorHandle = (error: AxiosError) => {
|
||||
if (error.response) {
|
||||
const status = error.response?.status;
|
||||
switch (status) {
|
||||
case 401:
|
||||
message.warning('请先登录');
|
||||
window.location.href = `${OA_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
|
||||
break;
|
||||
case 403:
|
||||
message.warning('权限不足');
|
||||
break;
|
||||
case 500:
|
||||
message.warning('服务器出错了…… (>_<)');
|
||||
break;
|
||||
default:
|
||||
message.warning('服务器出错了…… (>_<)');
|
||||
break;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
message.error(error.message);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
//响应拦截器
|
||||
baseAxios.interceptors.response.use((response: AxiosResponse) => {
|
||||
const { data, status } = response;
|
||||
|
||||
if (status !== 200) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
if (data.code) {
|
||||
if (data.code === 200) {
|
||||
return data;
|
||||
} else {
|
||||
message.warning(data.message);
|
||||
return Promise.reject(data);
|
||||
}
|
||||
}
|
||||
}, errorHandle);
|
||||
|
||||
type RequestConfig = Omit<AxiosRequestConfig, 'baseURL'> & { baseURL?: keyof typeof requestType }
|
||||
const request = <T = any>(config: RequestConfig) => {
|
||||
const host = requestType[config.baseURL || 'base'];
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
baseAxios
|
||||
.request<any, T>({ ...config, baseURL: host })
|
||||
.then((res: T) => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const get = <T = any>(config?: RequestConfig) => {
|
||||
return request<ResopnseType<T>>({
|
||||
...config,
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
||||
const post = <T = any>(config?: RequestConfig) =>
|
||||
request<ResopnseType<T>>({
|
||||
...config,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const put = <T = any>(config?: RequestConfig) =>
|
||||
request<ResopnseType<T>>({
|
||||
...config,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const del = <T = any>(config?: { url: string }) => request<ResopnseType<T>>({ ...config, method: 'DELETE' });
|
||||
|
||||
export { get, post, del, put, request };
|
||||
3
src/views/data-overview/index.vue
Normal file
3
src/views/data-overview/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
data
|
||||
</template>
|
||||
3
src/views/flow-manager/create/index.vue
Normal file
3
src/views/flow-manager/create/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
create
|
||||
</template>
|
||||
8
src/views/flow-manager/list/index.vue
Normal file
8
src/views/flow-manager/list/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="normal-container">
|
||||
111
|
||||
</div>
|
||||
<div class="normal-container mt-16">
|
||||
222
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user