added excel demo

This commit is contained in:
midfar 2023-03-14 10:10:07 +08:00
parent b5b8970ab5
commit 7e6bb6998e
12 changed files with 711 additions and 46 deletions

View File

@ -1,6 +1,6 @@
# vue3-element-admin
这个模板使用了最新的 vue3 和 element-plus UI框架vite 构建工具、pinia 状态管理、vue-router 路由管理、mockjs 数据模拟。功能从Vue Element Admin 移植而来,详细使用可以参考[该文档](https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html)。
这个模板使用了最新的 vue3 和 element-plus UI 框架vite 构建工具、pinia 状态管理、vue-router 路由管理、mockjs 数据模拟。功能从 Vue Element Admin 移植而来,详细使用可以参考[该文档](https://panjiachen.github.io/vue-element-admin-site/zh/guide/essentials/router-and-nav.html)。
# 在线示例
@ -8,13 +8,13 @@
[element plus](https://element-plus.midfar.com/)
## 推荐的IDE工具和插件
## 推荐的 IDE 工具和插件
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (禁用 Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Vite构建工具配置
## Vite 构建工具配置
参考 [Vite配置](https://vitejs.dev/config/).
参考 [Vite 配置](https://vitejs.dev/config/).
## 安装依赖
@ -44,10 +44,10 @@ npm run lint
如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 :tropical_drink:
<img src="https://vue3-element-admin.midfar.com/midfar_pay.jpg" alt="捐赠" style="zoom: 20%;" />
wechat: midfar-sun
## License
[MIT](https://opensource.org/licenses/MIT)
Copyright (c) 2022-present, Midfar Sun
Copyright (c) 2022-present, Midfar Sun

1
components.d.ts vendored
View File

@ -77,6 +77,7 @@ declare module '@vue/runtime-core' {
Sticky: typeof import('./src/components/Sticky/index.vue')['default']
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
Tinymce: typeof import('./src/components/Tinymce/index.vue')['default']
UploadExcel: typeof import('./src/components/UploadExcel/index.vue')['default']
VueCountTo: typeof import('./src/components/vue-count-to/vue-countTo.vue')['default']
}
export interface ComponentCustomProperties {

View File

@ -0,0 +1,147 @@
<template>
<div>
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
Drop excel file here or
<el-button :loading="loading" style="margin-left:16px;" size="small" type="primary" @click="handleUpload">
Browse
</el-button>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import * as XLSX from 'xlsx';
export default defineComponent({
props: {
beforeUpload: {
type: Function,
// eslint-disable-next-line @typescript-eslint/no-empty-function
default: () => {}
},
onSuccess: {
type: Function,
// eslint-disable-next-line @typescript-eslint/no-empty-function
default: () => {}
}
},
data() {
return {
loading: false,
excelData: {
header: null,
results: null
}
};
},
methods: {
generateData({ header, results }) {
this.excelData.header = header;
this.excelData.results = results;
this.onSuccess && this.onSuccess(this.excelData);
},
handleDrop(e) {
e.stopPropagation();
e.preventDefault();
if (this.loading) return;
const files = e.dataTransfer.files;
if (files.length !== 1) {
ElMessage.error('Only support uploading one file!');
return;
}
const rawFile = files[0]; // only use files[0]
if (!this.isExcel(rawFile)) {
ElMessage.error('Only supports upload .xlsx, .xls, .csv suffix files');
return false;
}
this.upload(rawFile);
e.stopPropagation();
e.preventDefault();
},
handleDragover(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
},
handleUpload() {
this.$refs['excel-upload-input'].click();
},
handleClick(e) {
const files = e.target.files;
const rawFile = files[0]; // only use files[0]
if (!rawFile) return;
this.upload(rawFile);
},
upload(rawFile) {
this.$refs['excel-upload-input'].value = null; // fix can't select the same excel
if (!this.beforeUpload) {
this.readerData(rawFile);
return;
}
const before = this.beforeUpload(rawFile);
if (before) {
this.readerData(rawFile);
}
},
readerData(rawFile) {
this.loading = true;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
const data = e.target.result;
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const header = this.getHeaderRow(worksheet);
const results = XLSX.utils.sheet_to_json(worksheet);
this.generateData({ header, results });
this.loading = false;
resolve();
};
reader.readAsArrayBuffer(rawFile);
});
},
getHeaderRow(sheet) {
const headers = [];
const range = XLSX.utils.decode_range(sheet['!ref']);
let C;
const R = range.s.r;
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })];
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C; // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell);
headers.push(hdr);
}
return headers;
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name);
}
}
});
</script>
<style scoped>
.excel-upload-input{
display: none;
z-index: -9999;
}
.drop{
border: 2px dashed #bbb;
width: 600px;
height: 160px;
line-height: 160px;
margin: 0 auto;
font-size: 24px;
border-radius: 5px;
text-align: center;
color: #bbb;
position: relative;
}
</style>

