update: store、router使用ts重写;新增示例

This commit is contained in:
midfar 2023-03-10 17:54:00 +08:00
parent 7640cfa278
commit 80f9a724df
42 changed files with 2348 additions and 482 deletions

6
auto-imports.d.ts vendored Normal file
View 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
View 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']
}
}

View File

@ -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
View File

@ -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",

View File

@ -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"
},

View File

@ -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 }
});
}

View 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>

View 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>

View 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>

View 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();
}
}
};

View File

@ -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),

View 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>

View 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;

View 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;
}

View 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];
}
};

View File

@ -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>

View File

@ -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)

View File

@ -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
*
* hiddenalwaysShow 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
*
* hiddenalwaysShow 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
}

View File

@ -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 }
}

View File

@ -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' }
}

View File

@ -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;
}, {});

View File

@ -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
View 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);
}
}
});

View File

@ -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
});

View 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;
}
}
});

View File

@ -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
});

View 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;
}
}
}
});

View File

@ -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
});

View 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;
}
}
}
}
});

View File

@ -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
View 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();
}
}
});

View 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
View 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>

View 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>

View File

@ -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) {

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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,