This commit is contained in:
tanyp 2023-03-28 09:22:21 +08:00
parent dd6f59222f
commit 841a0dabcb
36 changed files with 1110 additions and 0 deletions

View File

@ -0,0 +1 @@
VITE_BASE_API = 'http://localhost:8081/'

View File

@ -0,0 +1 @@
VITE_BASE_API = 'http://localhost:8081/'

14
tansci-boot-ui/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
.vite-ssg-temp
node_modules
.DS_Store
dist
dist-ssr
*.local
# lock
yarn.lock
package-lock.json
pnpm-lock.yaml
*.log

2
tansci-boot-ui/.npmrc Normal file
View File

@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

13
tansci-boot-ui/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>Tansci Boot</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,31 @@
{
"name": "tansci-boot-ui",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"generate": "vite-ssg build",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"axios": "^1.3.4",
"element-plus": "^2.2.13",
"vue": "^3.2.36",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.4",
"@types/node": "^18.14.0",
"@vitejs/plugin-vue": "^4.0.0",
"sass": "^1.52.1",
"typescript": "^4.7.2",
"unocss": "^0.49.7",
"unplugin-vue-components": "^0.24.0",
"vite": "^4.1.2",
"vite-ssg": "^0.22.1",
"vue-tsc": "^1.1.3"
},
"license": "MIT"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,5 @@
<template>
<el-config-provider namespace="el">
<router-view />
</el-config-provider>
</template>

View File

