update: store、router使用ts重写;新增示例
This commit is contained in:
parent
7640cfa278
commit
80f9a724df
6
auto-imports.d.ts
vendored
Normal file
6
auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Generated by 'unplugin-auto-import'
|
||||
export {}
|
||||
declare global {
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
}
|
69
components.d.ts
vendored
Normal file
69
components.d.ts
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
Breadcrumb: typeof import('./src/components/Breadcrumb/index.vue')['default']
|
||||
DropdownMenu: typeof import('./src/components/Share/DropdownMenu.vue')['default']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRate: typeof import('element-plus/es')['ElRate']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
GithubCorner: typeof import('./src/components/GithubCorner/index.vue')['default']
|
||||
Hamburger: typeof import('./src/components/Hamburger/index.vue')['default']
|
||||
HeaderSearch: typeof import('./src/components/HeaderSearch/index.vue')['default']
|
||||
Keyboard: typeof import('./src/components/Charts/Keyboard.vue')['default']
|
||||
LineMarker: typeof import('./src/components/Charts/LineMarker.vue')['default']
|
||||
Mallki: typeof import('./src/components/TextHoverEffect/Mallki.vue')['default']
|
||||
MixChart: typeof import('./src/components/Charts/MixChart.vue')['default']
|
||||
Pagination: typeof import('./src/components/Pagination/index.vue')['default']
|
||||
PanThumb: typeof import('./src/components/PanThumb/index.vue')['default']
|
||||
RightPanel: typeof import('./src/components/RightPanel/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Screenfull: typeof import('./src/components/Screenfull/index.vue')['default']
|
||||
SizeSelect: typeof import('./src/components/SizeSelect/index.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
|
||||
VueCountTo: typeof import('./src/components/vue-count-to/vue-countTo.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
@ -4,7 +4,9 @@ export const constantRoutes = [
|
||||
{
|
||||
path: '/redirect',
|
||||
component: 'layout/Layout',
|
||||
hidden: true,
|
||||
meta: {
|
||||
hidden: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path*',
|
||||
@ -15,22 +17,30 @@ export const constantRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: 'views/login/index',
|
||||
hidden: true
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/auth-redirect',
|
||||
component: 'views/login/auth-redirect',
|
||||
hidden: true
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
component: 'views/error-page/404',
|
||||
hidden: true
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/401',
|
||||
component: 'views/error-page/401',
|
||||
hidden: true
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
@ -77,8 +87,8 @@ export const asyncRoutes = [
|
||||
path: '/permission',
|
||||
component: 'layout/Layout',
|
||||
redirect: '/permission/index',
|
||||
alwaysShow: true,
|
||||
meta: {
|
||||
alwaysShow: true,
|
||||
title: 'Permission',
|
||||
icon: 'lock',
|
||||
roles: ['admin', 'editor']
|
||||
@ -333,8 +343,7 @@ export const asyncRoutes = [
|
||||
path: 'edit/:id(\\d+)',
|
||||
component: 'views/example/edit',
|
||||
name: 'EditArticle',
|
||||
meta: { title: 'Edit Article', noCache: true },
|
||||
hidden: true
|
||||
meta: { hidden: true, title: 'Edit Article', noCache: true }
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
@ -438,8 +447,7 @@ export const asyncRoutes = [
|
||||
path: '/zip',
|
||||
component: 'layout/Layout',
|
||||
redirect: '/zip/download',
|
||||
alwaysShow: true,
|
||||
meta: { title: 'Zip', icon: 'zip' },
|
||||
meta: { alwaysShow: true, title: 'Zip', icon: 'zip' },
|
||||
children: [
|
||||
{
|
||||
path: 'download',
|
||||
@ -466,7 +474,9 @@ export const asyncRoutes = [
|
||||
{
|
||||
path: '/pdf/download',
|
||||
component: 'views/pdf/download',
|
||||
hidden: true
|
||||
meta: {
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
@ -521,5 +531,5 @@ export const asyncRoutes = [
|
||||
]
|
||||
},
|
||||
|
||||
{ path: '*', redirect: '/404', hidden: true }
|
||||
{ path: '*', redirect: '/404', meta: { hidden: true }}
|
||||
];
|
||||
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -5901,6 +5901,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
|
@ -22,6 +22,7 @@
|
||||
"path-to-regexp": "6.2.1",
|
||||
"pinia": "2.0.28",
|
||||
"sass": "1.56.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"vue": "3.2.45",
|
||||
"vue-router": "4.1.6"
|
||||
},
|
||||
|
@ -16,9 +16,10 @@ export function getInfo(token) {
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function logout(token) {
|
||||
return request({
|
||||
url: '/vue-element-admin/user/logout',
|
||||
method: 'post'
|
||||
method: 'post',
|
||||
params: { token }
|
||||
});
|
||||
}
|
||||
|
148
src/components/Charts/Keyboard.vue
Normal file
148
src/components/Charts/Keyboard.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div :id="id" :class="className" :style="{height:height,width:width}" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import resize from './mixins/resize';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
|
||||
const xAxisData = [];
|
||||
const data = [];
|
||||
const data2 = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
xAxisData.push(i);
|
||||
data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5);
|
||||
data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3);
|
||||
}
|
||||
this.chart.setOption({
|
||||
backgroundColor: '#08263a',
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%'
|
||||
},
|
||||
xAxis: [{
|
||||
show: false,
|
||||
data: xAxisData
|
||||
}, {
|
||||
show: false,
|
||||
data: xAxisData
|
||||
}],
|
||||
visualMap: {
|
||||
show: false,
|
||||
min: 0,
|
||||
max: 50,
|
||||
dimension: 0,
|
||||
inRange: {
|
||||
color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#4a657a'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#08263f'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'back',
|
||||
type: 'bar',
|
||||
data: data2,
|
||||
z: 1,
|
||||
itemStyle: {
|
||||
opacity: 0.4,
|
||||
barBorderRadius: 5,
|
||||
shadowBlur: 3,
|
||||
shadowColor: '#111'
|
||||
}
|
||||
}, {
|
||||
name: 'Simulate Shadow',
|
||||
type: 'line',
|
||||
data,
|
||||
z: 2,
|
||||
showSymbol: false,
|
||||
animationDelay: 0,
|
||||
animationEasing: 'linear',
|
||||
animationDuration: 1200,
|
||||
lineStyle: {
|
||||
color: 'transparent'
|
||||
},
|
||||
areaStyle: {
|
||||
color: '#08263a',
|
||||
shadowBlur: 50,
|
||||
shadowColor: '#000'
|
||||
}
|
||||
}, {
|
||||
name: 'front',
|
||||
type: 'bar',
|
||||
data,
|
||||
xAxisIndex: 1,
|
||||
z: 3,
|
||||
itemStyle: {
|
||||
barBorderRadius: 5
|
||||
}
|
||||
}],
|
||||
animationEasing: 'elasticOut',
|
||||
animationEasingUpdate: 'elasticOut',
|
||||
animationDelay(idx) {
|
||||
return idx * 20;
|
||||
},
|
||||
animationDelayUpdate(idx) {
|
||||
return idx * 20;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
208
src/components/Charts/LineMarker.vue
Normal file
208
src/components/Charts/LineMarker.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div :id="id" :class="className" :style="{height:height,width:width}" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import resize from './mixins/resize';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
|
||||
this.chart.setOption({
|
||||
backgroundColor: '#394056',
|
||||
title: {
|
||||
top: 20,
|
||||
text: 'Requests',
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 16,
|
||||
color: '#F1F1F3'
|
||||
},
|
||||
left: '1%'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 20,
|
||||
icon: 'rect',
|
||||
itemWidth: 14,
|
||||
itemHeight: 5,
|
||||
itemGap: 13,
|
||||
data: ['CMCC', 'CTCC', 'CUCC'],
|
||||
right: '4%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#F1F1F3'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 100,
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
},
|
||||
data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
name: '(%)',
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 10,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
}
|
||||
}],
|
||||
series: [{
|
||||
name: 'CMCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(137, 189, 27, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(137, 189, 27, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgb(137,189,27)',
|
||||
borderColor: 'rgba(137,189,2,0.27)',
|
||||
borderWidth: 12
|
||||
},
|
||||
data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
|
||||
}, {
|
||||
name: 'CTCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 136, 212, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(0, 136, 212, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgb(0,136,212)',
|
||||
borderColor: 'rgba(0,136,212,0.2)',
|
||||
borderWidth: 12
|
||||
},
|
||||
data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
|
||||
}, {
|
||||
name: 'CUCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 1
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(219, 50, 51, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(219, 50, 51, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'rgb(219,50,51)',
|
||||
borderColor: 'rgba(219,50,51,0.2)',
|
||||
borderWidth: 12
|
||||
},
|
||||
data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
266
src/components/Charts/MixChart.vue
Normal file
266
src/components/Charts/MixChart.vue
Normal file
@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div :id="id" :class="className" :style="{height:height,width:width}" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import resize from './mixins/resize';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id));
|
||||
const xData = (function() {
|
||||
const data = [];
|
||||
for (let i = 1; i < 13; i++) {
|
||||
data.push(i + 'month');
|
||||
}
|
||||
return data;
|
||||
}());
|
||||
this.chart.setOption({
|
||||
backgroundColor: '#344b58',
|
||||
title: {
|
||||
text: 'statistics',
|
||||
x: '20',
|
||||
top: '20',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: '22'
|
||||
},
|
||||
subtextStyle: {
|
||||
color: '#90979c',
|
||||
fontSize: '16'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
borderWidth: 0,
|
||||
top: 150,
|
||||
bottom: 95,
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
x: '5%',
|
||||
top: '10%',
|
||||
textStyle: {
|
||||
color: '#90979c'
|
||||
},
|
||||
data: ['female', 'male', 'average']
|
||||
},
|
||||
calculable: true,
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#90979c'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitArea: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0
|
||||
|
||||
},
|
||||
data: xData
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#90979c'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0
|
||||
},
|
||||
splitArea: {
|
||||
show: false
|
||||
}
|
||||
}],
|
||||
dataZoom: [{
|
||||
show: true,
|
||||
height: 30,
|
||||
xAxisIndex: [
|
||||
0
|
||||
],
|
||||
bottom: 30,
|
||||
start: 10,
|
||||
end: 80,
|
||||
handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
|
||||
handleSize: '110%',
|
||||
handleStyle: {
|
||||
color: '#d3dee5'
|
||||
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff' },
|
||||
borderColor: '#90979c'
|
||||
|
||||
}, {
|
||||
type: 'inside',
|
||||
show: true,
|
||||
height: 15,
|
||||
start: 1,
|
||||
end: 35
|
||||
}],
|
||||
series: [{
|
||||
name: 'female',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barMaxWidth: 35,
|
||||
barGap: '10%',
|
||||
itemStyle: {
|
||||
color: 'rgba(255,144,128,1)',
|
||||
label: {
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
position: 'insideTop',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
709,
|
||||
1917,
|
||||
2455,
|
||||
2610,
|
||||
1719,
|
||||
1433,
|
||||
1544,
|
||||
3285,
|
||||
5208,
|
||||
3372,
|
||||
2484,
|
||||
4078
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'male',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
itemStyle: {
|
||||
color: 'rgba(0,191,183,1)',
|
||||
barBorderRadius: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
327,
|
||||
1776,
|
||||
507,
|
||||
1200,
|
||||
800,
|
||||
482,
|
||||
204,
|
||||
1390,
|
||||
1001,
|
||||
951,
|
||||
381,
|
||||
220
|
||||
]
|
||||
}, {
|
||||
name: 'average',
|
||||
type: 'line',
|
||||
stack: 'total',
|
||||
symbolSize: 10,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: 'rgba(252,230,48,1)',
|
||||
barBorderRadius: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : '';
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
1036,
|
||||
3693,
|
||||
2962,
|
||||
3810,
|
||||
2519,
|
||||
1915,
|
||||
1748,
|
||||
4675,
|
||||
6209,
|
||||
4323,
|
||||
2865,
|
||||
4298
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
56
src/components/Charts/mixins/resize.js
Normal file
56
src/components/Charts/mixins/resize.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { debounce } from '@/utils';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
$_sidebarElm: null,
|
||||
$_resizeHandler: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initListener();
|
||||
},
|
||||
activated() {
|
||||
if (!this.$_resizeHandler) {
|
||||
// avoid duplication init
|
||||
this.initListener();
|
||||
}
|
||||
|
||||
// when keep-alive chart activated, auto resize
|
||||
this.resize();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyListener();
|
||||
},
|
||||
deactivated() {
|
||||
this.destroyListener();
|
||||
},
|
||||
methods: {
|
||||
// use $_ for mixins properties
|
||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
||||
$_sidebarResizeHandler(e) {
|
||||
if (e.propertyName === 'width') {
|
||||
this.$_resizeHandler();
|
||||
}
|
||||
},
|
||||
initListener() {
|
||||
this.$_resizeHandler = debounce(() => {
|
||||
this.resize();
|
||||
}, 100);
|
||||
window.addEventListener('resize', this.$_resizeHandler);
|
||||
|
||||
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0];
|
||||
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler);
|
||||
},
|
||||
destroyListener() {
|
||||
window.removeEventListener('resize', this.$_resizeHandler);
|
||||
this.$_resizeHandler = null;
|
||||
|
||||
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler);
|
||||
},
|
||||
resize() {
|
||||
const { chart } = this;
|
||||
chart && chart.resize();
|
||||
}
|
||||
}
|
||||
};
|
@ -95,7 +95,7 @@ export default defineComponent({
|
||||
|
||||
for (const router of routes) {
|
||||
// skip hidden router
|
||||
if (router.hidden) { continue; }
|
||||
if (router.meta && router.meta.hidden) { continue; }
|
||||
|
||||
const data = {
|
||||
path: path.resolve(basePath, router.path),
|
||||
|
102
src/components/Pagination/index.vue
Normal file
102
src/components/Pagination/index.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div :class="{'hidden':hidden}" class="pagination-container">
|
||||
<el-pagination
|
||||
:background="background"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:total="total"
|
||||
v-bind="$attrs"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { scrollTo } from '@/utils/scroll-to';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
total: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'total, sizes, prev, pager, next, jumper'
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.page;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:page', val);
|
||||
}
|
||||
},
|
||||
pageSize: {
|
||||
get() {
|
||||
return this.limit;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:limit', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSizeChange(val) {
|
||||
this.$emit('pagination', { page: this.currentPage, limit: val });
|
||||
if (this.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.$emit('pagination', { page: val, limit: this.pageSize });
|
||||
if (this.autoScroll) {
|
||||
scrollTo(0, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
background: #fff;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.pagination-container.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
13
src/directive/waves/index.js
Normal file
13
src/directive/waves/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
import waves from './waves';
|
||||
|
||||
const install = function(Vue) {
|
||||
Vue.directive('waves', waves);
|
||||
};
|
||||
|
||||
if (window.Vue) {
|
||||
window.waves = waves;
|
||||
Vue.use(install); // eslint-disable-line
|
||||
}
|
||||
|
||||
waves.install = install;
|
||||
export default waves;
|
26
src/directive/waves/waves.css
Normal file
26
src/directive/waves/waves.css
Normal file
@ -0,0 +1,26 @@
|
||||
.waves-ripple {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
background-clip: padding-box;
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-transform: scale(0);
|
||||
-ms-transform: scale(0);
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.waves-ripple.z-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(2);
|
||||
-ms-transform: scale(2);
|
||||
transform: scale(2);
|
||||
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
|
||||
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
|
||||
}
|
72
src/directive/waves/waves.js
Normal file
72
src/directive/waves/waves.js
Normal file
@ -0,0 +1,72 @@
|
||||
import './waves.css';
|
||||
|
||||
const context = '@@wavesContext';
|
||||
|
||||
function handleClick(el, binding) {
|
||||
function handle(e) {
|
||||
const customOpts = Object.assign({}, binding.value);
|
||||
const opts = Object.assign({
|
||||
ele: el, // 波纹作用元素
|
||||
type: 'hit', // hit 点击位置扩散 center中心点扩展
|
||||
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
|
||||
},
|
||||
customOpts
|
||||
);
|
||||
const target = opts.ele;
|
||||
if (target) {
|
||||
target.style.position = 'relative';
|
||||
target.style.overflow = 'hidden';
|
||||
const rect = target.getBoundingClientRect();
|
||||
let ripple = target.querySelector('.waves-ripple');
|
||||
if (!ripple) {
|
||||
ripple = document.createElement('span');
|
||||
ripple.className = 'waves-ripple';
|
||||
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px';
|
||||
target.appendChild(ripple);
|
||||
} else {
|
||||
ripple.className = 'waves-ripple';
|
||||
}
|
||||
switch (opts.type) {
|
||||
case 'center':
|
||||
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px';
|
||||
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px';
|
||||
break;
|
||||
default:
|
||||
ripple.style.top =
|
||||
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
|
||||
document.body.scrollTop) + 'px';
|
||||
ripple.style.left =
|
||||
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
|
||||
document.body.scrollLeft) + 'px';
|
||||
}
|
||||
ripple.style.backgroundColor = opts.color;
|
||||
ripple.className = 'waves-ripple z-active';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!el[context]) {
|
||||
el[context] = {
|
||||
removeHandle: handle
|
||||
};
|
||||
} else {
|
||||
el[context].removeHandle = handle;
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
export default {
|
||||
bind(el, binding) {
|
||||
el.addEventListener('click', handleClick(el, binding), false);
|
||||
},
|
||||
update(el, binding) {
|
||||
el.removeEventListener('click', el[context].removeHandle, false);
|
||||
el.addEventListener('click', handleClick(el, binding), false);
|
||||
},
|
||||
unbind(el) {
|
||||
el.removeEventListener('click', el[context].removeHandle, false);
|
||||
el[context] = null;
|
||||
delete el[context];
|
||||
}
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
<!--<template>
|
||||
<svg-icon :icon-class="icon || 'sub-el-icon'" />
|
||||
<template #title>
|
||||
<span class="text">{{ title }}</span>
|
||||
</template>
|
||||
</template> -->
|
||||
|
||||
<script>
|
||||
import { defineComponent, h } from 'vue'; // renderSlot
|
||||
import SvgIcon from '@/components/SvgIcon';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
slots: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const vnodes = [];
|
||||
if (this.icon) {
|
||||
vnodes.push(h(SvgIcon, {
|
||||
'icon-class': this.icon || 'sub-el-icon'
|
||||
}));
|
||||
}
|
||||
// vnodes.push(
|
||||
// h('span', {
|
||||
// 'class': 'text',
|
||||
// 'v-slot': 'title'
|
||||
// }, this.title)
|
||||
// );
|
||||
vnodes.push(
|
||||
h('slot', {
|
||||
'name': 'title'
|
||||
}, this.title)
|
||||
);
|
||||
return vnodes;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sub-el-icon {
|
||||
color: currentColor;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden" class="root-sidebar-item">
|
||||
<div v-if="!isItemHidden" class="root-sidebar-item">
|
||||
<template
|
||||
v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
|
||||
v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !(item.meta && item.meta.alwaysShow)">
|
||||
<app-link class="link" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item class="left-menu-item" v-if="onlyOneChild.meta" :index="resolvePath(onlyOneChild.path)"
|
||||
:class="{ 'submenu-title-noDropdown': !isNest }">
|
||||
@ -64,10 +64,18 @@ export default defineComponent({
|
||||
this.onlyOneChild = null;
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
isItemHidden() {
|
||||
if (this.item.meta && this.item.meta.hidden) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
if (this.isItemHidden) {
|
||||
return false;
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
|
@ -1,50 +1,53 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'; // createWebHashHistory, createWebHistory
|
||||
import type { Router, RouteRecordRaw, RouteComponent } from 'vue-router';
|
||||
|
||||
/* Layout */
|
||||
const Layout = () => import('@/layout');
|
||||
const Layout = ():RouteComponent => import('@/layout/index.vue');
|
||||
|
||||
/* Router Modules */
|
||||
// import componentsRouter from './modules/components';
|
||||
// import chartsRouter from './modules/charts';
|
||||
// import tableRouter from './modules/table';
|
||||
import chartsRouter from './modules/charts';
|
||||
// import nestedRouter from './modules/nested';
|
||||
import tableRouter from './modules/table';
|
||||
|
||||
/**
|
||||
* constantRoutes
|
||||
* a base page that does not have permission requirements
|
||||
* all roles can be accessed
|
||||
*
|
||||
* 注意:hidden、alwaysShow 属性配置移动到了meta中!!!
|
||||
*/
|
||||
export const constantRoutes = [
|
||||
export const constantRoutes:RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/redirect',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: '/redirect/:path(.*)',
|
||||
component: () => import('@/views/redirect/index')
|
||||
component: () => import('@/views/redirect/index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index'),
|
||||
hidden: true
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/auth-redirect',
|
||||
component: () => import('@/views/login/auth-redirect'),
|
||||
hidden: true
|
||||
component: () => import('@/views/login/auth-redirect.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404'),
|
||||
hidden: true
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/401',
|
||||
component: () => import('@/views/error-page/401'),
|
||||
hidden: true
|
||||
component: () => import('@/views/error-page/401.vue'),
|
||||
meta: { hidden: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
@ -53,7 +56,7 @@ export const constantRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('@/views/dashboard/index'),
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
name: 'Dashboard',
|
||||
meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
|
||||
}
|
||||
@ -65,7 +68,7 @@ export const constantRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/documentation/index'),
|
||||
component: () => import('@/views/documentation/index.vue'),
|
||||
name: 'Documentation',
|
||||
meta: { title: 'Documentation', icon: 'documentation', affix: true }
|
||||
}
|
||||
@ -78,7 +81,7 @@ export const constantRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/guide/index'),
|
||||
component: () => import('@/views/guide/index.vue'),
|
||||
name: 'Guide',
|
||||
meta: { title: 'Guide', icon: 'guide', noCache: true }
|
||||
}
|
||||
@ -88,11 +91,11 @@ export const constantRoutes = [
|
||||
path: '/profile',
|
||||
component: Layout,
|
||||
redirect: '/profile/index',
|
||||
hidden: true,
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/profile/index'),
|
||||
component: () => import('@/views/profile/index.vue'),
|
||||
name: 'Profile',
|
||||
meta: { title: 'Profile', icon: 'user', noCache: true }
|
||||
}
|
||||
@ -103,15 +106,17 @@ export const constantRoutes = [
|
||||
/**
|
||||
* asyncRoutes
|
||||
* the routes that need to be dynamically loaded based on user roles
|
||||
*
|
||||
* 注意:hidden、alwaysShow 属性配置移动到了meta中!!!
|
||||
*/
|
||||
export const asyncRoutes = [
|
||||
export const asyncRoutes:RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/permission',
|
||||
component: Layout,
|
||||
redirect: '/permission/page',
|
||||
alwaysShow: true, // will always show the root menu
|
||||
name: 'Permission',
|
||||
meta: {
|
||||
alwaysShow: true, // will always show the root menu
|
||||
title: 'Permission',
|
||||
icon: 'lock',
|
||||
roles: ['admin', 'editor'] // you can set roles in root nav
|
||||
@ -119,7 +124,7 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'page',
|
||||
component: () => import('@/views/permission/page'),
|
||||
component: () => import('@/views/permission/page.vue'),
|
||||
name: 'PagePermission',
|
||||
meta: {
|
||||
title: 'Page Permission',
|
||||
@ -128,7 +133,7 @@ export const asyncRoutes = [
|
||||
},
|
||||
{
|
||||
path: 'directive',
|
||||
component: () => import('@/views/permission/directive'),
|
||||
component: () => import('@/views/permission/directive.vue'),
|
||||
name: 'DirectivePermission',
|
||||
meta: {
|
||||
title: 'Directive Permission'
|
||||
@ -137,7 +142,7 @@ export const asyncRoutes = [
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
component: () => import('@/views/permission/role'),
|
||||
component: () => import('@/views/permission/role.vue'),
|
||||
name: 'RolePermission',
|
||||
meta: {
|
||||
title: 'Role Permission',
|
||||
@ -153,7 +158,7 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/icons/index'),
|
||||
component: () => import('@/views/icons/index.vue'),
|
||||
name: 'Icons',
|
||||
meta: { title: 'Icons', icon: 'icon', noCache: true }
|
||||
}
|
||||
@ -162,9 +167,9 @@ export const asyncRoutes = [
|
||||
|
||||
// /** when your routing map is too long, you can split it into small modules **/
|
||||
// componentsRouter,
|
||||
// chartsRouter,
|
||||
chartsRouter,
|
||||
// nestedRouter,
|
||||
// tableRouter,
|
||||
tableRouter,
|
||||
|
||||
// {
|
||||
// path: '/example',
|
||||
@ -223,13 +228,13 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: '401',
|
||||
component: () => import('@/views/error-page/401'),
|
||||
component: () => import('@/views/error-page/401.vue'),
|
||||
name: 'Page401',
|
||||
meta: { title: '401', noCache: true }
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
component: () => import('@/views/error-page/404'),
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
name: 'Page404',
|
||||
meta: { title: '404', noCache: true }
|
||||
}
|
||||
@ -290,9 +295,8 @@ export const asyncRoutes = [
|
||||
// path: '/zip',
|
||||
// component: Layout,
|
||||
// redirect: '/zip/download',
|
||||
// alwaysShow: true,
|
||||
// name: 'Zip',
|
||||
// meta: { title: 'Zip', icon: 'zip' },
|
||||
// meta: { alwaysShow: true, title: 'Zip', icon: 'zip' },
|
||||
// children: [
|
||||
// {
|
||||
// path: 'download',
|
||||
@ -341,7 +345,7 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/clipboard/index'),
|
||||
component: () => import('@/views/clipboard/index.vue'),
|
||||
name: 'ClipboardDemo',
|
||||
meta: { title: 'Clipboard', icon: 'clipboard' }
|
||||
}
|
||||
@ -354,7 +358,8 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'https://element-plus.midfar.com',
|
||||
meta: { title: 'External Link', icon: 'link' }
|
||||
meta: { title: 'External Link', icon: 'link' },
|
||||
redirect: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -370,13 +375,13 @@ export const asyncRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'element-demo',
|
||||
component: () => import('@/views/mydemo/ElementDemo'),
|
||||
component: () => import('@/views/mydemo/ElementDemo.vue'),
|
||||
name: 'ElementDemo',
|
||||
meta: { title: 'ElementDemo', icon: 'skill' }
|
||||
},
|
||||
{
|
||||
path: 'store-demo',
|
||||
component: () => import('@/views/mydemo/StoreDemo'),
|
||||
component: () => import('@/views/mydemo/StoreDemo.vue'),
|
||||
name: 'StoreDemo',
|
||||
meta: { title: 'StoreDemo', icon: 'lock' }
|
||||
}
|
||||
@ -384,10 +389,10 @@ export const asyncRoutes = [
|
||||
},
|
||||
|
||||
// 404 page must be placed at the end !!!
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/404', hidden: true }
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true }}
|
||||
];
|
||||
|
||||
const createTheRouter = () => createRouter({
|
||||
const createTheRouter = ():Router => createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
// 注意,如果要配置 HTML5 模式,则需要修改nginx配置,参考资料:
|
||||
// https://router.vuejs.org/zh/guide/essentials/history-mode.html
|
||||
@ -396,11 +401,15 @@ const createTheRouter = () => createRouter({
|
||||
routes: constantRoutes
|
||||
});
|
||||
|
||||
const router = createTheRouter();
|
||||
interface RouterPro extends Router {
|
||||
matcher: unknown;
|
||||
}
|
||||
|
||||
const router = createTheRouter() as RouterPro;
|
||||
|
||||
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
|
||||
export function resetRouter() {
|
||||
const newRouter = createTheRouter();
|
||||
const newRouter = createTheRouter() as RouterPro;
|
||||
router.matcher = newRouter.matcher; // reset router
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/** When your routing table is too long, you can split it into small modules**/
|
||||
|
||||
const Layout = () => import('@/layout');
|
||||
const Layout = () => import('@/layout/index.vue');
|
||||
|
||||
const chartsRouter = {
|
||||
path: '/charts',
|
||||
@ -14,19 +14,19 @@ const chartsRouter = {
|
||||
children: [
|
||||
{
|
||||
path: 'keyboard',
|
||||
component: () => import('@/views/charts/keyboard'),
|
||||
component: () => import('@/views/charts/keyboard.vue'),
|
||||
name: 'KeyboardChart',
|
||||
meta: { title: 'Keyboard Chart', noCache: true }
|
||||
},
|
||||
{
|
||||
path: 'line',
|
||||
component: () => import('@/views/charts/line'),
|
||||
component: () => import('@/views/charts/line.vue'),
|
||||
name: 'LineChart',
|
||||
meta: { title: 'Line Chart', noCache: true }
|
||||
},
|
||||
{
|
||||
path: 'mix-chart',
|
||||
component: () => import('@/views/charts/mix-chart'),
|
||||
component: () => import('@/views/charts/mix-chart.vue'),
|
||||
name: 'MixChart',
|
||||
meta: { title: 'Mix Chart', noCache: true }
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/** When your routing table is too long, you can split it into small modules **/
|
||||
|
||||
const Layout = () => import('@/layout');
|
||||
const Layout = () => import('@/layout/index.vue');
|
||||
|
||||
const tableRouter = {
|
||||
path: '/table',
|
||||
@ -14,25 +14,25 @@ const tableRouter = {
|
||||
children: [
|
||||
{
|
||||
path: 'dynamic-table',
|
||||
component: () => import('@/views/table/dynamic-table/index'),
|
||||
component: () => import('@/views/table/dynamic-table/index.vue'),
|
||||
name: 'DynamicTable',
|
||||
meta: { title: 'Dynamic Table' }
|
||||
},
|
||||
{
|
||||
path: 'drag-table',
|
||||
component: () => import('@/views/table/drag-table'),
|
||||
component: () => import('@/views/table/drag-table.vue'),
|
||||
name: 'DragTable',
|
||||
meta: { title: 'Drag Table' }
|
||||
},
|
||||
{
|
||||
path: 'inline-edit-table',
|
||||
component: () => import('@/views/table/inline-edit-table'),
|
||||
component: () => import('@/views/table/inline-edit-table.vue'),
|
||||
name: 'InlineEditTable',
|
||||
meta: { title: 'Inline Edit' }
|
||||
},
|
||||
{
|
||||
path: 'complex-table',
|
||||
component: () => import('@/views/table/complex-table'),
|
||||
component: () => import('@/views/table/complex-table.vue'),
|
||||
name: 'ComplexTable',
|
||||
meta: { title: 'Complex Table' }
|
||||
}
|
@ -1,17 +1,19 @@
|
||||
import { createPinia, acceptHMRUpdate } from 'pinia';
|
||||
import type { StoreDefinition } from 'pinia';
|
||||
// https://webpack.js.org/guides/dependency-management/#requirecontext
|
||||
const modulesFiles = import.meta.globEager('./modules/*.js');
|
||||
const modulesFiles = import.meta.globEager('./modules/*.ts');
|
||||
|
||||
// console.log('modulesFiles=', modulesFiles);
|
||||
|
||||
// you do not need `import app from './modules/app'`
|
||||
// it will auto require all vuex module from modules file
|
||||
const modules = {};
|
||||
const modules:Record<string, StoreDefinition> = {};
|
||||
Object.keys(modulesFiles).forEach((modulePath) => {
|
||||
// console.log('modulePath=', modulePath);
|
||||
// set './modules/app.js' => 'app'
|
||||
const moduleName = modulePath.replace(/^\.\/modules\/(.*)\.\w+$/, '$1');
|
||||
const value = modulesFiles[modulePath];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const value = modulesFiles[modulePath] as any;
|
||||
modules[moduleName] = value.default;
|
||||
}, {});
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const getDefaultState = () => ({
|
||||
sidebar: {
|
||||
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
device: 'desktop',
|
||||
size: Cookies.get('size') || 'medium'
|
||||
});
|
||||
|
||||
const getters = {
|
||||
};
|
||||
|
||||
const actions = {
|
||||
toggleSidebar() {
|
||||
this.sidebar.opened = !this.sidebar.opened;
|
||||
this.sidebar.withoutAnimation = false;
|
||||
if (this.sidebar.opened) {
|
||||
Cookies.set('sidebarStatus', 1);
|
||||
} else {
|
||||
Cookies.set('sidebarStatus', 0);
|
||||
}
|
||||
},
|
||||
closeSidebar({ withoutAnimation }) {
|
||||
Cookies.set('sidebarStatus', 0);
|
||||
this.sidebar.opened = false;
|
||||
this.sidebar.withoutAnimation = withoutAnimation;
|
||||
},
|
||||
toggleDevice(device) {
|
||||
this.device = device;
|
||||
},
|
||||
setSize(size) {
|
||||
this.size = size;
|
||||
Cookies.set('size', size);
|
||||
}
|
||||
};
|
||||
|
||||
export default defineStore({
|
||||
id: 'app',
|
||||
state: getDefaultState,
|
||||
getters,
|
||||
actions
|
||||
});
|
47
src/store/modules/app.ts
Normal file
47
src/store/modules/app.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface IAppState {
|
||||
sidebar: {
|
||||
opened: boolean;
|
||||
withoutAnimation: boolean;
|
||||
};
|
||||
device: 'desktop' | 'mobile';
|
||||
size: string;
|
||||
}
|
||||
|
||||
export default defineStore({
|
||||
id: 'app',
|
||||
state: ():IAppState => ({
|
||||
sidebar: {
|
||||
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
device: 'desktop',
|
||||
size: Cookies.get('size') || 'medium'
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
toggleSidebar() {
|
||||
this.sidebar.opened = !this.sidebar.opened;
|
||||
this.sidebar.withoutAnimation = false;
|
||||
if (this.sidebar.opened) {
|
||||
Cookies.set('sidebarStatus', 1);
|
||||
} else {
|
||||
Cookies.set('sidebarStatus', 0);
|
||||
}
|
||||
},
|
||||
closeSidebar({ withoutAnimation }) {
|
||||
Cookies.set('sidebarStatus', 0);
|
||||
this.sidebar.opened = false;
|
||||
this.sidebar.withoutAnimation = withoutAnimation;
|
||||
},
|
||||
toggleDevice(device) {
|
||||
this.device = device;
|
||||
},
|
||||
setSize(size) {
|
||||
this.size = size;
|
||||
Cookies.set('size', size);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,67 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { asyncRoutes, constantRoutes } from '@/router';
|
||||
|
||||
/**
|
||||
* Use meta.role to determine if the current user has permission
|
||||
* @param roles
|
||||
* @param route
|
||||
*/
|
||||
function hasPermission(roles, route) {
|
||||
if (route.meta && route.meta.roles) {
|
||||
return roles.some(role => route.meta.roles.includes(role));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter asynchronous routing tables by recursion
|
||||
* @param routes asyncRoutes
|
||||
* @param roles
|
||||
*/
|
||||
export function filterAsyncRoutes(routes, roles) {
|
||||
const res = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
const tmp = { ...route };
|
||||
if (hasPermission(roles, tmp)) {
|
||||
if (tmp.children) {
|
||||
tmp.children = filterAsyncRoutes(tmp.children, roles);
|
||||
}
|
||||
res.push(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const getDefaultState = () => ({
|
||||
routes: [],
|
||||
addRoutes: []
|
||||
});
|
||||
|
||||
const getters = {};
|
||||
|
||||
const actions = {
|
||||
setRoutes(routes) {
|
||||
this.addRoutes = routes;
|
||||
this.routes = constantRoutes.concat(routes);
|
||||
},
|
||||
generateRoutes(roles) {
|
||||
let accessedRoutes;
|
||||
if (roles.includes('admin')) {
|
||||
accessedRoutes = asyncRoutes || [];
|
||||
} else {
|
||||
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
||||
}
|
||||
this.setRoutes(accessedRoutes);
|
||||
return accessedRoutes;
|
||||
}
|
||||
};
|
||||
|
||||
export default defineStore({
|
||||
id: 'permission',
|
||||
state: getDefaultState,
|
||||
getters,
|
||||
actions
|
||||
});
|
68
src/store/modules/permission.ts
Normal file
68
src/store/modules/permission.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { asyncRoutes, constantRoutes } from '@/router';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
interface IPermissionState {
|
||||
routes: Array<RouteRecordRaw>;
|
||||
addRoutes: Array<RouteRecordRaw>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use meta.role to determine if the current user has permission
|
||||
* @param roles
|
||||
* @param route
|
||||
*/
|
||||
function hasPermission(roles:string[], route:RouteRecordRaw):boolean {
|
||||
if (route.meta && route.meta.roles) {
|
||||
const rolesArr = route.meta.roles as string[];
|
||||
return roles.some(role => rolesArr.includes(role));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter asynchronous routing tables by recursion
|
||||
* @param routes asyncRoutes
|
||||
* @param roles
|
||||
*/
|
||||
export function filterAsyncRoutes(routes:RouteRecordRaw[], roles: string[]): Array<RouteRecordRaw> {
|
||||
const res:Array<RouteRecordRaw> = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
const tmp = { ...route };
|
||||
if (hasPermission(roles, tmp)) {
|
||||
if (tmp.children) {
|
||||
tmp.children = filterAsyncRoutes(tmp.children, roles);
|
||||
}
|
||||
res.push(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export default defineStore({
|
||||
id: 'permission',
|
||||
state: ():IPermissionState => ({
|
||||
routes: [],
|
||||
addRoutes: []
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
setRoutes(routes: RouteRecordRaw[]) {
|
||||
this.addRoutes = routes;
|
||||
this.routes = constantRoutes.concat(routes);
|
||||
},
|
||||
generateRoutes(roles: string[]) {
|
||||
let accessedRoutes;
|
||||
if (roles.includes('admin')) {
|
||||
accessedRoutes = asyncRoutes || [];
|
||||
} else {
|
||||
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
|
||||
}
|
||||
this.setRoutes(accessedRoutes);
|
||||
return accessedRoutes;
|
||||
}
|
||||
}
|
||||
});
|
@ -1,30 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import defaultSettings from '@/settings';
|
||||
|
||||
const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings;
|
||||
|
||||
const getDefaultState = () => ({
|
||||
theme: '#1890ff',
|
||||
showSettings: showSettings,
|
||||
tagsView: tagsView,
|
||||
fixedHeader: fixedHeader,
|
||||
sidebarLogo: sidebarLogo
|
||||
});
|
||||
|
||||
const getters = {};
|
||||
|
||||
const actions = {
|
||||
changeSetting({ key, value }) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (this.hasOwnProperty(key)) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineStore({
|
||||
id: 'settings',
|
||||
state: getDefaultState,
|
||||
getters,
|
||||
actions
|
||||
});
|
24
src/store/modules/settings.ts
Normal file
24
src/store/modules/settings.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import defaultSettings from '@/settings';
|
||||
|
||||
const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings;
|
||||
|
||||
export default defineStore({
|
||||
id: 'settings',
|
||||
state: () => ({
|
||||
theme: '#1890ff',
|
||||
showSettings: showSettings,
|
||||
tagsView: tagsView,
|
||||
fixedHeader: fixedHeader,
|
||||
sidebarLogo: sidebarLogo
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
changeSetting({ key, value }) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (this.hasOwnProperty(key)) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -1,93 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
const getDefaultState = () => ({
|
||||
visitedViews: [],
|
||||
cachedViews: []
|
||||
});
|
||||
|
||||
const getters = {};
|
||||
|
||||
const actions = {
|
||||
addView(view) {
|
||||
this.addVisitedView(view);
|
||||
this.addCachedView(view);
|
||||
},
|
||||
addVisitedView(view) {
|
||||
if (this.visitedViews.some(v => v.path === view.path)) return;
|
||||
this.visitedViews.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta.title || 'no-name'
|
||||
})
|
||||
);
|
||||
},
|
||||
addCachedView(view) {
|
||||
if (this.cachedViews.includes(view.name)) return;
|
||||
if (!view.meta.noCache) {
|
||||
this.cachedViews.push(view.name);
|
||||
}
|
||||
},
|
||||
delView(view) {
|
||||
this.delVisitedView(view);
|
||||
this.delCachedView(view);
|
||||
},
|
||||
delVisitedView(view) {
|
||||
for (const [i, v] of this.visitedViews.entries()) {
|
||||
if (v.path === view.path) {
|
||||
this.visitedViews.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
delCachedView(view) {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
index > -1 && this.cachedViews.splice(index, 1);
|
||||
},
|
||||
|
||||
delOthersViews(view) {
|
||||
this.delOthersVisitedViews(view);
|
||||
this.delOthersCachedViews(view);
|
||||
},
|
||||
delOthersVisitedViews(view) {
|
||||
this.visitedViews = this.visitedViews.filter(v => {
|
||||
return v.meta.affix || v.path === view.path;
|
||||
});
|
||||
},
|
||||
delOthersCachedViews(view) {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
if (index > -1) {
|
||||
this.cachedViews = this.cachedViews.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
this.cachedViews = [];
|
||||
}
|
||||
},
|
||||
|
||||
delAllViews() {
|
||||
this.delAllVisitedViews();
|
||||
this.delAllCachedViews();
|
||||
},
|
||||
delAllVisitedViews() {
|
||||
// keep affix tags
|
||||
const affixTags = this.visitedViews.filter(tag => tag.meta.affix);
|
||||
this.visitedViews = affixTags;
|
||||
},
|
||||
delAllCachedViews() {
|
||||
this.cachedViews = [];
|
||||
},
|
||||
|
||||
updateVisitedView(view) {
|
||||
for (let v of this.visitedViews) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineStore({
|
||||
id: 'tagsView',
|
||||
state: getDefaultState,
|
||||
getters,
|
||||
actions
|
||||
});
|
93
src/store/modules/tagsView.ts
Normal file
93
src/store/modules/tagsView.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import type { RouteRecord } from 'vue-router';
|
||||
|
||||
interface ITagsViewState {
|
||||
visitedViews: Array<RouteRecord>;
|
||||
cachedViews: Array<RouteRecord>;
|
||||
}
|
||||
|
||||
export default defineStore({
|
||||
id: 'tagsView',
|
||||
state: ():ITagsViewState => ({
|
||||
visitedViews: [],
|
||||
cachedViews: []
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
addView(view) {
|
||||
this.addVisitedView(view);
|
||||
this.addCachedView(view);
|
||||
},
|
||||
addVisitedView(view) {
|
||||
if (this.visitedViews.some(v => v.path === view.path)) return;
|
||||
this.visitedViews.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta.title || 'no-name'
|
||||
})
|
||||
);
|
||||
},
|
||||
addCachedView(view) {
|
||||
if (this.cachedViews.includes(view.name)) return;
|
||||
if (!view.meta.noCache) {
|
||||
this.cachedViews.push(view.name);
|
||||
}
|
||||
},
|
||||
delView(view) {
|
||||
this.delVisitedView(view);
|
||||
this.delCachedView(view);
|
||||
},
|
||||
delVisitedView(view) {
|
||||
for (const [i, v] of this.visitedViews.entries()) {
|
||||
if (v.path === view.path) {
|
||||
this.visitedViews.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
delCachedView(view) {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
index > -1 && this.cachedViews.splice(index, 1);
|
||||
},
|
||||
|
||||
delOthersViews(view) {
|
||||
this.delOthersVisitedViews(view);
|
||||
this.delOthersCachedViews(view);
|
||||
},
|
||||
delOthersVisitedViews(view) {
|
||||
this.visitedViews = this.visitedViews.filter(v => {
|
||||
return v.meta.affix || v.path === view.path;
|
||||
});
|
||||
},
|
||||
delOthersCachedViews(view) {
|
||||
const index = this.cachedViews.indexOf(view.name);
|
||||
if (index > -1) {
|
||||
this.cachedViews = this.cachedViews.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
this.cachedViews = [];
|
||||
}
|
||||
},
|
||||
|
||||
delAllViews() {
|
||||
this.delAllVisitedViews();
|
||||
this.delAllCachedViews();
|
||||
},
|
||||
delAllVisitedViews() {
|
||||
// keep affix tags
|
||||
const affixTags = this.visitedViews.filter(tag => tag.meta.affix);
|
||||
this.visitedViews = affixTags;
|
||||
},
|
||||
delAllCachedViews() {
|
||||
this.cachedViews = [];
|
||||
},
|
||||
|
||||
updateVisitedView(view) {
|
||||
for (let v of this.visitedViews) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -1,118 +0,0 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { login, logout, getInfo } from '@/api/user';
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth';
|
||||
import router, { resetRouter } from '@/router';
|
||||
import tagsViewStore from './tagsView';
|
||||
import permissionStore from './permission';
|
||||
|
||||
const getDefaultState = () => ({
|
||||
token: getToken(),
|
||||
name: '',
|
||||
avatar: '',
|
||||
introduction: '',
|
||||
roles: []
|
||||
});
|
||||
|
||||
const getters = {};
|
||||
|
||||
const actions = {
|
||||
// user login
|
||||
login(userInfo) {
|
||||
const { username, password } = userInfo;
|
||||
return new Promise((resolve, reject) => {
|
||||
login({ username: username.trim(), password: password }).then(response => {
|
||||
const { data } = response;
|
||||
this.token = data.token;
|
||||
setToken(data.token);
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// get user info
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInfo(this.token).then(response => {
|
||||
const { data } = response;
|
||||
|
||||
if (!data) {
|
||||
reject('Verification failed, please Login again.');
|
||||
}
|
||||
|
||||
const { roles, name, avatar, introduction } = data;
|
||||
|
||||
// roles must be a non-empty array
|
||||
if (!roles || roles.length <= 0) {
|
||||
reject('getInfo: roles must be a non-null array!');
|
||||
}
|
||||
|
||||
this.roles = roles;
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.introduction = introduction;
|
||||
resolve(data);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// user logout
|
||||
logout() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logout(this.token).then(() => {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
removeToken();
|
||||
resetRouter();
|
||||
|
||||
// reset visited views and cached views
|
||||
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
|
||||
tagsViewStore().delAllViews();
|
||||
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// remove token
|
||||
resetToken() {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
removeToken();
|
||||
},
|
||||
|
||||
// dynamically modify permissions
|
||||
async changeRoles(role) {
|
||||
const token = role + '-token';
|
||||
|
||||
this.token = token;
|
||||
setToken(token);
|
||||
|
||||
const { roles } = await this.getInfo({ token });
|
||||
|
||||
resetRouter();
|
||||
|
||||
// generate accessible routes map based on roles
|
||||
const accessRoutes = await permissionStore().generateRoutes(roles);
|
||||
// dynamically add accessible routes
|
||||
// router.addRoutes(accessRoutes);
|
||||
accessRoutes.forEach(item => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
|
||||
// reset visited views and cached views
|
||||
tagsViewStore().delAllViews();
|
||||
}
|
||||
};
|
||||
|
||||
export default defineStore({
|
||||
id: 'user',
|
||||
state: getDefaultState,
|
||||
getters,
|
||||
actions
|
||||
});
|
125
src/store/modules/user.ts
Normal file
125
src/store/modules/user.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { login as apiLogin, logout as apiLogout, getInfo as apiGetInfo } from '@/api/user';
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth';
|
||||
import router, { resetRouter } from '@/router';
|
||||
import tagsViewStore from './tagsView';
|
||||
import permissionStore from './permission';
|
||||
|
||||
interface IUserState {
|
||||
token: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
introduction: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export default defineStore({
|
||||
id: 'user',
|
||||
state: ():IUserState => ({
|
||||
token: getToken(),
|
||||
name: '',
|
||||
avatar: '',
|
||||
introduction: '',
|
||||
roles: []
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// user login
|
||||
login(userInfo):Promise<void> {
|
||||
const { username, password } = userInfo;
|
||||
return new Promise((resolve, reject) => {
|
||||
apiLogin({ username: username.trim(), password: password }).then(response => {
|
||||
const { data } = response;
|
||||
this.token = data.token;
|
||||
setToken(data.token);
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// get user info
|
||||
getInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiGetInfo(this.token).then(response => {
|
||||
const { data } = response;
|
||||
|
||||
if (!data) {
|
||||
reject('Verification failed, please Login again.');
|
||||
}
|
||||
|
||||
const { roles, name, avatar, introduction } = data;
|
||||
|
||||
// roles must be a non-empty array
|
||||
if (!roles || roles.length <= 0) {
|
||||
reject('getInfo: roles must be a non-null array!');
|
||||
}
|
||||
|
||||
this.roles = roles;
|
||||
this.name = name;
|
||||
this.avatar = avatar;
|
||||
this.introduction = introduction;
|
||||
resolve(data);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// user logout
|
||||
logout():Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiLogout(this.token).then(() => {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
removeToken();
|
||||
resetRouter();
|
||||
|
||||
// reset visited views and cached views
|
||||
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
|
||||
tagsViewStore().delAllViews();
|
||||
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// remove token
|
||||
resetToken() {
|
||||
this.token = '';
|
||||
this.roles = [];
|
||||
removeToken();
|
||||
},
|
||||
|
||||
// dynamically modify permissions
|
||||
async changeRoles(role) {
|
||||
const token = role + '-token';
|
||||
|
||||
this.token = token;
|
||||
setToken(token);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const infoRes = await this.getInfo() as any;
|
||||
let roles = [];
|
||||
if (infoRes.roles) {
|
||||
roles = infoRes.roles;
|
||||
}
|
||||
|
||||
resetRouter();
|
||||
|
||||
// generate accessible routes map based on roles
|
||||
const accessRoutes = await permissionStore().generateRoutes(roles);
|
||||
// dynamically add accessible routes
|
||||
// router.addRoutes(accessRoutes);
|
||||
accessRoutes.forEach(item => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
|
||||
// reset visited views and cached views
|
||||
tagsViewStore().delAllViews();
|
||||
}
|
||||
}
|
||||
});
|
24
src/views/charts/keyboard.vue
Normal file
24
src/views/charts/keyboard.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<chart height="100%" width="100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import Chart from '@/components/Charts/Keyboard';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'KeyboardChart',
|
||||
components: { Chart }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
</style>
|
||||
|
24
src/views/charts/line.vue
Normal file
24
src/views/charts/line.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<chart height="100%" width="100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import Chart from '@/components/Charts/LineMarker';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LineChart',
|
||||
components: { Chart }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
</style>
|
||||
|
24
src/views/charts/mix-chart.vue
Normal file
24
src/views/charts/mix-chart.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<chart height="100%" width="100%" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import Chart from '@/components/Charts/MixChart';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MixChart',
|
||||
components: { Chart }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
</style>
|
||||
|
@ -113,11 +113,11 @@ export default defineComponent({
|
||||
|
||||
for (let route of routes) {
|
||||
// skip some route
|
||||
if (route.hidden) { continue; }
|
||||
if (route.meta && route.meta.hidden) { continue; }
|
||||
|
||||
const onlyOneShowingChild = this.onlyOneShowingChild(route.children, route);
|
||||
|
||||
if (route.children && onlyOneShowingChild && !route.alwaysShow) {
|
||||
if (route.children && onlyOneShowingChild && !(route.meta && route.meta.alwaysShow)) {
|
||||
route = onlyOneShowingChild;
|
||||
}
|
||||
|
||||
@ -237,7 +237,12 @@ export default defineComponent({
|
||||
// reference: src/view/layout/components/Sidebar/SidebarItem.vue
|
||||
onlyOneShowingChild(children = [], parent) {
|
||||
let onlyOneChild = null;
|
||||
const showingChildren = children.filter(item => !item.hidden);
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.meta && item.meta.hidden) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// When there is only one child route, the child route is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
|
388
src/views/table/complex-table.vue
Normal file
388
src/views/table/complex-table.vue
Normal file
@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-input v-model="listQuery.title" placeholder="Title" style="width: 200px;" class="filter-item" @keyup.enter="handleFilter" />
|
||||
<el-select v-model="listQuery.importance" placeholder="Imp" clearable style="width: 90px" class="filter-item">
|
||||
<el-option v-for="item in importanceOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
<el-select v-model="listQuery.type" placeholder="Type" clearable class="filter-item" style="width: 130px">
|
||||
<el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name+'('+item.key+')'" :value="item.key" />
|
||||
</el-select>
|
||||
<el-select v-model="listQuery.sort" style="width: 140px" class="filter-item" @change="handleFilter">
|
||||
<el-option v-for="item in sortOptions" :key="item.key" :label="item.label" :value="item.key" />
|
||||
</el-select>
|
||||
<el-button v-waves class="filter-item" type="primary" :icon="iconSearch" @click="handleFilter">
|
||||
Search
|
||||
</el-button>
|
||||
<el-button class="filter-item" style="margin-left: 10px;" type="primary" :icon="iconEdit" @click="handleCreate">
|
||||
Add
|
||||
</el-button>
|
||||
<el-button v-waves :loading="downloadLoading" class="filter-item" type="primary" :icon="iconDownload" @click="handleDownload">
|
||||
Export
|
||||
</el-button>
|
||||
<el-checkbox v-model="showReviewer" class="filter-item" style="margin-left:15px;" @change="tableKey=tableKey+1">
|
||||
reviewer
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:key="tableKey"
|
||||
v-loading="listLoading"
|
||||
:data="list"
|
||||
border
|
||||
fit
|
||||
highlight-current-row
|
||||
style="width: 100%;"
|
||||
@sort-change="sortChange"
|
||||
>
|
||||
<el-table-column label="ID" prop="id" sortable="custom" align="center" width="80" :class-name="getSortClass('id')">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Date" width="150px" align="center">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ parseTime(row.timestamp, '{y}-{m}-{d} {h}:{i}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Title" min-width="150px">
|
||||
<template v-slot="{row}">
|
||||
<span class="link-type" @click="handleUpdate(row)">{{ row.title }}</span>
|
||||
<el-tag>{{ typeFilter(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Author" width="110px" align="center">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.author }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">
|
||||
<template v-slot="{row}">
|
||||
<span style="color:red;">{{ row.reviewer }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Imp" width="80px">
|
||||
<template v-slot="{row}">
|
||||
<svg-icon v-for="n in row.importance" :key="n" icon-class="star" class="meta-item__icon" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Readings" align="center" width="95">
|
||||
<template v-slot="{row}">
|
||||
<span v-if="row.pageviews" class="link-type" @click="handleFetchPv(row.pageviews)">{{ row.pageviews }}</span>
|
||||
<span v-else>0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status" class-name="status-col" width="100">
|
||||
<template v-slot="{row}">
|
||||
<el-tag :type="statusFilter(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Actions" align="center" width="230" class-name="small-padding fixed-width">
|
||||
<template v-slot="{row,$index}">
|
||||
<el-button type="primary" size="small" @click="handleUpdate(row)">
|
||||
Edit
|
||||
</el-button>
|
||||
<el-button v-if="row.status!='published'" size="small" type="success" @click="handleModifyStatus(row,'published')">
|
||||
Publish
|
||||
</el-button>
|
||||
<el-button v-if="row.status!='draft'" size="small" @click="handleModifyStatus(row,'draft')">
|
||||
Draft
|
||||
</el-button>
|
||||
<el-button v-if="row.status!='deleted'" size="small" type="danger" @click="handleDelete(row,$index)">
|
||||
Delete
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total>0" :total="total" v-model:page="listQuery.page" v-model:limit="listQuery.limit" @pagination="getList" />
|
||||
|
||||
<el-dialog :title="textMap[dialogStatus]" v-model="dialogFormVisible">
|
||||
<el-form ref="dataForm" :rules="rules" :model="temp" label-position="left" label-width="70px" style="width: 400px; margin-left:50px;">
|
||||
<el-form-item label="Type" prop="type">
|
||||
<el-select v-model="temp.type" class="filter-item" placeholder="Please select">
|
||||
<el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Date" prop="timestamp">
|
||||
<el-date-picker v-model="temp.timestamp" type="datetime" placeholder="Please pick a date" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Title" prop="title">
|
||||
<el-input v-model="temp.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Status">
|
||||
<el-select v-model="temp.status" class="filter-item" placeholder="Please select">
|
||||
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Imp">
|
||||
<el-rate v-model="temp.importance" :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" style="margin-top:8px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Remark">
|
||||
<el-input v-model="temp.remark" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="Please input" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogFormVisible = false">
|
||||
Cancel
|
||||
</el-button>
|
||||
<el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
|
||||
Confirm
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="dialogPvVisible" title="Reading statistics">
|
||||
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
|
||||
<el-table-column prop="key" label="Channel" />
|
||||
<el-table-column prop="pv" label="Pv" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="dialogPvVisible = false">Confirm</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search, Edit, Download } from '@element-plus/icons-vue';
|
||||
import { fetchList, fetchPv, createArticle, updateArticle } from '@/api/article';
|
||||
import waves from '@/directive/waves'; // waves directive
|
||||
import { parseTime } from '@/utils';
|
||||
import Pagination from '@/components/Pagination'; // secondary package based on el-pagination
|
||||
|
||||
const calendarTypeOptions = [
|
||||
{ key: 'CN', display_name: 'China' },
|
||||
{ key: 'US', display_name: 'USA' },
|
||||
{ key: 'JP', display_name: 'Japan' },
|
||||
{ key: 'EU', display_name: 'Eurozone' }
|
||||
];
|
||||
|
||||
// arr to obj, such as { CN : "China", US : "USA" }
|
||||
const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {
|
||||
acc[cur.key] = cur.display_name;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComplexTable',
|
||||
components: { Pagination },
|
||||
directives: { waves },
|
||||
data() {
|
||||
return {
|
||||
iconSearch: markRaw(Search),
|
||||
iconEdit: markRaw(Edit),
|
||||
iconDownload: markRaw(Download),
|
||||
tableKey: 0,
|
||||
list: null,
|
||||
total: 0,
|
||||
listLoading: true,
|
||||
listQuery: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
importance: undefined,
|
||||
title: undefined,
|
||||
type: undefined,
|
||||
sort: '+id'
|
||||
},
|
||||
importanceOptions: [1, 2, 3],
|
||||
calendarTypeOptions,
|
||||
sortOptions: [{ label: 'ID Ascending', key: '+id' }, { label: 'ID Descending', key: '-id' }],
|
||||
statusOptions: ['published', 'draft', 'deleted'],
|
||||
showReviewer: false,
|
||||
temp: {
|
||||
id: undefined,
|
||||
importance: 1,
|
||||
remark: '',
|
||||
timestamp: new Date(),
|
||||
title: '',
|
||||
type: '',
|
||||
status: 'published'
|
||||
},
|
||||
dialogFormVisible: false,
|
||||
dialogStatus: '',
|
||||
textMap: {
|
||||
update: 'Edit',
|
||||
create: 'Create'
|
||||
},
|
||||
dialogPvVisible: false,
|
||||
pvData: [],
|
||||
rules: {
|
||||
type: [{ required: true, message: 'type is required', trigger: 'change' }],
|
||||
timestamp: [{ type: 'date', required: true, message: 'timestamp is required', trigger: 'change' }],
|
||||
title: [{ required: true, message: 'title is required', trigger: 'blur' }]
|
||||
},
|
||||
downloadLoading: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
parseTime,
|
||||
statusFilter(status) {
|
||||
const statusMap = {
|
||||
published: 'success',
|
||||
draft: 'info',
|
||||
deleted: 'danger'
|
||||
};
|
||||
return statusMap[status];
|
||||
},
|
||||
typeFilter(type) {
|
||||
return calendarTypeKeyValue[type];
|
||||
},
|
||||
getList() {
|
||||
this.listLoading = true;
|
||||
fetchList(this.listQuery).then(response => {
|
||||
this.list = response.data.items;
|
||||
this.total = response.data.total;
|
||||
|
||||
// Just to simulate the time of the request
|
||||
setTimeout(() => {
|
||||
this.listLoading = false;
|
||||
}, 1.5 * 1000);
|
||||
});
|
||||
},
|
||||
handleFilter() {
|
||||
this.listQuery.page = 1;
|
||||
this.getList();
|
||||
},
|
||||
handleModifyStatus(row, status) {
|
||||
ElMessage({
|
||||
message: '操作Success',
|
||||
type: 'success'
|
||||
});
|
||||
row.status = status;
|
||||
},
|
||||
sortChange(data) {
|
||||
const { prop, order } = data;
|
||||
if (prop === 'id') {
|
||||
this.sortByID(order);
|
||||
}
|
||||
},
|
||||
sortByID(order) {
|
||||
if (order === 'ascending') {
|
||||
this.listQuery.sort = '+id';
|
||||
} else {
|
||||
this.listQuery.sort = '-id';
|
||||
}
|
||||
this.handleFilter();
|
||||
},
|
||||
resetTemp() {
|
||||
this.temp = {
|
||||
id: undefined,
|
||||
importance: 1,
|
||||
remark: '',
|
||||
timestamp: new Date(),
|
||||
title: '',
|
||||
status: 'published',
|
||||
type: ''
|
||||
};
|
||||
},
|
||||
handleCreate() {
|
||||
this.resetTemp();
|
||||
this.dialogStatus = 'create';
|
||||
this.dialogFormVisible = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs['dataForm'].clearValidate();
|
||||
});
|
||||
},
|
||||
createData() {
|
||||
this.$refs['dataForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.temp.id = parseInt(Math.random() * 100) + 1024; // mock a id
|
||||
this.temp.author = 'vue-element-admin';
|
||||
createArticle(this.temp).then(() => {
|
||||
this.list.unshift(this.temp);
|
||||
this.dialogFormVisible = false;
|
||||
this.$notify({
|
||||
title: 'Success',
|
||||
message: 'Created Successfully',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.temp = Object.assign({}, row); // copy obj
|
||||
this.temp.timestamp = new Date(this.temp.timestamp);
|
||||
this.dialogStatus = 'update';
|
||||
this.dialogFormVisible = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs['dataForm'].clearValidate();
|
||||
});
|
||||
},
|
||||
updateData() {
|
||||
this.$refs['dataForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
const tempData = Object.assign({}, this.temp);
|
||||
tempData.timestamp = +new Date(tempData.timestamp); // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464
|
||||
updateArticle(tempData).then(() => {
|
||||
const index = this.list.findIndex(v => v.id === this.temp.id);
|
||||
this.list.splice(index, 1, this.temp);
|
||||
this.dialogFormVisible = false;
|
||||
this.$notify({
|
||||
title: 'Success',
|
||||
message: 'Update Successfully',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row, index) {
|
||||
this.$notify({
|
||||
title: 'Success',
|
||||
message: 'Delete Successfully',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
this.list.splice(index, 1);
|
||||
},
|
||||
handleFetchPv(pv) {
|
||||
fetchPv(pv).then(response => {
|
||||
this.pvData = response.data.pvData;
|
||||
this.dialogPvVisible = true;
|
||||
});
|
||||
},
|
||||
handleDownload() {
|
||||
this.downloadLoading = true;
|
||||
import('@/vendor/Export2Excel').then(excel => {
|
||||
const tHeader = ['timestamp', 'title', 'type', 'importance', 'status'];
|
||||
const filterVal = ['timestamp', 'title', 'type', 'importance', 'status'];
|
||||
const data = this.formatJson(filterVal);
|
||||
excel.export_json_to_excel({
|
||||
header: tHeader,
|
||||
data,
|
||||
filename: 'table-list'
|
||||
});
|
||||
this.downloadLoading = false;
|
||||
});
|
||||
},
|
||||
formatJson(filterVal) {
|
||||
return this.list.map(v => filterVal.map(j => {
|
||||
if (j === 'timestamp') {
|
||||
return parseTime(v[j]);
|
||||
} else {
|
||||
return v[j];
|
||||
}
|
||||
}));
|
||||
},
|
||||
getSortClass: function(key) {
|
||||
const sort = this.listQuery.sort;
|
||||
return sort === `+${key}` ? 'ascending' : 'descending';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
154
src/views/table/drag-table.vue
Normal file
154
src/views/table/drag-table.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- Note that row-key is necessary to get a correct row order. -->
|
||||
<el-table ref="dragTable" v-loading="listLoading" :data="list" row-key="id" border fit highlight-current-row style="width: 100%">
|
||||
<el-table-column align="center" label="ID" width="65">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="180px" align="center" label="Date">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ parseTime( row.timestamp, '{y}-{m}-{d} {h}:{i}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column min-width="300px" label="Title">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="110px" align="center" label="Author">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.author }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="100px" label="Importance">
|
||||
<template v-slot="{row}">
|
||||
<svg-icon v-for="n in row.importance" :key="n" icon-class="star" class="icon-star" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" label="Readings" width="95">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.pageviews }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column class-name="status-col" label="Status" width="110">
|
||||
<template v-slot="{row}">
|
||||
<el-tag :type="statusFilter(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" label="Drag" width="80">
|
||||
<template v-slot="{}">
|
||||
<svg-icon class="drag-handler" icon-class="drag" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="show-d">
|
||||
<el-tag>The default order :</el-tag> {{ oldList }}
|
||||
</div>
|
||||
<div class="show-d">
|
||||
<el-tag>The after dragging order :</el-tag> {{ newList }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { fetchList } from '@/api/article';
|
||||
import Sortable from 'sortablejs';
|
||||
import { parseTime } from '@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DragTable',
|
||||
data() {
|
||||
return {
|
||||
list: null,
|
||||
total: null,
|
||||
listLoading: true,
|
||||
listQuery: {
|
||||
page: 1,
|
||||
limit: 10
|
||||
},
|
||||
sortable: null,
|
||||
oldList: [],
|
||||
newList: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
parseTime,
|
||||
statusFilter(status) {
|
||||
const statusMap = {
|
||||
published: 'success',
|
||||
draft: 'info',
|
||||
deleted: 'danger'
|
||||
};
|
||||
return statusMap[status];
|
||||
},
|
||||
async getList() {
|
||||
this.listLoading = true;
|
||||
const { data } = await fetchList(this.listQuery);
|
||||
this.list = data.items;
|
||||
this.total = data.total;
|
||||
this.listLoading = false;
|
||||
this.oldList = this.list.map(v => v.id);
|
||||
this.newList = this.oldList.slice();
|
||||
this.$nextTick(() => {
|
||||
this.setSort();
|
||||
});
|
||||
},
|
||||
setSort() {
|
||||
const el = this.$refs.dragTable.$el.querySelectorAll('.el-table__body-wrapper table tbody')[0];
|
||||
this.sortable = Sortable.create(el, {
|
||||
ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
|
||||
setData: function(dataTransfer) {
|
||||
// to avoid Firefox bug
|
||||
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
|
||||
dataTransfer.setData('Text', '');
|
||||
},
|
||||
onEnd: evt => {
|
||||
const targetRow = this.list.splice(evt.oldIndex, 1)[0];
|
||||
this.list.splice(evt.newIndex, 0, targetRow);
|
||||
|
||||
// for show the changes, you can delete in you code
|
||||
const tempIndex = this.newList.splice(evt.oldIndex, 1)[0];
|
||||
this.newList.splice(evt.newIndex, 0, tempIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sortable-ghost{
|
||||
opacity: .8;
|
||||
color: #fff!important;
|
||||
background: #42b983!important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.icon-star{
|
||||
margin-right:2px;
|
||||
}
|
||||
.drag-handler{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.show-d{
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
63
src/views/table/dynamic-table/components/FixedThead.vue
Normal file
63
src/views/table/dynamic-table/components/FixedThead.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-checkbox-group v-model="checkboxVal">
|
||||
<el-checkbox label="apple">
|
||||
apple
|
||||
</el-checkbox>
|
||||
<el-checkbox label="banana">
|
||||
banana
|
||||
</el-checkbox>
|
||||
<el-checkbox label="orange">
|
||||
orange
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<el-table :key="key" :data="tableData" border fit highlight-current-row style="width: 100%">
|
||||
<el-table-column prop="name" label="fruitName" width="180" />
|
||||
<el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
|
||||
<template v-slot="scope">
|
||||
{{ scope.row[fruit] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
const defaultFormThead = ['apple', 'banana'];
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tableData: [
|
||||
{
|
||||
name: 'fruit-1',
|
||||
apple: 'apple-10',
|
||||
banana: 'banana-10',
|
||||
orange: 'orange-10'
|
||||
},
|
||||
{
|
||||
name: 'fruit-2',
|
||||
apple: 'apple-20',
|
||||
banana: 'banana-20',
|
||||
orange: 'orange-20'
|
||||
}
|
||||
],
|
||||
key: 1, // table key
|
||||
formTheadOptions: ['apple', 'banana', 'orange'],
|
||||
checkboxVal: defaultFormThead, // checkboxVal
|
||||
formThead: defaultFormThead // 默认表头 Default header
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
checkboxVal(valArr) {
|
||||
this.formThead = this.formTheadOptions.filter(i => valArr.indexOf(i) >= 0);
|
||||
this.key = this.key + 1;// 为了保证table 每次都会重渲 In order to ensure the table will be re-rendered each time
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
52
src/views/table/dynamic-table/components/UnfixedThead.vue
Normal file
52
src/views/table/dynamic-table/components/UnfixedThead.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-checkbox-group v-model="formThead">
|
||||
<el-checkbox label="apple">
|
||||
apple
|
||||
</el-checkbox>
|
||||
<el-checkbox label="banana">
|
||||
banana
|
||||
</el-checkbox>
|
||||
<el-checkbox label="orange">
|
||||
orange
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" border fit highlight-current-row style="width: 100%">
|
||||
<el-table-column prop="name" label="fruitName" width="180" />
|
||||
<el-table-column v-for="fruit in formThead" :key="fruit" :label="fruit">
|
||||
<template v-slot="scope">
|
||||
{{ scope.row[fruit] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tableData: [
|
||||
{
|
||||
name: 'fruit-1',
|
||||
apple: 'apple-10',
|
||||
banana: 'banana-10',
|
||||
orange: 'orange-10'
|
||||
},
|
||||
{
|
||||
name: 'fruit-2',
|
||||
apple: 'apple-20',
|
||||
banana: 'banana-20',
|
||||
orange: 'orange-20'
|
||||
}
|
||||
],
|
||||
formThead: ['apple', 'banana']
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
25
src/views/table/dynamic-table/index.vue
Normal file
25
src/views/table/dynamic-table/index.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div style="margin:0 0 5px 20px">
|
||||
Fixed header, sorted by header order,
|
||||
</div>
|
||||
<fixed-thead />
|
||||
|
||||
<div style="margin:30px 0 5px 20px">
|
||||
Not fixed header, sorted by click order
|
||||
</div>
|
||||
<unfixed-thead />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import FixedThead from './components/FixedThead';
|
||||
import UnfixedThead from './components/UnfixedThead';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DynamicTable',
|
||||
components: { FixedThead, UnfixedThead }
|
||||
});
|
||||
</script>
|
||||
|
157
src/views/table/inline-edit-table.vue
Normal file
157
src/views/table/inline-edit-table.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-table v-loading="listLoading" :data="list" border fit highlight-current-row style="width: 100%">
|
||||
<el-table-column align="center" label="ID" width="80">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="180px" align="center" label="Date">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ parseTime(row.timestamp, '{y}-{m}-{d} {h}:{i}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="120px" align="center" label="Author">
|
||||
<template v-slot="{row}">
|
||||
<span>{{ row.author }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column width="100px" label="Importance">
|
||||
<template v-slot="{row}">
|
||||
<svg-icon v-for="n in row.importance" :key="n" icon-class="star" class="meta-item__icon" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column class-name="status-col" label="Status" width="110">
|
||||
<template v-slot="{row}">
|
||||
<el-tag :type="statusFilter(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column min-width="300px" label="Title">
|
||||
<template v-slot="{row}">
|
||||
<template v-if="row.edit">
|
||||
<el-input v-model="row.title" class="edit-input" size="small" />
|
||||
<el-button
|
||||
class="cancel-btn"
|
||||
size="small"
|
||||
:icon="iconRefresh"
|
||||
type="warning"
|
||||
@click="cancelEdit(row)"
|
||||
>
|
||||
cancel
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else>{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="center" label="Actions" width="120">
|
||||
<template v-slot="{row}">
|
||||
<el-button
|
||||
v-if="row.edit"
|
||||
type="success"
|
||||
size="small"
|
||||
:icon="iconCircleCheck"
|
||||
@click="confirmEdit(row)"
|
||||
>
|
||||
Ok
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="iconEdit"
|
||||
@click="row.edit=!row.edit"
|
||||
>
|
||||
Edit
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { fetchList } from '@/api/article';
|
||||
import { parseTime } from '@/utils';
|
||||
import { Refresh, CircleCheck, Edit } from '@element-plus/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InlineEditTable',
|
||||
data() {
|
||||
return {
|
||||
iconRefresh: markRaw(Refresh),
|
||||
iconCircleCheck: markRaw(CircleCheck),
|
||||
iconEdit: markRaw(Edit),
|
||||
list: null,
|
||||
listLoading: true,
|
||||
listQuery: {
|
||||
page: 1,
|
||||
limit: 10
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
parseTime,
|
||||
statusFilter(status) {
|
||||
const statusMap = {
|
||||
published: 'success',
|
||||
draft: 'info',
|
||||
deleted: 'danger'
|
||||
};
|
||||
return statusMap[status];
|
||||
},
|
||||
async getList() {
|
||||
this.listLoading = true;
|
||||
const { data } = await fetchList(this.listQuery);
|
||||
const items = data.items;
|
||||
this.list = items.map(v => {
|
||||
return {
|
||||
...v,
|
||||
edit: false,
|
||||
originalTitle: v.title // will be used when user click the cancel botton
|
||||
};
|
||||
});
|
||||
this.listLoading = false;
|
||||
},
|
||||
cancelEdit(row) {
|
||||
row.title = row.originalTitle;
|
||||
row.edit = false;
|
||||
ElMessage({
|
||||
message: 'The title has been restored to the original value',
|
||||
type: 'warning'
|
||||
});
|
||||
},
|
||||
confirmEdit(row) {
|
||||
row.edit = false;
|
||||
row.originalTitle = row.title;
|
||||
ElMessage({
|
||||
message: 'The title has been edited',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-input {
|
||||
padding-right: 100px;
|
||||
}
|
||||
.cancel-btn {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
}
|
||||
</style>
|
@ -20,7 +20,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["dom", "esnext"],
|
||||
"lib": ["dom", "esnext", "dom.iterable", "scripthost"],
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"removeComments": true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user