View File

@ -7,8 +7,11 @@
:class="{ 'submenu-title-noDropdown': !isNest }">
<!-- <item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> -->
<svg-icon v-if="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
:icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template v-if="get2MetaIconPath(onlyOneChild, item)">
<svg-icon v-if="typeof get2MetaIconPath(onlyOneChild, item) === 'string'"
:icon-class="get2MetaIconPath(onlyOneChild, item)" />
<component v-else :is="get2MetaIconPath(onlyOneChild, item)" class="svg-icon el-svg-icon" />
</template>
<template #title>
<span class="text text-one">{{ onlyOneChild.meta.title }}</span>
</template>
@ -19,7 +22,11 @@
<el-sub-menu class="left-sub-menu" v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="item.meta" #title>
<!-- <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> -->
<svg-icon :icon-class="(item.meta && item.meta.icon) || 'sub-el-icon'" />
<template v-if="getMetaIconPath(item)">
<svg-icon v-if="typeof getMetaIconPath(item) === 'string'" :icon-class="getMetaIconPath(item)" />
<component v-else :is="getMetaIconPath(item)" class="svg-icon el-svg-icon" />
</template>
<svg-icon v-else icon-class="sub-el-icon" />
<span class="text text-two">{{ item.meta.title }}</span>
</template>
<sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child"
@ -73,6 +80,12 @@ export default defineComponent({
}
},
methods: {
getMetaIconPath(item) {
return item.meta && item.meta.icon;
},
get2MetaIconPath(onlyOneChild, item) {
return onlyOneChild.meta.icon || (item.meta && item.meta.icon);
},
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (this.isItemHidden) {
@ -119,4 +132,12 @@ export default defineComponent({
.left-sub-menu :deep(.el-sub-menu__title) {
display: block;
}
.el-svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -1,5 +1,7 @@
import { markRaw } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router'; // createWebHashHistory, createWebHistory
import type { Router, RouteRecordRaw, RouteComponent } from 'vue-router';
import { Help as IconHelp } from '@element-plus/icons-vue';
/* Layout */
const Layout = ():RouteComponent => import('@/layout/index.vue');
@ -178,7 +180,7 @@ export const asyncRoutes:RouteRecordRaw[] = [
name: 'Example',
meta: {
title: 'Example',
icon: 'el-icon-s-help'
icon: markRaw(IconHelp)
},
children: [
{
@ -253,42 +255,42 @@ export const asyncRoutes:RouteRecordRaw[] = [
]
},
// {
// path: '/excel',
// component: Layout,
// redirect: '/excel/export-excel',
// name: 'Excel',
// meta: {
// title: 'Excel',
// icon: 'excel'
// },
// children: [
// {
// path: 'export-excel',
// component: () => import('@/views/excel/export-excel'),
// name: 'ExportExcel',
// meta: { title: 'Export Excel' }
// },
// {
// path: 'export-selected-excel',
// component: () => import('@/views/excel/select-excel'),
// name: 'SelectExcel',
// meta: { title: 'Export Selected' }
// },
// {
// path: 'export-merge-header',
// component: () => import('@/views/excel/merge-header'),
// name: 'MergeHeader',
// meta: { title: 'Merge Header' }
// },
// {
// path: 'upload-excel',
// component: () => import('@/views/excel/upload-excel'),
// name: 'UploadExcel',
// meta: { title: 'Upload Excel' }
// }
// ]
// },
{
path: '/excel',
component: Layout,
redirect: '/excel/export-excel',
name: 'Excel',
meta: {
title: 'Excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: () => import('@/views/excel/export-excel.vue'),
name: 'ExportExcel',
meta: { title: 'Export Excel' }
},
{
path: 'export-selected-excel',
component: () => import('@/views/excel/select-excel.vue'),
name: 'SelectExcel',
meta: { title: 'Export Selected' }
},
{
path: 'export-merge-header',
component: () => import('@/views/excel/merge-header.vue'),
name: 'MergeHeader',
meta: { title: 'Merge Header' }
},
{
path: 'upload-excel',
component: () => import('@/views/excel/upload-excel.vue'),
name: 'UploadExcel',
meta: { title: 'Upload Excel' }
}
]
},
{
path: '/zip',

View File

@ -0,0 +1,36 @@
<template>
<div style="display:inline-block;">
<label class="radio-label">Cell Auto-Width: </label>
<el-radio-group v-model="autoWidth">
<el-radio :label="true" border>
True
</el-radio>
<el-radio :label="false" border>
False
</el-radio>
</el-radio-group>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: true
}
},
computed: {
autoWidth: {
get() {
return this.modelValue;
},
set(val) {
this.$emit('update:modelValue', val);
}
}
}
});
</script>

View File

@ -0,0 +1,41 @@
<template>
<div style="display:inline-block;">
<label class="radio-label">Book Type: </label>
<el-select v-model="bookType" style="width:120px;">
<el-option
v-for="item in options"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
modelValue: {
type: String,
default: 'xlsx'
}
},
data() {
return {
options: ['xlsx', 'csv', 'txt']
};
},
computed: {
bookType: {
get() {
return this.modelValue;
},
set(val) {
this.$emit('update:modelValue', val);
}
}
}
});
</script>