@ -0,0 +1,52 @@
import request from '@/utils/request'
// token key
const tokenKey:string = 'tansci_boot_token'
// 获取token
export function getToken() {
return sessionStorage.getItem(tokenKey);
}
// 存储token
export function setToken(token:string) {
sessionStorage.setItem(tokenKey, token);
}
// 删除token
export function removeToken() {
sessionStorage.removeItem(tokenKey);
}
// 登录
export function login(data:any){
return new Promise((resolve, reject) => {
request({
url: '/system/security/login',
method: 'post',
data: {
username: data.username,
password: data.password,
code: data.code,
uuid: data.uuid
}
}).then((res:any) => {
var token = res.data
setToken(token)
resolve(token)
}).catch((e:any) => {
reject(e)
})
})
}
// 登出
export function logout(){
request({
url: '/system/security/logout',
method: 'get'
}).then(() => {
removeToken()
location.reload()
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import {defineProps} from 'vue'
const props = defineProps({
data: Array
})
</script>
<template>
<el-sub-menu :index="data.path">
<template #title>
<el-icon v-if="data.icon" style="vertical-align: middle;">
<component :is="data.icon"></component>
</el-icon>
<span style="vertical-align: middle;">{{data.meta.title}}</span>
</template>
<template v-for="item in data.children" :key="item">
<el-menu-item v-if="!item.children || item.children.length <= 1" :index="item.path">
<el-icon v-if="item.icon" style="vertical-align: middle;">
<component :is="item.icon"></component>
</el-icon>
<span style="vertical-align: middle;">{{item.meta.title}}</span>
</el-menu-item>
<Submenu v-else :data='item'></Submenu>
</template>
</el-sub-menu>
</template>

View File

@ -0,0 +1,158 @@
<script setup lang="ts">
import { watch, reactive, toRefs } from "vue"
import { TabsPaneContext } from "element-plus"
import { useRoute, useRouter } from "vue-router"
import { HOME_URL, TABS_BLACK_LIST } from "@/config/config"
const route = useRoute();
const router = useRouter();
const state = reactive({
tabsMenuValue: HOME_URL,
tabsMenuList:[
{title:'首页', path: HOME_URL, icon:'HomeFilled', close: false}
]
})
const {tabsMenuValue, tabsMenuList} = toRefs(state)
// 退/ tabsMenuValue
watch(
() => route.path,
() => {
let params = {
title: route.meta.title as string,
path: route.path,
close: true
};
onAddTabManu(params);
},
{
immediate: true
}
)
function onAddTabManu(tabItem: any){
if (TABS_BLACK_LIST.includes(tabItem.path)) return;
if (state.tabsMenuList.every(item => item.path !== tabItem.path)) {
state.tabsMenuList.push(tabItem);
}
state.tabsMenuValue = tabItem.path;
router.push(tabItem.path);
}
const onTabMenuClick = (tabItem: TabsPaneContext) =>{
let path = tabItem.props.name as string;
router.push(path);
}
const onTabMenuRemove = (tabItem: String) =>{
let _tabsMenuValue = state.tabsMenuValue;
let _tabsMenuList = state.tabsMenuList;
if (_tabsMenuValue === tabItem) {
_tabsMenuList.forEach((item, index) => {
if (item.path !== tabItem) return;
let nextTab = _tabsMenuList[index + 1] || _tabsMenuList[index - 1];
if (!nextTab) return;
_tabsMenuValue = nextTab.path;
router.push(nextTab.path);
});
}
state.tabsMenuValue = _tabsMenuValue;
state.tabsMenuList = _tabsMenuList.filter(item => item.path !== tabItem);
}
const onCloseCurrentTab = () =>{
if (state.tabsMenuValue === HOME_URL) return;
onTabMenuRemove(state.tabsMenuValue);
}
const onCloseOtherTab = () =>{
state.tabsMenuList = state.tabsMenuList.filter(item => {
return item.path === state.tabsMenuValue || item.path === HOME_URL;
});
}
const onCloseAllTab = () =>{
state.tabsMenuList = state.tabsMenuList.filter(item => {
return item.path === HOME_URL;
});
router.push(HOME_URL);
}
</script>
<template>
<div class="tabs-menu">
<el-tabs v-model="tabsMenuValue" type="card" @tab-click="onTabMenuClick" @tab-remove="onTabMenuRemove">
<el-tab-pane v-for="item in tabsMenuList"
:key="item.path"
:path="item.path"
:label="item.title"
:name="item.path"
:closable="item.close">
<template #label>
<el-icon v-if="item.icon" style="vertical-align: middle; padding-right: 0.2rem;">
<component :is="item.icon"></component>
</el-icon>
<span style="vertical-align: middle">{{ item.title }}</span>
</template>
</el-tab-pane>
</el-tabs>
<el-dropdown trigger="hover">
<el-button size="small" type="primary">
<span>更多</span>
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="CircleCloseFilled" @click="onCloseCurrentTab">关闭当前</el-dropdown-item>
<el-dropdown-item icon="CircleClose" @click="onCloseOtherTab">关闭其他</el-dropdown-item>
<el-dropdown-item icon="CloseBold" @click="onCloseAllTab">关闭所有</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<style lang="scss" >
.tabs-menu{
position: relative;
width: 100%;
// border-top: 1px transparent solid;
// border-image: linear-gradient(to right, var(--bg1),#DCDFE6, var(--bg1)) 1 10;
// box-shadow: rgba(27, 31, 35, 0.04) 0px 1px 0px, rgba(255, 255, 255, 0.25) 0px 1px 0px inset;
// margin-bottom: 0.2rem;
.el-dropdown {
position: absolute;
top: 8px;
right: 13px;
}
.el-tabs__nav-wrap {
position: absolute;
width: calc(100% - 120px);
}
.el-tabs--card > .el-tabs__header {
box-sizing: border-box;
height: 40px;
padding: 0;
margin: 0;
}
.el-tabs--card > .el-tabs__header .el-tabs__nav {
border: none;
}
.el-tabs--card > .el-tabs__header .el-tabs__item {
border: none;
}
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
color: var(--theme);
border: none;
}
.el-tabs__item .is-icon-close svg {
margin-top: 0.5px;
}
}
</style>

View File

@ -0,0 +1,153 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeMount, onDeactivated } from "vue"
import { isDark, toggleDark } from '@/composables'
import { useRouter } from 'vue-router'
import Submenu from "@/components/Submenu.vue"
import TabsMenu from "@/components/TabsMenu.vue"
const router = useRouter()
const logo = new URL('../../assets/image/logo.png', import.meta.url).href
const state = reactive({
headerHeight: '52px',
asideWidth: '180px',
routers: [],
defaultHeight: null,
})
onBeforeMount(() => {
state.defaultHeight = (document.body.clientHeight || document.documentElement.clientHeight);
})
onMounted(()=>{
//
let routers:any = [];
let _routes = router.options.routes;
_routes.forEach((item:any)=>{
if(item.children && item.type == 0){
routers.push(item)
}
})
state.routers = routers
window.addEventListener('resize', onDefaultHeight);
})
onDeactivated(()=>{
window.removeEventListener('resize', onDefaultHeight, false)
})
function onDefaultHeight(){
state.defaultHeight = window.innerHeight
}
</script>
<template>
<div class="layout-container">
<el-container>
<el-header :height="state.headerHeight">
<div class="header-logo">
<el-image :src="logo"></el-image>
</div>
<div class="header-content">
<div class="header-dark">
<el-button @click="toggleDark()" link type="primary">
<template #icon>
<el-icon><Sunny v-show="!isDark"/></el-icon>
<el-icon><Moon v-show="isDark"/></el-icon>
</template>
</el-button>
</div>
<div class="header-login">
<el-button circle>
<template #icon>
<el-icon><UserFilled /></el-icon>
</template>
</el-button>
</div>
</div>
</el-header>
<el-container>
<el-aside :width="state.asideWidth" :style="{height: state.defaultHeight+'px'}">
<el-menu router :default-active="$route.path">
<template v-for="item in state.routers" :key="item">
<el-menu-item v-if="!item.children || item.children.length <= 1" :index="item.path">
<el-icon v-if="item.icon" style="vertical-align: middle;">
<component :is="item.icon"></component>
</el-icon>
<span style="vertical-align: middle;">{{item.meta.title}}</span>
</el-menu-item>
<Submenu v-else :data="item"></Submenu>
</template>
</el-menu>
</el-aside>
<el-main>
<TabsMenu></TabsMenu>
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="scss" scoped>
.layout-container{
.el-header{
display: flex;
justify-content: space-between;
line-height: 52px;
background: var(--el-bg-color);
.header-logo{
display: flex;
height: 52px;
line-height: 52px;
padding-left: 0.2rem;
cursor: pointer;
}
.header-content{
display: flex;
justify-content: right;
}
}
.el-aside{
background: var(--el-bg-color);
::v-deep .el-menu{
margin: 0 0.6rem;
padding: 0 0.2rem;
border-right: none;
background: var(--el-bg-color);
.el-menu-item, .el-sub-menu__title {
border-radius: 0.2rem;
height: 36px;
line-height: 36px;
margin: 0.4rem 0;
}
.el-menu-item, .el-menu-item-group__title, .el-sub-menu, .el-sub-menu__title{
padding-left: 0;
}
.el-sub-menu__title:hover{
background: #fff !important;
color: var(--theme) !important;
}
.el-menu-item:hover{
background: #fff !important;
color: var(--theme) !important;
}
.el-menu-item.is-active {
background: #fff;
}
}
}
.el-aside::-webkit-scrollbar{
width: 0px;
}
.el-main{
background: var(--el-bg-color);
padding: 0;
overflow-x: hidden;
overflow-y: auto;
}
.el-main::-webkit-scrollbar{
width: 0px;
}
}
</style>

View File

@ -0,0 +1,4 @@
import { useDark, useToggle } from '@vueuse/core'
export const isDark = useDark()
export const toggleDark = useToggle(isDark)

View File

@ -0,0 +1 @@
export * from './dark'

View File

@ -0,0 +1,8 @@
// 全局不动配置项 只做导出不做修改
// 首页地址(默认)
export const HOME_URL: string = "/index";
// Tabs黑名单地址不需要添加到 tabs 的路由地址)
export const TABS_BLACK_LIST: string[] = ["/404", "/500", "/login"];

8
tansci-boot-ui/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import { DefineComponent } from "vue";
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -0,0 +1,20 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './styles/index.scss'
import * as ElIcons from '@element-plus/icons-vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'uno.css'
const app = createApp(App)
app.use(router)
app.use(ElementPlus,{
locale: zhCn,
size: 'default',
})
for (const icon in ElIcons) {
app.component(icon, (ElIcons as any)[icon])
}
app.mount('#app');

View File

@ -0,0 +1,24 @@
export default[
{
path: '/',
redirect: 'login',
},
{
path: '/login',
name: 'login',
meta: {title: "登录"},
component: () => import("@/views/common/Login.vue"),
},
{
path: '/404',
name: '404',
meta: {title: "404"},
component: () => import('@/views/common/404.vue')
},
{
path: '/500',
name: '500',
meta: {title: "500"},
component: () => import('@/views/common/500.vue')
}
]

View File

@ -0,0 +1,14 @@
import { createRouter, createWebHistory } from "vue-router"
import routers from './routers'
import common from './common'
const router = createRouter({
history: createWebHistory(),
routes: [
...common,
...routers
]
})
export default router

View File

@ -0,0 +1,84 @@
import Layout from '@/components/layout/Index.vue'
export default[
{
path: '/index',
name: 'index',
type: 0,
icon: 'HomeFilled',
meta: { title: "首页" },
component: () => Layout,
children: [
{
path: '/index',
name: 'index',
type: 0,
icon: 'HomeFilled',
meta: { title: "首页" },
component: () => import('@/views/Index.vue')
}
]
},
{
path: '/system',
name: 'system',
type: 0,
icon: 'Grid',
meta: { title: "系统管理" },
children: [
{
path: '/menu',
name: 'menu',
type: 0,
icon: 'Grid',
meta: { title: "菜单管理" },
component: () => Layout,
children: [
{
path: '/menu',
name: 'menu',
type: 0,
icon: 'Grid',
meta: { title: "菜单管理" },
component: () => import('@/views/system/Manu.vue')
},
]
},
{
path: '/org',
name: 'org',
type: 0,
icon: 'QuestionFilled',
meta: { title: "组织管理" },
component: () => Layout,
children: [
{
path: '/org',
name: 'org',
type: 0,
icon: 'QuestionFilled',
meta: { title: "组织管理" },
component: () => import('@/views/system/Org.vue')
}
]
},
{
path: '/user',
name: 'user',
type: 0,
icon: 'QuestionFilled',
meta: { title: "用户管理" },
component: () => Layout,
children: [
{
path: '/user',
name: 'user',
type: 0,
icon: 'QuestionFilled',
meta: { title: "用户管理" },
component: () => import('@/views/system/User.vue')
}
]
},
]
},
]

View File

@ -0,0 +1,25 @@
$--colors: (
"primary": (
"base": #2F9688,
),
"success": (
"base": #21ba45,
),
"warning": (
"base": #f2711c,
),
"danger": (
"base": #db2828,
),
"error": (
"base": #db2828,
),
"info": (
"base": #42b8dd,
)
);
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
$colors: $--colors
);

View File

@ -0,0 +1,33 @@
$--colors: (
"primary": (
"base": #2F9688,
),
"success": (
"base": #21ba45,
),
"warning": (
"base": #f2711c,
),
"danger": (
"base": #db2828,
),
"error": (
"base": #db2828,
),
"info": (
"base": #42b8dd,
)
);
@forward "element-plus/theme-chalk/src/mixins/config.scss" with (
$namespace: "el"
);
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: $--colors,
$button-padding-horizontal: ("default": 50px)
);
@use "element-plus/theme-chalk/src/index.scss" as *;
@use "./dark.scss";

View File

@ -0,0 +1,51 @@
// import dark theme
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
:root {
// 主题
--theme: #2F9688;
// 局部背景
--el-bg-color: #eff4f9;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB","Microsoft YaHei", "微软雅黑", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 0;
overflow: hidden;
}
/**
* el-table 滚动条样式
*/
.el-table__body-wrapper::-webkit-scrollbar {
width: 5px;
height: 1px;
}
.el-table__body-wrapper::-webkit-scrollbar-thumb {
background-color: #909399;
border-radius: 5px;
}
.el-table__body-wrapper::-webkit-scrollbar-track {
border-radius: 5px;
background: #ededed;
}
/**
* 滚动条样式
*/
.scroll-div::-webkit-scrollbar{
width: 5px;
height: 1px;
}
.scroll-div::-webkit-scrollbar-thumb {
border-radius: 5px;
background: #909399;
}
.scroll-div::-webkit-scrollbar-track {
border-radius: 5px;
background: #ededed;
}

