first commit

This commit is contained in:
云泽网 2025-04-24 11:53:18 +08:00
commit d9e71524b2
79 changed files with 6226 additions and 0 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
# 本地运行端口号
VITE_PORT = 8686
# API接口域名配置
VITE_BASE_URL = http://localhost/

8
.env.development Normal file
View File

@ -0,0 +1,8 @@
# 开发环境
NODE_ENV = development
# 后台请求前缀这是mock地址
VITE_BASE_URL = '/apis'
VITE_PERMISSION_MODE = 'CONSTANT'
# VITE_PERMISSION_MODE = 'FRONT'
# VITE_PERMISSION_MODE = 'BACK'

8
.env.production Normal file
View File

@ -0,0 +1,8 @@
# 生产环境
NODE_ENV = prod
# 后台请求前缀这是mock地址
VITE_BASE_URL = 'https://mock.apifox.cn/m1/3365861-0-default'
VITE_PERMISSION_MODE = 'CONSTANT'
# VITE_PERMISSION_MODE = 'FRONT'
# VITE_PERMISSION_MODE = 'BACK'

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
src/views/test
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

127
LICENSE Normal file
View File

@ -0,0 +1,127 @@
木兰宽松许可证, 第2版
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证第2版“本许可证”的如下条款的约束
0. 定义
“软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体”是指提交贡献的机构及其“关联实体”。
“关联实体”是指对“本许可证”下的行为方而言控制、受控制或与其共同受控制的机构此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
如何将木兰宽松许可证第2版应用到您的软件
如果您希望将木兰宽松许可证第2版应用到您的新软件为了方便接收者查阅建议您完成如下三步
1 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2 请您在软件包的一级目录下创建以“LICENSE”为名的文件将整个许可证文本放入该文件中
3 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, control means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;
ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.

53
README.md Normal file
View File

