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

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_OA_BASEURL = https://oa.shiyuegame.com

1
.env.staging Normal file
View File

@ -0,0 +1 @@
VITE_OA_BASEURL = https://oa-pre.shiyue.com

25
.eslintrc.cjs Normal file
View File

@ -0,0 +1,25 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
semi: 2,
'vue/multi-word-component-names': 0,
indent: [
2, 2, {
SwitchCase: 1,
},
],
'vue/html-indent': 2,
'@typescript-eslint/comma-dangle': [2, 'always-multiline'],
},
};

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
localSet.js

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=http://sy-registry.shiyue.com

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint"
]
}

0
README.md Normal file
View File

20
components.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AFloatButton: typeof import('ant-design-vue/es')['FloatButton']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

8160
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "y-code",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --mode staging",
"build:pre": "vite build --mode staging",
"build": "vite build --mode production",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"ant-design-vue": "^4.1.2",
"axios": "^1.6.7",
"pinia": "^2.1.7",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.10",
"@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"less": "^4.2.0",
"npm-run-all2": "^6.1.1",
"typescript": "~5.3.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.11",
"vue-tsc": "^1.8.27"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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>

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.tsx", "components.d.ts", "*.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "esnext"
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

24
vite.config.ts Normal file
View File

@ -0,0 +1,24 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
Components({
resolvers: [AntDesignVueResolver({
importStyle: 'less',
})],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});