View File

@ -0,0 +1,61 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { showMessage } from './status'
import { ElMessage } from 'element-plus'
import { logout, getToken } from '@/api/auth'
import router from '../router'
// 根据环境获得不同的代理模式
const baseURL = import.meta.env.VITE_BASE_URL as string;
const axiosInstance: AxiosInstance = axios.create({
baseURL: baseURL,
timeout: 30 * 1000, // 超时时间
headers: {
'Content-Type': 'application/json'
},
})
// axios实例拦截请求
axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
// 设置token
if (getToken()) {
config.headers.Authorization = `Bearer ${getToken()}`
}
return config;
},
(error: any) => {
return Promise.reject(error);
}
)
// axios实例拦截响应
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status === 200 && response.data.code == 200) {
return response;
} else {
ElMessage.warning(showMessage(response.status));
if (response.data.code === 402) {
logout()
}
if (response.data.code == 403 || response.data.code == 401){
sessionStorage.clear();
router.push({path: 'login'});
}
return response;
}
},
// 请求失败
(error: any) => {
const {response} = error;
if (response) {
// 请求已发出但是不在2xx的范围
ElMessage.warning(showMessage(response.status));
return Promise.reject(response.data);
} else {
ElMessage.warning('网络连接异常,请稍后再试!');
}
}
)
export default axiosInstance

View File

@ -0,0 +1,42 @@
export const showMessage = (status:number|string) : string => {
let message:string = "";
switch (status) {
case 400:
message = "请求错误(400)";
break;
case 401:
// message = "未授权,请重新登录(401)";
message = "用户名或密码错误";
break;
case 403:
message = "拒绝访问(403)";
break;
case 404:
message = "请求出错(404)";
break;
case 408:
message = "请求超时(408)";
break;
case 500:
message = "服务器错误(500)";
break;
case 501:
message = "服务未实现(501)";
break;
case 502:
message = "网络错误(502)";
break;
case 503:
message = "服务不可用(503)";
break;
case 504:
message = "网络超时(504)";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `未授权,请重新登录!`;
}
return `${message}`;
};

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { reactive, onMounted } from "vue"
const state = reactive({
shadow: 'always',
})
onMounted(()=>{
})
</script>
<template>
<el-card class="home-container" :shadow="state.shadow">
首页
</el-card>
</template>
<style lang="scss" scoped>
.home-container{
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<div>
404
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
500
</div>
</template>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import {onBeforeMount,reactive,ref,toRefs} from "vue"
import type {FormInstance} from 'element-plus'
import {useRouter} from 'vue-router'
import {login} from '@/api/auth'
const router = useRouter()
const loginFormRef = ref<FormInstance>()
const logo = new URL('../../assets/image/logo.png', import.meta.url).href
const loginLogo = new URL('../../assets/image/login-icon.png', import.meta.url).href
const state = reactive({
loading: false,
loginStyle: {
height: '',
},
loginForm: {
username: '',
password: '',
},
})
onBeforeMount(() => {
state.loginStyle.height = (document.body.clientHeight || document.documentElement.clientHeight) + "px"
})
function copyYear(){
let date = new Date();
return date.getFullYear();
}
async function onSubmit(formEl: FormInstance | undefined) {
if (!formEl) return;
await formEl.validate((valid)=>{
if(valid){
// token
let param:any = {
username: state.loginForm.username,
password: state.loginForm.password
}
state.loading = true;
login(param).then((res:any) =>{
if(res){
state.loading = false;
router.push({path: 'index'});
}
}).catch(()=>{
state.loading = false;
})
}
});
}
</script>
<template>
<div class="login-container" :style="state.loginStyle">
<div class="login-header">
<div>
<el-image :src="logo" style="width: 100%; height: 100%;"></el-image>
</div>
<div>
<span class="title">Tansci Boot</span>
</div>
</div>
<div class="login-main">
<div class="main-title">帐号登录</div>
<div class="main-container">
<div class="logo">
<el-image :src="loginLogo" style="width: 100%; height: 100%;"></el-image>
</div>
<div class="form">
<el-form :model="state.loginForm" :rules="rules" size="large" ref="loginFormRef">
<el-form-item prop="username" :rules="[
{required: true,message: '请输入账号',trigger: 'blur'},
{pattern: /^[a-zA-Z]\w{4,17}$/,message: '账号有误,请重新输入',trigger: 'blur'}]">
<el-input v-model="state.loginForm.username" prefix-icon="Avatar" placeholder="请输入账号" style="width:100%"></el-input>
</el-form-item>
<el-form-item prop="password" :rules="[
{required: true,message: '请输入密码',trigger: 'blur'},
{pattern: /^[a-zA-Z]\w{5,17}$/,message: '密码有误,请重新输入',trigger: 'blur'}]">
<el-input type="password" v-model="state.loginForm.password" prefix-icon="Lock" show-password placeholder="请输入密码" style="width:100%"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit(loginFormRef)" :loading="loading" style="width:100%">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
<div class="login-footer">
<div>
<el-link href="https://typ1805.gitee.io" target="_blank">关于作者</el-link>
<el-divider direction="vertical" />
<el-link href="https://gitee.com/typ1805/tansci-boot" target="_blank">源码地址 Gitee & GitHub</el-link>
<el-divider direction="vertical" />
<el-link href="https://typ1805.gitee.io" target="_blank">联系作者</el-link>
</div>
<div class="copy">
&copy; {{copyYear()}} Tansci Boot 版权所有
</div>
</div>
</div>
</template>
<style lang="scss" scoped="scoped">
.login-container {
background-image: radial-gradient( white 0%, #FAFDFE 10%, #ddf8e7 50%, #FAFDFE 90%, white 100%);
// background-image: radial-gradient(#ddf8e7 00%, #FAFDFE 80%, white 100%);
.login-header{
width: 100%;
height: 5rem;
line-height: 5rem;
display: flex;
padding: 0 20%;
.title{
padding: 0 1rem;
color: var(--t9);
font-size: 20px;
font-weight: 700;
}
}
.login-main{
height: 80%;
.main-title{
font-size: 32px;
text-align: center;
padding: 6rem 0;
}
.main-container{
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
.logo{
width: 36rem;
padding-right: 2rem;
// transition: all .2s;
}
// .logo:hover{
// transform: scaleY(1.1) scaleX(1.1) translateZ(0);
// }
.form{
width: 26rem;
padding-left: 4rem;
border-left: 1px solid #c7f6da;
}
}
}
.login-footer{
height: 100%;
text-align: center;
color: #606266;
padding-top: 1.2rem;
.copy{
padding-top: 1rem;
}
}
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<div>
App
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
help
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
help
</div>
</template>

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"typeRoots": [
"node_modules/@types", //
"src/types"
],
"baseUrl": "./",
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"paths": {
"@": ["src"],
"@/*": ["src/*"]
},
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/**/*.vue"
]
}

View File

@ -0,0 +1,43 @@
import { defineConfig } from 'vite'
import path from 'path'
import vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import {
presetAttributify,
presetIcons,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/element/index.scss" as *;`,
},
},
},
plugins: [
vue(),
Unocss({
presets: [
presetUno(),
presetAttributify(),
presetIcons({
scale: 1.2,
warn: true,
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
]
}),
]
})