@ -0,0 +1,53 @@
### 介绍
**Yunzer-Admin** 基于 Vue3.3、TypeScript、Vite3、Pinia、Element-Plus 专注于表格,表单的企业级后台管理框架,取名 **Yunzer**源于NBA球队圣安东尼奥马刺队San Antonio Yunzer作为一支专业篮球队马刺的卓越不止在于技术水平和战术运筹的精妙更在于他们小石匠精神一直激励着大家。
马刺队更衣室里的一句话:
“当一切看起来无可挽回之时,我跑去看石匠重复捶击他面前的岩石一百次,而那块石头连
一个裂缝都没有露出来。接下来的第一百零一次捶击之时,此石一分为二。不是因为这
一次捶击,而是因为你的始终如一。”
共勉......
[代码gitee地址](https://gitee.com/3439/Yunzer-Admin)
[在线预览](http://jdvip.suipin.net)
### 技术栈+版本
本项目技术栈基于`npm^6.14.7+node^14.8.1+Vue3.3.4 + TypeScript + Vite4.4.5 + vue-router4.2.4 + pinia + axios`
### 运行
```javascript
克隆项目
git clone https://gitee.com/3439/Yunzer-Admin.git
进入项目目录
cd Yunzer-Admin
安装依赖
npm install
本地开发 启动项目
npm run dev
```
### 系列文章
[从零开始vue3+vite+ts+pinia+router4后台管理(1)](https://juejin.cn/post/7286112965609357347)
[从零开始vue3+vite+ts+pinia+router4后台管理(2)-页面布局](https://juejin.cn/post/7286508785104322594)
[从零开始vue3+vite+ts+pinia+router4后台管理(3)-动态路由](https://juejin.cn/post/7286679458131312674)
[从零开始vue3+vite+ts+pinia+router4后台管理(4)-导航标签栏和keep-alive缓存](https://juejin.cn/post/7287053284787028003)
[从零开始vue3+vite+ts+pinia+router4后台管理(5)-二次封装表格1.0](https://juejin.cn/post/7288963909581635618)
[从零开始vue3+vite+ts+pinia+router4后台管理(6)-全局自定义指令实现节流与防抖](https://juejin.cn/post/7290470513116856320)
[什么才是完美的表格二次封装elementPlus表格-从零开始vue3+vite+ts+pinia+router4后台管理(7)](https://juejin.cn/post/7301903019222155264)

6
env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
//解决import vue文件红色波浪线问题
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

13
index.html Normal file
View File

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

2391
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "Yunzer-Admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@imengyu/vue3-context-menu": "^1.3.3",
"axios": "^1.5.0",
"element-plus": "^2.4.2",
"element-plus-table-dragable": "^1.0.0",
"js-cookie": "^3.0.5",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@types/nprogress": "^0.2.3",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^1.8.5"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
src/App.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<el-config-provider :locale="zh">
<router-view></router-view>
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import zh from 'element-plus/es/locale/lang/zh-cn'
</script>
<style scoped>
</style>

12
src/api/system/index.ts Normal file
View File

@ -0,0 +1,12 @@
import request from '@/utils/request'
export default {
//获取菜单管理列表
getMenuList(data) {
return request({
url: '/system/getMenuList',
method: 'post',
params: data
})
},
}

12
src/api/table/index.ts Normal file
View File

@ -0,0 +1,12 @@
import request from '@/utils/request'
export default {
//获取表格数据
packTableList(data:any) {
return request({
url: '/table/packTable',
method: 'post',
params: data
})
},
}

14
src/api/user/index.ts Normal file
View File

@ -0,0 +1,14 @@
import request from '@/utils/request'
export function login(data:object) {
return request({
url: '/system/login',
method: 'post',
data: data
})
}
export function getIndex() {
return request({
url: '/system/index',
method: 'post'
})
}

69
src/assets/background.svg Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,99 @@
/*自己定义主题的css样式开始*/
$--colors: (
'primary': (
'base': #16baaa
),
'success': (
'base': #16b777
),
'warning': (
'base': #FFB800
),
'danger': (
'base': #FF5722
),
'error': (
'base': #f56c6c
),
'info': (
'base': #909399
),
);
@forward "element-plus/theme-chalk/src/common/var.scss" with
(
$colors: $--colors
);
@use "element-plus/theme-chalk/src/index.scss" as *;
/*自己定义主题的css样式结束*/
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
padding: 0;
margin: 0;
outline: none !important;
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
a {
text-decoration: none
}
html,
body,
#app {
padding: 0;
margin: 0;
background: #fff;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.app-container {
box-sizing: border-box;
}
.table-con {
margin-top: 10px;
}
.green-button {
background: #16baaa;
border: none;
color: #fff;
}
.table-con{
width: 100%;
overflow-x: auto;
}

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,55 @@
<!-- 分页el-pagination 二次封装 -->
<template>
<div v-show="total>0" class="pagination-con">
<el-pagination
v-model:current-page="curPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40, 50]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup name="Pagination" lang="ts">
import { computed} from 'vue'
const props = defineProps({
page: { type: Number, default: 1 },
size: { type: Number, default: 10 },
total: { type: Number, default: 0 }
})
// 1defineEmits
// 2使defineEmits使emit()
// 3emits
const emit = defineEmits(['update:size', 'update:page', 'pagination'])
// console.vlog(emit)
const pageSize = computed({
get: () => props.size,
set: (val) => {
emit('update:size', val)
}
})
const curPage = computed({
get: () => props.page,
set: (val) => {
emit('update:page', val)
}
})
function handleSizeChange() {
emit('pagination')
}
function handleCurrentChange() {
emit('pagination')
}
</script>
<style scoped>
.pagination-con{
margin-top: 10px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="spurs-dialog">
<el-dialog
v-bind="$attrs"
v-model="dialogVisible"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
draggable
>
<slot name="body"></slot>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible=false">取消</el-button>
<el-button v-if="dialogType!='readonlyDialog'" type="primary" @click="saveSubmit">
</el-button>
<slot name="footer"></slot>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
let dialogVisible = ref(false)
defineExpose({
dialogVisible
})
const props = defineProps({
dialogType: {type:String, default:""}//readonlyDialogdialog
});
const close = () =>{
dialogVisible.value = false
}
const emit = defineEmits(['saveSubmit'])
const saveSubmit = () => {
emit('saveSubmit');
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,31 @@
type FormType =
|'input'
| 'password'
| 'select'
| 'datepicker'
| 'timepicker'
| 'switch'
| 'radio'
| 'textarea'
interface ItemOption {
label: string
value: string | number
}
export interface FormItem {
field: string //字段名
type?: FormType //输入框类型
label: string //输入框标题
colSpan?: number// 栅格占据的列数默认24
disabled?:boolean//表单是否可修改 默认false
placeholder?: any //输入框默认显示内容
prop?: string //表单校验
options?: ItemOption[] //选择器的可选子选项 select
otherOptions?: any//特殊情况
isHidden?: boolean
slotName?: string//处理一些自定义内容
}
export interface FormOption {
formItems: FormItem[]
labelWidth?: string//标签的长度
}

View File

@ -0,0 +1,88 @@
<template>
<div class="header">
<slot name="header"> </slot>
</div>
<el-form
ref="ruleFormRef"
:label-width="labelWidth"
status-icon
:model="modelValue"
v-bind="$attrs"
>
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col :span="item.colSpan??24">
<el-form-item
v-if="!item.isHidden"
:label="item.label"
:prop="item.field"
>
<template v-if="item.type === 'input' || item.type === 'password'">
<el-input
:disabled="item.disabled??false"
:placeholder="item.placeholder"
:show-password="item.type === 'password'"
v-model="modelValue[`${item.field}`]"
clearable
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-model="modelValue[`${item.field}`]"
style="width: 100%"
clearable
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
:label="option.label"
>
</el-option>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
unlink-panels
value-format="YYYY-MM-DD"
v-bind="item.otherOptions"
v-model="modelValue[`${item.field}`]"
></el-date-picker>
</template>
<template v-if="item.slotName!=undefined">
<slot :name="item.slotName"></slot>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
<el-form-item>
<slot name="footer"></slot>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import {FormItem} from "@/components/YunzerForm/formType.ts";
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
//
interface Props {
formItems: FormItem[] //
labelWidth?: string //
modelValue: object //
}
const props = withDefaults(defineProps<Props>(), {
formItems: () => [],
})
const ruleFormRef = ref<FormInstance>()
defineExpose({
ruleFormRef
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,78 @@
<template>
<el-table
ref="tableRef"
style="width: 100%"
v-loading="loading"
:data="tableData"
border
v-bind="$attrs"
>
<!-- 1.传入showSelectColumn时展示的全选列 -->
<template v-if="showSelectColumn">
<el-table-column type="selection" />
</template>
<!-- 2.传入showIndexColumn时展示的序号列 -->
<template v-if="showIndexColumn">
<el-table-column type="index" label="#" />
</template>
<!-- 3.propList里面的所有列 -->
<template v-for="item in propList" :key="item.prop">
<el-table-column v-bind="item" show-overflow-tooltip>
<!-- 传有slotName时展示的插槽列 -->
<template #default="scope" v-if="item.filter">
{{item.filter(scope.row[`${item.prop}`])}}
</template>
<template #default="scope" v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</el-table-column>
</template>
</el-table>
<Pagination
v-if="showPagination"
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
</template>
<script setup lang="ts">
import {withDefaults} from 'vue'
import {ColumnOption} from "@/components/YunzerTable/tableType.ts";
import {useTable} from '@/hooks/useTable.ts'
interface Props {
requestApi: Function // apiaxios ==>
queryForm?:any
propList: ColumnOption[] //el-table-column
showIndexColumn?: boolean //
showSelectColumn?: boolean //
showPagination?: boolean //
childrenProps?: object //
}
const props = withDefaults(defineProps<Props>(), {
showIndexColumn: false,
showSelectColumn: false,
showPagination: false,
childrenProps: () => ({})
})
const {
tableData,
pagination,
total,
loading,
getTableData,
handleSearch,//
refreshTableInfo//
} = useTable(props.requestApi,props.queryForm)
defineExpose({
tableData,
handleSearch,
refreshTableInfo
})
// console.log(props);
</script>
<style scoped></style>

View File

@ -0,0 +1,17 @@
//表格行el-table-column配置项
export interface ColumnOption {
prop?: string
label: string
minWidth?: string
slotName?: string
align?: string
filter?: Function | undefined
}
//表格配置项
export interface TableOption {
propList: ColumnOption[]
showIndexColumn?: boolean
showSelectColumn?: boolean
showPagination?: boolean
childrenProps?: object
}

23
src/directive/debounce.ts Normal file
View File

@ -0,0 +1,23 @@
// 输入框节流
import { App, DirectiveBinding } from "vue";
// 输入框防抖
export default (app: App) => {
app.directive("debounce", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const func = binding.value?binding.value.func : null//binding.value.func 这是输入框传过来的方法
el.timer = null
el.addEventListener('input', () => {
console.log(binding);
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
}
el.timer = setTimeout(() => {
func && func()
}, 1000)
})
}
})
}

53
src/directive/index.ts Normal file
View File

@ -0,0 +1,53 @@
//bindingDirectiveBinding一个包含指令信息的对象。例如包含指令的名称、值、参数等等
import {App, DirectiveBinding} from "vue";
export default (app: App) => {
// 节流 防止按钮多次点击,多次请求
app.directive("throttle", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const time = binding.value ? binding.value : 2000
el.timer = null
el.addEventListener('click', () => {
//console.log(binding);
el.disabled = true
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
el.disabled = true
}
el.timer = setTimeout(() => {
el.disabled = false
}, time)
})
}
})
// 输入框防抖
app.directive("debounce", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const time = binding.value.time ? binding.value.time : 1000//binding.value.time这是输入框传过来的时间
const func = binding.value ? binding.value.func : null//binding.value.func 这是输入框传过来的方法
el.timer = null
el.addEventListener('input', () => {
//console.log(binding);
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
}
el.timer = setTimeout(() => {
func && func()
}, time)
})
}
})
//表格可视区域内滚动
app.directive("allheight", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const top:number = el.offsetTop//el-table距离窗口顶部偏移量
const bottom:number = binding?.value?.bottom ? binding.value.bottom : 65//底部预留的高度 默认高度52
const pageHeight:number = window.innerHeight//页面的高度
el.style.height = pageHeight - top - bottom-90 + 'px'
}
})
}

0
src/hooks/useForm.ts Normal file
View File

61
src/hooks/useTable.ts Normal file
View File

@ -0,0 +1,61 @@
import {reactive, onMounted, ref, nextTick} from 'vue'
import { useRoute, useRouter } from 'vue-router'
export function useTable(loadDataFunc: Function,queryForm: {}) {
let loading = ref(true)
let tableData = ref(new Array<any>())
let total = ref(0)
let tableHeight = ref("0px")
const router = useRouter()
const route = useRoute()
const pagination = reactive({
pageNum: 1,
pageSize: 10
})
const getTableData = async () => {
loading.value = true;
getTableHeight(160,45)//获取表格的自适应高度
const res = await loadDataFunc({...queryForm,...pagination})
tableData.value = res.data.list;
total.value = res.data.total
loading.value = false;
}
const getTableHeight = (tabletop:number,tablebottom:number):void =>{
const top:number = tabletop//el-table距离窗口顶部偏移量
const bottom:number = tablebottom//
const pageHeight:number = window.innerHeight//页面的高度
if(document.getElementsByClassName("el-pagination").length>0){
//判断页面有木有分页,来控制表格的高度自适应
tableHeight.value = pageHeight - top - bottom + 'px'
}else {
tableHeight.value = pageHeight - top + 'px'
}
}
onMounted(() => {
getTableData()
})
// 搜索
const handleSearch = () => {
pagination.pageNum = 1
getTableData()
}
const refreshTableInfo = () => {
//刷新当前路由
const { fullPath } = route
nextTick(() => {
router.replace({
path: '/redirect' + fullPath
})
})
}
return {
loading,
tableData,
total,
pagination,
getTableData,
handleSearch,
tableHeight,
refreshTableInfo
}
}

View File

@ -0,0 +1,33 @@
<template>
<div class="common-layout">
<el-container>
<el-aside :width="menuCollapse?'64px':'200px'">
<v-aside></v-aside>
</el-aside>
<el-container style="overflow-y: auto">
<el-header style="padding: 0px;height: 90px">
<v-header></v-header>
</el-header>
<el-main style="padding: 0px;">
<el-scrollbar>
<v-main></v-main>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import VAside from "@/layout/components/vAside/vAside.vue";
import VMain from "@/layout/components/vMain/vMain.vue";
import VHeader from "@/layout/components/vHeader/vHeader.vue";
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
import {computed} from 'vue'
const menuCollapse = computed(() => settingsStore.menuCollapse)
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="sidebar-logo-container" :class="{ menuCollapse: menuCollapse }">
<transition name="sidebarLogoFade">
<router-link
class="sidebar-logo-link"
to="/"
>
<img :src="LogoImg" class="sidebar-logo" />
<span class="sidebar-title">{{title}}</span>
</router-link>
</transition>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
import LogoImg from '@/assets/logo.png'
const state = reactive({
title: 'Yunzer-Admin'
})
const { title, logo } = toRefs(state)
const props = defineProps({
menuCollapse: {
type: Boolean,
required: true
}
})
// console.log(props.menuCollapse);
</script>
<style scoped>
.sidebar-logo{
width: 40px;
height: 40px;
vertical-align: middle;
margin-right: 12px;
padding-bottom: 4px;
}
.sidebar-title{
line-height: 50px;
font-weight: bold;
display: inline-block;
vertical-align: middle;
color: #333;
font-size: 18px;
}
.sidebar-logo-container{
height: 50px;
line-height: 50px;
border-bottom: 1px solid #ddd;
text-align: center;
box-sizing: border-box;
overflow: hidden;
}
.sidebar-logo-link{
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<el-sub-menu
:index="item.path"
v-if="item && !item.hidden && item.children && item.name!='Dashboard'"
>
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{item.meta.title}}</span>
</template>
<el-menu-item
style="background: #f6f6f6"
v-for="route in item.children"
:key="route.path"
:index="route.path"
>
<span>{{route.meta.title}}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item style="background: #f6f6f6" :index="item.children[0].path" v-else-if="item && !item.hidden && item.name=='Dashboard'">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>{{item.meta.title}}</template>
</el-menu-item>
<el-menu-item style="background: #f6f6f6" :index="item.path" v-else-if="item && !item.hidden">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>{{item.meta.title}}</template>
</el-menu-item>
</template>
<script setup lang="ts" name="SidebarItem">
const props = defineProps({
item: { type: Object, default: () => {} }
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,55 @@
<template>
<div class="aside-con">
<logo :menuCollapse=menuCollapse />
<el-scrollbar class="vertical-menus-scrollbar">
<el-menu
:collapse-transition="false"
:collapse="menuCollapse"
:default-active="activeMenu"
class="el-menu-vertical-demo"
router
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
>
</sidebar-item>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { usePermissionStore } from '@/store/permission'
import { useSettingsStore } from '@/store/settings'
import { computed } from 'vue'
import SidebarItem from '@/layout/components/vAside/components/index.vue'
import Logo from '@/layout/components/vAside/components/Logo.vue'
const permissionStore = usePermissionStore()
const settingsStore = useSettingsStore()
const routes = computed(() => permissionStore.routes)
const menuCollapse = computed(() => settingsStore.menuCollapse)
import { useRoute } from 'vue-router'
const route = useRoute()
const activeMenu = computed(() => {
const {path } = route
return path
})
// npm install vue-awesome-console --save-dev
// console.vlog(routes)
</script>
<style scoped>
.aside-con{
height: 100vh;
overflow: hidden;
background-color: #f6f6f6;
}
.el-menu-vertical-demo{
background: #f6f6f6;
border-right: none;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="drawer-wrapper">
<el-drawer
v-model="showSettings"
title="系统配置"
direction="rtl"
size="310px"
>
<el-divider>布局方式</el-divider>
<div class="layout-wrapper">
<el-row :gutter="20">
<el-col @click="handleLayoutMode('Classic')" :span="12">
<div
class="classic"
:class="{'active-layout': layoutMode === 'Classic' ? true : false}"
>
<div class="sidebar"></div>
<div class="main-wrapper">
<div class="main-header"></div>
<div class="main"></div>
</div>
<div class="introduce">经典</div>
</div>
</el-col>
<el-col @click="handleLayoutMode('Streamline')" :span="12">
<div
class="streamline"
:class="{'active-layout': layoutMode === 'Streamline' ? true : false}"
>
<div class="main-wrapper">
<div class="main-header"></div>
<div class="main"></div>
</div>
<div class="introduce">单栏</div>
</div>
</el-col>
</el-row>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs,computed } from 'vue'
import { useSettingsStore } from '@/store/settings'
const settingsStore = useSettingsStore()
const state = reactive({
showSettings:false,
layoutMode:null
})
const {
showSettings
} = toRefs(state)
const layoutMode = computed(() => settingsStore.layoutMode)
const handleLayoutMode = (mode:any) => {
state.layoutMode = mode
state.showSettings = false
settingsStore.changeSetting({
key: 'layoutMode',
value: mode
})
}
defineExpose({
showSettings
})
</script>
<style scoped>
.active-layout {
border: 1px solid #409eff;
}
.streamline{
cursor: pointer;
position: relative;
display: flex;
height: 100px;
box-sizing: border-box;
}
.classic{
cursor: pointer;
position: relative;
display: flex;
height: 100px;
box-sizing: border-box;
}
.sidebar {
width: 20%;
background-color: #ebeef5;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 5px;
.main-header {
height: 10%;
background-color: #dcdfe6;
}
.main {
flex: 1;
margin-top: 5px;
background-color: #f2f6fc;
}
}
.introduce{
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #a0cfff;
}
</style>

View File

@ -0,0 +1,310 @@
<template>
<div class="tags-view-container">
<el-scrollbar
@scroll="handleScroll"
ref="refScrollbar"
class="tags-view-wrapper"
>
<router-link
to='/'
:class="route.path=='/dashboard' ? 'active' : ''"
class="tags-view-item"
>
<span class="tags-title">首页</span>
</router-link>
<router-link
v-for="tag in visitedViews"
ref="refTag"
:class="isActive(tag) ? 'active' : ''"
class="tags-view-item"
:to="tag.path"
:key="tag.path"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
<span class="tags-title">{{tag.meta.title}}</span>
<el-icon @click.prevent.stop="closeSelectedTag(tag)" class="tags-icon">
<Close />
</el-icon>
</router-link>
</el-scrollbar>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
关闭
</li>
<li @click="closeAllTags(selectedTag)">关闭全部</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, toRefs, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsViewStore } from '@/store/tagsView'
const tagsViewStore = useTagsViewStore()
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
const visitedViews = computed(() => tagsViewStore.visitedViews)
const tagAndTagSpacing = 4 // tagAndTagSpacing
const state = reactive({
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
refTag: null,
refScrollbar: null
})
const { visible, top, left, selectedTag, refTag, refScrollbar } = toRefs(state)
const router = useRouter()
const route = useRoute()
// console.log(router);
// console.log(route);
const isActive = (tag) => {
return tag.path === route.path
}
const isAffix = (tag) => {
return tag.meta && tag.meta.affix
}
const addTags = () => {
const { name } = route
if (name && name!='Dashboard') {
tagsViewStore.addView(route)
}
return false
}
const moveToTarget = (currentTag) => {
const $container = refScrollbar.value.wrapRef
const $containerWidth = $container.offsetWidth
const $scrollWidth = $container.scrollWidth
const tagList = refTag.value
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$container.scrollLeft = 0
} else if (lastTag === currentTag) {
$container.scrollLeft = $scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex((item) => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $container.scrollLeft + $containerWidth) {
$container.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $container.scrollLeft) {
$container.scrollLeft = beforePrevTagOffsetLeft
}
}
}
const moveToCurrentTag = () => {
nextTick(() => {
for (const tag of refTag.value) {
if (tag.to.path === route.path) {
moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== route.fullPath) {
tagsViewStore.updateVisitedView(route)
}
break
}
}
})
}
const refreshSelectedTag = (view) => {
// console.vlog(view);
tagsViewStore.delCachedView(view).then(() => {
const { fullPath } = view
nextTick(() => {
console.log(fullPath);
router.replace({
path: '/redirect' + fullPath
})
})
})
}
const closeSelectedTag = (view) => {
tagsViewStore.delView(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
const closeOthersTags = () => {
router.push(selectedTag)
tagsViewStore.delOthersViews(selectedTag).then(() => {
moveToCurrentTag()
})
}
const closeAllTags = (view) => {
tagsViewStore.delAllViews().then(({ visitedViews }) => {
if (state.affixTags.some((tag) => tag.path === view.path)) {
return
}
toLastView(visitedViews, view)
})
}
const toLastView = (visitedViews, view) => {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath })
} else {
router.push('/')
}
}
}
const openMenu = (tag, e) => {
const menuMinWidth = 105
let offsetLeft:number = 0;
if(!settingsStore.menuCollapse && settingsStore.layoutMode!='Streamline'){
offsetLeft = refScrollbar.value.wrapRef.getBoundingClientRect().left-200 // container margin left
}else {
offsetLeft = refScrollbar.value.wrapRef.getBoundingClientRect().left // container margin left
}
const offsetWidth = refScrollbar.value.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const currentLeft = e.clientX - offsetLeft + 15 // 15: margin right
if (currentLeft > maxLeft) {
state.left = maxLeft
} else {
state.left = currentLeft
}
state.top = e.clientY
state.visible = true
state.selectedTag = tag
}
const closeMenu = () => {
state.visible = false
}
const handleScroll = () => {
closeMenu()
}
onMounted(() => {
addTags()
})
watch(
() => route.path,
() => {
addTags()//
}
)
watch(
() => state.visible,
(newValue) => {
if (newValue) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
}
)
// console.vlog(router);
// console.vlog(route);
</script>
<style scoped>
.tags-view-wrapper{
display: flex;
align-items: center;
height: 40px;
}
.tags-view-item{
display: inline-block;
position: relative;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
padding: 0 10px;
margin-top: 7px;
color: #495060;
font-size: 12px;
margin-right: 10px;
}
.tags-title{
}
.tags-icon{
margin-top: -2px;
vertical-align:middle;
margin-left: 4px;
border-radius: 50%;
}
.tags-icon:hover{
background-color: #b4bccc;
color: #fff;
}
.active{
color: #000c;
margin-right: 10px;
}
.active .tags-title:before{
content: "";
background: #16baaa;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li{
margin: 0;
padding: 7px 16px;
cursor: pointer;
}
.contextmenu li:hover{
background: #eee;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div class="fixed-header">
<div class="navbar" v-if="layoutMode=='Classic'">
<div class="left-menu">
<div @click="toggleClick" class="hamburger-container">
<el-icon v-if="menuCollapse" color="#3c3c3c" :size="20">
<Expand/>
</el-icon>
<el-icon v-else="!menuCollapse" color="#3c3c3c" :size="20">
<Fold/>
</el-icon>
</div>
<div class="breadcrumb-container">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
v-for="(item,index) in levelList"
:key="index"
>
<a :href="item.path">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div class="right-menu">
<div class="user-container">
<el-icon @click="handleDrawerOpen" style="margin-right: 5px" color="#333" size="20px">
<Setting />
</el-icon>
<el-dropdown trigger="click">
<div class="user-wrapper">
<span class="user-icon">
<!-- <el-avatar size="small" shape="square"> {{userInfo.userName.charAt(0)}} </el-avatar> -->
</span>
<!-- <span class="user-name">&nbsp;{{userInfo.userName}}</span> -->
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="navbar-wrapper" v-if="layoutMode=='Streamline'">
<div class="navbar-logo">
<logo :menuCollapse="false" />
</div>
<div class="navbar-left">
<el-scrollbar class="vertical-menus-scrollbar">
<el-menu
mode="horizontal"
:collapse-transition="false"
:collapse="menuCollapse"
:default-active="activeMenu"
class="el-menu-vertical-demo"
router
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
>
</sidebar-item>
</el-menu>
</el-scrollbar>
</div>
<div class="navbar-right">
<div class="user-container">
<el-icon @click="handleDrawerOpen" style="margin-right: 5px" color="#333" size="20px">
<Setting />
</el-icon>
<el-dropdown trigger="click">
<div class="user-wrapper">
<span class="user-icon">
<!-- <el-avatar size="small" shape="square"> {{userInfo.userName.charAt(0)}} </el-avatar> -->
</span>
<!-- <span class="user-name">&nbsp;{{userInfo.userName}}</span> -->
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="tags-view">
<tags-view></tags-view>
</div>
<setting-view ref="settingView"></setting-view>
</div>
</template>
<script setup lang="ts">
import TagsView from "@/layout/components/vHeader/components/TagsView.vue";
import SettingView from "@/layout/components/vHeader/components/SettingView.vue";
import Logo from '@/layout/components/vAside/components/Logo.vue'
const routes = computed(() => permissionStore.routes)
import {computed, reactive, toRefs, watch,ref} from 'vue'
import { useUserStore } from '@/store/user'
import {useSettingsStore} from '@/store/settings'
import { usePermissionStore } from '@/store/permission'
const permissionStore = usePermissionStore()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const settingsStore = useSettingsStore()
const menuCollapse = computed(() => settingsStore.menuCollapse)
const layoutMode = computed(() => settingsStore.layoutMode)
import {useRoute,useRouter} from 'vue-router'
import SidebarItem from "@/layout/components/vAside/components/index.vue";
const settingView:any = ref(null)
const state:any = reactive({
levelList: null
})
const {levelList} = toRefs(state)
const route = useRoute()
const router = useRouter()
const activeMenu = computed(() => {
const {path } = route
return path
})
const toggleClick = () => {
settingsStore.menuCollapse = !settingsStore.menuCollapse
}
const handleDrawerOpen = () => {
settingView.value.showSettings = true;
}
const getBreadcrumb = () => {
state.levelList = route.matched.filter(x => x.name != "Dashboard")
}
getBreadcrumb()
const logout = async () => {
await userStore.logout()
router.push('/login')
}
// watch
watch(
() => route.path,
() => {
getBreadcrumb()
}
)
</script>
<style scoped>
.navbar-wrapper{
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
background-color: #fff;
border-bottom: solid 1px #e6e6e6;
box-sizing: border-box;
}
.navbar-left{
flex: 1;
width: 50%;
height: 100%;
}
.navbar-logo{
display: flex;
align-items: center;
height: 100%;
padding: 0 10px;
}
.navbar-right{
display: flex;
align-items: center;
height: 100%;
padding: 0 10px;
}
.tags-view{
width: 100%;
padding: 0 15px;
height: 40px;
background: #fff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
box-sizing: border-box;
}
.user-container{
display: flex;
align-items: center;
height: 100%;
transition: background 0.3s;
cursor: pointer;
}
.navbar {
display: flex;
align-items: center; /*上下的位置*/
justify-content: space-between; /*均匀排列每个元素*/
height: 50px;
background: #fff;
border-bottom: 1px solid #ddd;
box-sizing: border-box;
}
.left-menu {
display: flex;
align-items: center;
height: 100%;
}
.right-menu {
display: flex;
align-items: center;
justify-content: flex-end; /* 从行尾位置开始排列 */
height: 100%;
width: 200px;
}
.hamburger-container {
display: flex;
align-items: center;
height: 100%;
padding: 0 15px;
transition: background 0.3s;
cursor: pointer;
}
.breadcrumb-container {
display: flex;
align-items: center;
height: 100%;
margin-left: 10px;
white-space: nowrap;
}
.user-wrapper{
position: relative;
height: 49px;
line-height: 49px;
padding: 0 10px;
padding-left: 5px;
}
.user-wrapper:hover{
background: #f6f6f6;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTagsViewStore } from '@/store/tagsView'
const tagsViewStore = useTagsViewStore()
const cachedViews = computed(() => tagsViewStore.cachedViews)
// console.vlog("cachedViews",cachedViews);
</script>
<style scoped>
.app-main{
padding: 10px;
padding-left: 15px;
box-sizing: border-box;
}
</style>

19
src/layout/index.vue Normal file
View File

@ -0,0 +1,19 @@
<template>
<div class="common-layout">
<component :is="layoutMode" />
</div>
</template>
<script setup lang="ts">
import Classic from '@/layout/classic/index.vue'
import Streamline from '@/layout/streamline/index.vue'
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
import {computed} from 'vue'
const layoutEnum:any = {
Classic: Classic,//
Streamline: Streamline//
}
const layoutMode = computed(() => layoutEnum[settingsStore.layoutMode])
</script>
<style scoped>
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="common-layout">
<el-container>
<el-header style="padding: 0px;height: 90px">
<v-header></v-header>
</el-header>
<el-main style="padding: 0px">
<v-main></v-main>
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts">
import VMain from "@/layout/components/vMain/vMain.vue";
import VHeader from "@/layout/components/vHeader/vHeader.vue";
</script>
<style scoped>
</style>

40
src/main.ts Normal file
View File

@ -0,0 +1,40 @@
import { createApp } from 'vue' //引入vue
const app = createApp(App)// 创建vue实例
import App from './App.vue'//引入入口组件
import router from './router/index'// 引入路由
import ElementPlus from 'element-plus'//引入element-plus
import 'element-plus/dist/index.css'//引入element-plus样式
import './assets/style/style.scss'//引入全局样式
import "@/permission.ts"//路由钩子权限
import * as ElementPlusIconsVue from '@element-plus/icons-vue'//引入element-plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}// 全局导入plus图标
import directive from "@/directive/index";// 引入全局自定义指令
app.use(directive);//注册全局自定义指令
import * as filters from "@/utils/filters.ts";//引入全局过滤器
app.config.globalProperties.$filters =filters//注册全局过滤器
import vlog from '@/utils/vue-awesome-console.ts'//解决console.log打印出对象的问题 自己用的
// 你可以选择将 vlog 方法挂在 console 对象上,然后像使用 console.log 一样使用 console.vlog
// 同时你也可以根据项目中的开发/生产模式,进行不同的使用方式
if (process.env.NODE_ENV === 'development') {
// @ts-ignore
window.console.vlog = vlog
} else {
// @ts-ignore
window.console.vlog = () => {}
}
import Pagination from '@/components/Pagination/index.vue'//引入分页组件
app.component('Pagination', Pagination)//注册分页
import YunzerDialog from '@/components/YunzerDialog/index.vue'//引入dialog组件
app.component('YunzerDialog', YunzerDialog)//注册dialog
import {createPinia} from 'pinia'//(读音:皮尼亚)引入状态管理
const pinia = createPinia()// 创建pinia实例
app.use(router)// 注册路由
app.use(ElementPlus)// 注册element-plus
app.use(pinia)// 注册状态管理
app.mount('#app')// 挂载vue实例

56
src/permission.ts Normal file
View File

@ -0,0 +1,56 @@
import router from './router/index'
import { useUserStore } from '@/store/user'
import { usePermissionStore } from '@/store/permission'
import {getToken} from '@/utils/storage.ts'
import NProgress from 'nprogress'
import'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login','/test'] // 白名单
router.beforeEach(async (to) => {
NProgress.start();
document.title = `${to.meta.title} | Yunzer-Admin`
const hasToken = getToken('Access-Token')
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// if (hasToken) {//判断token是否存在 存在即为已经登录
// if (to.path !== "/login") {
// if (userStore.init) { // 获取了动态路由 init一定true,就无需再次请求 直接放行
// return true
// }else {
// // init为false,一定没有获取动态路由,就跳转到获取动态路由的方法
// const result:any = await userStore.getInfo() //获取路由
// const accessRoutes:any = await permissionStore.generateRoutes(result.menus) //解析路由,存储路由
// // console.log("accessRoutes",accessRoutes);
// // 动态挂载路由
// accessRoutes.forEach((route:any) => {
// router.addRoute(route)
// })
// userStore.init = true//init改为true,路由初始化完成
// return { ...to, replace: true }// hack方法 确保addRoute已完成
// }
// }else {
// NProgress.done()
// return '/'
// }
// }else {
// // 白名单,直接放行
// if (whiteList.indexOf(to.path) > -1) return true
// // 非白名单,去登录
// else return '/login'
// NProgress.done()
// }
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
// 1. 在路由钩子里面判断是否首次进入系统(permission.ts)
// 2. init为true说明已经获取过路由,就直接放行,init为false则向后台请求用户路由
// 3. 获取路由
// 4. 解析路由,存储权限
// 5. 使用router的api,addRouter拼接路由
// 6. 存储路由
// 7. init改为true,路由初始化完成
// 8. 放行路由

69
src/router/index.ts Normal file
View File

@ -0,0 +1,69 @@
import {createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/index.vue'
//constantRoutes 静态路由 登陆,首页等。。。
export const constantRoutes = [
{
path: '/redirect',//解决刷新问题
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue')
}
]
},
{
path: "/",
name: 'Dashboard',
redirect: '/dashboard',
meta: {
title: '首页',
icon: "House",
},
component: Layout,
children: [
{
path: '/dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: "/login",
name: "Login",
meta: {
title: '登录'
},
component: () => import('@/views/user/login/index.vue'),
hidden: true
},
// {
// path: "/test",
// name: "Test",
// meta: {
// title: '测试'
// },
// component: () => import('@/views/test/test6.vue'),
// hidden: true
// },
{
path: '/:pathMatch(.*)*',// 此写法解决动态路由页面刷新的 warning 警告
component: () => import('@/views/user/error-page/404.vue'),
hidden: true
},
]
//动态路由 asyncRoutes
export const asyncRoutes = [];
const router = createRouter({
history: createWebHashHistory(),// hash模式,history是createWebHistory(地址栏不带#)hash是createWebHashHistory地址栏带#
routes: constantRoutes
})
export default router
// 1. 定义路由组件.
// 也可以从其他文件导入
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。

79
src/store/permission.ts Normal file
View File

@ -0,0 +1,79 @@
import {defineStore} from 'pinia'
import {constantRoutes} from '@/router'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('@/views/**/**.vue')
interface RoutesItem {
path: string,
name: string,
meta: {
title: string,
icon: string
keepAlive: boolean // 是否使用 keep-alive
},
component: object | null,
children?:Object[]| null
}
// console.log(modules);
export const filterAsyncRoutes = (routerList:any) => {
//进行递归解析
//testData 后端获取的路由
const res:Object[] = []
// console.log(testData);
routerList.forEach((e:any) => {
// console.log(e.component);
let e_new:RoutesItem = {
path: e.url,
name: e.name,
meta: {
title: e.menuName,
icon: e.icon,
keepAlive: true // 是否使用 keep-alive
},
component: null,
}
if (e.menuType === 'M') {
// @ts-ignore
e_new.component = Layout
}else {
// console.log("22222",e.url);
// @ts-ignore
e_new.component = modules[`/src/views${e.url}/index.vue`]
}
// console.log(e_new);
if (e.children && e.children!=null) {
const children:any = filterAsyncRoutes(e.children)
// 保存权限
e_new = { ...e_new, children: children }
}
res.push(e_new)
})
// console.vlog("111",res);
return res
}
export const usePermissionStore = defineStore('permission', {
state: () => {
return {
routes: new Array<any>(),//全部路由
addRoutes: new Array<any>()//后端增加的路由
}
},
actions: {
generateRoutes(routes:any) {
// console.log(routes);
let routerList = JSON.parse(JSON.stringify(routes))
// console.log(routerList);
return new Promise((resolve) => {
const accessedRoutes:any = filterAsyncRoutes(routerList)
// console.log(accessedRoutes);
this.addRoutes = accessedRoutes
// console.log("111",accessedRoutes);
this.routes = constantRoutes.concat(accessedRoutes)
resolve(accessedRoutes)
})
}
}
})

21
src/store/settings.ts Normal file
View File

@ -0,0 +1,21 @@
import {defineStore} from 'pinia'
import {getToken,setToken} from "@/utils/storage.ts";
export const useSettingsStore = defineStore('settings', {
state: () => {
return {
menuCollapse: false,//// 是否水平折叠收起菜单
// 布局方式 Classic 经典布局 Streamline 单行布局
layoutMode: getToken('layoutMode')?getToken('layoutMode'):'Classic'
}
},
actions: {
// @ts-ignore
changeSetting({ key, value }) {
//改变全局变量的方法
// @ts-ignore
this[key] = value
setToken(key, value)
},
}
})

119
src/store/tagsView.ts Normal file
View File

@ -0,0 +1,119 @@
import { defineStore } from 'pinia'
export const useTagsViewStore = defineStore('tagsView', {
state: () => {
return {
visitedViews: new Array<any>(),
cachedViews: ['Dashboard']//缓存的路由
}
},
actions: {
addView(view:any) {
this.addVisitedView(view)
this.addCachedView(view)
},
addVisitedView(view:any) {
if (this.visitedViews.some((v:any) => v.path === view.path)) return
this.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name'
})
)
},
addCachedView(view:any) {
if (this.cachedViews.includes(view.name)) return
this.cachedViews.push(view.name)
// console.vlog("cachedViews",this.cachedViews);
},
delView(view:any) {
return new Promise((resolve) => {
this.delVisitedView(view)
this.delCachedView(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delVisitedView(view:any) {
return new Promise((resolve) => {
for (const [i, v] of this.visitedViews.entries()) {
if (v.path === view.path) {
this.visitedViews.splice(i, 1)
break
}
}
resolve([...this.visitedViews])
})
},
delCachedView(view:any) {
return new Promise((resolve) => {
const index = this.cachedViews.indexOf(view.name)
index > -1 && this.cachedViews.splice(index, 1)
resolve([...this.cachedViews])
})
},
delOthersViews(view:any) {
return new Promise((resolve) => {
this.delOthersVisitedViews(view)
this.delOthersCachedViews(view)
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delOthersVisitedViews(view:any) {
return new Promise((resolve) => {
this.visitedViews = this.visitedViews.filter((v) => {
return v.meta.affix || v.path === view.path
})
resolve([...this.visitedViews])
})
},
delOthersCachedViews(view:any) {
return new Promise((resolve) => {
const index = this.cachedViews.indexOf(view.name)
if (index > -1) {
this.cachedViews = this.cachedViews.slice(index, index + 1)
} else {
// if index = -1, there is no cached tags
this.cachedViews = []
}
resolve([...this.cachedViews])
})
},
delAllViews() {
return new Promise((resolve) => {
this.delAllVisitedViews()
this.delAllCachedViews()
resolve({
visitedViews: [...this.visitedViews],
cachedViews: [...this.cachedViews]
})
})
},
delAllVisitedViews() {
return new Promise((resolve) => {
// keep affix tags
const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = affixTags
resolve([...this.visitedViews])
})
},
delAllCachedViews() {
return new Promise((resolve) => {
this.cachedViews = []
resolve([...this.cachedViews])
})
},
updateVisitedView(view:any) {
for (let v of this.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
}
}
})

88
src/store/user.ts Normal file
View File

@ -0,0 +1,88 @@
import {defineStore} from 'pinia'
import {getToken, setToken, removeToken} from '@/utils/storage.ts'
import {login,getIndex} from "@/api/user";
interface LoginData {
loginName: string
password: string
}
//1.创建store
// store实例相当于一个容器里面存放的有类似于data计算属性方法之类的东西。通过defineStore()方法定义
// 在src下面创建一个store文件夹再创建与之对应的js文件比如user.js
// 在pinia中你可以认为 state 是 store 的数据 (data)getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)相比vuexpinia没有mutations
export const useUserStore = defineStore('user', {
state: () => {
return {
token: getToken("Access-Token"), // 登录信息
// routerList: [], // 路由权限
init:false,//判断是否获取过路由
userInfo: null // 用户信息
}
},
actions: {
userLogin(loginData:LoginData) {
const data = {
// 'user': JSON.stringify(userInfo),
'loginName': loginData.loginName,
'password': loginData.password
}
return new Promise((resolve) => {
login(data).then((response) => {
const {data} = response
this.token = data.accessToken;
setToken("Access-Token",data.accessToken)
resolve(response)
}).catch((error) => {
resolve(error)
})
})
},
getInfo(){
return new Promise((resolve, reject) => {
getIndex().then((response:any) => {
const {data} = response
// console.log("222",response);
this.userInfo = response.user
resolve(data)
}).catch(error => {
console.log(error);
this.init = false
reject(error)
})
})
},
logout() {
return new Promise(() => {
this.token = ''
this.init = false
this.userInfo = null;
removeToken("Access-Token")
window.location.reload()
})
}
}
})
// import { defineStore } from 'pinia'
// import { getToken, setToken, removeToken } from '@/utils/storage'
// //1.创建store
// // store实例相当于一个容器里面存放的有类似于data计算属性方法之类的东西。通过defineStore()方法定义
// // 在src下面创建一个store文件夹再创建与之对应的js文件比如user.js
// console.log(defineStore);
// export const useUserStore = defineStore({
// id: 'user', // id必填且需要唯一
// state: () => {
// return {
// name: '以和为贵',
// age: 20
// }
// },
// actions: {
// updateName(name: string) {
// this.name = name
// },
// updateAge(age: number) {
// this.age = age
// }
// }
// })

14
src/utils/filters.ts Normal file
View File

@ -0,0 +1,14 @@
// 全局过滤
// 全局过滤器
export function getTime(time:any) {
if(time){
var str = time.substring(0, 6)
return str
}
}
export function getTime1(time:any) {
if(time){
var str = time.substring(0, 10)
return str
}
}

65
src/utils/getTable.ts Normal file
View File

@ -0,0 +1,65 @@
// 表格数据的一些公用方法
export function getMenu(menu:any) {
var menuData = menu;
menuData.forEach((ele:any) => {
let parentId = ele.parentId;
if (parentId === 0) {
//是根元素的hua ,不做任何操作,如果是正常的for-i循环,可以直接continue.
} else {
//如果ele是子元素的话 ,把ele扔到他的父亲的child数组中.
menuData.forEach((d:any) => {
if (d.id === parentId) {
let childArray = d.children;
if (!childArray) {
childArray = []
}
childArray.push(ele);
d.children = childArray;
}
})
}
});
//去除重复元素
menuData = menuData.filter((ele:any) => ele.parentId === 0);
// console.log('最终等到的tree结构数据: ', menuData);
return menuData
}
export function getMenuTree(menu:any) {
var menuData = menu;
menuData.forEach((ele:any) => {
let pId = ele.pId;
if (pId === 0) {
//是根元素的hua ,不做任何操作,如果是正常的for-i循环,可以直接continue.
} else {
//如果ele是子元素的话 ,把ele扔到他的父亲的child数组中.
menuData.forEach((d:any) => {
if (d.id === pId) {
let childArray = d.children;
if (!childArray) {
childArray = []
}
childArray.push(ele);
d.children = childArray;
}
})
}
});
//去除重复元素
menuData = menuData.filter((ele:any) => ele.pId === 0);
// console.log('最终等到的tree结构数据: ', menuData);
return menuData
}
//组合选中的权限菜单和id
export function getMenuIds(menu:any) {
var menuIds = [];
for (var i = 0; i < menu.length; i++) {
if(menu[i].checked == true){
menuIds.push(menu[i].id)
}
}
return menuIds
}

72
src/utils/request.ts Normal file
View File

@ -0,0 +1,72 @@
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/user'
import {getToken} from "@/utils/storage";
// console.log(import.meta.env.VITE_BASE_URL);
//创建实例 设置参数
const service = axios.create({
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},//设置请求头
transformRequest: [function (data) { //表示允许在向服务器发送前,修改请求数据 对 data 进行任意转换处理
if (typeof data === 'object') {
let ret = []
for (let it in data) {
ret.push(encodeURIComponent(it) + '=' + encodeURIComponent(data[it]))
}
return ret.join('&')
} else return data
}],
baseURL: import.meta.env.VITE_BASE_URL,// 域名配置
withCredentials: true, // 跨域请求时是否需要使用凭证
timeout: 30000 // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
config => {
const userStore = useUserStore()
if (userStore.token) {
// console.log(getToken());
config.headers['X-Token'] = getToken('Access-Token');
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.httpCode !== 200 && res.httpCode!=undefined) {
ElMessage({
message: res.msg || res.data,
type: 'error',
duration: 5 * 1000,
showClose: true,
})
if (res.httpCode === 403 || res.httpCode === 402 || res.httpCode === 50014) {
// to re-login
ElMessageBox.confirm('您已注销,可以取消以留在此页,或重新登录', '确认?', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
location.reload()
})
}
return Promise.reject(res)
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default service

16
src/utils/storage.ts Normal file
View File

@ -0,0 +1,16 @@
// @ts-ignore
import Cookies from 'js-cookie'
// const TokenKey = 'Access-Token'//后端存登陆信息的
export function getToken(TokenKey:string) {
return Cookies.get(TokenKey)
}
export function setToken(TokenKey:string,token:string) {
return Cookies.set(TokenKey, token)
}
export function removeToken(TokenKey:string) {
return Cookies.remove(TokenKey)
}

View File

@ -0,0 +1,115 @@
import { isRef, isReactive, toRaw } from 'vue'
function getObjType(data) {
var toString = Object.prototype.toString;
var dataType = toString
.call(data)
.replace(/\[object\s(.+)\]/, "$1")
.toLowerCase();
return dataType
};
function deepCopy(obj) {
if ( !obj ) return;
let newObj = obj.constructor === Array ? [] : {}
for (let i in obj) {
// 对象类型包括普通数组、reactive的proxy代理对象、ref包装对象、computed计算结果的ref包装对象
if (typeof obj[i] === 'object') {
// reactive的proxy代理对象
if ( isReactive(obj[i]) ) {
newObj[i] = deepCopy( toRaw(obj[i]) )
}
else if ( isRef(obj[i]) ) {
// ref包装对象包括ref()方法和 computed计算
let val = obj[i].value
if ( val && isReactive(val) ) {
// ref()方法包装对象其value是响应式
newObj[i] = deepCopy( toRaw(val) )
}
else if ( val && !isReactive(val) ) {
// computed 计算属性如果此处value不是响应式的说明取到了最底层的基础数据类型直接赋值
newObj[i] = val
}
}
else {
newObj[i] = deepCopy( toRaw(obj[i]) )
}
}
else {
// js基础数据类型
newObj[i] = obj[i]
}
}
return newObj
}
const vlog = function() {
// 获取参数的数组形式
let args = Array.prototype.slice.call(arguments)
// 除了 object 之外的数据类型此处的object 指json类型和vue3中的代理或者包装对象Proxy、ComputedRefImpl、RefImpl
let jsBasicTypes = ['number', 'string', 'boolean', 'undefined', 'symbol', 'date', 'regexp', 'null', 'function', 'undefined']
let basicTypes = ['array', ...jsBasicTypes]
// 保存从 object 里面获得的原始值
let arr = []
// 窗口节点类型。通过 getObjType获取的包括 window html.+ 两类
// 后者包括 HTMLCollection(如getElementsByTagName获取)、HTMLDocument或者HTML.+Element类型
let winTypes = ['window', 'html']
args.forEach(item => {
if ( basicTypes.includes(getObjType(item)) ) {
if ( getObjType(item) === 'array' ) {
// 虽然此处是指非object类型的数组其中可能含有object类型的元素
let newArr = deepCopy(item)
arr.push(newArr)
}
else {
// 除了 object和array以外其他类型
arr.push(item)
}
}
else {
// object 类型
let rawValue = ''
let isWinType = false
winTypes.forEach(type => {
if ( getObjType(item).includes(type) ) {
isWinType = true
}
})
if ( isWinType ) {
rawValue = item
}
else if ( isReactive(item) ) {
// 通过reactive 包装的,是 Proxy代理对象
rawValue = toRaw(item)
}
else if ( isRef(item) ) {
let val = item.value
if ( val && isReactive(val) ) {
// Ref包装对象其value是响应式
rawValue = toRaw(val)
}
else if ( val && !isReactive(val) ) {
// computed 计算属性其value不是响应式的
rawValue = val
}
}
else {
// 非vue3 中的、普通的 json类型
rawValue = item
}
// 打印非节点类型
if ( !isWinType && !jsBasicTypes.includes(getObjType(rawValue)) ) {
rawValue = deepCopy(rawValue)
}
arr.push( rawValue )
}
})
arr.length && console.log(...arr)
}
export default vlog

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
</script>
<template>
<el-card class="box-card">
<template #header>
<div class="card-header">
<span style="font-weight: bold;color: #303133">Yunzer-Admin 介绍</span>
</div>
</template>
<div>
<!-- <span style="font-weight: bold;color: #303133">Yunzer-Admin</span> 基于 Vue3.3TypeScriptVite3PiniaElement-Plus 专注于表格表单的企业级后台管理框架,取名-->
<!-- <span style="font-weight: bold;color: #303133">Yunzer</span>源于NBA球队圣安东尼奥马刺队San Antonio Yunzer作为一支专业篮球队马刺的卓越不止在于技术水平和战术运筹的精妙更在于他们小石匠精神一直激励着大家<br>-->
<!-- 马刺队更衣室里的一句话<br>-->
<!-- <p style="font-weight: bold;color: #303133">当一切看起来无可挽回之时我跑去看石匠重复捶击他面前的岩石一百次而那块石头连<br>一个裂缝都没有露出来接下来的第一百零一次捶击之时此石一分为二不是因为这<br>一次捶击而是因为你的始终如一<br></p>-->
<!-- <p>共勉......</p>-->
<!-- <el-link type="primary" href="https://gitee.com/3439/Yunzer-Admin" target="_blank">代码gitee地址</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="http://jdvip.suipin.net" target="_blank">在线预览</el-link>-->
<!-- <h4>系列文章</h4>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7286112965609357347" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(1)</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7286508785104322594" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(2)-页面布局</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7286679458131312674" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(3)-动态路由</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7287053284787028003" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(4)-导航标签栏和keep-alive缓存</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7288963909581635618" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(5)-二次封装表格1.0</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7290470513116856320" target="_blank">从零开始vue3+vite+ts+pinia+router4后台管理(6)-全局自定义指令实现节流与防抖</el-link>-->
<!-- <br>-->
<!-- <el-link type="primary" href="https://juejin.cn/post/7301903019222155264" target="_blank">什么才是完美的表格二次封装elementPlus表格-从零开始vue3+vite+ts+pinia+router4后台管理(7)</el-link>-->
<!-- <br>-->
</div>
</el-card>
</template>
<style scoped>
.box-card{
width: 950px;
}
:deep(.el-link__inner){
font-size: 18px;
line-height: 36px;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="simple-dialog">
<el-button @click="spursDialogRef.dialogVisible = true">打开dialog</el-button>
<el-button @click="spursDialogRef3.dialogVisible = true">打开第二个dialog</el-button>
<spurs-dialog
ref="spursDialogRef"
:title="title"
width="900"
@saveSubmit="saveSubmit"
>
<template #body>
<span>我是dialog111</span>
<br>
<el-button @click="spursDialogRef2.dialogVisible = true">打开内层 Dialog</el-button>
<spurs-dialog
ref="spursDialogRef2"
:title="title"
@saveSubmit="saveSubmitInner"
>
<template #body>
<span>我是内层 Dialog222222222</span>
</template>
</spurs-dialog>
</template>
<template #footer>
<el-button type="success" @click="spursDialogRef.dialogVisible = true">我是操作2</el-button>
</template>
</spurs-dialog>
<spurs-dialog
ref="spursDialogRef3"
:title="title"
:dialogType="dialogType"
@saveSubmit="saveSubmit"
>
<template #body>
<span>我是Dialog222222222</span>
</template>
</spurs-dialog>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
let dialogType = ref('readonlyDialog')
let title = ref('提示')
const spursDialogRef = ref <any> ()
const spursDialogRef2 = ref <any> ()
const spursDialogRef3 = ref <any> ()
const saveSubmit = () => {
console.log("点击了确定按钮")
}
const saveSubmitInner = () => {
console.log("点击了内层确定按钮")
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,16 @@
<template>
<div>
部门列表5564546
<el-input v-model="input" placeholder="Please input" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const input = ref('')
</script>
<style scoped>
</style>

View File

@ -0,0 +1,65 @@
<template>
<div class="app-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>函数的方式实现节流防抖(浏览器控制台看效果)</span>
</div>
</template>
<el-button @click="throttleBtn" type="primary">节流点击</el-button>
<br/>
<br/>
<el-input v-model="inputValue" @input="debounceInput" placeholder="输入防抖" clearable />
</el-card>
<el-card style="margin-top: 60px" class="box-card">
<template #header>
<div class="card-header">
<span>全局自定义指令实现节流防抖(浏览器控制台看效果)</span>
</div>
</template>
<el-button v-throttle @click="testThrottle" type="primary">节流点击</el-button>
<br/>
<br/>
<el-input v-model="inputValue2" v-debounce="{ time: 3000, func: () => testDebounceInput() }" placeholder="输入防抖" clearable />
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
const inputValue = ref("")
const inputValue2 = ref("")
const throttle:any = ref(null)//
const debounce:any = ref(null)//
const throttleBtn = () =>{
if(throttle.value){ //setTimeout()
return
}else{//
console.log("我两秒钟只能被执行一次")
throttle.value = setTimeout(()=>{
throttle.value = null //
},2000)
}
}
const debounceInput = () =>{
clearTimeout(debounce.value) //
debounce.value = setTimeout(()=>{ //2
console.log("每次调用我1秒后才能执行"+inputValue.value)
},1000)
//
//
}
function testThrottle() {
console.log('我两秒钟只能被执行一次')
}
function testDebounceInput(){
console.log("每次调用我3秒后才能执行"+inputValue2.value)
}
</script>
<style scoped>
.box-card{
width: 500px;
}
</style>

View File

@ -0,0 +1,65 @@
import {FormOption} from "@/components/YunzerForm/formType.ts";
export const formConfig: FormOption = {
formItems: [
{
field: 'id',
type: 'input',
label: '用户id',
placeholder: '请输入用户id',
colSpan:9,
prop:"id"
},
{
field: 'account',
type: 'input',
label: '用户名',
disabled:true,
placeholder: '请输入用户名'
},
{
field: 'realname',
type: 'input',
label: '真实姓名',
placeholder: '请输入真实姓名'
},
{
field: 'cellphone',
type: 'input',
label: '电话号码',
placeholder: '请输入电话号码'
},
{
field: 'enable',
type: 'select',
label: '用户状态',
placeholder: '请选择用户状态',
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
},
{
field: 'createAt',
type: 'datepicker',
label: '创建时间',
otherOptions: {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange'
}
},
{
field: 'special',
slotName:'special',
label: '自定义内容',
colSpan:12
},
{
field: 'special2',
slotName:'special2',
label: '自定义内容2',
colSpan:12
},
],
labelWidth: '120px'//标签的长度
}

View File

@ -0,0 +1,87 @@
<template>
<div class="role-form">
<spurs-form
ref="spursFormRef"
v-bind="formConfig"
:modelValue="modelValue"
:rules="rules"
>
<template #header>
<div class="header">
<h1>我是头部</h1>
</div>
</template>
<template #special>
<div class="special">
我是自定义内容
</div>
</template>
<template #special2>
<div class="special2">
我是自定义内容2
</div>
</template>
<template #footer>
<el-button type="primary" @click="submitForm(spursFormRef.ruleFormRef)"> </el-button>
<el-button @click="resetClick"> </el-button>
</template>
</spurs-form>
</div>
</template>
<script setup lang="ts">
import YunzerForm from '@/components/YunzerForm/index.vue'
import {onMounted, reactive, ref} from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import {formConfig} from './formConfig'
const modelValue = reactive({
id: '12',
account: '用户名1',
realname: '张三',
})
// console.log(formConfig.formItems[4].options);
onMounted(() => {
// formConfig.formItems[4].options = [];
// console.log("222",formConfig);
})
const spursFormRef = ref <any> ()
const rules = reactive<FormRules>({
id: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
createAt: [
{ required: true, message: '请选择日期', trigger: 'blur' }
]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!')
} else {
console.log('error submit!', fields)
}
})
}
//
const resetClick = () => {
formConfig.formItems[4].options = [];
console.log(formConfig);
console.log(modelValue);
}
</script>
<style scoped>
.role-form{
width: 800px;
}
.header {
padding-top: 20px;
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>menu8888888888888888888888888888888</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>menu111111</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>menu8888888888888888888888888888888</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>654456546654654</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<template>
<div></div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const { params, query } = route
const { path } = params
const router = useRouter()
router.replace({ path: '/' + path, query })
</script>

View File

@ -0,0 +1,16 @@
<template>
<div>
部门列表5564546
<el-input v-model="input" placeholder="Please input" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const input = ref('')
</script>
<style scoped>
</style>

View File

@ -0,0 +1,16 @@
<template>
<div>
菜单列表333
<el-input v-model="input" placeholder="Please input" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const input = ref('')
</script>
<style scoped>
</style>

View File

@ -0,0 +1,16 @@
<template>
<div>
角色列表
<el-input v-model="input" placeholder="Please input" />
</div>
</template>
<script setup lang="ts" name="Role">
import { ref } from 'vue'
const input = ref('')
</script>
<style scoped>
</style>

View File

@ -0,0 +1,16 @@
<script setup lang="ts" name="User">
import { ref } from 'vue'
const input1 = ref('')
</script>
<template>
<div>
用户列表
<el-input v-model="input1" placeholder="Please input" />
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,118 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input class="w-100" v-model="queryForm.keyword" placeholder="关键字搜索" />
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button class="green-button" :icon="Plus" @click="handleSearch">增加</el-button>
<el-button class="green-button" :icon="Plus" @click="refreshTableInfo">刷新</el-button>
</div>
<div class="table-con">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
@row-contextmenu="handleRowContextmenu"
border
>
<el-table-column prop="nickName" label="姓名"/>
<el-table-column prop="roleName" label="权限名称"/>
<el-table-column prop="title" label="介绍"/>
<el-table-column prop="phone" label="联系方式"/>
<el-table-column prop="address" label="地址"/>
<el-table-column prop="createTime" label="日期"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default>
<el-button link type="primary" size="small">修改</el-button>
<el-button link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<Pagination
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
<div>
<context-menu
v-model:show="show"
:options="optionsComponent"
>
<context-menu-item label="操作1" @click="onMenuClick(1)" />
<context-menu-group label="操作2">
<context-menu-item label="Item1" @click="onMenuClick(2)" />
<context-menu-item label="Item2" @click="onMenuClick(3)" />
<context-menu-group label="Child with v-for 8">
<context-menu-item v-for="index of 6" :key="index" :label="'Item3-'+index" @click="onLoopMenuClick(index)" />
</context-menu-group>
</context-menu-group>
<context-menu-item label="刷新" @click="onMenuClick(1)">
<template #icon>
<Refresh style="width: 1em; height: 1em; margin-right: 8px" />
</template>
</context-menu-item>
<context-menu-item label="删除" @click="onMenuClick(1)">
<template #icon>
<Delete style="width: 1em; height: 1em; margin-right: 8px" />
</template>
</context-menu-item>
</context-menu>
</div>
</div>
</template>
<script setup lang="ts">
import {reactive, ref} from 'vue'
import { Search,Plus } from '@element-plus/icons-vue'
import {useTable} from '@/hooks/useTable'
import tableApi from '@/api/table'
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
//
import { ContextMenu, ContextMenuGroup, ContextMenuItem } from '@imengyu/vue3-context-menu';
import type { MenuOptions } from '@imengyu/vue3-context-menu';
const queryForm = reactive({
keyword: ''
})
const {
tableData,
pagination,
total,
loading,
getTableData,
handleSearch,//
refreshTableInfo,//
} = useTable(tableApi.packTableList,queryForm)
let show = ref(false)
const optionsComponent:MenuOptions = reactive({
zIndex: 3,
minWidth: 230,
x: 500,
y: 200
})
const handleRowContextmenu = (row:any, column:any, event:any)=>{
console.log(row);
console.log(column);
console.log(event);
event.preventDefault()
show.value = true;
optionsComponent.x = event.clientX;
optionsComponent.y = event.clientY;
}
const onMenuClick = (num:number)=>{
}
const onLoopMenuClick = (index:number)=>{
}
</script>
<style scoped>
.w-100{
width: 200px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input class="w-100" v-model="queryForm.keyword" placeholder="关键字搜索" />
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button class="green-button" :icon="Plus" @click="handleSearch">增加</el-button>
<el-button class="green-button" :icon="Plus" @click="getDragableData">查看排序后的数据</el-button>
</div>
<div class="table-con">
<el-table
v-dragable="dragOptions"
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
border
>
<el-table-column prop="nickName" label="姓名"/>
<el-table-column label="排序" align="center" width="55">
<template #default>
<DCaret style="width: 1em; height: 1em; margin-right: 8px;cursor: all-scroll;margin-top: 5px"/>
</template>
</el-table-column>
<el-table-column prop="roleName" label="权限名称"/>
<el-table-column prop="title" label="介绍"/>
<el-table-column prop="phone" label="联系方式"/>
<el-table-column prop="address" label="地址"/>
<el-table-column prop="createTime" label="日期"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default>
<el-button link type="primary" size="small">修改</el-button>
<el-button link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<Pagination
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
</div>
</template>
<script setup lang="ts">
import {reactive} from 'vue'
import { Search,Plus } from '@element-plus/icons-vue'
import {useTable} from '@/hooks/useTable'
import tableApi from '@/api/table'
import { vDragable } from "element-plus-table-dragable";
const queryForm = reactive({
keyword: ''
})
const {
tableData,
pagination,
total,
loading,
getTableData,
handleSearch,//
} = useTable(tableApi.packTableList,queryForm)
const dragOptions = [
{
selector: "tbody", // css
option: {
animation: 150,//number ms
onEnd: (evt:any) => {//function
console.log(evt);
const itemEl = evt.item; // dragged HTMLElement
console.log(itemEl);
console.log("222",evt.oldIndex, evt.newIndex);
const copyRow = JSON.parse(JSON.stringify(tableData.value[evt.oldIndex]));//
tableData.value.splice(evt.oldIndex, 1);//
tableData.value.splice(parseInt(evt.newIndex), 0, copyRow);//
},
},
},
]
const getDragableData = () =>{
console.log(tableData.value);
}
</script>
<style scoped>
.w-100{
width: 200px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input class="w-100" v-model="queryForm.keyword" placeholder="关键字搜索"/>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button class="green-button" :icon="Plus" @click="handleSearch">增加</el-button>
<el-button class="green-button" :icon="Plus" @click="getDragableData">查看排序后的数据</el-button>
</div>
<div class="table-con">
<el-table
@cell-dblclick="doubleClickCell"
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
@current-change="close"
border
>
<el-table-column prop="nickName" label="姓名">
<template #default="{row}">
<el-input v-if="row.edit==1" v-model="row.nickName"></el-input>
<span v-else>{{ row.nickName }}</span>
</template>
</el-table-column>
<el-table-column prop="roleName" label="权限名称"/>
<el-table-column prop="title" label="介绍"/>
<el-table-column prop="phone" label="联系方式"/>
<el-table-column prop="address" label="地址"/>
<el-table-column prop="createTime" label="日期"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default>
<el-button link type="primary" size="small">修改</el-button>
<el-button link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<Pagination
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
</div>
</template>
<script setup lang="ts">
import {reactive, onUpdated} from 'vue'
import {Search, Plus} from '@element-plus/icons-vue'
import {useTable} from '@/hooks/useTable'
import tableApi from '@/api/table'
const queryForm = reactive({
keyword: ''
})
const {
tableData,
pagination,
total,
loading,
getTableData,
handleSearch,//
refreshTableInfo,//
} = useTable(tableApi.packTableList, queryForm)
/** 添加上一个状态值控制是否可编辑 */
onUpdated(() => {
for (let i = 0; i < tableData.value.length; i++) {
tableData.value[i].edit = 0
}
})
const close = (currentRow:any, oldCurrentRow:any) => {
console.log(currentRow);
console.log(oldCurrentRow);
if(oldCurrentRow!==null){
oldCurrentRow.edit=0
}
}
console.log("12121", tableData.value);
const doubleClickCell = (row: any) => {
console.log("111", tableData.value);
row.edit=1
}
const getDragableData = () =>{
console.log(tableData.value);
}
</script>
<style scoped>
.w-100 {
width: 200px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input class="w-100" v-model="queryForm.keyword" placeholder="关键字搜索" />
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button class="green-button" :icon="Plus" @click="handleSearch">增加</el-button>
<el-button type="success" :icon="Refresh" @click="refreshTableInfo">刷新</el-button>
</div>
<div class="table-con">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%;"
:max-height="tableHeight"
row-key="id"
border
>
<el-table-column prop="nickName" label="姓名"/>
<el-table-column prop="roleName" label="权限名称"/>
<el-table-column prop="title" label="介绍"/>
<el-table-column prop="phone" label="联系方式"/>
<el-table-column prop="address" label="地址"/>
<el-table-column prop="createTime" label="日期"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default>
<el-button link type="primary" size="small">修改</el-button>
<el-button link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<Pagination
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
</div>
</template>
<script setup lang="ts">
import {reactive} from 'vue'
import { Search,Plus,Refresh } from '@element-plus/icons-vue'
import {useTable} from '@/hooks/useTable'
import tableApi from '@/api/table'
const queryForm = reactive({
keyword: ''
})
const {
tableData,
pagination,
total,
loading,
getTableData,
tableHeight,
handleSearch,//
refreshTableInfo,//
} = useTable(tableApi.packTableList,queryForm)
</script>
<style scoped>
.w-100{
width: 200px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div class="app-container">
<div class="filter-container">
<el-input class="w-100" v-model="queryForm.keyword" placeholder="关键字搜索" />
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button class="green-button" @click="refreshTableInfo" :icon="Plus">刷新</el-button>
{{$filters.getTime('2023-11-23 00:00:00.000')}}
</div>
<div v-dragable="dragOptions" class="table-con">
<spurs-table
ref="spursTableRef"
v-bind="tableConfig"
:queryForm="queryForm"
:requestApi="getTableList"
@selection-change="handleSelectionChange"
>
<template #state="scope">
<el-tag type="warning" v-if="scope.row.state === 0">禁用</el-tag>
<el-tag v-else>启用</el-tag>
</template>
<template #handler="scope">
<el-button
size="small"
:icon="Edit"
link
@click="handleEditClick(scope.row)"
>编辑</el-button
>
<el-button
size="small"
:icon="Delete"
type="warning"
link
@click="deleteBtnClick(scope.row)"
>删除</el-button>
</template>
</spurs-table>
</div>
</div>
</template>
<script setup lang="ts">
import {reactive,ref} from 'vue'
import { Search,Plus,Delete,Edit } from '@element-plus/icons-vue'
import { tableConfig } from './tableConfig.ts'//
import YunzerTable from '@/components/YunzerTable/index.vue'
import tableApi from '@/api/table'
import { vDragable } from "element-plus-table-dragable";
const queryForm = reactive({
keyword: ''
})
const getTableList = (params: any) => {
return tableApi.packTableList(params);
};
const spursTableRef = ref <any> ()
const multipleSelection = ref<any>([])
const handleSelectionChange = (val:[]) => {
multipleSelection.value = val
console.log(multipleSelection.value);
}
const handleSearch = () => {
//
spursTableRef.value.handleSearch();
console.log(spursTableRef.value.tableData);
// spursTable.value.tableData[0].nickName = ""
}
const refreshTableInfo = () => {
// queryForm
queryForm.keyword="";
spursTableRef.value.refreshTableInfo();
}
const dragOptions = [
{
selector: "tbody", // css
option: {
animation: 150,//number ms
onEnd: (evt:any) => {//function
console.log(evt);
const itemEl = evt.item; // dragged HTMLElement
console.log(itemEl);
console.log("222",evt.oldIndex, evt.newIndex);
// const copyRow = JSON.parse(JSON.stringify(spursTableRef.value.tableData[evt.oldIndex]));//
// spursTableRef.value.tableData.splice(evt.oldIndex, 1);//
// spursTableRef.value.tableData.splice(parseInt(evt.newIndex), 0, copyRow);//
},
},
},
]
const handleEditClick = (row: any) => {
console.log('点击了编辑按钮,数据为:', row)
// getTableData();
}
//
const deleteBtnClick = (row: any) => {
console.log('点击了删除按钮,数据为:', row)
}
</script>
<style scoped>
.w-100{
width: 200px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,49 @@
//表格配置json
import {TableOption} from "@/components/YunzerTable/tableType.ts";
//局部过滤器
const getState = (state:any) =>{
let txt = "";
switch (state) {
case 0:
txt = "女";
break;
case 1:
txt = "男";
break;
}
return txt;
}
export const tableConfig: TableOption = {
// 表格配置
propList: [
{ prop: 'nickName', label: '姓名', minWidth: '100', align: 'left' },
{ prop: 'state', label: '性别', minWidth: '100', align: 'left',filter:getState},
{ prop: 'roleName', label: '权限名称', minWidth: '100', align: 'left' },
{ prop: 'userMoney.balanceMoney', label: '用户余额', minWidth: '100', align: 'left' },//获取表格list下一级的数据userMoney.balanceMoney
{ prop: 'title', label: '介绍', minWidth: '300', align: 'left' },
{ prop: 'phone', label: '联系方式', minWidth: '100', align: 'left' },
{ prop: 'address', label: '地址', minWidth: '100', align: 'left' },
{ prop: 'createTime', label: '日期', minWidth: '180', align: 'left'},
{
prop: 'state',
label: '状态',
minWidth: '100',
slotName: 'state',
align: 'left'
},
{
label: '操作',
minWidth: '120',
slotName: 'handler',
align: 'left'
}
],
// 表格具有序号列
showIndexColumn: true,
// 表格具有可选列
showSelectColumn: true,
//是否显示分页
showPagination:true
}

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>404页面找不到</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="login-wrap" :style="{background: 'url(' + Bg2 + ') no-repeat 30%' }">
<div class="login-form">
<div class="login-tit">Yunzer-Admin</div>
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="0px"
class="demo-ruleForm"
size="large"
>
<el-form-item prop="loginName">
<el-input size="large" :prefix-icon="User" placeholder="账号:admin" v-model="ruleForm.loginName" />
</el-form-item>
<el-form-item prop="password">
<el-input type="password" size="large" placeholder="密码:123456" @keyup.enter.native="submitForm(ruleFormRef)" :prefix-icon="Lock" v-model="ruleForm.password" />
</el-form-item>
<el-form-item>
<el-button :loading="state.loading" style="width: 100%" type="primary" @click="submitForm(ruleFormRef)">
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive,ref} from 'vue'
import { Lock, User } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import Bg2 from '@/assets/background.svg'
import { useUserStore } from '@/store/user'
import { useRouter } from 'vue-router'
const router = useRouter()
// reactive
// reactive
// ref .value
//
// 使 reactive 使 ref
const ruleFormRef = ref<FormInstance>()
const state = reactive({
loading:false
})
const ruleForm = ref({
loginName: '',
password: '',
})
const rules = reactive<FormRules>({
loginName: [
{required: true, message: '请输入登录账号', trigger: 'blur'}
],
password: [
{required: true, message: '密码不能为空', trigger: 'blur'}
],
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
state.loading = true;
const userStore = useUserStore()
const result:any = await userStore.userLogin(ruleForm.value)
if(result.httpCode==200){
router.push('/dashboard')
}
state.loading = false;
}else {
state.loading = false;
}
})
}
</script>
<style scoped>
.login-wrap {
width: 100vw;
height: 100vh;
position: relative;
padding-top: 230px;
}
.login-form{
width: 480px;
padding: 40px 35px 0;
margin: 0 auto;
overflow: hidden;
box-sizing: border-box;
}
.login-tit{
width: 100%;
text-align: center;
font-weight: 700;
font-size: 26px;
margin-bottom: 15px;
}
</style>

11
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/// <reference types="vite/client" />
/// <reference types="vite/client" />
declare module "js-mark";
// 解决找不到模块“*.vue”或其相应的类型声明。
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES2020",// ECMAScript
"useDefineForClassFields": true,// TypeScript
"module": "ESNext",//
"removeComments": true, //
"outDir": "./dist", //
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": false,
"baseUrl": "./", //
"paths": {"@/*": ["src/*"]}, // baseUrl
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* tsx */
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

34
vite.config.ts Normal file
View File

@ -0,0 +1,34 @@
// https://vitejs.dev/config/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from "path";//这个path用到了上面安装的@types/node
import vueJsx from '@vitejs/plugin-vue-jsx'//tsx语法
import vitePluginVueSetupExtend from 'vite-plugin-vue-setup-extend'//可以给语法糖组件直接命名比如<script lang="ts" setup name="A"></script>
// https://vitejs.dev/config/
export default ({ mode }) => {
return defineConfig({
plugins: [vue(),vitePluginVueSetupExtend(),vueJsx()],
//这里进行配置别名
resolve: {
alias: {
'@': path.resolve('./src') // @代替src
}
},
// @ts-ignore
lintOnSave: false,
server: {
port: 9527, // 设置服务启动端口号
open: true, // 设置服务启动时是否自动打开浏览器
host: '127.0.0.1',//你自己本地的ip
proxy: {
'/apis': {
target: process.env.VITE_BASE_URL,
secure: false, // 如果是https接口需要配置这个参数
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
rewrite: (path) => path.replace(/^\/apis/, '/')
}
}
}
})
}