View File

@ -0,0 +1,35 @@
<template>
<div style="display:inline-block;">
<label class="radio-label" style="padding-left:0;">Filename: </label>
<el-input v-model="filename" placeholder="Please enter the file name (default excel-list)" style="width:345px;" :prefix-icon="IconDocument" />
</div>
</template>
<script>
import { defineComponent, markRaw } from 'vue';
import { Document as IconDocument } from '@element-plus/icons-vue';
export default defineComponent({
props: {
modelValue: {
type: String,
default: ''
}
},
data() {
return {
IconDocument: markRaw(IconDocument)
};
},
computed: {
filename: {
get() {
return this.modelValue;
},
set(val) {
this.$emit('update:modelValue', val);
}
}
}
});
</script>

View File

@ -0,0 +1,120 @@
<template>
<div class="app-container">
<div style="margin: 0 0 20px 0;">
<FilenameOption v-model="filename" />
<AutoWidthOption v-model="autoWidth" />
<BookTypeOption v-model="bookType" />
<el-button :loading="downloadLoading" style="margin:0 0 0 20px;" type="primary" :icon="IconDocument" @click="handleDownload">
Export Excel
</el-button>
<a href="https://panjiachen.github.io/vue-element-admin-site/feature/component/excel.html" target="_blank" style="margin-left:15px;">
<el-tag type="info" size="large">Documentation</el-tag>
</a>
</div>
<el-table v-loading="listLoading" :data="list" element-loading-text="Loading..." border fit highlight-current-row>
<el-table-column align="center" label="Id" width="95">
<template v-slot="scope">
{{ scope.$index }}
</template>
</el-table-column>
<el-table-column label="Title">
<template v-slot="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="Author" width="110" align="center">
<template v-slot="scope">
<el-tag>{{ scope.row.author }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Readings" width="115" align="center">
<template v-slot="scope">
{{ scope.row.pageviews }}
</template>
</el-table-column>
<el-table-column align="center" label="Date" width="220">
<template v-slot="scope">
<el-icon><IconTimer /></el-icon>
<span>{{ parseTime(scope.row.timestamp, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { defineComponent, markRaw } from 'vue';
import { fetchList } from '@/api/article';
import { parseTime } from '@/utils';
// options components
import FilenameOption from './components/FilenameOption';
import AutoWidthOption from './components/AutoWidthOption';
import BookTypeOption from './components/BookTypeOption';
import { Document as IconDocument, Timer as IconTimer } from '@element-plus/icons-vue';
export default defineComponent({
name: 'ExportExcel',
components: { FilenameOption, AutoWidthOption, BookTypeOption, IconTimer },
data() {
return {
IconDocument: markRaw(IconDocument),
list: null,
listLoading: true,
downloadLoading: false,
filename: '',
autoWidth: true,
bookType: 'xlsx'
};
},
created() {
this.fetchData();
},
methods: {
parseTime,
fetchData() {
this.listLoading = true;
fetchList().then(response => {
this.list = response.data.items;
this.listLoading = false;
});
},
handleDownload() {
this.downloadLoading = true;
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['Id', 'Title', 'Author', 'Readings', 'Date'];
const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time'];
const list = this.list;
const data = this.formatJson(filterVal, list);
excel.export_json_to_excel({
header: tHeader,
data,
filename: this.filename,
autoWidth: this.autoWidth,
bookType: this.bookType
});
this.downloadLoading = false;
});
},
formatJson(filterVal, jsonData) {
return jsonData.map(v => filterVal.map(j => {
if (j === 'timestamp') {
return parseTime(v[j]);
} else {
return v[j];
}
}));
}
}
});
</script>
<style>
.radio-label {
font-size: 14px;
color: #606266;
line-height: 40px;
padding: 0 12px 0 30px;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div class="app-container">
<el-button :loading="downloadLoading" style="margin-bottom:20px" type="primary" :icon="IconDocument" @click="handleDownload">Export</el-button>
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="list"
element-loading-text="Loading"
border
fit
highlight-current-row
>
<el-table-column align="center" label="Id" width="95">
<template v-slot="scope">
{{ scope.$index }}
</template>
</el-table-column>
<el-table-column label="Main Information" align="center">
<el-table-column label="Title">
<template v-slot="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="Author" width="110" align="center">
<template v-slot="scope">
<el-tag>{{ scope.row.author }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Readings" width="115" align="center">
<template v-slot="scope">
{{ scope.row.pageviews }}
</template>
</el-table-column>
</el-table-column>
<el-table-column align="center" label="Date" width="220">
<template v-slot="scope">
<el-icon><IconTimer /></el-icon>
<span>{{ parseTime(scope.row.timestamp, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { defineComponent, markRaw } from 'vue';
import { fetchList } from '@/api/article';
import { parseTime } from '@/utils';
import { Document as IconDocument, Timer as IconTimer } from '@element-plus/icons-vue';
export default defineComponent({
name: 'MergeHeader',
components: { IconTimer },
data() {
return {
IconDocument: markRaw(IconDocument),
list: null,
listLoading: true,
downloadLoading: false
};
},
created() {
this.fetchData();
},
methods: {
parseTime,
fetchData() {
this.listLoading = true;
fetchList(this.listQuery).then(response => {
this.list = response.data.items;
this.listLoading = false;
});
},
handleDownload() {
this.downloadLoading = true;
import('@/vendor/Export2Excel').then(excel => {
const multiHeader = [['Id', 'Main Information', '', '', 'Date']];
const header = ['', 'Title', 'Author', 'Readings', ''];
const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time'];
const list = this.list;
const data = this.formatJson(filterVal, list);
const merges = ['A1:A2', 'B1:D1', 'E1:E2'];
excel.export_json_to_excel({
multiHeader,
header,
merges,
data
});
this.downloadLoading = false;
});
},
formatJson(filterVal, jsonData) {
return jsonData.map(v => filterVal.map(j => {
if (j === 'timestamp') {
return parseTime(v[j]);
} else {
return v[j];
}
}));
}
}
});
</script>

View File

@ -0,0 +1,113 @@
<template>
<div class="app-container">
<div style="margin-bottom:20px">
<el-input v-model="filename" placeholder="Please enter the file name (default excel-list)" style="width:350px;" :prefix-icon="IconDocument" />
<el-button :loading="downloadLoading" type="primary" :icon="IconDocument" @click="handleDownload">
Export Selected Items
</el-button>
<a href="https://panjiachen.github.io/vue-element-admin-site/feature/component/excel.html" target="_blank" style="margin-left:15px;">
<el-tag type="info" size="large">Documentation</el-tag>
</a>
</div>
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="list"
element-loading-text="拼命加载中"
border
fit
highlight-current-row
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" align="center" />
<el-table-column align="center" label="Id" width="95">
<template v-slot="scope">
{{ scope.$index }}
</template>
</el-table-column>
<el-table-column label="Title">
<template v-slot="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="Author" width="110" align="center">
<template v-slot="scope">
<el-tag>{{ scope.row.author }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Readings" width="115" align="center">
<template v-slot="scope">
{{ scope.row.pageviews }}
</template>
</el-table-column>
<el-table-column align="center" label="PDate" width="220">
<template v-slot="scope">
<el-icon><IconTimer /></el-icon>
<span>{{ scope.row.display_time }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { defineComponent, markRaw } from 'vue';
import { fetchList } from '@/api/article';
import { Document as IconDocument, Timer as IconTimer } from '@element-plus/icons-vue';
export default defineComponent({
name: 'SelectExcel',
components: { IconTimer },
data() {
return {
IconDocument: markRaw(IconDocument),
list: null,
listLoading: true,
multipleSelection: [],
downloadLoading: false,
filename: ''
};
},
created() {
this.fetchData();
},
methods: {
fetchData() {
this.listLoading = true;
fetchList(this.listQuery).then(response => {
this.list = response.data.items;
this.listLoading = false;
});
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleDownload() {
if (this.multipleSelection.length) {
this.downloadLoading = true;
import('@/vendor/Export2Excel').then(excel => {
const tHeader = ['Id', 'Title', 'Author', 'Readings', 'Date'];
const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time'];
const list = this.multipleSelection;
const data = this.formatJson(filterVal, list);
excel.export_json_to_excel({
header: tHeader,
data,
filename: this.filename
});
this.$refs.multipleTable.clearSelection();
this.downloadLoading = false;
});
} else {
ElMessage({
message: 'Please select at least one item',
type: 'warning'
});
}
},
formatJson(filterVal, jsonData) {
return jsonData.map(v => filterVal.map(j => v[j]));
}
}
});
</script>

View File

@ -0,0 +1,43 @@
<template>
<div class="app-container">
<upload-excel-component :on-success="handleSuccess" :before-upload="beforeUpload" />
<el-table :data="tableData" border highlight-current-row style="width: 100%;margin-top:20px;">
<el-table-column v-for="item of tableHeader" :key="item" :prop="item" :label="item" />
</el-table>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import UploadExcelComponent from '@/components/UploadExcel/index.vue';
export default defineComponent({
name: 'UploadExcel',
components: { UploadExcelComponent },
data() {
return {
tableData: [],
tableHeader: []
};
},
methods: {
beforeUpload(file) {
const isLt1M = file.size / 1024 / 1024 < 1;
if (isLt1M) {
return true;
}
ElMessage({
message: 'Please do not upload files larger than 1m in size.',
type: 'warning'
});
return false;
},
handleSuccess({ results, header }) {
this.tableData = results;
this.tableHeader = header;
}
}
});
</script>