init project

This commit is contained in:
林梓阳
2024-07-02 15:39:51 +08:00
commit aeba86bb91
33 changed files with 8946 additions and 0 deletions

18
src/App.vue Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

44
src/global.less Normal file
View 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;
}

View 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>

View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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 };

View File

@@ -0,0 +1,3 @@
<template>
data
</template>

View File

@@ -0,0 +1,3 @@
<template>
create
</template>

View File

@@ -0,0 +1,8 @@
<template>
<div class="normal-container">
111
</div>
<div class="normal-container mt-16">
222
</div>
</template>