Commit 8501b02b authored by superDragon's avatar superDragon

init:初始化项目🎉🎉

parents
{
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
}
\ No newline at end of file
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
tsx: true
}
},
env: {
browser: true,
node: true
},
plugins: [
'@typescript-eslint'
],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended'
],
rules: {
'vue/html-indent': ['error', 4],
indent: ['error', 4], // 4行缩进
'vue/script-indent': ['error', 4],
quotes: ['error', 'single'], // 单引号
'vue/html-quotes': ['error', 'single'],
semi: ['error', 'never'], // 禁止使用分号
'space-infix-ops': ['error', { int32Hint: false }], // 要求操作符周围有空格
'no-multi-spaces': 'error', // 禁止多个空格
'no-whitespace-before-property': 'error', // 禁止在属性前使用空格
'space-before-blocks': 'error', // 在块之前强制保持一致的间距
'space-before-function-paren': ['error', 'never'], // 在“ function”定义打开括号之前强制不加空格
'space-in-parens': ['error', 'never'], // 强制括号左右的不加空格
'space-infix-ops': 'error', // 运算符之间留有间距
'spaced-comment': ['error', 'always'], // 注释间隔
'template-tag-spacing': ['error', 'always'], // 在模板标签及其文字之间需要空格
'no-var': 'error',
'prefer-destructuring': ['error', { // 优先使用数组和对象解构
array: true,
object: true
}, {
enforceForRenamedProperties: false
}],
'comma-dangle': ['error', 'never'], // 最后一个属性不允许有逗号
'arrow-spacing': 'error', // 箭头函数空格
'prefer-template': 'error',
'template-curly-spacing': 'error',
'quote-props': ['error', 'as-needed'], // 对象字面量属性名称使用引号
'object-curly-spacing': ['error', 'always'], // 强制在花括号中使用一致的空格
'no-unneeded-ternary': 'error', // 禁止可以表达为更简单结构的三元操作符
'no-restricted-syntax': ['error', 'WithStatement', 'BinaryExpression[operator="in"]'], // 禁止with/in语句
'no-lonely-if': 'error', // 禁止 if 语句作为唯一语句出现在 else 语句块中
'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], // 要求方法链中每个调用都有一个换行符
// 路径别名设置
'no-submodule-imports': ['off', '/@'],
'no-implicit-dependencies': ['off', ['/@']],
'@typescript-eslint/no-explicit-any': 'off' // 类型可以使用any
}
}
\ No newline at end of file
node_modules
.DS_Store
*.local
dist
\ No newline at end of file
module.exports = {
processors: [],
plugins: [],
extends: "stylelint-config-standard", // 这是官方推荐的方式
ignoreFiles: ["node_modules/**", "dist/**"],
rules: {
"at-rule-no-unknown": [ true, {
"ignoreAtRules": [
"responsive",
"tailwind"
]
}],
"indentation": 4, // 4个空格
"selector-pseudo-element-no-unknown": [true, {
"ignorePseudoElements": ["v-deep"]
}]
}
}
\ No newline at end of file
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "edge",
"request": "launch",
"name": "element-plus-admin",
"url": "http://localhost:3002",
"webRoot": "${workspaceFolder}"
}
]
}
\ No newline at end of file
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.tslint": true,
"source.fixAll.stylelint": true
},
"editor.tabSize": 4,
"vetur.validation.template": false,
"vetur.experimental.templateInterpolationService": true,
"vetur.validation.interpolation": false,
"scss.lint.unknownAtRules": "ignore",
"css.validate": true,
"scss.validate": true,
"files.associations": {
"*.css": "scss"
},
"volar.tsPlugin": true,
"volar.tsPluginStatus": false
}
\ No newline at end of file
{
"console.log": {
"scope": "javascript,typescript,vue",
"prefix": "con",
"body": [
"console.log($1)"
],
"description": "Log output to console"
},
"style postcss scoped": {
"scope": "vue",
"prefix": "style",
"body": [
"<style lang='postcss' scoped>",
" $1",
"</style>"
],
"description": ""
},
"vue新建页面": {
"scope": "vue",
"prefix": "page",
"body": [
"<template>",
" <div>",
" $2",
" </div>",
"</template>",
"<script lang='ts'>",
"import { defineComponent } from 'vue'",
"",
"export default defineComponent({",
" name: '$1',",
" setup() {",
" $3",
" return {",
" $4",
" }",
" },",
"})",
"</script>"
],
"description": ""
}
}
\ No newline at end of file
MIT License
Copyright (c) 2021 李祥
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Vue3 ElementPlus Admin">
<meta name="description" content="基于vite和element-plus和typescript的管理系统">
<meta name="keywords" content="vite,vuejs3,elementPlus,typescript,vue-next">
<meta name="baidu-site-verification" content="code-jvGVv9Wfi7" />
<title>Vue3 ElementPlus Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?6f30ec463f12087163460a93581d2f3d";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>
module.exports = {
moduleNameMapper: {
'/@/(.*)$': '<rootDir>/src/$1'
},
// 转义
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\js$': 'babel-jest',
'^.+\\.(t|j)sx?$': 'ts-jest'
},
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node']
}
\ No newline at end of file
This diff is collapsed.
{
"author": "hsianglee",
"name": "element-plus-admin",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vuedx-typecheck . && vite build",
"preview": "vite preview",
"test": "jest ./test",
"eslint": "npx eslint . --ext .js,.jsx,.ts,.tsx --fix",
"stylelint": "npx stylelint **/*.{css,vue} --fix"
},
"dependencies": {
"echarts": "^5.0.2",
"element-plus": "^1.0.2-beta.35",
"vue": "^3.0.7"
},
"license": "MIT",
"repository": "https://github.com/hsiangleev/element-plus-admin",
"devDependencies": {
"@babel/preset-env": "^7.12.11",
"@testing-library/jest-dom": "^5.11.8",
"@types/jest": "^26.0.20",
"@types/mockjs": "^1.0.3",
"@types/node": "^14.14.20",
"@types/nprogress": "^0.2.0",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"@vitejs/plugin-vue": "^1.1.4",
"@vue/compiler-sfc": "^3.0.5",
"@vue/test-utils": "^2.0.0-beta.13",
"@vuedx/typecheck": "^0.6.0",
"@vuedx/typescript-plugin-vue": "^0.6.0",
"autoprefixer": "^10.1.0",
"axios": "^0.21.1",
"babel-jest": "^26.6.3",
"eslint": "^7.15.0",
"eslint-plugin-vue": "^7.2.0",
"jest": "^26.6.3",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"postcss": "^8.2.2",
"postcss-import": "^14.0.0",
"postcss-nested": "^5.0.3",
"postcss-simple-vars": "^6.0.2",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",
"tailwindcss": "^2.0.2",
"ts-jest": "^26.4.4",
"typescript": "^4.1.3",
"vite": "^2.1.2",
"vue-jest": "^5.0.0-alpha.7",
"vue-router": "^4.0.0-rc.6",
"vuex": "^4.0.0-rc.2"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"eslintIgnore": [
"node_modules",
"dist"
]
}
<p align="center">
<img width="320" src="https://images.hsianglee.cn/elementPlusAdmin/title.png">
</p>
<p align="center">
<a href="https://github.com/vuejs/vue-next">
<img src="https://img.shields.io/badge/vue3-3.0.7-brightgreen.svg" alt="vue">
</a>
<a href="https://github.com/element-plus/element-plus">
<img src="https://img.shields.io/badge/elementPlus-1.0.2beta.35-brightgreen.svg" alt="element-plus">
</a>
<a href="https://github.com/vitejs/vite">
<img src="https://img.shields.io/badge/vite-2.1.2-brightgreen.svg" alt="vite">
</a>
<a href="https://github.com/microsoft/TypeScript">
<img src="https://img.shields.io/badge/typescript-4.1.3-brightgreen.svg" alt="typescript">
</a>
<a href="https://github.com/postcss/postcss">
<img src="https://img.shields.io/badge/postcss-8.2.2-brightgreen.svg" alt="postcss">
</a>
<a href="https://github.com/hsiangleev/element-plus-admin/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
</a>
</p>
## 简介
[element-plus-admin](https://github.com/hsiangleev/element-plus-admin) 是一个后台前端解决方案,它基于 [vue-next](https://github.com/vuejs/vue-next)[element-plus](https://github.com/element-plus/element-plus)实现。它使用了最新的前端技术栈vite,typescript和postcss构建,内置了 动态路由,权限验证,皮肤更换,提供了丰富的功能组件,它可以帮助你快速搭建中后台产品原型。
- [在线预览](https://element-plus-admin.hsianglee.cn/)
- [Gitee](https://gitee.com/hsiangleev/element-plus-admin)
## 前序准备
你需要在本地安装 [node](http://nodejs.org/)[git](https://git-scm.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)[vue-next](https://github.com/vuejs/vue-next)[typescript](https://github.com/microsoft/TypeScript)[vite](https://github.com/vitejs/vite)[postcss](https://github.com/postcss/postcss)[element-plus](https://github.com/element-plus/element-plus),所有的请求数据都使用[Mock.js](https://github.com/nuysoft/Mock)进行模拟,提前了解和学习这些知识会对使用本项目有很大的帮助。
<p align="center">
<img width="900" src="https://images.hsianglee.cn/elementPlusAdmin/element-plus-admin.png">
</p>
## 开发
```bash
# 克隆项目
git clone https://github.com/hsiangleev/element-plus-admin.git
# 进入项目目录
cd element-plus-admin
# 安装依赖
npm install
# 启动服务
npm run dev
```
浏览器访问 http://localhost:3002
## 发布
```bash
# 发布
npm run build
# 预览
npm run preview
```
## 其它
```bash
# eslint代码校验
npm run eslint
# stylelint代码校验
npm run stylelint
```
## vscode扩展
1. 使用johnsoncodehk.volar,并禁用vetur,支持template代码里面的数据类型提示
## 浏览器
**目前仅支持现代浏览器**
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| Edge | last 2 versions | last 2 versions | last 2 versions |
## 捐赠
如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 :tropical_drink:</br>
![donate](https://images.hsianglee.cn/pay.png?v=0.0.1)
## License
[MIT](https://github.com/hsiangleev/element-plus-admin/blob/master/LICENSE)
Copyright (c) 2020-present hsiangleev
<template>
<router-view />
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
export default defineComponent ({
name: 'App'
})
</script>
import request from '/@/utils/request'
import { AxiosResponse } from 'axios'
const api = {
getTableList: '/getTableList'
}
export type ITag = '所有' | '家' | '公司' | '学校' | '超市'
export interface ITableList {
page: number
size: number
tag: ITag
}
export function getTableList(tableList: ITableList): Promise<AxiosResponse<IResponse>> {
return request({
url: api.getTableList,
method: 'get',
data: tableList
})
}
\ No newline at end of file
import request from '/@/utils/request'
import { AxiosResponse } from 'axios'
import { store } from '/@/store/index'
import { IMenubarList } from '/@/type/store/layout'
const api = {
login: '/login',
getUser: '/getUser',
getRouterList: '/getRoute'
}
export interface loginParam {
username: string,
password: string
}
export function login(param: loginParam):Promise<AxiosResponse<IResponse<string>>> {
return request({
url: api.login,
method: 'post',
data: param
})
}
interface IGetuserRes {
name: string
role: Array<string>
}
export function getUser(): Promise<AxiosResponse<IResponse<IGetuserRes>>> {
return request({
url: api.getUser,
method: 'get',
data: { token: store.state.layout.token.ACCESS_TOKEN }
})
}
export function getRouterList(): Promise<AxiosResponse<IResponse<Array<IMenubarList>>>> {
return request({
url: api.getRouterList,
method: 'get',
data: { token: store.state.layout.token.ACCESS_TOKEN }
})
}
\ No newline at end of file
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
overflow: hidden;
}
button:focus {
outline: none;
}
@responsive {
.transition-width {
transition-property: width;
}
.flex-center {
justify-content: center;
align-items: center;
}
.min-height-10 {
min-height: 2.5rem;
}
.min-width-32 {
min-width: 8rem;
}
}
<template>
<div class='card-list mb-2'>
<el-card
class='box-card'
shadow='hover'
>
<template
v-if='showHeader'
#header
>
<div class='card-list-header flex justify-between items-center'>
<span>{{ title }}</span>
<slot name='btn' />
</div>
</template>
<ul
v-if='type === "default"'
class='card-list-body flex flex-col text-sm'
>
<li
v-for='(v,i) in listItem'
:key='i'
>
<div :class='{"card-list-text": true,"nowrap": isNowrap, "wrap": !isNowrap}'>
<span
v-if='showListstyle'
class='card-list-item-circle'
/>
<a
v-if='v.url'
:href='v.url'
:target='v.target || "_self"'
:class='{"nowrap": isNowrap, "wrap": !isNowrap}'
>{{ v.text }}</a>
<template v-else>
{{ v.text }}
</template>
</div>
<div
v-if='v.mark'
class='card-list-mark'
>
{{ v.mark }}
</div>
</li>
</ul>
<slot
v-if='type === "keyvalue"'
name='keyvalue'
/>
</el-card>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
interface IListItem {
url?: string
target?: string
mark: string
text: string
}
type IType = 'default' | 'keyvalue'
export default defineComponent({
name: 'CardList',
props: {
type: {
type: String as PropType<IType>,
default: 'default'
},
listItem: {
type: Array as PropType<Array<IListItem>>,
default: () => []
},
title: {
type: String,
default: '标题'
},
showHeader: {
type: Boolean,
default: false
},
// 是否不换行
isNowrap: {
type: Boolean,
default: true
},
showListstyle: {
type: Boolean,
default: true
}
},
setup() {
return {}
}
})
</script>
<style lang="postcss" scoped>
::v-deep(.el-card__header) {
padding: 7px 15px;
}
::v-deep(.el-button) {
padding: 4px 6px;
border-radius: 3px;
}
.card-list-header {
height: 28px;
line-height: 28px;
}
.card-list-body {
list-style: square;
& > li {
list-style: square;
display: flex;
padding: 3px 0;
align-items: center;
justify-content: space-between;
& > .card-list-mark {
color: #888;
}
& > .card-list-text {
padding-right: 10px;
color: #666;
& > a:hover {
color: #409eff;
}
& > span.card-list-item-circle {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #666;
margin-right: 10px;
}
&.nowrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
& > span.card-list-item-circle {
transform: translateY(-50%);
}
}
&.wrap {
display: flex;
align-items: center;
& > span.card-list-item-circle {
min-width: 5px;
min-height: 5px;
max-width: 5px;
max-height: 5px;
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<el-col
:xs='24'
:sm='12'
:xl='8'
>
<div class='card-list-item flex mb-3 text-sm'>
<div
class='text-right pr-3'
:style='{"lineHeight": "28px","width": width, "minWidth": width}'
>
<span
v-if='isRequire'
class='text-red-600 select-none'
>*</span>
<slot name='key' />
<span>:</span>
</div>
<div
class='flex-1 font-semibold'
:class='{"truncate": !prop}'
:style='{"lineHeight": !prop ? "28px" : "inherit"}'
>
<el-form-item
v-if='prop'
:prop='prop'
>
<slot name='value' />
</el-form-item>
<slot
v-else
name='value'
/>
</div>
</div>
</el-col>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'CardListItem',
props: {
width: {
type: String,
default: '80px'
},
isRequire: {
type: Boolean,
default: false
},
prop: {
type: String,
default: ''
}
},
setup() {
return {}
}
})
</script>
<style lang='postcss' scoped>
::v-deep(.el-select),
::v-deep(.el-date-editor.el-input),
::v-deep(.el-date-editor.el-input__inner) {
width: 100%;
}
::v-deep(.el-form-item--mini.el-form-item),
::v-deep(.el-form-item--small.el-form-item) {
margin-bottom: 0;
}
</style>
\ No newline at end of file
import * as echarts from 'echarts/core'
import {
BarChart,
// 系列类型的定义后缀都为 SeriesOption
BarSeriesOption,
LineChart,
RadarChart,
PieChart,
PieSeriesOption,
RadarSeriesOption,
LineSeriesOption
} from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
RadarComponent,
GridComponent,
// 组件类型的定义后缀都为 ComponentOption
TitleComponentOption,
GridComponentOption
} from 'echarts/components'
import {
CanvasRenderer
} from 'echarts/renderers'
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
type ECOption = echarts.ComposeOption<
BarSeriesOption | LineSeriesOption | TitleComponentOption | GridComponentOption | RadarSeriesOption | PieSeriesOption
>;
// 注册必须的组件
echarts.use(
[TitleComponent, TooltipComponent, GridComponent, BarChart, LineChart, RadarChart, RadarComponent, PieChart, CanvasRenderer]
)
export {
ECOption,
echarts
}
\ No newline at end of file
<template>
<div v-if='type==="default"'>
<div
v-for='(val,index) in data'
:key='index'
class='py-2 border-b hover:bg-gray-100'
>
<div class='flex justify-between items-center'>
<div class='flex items-center'>
<div
v-if='val.imgUrl || val.iconClass'
class='mr-4'
>
<el-avatar
v-if='val.imgUrl'
size='large'
:src='val.imgUrl'
/>
<i
v-if='val.iconClass'
:class='{"text-3xl": true, [val.iconClass]: true}'
/>
</div>
<div>
<el-link
v-if='val.href'
type='primary'
:underline='false'
:href='val.href'
>
<p class='text-sm mb-1'>
{{ val.subTitle }}
<el-tag v-if='val.tag'>
{{ val.tag }}
</el-tag>
</p>
</el-link>
<p
v-else
class='text-sm mb-1'
>
{{ val.subTitle }}
<el-tag v-if='val.tag'>
{{ val.tag }}
</el-tag>
</p>
<p
v-if='val.time'
class='text-xs text-gray-500'
>
{{ val.time }}
</p>
</div>
</div>
<slot :item='val' />
</div>
</div>
</div>
<div
v-if='type==="card"'
class='component-list-card'
>
<el-card
shadow='never'
class='mb-2'
>
<template #header>
<slot name='header' />
</template>
<el-row>
<el-col
v-for='(val,index) in data'
:key='index'
:xs='24'
:sm='12'
:md='8'
class='c-list-card-body h-40 text-sm text-gray-400'
>
<div
v-if='val.title'
class='flex items-center py-1 text-black font-medium'
>
<div>
<el-avatar
v-if='val.imgUrl'
size='small'
:src='val.imgUrl'
/>
<i
v-if='val.iconClass'
:class='{"text-3xl": true, [val.iconClass]: true}'
/>
</div>
<div class='px-4 truncate text-base'>
{{ val.title }}
</div>
</div>
<div class='py-1 h-16 overflow-ellipsis overflow-hidden leading-6'>
<el-link
v-if='val.href'
type='primary'
:underline='false'
:href='val.href'
>
<p class='text-sm mb-1'>
{{ val.subTitle }}
</p>
</el-link>
<p v-else>
{{ val.subTitle }}
</p>
</div>
<div class='flex items-center justify-between'>
<div>{{ val.tag }}</div>
<div>{{ val.time }}</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export interface IList {
imgUrl?: string
iconClass?: string
title?: string
subTitle?: string
href?:string
tag?:string
time?:string
}
export type IListType = 'default' | 'card'
export default defineComponent({
name: 'List',
props: {
data: {
type: Array as PropType<Array<IList>>,
default: () => []
},
type: {
type: String as PropType<IListType>,
default: 'default'
}
},
setup() {
return {}
}
})
</script>
<style lang='postcss' scoped>
::v-deep(.el-card__header) {
margin-bottom: -1px;
}
::v-deep(.el-card__body) {
padding: 0;
}
.c-list-card-body {
transition: all 0.3s;
position: relative;
padding: 15px;
box-shadow: 1px 0 0 0 #f0f0f0, 0 1px 0 0 #f0f0f0, 1px 1px 0 0 #f0f0f0, inset 1px 0 0 0 #f0f0f0, inset 0 1px 0 0 #f0f0f0;
}
.c-list-card-body:hover {
z-index: 1;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
</style>
\ No newline at end of file
<template>
<transition name='el-fade-in'>
<div
v-if='isShow'
class='open-select-mask w-full h-full bg-black bg-opacity-30 z-50 fixed top-0 left-0 flex flex-center'
>
<div class='open-select w-11/12 max-w-screen-xl h-5/6 bg-white flex flex-col overflow-x-hidden overflow-y-auto'>
<div class='h-10 flex justify-between items-center px-3 shadow-sm border-b border-gray-100'>
<span>{{ title }}</span>
<div>
<i
class='el-icon-close cursor-pointer'
@click='close'
/>
</div>
</div>
<div class='flex-1 overflow-hidden'>
<el-scrollbar>
<slot name='default' />
</el-scrollbar>
</div>
<div
v-if='slots.btn'
class='open-select-btn h-12 flex border-t border-gray-100'
>
<slot name='btn' />
</div>
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, SetupContext } from 'vue'
export default defineComponent({
name: 'SelectPage',
props: {
isShow: {
type: Boolean,
default: false
},
title: {
type: String,
default: '新窗口'
}
},
emits: ['update:show'],
setup(props, context: SetupContext) {
const close = () => context.emit('update:show', !props.isShow)
return {
close,
slots: context.slots
}
}
})
</script>
<style lang="postcss" scoped>
.open-select-btn .el-button {
border-radius: 0;
flex: 1;
& + .el-button {
margin-left: -1px;
}
&:hover {
z-index: 1;
}
}
</style>
\ No newline at end of file
<template>
<div class='table-search flex flex-col'>
<div
ref='searchEl'
class='table-search-form'
>
<slot name='search' />
</div>
<div class='flex justify-between items-center mb-2'>
<div>
<el-button-group>
<el-button
type='primary'
icon='el-icon-edit'
/>
<el-button
type='primary'
icon='el-icon-share'
/>
<el-button
type='primary'
icon='el-icon-delete'
/>
</el-button-group>
</div>
<el-button
type='text'
@click='toggleSearch'
>
高级搜索<i :class='{"el-icon-arrow-down": !isShow, "el-icon-arrow-up": isShow}' />
</el-button>
</div>
<slot />
<el-pagination
:current-page='currentPage'
:page-sizes='[10, 20, 50, 100]'
:page-size='pageSize'
layout='total, sizes, prev, pager, next, jumper'
:total='total'
@size-change='handleSizeChange'
@current-change='handleCurrentChange'
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, SetupContext } from 'vue'
import { slide } from '/@/utils/animate'
export default defineComponent({
name: 'TableSearch',
props: {
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
}
},
emits: ['size-change', 'current-change'],
setup(props, context: SetupContext) {
const isShow = ref(false)
const handleSizeChange = (v:any) => context.emit('size-change', v)
const handleCurrentChange = (v: any) => context.emit('current-change', v)
const toggleSearch = () => {
isShow.value = !isShow.value
slide(searchEl, isShow.value)
}
const searchEl = ref(null)
return {
isShow,
handleSizeChange,
handleCurrentChange,
searchEl,
toggleSearch
}
}
})
</script>
<style lang="postcss" scoped>
.table-search-form {
overflow: hidden;
height: 0;
}
</style>
\ No newline at end of file
import { ITheme } from '/@/type/config/theme'
const theme:Array<ITheme> = [
{
tagsActiveColor: '#fff',
tagsActiveBg: '#409EFF',
mainBg: '#f0f2f5',
sidebarColor: '#fff',
sidebarBg: '#001529',
sidebarChildrenBg: '#000c17',
sidebarActiveColor: '#fff',
sidebarActiveBg: '#409EFF',
sidebarActiveBorderRightBG: '#1890ff'
},
{
tagsActiveColor: '#fff',
tagsActiveBg: '#409EFF',
navbarColor: '#fff',
navbarBg: '#393D49',
mainBg: '#f0f2f5',
sidebarColor: '#fff',
sidebarBg: '#001529',
sidebarChildrenBg: '#000c17',
sidebarActiveColor: '#fff',
sidebarActiveBg: '#409EFF',
sidebarActiveBorderRightBG: '#1890ff'
},
{
tagsActiveColor: '#fff',
tagsActiveBg: '#409EFF',
mainBg: '#f0f2f5',
sidebarColor: '#333',
sidebarBg: '#fff',
sidebarChildrenBg: '#fff',
sidebarActiveColor: '#409EFF',
sidebarActiveBg: '#e6f7ff',
sidebarActiveBorderRightBG: '#409EFF'
},
{
logoColor: 'rgba(255,255,255,.7)',
logoBg: '#50314F',
tagsColor: '#333',
tagsBg: '#fff',
tagsActiveColor: '#fff',
tagsActiveBg: '#7A4D7B',
mainBg: '#f0f2f5',
sidebarColor: 'rgba(255,255,255,.7)',
sidebarBg: '#50314F',
sidebarChildrenBg: '#382237',
sidebarActiveColor: '#fff',
sidebarActiveBg: '#7A4D7B',
sidebarActiveBorderRightBG: '#7A4D7B'
},
{
logoColor: 'rgba(255,255,255,.7)',
logoBg: '#50314F',
navbarColor: 'rgba(255,255,255,.7)',
navbarBg: '#50314F',
tagsColor: '#333',
tagsBg: '#fff',
tagsActiveColor: '#fff',
tagsActiveBg: '#7A4D7B',
mainBg: '#f0f2f5',
sidebarColor: 'rgba(255,255,255,.7)',
sidebarBg: '#50314F',
sidebarChildrenBg: '#382237',
sidebarActiveColor: '#fff',
sidebarActiveBg: '#7A4D7B',
sidebarActiveBorderRightBG: '#7A4D7B'
}
]
export default theme
\ No newline at end of file
import { App, DirectiveBinding } from 'vue'
import { checkPermission, IPermissionType } from '/@/utils/permission'
const actionPermission = (el:HTMLElement, binding:DirectiveBinding) => {
const value:Array<string> = typeof binding.value === 'string' ? [binding.value] : binding.value
const arg:IPermissionType = binding.arg === 'and' ? 'and' : 'or'
if(!checkPermission(value, arg)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
export default (app:App<Element>):void => {
app.directive('action', {
mounted: (el, binding) => actionPermission(el, binding)
})
}
\ No newline at end of file
import { App, nextTick } from 'vue'
import { format, unformat } from '/@/utils/index'
export default (app:App<Element>):void => {
app.directive('format', {
beforeMount(el, binding) {
const { arg, value } = binding
if(arg === 'money') {
const elem = el.firstElementChild
nextTick(() => elem.value = format(elem.value))
elem.addEventListener('focus', (event:MouseEvent) => {
if(!event.target) return
const target = event.target as HTMLInputElement
target.value = String(unformat(target.value))
value[0][value[1]] = target.value
}, true)
elem.addEventListener('blur', (event: MouseEvent) => {
if(!event.target) return
const target = event.target as HTMLInputElement
const val = unformat(format(target.value))
value[0][value[1]] = val === '' ? 0 : val
nextTick(() => target.value = format(val))
}, true)
}
}
})
}
\ No newline at end of file
import { App } from 'vue'
const modules = import.meta.glob('../directive/**/**.ts')
// 自动导入当前文件夹下的所有自定义指令(默认导出项)
export default (app:App<Element>):void => {
for (const path in modules) {
// 排除当前文件
if(path !== '../directive/index.ts') {
modules[path]().then((mod) => {
mod.default(app)
})
}
}
}
\ No newline at end of file
<template>
<div>
<router-view />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'LayoutBlank',
setup() {
return {}
}
})
</script>
\ No newline at end of file
<template>
<el-scrollbar>
<router-view
v-slot='{ Component }'
>
<transition
name='fade-transform'
mode='out-in'
>
<keep-alive
:include='layout.setting.showTags ? data.cachedViews : []'
>
<component
:is='Component'
:key='key'
class='page m-3 relative'
/>
</keep-alive>
</transition>
</router-view>
<el-backtop
target='.layout-main-content>.el-scrollbar>.el-scrollbar__wrap'
:bottom='15'
:right='15'
>
<div><i class='el-icon-caret-top' /></div>
</el-backtop>
</el-scrollbar>
</template>
<script lang='ts'>
import { computed, defineComponent, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from '/@/store/index'
export default defineComponent ({
name: 'LayoutContent',
setup() {
const route = useRoute()
const store = useStore()
const key = computed(() => route.path)
let data = reactive({
cachedViews: [...store.state.layout.tags.cachedViews]
})
// keep-alive的include重新赋值,解决bug https://github.com/vuejs/vue-next/issues/2550
watch(
() => store.state.layout.tags.cachedViews.length,
() => data.cachedViews = [...store.state.layout.tags.cachedViews]
)
return {
key,
data,
layout: store.state.layout
}
}
})
</script>
<style lang='postcss' scoped>
::v-deep(.el-card) {
overflow: visible;
}
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
\ No newline at end of file
<template>
<el-scrollbar wrap-class='scrollbar-wrapper'>
<el-menu
:default-active='activeMenu'
:collapse='menubar.status === 1 || menubar.status === 3'
class='el-menu-vertical-demo'
:class='{
"w-64": menubar.status === 0 || menubar.status === 2,
"w-0": menubar.status === 3,
"w-16": menubar.status === 1,
}'
:collapse-transition='false'
:unique-opened='false'
@select='onOpenChange'
>
<menubar-item
v-for='v in filterMenubarData'
:key='v.path'
:index='v.path'
:menu-list='v'
/>
</el-menu>
</el-scrollbar>
</template>
<script lang='ts'>
import { defineComponent, computed } from 'vue'
import MenubarItem from '/@/layout/components/menubarItem.vue'
import { useStore } from '/@/store/index'
import { useRoute, useRouter } from 'vue-router'
import { IMenubarList } from '/@/type/store/layout'
// 过滤隐藏的菜单,并提取单条的子菜单
const filterMenubar = (menuList:IMenubarList[]) => {
const f = (menuList:IMenubarList[]) => {
let arr:IMenubarList[] = []
menuList.filter(v => !v.meta.hidden).forEach(v => {
let child = v.children && v.children.filter(v => !v.meta.hidden)
let currentItem = v
if(!v.meta.alwaysShow && child && child.length === 1) {
[currentItem] = child
}
arr.push(currentItem)
if(currentItem.children && currentItem.children.length > 0) {
arr[arr.length - 1].children = f(currentItem.children)
}
})
return arr
}
return f(menuList)
}
export default defineComponent ({
name: 'LayoutMenubar',
components: {
MenubarItem
},
setup() {
const store = useStore()
const route = useRoute()
const router = useRouter()
const { menubar } = store.state.layout
const filterMenubarData = filterMenubar(menubar.menuList)
const activeMenu = computed(() => {
if(route.meta.activeMenu) return route.meta.activeMenu
return route.path
})
const onOpenChange = (d: any) => {
router.push({ path: d })
menubar.status === 2 && store.commit('layout/changeCollapsed')
}
return {
menubar,
filterMenubarData,
activeMenu,
onOpenChange
}
}
})
</script>
\ No newline at end of file
<template>
<el-submenu
v-if='menuList.children && menuList.children.length > 0'
:key='menuList.path'
:index='menuList.path'
>
<template #title>
<i :class='menuList.meta.icon || "el-icon-location"' />
<span>{{ menuList.meta.title }}</span>
</template>
<el-menu-item-group>
<menubar-item
v-for='v in menuList.children'
:key='v.path'
:index='v.path'
:menu-list='v'
/>
</el-menu-item-group>
</el-submenu>
<el-menu-item
v-else
:key='menuList.path'
:index='menuList.path'
>
<i :class='menuList.meta.icon || "el-icon-setting"' />
<template #title>
{{ menuList.meta.title }}
</template>
</el-menu-item>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IMenubarList } from '/@/type/store/layout'
export default defineComponent({
name: 'MenubarItem',
props: {
menuList: {
type: Object as PropType<IMenubarList>,
default: () => {return {}}
}
},
setup() {
return {}
}
})
</script>
\ No newline at end of file
<template>
<div class='flex items-center px-4'>
<span
class='text-2xl cursor-pointer'
:class='{ "el-icon-s-fold": !menubar.status, "el-icon-s-unfold": menubar.status }'
@click='changeCollapsed'
/>
<!-- 面包屑导航 -->
<div class='px-4 hidden-xs-only'>
<el-breadcrumb separator='/'>
<transition-group name='breadcrumb'>
<el-breadcrumb-item
key='/'
:to='{ path: "/" }'
>
主页
</el-breadcrumb-item>
<el-breadcrumb-item
v-for='v in data.breadcrumbList'
:key='v.path'
:to='v.path'
>
{{ v.title }}
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>
</div>
<div class='flex items-center flex-row-reverse px-4 min-width-32'>
<!-- 用户下拉 -->
<el-dropdown>
<span class='el-dropdown-link flex flex-center mx-2'>
<el-avatar
:size='30'
src='https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
/>
<span class='ml-2'>{{ userInfo.name }}</span>
<i class='el-icon-arrow-down el-icon--right' />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-link
href='https://github.com/hsiangleev'
target='_blank'
:underline='false'
>
个人中心
</el-link>
</el-dropdown-item>
<el-dropdown-item>
<el-link
href='https://github.com/hsiangleev/element-plus-admin'
target='_blank'
:underline='false'
>
项目地址
</el-link>
</el-dropdown-item>
<el-dropdown-item
divided
@click='logout'
>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<Notice />
</div>
</template>
<script lang='ts'>
import { defineComponent, reactive, watch } from 'vue'
import { useStore } from '/@/store/index'
import { useRoute, RouteLocationNormalizedLoaded } from 'vue-router'
import Notice from '/@/layout/components/notice.vue'
interface IBreadcrumbList {
path: string
title: string | symbol
}
// 面包屑导航
const breadcrumb = (route: RouteLocationNormalizedLoaded) => {
const fn = () => {
const breadcrumbList:Array<IBreadcrumbList> = []
const notShowBreadcrumbList = ['Dashboard', 'RedirectPage'] // 不显示面包屑的导航
if(route.matched[0] && (notShowBreadcrumbList.includes(route.matched[0].name as string))) return breadcrumbList
route.matched.forEach(v => {
const obj:IBreadcrumbList = {
title: v.meta.title,
path: v.path
}
breadcrumbList.push(obj)
})
return breadcrumbList
}
let data = reactive({
breadcrumbList: fn()
})
watch(() => route.path, () => data.breadcrumbList = fn())
return { data }
}
export default defineComponent ({
name: 'LayoutNavbar',
components: {
Notice
},
setup() {
const store = useStore()
const route = useRoute()
const changeCollapsed = () => store.commit('layout/changeCollapsed')
const logout = () => store.commit('layout/logout')
return {
menubar: store.state.layout.menubar,
userInfo: store.state.layout.userInfo,
changeCollapsed,
logout,
...breadcrumb(route)
}
}
})
</script>
<style lang='postcss' scoped>
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.5s;
}
.breadcrumb-leave-active {
position: absolute;
}
</style>
\ No newline at end of file
<template>
<el-dropdown trigger='click'>
<el-badge
:value='6'
type='danger'
class='el-dropdown-link item mx-2 cursor-pointer leading-none'
>
<i class='el-icon-bell text-xl' />
</el-badge>
<template #dropdown>
<el-dropdown-menu>
<el-tabs
type='border-card'
class='notice-tabs z-10'
>
<el-tab-pane
label='通知'
class='notice-tabs-pane'
>
<el-scrollbar class='scrollbar-wrapper'>
<list :data='data' />
<el-pagination
layout='prev, pager, next'
:total='1000'
:hide-on-single-page='false'
small
:pager-count='5'
/>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane
label='关注'
class='notice-tabs-pane'
>
<el-scrollbar class='scrollbar-wrapper'>
<list :data='data' />
</el-scrollbar>
</el-tab-pane>
<el-tab-pane
label='待办'
class='notice-tabs-pane'
>
<el-scrollbar class='scrollbar-wrapper'>
<list :data='data' />
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import List, { IList } from '/@/components/List/index.vue'
export default defineComponent({
name: 'Notice',
components: {
List
},
setup() {
const data:IList[] = [
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32', href: 'javascript:;' },
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32' },
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32' },
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32' },
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32' },
{ imgUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', subTitle: '斗通关无际县军连用知政以该果思快领a。', time: '2021/01/28 15:21:32' }
]
return {
data
}
}
})
</script>
<style lang='postcss' scoped>
.notice-tabs {
margin: -10px 0;
border: none;
width: 300px;
}
.notice-tabs-pane {
height: 400px;
overflow: hidden;
margin: -15px;
}
.scrollbar-wrapper {
padding: 15px;
}
</style>
\ No newline at end of file
<template>
<div class='position'>
<el-scrollbar
ref='scrollbar'
wrap-class='scrollbar-wrapper'
>
<div class='layout-tags-container whitespace-nowrap'>
<span
v-for='v in tagsList'
:key='v.path'
:ref='getTagsDom'
class='border border-gray-200 px-2 py-1 mx-1 cursor-pointer rounded-md'
:class='{"layout-tags-active": v.isActive}'
@contextmenu.prevent='contextRightMenu(v,$event)'
>
<i
v-if='v.isActive'
class='rounded-full inline-block w-2 h-2 bg-white -ml-1 mr-1'
/>
<router-link :to='v.path'>{{ v.title }}</router-link>
<i
v-if='tagsList.length>1'
class='el-icon-close text-xs hover:bg-gray-300 hover:text-white rounded-full leading-3 p-0.5 ml-1 -mr-1'
@click='removeTagNav(v)'
/>
</span>
</div>
</el-scrollbar>
<ul
ref='rightMenuEl'
class='border border-gray-200 absolute w-24 leading-none bg-white shadow-md rounded-lg py-0.5 z-10'
:style='menuPos'
>
<li
class='px-4 py-2 cursor-pointer hover:bg-gray-200'
@click='refresh'
>
刷新
</li>
<li
class='px-4 py-2 cursor-pointer hover:bg-gray-200'
@click='closeOther'
>
关闭其它
</li>
<li
class='px-4 py-2 cursor-pointer hover:bg-gray-200'
@click='closeAll'
>
关闭所有
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, ref, watch, onBeforeUpdate, onMounted, reactive, Ref, ComponentInternalInstance } from 'vue'
import { useStore } from '/@/store/index'
import { Store } from 'vuex'
import { useRoute, useRouter, Router, RouteLocationNormalizedLoaded } from 'vue-router'
import { ITagsList } from '/@/type/store/layout'
// 右键菜单
const rightMenu = (store:Store<IState>, router: Router, route: RouteLocationNormalizedLoaded) => {
const menuPos = reactive({
left: '0px',
top: '0px',
display: 'none'
})
const rightMenuEl:Ref<HTMLElement | null> = ref(null)
// 当前右键的那个标签
let currentRightTags:ITagsList
const contextRightMenu = (v:ITagsList, event:MouseEvent) => {
currentRightTags = v
menuPos.display = 'block'
nextTick(() => {
let left = event.clientX - 5
if(!rightMenuEl.value) return
if(event.clientX + rightMenuEl.value.offsetWidth > document.body.offsetWidth) {
left = event.clientX - rightMenuEl.value.offsetWidth
}
menuPos.left = `${left}px`
menuPos.top = `${event.clientY + 10}px`
})
}
const refresh = () => {
if(currentRightTags.path === route.path) {
router.replace(`/redirect${currentRightTags.path}`)
}else{
router.push(`/redirect${currentRightTags.path}`)
}
}
const closeOther = () => store.commit('layout/removeOtherTagNav', currentRightTags)
document.body.addEventListener('click', () => menuPos.display = 'none')
return { menuPos, contextRightMenu, refresh, rightMenuEl, closeOther }
}
// 标签页滚动
const tagScroll = (store:Store<IState>) => {
const { tagsList, cachedViews } = store.state.layout.tags
const scrollbar:Ref<{wrap:HTMLElement, update():void} | null> = ref(null)
const layoutTagsItem:Ref<Array<ComponentInternalInstance | Element | null>> = ref([])
const getTagsDom = (el:ComponentInternalInstance | Element | null) => el && layoutTagsItem.value.push(el)
// 监听标签页导航
watch(
() => tagsList.length,
() => nextTick(() => {
if(!scrollbar.value) return
scrollbar.value.update()
nextTick(() => {
const itemWidth = layoutTagsItem.value.filter(v => v).reduce((acc, v) => {
const val = v as HTMLElement
return acc + val.offsetWidth + 6
}, 0)
if(!scrollbar.value) return
const scrollLeft = itemWidth - scrollbar.value.wrap.offsetWidth + 70
if(scrollLeft > 0) scrollbar.value.wrap.scrollLeft = scrollLeft
})
})
)
// 确保在每次变更之前重置引用
onBeforeUpdate(() => {
layoutTagsItem.value = []
})
return { tagsList, scrollbar, layoutTagsItem, cachedViews, getTagsDom }
}
export default defineComponent({
name: 'LayoutTags',
setup() {
const store = useStore()
const route = useRoute()
const router = useRouter()
const removeTagNav = (v: any) => store.commit('layout/removeTagNav', { cPath: route.path, tagsList: v })
const closeAll = () => store.commit('layout/removeAllTagNav')
onMounted(() => {
store.commit('layout/addCachedViews', { name: route.name, noCache: route.meta.noCache })
})
const rightMenuData = rightMenu(store, router, route)
const tagScrollData = tagScroll(store)
return {
removeTagNav,
...tagScrollData,
...rightMenuData,
closeAll
}
}
})
</script>
\ No newline at end of file
<template>
<i
class='el-icon-s-tools text-2xl px-2 py-1 cursor-pointer rounded-l-md'
@click='drawer=!drawer'
/>
<el-drawer
v-model='drawer'
title='主题设置'
size='260px'
>
<el-row :gutter='20'>
<el-col
v-for='(val,index) in theme'
:key='index'
:span='8'
>
<div
class='flex shadow-lg border border-gray-100 w-18 cursor-pointer m-1'
@click='changeTheme(index)'
>
<div class='flex flex-col w-4 h-16'>
<div
class='h-3'
:style='{"backgroundColor": (val.logoBg || val.sidebarBg)}'
/>
<div
class='flex-1'
:style='{"backgroundColor": val.sidebarBg}'
/>
</div>
<div class='flex flex-col flex-1'>
<div
class='h-3'
:style='{"backgroundColor": val.navbarBg || "#fff"}'
/>
<div
v-if='layout.setting.showTags'
class='h-2'
:style='{"backgroundColor": val.tagsBg || "#fff"}'
/>
<div
class='flex-1 relative'
:style='{"backgroundColor": val.mainBg}'
>
<i
v-if='layout.setting.theme===index'
class='el-icon-check absolute left-2/4 top-2/4 transform -translate-x-2/4 -translate-y-2/4'
style='color: #1890ff;'
/>
</div>
</div>
</div>
</el-col>
</el-row>
<div class='flex justify-between mt-5 px-2 py-1 items-center'>
<div class='text-sm'>
开启 Tags-View
</div>
<el-switch v-model='showTags' />
</div>
</el-drawer>
</template>
<script lang='ts'>
import { ref, defineComponent, watch } from 'vue'
import theme from '/@/config/theme'
import { useStore } from '/@/store/index'
export default defineComponent ({
name: 'LayoutTheme',
setup() {
const store = useStore()
const drawer = ref(false)
const changeTheme = (index:number) => store.commit('layout/changeTheme', index)
const showTags = ref(store.state.layout.setting.showTags)
watch(() => showTags.value, () => store.commit('layout/changeTagsSetting', showTags.value))
return {
drawer,
theme,
changeTheme,
layout: store.state.layout,
showTags
}
}
})
</script>
<template>
<div class='layout flex h-screen'>
<div
class='layout-sidebar-mask fixed w-screen h-screen bg-black bg-opacity-25 z-20'
:class='{"hidden": layout.menubar.status !== 2 }'
@click='changeCollapsed'
/>
<div
class='layout-sidebar flex flex-col h-screen transition-width duration-200 shadow'
:class='{
"w-64": layout.menubar.status === 0 || layout.menubar.status === 2,
"w-0": layout.menubar.status === 3,
"w-16": layout.menubar.status === 1,
"absolute z-30": layout.menubar.status === 2 || layout.menubar.status === 3,
}'
>
<div class='layout-sidebar-logo flex h-12 relative flex-center shadow-lg'>
{{ layout.menubar.status === 0 || layout.menubar.status === 2 ? 'hsianglee' : (layout.menubar.status === 1 ? 'lee' : '') }}
</div>
<div class='layout-sidebar-menubar flex flex-1 overflow-hidden'>
<layout-menubar />
</div>
</div>
<div class='layout-main flex flex-1 flex-col overflow-x-hidden overflow-y-auto'>
<div class='layout-main-navbar flex justify-between items-center h-12 shadow-sm border-b border-gray-100 overflow-hidden'>
<layout-navbar />
</div>
<div
v-if='layout.setting.showTags'
class='layout-main-tags h-10 leading-10 overflow-hidden shadow text-sm text-gray-600 px-3 position'
>
<layout-tags />
</div>
<div class='layout-main-content flex-1 overflow-hidden'>
<layout-content />
</div>
</div>
<div class='layout-sidebar-theme fixed right-0 top-64 z-10'>
<layout-Theme />
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted } from 'vue'
import LayoutContent from '/@/layout/components/content.vue'
import LayoutMenubar from '/@/layout/components/menubar.vue'
import LayoutNavbar from '/@/layout/components/navbar.vue'
import LayoutTags from '/@/layout/components/tags.vue'
import LayoutTheme from '/@/layout/components/theme.vue'
import { useStore } from '/@/store/index'
import { throttle } from '/@/utils/index'
export default defineComponent ({
name: 'Layout',
components: {
LayoutContent,
LayoutMenubar,
LayoutNavbar,
LayoutTags,
LayoutTheme
},
setup() {
const store = useStore()
const changeDeviceWidth = () => store.commit('layout/changeDeviceWidth')
const changeCollapsed = () => store.commit('layout/changeCollapsed')
store.commit('layout/changeTheme')
onMounted(() => {
changeDeviceWidth()
const throttleFn = throttle(300)
let throttleF = async function() {
await throttleFn()
changeDeviceWidth()
}
window.addEventListener('resize', throttleF, true)
})
return {
layout: store.state.layout,
changeCollapsed
}
}
})
</script>
\ No newline at end of file
<script lang="ts">
import { defineComponent, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
name: 'Redirect',
setup() {
const route = useRoute()
const router = useRouter()
const { pathMatch } = route.params
router.replace({ path: typeof pathMatch === 'string' ? `/${pathMatch}` : `/${pathMatch.join('/')}` })
},
render() {
return h('div')
}
})
</script>
\ No newline at end of file
import { createApp } from 'vue'
import App from '/@/App.vue'
import ElementPlus from 'element-plus'
import direct from '/@/directive/index'
import router from '/@/router/index'
import { store } from '/@/store/index'
import '/@/mock/index'
import '/@/permission'
import 'element-plus/lib/theme-chalk/index.css'
import 'element-plus/lib/theme-chalk/display.css'
import 'nprogress/nprogress.css'
import '/@/assets/css/index.css'
const app = createApp(App)
direct(app)
app.use(ElementPlus)
app.use(router)
app.use(store)
app.mount('#app')
\ No newline at end of file
import { IMenubarList } from '/@/type/store/layout'
export const user = [
{ name: 'admin', pwd: 'admin' },
{ name: 'dev', pwd: 'dev' },
{ name: 'test', pwd: 'test' }
]
export const role = [
{ name: 'admin', description: '管理员' },
{ name: 'dev', description: '开发人员' },
{ name: 'test', description: '测试人员' }
]
export const user_role = [
{ userName: 'admin', roleName: 'admin' },
{ userName: 'dev', roleName: 'dev' },
{ userName: 'test', roleName: 'test' }
]
export const permission = [
{ name: 'add', description: '新增' },
{ name: 'update', description: '修改' },
{ name: 'remove', description: '删除' }
]
export const role_route = [
{ roleName: 'admin', id: 1, permission: [] },
{ roleName: 'admin', id: 10, permission: [] },
{ roleName: 'admin', id: 2, permission: [] },
{ roleName: 'admin', id: 20, permission: [] },
{ roleName: 'admin', id: 21, permission: [] },
{ roleName: 'admin', id: 22, permission: [] },
{ roleName: 'admin', id: 3, permission: [] },
{ roleName: 'admin', id: 30, permission: [] },
{ roleName: 'admin', id: 300, permission: [] },
{ roleName: 'admin', id: 31, permission: [] },
{ roleName: 'admin', id: 310, permission: [] },
{ roleName: 'admin', id: 4, permission: [] },
{ roleName: 'admin', id: 40, permission: [] },
{ roleName: 'admin', id: 41, permission: [] },
{ roleName: 'admin', id: 42, permission: [] },
{ roleName: 'admin', id: 43, permission: [] },
{ roleName: 'admin', id: 5, permission: [] },
{ roleName: 'admin', id: 50, permission: ['add', 'update', 'remove'] },
{ roleName: 'dev', id: 1, permission: [] },
{ roleName: 'dev', id: 10, permission: [] },
{ roleName: 'dev', id: 5, permission: [] },
{ roleName: 'dev', id: 50, permission: ['add'] },
{ roleName: 'test', id: 1, permission: [] },
{ roleName: 'test', id: 10, permission: [] },
{ roleName: 'test', id: 5, permission: [] },
{ roleName: 'test', id: 50, permission: ['update'] }
]
export const route:Array<IMenubarList> = [
{
id: 2,
parentId: 0,
name: 'Project',
path: '/Project',
component: 'Layout',
redirect: '/Project/ProjectList',
meta: { title: '项目管理', icon: 'el-icon-phone' }
},
{
id: 20,
parentId: 2,
name: 'ProjectList',
path: '/Project/ProjectList',
component: 'ProjectList',
meta: { title: '项目列表', icon: 'el-icon-goods' }
},
{
id: 21,
parentId: 2,
name: 'ProjectDetail',
path: '/Project/ProjectDetail/:projName',
component: 'ProjectDetail',
meta: { title: '项目详情', icon: 'el-icon-question', activeMenu: '/Project/ProjectList', hidden: true }
},
{
id: 22,
parentId: 2,
name: 'ProjectImport',
path: '/Project/ProjectImport',
component: 'ProjectImport',
meta: { title: '项目导入', icon: 'el-icon-help' }
},
{
id: 3,
parentId: 0,
name: 'Nav',
path: '/Nav',
component: 'Layout',
redirect: '/Nav/SecondNav/ThirdNav',
meta: { title: '多级导航', icon: 'el-icon-picture' }
},
{
id: 30,
parentId: 3,
name: 'SecondNav',
path: '/Nav/SecondNav',
redirect: '/Nav/SecondNav/ThirdNav',
component: 'SecondNav',
meta: { title: '二级导航', icon: 'el-icon-camera', alwaysShow: true }
},
{
id: 300,
parentId: 30,
name: 'ThirdNav',
path: '/Nav/SecondNav/ThirdNav',
component: 'ThirdNav',
meta: { title: '三级导航', icon: 'el-icon-s-platform' }
},
{
id: 31,
parentId: 3,
name: 'SecondText',
path: '/Nav/SecondText',
redirect: '/Nav/SecondText/ThirdText',
component: 'SecondText',
meta: { title: '二级文本', icon: 'el-icon-s-opportunity', alwaysShow: true }
},
{
id: 310,
parentId: 31,
name: 'ThirdText',
path: '/Nav/SecondText/ThirdText',
component: 'ThirdText',
meta: { title: '三级文本', icon: 'el-icon-menu' }
},
{
id: 4,
parentId: 0,
name: 'Components',
path: '/Components',
component: 'Layout',
redirect: '/Components/OpenWindowTest',
meta: { title: '组件测试', icon: 'el-icon-phone' }
},
{
id: 40,
parentId: 4,
name: 'OpenWindowTest',
path: '/Components/OpenWindowTest',
component: 'OpenWindowTest',
meta: { title: '选择页', icon: 'el-icon-goods' }
},
{
id: 41,
parentId: 4,
name: 'CardListTest',
path: '/Components/CardListTest',
component: 'CardListTest',
meta: { title: '卡片列表', icon: 'el-icon-question' }
},
{
id: 42,
parentId: 4,
name: 'TableSearchTest',
path: '/Components/TableSearchTest',
component: 'TableSearchTest',
meta: { title: '表格搜索', icon: 'el-icon-question' }
},
{
id: 43,
parentId: 4,
name: 'ListTest',
path: '/Components/ListTest',
component: 'ListTest',
meta: { title: '标签页列表', icon: 'el-icon-question' }
},
{
id: 5,
parentId: 0,
name: 'Permission',
path: '/Permission',
component: 'Layout',
redirect: '/Permission/Directive',
meta: { title: '权限管理', icon: 'el-icon-phone', alwaysShow: true }
},
{
id: 50,
parentId: 5,
name: 'Directive',
path: '/Permission/Directive',
component: 'Directive',
meta: { title: '指令管理', icon: 'el-icon-goods' }
}
]
\ No newline at end of file
import { mock, Random } from 'mockjs'
import { login, setToken, checkToken, getUser, getRoute } from '/@/mock/response'
interface IReq {
body: any
}
mock('/login', 'post', (req: IReq) => {
const { username, password } = JSON.parse(req.body)
if(login(username, password)) {
return mock({
Code: 200,
Msg: '登陆成功',
Data: setToken(username)
})
}
return mock({
Code: 401,
Msg: '用户名或密码错误',
Data: ''
})
})
mock('/getUser', 'get', (req: IReq) => {
const { token } = JSON.parse(req.body)
const userName = checkToken(token)
if(!userName) {
return mock({
Code: 401,
Msg: '身份认证失败',
Data: ''
})
}
return mock({
Code: 200,
Msg: '',
Data: getUser(userName)
})
})
mock('/getRoute', 'get', (req: IReq) => {
const { token } = JSON.parse(req.body)
const userName = checkToken(token)
if(!userName) {
return mock({
Code: 401,
Msg: '身份认证失败',
Data: ''
})
}
return mock({
Code: 200,
Data: getRoute(userName),
Msg: ''
})
})
Random.extend({
tag: function() {
const tag = ['家', '公司', '学校', '超市']
return this.pick(tag)
}
})
interface ITableList {
list: Array<{
date: string
name: string
address: string
tag: '家' | '公司' | '学校' | '超市'
amt: number
}>
}
const tableList: ITableList = mock({
// 属性 list 的值是一个数组,其中含有 1 到 10 个元素
'list|100': [{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1,
date: () => Random.date('yyyy-MM-dd'),
name: () => Random.name(),
address: () => Random.cparagraph(1),
tag: () => Random.tag(),
amt: () => Number(Random.float(-100000,100000).toFixed(2))
}]
})
mock('/getTableList', 'get', (req: IReq) => {
const { page, size, tag } = JSON.parse(req.body)
const data = tag === '所有' ? tableList.list : tableList.list.filter(v => v.tag === tag)
return mock({
Code: 200,
Data: {
data: data.filter((v,i) => i >= (page - 1) * size && i < page * size),
total: data.length
},
Msg: ''
})
})
\ No newline at end of file
import { user, user_role, role_route, route } from '/@/mock/data/user'
import { IMenubarList } from '/@/type/store/layout'
export const setToken = function(name: string):string {
return `token_${name}_token`
}
export const checkToken = function(token: string):string {
const match = token.match(/^token_([\w|\W]+?)_token/)
return match ? match[1] : ''
}
export const login = function(name: string, pwd: string):boolean {
return user.findIndex(v => v.name === name && v.pwd === pwd) !== -1
}
export const getUser = function(name: string):{name:string, role: Array<string>} {
return {
name,
role: user_role.filter(v => v.userName === name).map(v => v.roleName)
}
}
export const getRoute = function(name: string):Array<IMenubarList> {
const { role } = getUser(name)
const arr = role_route.filter(v => role.findIndex(val => val === v.roleName) !== -1)
const filterRoute:Array<IMenubarList> = []
route.forEach(v => {
arr.forEach(val => {
if(val.id === v.id) {
const obj = Object.assign({},v)
obj.meta.permission = val.permission
filterRoute.push(obj)
}
})
})
return filterRoute
}
\ No newline at end of file
import router from '/@/router'
import { store } from '/@/store/index'
import { configure, start, done } from 'nprogress'
import { RouteRecordRaw } from 'vue-router'
configure({ showSpinner: false })
const loginRoutePath = '/Login'
const defaultRoutePath = '/'
router.beforeEach(async(to, from) => {
start()
const { layout } = store.state
// 判断当前是否在登陆页面
if (to.path.toLocaleLowerCase() === loginRoutePath.toLocaleLowerCase()) {
done()
if(layout.token.ACCESS_TOKEN) return typeof to.query.from === 'string' ? decodeURIComponent(decodeURIComponent(to.query.from)) : defaultRoutePath
return
}
// // 判断是否登录
if(!layout.token.ACCESS_TOKEN) {
return loginRoutePath + (to.fullPath ? `?from=${encodeURIComponent(encodeURIComponent(to.fullPath))}` : '')
}
document.title = document.title ? `${document.title.split(' |')[0]} | ${to.meta.title}` : to.meta.title
// 判断是否还没添加过路由
if(layout.menubar.menuList.length === 0) {
await store.dispatch('layout/GenerateRoutes')
await store.dispatch('layout/getUser')
for(let i = 0;i < layout.menubar.menuList.length;i++) {
router.addRoute(layout.menubar.menuList[i] as RouteRecordRaw)
}
store.commit('layout/concatAllowRoutes')
return to.fullPath
}
store.commit('layout/changeTagNavList', to) // 切换导航,记录打开的导航(标签页)
// 离开当前页面时是否需要添加当前页面缓存
!new RegExp(/^\/redirect\//).test(from.path)
&& store.state.layout.tags.tagsList.some(v => v.name === from.name)
&& !store.state.layout.tags.cachedViews.some(v => v === from.name)
&& store.commit('layout/addCachedViews', { name: from.name, noCache: from.meta.noCache })
})
router.afterEach(() => {
done()
})
\ No newline at end of file
import { IMenubarList } from '/@/type/store/layout'
import { listToTree } from '/@/utils/index'
import { store } from '/@/store/index'
// 动态路由名称映射表
const modules = import.meta.glob('../views/**/**.vue')
const components:IObject<() => Promise<typeof import('*.vue')>> = {
Layout: (() => import('/@/layout/index.vue')) as unknown as () => Promise<typeof import('*.vue')>
}
Object.keys(modules).forEach(key => {
const nameMatch = key.match(/^\.\.\/views\/(.+)\.vue/)
if(!nameMatch) return
// 如果页面以Index命名,则使用父文件夹作为name
const indexMatch = nameMatch[1].match(/(.*)\/Index$/i)
let name = indexMatch ? indexMatch[1] : nameMatch[1];
[name] = name.split('/').splice(-1)
components[name] = modules[key] as () => Promise<typeof import('*.vue')>
})
const asyncRouter:IMenubarList[] = [
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: components['404'],
meta: {
title: 'NotFound',
icon: '',
hidden: true
},
redirect: {
name: '404'
}
}
]
export const generatorDynamicRouter = (data:IMenubarList[]):void => {
const routerList:IMenubarList[] = listToTree(data, 0)
asyncRouter.forEach(v => routerList.push(v))
const f = (data:IMenubarList[], pData:IMenubarList|null) => {
for(let i = 0,len = data.length;i < len;i++) {
const v:IMenubarList = data[i]
if(typeof v.component === 'string') v.component = components[v.component]
if(!v.meta.permission || pData && v.meta.permission.length === 0) {
v.meta.permission = pData && pData.meta && pData.meta.permission ? pData.meta.permission : []
}
if(v.children && v.children.length > 0) {
f(v.children, v)
}
}
}
f(routerList, null)
store.commit('layout/setRoutes', routerList)
}
\ No newline at end of file
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { IMenubarList } from '/@/type/store/layout'
const components:IObject<() => Promise<typeof import('*.vue')>> = {
Layout: (() => import('/@/layout/index.vue')) as unknown as () => Promise<typeof import('*.vue')>,
Redirect: (() => import('/@/layout/redirect.vue')) as unknown as () => Promise<typeof import('*.vue')>,
LayoutBlank: (() => import('/@/layout/blank.vue')) as unknown as () => Promise<typeof import('*.vue')>,
404: (() => import('/@/views/ErrorPage/404.vue')) as unknown as () => Promise<typeof import('*.vue')>,
Workplace: (() => import('/@/views/Dashboard/Workplace.vue')) as unknown as () => Promise<typeof import('*.vue')>,
Login: (() => import('/@/views/User/Login.vue')) as unknown as () => Promise<typeof import('*.vue')>
}
// 静态路由页面
export const allowRouter:Array<IMenubarList> = [
{
name: 'Dashboard',
path: '/',
component: components['Layout'],
redirect: '/Dashboard/Workplace',
meta: { title: '仪表盘', icon: 'el-icon-eleme' },
children: [
{
name: 'Workplace',
path: '/Dashboard/Workplace',
component: components['Workplace'],
meta: { title: '工作台', icon: 'el-icon-s-tools' }
}
]
},
{
name: 'ErrorPage',
path: '/ErrorPage',
meta: { title: '错误页面', hidden: true, icon: 'el-icon-eleme' },
component: components.LayoutBlank,
redirect: '/ErrorPage/404',
children: [
{
name: '404',
path: '/ErrorPage/404',
component: components['404'],
meta: { title: '404', icon: 'el-icon-s-tools' }
}
]
},
{
name: 'RedirectPage',
path: '/redirect',
component: components['Layout'],
meta: { title: '重定向页面', icon: 'el-icon-eleme', hidden: true },
children: [
{
name: 'Redirect',
path: '/redirect/:pathMatch(.*)*',
meta: {
title: '重定向页面',
icon: ''
},
component: components.Redirect
}
]
},
{
name: 'Login',
path: '/Login',
component: components.Login,
meta: { title: '登录', icon: 'el-icon-eleme', hidden: true }
}
]
const router = createRouter({
history: createWebHashHistory(), // createWebHistory
routes: allowRouter as RouteRecordRaw[]
})
export default router
\ No newline at end of file
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import layout from '/@/store/module/layout'
export const store = createStore<IState>({
modules: {
layout
}
})
export function useStore(): Store<IState> {
return baseUseStore()
}
import { login, loginParam, getRouterList, getUser } from '/@/api/layout/index'
import { ILayout, IMenubarStatus, ITagsList, IMenubarList, ISetting, IToken } from '/@/type/store/layout'
import { ActionContext } from 'vuex'
import router from '/@/router/index'
import { allowRouter } from '/@/router/index'
import { generatorDynamicRouter } from '/@/router/asyncRouter'
import changeTheme from '/@/utils/changeTheme'
import { setLocal, useLocal, getLocal } from '/@/utils/index'
import { RouteLocationNormalizedLoaded } from 'vue-router'
const setting = getLocal<ISetting>('setting')
const token = getLocal<IToken>('token')
// 前端检查token是否失效
useLocal('token')
.then(d => token.ACCESS_TOKEN = d.ACCESS_TOKEN)
.catch(() => mutations.logout(state))
const state:ILayout = {
menubar: {
status: document.body.offsetWidth < 768 ? IMenubarStatus.PHN : IMenubarStatus.PCE,
menuList: [],
isPhone: document.body.offsetWidth < 768
},
// 用户信息
userInfo: {
name: '',
role: []
},
// 标签栏
tags: {
tagsList: [],
cachedViews: []
},
token: {
ACCESS_TOKEN: token.ACCESS_TOKEN || ''
},
setting: {
theme: setting.theme !== undefined ? setting.theme : 0,
showTags: setting.showTags !== undefined ? setting.showTags : true
},
isLoading: false
}
const mutations = {
changeCollapsed(state: ILayout):void {
if(state.menubar.isPhone) {
state.menubar.status = state.menubar.status === IMenubarStatus.PHN ? IMenubarStatus.PHE : IMenubarStatus.PHN
}else{
state.menubar.status = state.menubar.status === IMenubarStatus.PCN ? IMenubarStatus.PCE : IMenubarStatus.PCN
}
},
changeDeviceWidth(state: ILayout):void {
if(document.body.offsetWidth < 768) {
state.menubar.isPhone = true
state.menubar.status = IMenubarStatus.PHN
}else{
state.menubar.isPhone = false
state.menubar.status = IMenubarStatus.PCE
}
},
// 切换导航,记录打开的导航
changeTagNavList(state: ILayout, cRouter:RouteLocationNormalizedLoaded):void {
if(!state.setting.showTags) return // 判断是否开启多标签页
if(cRouter.meta.hidden && !cRouter.meta.activeMenu) return // 隐藏的菜单如果不是子菜单则不添加到标签
const index = state.tags.tagsList.findIndex(v => v.path === cRouter.path)
state.tags.tagsList.forEach(v => v.isActive = false)
// 判断页面是否打开过
if(index !== -1) {
state.tags.tagsList[index].isActive = true
return
}
const tagsList:ITagsList = {
name: cRouter.name as string,
title: cRouter.meta.title,
path: cRouter.path,
isActive: true
}
state.tags.tagsList.push(tagsList)
},
removeTagNav(state: ILayout, obj:{tagsList:ITagsList, cPath: string}):void {
const index = state.tags.tagsList.findIndex(v => v.path === obj.tagsList.path)
if(state.tags.tagsList[index].path === obj.cPath) {
state.tags.tagsList.splice(index, 1)
const i = index === state.tags.tagsList.length ? index - 1 : index
state.tags.tagsList[i].isActive = true
mutations.removeCachedViews(state, { name: obj.tagsList.name, index })
router.push({ path: state.tags.tagsList[i].path })
}else{
state.tags.tagsList.splice(index, 1)
mutations.removeCachedViews(state, { name: obj.tagsList.name, index })
}
},
removeOtherTagNav(state: ILayout, tagsList:ITagsList):void {
const index = state.tags.tagsList.findIndex(v => v.path === tagsList.path)
state.tags.tagsList.splice(index + 1)
state.tags.tagsList.splice(0, index)
state.tags.cachedViews.splice(index + 1)
state.tags.cachedViews.splice(0, index)
router.push({ path: tagsList.path })
},
removeAllTagNav(state: ILayout):void {
state.tags.tagsList.splice(0)
state.tags.cachedViews.splice(0)
router.push({ path: '/redirect/' })
},
// 添加缓存页面
addCachedViews(state: ILayout, obj: {name: string, noCache: boolean}):void {
if(!state.setting.showTags) return // 判断是否开启多标签页
if(obj.noCache || state.tags.cachedViews.includes(obj.name)) return
state.tags.cachedViews.push(obj.name)
},
// 删除缓存页面
removeCachedViews(state: ILayout, obj: { name: string, index: number }):void {
// 判断标签页是否还有该页面
if(state.tags.tagsList.map(v => v.name).includes(obj.name)) return
state.tags.cachedViews.splice(obj.index, 1)
},
login(state: ILayout, token = ''):void {
state.token.ACCESS_TOKEN = token
setLocal('token', state.token, 1000 * 60 * 60)
const { query } = router.currentRoute.value
router.push(typeof query.from === 'string' ? decodeURIComponent(decodeURIComponent(query.from)) : '/')
},
logout(state: ILayout):void {
state.token.ACCESS_TOKEN = ''
localStorage.removeItem('token')
history.go(0)
},
setRoutes(state: ILayout, data: Array<IMenubarList>):void {
state.menubar.menuList = data
},
concatAllowRoutes(state: ILayout):void {
allowRouter.reverse().forEach(v => state.menubar.menuList.unshift(v))
},
getUser(state: ILayout, userInfo:{name:string, role: Array<string>}):void {
state.userInfo.name = userInfo.name
state.userInfo.role = userInfo.role
},
// 修改主题
changeTheme(state: ILayout, num:number):void {
if(num === state.setting.theme) return
if(typeof num !== 'number') num = state.setting.theme
changeTheme(num)
state.setting.theme = num
localStorage.setItem('setting', JSON.stringify(state.setting))
},
changeTagsSetting(state: ILayout, showTags:boolean):void {
state.setting.showTags = showTags
localStorage.setItem('setting', JSON.stringify(state.setting))
if(showTags) {
const index = state.tags.tagsList.findIndex(v => v.path === router.currentRoute.value.path)
if(index !== -1) {
state.tags.tagsList.forEach(v => v.isActive = false)
state.tags.tagsList[index].isActive = true
}else{
mutations.changeTagNavList(state, router.currentRoute.value)
}
}
}
}
const actions = {
async login(context:ActionContext<ILayout,IState>, param: loginParam):Promise<void> {
const res = await login(param)
const token = res.data.Data
context.commit('login', token)
},
async getUser(context:ActionContext<ILayout,IState>):Promise<void> {
const res = await getUser()
const userInfo = res.data.Data
context.commit('getUser', userInfo)
},
async GenerateRoutes():Promise<void> {
const res = await getRouterList()
const { Data } = res.data
generatorDynamicRouter(Data)
}
}
const layoutState = {
namespaced: true,
state,
mutations,
actions
}
export default layoutState
\ No newline at end of file
export interface ITheme {
// logo不传则使用侧边栏sidebar样式
logoColor?: string
logoBg?: string
// 顶部导航栏和标签栏不传则背景色使用白色,字体颜色默认
navbarColor?: string
navbarBg?: string
tagsColor?: string
tagsBg?: string
tagsActiveColor?: string
tagsActiveBg?: string
mainBg: string
sidebarColor: string
sidebarBg: string
sidebarChildrenBg: string
sidebarActiveColor: string
sidebarActiveBg: string
sidebarActiveBorderRightBG?: string
}
\ No newline at end of file
import { ILayout } from '/@/type/store/layout'
declare global {
interface IResponse<T = any> {
Code: number;
Msg: string;
Data: T;
}
interface IObject<T> {
[index: string]: T
}
interface IState {
layout: ILayout
}
interface ITable<T = any> {
data : Array<T>
total: number
page: number
size: number
}
}
\ No newline at end of file
declare module '*.vue' {
import { defineComponent } from 'vue'
const Component: ReturnType<typeof defineComponent>
export default Component
}
\ No newline at end of file
export enum IMenubarStatus {
PCE, // 电脑展开
PCN, // 电脑合并
PHE, // 手机展开
PHN // 手机合并
}
export interface ISetting {
theme: number
showTags: boolean
}
export interface IToken {
ACCESS_TOKEN: string
}
export interface ILayout {
// 左侧导航栏
menubar: {
status: IMenubarStatus
menuList: Array<IMenubarList>
isPhone: boolean
}
// 用户信息
userInfo: {
name: string,
role: Array<string>
}
// 标签栏
tags: {
tagsList: Array<ITagsList>
cachedViews: Array<string>
}
token: IToken
setting: ISetting
isLoading: boolean
}
export interface IMenubarList {
parentId?: number | string
id?: number | string
name: string
path: string
redirect?: string | {name: string}
meta: {
icon: string
title: string
permission?: Array<string>
activeMenu?: string // 路由设置了该属性,则会高亮相对应的侧边栏
noCache?: boolean // 页面是否不缓存
hidden?: boolean // 是否隐藏路由
alwaysShow?: boolean // 当子路由只有一个的时候是否显示当前路由
}
component: (() => Promise<typeof import('*.vue')>) | string
children?: Array<IMenubarList>
}
export interface ITagsList {
name: string
title: string
path: string
isActive: boolean
}
\ No newline at end of file
export interface ILocalStore {
startTime: number
expires: number
[propName: string]: any
}
\ No newline at end of file
export type ITag = '家' | '公司' | '学校' | '超市'
export interface IRenderTableList {
date: string
name: string
address: string
tag: ITag
amt: number
}
\ No newline at end of file
import { Ref, nextTick } from 'vue'
interface IAnimate {
timing(p: number): number
draw(p: number): void
duration: number
}
export function animate(param:IAnimate):void {
const { timing, draw, duration } = param
const start = performance.now()
requestAnimationFrame(function animate(time) {
// timeFraction 从 0 增加到 1
let timeFraction = (time - start) / duration
if (timeFraction > 1) timeFraction = 1
// 计算当前动画状态,百分比,0-1
const progress = timing(timeFraction)
draw(progress) // 绘制
if (timeFraction < 1) {
requestAnimationFrame(animate)
}
})
}
/**
* 下拉动画,0=>auto,auto=>0
* @param el dom节点
* @param isShow 是否显示
* @param duration 持续时间
*/
export async function slide(el:Ref<HTMLDivElement | null>, isShow:boolean, duration = 200):Promise<void> {
if(!el.value) return
const { position, zIndex } = getComputedStyle(el.value)
if(isShow) {
el.value.style.position = 'absolute'
el.value.style.zIndex = '-100000'
el.value.style.height = 'auto'
}
await nextTick()
const height = el.value.offsetHeight
if(isShow) {
el.value.style.position = position
el.value.style.zIndex = zIndex
el.value.style.height = '0px'
}
animate({
timing: timing.linear,
draw: function(progress) {
if(!el.value) return
el.value.style.height = isShow
? progress === 1
? 'auto'
: (`${progress * height}px`)
: progress === 0
? 'auto'
: (`${(1 - progress) * height}px`)
},
duration: duration
})
}
const timing = {
// 线性
linear(timeFraction: number): number {
return timeFraction
},
// n 次幂
quad(timeFraction: number, n = 2): number {
return Math.pow(timeFraction, n)
},
// 圆弧
circle(timeFraction: number): number {
return 1 - Math.sin(Math.acos(timeFraction))
}
}
\ No newline at end of file
import theme from '/@/config/theme'
import { ITheme } from '/@/type/config/theme'
export default function(num:number):HTMLStyleElement {
const themeStyle:ITheme = num >= theme.length ? theme[0] : theme[num]
const themeDom = document.createElement('style')
themeDom.className = 'layout-theme-setting'
themeDom.innerText = `
.layout-sidebar-logo {
background-color: ${themeStyle.logoBg || themeStyle.sidebarBg};
color: ${themeStyle.logoColor || themeStyle.sidebarColor};
}
.layout-sidebar {
background-color: ${themeStyle.sidebarBg};
}
.layout-sidebar .el-menu {
background-color: ${themeStyle.sidebarBg};
border-right: 0;
}
.layout-sidebar .el-menu .el-menu {
background-color: ${themeStyle.sidebarChildrenBg};
}
.layout-sidebar .el-submenu__title {
color: ${themeStyle.sidebarColor};
}
.layout-sidebar .el-menu-item {
color: ${themeStyle.sidebarColor};
}
.layout-sidebar .el-menu-item:focus,
.layout-sidebar .el-menu-item:hover,
.layout-sidebar .el-submenu__title:hover {
background-color: transparent;
color: ${themeStyle.sidebarActiveColor};
}
.layout-sidebar .el-menu-item-group__title {
padding: 0;
}
.layout-sidebar .el-submenu.is-active > .el-submenu__title,
.layout-sidebar .el-submenu.is-active > .el-submenu__title > i {
color: ${themeStyle.sidebarActiveColor};
}
.layout-sidebar .el-menu-item.is-active {
background-color: ${themeStyle.sidebarActiveBg};
color: ${themeStyle.sidebarActiveColor};
border-right: 3px solid ${themeStyle.sidebarActiveBorderRightBG};
}
${(function() {
let s = ''
if(themeStyle.navbarBg) {
s += `.layout-main-navbar {
background-color: ${themeStyle.navbarBg};
}`
}
if(themeStyle.navbarColor) {
s += `.layout-main-navbar {
color: ${themeStyle.navbarColor};
}
.layout-main-navbar .el-breadcrumb .el-breadcrumb__inner,
.layout-main-navbar .el-breadcrumb .el-breadcrumb__separator,
.layout-main-navbar .el-breadcrumb .el-breadcrumb__inner:hover,
.layout-main-navbar .el-breadcrumb .el-breadcrumb__separator:hover,
.layout-main-navbar .el-dropdown {
color: ${themeStyle.navbarColor};
}`
}
if(themeStyle.tagsBg) {
s += `.layout-main-tags {
background-color: ${themeStyle.tagsBg};
}`
}
if(themeStyle.tagsColor) {
s += `.layout-main-tags {
color: ${themeStyle.tagsColor};
}`
}
return s
})()}
.layout-main-content {
background-color: ${themeStyle.mainBg};
}
.layout-tags-active {
background-color: ${themeStyle.tagsActiveBg};
color: ${themeStyle.tagsActiveColor};
}
.layout-sidebar-theme > i {
background-color: ${themeStyle.sidebarActiveBg};
color: ${themeStyle.sidebarColor};
}
`.replace(/\n/g, '').replace(/ {4}/g, '')
const prevTheme = document.querySelector('style.layout-theme-setting')
prevTheme && prevTheme.remove()
document.head.appendChild(themeDom)
return themeDom
}
\ No newline at end of file
import { Ref, unref } from 'vue'
/**
* 表单校验
* @param ref 节点
* @param isGetError 是否获取错误项
*/
export async function validate(ref: Ref|any, isGetError = false):Promise<boolean | {valid: boolean, object: any}> {
const validateFn = unref(ref).validate
return new Promise(resolve => validateFn((valid:boolean, object: any) => isGetError ? resolve({ valid, object }) : resolve(valid)))
}
/**
* 对部分表单字段进行校验的方法
* @param ref 节点
* @param props 字段属性
*/
export async function validateField(ref: Ref|any, props: Array<string> | string):Promise<string> {
const validateFieldFn = unref(ref).validateField
return new Promise(resolve => validateFieldFn(props, (errorMessage: string) => resolve(errorMessage)))
}
/**
* 重置表单
* @param ref 节点
*/
export function resetFields(ref: Ref|any):void {
const resetFieldsFn = unref(ref).resetFields
resetFieldsFn()
}
/**
* 移除表单项的校验结果
* @param ref 节点
* @param props 字段属性
*/
export function clearValidate(ref: Ref|any, props?: Array<string> | string):void {
const clearValidateFn = unref(ref).clearValidate
props ? clearValidateFn(props) : clearValidateFn()
}
\ No newline at end of file
export * from '/@/utils/tools'
\ No newline at end of file
import router from '/@/router/index'
export type IPermissionType = 'or' | 'and'
export function checkPermission(permission:string|Array<string>, type:IPermissionType = 'or'):boolean {
const value:Array<string> = typeof permission === 'string' ? [permission] : permission
const currentRoute = router.currentRoute.value
const roles:Array<string> = currentRoute.meta.permission || []
const isShow = type === 'and'
? value.every(v => roles.includes(v))
: value.some(v => roles.includes(v))
return isShow
}
\ No newline at end of file
import { store } from '/@/store/index'
import axios from 'axios'
import { AxiosResponse } from 'axios'
import { ElLoading, ElNotification } from 'element-plus'
let loading:{close():void}
// 创建 axios 实例
const request = axios.create({
// API 请求的默认前缀
baseURL: import.meta.env.VUE_APP_API_BASE_URL as string | undefined,
timeout: 60000 // 请求超时时间
})
// 异常拦截处理器
const errorHandler = (error:{message:string}) => {
loading.close()
console.log(`err${error}`)
ElNotification({
title: '请求失败',
message: error.message,
type: 'error'
})
return Promise.reject(error)
}
// request interceptor
request.interceptors.request.use(config => {
loading = ElLoading.service({
lock: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.4)'
})
const token = store.state.layout.token.ACCESS_TOKEN
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
if (token) {
config.headers['Access-Token'] = token
}
return config
}, errorHandler)
// response interceptor
request.interceptors.response.use((response:AxiosResponse<IResponse>) => {
const { data } = response
loading.close()
if(data.Code !== 200) {
let title = '请求失败'
if(data.Code === 401) {
if (store.state.layout.token.ACCESS_TOKEN) {
store.commit('layout/logout')
}
title = '身份认证失败'
}
ElNotification({
title,
message: data.Msg,
type: 'error'
})
return Promise.reject(new Error(data.Msg || 'Error'))
}
return response
}, errorHandler)
export default request
\ No newline at end of file
import { ILocalStore } from '/@/type/utils/tools'
import { IMenubarList } from '/@/type/store/layout'
/**
* 睡眠函数
* @param time
*/
export async function sleep(time:number):Promise<void> {
await new Promise(resolve => {
setTimeout(() => resolve, time)
})
}
/**
* 金额格式化
* @param num 金额
* @param symbol 金额前面修饰符号,如$,¥
*/
export function format(num:number|string, symbol = '¥'):string {
if(Number.isNaN(Number(num))) return `${symbol}0.00`
return symbol + (Number(num).toFixed(2))
.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
}
/**
* 取消金额格式化
* @param str 金额
*/
export function unformat(str:string):number|string {
const s = str.substr(1).replace(/\,/g, '')
return Number.isNaN(Number(s)) || Number(s) === 0 ? '' : Number(s)
}
/**
* 表格合计行
* @param str 金额
*/
export function tableSummaries(param: { columns: any; data: any }):Array<string | number> {
const { columns, data } = param
const sums:Array<string | number> = []
columns.forEach((column: { property: string | number }, index:number) => {
if (index === 0) {
sums[index] = '合计'
return
}
const values = data.map((item: { [x: string]: any }) => Number(item[column.property]))
if (!values.every((value: number) => isNaN(value))) {
sums[index] = values.reduce((prev: number, curr: number) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
const sum = sums[index]
if(typeof sum === 'number') {
sums[index] = format(sum.toFixed(2))
}
} else {
sums[index] = 'N/A'
}
})
return sums
}
export function isInput(el: HTMLElement): boolean {
return el.nodeName.toLocaleLowerCase() === 'input'
}
export function isTextarea(el: HTMLElement): boolean {
return el.nodeName.toLocaleLowerCase() === 'textarea'
}
/**
* localStorage设置有效期
* @param name localStorage设置名称
* @param data 数据对象
* @param pExpires 有效期(默认100年)
*/
export function setLocal(name:string, data:IObject<any>, pExpires = 1000 * 60 * 60 * 24 * 365 * 100):void {
const d = data as ILocalStore
d.startTime = Date.now()
d.expires = pExpires
localStorage.setItem(name, JSON.stringify(data))
}
/**
* 判断localStorage有效期是否失效
* @param name localStorage设置名称
*/
export async function useLocal(name: string):Promise<ILocalStore> {
return new Promise((resolve, reject) => {
const local = getLocal<ILocalStore>(name)
if(local.startTime + local.expires < Date.now()) reject(`${name}已超过有效期`)
resolve(local)
})
}
/**
* 获取localStorage对象并转成对应的类型
* @param name localStorage设置名称
*/
export function getLocal<T>(name:string):T {
const l = localStorage.getItem(name)
const local = JSON.parse(l !== null ? l : '{}') as unknown as T
return local
}
/**
* 函数节流
* @param time 间隔时间
*/
export function throttle(time = 500):()=>Promise<void> {
let timer:NodeJS.Timeout | null = null
let firstTime = true
return () => {
return new Promise(resolve => {
if(firstTime) {
resolve()
return firstTime = false
}
if(timer) return false
timer = setTimeout(() => {
if(timer) clearTimeout(timer)
timer = null
resolve()
}, time)
})
}
}
/**
* list结构转tree
* @param data list原始数据
* @param pid 最外层pid
*/
export function listToTree(data:Array<IMenubarList>, pid: string | number = 1, isChildNull = false):Array<IMenubarList> {
const d:Array<IMenubarList> = []
data.forEach(val => {
if(val.parentId == pid) {
const list = listToTree(data, val.id, isChildNull)
const obj:IMenubarList = { ...val }
if(!isChildNull || list.length !== 0) {
obj.children = list
}
d.push(obj)
}
})
return d
}
\ No newline at end of file
<template>
<div>
budApply
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'BudApply'
})
</script>
<template>
<div>
budList
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'BudList'
})
</script>
This diff is collapsed.
<template>
<el-tabs type='border-card'>
<el-tab-pane label='通知'>
<list :data='data'>
<template #default='scope'>
<el-button @click='edit(scope.item)'>
操作
</el-button>
</template>
</list>
</el-tab-pane>
<el-tab-pane label='关注'>
<list :data='data' />
</el-tab-pane>
<el-tab-pane label='待办'>
<list :data='data' />
</el-tab-pane>
</el-tabs>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import List, { IList } from '/@/components/List/index.vue'
export default defineComponent({
name: 'ListTest',
components: {
List
},
setup() {
const data:IList[] = [
{
iconClass: 'el-icon-goods',
subTitle: '斗通关无际县军连用知政以该果思快领c。',
tag: '科学搬砖组',
time: '2021/01/28 15:21:32',
href: 'javascript:;'
},
{
iconClass: 'el-icon-goods',
subTitle: '斗通关无际县军连用知政以该果思快领c。',
tag: '科学搬砖组',
time: '2021/01/28 15:21:32',
href: 'javascript:;'
},
{
iconClass: 'el-icon-goods',
subTitle: '斗通关无际县军连用知政以该果思快领c。',
tag: '科学搬砖组',
time: '2021/01/28 15:21:32',
href: 'javascript:;'
}
]
const edit = (item:IList) => console.log(item)
return {
data,
edit
}
}
})
</script>
<template>
<div class='content'>
<el-button @click='show = true'>
打开窗体
</el-button>
<open-window
v-model:show='show'
:is-show='show'
title='选择页'
>
<p style='height: 1500px;'>
aaa
</p>
<template #btn>
<el-button>
默认按钮
</el-button>
<el-button>
默认按钮
</el-button>
<el-button>
默认按钮
</el-button>
</template>
</open-window>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import OpenWindow from '/@/components/OpenWindow/index.vue'
export default defineComponent({
name: 'OpenWindowTest',
components: {
OpenWindow
},
setup() {
const show = ref(false)
return {
show
}
}
})
</script>
\ No newline at end of file
<template>
<table-search
:current-page='table.page'
:page-size='table.size'
:total='table.total'
@size-change='handleSizeChange'
@current-change='handleCurrentChange'
>
<template #search>
<el-row
:gutter='15'
class='clear-both'
>
<el-col :span='24'>
<card-list
title='高级搜索'
type='keyvalue'
:show-header='true'
>
<template #btn>
<el-button-group>
<el-button
icon='el-icon-search'
size='mini'
@click='submit'
>
搜索
</el-button>
</el-button-group>
</template>
<template #keyvalue>
<el-form
ref='refForm'
class='card-list-form'
:model='form'
:rules='rules'
size='mini'
>
<el-row :gutter='15'>
<card-list-item
width='100px'
prop='name'
>
<template #key>
日期
</template>
<template #value>
<el-date-picker
v-model='form.date'
type='daterange'
range-separator='至'
start-placeholder='开始日期'
end-placeholder='结束日期'
/>
</template>
</card-list-item>
<card-list-item
width='100px'
prop='name'
>
<template #key>
姓名
</template>
<template #value>
<el-input
v-model='form.name'
placeholder='请输入姓名'
/>
</template>
</card-list-item>
<card-list-item
width='100px'
prop='address'
>
<template #key>
地址
</template>
<template #value>
<el-input
v-model='form.address'
placeholder='请输入地址'
/>
</template>
</card-list-item>
<card-list-item
width='100px'
prop='tag'
>
<template #key>
标签
</template>
<template #value>
<el-radio-group v-model='form.tag'>
<el-radio label='所有' />
<el-radio label='家' />
<el-radio label='学校' />
<el-radio label='超市' />
<el-radio label='公司' />
</el-radio-group>
</template>
</card-list-item>
</el-row>
</el-form>
</template>
</card-list>
</el-col>
</el-row>
</template>
<el-table
ref='filterTable'
row-key='date'
border
:data='tableData.data'
style='width: 100%;'
:summary-method='getSummaries'
show-summary
>
<el-table-column
type='index'
width='50'
:index='indexMethod'
/>
<el-table-column
prop='date'
label='日期'
sortable
width='180'
column-key='date'
/>
<el-table-column
prop='name'
label='姓名'
width='180'
/>
<el-table-column
prop='address'
label='地址'
/>
<el-table-column
prop='amt'
label='金额'
>
<template #default='scope'>
<el-input
v-model.number='scope.row.amt'
v-format:money='[scope.row, "amt"]'
/>
</template>
</el-table-column>
<el-table-column
prop='tag'
label='标签'
width='100'
>
<template #default='scope'>
<el-tag
:type='scope.row.tag === "家" ? "primary" : (scope.row.tag === "公司" ? "danger" : scope.row.tag === "超市" ? "info" : "success")'
disable-transitions
>
{{ scope.row.tag }}
</el-tag>
</template>
</el-table-column>
</el-table>
</table-search>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import TableSearch from '/@/components/TableSearch/index.vue'
import CardList from '/@/components/CardList/CardList.vue'
import CardListItem from '/@/components/CardList/CardListItem.vue'
import { getTableList, ITag } from '/@/api/components/index'
import { format, tableSummaries } from '/@/utils/index'
import { validate } from '/@/utils/formExtend'
import { IRenderTableList } from '/@/type/views/Components/TableSearchTest'
interface ISearchForm {
date: string
name: string
address: string
tag: ITag
}
// 键值对样式,及表单校验
const search = (table: ITable<IRenderTableList>, form: ISearchForm) => {
const rules = reactive({})
const refForm = ref(null)
const submit = async() => {
if(!await validate(refForm)) return
table.page = 1
renderTableList(table, form)
}
return {
rules,
submit,
refForm
}
}
const renderTableList = async(table: ITable<IRenderTableList>, form: ISearchForm) => {
const d = await getTableList({ page: table.page, size: table.size, tag: form.tag })
table.data = d.data.Data.data
table.total = d.data.Data.total
}
const tableRender = (table: ITable<IRenderTableList>, form: ISearchForm) => {
renderTableList(table, form)
const handleSizeChange = (v: number) => (table.size = v) && renderTableList(table, form)
const handleCurrentChange = (v: number) => (table.page = v) && renderTableList(table, form)
const indexMethod = (index: number) => (table.page - 1) * table.size + index + 1
const getSummaries = tableSummaries
return { table, handleSizeChange, handleCurrentChange, indexMethod, getSummaries }
}
export default defineComponent({
name: 'TableSearchTest',
components: {
TableSearch,
CardList,
CardListItem
},
setup() {
const form: ISearchForm = reactive({
date: '',
name: '',
address: '',
tag: '所有'
})
// const
const tableData: ITable<IRenderTableList> = reactive({
data : [],
total: 0,
page: 1,
size: 10
})
return {
form,
tableData,
format,
...tableRender(tableData, form),
...search(tableData, form)
}
}
})
</script>
This diff is collapsed.
<template>
<div>
404 page
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: '404'
})
</script>
\ No newline at end of file
<template>
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'SecondNav'
})
</script>
<template>
<div>三级导航</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ThirdNav'
})
</script>
<template>
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'SecondText'
})
</script>
<template>
<div>
ThirdText
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ThirdText'
})
</script>
\ No newline at end of file
<template>
<div>
<el-row
v-action='"add"'
class='mb-1'
>
<el-button type='primary'>
添加权限
</el-button>
<el-tag class='ml-1'>
v-action='"add"'
</el-tag>
</el-row>
<el-row
v-if='checkPermission("add")'
class='mb-1'
>
<el-button type='primary'>
添加权限
</el-button>
<el-tag class='ml-1'>
v-if='checkPermission("add")'
</el-tag>
</el-row>
<el-row
v-action='"update"'
class='mb-1'
>
<el-button type='primary'>
修改权限
</el-button>
<el-tag class='ml-1'>
v-action='"update"'
</el-tag>
</el-row>
<el-row
v-action='"remove"'
class='mb-1'
>
<el-button type='primary'>
删除权限
</el-button>
<el-tag class='ml-1'>
v-action='"remove"'
</el-tag>
</el-row>
<el-row
v-action='["add", "update", "remove"]'
class='mb-1'
>
<el-button type='primary'>
添加,编辑,删除权限(或者关系,满足一个就可以显示)
</el-button>
<el-tag class='ml-1'>
v-action='["add", "update", "remove"]'
</el-tag>
</el-row>
<el-row
v-action:and='["add", "update", "remove"]'
class='mb-1'
>
<el-button type='primary'>
添加,编辑,删除权限(并且关系,全部满足才能显示)
</el-button>
<el-tag class='ml-1'>
v-action:and='["add", "update", "remove"]'
</el-tag>
</el-row>
<el-row>
<el-button
type='primary'
@click='logout'
>
退出并切换用户
</el-button>
</el-row>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from '/@/store/index'
import { checkPermission } from '/@/utils/permission'
export default defineComponent({
name: 'Directive',
setup() {
const store = useStore()
const logout = () => store.commit('layout/logout')
return {
logout,
checkPermission
}
}
})
</script>
<template>
<div id='dynamicexample'>
<h2>项目详情</h2>
<p>项目编码:{{ projName }}</p>
<el-input v-model='input' />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'ProjectDetail',
setup() {
const route = useRoute()
return {
projName: route.params.projName,
input: ref('')
}
}
})
</script>
<template>
<div>
<p style='height: 1500px;'>
高度超出,滚动条测试
</p>
<span>aa</span>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'ProjectImport',
setup() {
const input = ref('')
return {
input
}
}
})
</script>
<template>
<div>
<el-table
:data='list.tableData'
border
style='width: 100%;'
>
<el-table-column
fixed
prop='date'
label='日期'
/>
<el-table-column
prop='name'
label='姓名'
/>
<el-table-column
prop='province'
label='省份'
/>
<el-table-column
fixed='right'
label='操作'
>
<template #default='scope'>
<el-button
type='text'
size='small'
>
<router-link :to='"/Project/ProjectDetail/" + scope.row.projName'>
编辑
</router-link>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
export default defineComponent({
name: 'ProjectList',
setup() {
const list = reactive({
tableData: [{
date: '2016-05-02',
name: '王小虎',
province: '上海',
projName: '001'
}, {
date: '2016-05-04',
name: '王小虎',
province: '上海',
projName: '002'
}, {
date: '2016-05-01',
name: '王小虎',
province: '上海',
projName: '003'
}, {
date: '2016-05-03',
name: '王小虎',
province: '上海',
projName: '004'
}, {
date: '2016-05-03',
name: '王小虎',
province: '上海',
projName: '005'
}]
})
const handleClick = (row: any) => {
console.log(row)
}
return {
list,
handleClick
}
}
})
</script>
<template>
<div
class='layout-login'
@keyup='enterSubmit'
>
<el-form
ref='ruleForm'
label-position='right'
label-width='80px'
:model='form'
:rules='rules'
>
<el-form-item
label='用户名'
prop='name'
>
<el-input v-model='form.name' />
</el-form-item>
<el-form-item
label='密码'
prop='pwd'
>
<el-input
v-model='form.pwd'
type='password'
autocomplete='off'
/>
</el-form-item>
<el-form-item>
<el-button
type='primary'
@click='onSubmit'
>
登录
</el-button>
<el-button @click='resetFields(ruleForm)'>
重置
</el-button>
</el-form-item>
<el-form-item>
<p class='leading-5'>
账号: admin 密码: admin
</p>
<p class='leading-5'>
账号: dev 密码: dev
</p>
<p class='leading-5'>
账号: test 密码: test
</p>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import { store } from '/@/store/index'
import { ElNotification } from 'element-plus'
import { validate, resetFields } from '/@/utils/formExtend'
const formRender = () => {
let form = reactive({
name: 'admin',
pwd: 'admin'
})
const ruleForm = ref(null)
const enterSubmit = (e:KeyboardEvent) => {
if(e.key === 'Enter') {
onSubmit()
}
}
const onSubmit = async() => {
let { name, pwd } = form
if(!await validate(ruleForm)) return
await store.dispatch('layout/login', { username: name, password: pwd })
ElNotification({
title: '欢迎',
message: '欢迎回来',
type: 'success'
})
}
const rules = reactive({
name: [
{ validator: (rule: any, value: any, callback: (arg0?: Error|undefined) => void) => {
if (!value) {
return callback(new Error('用户名不能为空'))
}
callback()
}, trigger: 'blur'
}
],
pwd: [
{ validator: (rule: any, value: any, callback: (arg0?: Error|undefined) => void) => {
if (!value) {
return callback(new Error('密码不能为空'))
}
callback()
}, trigger: 'blur'
}
]
})
return {
form,
onSubmit,
enterSubmit,
rules,
resetFields,
ruleForm
}
}
export default defineComponent({
name: 'Login',
setup() {
return {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
...formRender()
}
}
})
</script>
<style scoped>
.layout-login {
padding-top: 200px;
width: 400px;
margin: 0 auto;
}
</style>
\ No newline at end of file
module.exports = {
purge: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {}
},
variants: {},
plugins: []
}
\ No newline at end of file
import { mount, VueWrapper } from '@vue/test-utils'
import { nextTick, ComponentPublicInstance, ref } from 'vue'
import CardList from '/@/components/CardList/CardList.vue'
import CardListItem from '/@/components/CardList/CardListItem.vue'
import ElementPlus from 'element-plus'
describe('CardList.vue', () => {
const createCardList = function(props:string, opts:IObject<any> | null, slot?:string): VueWrapper<ComponentPublicInstance> {
return mount(Object.assign({
components: {
CardList,
CardListItem
},
template: `
<card-list
${props}
>${slot}</card-list>
`
}, opts), {
global: {
plugins: [ElementPlus]
}
})
}
const listItem = ref([
{ text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21', url: 'http://baidu.com', target: '_blank' },
{ text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21' },
{ text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21' }
])
it('show title', async() => {
const wrapper: VueWrapper<ComponentPublicInstance> = createCardList(
':list-item="listItem" :show-header="true" title="显示标题"',
{
setup() {
return { listItem }
}
}
)
await nextTick()
expect(wrapper.find('.card-list .el-card__header>div>span').text()).toEqual('显示标题')
})
it('hide liststyle', async() => {
const wrapper: VueWrapper<ComponentPublicInstance> = createCardList(
':list-item="listItem" :show-liststyle="false"',
{
setup() {
return { listItem }
}
}
)
await nextTick()
expect(wrapper.find('.card-list .card-list-body .card-list-item-circle').exists()).toBe(false)
})
it('wrap', async() => {
const wrapper: VueWrapper<ComponentPublicInstance> = createCardList(
':list-item="listItem" :is-nowrap="false"',
{
setup() {
return { listItem }
}
}
)
await nextTick()
expect(wrapper.find('.card-list .card-list-body .card-list-text').classes()).toContain('wrap')
})
it('hide liststyle', async() => {
const wrapper: VueWrapper<ComponentPublicInstance> = createCardList(
':list-item="listItem" :show-liststyle="false"',
{
setup() {
return { listItem }
}
}
)
await nextTick()
expect(wrapper.find('.card-list .card-list-body .card-list-item-circle').exists()).toBe(false)
})
it('keyvalue', async() => {
const wrapper: VueWrapper<ComponentPublicInstance> = createCardList(
'type="keyvalue"',
{
setup() {
return { }
}
},
`
<template #keyvalue>
<card-list-item>
<template #key>
申请单号
</template>
<template #value>
2020001686
</template>
</card-list-item>
</template>
`
)
await nextTick()
expect(wrapper.find('.card-list .card-list-item .text-right span').text()).toEqual(':')
expect(wrapper.find('.card-list .card-list-item .font-semibold.truncate').text()).toEqual('2020001686')
})
})
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment