Commit 3a7df3e9 authored by 李厚安's avatar 李厚安 😑

第一版静态页面

parents
Pipeline #69991 failed with stages
VITE_REQUEST_BASE_URL = /api
\ No newline at end of file
VITE_REQUEST_BASE_URL = /api
\ No newline at end of file
node_modules
.DS_Store
dist
dist-ssr
*.local
{
"recommendations": [
"johnsoncodehk.volar"
]
}
\ No newline at end of file
MIT License
Copyright (c) 2021 good luck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
<div align="center">
<img width="320" src="http://www.lelebk.com/docs/img/logo.png">
</div>
<div align="center">
<h1>vue3-antd-admin</h1>
<div>
<div align="center">
<img src="https://img.shields.io/badge/vue-3.0.5-brightgreen.svg">
<img src="https://img.shields.io/badge/ant--design-2.2.6-brightgreen.svg">
<img src="https://img.shields.io/badge/build-rollup-brightgreen.svg">
<img src="https://img.shields.io/badge/vite-2.4.0-brightgreen.svg">
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg">
</div>
<div align="left">
<h2>简介</h2>
<p>vue3-antd-admin是一个后台前端解决方案,它基于vue3和antd-ui实现。它使用了最新的前端技术栈,集成了动态路由、鉴权登录、菜单管理等基础功能,在此之上对样式进行优化,解决了其他框架菜单超出不滚动、未BFC隔离等被忽略的问题。它与同类型框架相比逻辑更加清晰简洁,没有冗余代码上手更加快速。相信不管你的需求是什么,本项目都能帮助到你。good luck!</p>
<div>
>仅对对华友好并尊重中华人民共和国主权与领土完整的各界人士开放使用!
>It can be used by people who respect the sovereignty and territorial integrity of the People's Republic of China
<div align="left">
<h2>资料</h2>
<p><a href="http://www.lelebk.com/admin/" target="_blank">demo在线预览</a></p>
<p><a href="http://www.lelebk.com/docs/" target="_blank">使用文档</a></p>
<p>账号:admin | 密码:123456</p>
<div>
## 安装使用
```
# 克隆项目
git clone git@github.com:llyyayx/vue3-antd-admin.git
# 进入项目目录
cd vue3-antd-admin
# 安装依赖
npm install
# 本地开发 启动项目
npm run dev
```
具体详细的使用说明请阅读文档。为了更好的使用请务必阅读文档-开箱即用部分。
## 发布
```
# 构建生产环境
npm run build
```
## 功能
```
- 登录 / 注销
- 权限验证
- 页面权限
- 二步登录
- 多环境发布
- dev
- sit
- stage
- prod
- 全局功能
- 动态侧边栏(支持多级路由嵌套)
- 快捷导航(标签页)
- 自适应收缩侧边栏
- 表格
- josn配置省心开发
- 错误页面
- 401
- 404
- 500
- 组件
- tableLayout表格
- a-icon图标
```
## 交流
vue3-antd-admin是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。
- QQ 群 `719251151`
## 未来规划
本项目当前只是基础部分完成了,还有很多地方需要完善,例如:H5适配、页面动画、指令权限、更多好用的组件等等等等,由于本人平时工作原因并不能快速迭代更新,所以希望更多的开发者加入进来,
你只需要Fork本仓库,创建自己的分支并`pull request`你的杰作即可,贡献者名单中也会收录你的名字。当然在使用过程中遇到问题你也可以提出`issue`,我们会尽快安排时间解决。
在这里非常欢迎你的加入,期待你的一个`issue`或者是一个`pull reques`
## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/llyyayx/vue3-antd-admin/issues) 或者提交一个 Pull Request。
**Pull Request:**
1. Fork 代码!
2. 创建自己的分支: `git checkout -b feat/xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交`pull request`
## 浏览器支持
本地开发推荐使用`Chrome 80+` 浏览器
支持现代浏览器, 不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: | :-: |
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 相关仓库
如果这些插件对你有帮助,可以给一个 star 支持下
- [vite-plugin-mock](https://github.com/anncwb/vite-plugin-mock) - 用于本地及开发环境数据 mock
- [vite-plugin-html](https://github.com/anncwb/vite-plugin-html) - 用于 html 模版转换及压缩
- [vite-plugin-style-import](https://github.com/anncwb/vite-plugin-style-import) - 用于组件库样式按需引入
- [vite-plugin-theme](https://github.com/anncwb/vite-plugin-theme) - 用于在线切换主题色等颜色相关配置
- [vite-plugin-imagemin](https://github.com/anncwb/vite-plugin-imagemin) - 用于打包压缩图片资源
- [vite-plugin-compression](https://github.com/anncwb/vite-plugin-compression) - 用于打包输出.gz|.brotil 文件
- [vite-plugin-svg-icons](https://github.com/anncwb/vite-plugin-svg-icons) - 用于快速生成 svg 雪碧图
## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
<img width="320" src="http://www.lelebk.com/docs/img/zsm.jpg">
## License
[MIT © llyyayx-2021](./LICENSE)
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-cn">
<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>
<style>
.windowLoading{margin:0;width:100%;height:100vh;overflow:hidden}
.windowLoading{display:flex;flex-direction:column;justify-content:center;align-items:center}
.windowLoading .loadingText{font-family:'Helvetica','Arial',sans-serif;font-size:16px;font-weight:700;text-align:center;margin:10px 0;}
.windowLoading .logo{position:relative;width:100px;height:100px;box-sizing:border-box;background-color:white}
.windowLoading .logo::before,.logo::after{content:'';position:absolute;width:100%;height:100%;box-sizing:border-box;border:4px solid transparent;animation-timing-function:linear}
.windowLoading .logo::before{top:0;left:0;border-top-color:black;border-right-color:black;animation:windowLoading-border-before 1.5s infinite;animation-direction:alternate}
.windowLoading .logo::after{bottom:0;right:0;border-bottom-color:black;border-left-color:black;animation:windowLoading-border-after 1.5s infinite;animation-direction:alternate}
.windowLoading .logo>div{position:absolute}
.windowLoading .logo .red{top:4px;bottom:0;left:0;border-right:4px solid black;background-color:#ea5664;animation:windowLoading-red 1.5s infinite;animation-direction:alternate}
.windowLoading .logo .orange{bottom:0;left:27%;right:4px;border-top:4px solid black;background-color:#f3b93f;animation:orange 1.5s infinite;animation-direction:alternate}
.windowLoading .logo .white{right:5px;top:4px;bottom:50%;height:50%;border-left:4px solid black;background-color:#fff;animation:windowLoading-white 1.5s infinite;animation-direction:alternate}
@keyframes windowLoading-border-before{0%{width:0;height:0;border-right-color:transparent}5.99%{border-right-color:transparent}6%{height:0;width:100%;border-right-color:black}25%,100%{width:100%;height:100%}}
@keyframes windowLoading-border-after{0%,24.99%{width:0;height:0;border-left-color:transparent;border-bottom-color:transparent}25%{border-left-color:transparent;border-bottom-color:black}36.99%{border-left-color:transparent}37%{height:0;width:100%;border-left-color:black}50%,100%{width:100%;height:100%}}
@keyframes windowLoading-red{0%,50%{width:0;opacity:0}50.01%{opacity:1}65%,100%{width:27%;opacity:1}}@keyframes orange{0%,65%{height:0;opacity:0}65.01%{opacity:1}80%,100%{height:50%;opacity:1}}
@keyframes windowLoading-white{0%,75%{width:0;opacity:0}75.01%{opacity:1}90%,100%{width:27%;opacity:1}}
</style>
</head>
<body>
<div id="app">
<div class="windowLoading">
<div class="logo">
<div class="white"></div>
<div class="orange"></div>
<div class="red"></div>
</div>
<div class="loadingText">Loading ...</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
This diff is collapsed.
{
"name": "talentadmin",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"ant-design-vue": "^2.2.6",
"axios": "^0.21.1",
"store": "^2.0.12",
"vue": "^3.0.5",
"vue-router": "^4.0.11",
"vuex": "^4.0.2"
},
"devDependencies": {
"@types/node": "^16.3.1",
"@types/store": "^2.0.2",
"@vitejs/plugin-vue": "^1.2.4",
"@vitejs/plugin-vue-jsx": "^1.1.6",
"@vue/compiler-sfc": "^3.0.5",
"sass": "^1.35.2",
"typescript": "^4.3.2",
"vite": "^2.4.0",
"vue-tsc": "^0.0.24"
}
}
<template>
<a-config-provider v-bind="getPopupContainer">
<router-view></router-view>
</a-config-provider>
</template>
<script lang="ts">
import 'moment/dist/locale/zh-cn'
import { defineComponent } from 'vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
export default defineComponent({
name: 'App',
data () {
return {
getPopupContainer: {
locale: zhCN
}
}
}
})
</script>
<style>
#app {
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
</style>
import request from '../request'
import { AxiosResponse } from 'axios'
import { LoginFrom } from '@/types/views/login'
import { ResponseData } from '@/types/api/public'
import { LoginSuccess, UserInfo, RouterData } from '@/types/api/login'
type ConfigType<T = ResponseData> = Promise<AxiosResponse<T>>
/**
* @desc: 登录
* @param { Object } data 输入的账号密码
*/
export const login = (data: LoginFrom): ConfigType<LoginSuccess> => {
return request({
url: '/login',
method: 'post',
data
})
}
/**
* @desc: 获取用户信息
* @returns data
*/
export const info = (): ConfigType<UserInfo> => {
return request({
url: '/info',
method: 'get'
})
}
/**
* @desc: 获取菜单
*/
export const menu = (): ConfigType<RouterData> => {
return request({
url: '/menu',
method: 'get'
})
}
/**
* @desc: 退出登录
*/
export const logout = (): ConfigType => {
return request({
url: '/logout',
method: 'post'
})
}
export const data1 = [{
"id": 1,
"pid": 0,
"name": "基础模板",
"path": "/",
"redirect": "/page/pendingdrive_page",
"component": "BasicLayout",
"icon": "AppleOutlined",
"key": "layout",
"children": [{
"id": 2,
"pid": 1,
"name": "幕后管理",
"path": "/page",
"redirect": "/page/pendingdrive_page",
"component": "RouteView",
"icon": "ChromeOutlined",
"key": "page",
"children": [{
"id": 10,
"pid": 2,
"name": "待审驱动",
"path": "/page/pendingdrive_page",
"redirect": "",
"component": "/page/pendingdrive_page",
"icon": "",
"key": "pendingdrive_page",
"keepAlive": true
},
{
"id": 11,
"pid": 2,
"name": "品牌型号",
"path": "/page/brandmodel_page",
"redirect": "",
"component": "/page/brandmodel_page",
"icon": "",
"key": "brandmodel_page"
},
{
"id": 12,
"pid": 2,
"name": "驱动列表",
"path": "/page/drivelist_page",
"redirect": "",
"component": "/page/drivelist_page",
"icon": "",
"key": "drivelist_page"
},
{
"id": 12,
"pid": 2,
"name": "驱动修改",
"path": "/page/driverAddorEdite_page",
"redirect": "",
"component": "/page/driverAddorEdite_page",
"icon": "",
"key": "driverAddorEdite_page",
"hidden":true
},
{
"id": 13,
"pid": 2,
"name": "型号维护",
"path": "/page/modelmaintain_page",
"redirect": "",
"component": "/page/modelmaintain_page",
"icon": "",
"key": "modelmaintain_page",
"hidden":true
}
]
},
{
"id": 3,
"pid": 1,
"name": "网关管理",
"path": "/page",
"redirect": "/page/gatewaylist_page",
"component": "RouteView",
"icon": "WechatOutlined",
"key": "gatewaylist_page",
"children": [{
"id": 12,
"pid": 3,
"name": "网关列表",
"path": "/page/gatewaylist_page",
"redirect": "",
"component": "/page/gatewaylist_page",
"icon": "",
"key": "join"
}]
},
{
"id": 4,
"pid": 0,
"name": "平台管理",
"path": "/page",
"redirect": "/page/usermng_page",
"component": "RouteView",
"icon": "AppleOutlined",
"key": "lan",
"children": [{
"id": 14,
"pid": 4,
"name": "用户管理",
"path": "/page/usermng_page",
"redirect": "",
"component": "/page/usermng_page",
"icon": "",
"key": "usermng_page",
"keepAlive": true
}
]
}
]
}
]
import request from '../request'
import { AxiosResponse } from 'axios'
import { TableList, EditData, UploadApi, OptionsData } from '@/types/api/table'
import { ResponseData } from '@/types/api/public'
type ConfigType<T=ResponseData> = Promise<AxiosResponse<T>>
export const getData = (params: any): ConfigType<TableList> => {
return request({
url: '/table',
method: 'get',
params
})
}
export const addData = (data: any): ConfigType => {
return request({
url: '/table/add',
method: 'post',
data
})
}
export const editGetData = (params: any): ConfigType<EditData> => {
return request({
url: '/table/editGetData',
method: 'get',
params
})
}
export const editData = (data: any): ConfigType => {
return request({
url: '/table/edit',
method: 'post',
data
})
}
export const delData = (data: any): ConfigType => {
return request({
url: '/table/del',
method: 'post',
data
})
}
export const upload = (data: any): ConfigType<UploadApi> => {
return request({
url: '/table/upload',
method: 'post',
data
})
}
export const options = (): ConfigType<OptionsData> => {
return request({
url: '/table/options',
method: 'get'
})
}
\ No newline at end of file
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
strong {
font-weight: 700;
}
\ No newline at end of file
This diff is collapsed.
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6395" height="1080" viewBox="0 0 6395 1080">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
</clipPath>
<linearGradient id="linear-gradient" x1="0.631" y1="0.5" x2="0.958" y2="0.488" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#2e364a"/>
<stop offset="1" stop-color="#2c344a"/>
</linearGradient>
</defs>
<g id="Web_1920_1" data-name="Web 1920 – 1" clip-path="url(#clip-Web_1920_1)">
<g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
<g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
<path id="Path_142" data-name="Path 142" d="M6271.734-6.176s-222.478,187.809-55.349,583.254c44.957,106.375,81.514,205.964,84.521,277,8.164,192.764-156.046,268.564-156.046,268.564l-653.53-26.8L5475.065-21.625Z" transform="translate(-4876.383)" fill="#2d3750"/>
<path id="Union_6" data-name="Union 6" d="M-2631.1,1081.8v-1.6H-8230.9V.022h5599.8V0h759.7s-187.845,197.448-91.626,488.844c49.167,148.9,96.309,256.289,104.683,362.118,7.979,100.852-57.98,201.711-168.644,254.286-65.858,31.29-144.552,42.382-223.028,42.383C-2441.2,1147.632-2631.1,1081.8-2631.1,1081.8Z" transform="translate(3259.524 0.803)" fill="url(#linear-gradient)"/>
</g>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6395" height="1079" viewBox="0 0 6395 1079">
<defs>
<clipPath id="clip-path">
<rect width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
</clipPath>
<linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#2c41b4"/>
<stop offset="1" stop-color="#1b4fab"/>
</linearGradient>
</defs>
<g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
<g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
<path id="Path_142" data-name="Path 142" d="M6271.734-6.176s-222.478,187.809-55.349,583.254c44.957,106.375,81.514,205.964,84.521,277,8.164,192.764-156.046,268.564-156.046,268.564l-653.53-26.8L5475.065-21.625Z" transform="translate(-4876.383 0)" fill="#f1f5f8"/>
<path id="Union_6" data-name="Union 6" d="M-2631.1,1081.8v-1.6H-8230.9V.022H-2631.1V0H-1871.4s-187.845,197.448-91.626,488.844c49.167,148.9,96.309,256.289,104.683,362.118,7.979,100.852-57.98,201.711-168.644,254.286-65.858,31.29-144.552,42.382-223.028,42.383C-2441.2,1147.632-2631.1,1081.8-2631.1,1081.8Z" transform="translate(3259.524 0.803)" fill="url(#linear-gradient)"/>
</g>
</g>
</svg>
<svg id="a622e68e-7a65-46e9-94a9-d455de519afc" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="971.44" height="502" viewBox="0 0 971.44 502"><defs><linearGradient id="341b0e5e-a21f-44db-b85f-76180f33f0d3" x1="599.5" y1="668.05" x2="599.5" y2="199" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67" x1="485.72" y1="258.88" x2="485.72" y2="71.12" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="fe76f7c7-2126-4e48-920d-21143a22d340" x1="132" y1="515" x2="303" y2="515" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="2cf89a04-5a05-413b-983a-d2bc296cbb5e" x1="933" y1="568.28" x2="1031" y2="568.28" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/></defs><title>responsive</title><g opacity="0.7"><path d="M852.69,199H346.31A16.37,16.37,0,0,0,330,215.42V563.94a16.37,16.37,0,0,0,16.31,16.42H520.47v60.16h-7.94a8.3,8.3,0,0,0-8.27,8.33v12.07h16.21v7.14H678.53v-7.14h16.21V648.85a8.3,8.3,0,0,0-8.27-8.33H679V640h-.51V580.36H852.69A16.37,16.37,0,0,0,869,563.94V215.42A16.37,16.37,0,0,0,852.69,199Z" transform="translate(-114.28 -199)" fill="url(#341b0e5e-a21f-44db-b85f-76180f33f0d3)"/></g><rect x="407.72" y="371" width="156" height="92" fill="#bdbdbd"/><g opacity="0.1"><path d="M525.07,579H675.24c1.81-7.87,3.26-13,3.26-13h-157S523.11,571.11,525.07,579Z" transform="translate(-114.28 -199)"/></g><path d="M235.82,3h499.8a16.1,16.1,0,0,1,16.1,16.1V327a0,0,0,0,1,0,0h-532a0,0,0,0,1,0,0V19.1A16.1,16.1,0,0,1,235.82,3Z" fill="#535461"/><path d="M849.9,576H350.1A16.1,16.1,0,0,1,334,559.9V526H866v33.9A16.1,16.1,0,0,1,849.9,576Z" transform="translate(-114.28 -199)" fill="#bdbdbd"/><circle cx="485.72" cy="352" r="9" fill="#535461"/><path d="M399.89,436H571.55a8.17,8.17,0,0,1,8.17,8.17V456a0,0,0,0,1,0,0h-188a0,0,0,0,1,0,0V444.17A8.17,8.17,0,0,1,399.89,436Z" fill="#bdbdbd"/><g opacity="0.5"><rect x="320.72" y="71.12" width="330" height="187.76" rx="4.5" ry="4.5" fill="url(#9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67)"/></g><rect x="324.95" y="72.5" width="321.54" height="183.96" rx="4.5" ry="4.5" fill="#fff"/><g opacity="0.5"><rect x="414.52" y="98.91" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="98.91" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="109.55" width="79.54" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="148.53" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="148.53" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="159.16" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="198.15" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="198.15" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="208.78" width="96.33" height="3.54" rx="1.59" ry="1.59" fill="#e0e0e0"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="279" x2="485.72" y2="310" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="251" x2="485.72" y2="279.87" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="251" r="4" fill="#0960bd"/><line x1="305.72" y1="168.5" x2="274.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="333.22" y1="168.5" x2="304.35" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="333.22" cy="168.5" r="4" fill="#0960bd"/><g opacity="0.1"><rect x="408.22" y="435.5" width="156" height="3"/></g><g opacity="0.7"><path d="M293.48,566.06H221.08l1-8.14c20.46-18.37,33.69-67.31,33.69-67.31a6.78,6.78,0,0,0-.87.18c-12,2.42-20.54,7.35-26.51,13.28l2.54-21.66c37.8-8.14,52.79-58.14,52.79-58.14-24.12,5.35-39.16,13.63-48.5,21.49l3.72-31.82c25.56,8.77,52-37.82,52-37.82l-1-.21.5-.32-.76.27c-28.25-6.09-43.35,10.06-48.25,16.77l.37-3.12q-1.12,3-2.18,5.88h0l0,.08q-3,8.13-5.49,16.06l0,0h0q-2.17,6.77-4.06,13.4l0-.06s-1.17-28.46-31.18-35.95c0,0,3.15,62.07,26.93,51.91h0c-2.2,9-4,17.66-5.56,26.07h0q-1.49,8.21-2.6,16l-.14.16.14-.12-.06.41v0h0q-1,7.07-1.7,13.78c.46-8.62-1.11-33.52-30.45-56.92,0,0-39,68.54,27.5,82,.15.13.3.26.44.38l-.1-.31.6.13.27-3.52a369.39,369.39,0,0,0,.23,44.1h0c.07,1,.14,2,.21,2.95H141.37c-27.94,57.79,15.52,89.46,15.52,89.46h120C323.49,596.66,293.48,566.06,293.48,566.06Zm-78-65.68h0v0Z" transform="translate(-114.28 -199)" fill="url(#fe76f7c7-2126-4e48-920d-21143a22d340)"/></g><path d="M217,588s-19-83,23-190" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="3" opacity="0.6"/><path d="M143,563H290s29,37-16,92H158S116,617,143,563Z" transform="translate(-114.28 -199)" fill="#0960bd"/><path d="M237.89,403.5s14.61-26,49.61-18c0,0-28.93,49.26-55,33.13Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M228.63,431.09S227.5,404.5,198.5,397.5c0,0,3,58,26,48.5Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M219.15,470.36s5.35-27.86,61.35-39.86c0,0-17.86,57.62-63.93,55.31Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M214.61,501.63s5.89-29.13-29.11-56.13c0,0-38,64.67,27.48,76.83Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M213.56,541.67S209.5,500.5,253.5,492.5c0,0-16.07,57.49-40,67.74Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M233,419s38-29,54-34" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M216.5,485.5s46-49,64-55" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M198.5,397.5s28,38,26,48" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M185.5,445.5s15,68,27,77" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M213.5,560.5s24-66,40-68" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><g opacity="0.1"><path d="M290,563H143c-.33.67-.65,1.34-1,2H285s28.29,36.11-14.4,90H274C319,600,290,563,290,563Z" transform="translate(-114.28 -199)"/></g><rect y="455.6" width="971.44" height="32.93" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="13.47" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="4.49" opacity="0.1"/><line x1="690.22" y1="168.5" x2="696.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="637.22" y1="168.5" x2="682.1" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="637.22" cy="168.5" r="4" fill="#0960bd"/><circle cx="686.22" cy="168.5" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><g opacity="0.7"><path d="M1027,643.88l.1-.15q.31-.48.61-1l.11-.19q.29-.49.55-1l.09-.17c.2-.39.39-.78.56-1.19h0a23.79,23.79,0,0,0,.94-2.51l.1-.33c.09-.31.18-.62.26-.93l.1-.44q.1-.42.18-.85c0-.16.06-.32.09-.48s.09-.56.13-.85,0-.33.06-.49.06-.61.08-.92c0-.14,0-.29,0-.43,0-.45,0-.91,0-1.36V548h-13.85V507.52h-17V548H988.39V489.86h-17V548H965V481.55h-17V548H933V630.6c0,13.48,11.21,24.4,25,24.4H1006a25.19,25.19,0,0,0,20.24-10.06l0,0Q1026.61,644.41,1027,643.88Z" transform="translate(-114.28 -199)" fill="url(#2cf89a04-5a05-413b-983a-d2bc296cbb5e)"/></g><rect x="835.72" y="321" width="16" height="100" fill="#535461"/><rect x="835.72" y="288" width="16" height="33" fill="#3ad29f"/><rect x="857.72" y="329" width="16" height="100" fill="#535461"/><rect x="857.72" y="296" width="16" height="33" fill="#4d8af0"/><rect x="884.72" y="346" width="16" height="100" fill="#535461"/><rect x="884.72" y="313" width="16" height="33" fill="#f55f44"/><path d="M821.72,352h92a0,0,0,0,1,0,0v79.5a23.5,23.5,0,0,1-23.5,23.5h-45a23.5,23.5,0,0,1-23.5-23.5V352A0,0,0,0,1,821.72,352Z" fill="#0960bd"/><g opacity="0.1"><path d="M936,551v4h88v79.5a23.39,23.39,0,0,1-5,14.49,23.45,23.45,0,0,0,9-18.49V551Z" transform="translate(-114.28 -199)"/></g></svg>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<template>
<component :is='type'></component>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import * as antIcons from "@ant-design/icons-vue"
export default defineComponent({
name: 'aicon',
components: antIcons,
props: {
type: {
required: true,
type: String
}
}
})
</script>
<style lang="scss" scoped>
</style>
\ No newline at end of file
<template>
<div class="copypermis">
<a-modal
v-model:visible="visible"
title="权限设置"
@ok="handleOk"
width="1000px"
>
<a-row class="row caozuo">
<a-button type="primary">选择</a-button>
<a-button style="margin-left: 20px" @click="cancel">取消</a-button>
</a-row>
<a-table
:dataSource="dataSource"
:columns="columns"
:pagination="false"
:customRow="onClickRow"
:rowClassName="setRowClassName"
/>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, defineExpose, reactive } from "vue";
export default defineComponent({
setup() {
const visible = ref(false);
const handleOk = () => {};
const rowId = ref(null);
const cancel = () => {
visible.value = false;
};
// 选中行
const onClickRow = (record) => {
return {
onClick: () => {
rowId.value = record.key;
console.log("进来" + record);
console.log(record);
},
onMouseenter: () => {},
};
};
const setRowClassName = (record) => {
return record.key === rowId.value ? "clickRowStyl" : "";
};
defineExpose({
visible,
});
return {
setRowClassName,
onClickRow,
visible,
handleOk,
cancel,
dataSource: [
{
key: "1",
name: "胡彦斌",
age: 32,
address: "西湖区湖底公园1号",
},
{
key: "2",
name: "胡彦祖",
age: 42,
address: "西湖区湖底公园1号",
},
],
columns: [
{
title: "姓名",
dataIndex: "name",
key: "name",
},
{
title: "年龄",
dataIndex: "age",
key: "age",
},
{
title: "住址",
dataIndex: "address",
key: "address",
},
],
};
},
});
</script>
<style lang='scss'>
.caozuo {
margin-bottom: 20px;
}
.clickRowStyl,
.ant-table-tbody > .clickRowStyl:hover > td {
background-color: #f4ac0a;
color: #fff;
}
.ant-table-tbody
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td {
background: transparent !important;
}
</style>
<template>
<div>
<a-modal
v-model:visible="visible"
title="权限设置"
@ok="handleOk"
width="1000px"
>
<div class="title">功能权限</div>
<a-row class="row fontweight">
<a-col :span="4">可见页面</a-col>
<a-col :span="20">功能权限</a-col>
</a-row>
<a-row class="row" v-for="(item, index) in dataList" :key="index">
<a-col :span="4"
><a-checkbox
v-model:checked="item.checkAll"
@change="(e) => onCheckAllChange(e, index)"
>
{{ item.name }}
</a-checkbox></a-col
>
<a-col :span="20">
<a-checkbox-group
v-model:value="item.checkedList"
:options="item.plainOptions"
/>
</a-col>
</a-row>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, defineExpose, reactive } from "vue";
export default defineComponent({
setup() {
const visible = ref(false);
const handleOk = () => {};
const dataList = reactive([
{
name: "幕后管理",
checkedList: [],
plainOptions: ["待审驱动", "品牌型号", "驱动列表"],
checkAll: false,
},
{
name: "网关管理",
checkedList: [],
plainOptions: ["网关列表"],
},
{
name: "平台管理",
checkedList: [],
plainOptions: ["用户管理"],
},
]);
const onCheckAllChange = (e, index) => {
console.log("哈哈哈哈哈哈"+e.target.checked);
let list=[...dataList[index].plainOptions]
list.push(dataList[index].name);
console.log(list);
dataList[index].checkedList=e.target.checked? list:[]
};
defineExpose({
visible,
});
return {
visible,
handleOk,
dataList,
onCheckAllChange,
};
},
});
</script>
<style scoped>
.title {
color: #000;
font-weight: bold;
margin: -10px 0px 10px;
}
.row {
width: 100%;
height: 40px;
display: flex;
align-items: center;
border-bottom: 1px solid #efefef;
}
.fontweight {
font-weight: bold;
}
</style>
<template>
<a-form
ref="formRef"
:rules="rules"
:model="formData"
:name="name"
layout="inline"
class="comform"
>
<template v-for="item in formItem">
<a-form-item
:label="item.title"
:name="item.key"
:style="{ width: item.itemWidth || 'calc(50% - 20px)' }"
:labelCol="{span: item.labelCol ? item.labelCol : 6}"
:wrapperCol="{span: item.labelCol ? 24-item.labelCol : 18}"
class="form__item"
>
<!-- 输入框 -->
<a-input
v-model:value="formData[item.key]"
:allowClear="true"
autocomplete="off"
:placeholder="'请输入'+item.title"
:disabled="item.disabled ? true : false"
v-if="item.type === 'input'"
/>
<!-- 下拉选择框 -->
<a-select
v-model:value="formData[item.key]"
:placeholder="'请选择'+item.title"
:allowClear="true"
:disabled="item.disabled ? true : false"
v-if="item.type === 'select'"
>
<a-select-option :value="option.value" v-for="option in item.options">{{ option.label }}</a-select-option>
</a-select>
<!-- 时间选择框 -->
<a-date-picker
v-model:value="formData[item.key]"
:allowClear="true"
valueFormat="YYYY-MM-DD"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'datePicker'"
/>
<!-- 时间范围选择框 -->
<a-range-picker
v-model:value="formData[item.key]"
:allowClear="true"
valueFormat="YYYY-MM-DD"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'rangePicker'"
/>
<!-- 多行文本输入 -->
<a-textarea
v-model:value="formData[item.key]"
:allowClear="true"
autocomplete="off"
:placeholder="'请输入'+item.title"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'textarea'"
/>
<!-- 树形下拉选择框 -->
<a-tree-select
v-model:value="formData[item.key]"
:placeholder="'请选择'+item.title"
:allowClear="true"
:tree-data="item.options"
:dropdownStyle="{ maxHeight: '500px' }"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'treeSelect'"
/>
<!-- 数值输入框 -->
<a-input-number
v-model:value="formData[item.key]"
autocomplete="off"
:placeholder="'请输入'+item.title"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'number'"
/>
<!-- 密码输入框 -->
<a-input-password
v-model:value="formData[item.key]"
autocomplete="off"
:placeholder="'请输入'+item.title"
style="width: 100%;"
:disabled="item.disabled ? true : false"
v-if="item.type === 'password'"
/>
<!-- 开关 -->
<a-switch
v-model:checked="formData[item.key]"
checked-children="开"
un-checked-children="关"
:disabled="item.disabled ? true : false"
v-if="item.type === 'switch'"
/>
<!-- 单选框 -->
<a-radio-group
v-model:value="formData[item.key]"
:options="item.options"
:disabled="item.disabled ? true : false"
v-if="item.type === 'radio'"
/>
<!-- 多选框 -->
<a-checkbox-group
v-model:value="formData[item.key]"
:options="item.options"
:disabled="item.disabled ? true : false"
v-if="item.type === 'checkbox'"
/>
<!-- 图片上传 -->
<upload
v-model:value="formData[item.key]"
:upload="item.upload"
:disabled="item.disabled ? true : false"
v-if="item.type === 'upload'"
/>
<!-- 文件上传 -->
<uploadFile
v-model:value="formData[item.key]"
:upload="item.upload"
:disabled="item.disabled ? true : false"
v-if="item.type === 'uploadFile'"
/>
<!-- 自定义插槽 -->
<slot :name="item.slotName" :formData="formData" :key="item.key" v-if="item.type === 'slot'" />
</a-form-item>
</template>
</a-form>
</template>
<script lang="ts">
import utils from './utils'
import upload from './upload.vue'
import uploadFile from './uploadFile.vue'
import { message } from 'ant-design-vue'
import { FormItem, SetData } from './type'
import { defineComponent, PropType, ref, watch, reactive, computed } from 'vue'
export default defineComponent({
name: 'comForm',
emits: ['succeed', 'fail'],
components: {
upload,
uploadFile
},
props: {
// 表单项
formItem: {
type: Array as PropType<FormItem[]>,
required: true
},
// 提交数据的api接口
setData: {
type: Function as PropType<SetData>,
required: true
},
// 表单名称
name: {
type: String,
required: false,
default: 'form'
},
// 修改数据的key
dataKey: {
type: String,
required: false,
default: undefined
},
// 规则
rules: {
type: Object,
required: false,
default: {}
},
// 初始化数据(编辑)
defaultData: {
type: Object,
required: false,
default: {}
},
// 表单额外追加数据
additional: {
type: Object,
required: false,
default: {}
}
},
setup (props, context) {
// 表单数据
const formData = computed(() => {
let data = reactive({})
if (Object.keys(props.defaultData).length > 0) {
props.formItem.forEach(item => {
data[item.key] = props.defaultData[item.key] || undefined
})
if (props.dataKey) {
data[props.dataKey] = props.defaultData[props.dataKey] || undefined
}
} else {
utils.initData(props.formItem, data)
}
return data
})
const formRef = ref()
/**
* @desc: 表单提交
*/
const onSubmit = () => {
formRef.value.validate().then(() => {
Object.assign(formData.value, props.additional)
props.setData(formData.value).then(e => {
message.success(e.data.message)
context.emit('succeed', e)
}).catch(err => {
message.error(err.message || err.data.message)
context.emit('fail', 'api返回错误')
})
}).catch(() => {
context.emit('fail', '规则验证未通过')
})
}
/**
* @desc: 表单重置
*/
const reset = () => {
formRef.value.resetFields()
}
return { formData, formRef, onSubmit, reset }
}
})
</script>
<style lang="scss" scoped>
.comform {
& .form__item {
margin-bottom: 16px;
& .ant-form-item-label {
flex-grow: 1 !important;
flex-shrink: 0;
}
}
}
</style>
<style lang="scss">
.comform {
& .form__item {
& .ant-form-item-label {
flex-grow: 1;
flex-shrink: 0;
}
}
}
</style>
\ No newline at end of file
<template>
<a-modal
:title="title"
:width="width + 'px'"
:visible="show"
:confirm-loading="modalLoading"
@ok="modalOk"
@cancel="modalCancel"
>
<slot />
</a-modal>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'comModal',
emits: ['ok', 'cancel'],
props: {
// 弹框标题(必填)
title: {
type: String,
required: true
},
// 弹框宽度(选填)
width: {
type: [Number, String],
required: false,
default: 900
}
},
setup (props, context) {
// 弹框显隐
let show = ref<boolean>(false)
// 弹框路由loading
let modalLoading = ref<boolean>(false)
// @desc: 弹框确认事件(回调)
const modalOk = () => {
context.emit('ok')
}
// @desc: 弹框取消事件(回调)
const modalCancel = () => {
context.emit('cancel')
show.value = false
modalLoading.value = false
}
// @Desc: 弹框打开方法(供父组件调用)
const open = () => {
show.value = true
}
// @Desc: 弹框关闭方法(供父组件调用)
const close = () => {
show.value = false
modalLoading.value = false
}
/**
* @desc: 控制按钮loading方法(供父组件调用)
* @param { Boolean } status 打开true关闭false
*/
const loading = (status: boolean) => {
modalLoading.value = status
}
return { show, modalLoading, modalOk, modalCancel, open, close, loading }
}
})
</script>
\ No newline at end of file
This diff is collapsed.
import { AxiosResponse } from 'axios'
import { ColumnProps, tableProps } from 'ant-design-vue/es/table/interface'
interface ResponseData<T = any> {
code: number,
message: string,
data?: T
}
interface TableList extends ResponseData {
total: number,
current: number,
pageSize: number,
data: []
}
// 表单项类型_增改查
export interface FormItem {
title: string,
key: string,
type: string,
options?: any[],
itemWidth?: string,
defaultVal?: any,
labelCol?: number,
upload?: UploadFun,
slotName?: string,
optionKey?: string,
disabled?: boolean
}
// 表单提交函数类型_增改删
export type SetData = (x?: any) => Promise<AxiosResponse<ResponseData>>
// 表单提交函数类型_修改数据查询结果
export type EditData = (x?: any) => Promise<AxiosResponse<{
code: number,
message: string,
data: any
}>>
// 表单提交函数类型_查询
export type GetData = (x?: any) => Promise<AxiosResponse<TableList>>
// options选项查询函数
export type OptionsData = () => Promise<AxiosResponse<{
code: number,
message: string,
data: any
}>>
// 文件上传组件
export interface UploadData extends ResponseData {
url: string
}
export type UploadFun = (x?: any) => Promise<AxiosResponse<UploadData>>
export interface TableProps {
columns: ColumnProps[],
formItem: FormItem[],
selectItem?: FormItem[],
rules?: any,
get: SetData,
add?: SetData,
editData?: EditData,
edit?: SetData,
del?: SetData,
options?: OptionsData,
rowkey?: string,
page?: boolean,
operationWidth?: number,
operationShow?: boolean,
addItem?: FormItem[],
editItem?: FormItem[],
editKey?: string,
delKey?: string,
addRules?: any,
editRules?: any,
params?: any,
addToData?: any
}
\ No newline at end of file
<template>
<a-upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:disabled="disabled"
>
<a-avatar v-if="imageUrl" :size="128" shape="square" :src="imageUrl" />
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">点击上传</div>
</div>
</a-upload>
</template>
<script lang="ts">
import { UploadFun } from './type'
import { message } from 'ant-design-vue'
import { defineComponent, ref, watch, PropType } from 'vue'
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'uploadImg',
components: {
LoadingOutlined,
PlusOutlined,
},
emits: ['update:value', 'change'],
props: {
value: {
      type: String,
      required: false
    },
// 上传文件的api接口
upload: {
type: Function as PropType<UploadFun>,
required: false
},
// 是否禁用
disabled: {
      type: Boolean,
      required: false
    },
},
setup (props, context) {
const loading = ref<boolean>(false);
const imageUrl = ref<string | undefined>('');
if (props.value) {
imageUrl.value = props.value
}
watch(props, (data) => {
imageUrl.value = data.value
})
/**
* @Desc: 自定义上传头像
* @param { Object } data FormData对象
*/
const customRequest = (data: any) => {
if (props.upload) {
loading.value = true
const formdata = new FormData()
formdata.append('file', data.file)
props.upload(formdata).then(e => {
loading.value = false
context.emit('update:value', e.data.url)
}).catch(err => {
loading.value = false
context.emit('update:value', '')
message.error(err.message || err.data.message)
})
} else {
message.error('请添加上传图片的接口')
}
}
return { loading, imageUrl, customRequest }
}
})
</script>
\ No newline at end of file
<template>
<div class="uploadFile__component">
<a-upload
name="file"
class="file-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:disabled="disabled"
>
<a-button type="primary" :loading="loading"><CloudUploadOutlined v-show="!loading" /> 上传文件</a-button>
</a-upload>
<a :href="fileUrl" target="_blank" >{{ fileUrl }}</a>
</div>
</template>
<script lang="ts">
import { UploadFun } from './type'
import { message } from 'ant-design-vue'
import { defineComponent, ref, watch, PropType } from 'vue'
import { CloudUploadOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'uploadFile',
components: {
CloudUploadOutlined
},
emits: ['update:value', 'change'],
props: {
value: {
      type: String,
      required: false
    },
// 上传文件的api接口
upload: {
type: Function as PropType<UploadFun>,
required: false
},
// 是否禁用
disabled: {
      type: Boolean,
      required: false
    },
},
setup (props, context) {
const loading = ref<boolean>(false);
const fileUrl = ref<string | undefined>('');
if (props.value) {
fileUrl.value = props.value
}
watch(props, (data) => {
fileUrl.value = data.value
})
/**
* @Desc: 自定义上传头像
* @param { Object } data FormData对象
*/
const customRequest = (data: any) => {
if (props.upload) {
loading.value = true
const formdata = new FormData()
formdata.append('file', data.file)
props.upload(formdata).then(e => {
loading.value = false
context.emit('update:value', e.data.url)
}).catch(err => {
loading.value = false
context.emit('update:value', '')
message.error(err.message || err.data.message)
})
} else {
message.error('请添加上传图片的接口')
}
}
return { loading, fileUrl, customRequest }
}
})
</script>
<style lang="scss" scoped>
.uploadFile__component {
display: flex;
align-items: flex-end;
& a {
flex-shrink: 1;
flex-grow: 1;
margin-left: 6px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>
\ No newline at end of file
import { FormItem } from './type'
export default {
/**
*@desc: 初始化(编辑)表单数据
* @param { FormItem[] } formItem
* @param { nay } formData
* @return { never } 结果
*/
initData (formItem: FormItem[], formData: any) {
// 数据录入组件值为数组的
const dataComponentArray: string[] = ['rangePicker', 'checkbox']
formItem.forEach(item => {
if (dataComponentArray.includes(item.type)) {
formData[item.key] = item.defaultVal || []
} else {
formData[item.key] = item.defaultVal || undefined
}
})
},
/**
*@desc: 数组中含有对象,判断值在不在数组的对象中
* @param { any[] } arr
* @param { string } key
* @param { (string | number) } value
* @return { boolean } 结果
*/
arrIsKey (arr: any[], key: string, value: string | number) {
let result = false
result = arr.some((item: any) => { if(item[key]==value){ return true } })
return result
}
}
\ No newline at end of file
<template>
<a-result status="403" title="403" sub-title="对不起,您没有权限访问此页面.">
<template #extra>
<a-button type="primary" @click="home">返回主页</a-button>
</template>
</a-result>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
export default defineComponent({
name: '403',
setup() {
const router = useRouter()
const home = () => { router.replace('/') }
return { home }
}
})
</script>
<template>
<a-result status="404" title="404" sub-title="对不起,您访问的页面不存在.">
<template #extra>
<a-button type="primary" @click="home">返回主页</a-button>
</template>
</a-result>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
export default defineComponent({
name: '404',
setup() {
const router = useRouter()
const home = () => { router.replace('/') }
return { home }
}
})
</script>
<template>
<a-result status="500" title="500" sub-title="对不起,服务器内部错误.">
<template #extra>
<a-button type="primary" @click="home">返回主页</a-button>
</template>
</a-result>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
export default defineComponent({
name: '500',
setup() {
const router = useRouter()
const home = () => { router.replace('/') }
return { home }
}
})
</script>
<template>
<div class="layout__header">
<div class="header__left">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="$emit('update:collapsed', !collapsed)"
/>
<menu-fold-outlined v-else class="trigger" @click="$emit('update:collapsed', !collapsed)" />
<div class="group__tabs">
<a-tabs :activeKey="activeKey" @tabClick="tabClick">
<a-tab-pane v-for="item in routers" :key="item.id" :tab="item.name" />
</a-tabs>
</div>
</div>
<div class="header__right">
<a-dropdown :trigger="['click', 'hover']">
<div class="header__avatar">
<a-avatar>
<template #icon>
<img :src="avatar" v-if="avatar.length > 0" />
<img src="@/assets/layout/avatar.png" v-else />
</template>
</a-avatar>
<div class="header__avatar-name">{{ name.length > 0 ? name : 'admin' }}</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="logout()">
<template #icon><a-icon type="PoweroffOutlined" /></template>
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</template>
<script lang="ts">
import router from '@/router'
import { mapState, useStore } from 'vuex'
import aIcon from '@/components/aicon/aicon.vue'
import { defineComponent, watch, computed, onBeforeMount } from "vue"
import { RouterObj, RouterTable } from '@/types/api/login'
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'layoutHeader',
components: {
MenuUnfoldOutlined,
MenuFoldOutlined,
aIcon
},
computed: {
...mapState({
name: (state: any) => state.user.name,
avatar: (state: any) => state.user.avatar,
routers: (state: any) => {
const array: any[] = []
state.user.routers.forEach((item: any) => { if (!item.hidden) array.push(item) })
return array
}
})
},
emits: ['update:collapsed'],
props: {
collapsed: {
required: true,
type: Boolean
}
},
setup() {
const store = useStore()
const activeKey = computed(() => store.state.menu.menuId)
// 退出登录
const logout = () => {
store.dispatch('user/logout').then(e => {
router.push('/login')
})
}
// 切换tab
const tabClick = (e: number) => {
const routers = store.state.user.routers
let menuRouter: RouterTable = []
routers.forEach((item: RouterObj) => {
if (item.id === e) {
menuRouter = item.children || []
}
})
store.commit('menu/setMenu', menuRouter)
store.commit('menu/setId', e)
}
watch(activeKey, () => {
tabClick(activeKey.value)
})
onBeforeMount(() => {
tabClick(activeKey.value)
})
return { logout, tabClick, activeKey }
}
})
</script>
<style lang="scss" scoped>
.layout__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 22px;
font-size: 20px;
& .header__left {
display: flex;
align-items: center;
flex-grow: 1;
& .group__tabs {
width: 500px;
margin-left: 22px;
margin-right: 22px;
flex-grow: 1;
}
}
& .header__right {
display: flex;
align-items: center;
flex-shrink: 0;
flex-grow: 0;
& .header__avatar {
display: flex;
align-items: center;
padding: 0 12px;
cursor: pointer;
& .header__avatar-name {
margin-left: 6px;
font-size: 14px;
vertical-align: middle;
}
}
}
}
</style>
<style lang="scss">
.layout__header {
& .header__left {
& .ant-tabs-bar {
margin: 0;
border: none;
}
}
}
</style>
\ No newline at end of file
<template>
<a-layout class="layout">
<!-- 侧边 -->
<a-layout-sider width="256" v-model:collapsed="collapsed" :trigger="null" collapsible>
<layout-menu :collapsed="collapsed" />
</a-layout-sider>
<a-layout>
<!-- 头部 -->
<a-layout-header class="header">
<layout-header v-model:collapsed="collapsed" />
</a-layout-header>
<!-- 内容 -->
<a-layout-content class="container">
<layout-tabs />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue"
import layoutMenu from './menu/menu.vue'
import layoutTabs from './tabs/tabs.vue'
import layoutHeader from './header/header.vue'
import aIcon from '@/components/aicon/aicon.vue'
export default defineComponent({
name: 'layout',
components: {
layoutMenu,
layoutHeader,
layoutTabs,
aIcon
},
setup() {
const collapsed = ref<boolean>(false)
return { collapsed }
}
})
</script>
<style lang="scss" scoped>
.layout {
overflow: hidden;
height: 100vh;
& .header {
background-color: #FFF;
padding: 0;
}
& .container {
padding: 8px;
overflow-y: auto;
overflow-x: hidden;
}
& .container::-webkit-scrollbar {
width: 6px;
}
& .container::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 5px #d8d8d8;
background: #C1C1C1;
}
& .container::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px #d8d8d8;
background: #ededed;
}
}
</style>
<style lang="scss">
.layout {
& .ant-layout-sider-children {
overflow-y: auto;
overflow-x: hidden;
}
& .ant-layout-sider-children::-webkit-scrollbar {
width: 4px;
}
& .ant-layout-sider-children::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 5px #d8d8d8;
background: #535353;
}
& .ant-layout-sider-children::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px #d8d8d8;
background: #ededed;
}
}
</style>
\ No newline at end of file
import { defineComponent, Slots } from "vue"
import { RouterObj } from '@/types/api/login'
import aIcon from '@/components/aicon/aicon.vue'
export default defineComponent({
components: {
aIcon
},
props: {
router: {
type: Object,
required: true
}
},
render () {
const menuSub = (router: RouterObj) => {
const subSlots: Slots = {
title: () => [<span>{ router.name }</span>],
icon: () => [<aIcon type= { router.icon || 'FolderOutlined' } />]
}
return (
<a-sub-menu v-slots={ subSlots } key={ router.key }>
{
router.children && router.children.map(item => (
menuCreate(item)
))
}
</a-sub-menu>
)
}
const menuItem = (router: RouterObj) => {
const itemSlots: Slots = {
icon: () => router.icon ? [<aIcon type= { router.icon || '' } />] : []
}
return (
<a-menu-item v-slots={ itemSlots } key={ router.key }>
<router-link to={ router.path }>{ router.name }</router-link>
</a-menu-item>
)
}
const menuCreate = (router: RouterObj) => {
if (router.children && !router.hidden) {
return menuSub(router)
} else if (!router.hidden) {
return menuItem(router)
}
}
return menuCreate(this.router as RouterObj)
}
})
\ No newline at end of file
<template>
<div class="menu__logo">
<div class="menu__logo-icon">
<img src="@/assets/layout/logo.png" />
</div>
<span v-show="!collapsed">HuansiIoTGateway</span>
</div>
<a-menu
theme="dark"
mode="inline"
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
>
<create :router="item" v-for="item in menuRouter" />
</a-menu>
</template>
<script lang="ts">
import create from './menu-create'
import { useRoute } from 'vue-router'
import { mapState, useStore } from 'vuex'
import aIcon from '@/components/aicon/aicon.vue'
import { defineComponent, ref, watch, onBeforeMount } from "vue"
export default defineComponent({
name: 'layoutMenu',
computed: {
...mapState({
menuRouter: (state: any) => state.menu.menuRouter
})
},
components: {
create,
aIcon
},
props: {
collapsed: {
required: true,
type: Boolean
}
},
setup() {
const store = useStore()
const route = useRoute()
const selectedKeys = ref<string[]>([])
const openKeys = ref<string[]>([])
const setMenuKey = () => {
if (!route.meta.hidden) {
selectedKeys.value = [route.name as string]
openKeys.value = []
route.matched.forEach(item => {
openKeys.value.push(item.name as string)
})
// 设置顶部tab(栏目)切换
store.commit('menu/setId', route.matched[0]['meta']['id'])
}
}
onBeforeMount(setMenuKey)
watch(route, setMenuKey)
return { selectedKeys, openKeys }
}
})
</script>
<style lang="scss" scoped>
.menu__logo {
display: flex;
align-items: center;
padding-left: 24px;
height: 64px;
line-height: 64px;
overflow: hidden;
white-space: nowrap;
& .menu__logo-icon {
width: 32px;
margin-right: 8px;
img {
display: block;
width: 100%;
}
}
& span {
display: inline-block;
font-size: 20px;
color: #fff;
}
}
</style>
\ No newline at end of file
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="includeList">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script lang="ts">
import { mapState } from 'vuex'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'layoutView',
computed: {
...mapState({
includeList: (state: any) => state.keepAlive.includeList
})
}
})
</script>
\ No newline at end of file
<template>
<a-tabs
v-model:activeKey="activeKey"
type="editable-card"
:tabBarGutter="6"
@tabClick="jump"
@edit="deltab"
hide-add
class="tabs__view"
>
<a-tab-pane :key="item.fullPath" v-for="(item, index) in tabList">
<template #tab>
<a-dropdown :trigger="['contextmenu']">
<div style="display: inline-block">{{ item.title }}</div>
<template #overlay>
<a-menu @click="condition(item, index, $event)">
<a-menu-item key="current">关闭当前标签</a-menu-item>
<a-menu-item key="right">关闭右侧</a-menu-item>
<a-menu-item key="left">关闭左侧</a-menu-item>
<a-menu-item key="other">关闭其他</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tab-pane>
</a-tabs>
<div class="main__container">
<router-view v-slot="{ Component }">
<keep-alive :include="includeList">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
<script lang="ts">
import { mapState, useStore } from 'vuex'
import { TabItem } from '@/store/modules/tabs'
import { defineComponent, watch, onBeforeMount, ref } from 'vue'
import { useRoute, useRouter, RouteLocationNormalizedLoaded } from 'vue-router'
export default defineComponent({
name: 'layoutTabs',
computed: {
...mapState({
tabList: (state: any) => state.tabs.tabList as TabItem[],
includeList: (state: any) => state.keepAlive.includeList
})
},
setup () {
// 激活的tab
let activeKey = ref<string>()
const store = useStore()
const route = useRoute()
const router = useRouter()
// 添加tab方法
const addTab = (data: RouteLocationNormalizedLoaded) => {
store.commit('tabs/steList', {
fullPath: data.fullPath,
name: data.name,
title: data.meta.title
})
}
// 设置路由缓存(白)名单方法
const setKeepAlive = (data: RouteLocationNormalizedLoaded) => {
if (data.meta.keepAlive) {
store.commit('keepAlive/setKeepAlive', data.name as string)
}
}
watch(route, to => {
addTab(to)
setKeepAlive(to)
activeKey.value = to.fullPath
})
onBeforeMount(() => {
addTab(route)
setKeepAlive(route)
activeKey.value = route.fullPath
})
/**
* @desc:tab点击
* @param { string } targetKey 点击的tabKey
*/
const jump = (targetKey: string) => {
if (route.fullPath !== targetKey) {
router.push(targetKey)
}
}
/**
* @desc:删除tab
* @param { string } targetKey 点击的tabKey
* @param { string } action 事件类型
*/
const deltab = (targetKey: string, action: string) => {
if (action === 'remove') {
store.commit('tabs/delList', targetKey)
}
}
/**
* @desc: 条件删除
* @param { TabItem } tab tab对象
* @param { number } index 序号
*/
const condition = (tab: TabItem, index: number, item: any) => {
switch (item.key) {
case 'current': store.commit('tabs/delList', tab.fullPath); break;
case 'right': store.commit('tabs/delRight', index); break;
case 'left': store.commit('tabs/delLeft', index); break;
case 'other': store.commit('tabs/delOther', index); break;
}
}
return { activeKey, jump, deltab, condition }
}
})
</script>
<style lang="scss" scoped>
.main__container {
background-color: #FFF;
min-height: 280px;
overflow: hidden;
}
</style>
<style lang="scss">
.tabs__view {
& .ant-tabs-tab {
user-select: none;
padding: 0 16px 0 0 !important;
& .ant-dropdown-trigger {
padding-left: 16px;
}
}
& .ant-tabs-bar {
margin: 0 0 8px 0;
}
& .ant-tabs-tab-active {
font-weight: normal;
border-bottom: 1px solid #f0f0f0 !important;
}
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import Antd from 'ant-design-vue'
import '@/assets/css/reset.css'
import 'ant-design-vue/dist/antd.css'
import App from './App.vue'
import router from './router'
import store from './store'
import { globalAxios } from './request'
const app = createApp(App)
// 使用antd
app.use(Antd)
// 使用全局axios
app.use(globalAxios)
// 使用vuex
app.use(store)
// 使用路由
app.use(router)
router.isReady().then(() => app.mount('#app'))
import { App } from 'vue'
import storage from 'store'
import router from '@/router'
import { regAxios } from './install'
import { message } from 'ant-design-vue'
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_REQUEST_BASE_URL as string,
timeout: 6000
})
/**
* @desc: 异常拦截处理器
* @param { Object } error 错误信息
*/
const errorHandler = (error: AxiosError): AxiosError | Promise<AxiosError> => {
message.error(error.message)
return Promise.reject(error)
}
/**
* @desc: 请求发送前拦截
* @param { Object } config 配置参数
*/
request.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig => {
config.headers['token'] = storage.get('token') || ''
return config
}, errorHandler)
/**
* @desc: 服务端响应后拦截
* @param { Object } response 返回的数据
*/
request.interceptors.response.use((response: AxiosResponse): AxiosResponse | Promise<AxiosResponse> => {
if (response.data.code === 200) {
return response
} else if (response.data.code === -401) {
// 登录失效
storage.remove('token')
router.push({ path: '/login', query: { redirect: router.currentRoute.value.fullPath } })
return Promise.reject(response)
} else {
return Promise.reject(response)
}
}, errorHandler)
export const globalAxios = {
install (app: App) {
app.use(regAxios, request)
}
}
export default request
\ No newline at end of file
import { App } from 'vue'
import { AxiosInstance } from 'axios'
// 全局注册axios -> this.$axios
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance
}
}
export const regAxios = {
install (app: App, request: AxiosInstance) {
Object.defineProperties(app.config.globalProperties, {
$axios: {
get: function get () {
return request
}
}
})
}
}
\ No newline at end of file
import { RouteRecordRaw } from 'vue-router'
// 基础路由
export const constantRouterMap: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('/src/views/login/login.vue')
},
{
path: '/layout',
name: 'layout',
component: () => import('/src/layout/index.vue')
},
{
path: '/403',
name: '403',
component: () => import('/src/errorPages/403.vue')
},
{
path: '/404',
name: '404',
component: () => import('/src/errorPages/404.vue')
},
{
path: '/500',
name: '500',
component: () => import('/src/errorPages/500.vue')
}
]
// 白名单(路径)
export const whiteList = ['/login', '/403', '/404', '/500']
\ No newline at end of file
import { App } from 'vue'
import { permission } from './permission'
import { constantRouterMap } from './basics.router'
import { createRouter, createWebHistory } from 'vue-router'
// 实例化路由
const router = createRouter({
history: createWebHistory('/'),
routes: constantRouterMap
})
permission(router)
export default router
\ No newline at end of file
import storage from 'store'
import store from '@/store'
import { whiteList } from './basics.router'
import { Router, RouteRecordRaw } from 'vue-router'
const loginPath = '/login'
const defultPath = '/'
// 权限验证
export const permission = (router: Router) => {
router.beforeEach((to, from, next) => {
if (storage.get('token')) {
if (to.path === loginPath) {
next({ path: defultPath })
} else {
if (store.state.user.name.length === 0) {
store.dispatch('user/userInfo').then(() => {
store.dispatch('user/menu').then(e => {
e.forEach((item: RouteRecordRaw) => {
router.addRoute(item)
})
router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' })
const redirect = from.query.redirect as string | undefined
if (redirect && to.fullPath === redirect) {
next({ ...to, replace: true })
} else {
next({ ...to })
}
})
}).catch(() => {
storage.remove('token')
next({ path: loginPath, query: { redirect: to.fullPath } })
})
} else {
next()
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next({ path: loginPath, query: { redirect: to.fullPath } })
}
}
})
}
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
import { createStore, ModuleTree } from 'vuex'
import user, { UserState } from './modules/user'
import tabs, { TabState } from './modules/tabs'
import menu, { menuState } from './modules/menu'
import keepAlive, { keepAliveState } from './modules/keepAlive'
export interface AllState {
user: UserState,
tabs: TabState,
menu: menuState
keepAlive: keepAliveState
}
// 实例化
const store = createStore<AllState>({
modules: {
user,
tabs,
menu,
keepAlive
}
})
export default store
// 路由缓存(白)名单
export type keepAliveState = {
includeList: string[]
}
const state: keepAliveState = {
// 需缓存路由名称
includeList: []
}
const keepAlive = {
namespaced: true,
state,
mutations: {
setKeepAlive (state: keepAliveState, routeName: string) {
if (routeName && !state.includeList.includes(routeName)) {
state.includeList.push(routeName)
}
}
}
}
export default keepAlive
\ No newline at end of file
import { RouterTable } from '@/types/api/login'
// 侧边菜单
export type menuState = {
menuRouter: RouterTable,
menuId: number
}
const state: menuState = {
// 侧边菜单
menuRouter: [],
// 顶层id(栏目)
menuId: 1
}
const menu = {
namespaced: true,
state,
mutations: {
setMenu (state: menuState, router: RouterTable) {
state.menuRouter = router
},
setId (state: menuState, id: number) {
state.menuId = id
}
}
}
export default menu
\ No newline at end of file
import router from '@/router'
import { constantRouterMap } from '@/router/basics.router'
// tabs栏
export interface TabItem {
title: string,
fullPath: string,
name?: string
}
export type TabState = {
tabList: TabItem[]
}
const state: TabState = {
tabList: []
}
const tabs = {
namespaced: true,
state,
mutations: {
/**
* @desc: 添加tab
* @param { TabItem } item
*/
steList (state: TabState, item: TabItem) {
const tabList = state.tabList
let result = true
// 防止固定路由
for (let m = 0; m < constantRouterMap.length; m++) {
if (constantRouterMap[m]['name'] === item.name) {
result = false
break
}
}
// 防止重复添加
for (let i = 0; i < tabList.length; i++) {
if ( tabList[i]['fullPath'] === item.fullPath ) {
result = false
break
}
}
// 防止添加不存在的路由
if (!router.hasRoute(item.name || '')) {
result = false
}
if (result) tabList.push(item)
},
/**
* @desc: 删除tab
* @param { string } key tabkey
*/
delList (state: TabState, key: string) {
const tabList = state.tabList
const route = router.currentRoute.value
if (tabList.length <= 1) {
return
}
for (let i = 0; i < tabList.length; i++) {
if (tabList[i]['fullPath'] === key) {
if (route.fullPath === key) {
if (tabList.length-1 === i) {
router.push(tabList[i-1]['fullPath'])
} else {
router.push(tabList[i+1]['fullPath'])
}
}
tabList.splice(i, 1)
break
}
}
},
/**
* @desc: 删除右侧
* @param { number } index 选择tab序号
*/
delRight (state: TabState, index: number) {
const tabList = state.tabList
const route = router.currentRoute.value
let current = 0
if (tabList.length <= 1 || tabList.length-1 === index) {
return
}
for(let i = 0; i < tabList.length; i++) {
if (tabList[i]['fullPath'] === route.fullPath) {
current = i
break
}
}
if (index < current) {
router.push(tabList[index]['fullPath'])
}
tabList.splice(index + 1, tabList.length-1)
},
/**
* @desc: 删除左侧
* @param { number } index 选择tab序号
*/
delLeft (state: TabState, index: number) {
const tabList = state.tabList
const route = router.currentRoute.value
let current = 0
if (tabList.length <= 1 || index === 0) {
return
}
for(let i = 0; i < tabList.length; i++) {
if (tabList[i]['fullPath'] === route.fullPath) {
current = i
break
}
}
if (index > current) {
router.push(tabList[index]['fullPath'])
}
tabList.splice(0, index)
},
/**
* @desc: 删除其他
* @param { number } index 选择tab序号
*/
delOther (state: TabState, index: number) {
router.push(state.tabList[index]['fullPath'])
state.tabList = [state.tabList[index]]
}
}
}
export default tabs
\ No newline at end of file
import storage from 'store'
import { AllState } from '../index'
import { ActionContext } from 'vuex'
import { message } from 'ant-design-vue'
import { LoginFrom } from '@/types/views/login'
import { RouterTable } from '@/types/api/login'
import { generator } from '@/utils/parsingRouter'
import { login, info, menu, logout ,data1 } from '@/api/login'
// 处理用户登录、登出、个人信息、权限路由
export type UserState = {
token: string,
name: string,
avatar: string,
roles: string[],
routers?: RouterTable,
username?: string,
password?: string
}
const state: UserState = {
// 标识
token: storage.get('token'),
// 昵称
name: '',
//登录账号
username: '',
//登录密码
password: '',
// 头像
avatar: '',
// 角色(鉴权)
roles: [],
// 路由表(原始未解析)
routers: []
}
const user = {
namespaced: true,
state,
mutations: {
//设置用户名和密码
setUserNameorPwd(state: UserState, info: UserState) {
const { username, password } = info
storage.set('username', username)
storage.set('password', password)
},
// 设置token
setToken(state: UserState, token: string) {
state.token = token
},
// 设置用户信息
setInfo(state: UserState, info: UserState) {
const { name, avatar, roles } = info
state.name = name
state.avatar = avatar
state.roles = roles
},
// 设置路由表(原始未解析)
setRouters(state: UserState, routers: RouterTable) {
state.routers = routers
},
// 用户退出登录
clearState(state: UserState) {
storage.remove('token')
// 为了重新加载用户信息及路由组
state.name = '';
},
clearloginInfo(state: UserState) {
storage.remove('username')
storage.remove('password')
}
},
actions: {
// 登录
login(context: ActionContext<UserState, AllState>, params: LoginFrom) {
return new Promise((resolve, reject) => {
login(params).then(e => {
const data = e.data
storage.set('token', data.token)
context.commit('setToken', data.token)
if (params.rememberMe) {
context.commit('setUserNameorPwd', params)
} else {
context.commit('clearloginInfo')
}
resolve(data)
}).catch(err => {
reject(err)
})
})
},
// 获取用户信息
userInfo(context: ActionContext<UserState, AllState>) {
return new Promise((resolve, reject) => {
info().then(e => {
const info = e.data.info
context.commit('setInfo', info)
resolve(e)
}).catch(err => {
message.error(err.message || err.data.message)
if (err.data && err.data.code !== -401) {
reject(err)
}
})
})
},
// 获取菜单
menu(context: ActionContext<UserState, AllState>) {
return new Promise((resolve) => {
menu().then(e => {
// const routeTable = e.data.data
const routeTable = data1
context.commit('setRouters', routeTable)
// 初始化侧边菜单
context.rootState.menu.menuRouter = routeTable[0]['children'] || []
context.rootState.menu.menuId = routeTable[0]['id']
resolve(generator(routeTable))
}).catch(err => {
message.error(err.message || err.data.message)
})
})
},
// 退出登录
logout(context: ActionContext<UserState, AllState>) {
return new Promise((resolve, reject) => {
logout().then(e => {
context.commit('clearState')
resolve(e)
}).catch(err => {
message.error(err.message || err.data.message)
reject(err)
})
})
}
}
}
export default user
\ No newline at end of file
import { ResponseData } from './public'
import { UserState } from '@/store/modules/user'
// 登录接口约束
export interface LoginSuccess extends ResponseData {
token: string
}
// 用户信息接口约束
export interface UserInfo extends ResponseData {
info: UserState
}
// 路由对象约束
export interface RouterObj {
id: number,
path: string,
name: string,
component: string,
key: string,
redirect?: string,
icon?: string,
children?: RouterObj[],
pid?: number,
hidden?: boolean,
keepAlive?: boolean
}
// 路由数组约束
export type RouterTable = RouterObj[]
// 路由接口约束
export interface RouterData extends ResponseData {
data: RouterTable
}
\ No newline at end of file
export interface ResponseData<T = any> {
code: number,
message: string,
data?: T
}
\ No newline at end of file
import { ResponseData } from './public'
export interface TableList extends ResponseData {
total: number,
current: number,
pageSize: number,
data: []
}
export interface EditData extends ResponseData {
data: any
}
export interface UploadApi extends ResponseData {
url: string
}
export interface OptionsData extends ResponseData {
data: any
}
\ No newline at end of file
export type LoginFrom = {
username: string | undefined,
password: string | undefined,
rememberMe?:boolean
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
/// <reference types="vite/client" />
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment