修改一些发现的问题,新增秒杀活动设置,优惠券动态时间 以及 精准发圈用户和小程序直播页面
parent
3eee6b35e8
commit
40bf9781b1
|
@ -27,7 +27,6 @@
|
|||
"print-js": "^1.0.63",
|
||||
"qrcodejs2": "0.0.2",
|
||||
"quill": "^1.3.7",
|
||||
"vue-qr": "^2.3.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sockjs-client": "^1.4.0",
|
||||
"stompjs": "^2.3.3",
|
||||
|
@ -40,8 +39,10 @@
|
|||
"vue-clipboard2": "^0.3.0",
|
||||
"vue-cropper": "^0.4.9",
|
||||
"vue-i18n": "^8.15.1",
|
||||
"vue-json-excel": "^0.3.0",
|
||||
"vue-json-pretty": "^1.4.1",
|
||||
"vue-lazyload": "^1.3.3",
|
||||
"vue-qr": "^2.3.0",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "^3.4.0",
|
||||
|
|
|
@ -17,6 +17,12 @@ export const whetherStar = params => {
|
|||
return getRequest(`/broadcast/studio/id/${params.id}&recommend=${params.recommend}`);
|
||||
};
|
||||
|
||||
// 添加优惠券活动
|
||||
export const addCouponActivity = params => {
|
||||
return postRequest(`/promotion/couponActivity/addCouponActivity`,params);
|
||||
};
|
||||
|
||||
|
||||
// 获取店铺直播间列表
|
||||
export const getLiveList = params => {
|
||||
return getRequest("/broadcast/studio", params);
|
||||
|
|
|
@ -17,10 +17,14 @@ export default {
|
|||
* @description api请求基础路径
|
||||
*/
|
||||
api_dev: {
|
||||
common: "https://common-api.pickmall.cn",
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
seller: "https://store-api.pickmall.cn",
|
||||
manager: "https://admin-api.pickmall.cn"
|
||||
// common: "https://common-api.pickmall.cn",
|
||||
// buyer: "https://buyer-api.pickmall.cn",
|
||||
// seller: "https://store-api.pickmall.cn",
|
||||
// manager: "https://admin-api.pickmall.cn"
|
||||
common: 'http://192.168.0.109:8890',
|
||||
buyer: 'http://192.168.0.109:8888',
|
||||
seller: 'http://192.168.0.109:8889',
|
||||
manager: 'http://192.168.0.109:8887'
|
||||
},
|
||||
api_prod: {
|
||||
common: "https://common-api.pickmall.cn",
|
||||
|
|
|
@ -224,6 +224,12 @@ export const otherRouter = {
|
|||
name: "add-platform-coupon",
|
||||
component: () => import("@/views/promotion/coupon/couponPublish.vue")
|
||||
},
|
||||
{
|
||||
path: "promotion/add-coupon-specify",
|
||||
title: "精准发劵",
|
||||
name: "add-coupon-specify",
|
||||
component: () => import("@/views/promotion/coupon/couponSpecify.vue")
|
||||
},
|
||||
{
|
||||
path: "promotion/edit-platform-coupon",
|
||||
title: "编辑平台优惠券",
|
||||
|
|
|
@ -211,10 +211,8 @@
|
|||
import { homeStatistics, hotGoods, hotShops, getNoticePage } from "@/api/index";
|
||||
import show from "./show.vue";
|
||||
import * as API_Goods from "@/api/goods";
|
||||
|
||||
import { Chart } from "@antv/g2";
|
||||
import * as API_Member from "@/api/member";
|
||||
import Cookies from "js-cookie";
|
||||
export default {
|
||||
name: "home",
|
||||
components: {
|
||||
|
@ -502,11 +500,13 @@ export default {
|
|||
let data = this.chartList;
|
||||
|
||||
data.forEach((item) => {
|
||||
|
||||
item.title = "历史在线人数";
|
||||
item.date = item.date.substring(5)
|
||||
|
||||
});
|
||||
this.historyMemberChart.data(data);
|
||||
|
||||
console.error(data)
|
||||
this.historyMemberChart.tooltip({
|
||||
showCrosshairs: true,
|
||||
shared: true,
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
<template>
|
||||
<div class="floor-list">
|
||||
<div class="list">
|
||||
<div class="template-saved" v-for="item in templateList" :key="item.id">
|
||||
<div class="template-title">
|
||||
<span>{{ item.name }}</span>
|
||||
<span :class="{ 'theme-color': item.pageShow == 'OPEN' }">{{
|
||||
item.pageShow == "OPEN" ? "已发布" : "未发布"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="template-content">
|
||||
<!-- <img :src="item.img || require(`@/assets/img-error.png`)" alt=""> -->
|
||||
<div class="cover">
|
||||
<Button icon="md-color-palette" @click="decorate(item.id)"
|
||||
>装修</Button
|
||||
>
|
||||
<Button icon="md-pricetags" @click="Template(item)">编辑</Button>
|
||||
<Button
|
||||
icon="md-send"
|
||||
v-if="item.pageShow == 'CLOSE'"
|
||||
@click="releaseTemplate(item.id)"
|
||||
>发布</Button
|
||||
>
|
||||
<Button
|
||||
icon="md-trash"
|
||||
v-if="item.pageShow == 'CLOSE'"
|
||||
@click="delTemplate(item.id)"
|
||||
>删除</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="new-template" @click="createTemp">
|
||||
<Icon type="md-add" />
|
||||
<div>新建模板</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
v-model="showModal"
|
||||
title="模板设置"
|
||||
draggable
|
||||
width="600"
|
||||
:z-index="100"
|
||||
:loading="loading"
|
||||
mask-closable="false"
|
||||
@on-ok="newTemplate"
|
||||
@on-cancel="showModal = false"
|
||||
>
|
||||
<Form ref="form" :model="formData" :rules="rules" :label-width="80">
|
||||
<FormItem label="模板名称" prop="name">
|
||||
<Input v-model="formData.name" placeholder="请输入模板名称" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as API_floor from "@/api/other.js";
|
||||
export default {
|
||||
name: "floorList",
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
templateList: [],
|
||||
formData: {
|
||||
status: false,
|
||||
name: "",
|
||||
},
|
||||
rules: {
|
||||
name: [{ required: true, message: "请输入模板名称", trigger: "blur" }],
|
||||
},
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
let height = window.innerHeight - 150;
|
||||
document.querySelector(".floor-list").style.height = height + "px";
|
||||
this.getTemplateList();
|
||||
},
|
||||
methods: {
|
||||
newTemplate() {
|
||||
// 添加,编辑模板
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
const data = this.formData;
|
||||
data.status ? (data.pageShow = "OPEN") : (data.pageShow = "CLOSE");
|
||||
delete data.status;
|
||||
(data.pageType = "INDEX"), (data.pageClientType = "PC");
|
||||
if (data.id) {
|
||||
API_floor.updateHome(data.id, data).then((res) => {
|
||||
this.$Message.success("编辑模板成功");
|
||||
this.showModal = false;
|
||||
this.getTemplateList();
|
||||
});
|
||||
} else {
|
||||
API_floor.setHomeSetup(data).then((res) => {
|
||||
this.$Message.success("新建模板成功");
|
||||
this.showModal = false;
|
||||
this.getTemplateList();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createTemp() {
|
||||
// 新建表单
|
||||
this.$refs.form.resetFields();
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
Template(item) {
|
||||
// 编辑表单
|
||||
item.pageShow == "OPEN" ? (item.status = true) : (item.status = false);
|
||||
this.formData = item;
|
||||
this.showModal = true;
|
||||
},
|
||||
|
||||
decorate(id) {
|
||||
// 装修
|
||||
this.$router.push({ name: "renovation", query: { id: id } });
|
||||
},
|
||||
|
||||
getTemplateList() {
|
||||
//模板列表
|
||||
let params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 999,
|
||||
pageType: "INDEX",
|
||||
pageClientType: "PC",
|
||||
};
|
||||
API_floor.getHomeList(params).then((res) => {
|
||||
if (res.success) {
|
||||
this.templateList = res.result.records;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
releaseTemplate(id) {
|
||||
//发布模板
|
||||
API_floor.releasePageHome(id).then((res) => {
|
||||
if (res.success) {
|
||||
this.$Message.success("发布模板成功");
|
||||
this.getTemplateList();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
delTemplate(id) {
|
||||
this.$Modal.confirm({
|
||||
title: "提示",
|
||||
content: "<p>确定删除该模板吗?</p>",
|
||||
onOk: () => {
|
||||
API_floor.removePageHome(id).then((res) => {
|
||||
if (res.success) {
|
||||
this.$Message.success("删除模板成功");
|
||||
this.getTemplateList();
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel: () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.floor-list {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
.list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
> div {
|
||||
width: 260px;
|
||||
height: 450px;
|
||||
margin: 10px 15px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.new-template {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 2px dashed $theme_color;
|
||||
color: $theme_color;
|
||||
background: #f88c7138;
|
||||
.ivu-icon {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.template-saved {
|
||||
overflow: hidden;
|
||||
.template-title {
|
||||
padding: 10px;
|
||||
background: #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.theme-color {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.template-content {
|
||||
height: 407px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: #ddd;
|
||||
&:hover {
|
||||
.cover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.cover {
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
height: 407px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.ivu-btn {
|
||||
margin-bottom: 10px;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
&:hover {
|
||||
background-color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
<div class="full-shadow" v-if="type == 'full'">
|
||||
<img :src="advertising[0].img" alt="" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
|||
<div class="decorate">
|
||||
<div class="decorate-title">全屏广告</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="decorate-list">
|
||||
<div
|
||||
|
@ -79,11 +79,11 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { btnWay } from "./btn.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
btnWay, // 按钮类型
|
||||
|
||||
type: "full", // 是否全屏
|
||||
|
||||
//全屏广告
|
||||
|
@ -100,21 +100,16 @@ export default {
|
|||
},
|
||||
methods: {},
|
||||
mounted() {
|
||||
console.log(this.btnWay);
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
|
||||
// 点击链接
|
||||
clickLink(item) {
|
||||
this.$refs.liliDialog.open('link')
|
||||
},
|
||||
|
||||
// 关闭
|
||||
closeDecorate(index) {
|
||||
this.$nextTick(() => {
|
||||
this.btnWay.splice(index, 1);
|
||||
});
|
||||
},
|
||||
|
||||
//点击图片解析成base64
|
||||
changeFile(item, index) {
|
||||
const file = document.getElementById("files" + index).files[0];
|
||||
|
@ -177,4 +172,4 @@ export default {
|
|||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { btnWay } from "./btn.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
@ -95,7 +95,7 @@ export default {
|
|||
},
|
||||
methods: {},
|
||||
mounted() {
|
||||
console.log(this.btnWay);
|
||||
|
||||
},
|
||||
methods: {
|
||||
// 点击链接
|
||||
|
@ -103,12 +103,7 @@ export default {
|
|||
this.$refs.liliDialog.open('link')
|
||||
},
|
||||
|
||||
// 关闭
|
||||
closeDecorate(index) {
|
||||
this.$nextTick(() => {
|
||||
this.btnWay.splice(index, 1);
|
||||
});
|
||||
},
|
||||
|
||||
//点击图片解析成base64
|
||||
changeFile(item, index) {
|
||||
const file = document.getElementById("files" + index).files[0];
|
||||
|
@ -171,4 +166,4 @@ export default {
|
|||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* btnWay 设置悬浮按钮功能
|
||||
*/
|
||||
|
||||
export const btnWay = [
|
||||
{
|
||||
img: require('@/assets/icon.png'),
|
||||
title: "",
|
||||
size:"80*80"
|
||||
},
|
||||
{
|
||||
img: require('@/assets/icon.png'),
|
||||
title: "",
|
||||
size:"80*80"
|
||||
},
|
||||
{
|
||||
img: require('@/assets/icon.png'),
|
||||
title: "",
|
||||
size:"80*80"
|
||||
},
|
||||
];
|
|
@ -1,147 +0,0 @@
|
|||
<template>
|
||||
<div class="model-view">
|
||||
<div class="model-view-content">
|
||||
<div class="content">
|
||||
<div class="wap-title">首页</div>
|
||||
<div class="draggable">
|
||||
<div class="position">
|
||||
<div class="btn-item">
|
||||
<img
|
||||
src="https://shopro-1253949872.file.myqcloud.com/uploads/20200704/f6b9c9d20d21df541ac52e9548486e1a.png"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="btn-item" v-for="(item, index) in btnWay" :key="index">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-config">
|
||||
<div class="decorate">
|
||||
<div class="decorate-title">悬浮按钮</div>
|
||||
|
||||
<div class="decorate-list">
|
||||
<div
|
||||
class="decorate-item"
|
||||
v-for="(item, index) in btnWay"
|
||||
:key="index"
|
||||
>
|
||||
<div class="decorate-item-title">
|
||||
<div>图标{{ index + 1 }}</div>
|
||||
<Icon
|
||||
@click="closeDecorate(index)"
|
||||
size="20"
|
||||
color="#e1251b"
|
||||
type="md-close-circle"
|
||||
/>
|
||||
</div>
|
||||
<div class="decorate-item-box">
|
||||
<!-- 选择照片 -->
|
||||
<div class="decorate-view">
|
||||
<div class="decorate-view-title">选择图标</div>
|
||||
<div>
|
||||
<img class="show-image" :src="item.img" alt />
|
||||
<input
|
||||
type="file"
|
||||
class="hidden-input"
|
||||
@change="changeFile(item, index)"
|
||||
ref="files"
|
||||
:id="'files' + index"
|
||||
/>
|
||||
<div class="tips">
|
||||
建议尺寸
|
||||
<span>{{ item.size }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selectBtn">
|
||||
<Button
|
||||
size="small"
|
||||
@click="handleClickFile(item, index)"
|
||||
ghost
|
||||
type="primary"
|
||||
>选择照片</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="primary" @click="addDecorate()" ghost>添加</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { btnWay } from "./btn.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
btnWay, // 按钮类型
|
||||
};
|
||||
},
|
||||
methods: {},
|
||||
mounted() {
|
||||
console.log(this.btnWay);
|
||||
},
|
||||
methods: {
|
||||
//添加设置
|
||||
addDecorate() {
|
||||
let way = {
|
||||
img: "https://picsum.photos/id/264/200/200",
|
||||
title: "标题",
|
||||
size: this.btnWay[0].size,
|
||||
};
|
||||
this.btnWay.push(way);
|
||||
},
|
||||
|
||||
// 关闭
|
||||
closeDecorate(index) {
|
||||
this.$nextTick(() => {
|
||||
this.btnWay.splice(index, 1);
|
||||
});
|
||||
},
|
||||
//点击图片解析成base64
|
||||
changeFile(item, index) {
|
||||
const file = document.getElementById("files" + index).files[0];
|
||||
if (file == void 0) return false;
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
this.$nextTick((res) => {
|
||||
reader.onload = (e) => {
|
||||
item.img = e.target.result;
|
||||
};
|
||||
});
|
||||
},
|
||||
// 点击选择照片
|
||||
handleClickFile(item, index) {
|
||||
document.getElementById("files" + index).click();
|
||||
// console.log(let files = files)
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
@import "./decorate.scss";
|
||||
.draggable {
|
||||
position: relative;
|
||||
}
|
||||
.position {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 50px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.btn-item {
|
||||
> img {
|
||||
margin: 4px 0;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
import index from './index.vue' //首页
|
||||
import btn from './btn.vue' //按钮
|
||||
|
||||
import advertising from './advertising.vue' //全屏活动
|
||||
import alertAdvertising from './alertAdvertising.vue' //弹窗活动
|
||||
|
||||
|
@ -7,7 +7,7 @@ import alertAdvertising from './alertAdvertising.vue' //弹窗活动
|
|||
|
||||
const templates = {
|
||||
index,
|
||||
btn,
|
||||
|
||||
advertising,
|
||||
alertAdvertising
|
||||
}
|
||||
|
|
|
@ -67,11 +67,7 @@ export default {
|
|||
name: "index",
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
title: "悬浮按钮",
|
||||
name: "btn",
|
||||
selected: false,
|
||||
},
|
||||
|
||||
{
|
||||
title: "全屏广告",
|
||||
name: "advertising",
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
<Button @click="addMember" type="primary">添加会员</Button>
|
||||
</Row>
|
||||
|
||||
<Table :loading="loading" border :columns="columns" :data="data" ref="table" sortable="custom" @on-sort-change="changeSort" @on-selection-change="changeSelect">
|
||||
</Table>
|
||||
<Table :loading="loading" border :columns="columns" :data="data" ref="table" sortable="custom" @on-sort-change="changeSort" @on-selection-change="changeSelect">
|
||||
</Table>
|
||||
<Row type="flex" justify="end" class="page">
|
||||
<Page :current="searchForm.pageNumber" :total="total" :page-size="searchForm.pageSize" @on-change="changePage" @on-page-size-change="changePageSize" :page-size-opts="[10, 20, 50]"
|
||||
size="small" show-total show-elevator show-sizer></Page>
|
||||
<Page :current="searchForm.pageNumber" :total="total" :page-size="searchForm.pageSize" @on-change="changePage" @on-page-size-change="changePageSize" :page-size-opts="[10, 20, 50]" size="small"
|
||||
show-total show-elevator show-sizer></Page>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
|
@ -137,12 +137,14 @@ export default {
|
|||
loading: true, // 表单加载状态
|
||||
addFlag: false, // modal显隐控制
|
||||
updateRegion: false, // 地区
|
||||
addMemberForm: { // 添加用户表单
|
||||
addMemberForm: {
|
||||
// 添加用户表单
|
||||
mobile: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
searchForm: { // 请求参数
|
||||
searchForm: {
|
||||
// 请求参数
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
order: "desc",
|
||||
|
@ -152,7 +154,8 @@ export default {
|
|||
},
|
||||
picModelFlag: false, // 选择图片
|
||||
formValidate: {}, // 表单数据
|
||||
addRule: { // 验证规则
|
||||
addRule: {
|
||||
// 验证规则
|
||||
mobile: [
|
||||
{ required: true, message: "请输入手机号码" },
|
||||
{
|
||||
|
@ -229,6 +232,7 @@ export default {
|
|||
{
|
||||
props: {
|
||||
size: "small",
|
||||
type: params.row.___selected ? "primary" : "",
|
||||
},
|
||||
style: {
|
||||
marginRight: "5px",
|
||||
|
@ -236,11 +240,11 @@ export default {
|
|||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.callback(params.row);
|
||||
this.callback(params.row, params.index);
|
||||
},
|
||||
},
|
||||
},
|
||||
"选择"
|
||||
params.row.___selected ? "已选择" : "选择"
|
||||
),
|
||||
|
||||
h(
|
||||
|
@ -251,7 +255,8 @@ export default {
|
|||
size: "small",
|
||||
},
|
||||
style: {
|
||||
marginRight: "5px", display: this.selectedMember ? "none" : "block",
|
||||
marginRight: "5px",
|
||||
display: this.selectedMember ? "none" : "block",
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
|
@ -270,7 +275,8 @@ export default {
|
|||
ghost: true,
|
||||
},
|
||||
style: {
|
||||
marginRight: "5px", display: this.selectedMember ? "none" : "block",
|
||||
marginRight: "5px",
|
||||
display: this.selectedMember ? "none" : "block",
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
|
@ -308,9 +314,17 @@ export default {
|
|||
total: 0, // 表单数据总数
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 是否为选中模式
|
||||
selectedMember: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 回调给父级
|
||||
callback(val) {
|
||||
callback(val, index) {
|
||||
val.___selected = !val.___selected;
|
||||
this.$emit("callback", val);
|
||||
},
|
||||
init() {
|
||||
|
@ -378,6 +392,9 @@ export default {
|
|||
API_Member.getMemberListData(this.searchForm).then((res) => {
|
||||
if (res.result.records) {
|
||||
this.loading = false;
|
||||
res.result.records.forEach((item) => {
|
||||
item.___selected = false;
|
||||
});
|
||||
this.data = res.result.records;
|
||||
this.total = res.result.total;
|
||||
}
|
||||
|
@ -476,7 +493,7 @@ export default {
|
|||
/deep/ .ivu-table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
/deep/ .ivu-card{
|
||||
/deep/ .ivu-card {
|
||||
width: 100%;
|
||||
}
|
||||
.face {
|
||||
|
|
|
@ -4,21 +4,10 @@
|
|||
<Row>
|
||||
<Form ref="searchForm" :model="searchForm" inline :label-width="70" class="search-form">
|
||||
<Form-item label="活动名称" prop="couponName">
|
||||
<Input
|
||||
type="text"
|
||||
v-model="searchForm.couponName"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
<Input type="text" v-model="searchForm.couponName" placeholder="请输入活动名称" clearable style="width: 200px" />
|
||||
</Form-item>
|
||||
<Form-item label="活动状态" prop="promotionStatus">
|
||||
<Select
|
||||
v-model="searchForm.promotionStatus"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<Select v-model="searchForm.promotionStatus" placeholder="请选择" clearable style="width: 200px">
|
||||
<Option value="NEW">未开始</Option>
|
||||
<Option value="START">已开始/上架</Option>
|
||||
<Option value="END">已结束/下架</Option>
|
||||
|
@ -26,66 +15,30 @@
|
|||
</Select>
|
||||
</Form-item>
|
||||
<Form-item label="活动时间">
|
||||
<DatePicker
|
||||
v-model="selectDate"
|
||||
type="daterange"
|
||||
clearable
|
||||
placeholder="选择起始时间"
|
||||
style="width: 200px"
|
||||
></DatePicker>
|
||||
<DatePicker v-model="selectDate" type="daterange" clearable placeholder="选择起始时间" style="width: 200px"></DatePicker>
|
||||
</Form-item>
|
||||
<Button @click="handleSearch" type="primary" icon="ios-search" class="search-btn">搜索</Button>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row class="operation padding-row">
|
||||
<Button @click="add" type="primary">添加</Button>
|
||||
<Button @click="add" type="primary">添加优惠券</Button>
|
||||
<Button @click="()=>{$router.push({path:'/promotion/add-coupon-specify'})}">精准发劵</Button>
|
||||
<Button @click="delAll">批量下架</Button>
|
||||
<!-- <Button @click="upAll" >批量上架</Button> -->
|
||||
</Row>
|
||||
<Table
|
||||
:loading="loading"
|
||||
border
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
ref="table"
|
||||
sortable="custom"
|
||||
@on-sort-change="changeSort"
|
||||
@on-selection-change="changeSelect"
|
||||
>
|
||||
<template slot-scope="{ row }" slot="action">
|
||||
<Button
|
||||
v-if="row.promotionStatus === 'NEW' || row.promotionStatus === 'CLOSE'"
|
||||
type="primary"
|
||||
size="small"
|
||||
style="margin-right: 10px"
|
||||
@click="edit(row)"
|
||||
>编辑
|
||||
</Button
|
||||
>
|
||||
<Button
|
||||
v-if="row.promotionStatus === 'START' || row.promotionStatus === 'NEW'"
|
||||
type="error"
|
||||
size="small"
|
||||
style="margin-right: 10px"
|
||||
@click="remove(row)"
|
||||
>下架
|
||||
</Button
|
||||
>
|
||||
<Table :loading="loading" border :columns="columns" :data="data" ref="table" sortable="custom" @on-sort-change="changeSort" @on-selection-change="changeSelect">
|
||||
<template slot-scope="{ row,index }" slot="action">
|
||||
<Button v-if="!checked && row.promotionStatus === 'NEW' || row.promotionStatus === 'CLOSE'" type="primary" size="small" style="margin-right: 10px" @click="edit(row)">编辑
|
||||
</Button>
|
||||
<Button v-if="!checked && row.promotionStatus === 'START' || row.promotionStatus === 'NEW'" type="error" size="small" style="margin-right: 10px" @click="remove(row)">下架
|
||||
</Button>
|
||||
<Button v-if="checked" :type="row.___selected ?'primary': '' " @click="check(row,index)">{{row.___selected ?'已':''}}选中
|
||||
</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<Row type="flex" justify="end" class="page">
|
||||
<Page
|
||||
:current="searchForm.pageNumber + 1"
|
||||
:total="total"
|
||||
:page-size="searchForm.pageSize"
|
||||
@on-change="changePage"
|
||||
@on-page-size-change="changePageSize"
|
||||
:page-size-opts="[10, 20, 50]"
|
||||
size="small"
|
||||
show-total
|
||||
show-elevator
|
||||
show-sizer
|
||||
></Page>
|
||||
<Page :current="searchForm.pageNumber + 1" :total="total" :page-size="searchForm.pageSize" @on-change="changePage" @on-page-size-change="changePageSize" :page-size-opts="[10, 20, 50]"
|
||||
size="small" show-total show-elevator show-sizer></Page>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -94,7 +47,6 @@
|
|||
<script>
|
||||
import {
|
||||
getPlatformCouponList,
|
||||
deletePlatformCoupon,
|
||||
updatePlatformCouponStatus,
|
||||
} from "@/api/promotion";
|
||||
|
||||
|
@ -121,7 +73,7 @@ export default {
|
|||
// 表单验证规则
|
||||
formValidate: {
|
||||
promotionName: [
|
||||
{required: true, message: "不能为空", trigger: "blur"},
|
||||
{ required: true, message: "不能为空", trigger: "blur" },
|
||||
],
|
||||
},
|
||||
submitLoading: false, // 添加或编辑提交状态
|
||||
|
@ -138,15 +90,16 @@ export default {
|
|||
{
|
||||
title: "活动名称",
|
||||
key: "promotionName",
|
||||
minWidth: 120,
|
||||
|
||||
fixed: "left",
|
||||
},
|
||||
{
|
||||
title: "优惠券名称",
|
||||
key: "couponName",
|
||||
minWidth: 120,
|
||||
tooltip: true
|
||||
}, {
|
||||
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: "面额/折扣",
|
||||
key: "price",
|
||||
width: 120,
|
||||
|
@ -165,10 +118,13 @@ export default {
|
|||
{
|
||||
title: "领取数量/总数量",
|
||||
key: "publishNum",
|
||||
width: 150,
|
||||
render: (h, params) => {
|
||||
return h(
|
||||
"div", params.row.receivedNum + "/" + params.row.publishNum)
|
||||
}
|
||||
"div",
|
||||
params.row.receivedNum + "/" + params.row.publishNum
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "优惠券类型",
|
||||
|
@ -204,20 +160,23 @@ export default {
|
|||
},
|
||||
{
|
||||
title: "活动时间",
|
||||
width: 120,
|
||||
render: (h, params) => {
|
||||
return h("div", {
|
||||
domProps:
|
||||
{innerHTML: params.row.startTime + "<br/>" + params.row.endTime}
|
||||
domProps: {
|
||||
innerHTML: params.row.startTime + "<br/>" + params.row.endTime,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
width: 100,
|
||||
key: "promotionStatus",
|
||||
fixed: "right",
|
||||
render: (h, params) => {
|
||||
let text = "未知",
|
||||
color = "";
|
||||
color = "red";
|
||||
if (params.row.promotionStatus == "NEW") {
|
||||
text = "未开始";
|
||||
color = "default";
|
||||
|
@ -249,13 +208,20 @@ export default {
|
|||
slot: "action",
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
width: 80
|
||||
width: 100,
|
||||
},
|
||||
],
|
||||
data: [], // 表单数据
|
||||
total: 0, // 表单数据总数
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 是否为选中模式
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (to.fullPath == "/promotion/manager-coupon") {
|
||||
|
@ -264,18 +230,25 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
// 选中优惠券 父级传值
|
||||
check(val,index) {
|
||||
|
||||
this.data[index].___selected = !this.data[index].___selected
|
||||
|
||||
this.$emit("selected", val);
|
||||
},
|
||||
init() {
|
||||
this.getDataList();
|
||||
},
|
||||
add() {
|
||||
this.$router.push({name: "add-platform-coupon"});
|
||||
this.$router.push({ name: "add-platform-coupon" });
|
||||
},
|
||||
/** 跳转至领取详情页面 */
|
||||
receiveInfo(v) {
|
||||
this.$router.push({name: "member-receive-coupon", query: {id: v.id}});
|
||||
this.$router.push({ name: "member-receive-coupon", query: { id: v.id } });
|
||||
},
|
||||
info(v) {
|
||||
this.$router.push({name: "platform-coupon-info", query: {id: v.id}});
|
||||
this.$router.push({ name: "platform-coupon-info", query: { id: v.id } });
|
||||
},
|
||||
changePage(v) {
|
||||
this.searchForm.pageNumber = v - 1;
|
||||
|
@ -319,6 +292,9 @@ export default {
|
|||
getPlatformCouponList(this.searchForm).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
res.result.records.forEach(item=>{
|
||||
item.___selected = false
|
||||
})
|
||||
this.data = res.result.records;
|
||||
this.total = res.result.total;
|
||||
}
|
||||
|
@ -360,7 +336,7 @@ export default {
|
|||
});
|
||||
},
|
||||
edit(v) {
|
||||
this.$router.push({name: "edit-platform-coupon", query: {id: v.id}});
|
||||
this.$router.push({ name: "edit-platform-coupon", query: { id: v.id } });
|
||||
},
|
||||
remove(v) {
|
||||
this.$Modal.confirm({
|
||||
|
@ -370,7 +346,10 @@ export default {
|
|||
loading: true,
|
||||
onOk: () => {
|
||||
// 删除
|
||||
updatePlatformCouponStatus({couponIds: v.id, promotionStatus: "CLOSE"})
|
||||
updatePlatformCouponStatus({
|
||||
couponIds: v.id,
|
||||
promotionStatus: "CLOSE",
|
||||
})
|
||||
.then((res) => {
|
||||
this.$Modal.remove();
|
||||
if (res.success) {
|
||||
|
|
|
@ -50,16 +50,27 @@
|
|||
<Input v-model="form.couponLimitNum" placeholder="领取限制" clearable style="width: 260px" />
|
||||
</FormItem>
|
||||
<FormItem label="有效期" prop="rangeTime">
|
||||
<DatePicker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择"
|
||||
:options="options"
|
||||
style="width: 260px"
|
||||
>
|
||||
</DatePicker>
|
||||
<div v-if="form.getType == 'ACTIVITY'">
|
||||
<RadioGroup v-model="rangeTimeType">
|
||||
|
||||
<Radio :label="1">
|
||||
起止时间
|
||||
</Radio>
|
||||
<Radio :label="0">固定时间</Radio>
|
||||
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div v-if="rangeTimeType == 1">
|
||||
<DatePicker type="datetimerange" v-model="form.rangeTime" format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" :options="options" style="width: 260px">
|
||||
</DatePicker>
|
||||
</div>
|
||||
<div class="effectiveDays" v-if="rangeTimeType == 0">
|
||||
领取当天开始
|
||||
<InputNumber v-model="form.effectiveDays" :min="1" style="width:100px;" :max="365" />
|
||||
天内有效(1-365间的整数)
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="使用范围" prop="scopeType">
|
||||
<RadioGroup type="button" button-style="solid" v-model="form.scopeType">
|
||||
<Radio label="ALL">全品类</Radio>
|
||||
|
@ -116,6 +127,16 @@ export default {
|
|||
components: {
|
||||
skuSelect,
|
||||
},
|
||||
watch: {
|
||||
"form.getType": {
|
||||
handler(val) {
|
||||
if (val == "FREE") {
|
||||
this.rangeTimeType = 1;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const checkPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
|
@ -140,6 +161,7 @@ export default {
|
|||
}
|
||||
};
|
||||
return {
|
||||
rangeTimeType: 1,
|
||||
modalType: 0, // 是否编辑
|
||||
form: {
|
||||
/** 店铺承担比例 */
|
||||
|
@ -154,9 +176,11 @@ export default {
|
|||
couponType: "PRICE",
|
||||
/** 优惠券名称 */
|
||||
couponName: "",
|
||||
promotionName: "",
|
||||
getType: "FREE",
|
||||
promotionGoodsList: [],
|
||||
scopeIdGoods: [],
|
||||
rangeDayType: "",
|
||||
},
|
||||
id: this.$route.query.id, // 优惠券id
|
||||
submitLoading: false, // 添加或编辑提交状态
|
||||
|
@ -314,13 +338,24 @@ export default {
|
|||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
const params = JSON.parse(JSON.stringify(this.form));
|
||||
params.startTime = this.$options.filters.unixToDate(
|
||||
this.form.rangeTime[0] / 1000
|
||||
);
|
||||
params.endTime = this.$options.filters.unixToDate(
|
||||
this.form.rangeTime[1] / 1000
|
||||
);
|
||||
delete params.rangeTime
|
||||
// 判断当前活动类型
|
||||
params.getType != "ACTIVITY" ? delete params.effectiveDays : "";
|
||||
|
||||
//判断当前时间类型
|
||||
if (this.rangeTimeType == 1) {
|
||||
params.rangeDayType = "FIXEDTIME";
|
||||
params.startTime = this.$options.filters.unixToDate(
|
||||
this.form.rangeTime[0] / 1000
|
||||
);
|
||||
params.endTime = this.$options.filters.unixToDate(
|
||||
this.form.rangeTime[1] / 1000
|
||||
);
|
||||
delete params.effectiveDays;
|
||||
} else {
|
||||
params.rangeDayType = "DYNAMICTIME";
|
||||
delete params.rangeTime;
|
||||
}
|
||||
|
||||
let scopeId = [];
|
||||
|
||||
if (
|
||||
|
@ -392,12 +427,13 @@ export default {
|
|||
);
|
||||
this.$router.go(-1);
|
||||
},
|
||||
openSkuList() { // 显示商品选择器
|
||||
openSkuList() {
|
||||
// 显示商品选择器
|
||||
this.$refs.skuSelect.open("goods");
|
||||
let data = JSON.parse(JSON.stringify(this.form.promotionGoodsList))
|
||||
data.forEach(e => {
|
||||
e.id = e.skuId
|
||||
})
|
||||
let data = JSON.parse(JSON.stringify(this.form.promotionGoodsList));
|
||||
data.forEach((e) => {
|
||||
e.id = e.skuId;
|
||||
});
|
||||
this.$refs.skuSelect.goodsData = data;
|
||||
},
|
||||
changeSelect(e) {
|
||||
|
@ -434,15 +470,15 @@ export default {
|
|||
// 回显已选商品
|
||||
let list = [];
|
||||
item.forEach((e) => {
|
||||
list.push({
|
||||
goodsName: e.goodsName,
|
||||
price: e.price,
|
||||
originalPrice: e.price,
|
||||
quantity: e.quantity,
|
||||
storeId: e.storeId,
|
||||
storeName: e.storeName,
|
||||
skuId: e.id,
|
||||
});
|
||||
list.push({
|
||||
goodsName: e.goodsName,
|
||||
price: e.price,
|
||||
originalPrice: e.price,
|
||||
quantity: e.quantity,
|
||||
storeId: e.storeId,
|
||||
storeName: e.storeName,
|
||||
skuId: e.id,
|
||||
});
|
||||
});
|
||||
this.form.promotionGoodsList = list;
|
||||
},
|
||||
|
@ -527,5 +563,12 @@ h4 {
|
|||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<div>
|
||||
<Card>
|
||||
<Form ref="form" :model="form" :label-width="120">
|
||||
<div class="base-info-item">
|
||||
<h4>优惠券将在指定发放时间发放到用户账号中</h4>
|
||||
<div class="form-item-view">
|
||||
<FormItem label="活动名称" prop="promotionName">
|
||||
<Input type="text" v-model="form.promotionName" placeholder="活动名称" clearable style="width: 260px" />
|
||||
</FormItem>
|
||||
<FormItem label="目标客户" prop="vipType">
|
||||
<RadioGroup v-model="vipType">
|
||||
<Radio :label="0">全平台客户</Radio>
|
||||
<Radio :label="1">指定客户</Radio>
|
||||
</RadioGroup>
|
||||
<Button type="primary" v-if="vipType==1" icon="ios-add" @click="addVip" ghost>添加客户</Button>
|
||||
</FormItem>
|
||||
<FormItem label="发放时间" prop="couponDiscount">
|
||||
<DatePicker type="datetime" v-model="form.rangeTime" format="yyyy-MM-dd HH:mm:ss" placeholder="请选择" :options="options" style="width: 260px">
|
||||
</DatePicker>
|
||||
</FormItem>
|
||||
<FormItem label="选择优惠券" prop="couponType">
|
||||
<Button type="primary" icon="ios-add" @click="checkCoupon=!checkCoupon" ghost>选择优惠券</Button>
|
||||
<Table class="table" :columns="couponColumns" :data="couponData"></Table>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div style="margin-left:100px">
|
||||
<Button type="text" @click="closeCurrentPage">返回</Button>
|
||||
<Button type="primary" :loading="submitLoading" @click="handleSubmit">提交</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
<Modal width="1200" v-model="checkCoupon">
|
||||
<couponList checked @selected="callbackSelectCoupon" />
|
||||
</Modal>
|
||||
<Modal width="1200" v-model="checkUserList">
|
||||
<userList @selected="callbackSelectUser" ref="memberLayout" />
|
||||
</Modal>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addCouponActivity } from "@/api/promotion";
|
||||
|
||||
import couponList from "./coupon";
|
||||
import userList from "@/views/member/list/index";
|
||||
// import userList from ''
|
||||
export default {
|
||||
components: {
|
||||
couponList,
|
||||
userList,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 选择优惠券
|
||||
checkCoupon: false,
|
||||
// 选择用户
|
||||
checkUserList: false,
|
||||
// 优惠券表格title
|
||||
couponColumns: [
|
||||
{
|
||||
title: "优惠券名称",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "有效期",
|
||||
key: "age",
|
||||
},
|
||||
{
|
||||
title: "优惠券数量",
|
||||
key: "address",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
},
|
||||
],
|
||||
// datpicker时间设置
|
||||
options: {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
},
|
||||
},
|
||||
|
||||
//
|
||||
vipType: 0, //客户会员类型 0全平台客户 1指定客户
|
||||
form: {},
|
||||
formRule: {},
|
||||
id: this.$route.query.id, // 优惠券id
|
||||
|
||||
callbackCoupon: [],
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
|
||||
// 添加指定用户
|
||||
addVip() {
|
||||
this.checkUserList = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.memberLayout.selectedMember = true;
|
||||
});
|
||||
},
|
||||
// 返回已选择的用户
|
||||
callbackSelectUser(val){
|
||||
console.log(val)
|
||||
},
|
||||
|
||||
// 返回已选择的优惠券
|
||||
callbackSelectCoupon(val) {
|
||||
if (val.___selected) {
|
||||
this.callbackCoupon.forEach((item, index) => {
|
||||
if (item.id == val.id) this.callbackCoupon.splice(index, 1);
|
||||
});
|
||||
} else {
|
||||
this.callbackCoupon.push(val);
|
||||
}
|
||||
},
|
||||
// 关闭当前页面
|
||||
closeCurrentPage() {
|
||||
this.$store.commit("removeTag", "add-coupon-specify");
|
||||
localStorage.pageOpenedList = JSON.stringify(
|
||||
this.$store.state.app.pageOpenedList
|
||||
);
|
||||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
let res = await addCouponActivity();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scpoed>
|
||||
.table {
|
||||
width: 800px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
.form-item-view {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,29 +2,12 @@
|
|||
<div class="seckill">
|
||||
<Card>
|
||||
<Row>
|
||||
<Form
|
||||
ref="searchForm"
|
||||
:model="searchForm"
|
||||
inline
|
||||
:label-width="70"
|
||||
class="search-form"
|
||||
>
|
||||
<Form ref="searchForm" :model="searchForm" inline :label-width="70" class="search-form">
|
||||
<Form-item label="活动名称" prop="promotionName">
|
||||
<Input
|
||||
type="text"
|
||||
v-model="searchForm.promotionName"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
<Input type="text" v-model="searchForm.promotionName" placeholder="请输入活动名称" clearable style="width: 200px" />
|
||||
</Form-item>
|
||||
<Form-item label="活动状态" prop="promotionStatus">
|
||||
<Select
|
||||
v-model="searchForm.promotionStatus"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<Select v-model="searchForm.promotionStatus" placeholder="请选择" clearable style="width: 200px">
|
||||
<Option value="NEW">未开始</Option>
|
||||
<Option value="START">已开始/上架</Option>
|
||||
<Option value="END">已结束/下架</Option>
|
||||
|
@ -32,120 +15,69 @@
|
|||
</Select>
|
||||
</Form-item>
|
||||
<Form-item label="活动时间">
|
||||
<DatePicker
|
||||
v-model="selectDate"
|
||||
type="daterange"
|
||||
clearable
|
||||
placeholder="选择起始时间"
|
||||
style="width: 200px"
|
||||
></DatePicker>
|
||||
<DatePicker v-model="selectDate" type="daterange" clearable placeholder="选择起始时间" style="width: 200px"></DatePicker>
|
||||
</Form-item>
|
||||
|
||||
<Button
|
||||
@click="handleSearch"
|
||||
type="primary"
|
||||
icon="ios-search"
|
||||
class="search-btn"
|
||||
>搜索</Button
|
||||
>
|
||||
<Button @click="handleSearch" type="primary" icon="ios-search" class="search-btn">搜索</Button>
|
||||
</Form>
|
||||
</Row>
|
||||
<Row class="operation padding-row">
|
||||
<Button type="primary" @click="add">添加活动</Button>
|
||||
</Row>
|
||||
<Tabs value="list" @on-click="clickTabPane">
|
||||
<TabPane label="秒杀活动列表" name="list">
|
||||
<Table :loading="loading" border :columns="columns" :data="data" ref="table" class="page">
|
||||
<template slot-scope="{ row }" slot="action">
|
||||
<Button type="info" size="small" class="mr_5" v-if="row.promotionStatus == 'NEW'" @click="edit(row)">编辑</Button>
|
||||
|
||||
<Table
|
||||
:loading="loading"
|
||||
border
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="page"
|
||||
>
|
||||
<template slot-scope="{ row }" slot="action">
|
||||
<Button
|
||||
type="info"
|
||||
size="small"
|
||||
class="mr_5"
|
||||
v-if="row.promotionStatus == 'NEW'"
|
||||
@click="edit(row)"
|
||||
>编辑</Button
|
||||
>
|
||||
<Button type="info" size="small" class="mr_5" v-else @click="manage(row)">查看</Button>
|
||||
|
||||
<Button
|
||||
type="info"
|
||||
size="small"
|
||||
class="mr_5"
|
||||
v-else
|
||||
@click="manage(row)"
|
||||
>查看</Button
|
||||
>
|
||||
<Button type="primary" size="small" class="mr_5" v-if="row.promotionStatus == 'NEW'" @click="manage(row)">管理</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="mr_5"
|
||||
v-if="row.promotionStatus == 'NEW'"
|
||||
@click="manage(row)"
|
||||
>管理</Button
|
||||
>
|
||||
|
||||
<!-- <Button type="success" size="small" class="mr_5" v-if="row.promotionStatus == 'NEW' || row.promotionStatus == 'END'" @click="upper(row)">上架</Button> -->
|
||||
<Button
|
||||
type="error"
|
||||
size="small"
|
||||
v-if="
|
||||
<Button type="error" size="small" v-if="
|
||||
row.promotionStatus == 'START' || row.promotionStatus == 'NEW'
|
||||
"
|
||||
class="mr_5"
|
||||
@click="off(row)"
|
||||
>下架</Button
|
||||
>
|
||||
|
||||
<Button
|
||||
type="error"
|
||||
size="small"
|
||||
v-if="row.promotionStatus == 'CLOSE'"
|
||||
ghost
|
||||
@click="expire(row)"
|
||||
>删除</Button
|
||||
>
|
||||
</template>
|
||||
</Table>
|
||||
" class="mr_5" @click="off(row)">下架</Button>
|
||||
|
||||
<Button type="error" size="small" v-if="row.promotionStatus == 'CLOSE'" ghost @click="expire(row)">删除</Button>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<Row type="flex" justify="end" class="page">
|
||||
<Page style="margin: 20px 0;" :current="searchForm.pageNumber" :total="total" :page-size="searchForm.pageSize" @on-change="changePage" @on-page-size-change="changePageSize"
|
||||
:page-size-opts="[10, 20, 50]" size="small" show-total show-elevator show-sizer></Page>
|
||||
</Row>
|
||||
</TabPane>
|
||||
<TabPane label="秒杀活动设置" name="setup">
|
||||
|
||||
<setupSeckill v-if="setupFlag"></setupSeckill>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Row type="flex" justify="end" class="page">
|
||||
<Page
|
||||
:current="searchForm.pageNumber + 1"
|
||||
:total="total"
|
||||
:page-size="searchForm.pageSize"
|
||||
@on-change="changePage"
|
||||
@on-page-size-change="changePageSize"
|
||||
:page-size-opts="[10, 20, 50]"
|
||||
size="small"
|
||||
show-total
|
||||
show-elevator
|
||||
show-sizer
|
||||
></Page>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSeckillList, delSeckill, closeSeckill } from "@/api/promotion";
|
||||
import setupSeckill from "@/views/promotion/seckill/setupSeckill";
|
||||
export default {
|
||||
name: "seckill",
|
||||
components: {
|
||||
setupSeckill,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true, // 表单加载状态
|
||||
searchForm: {
|
||||
// 搜索框初始化对象
|
||||
pageNumber: 0, // 当前页数
|
||||
pageNumber: 1, // 当前页数
|
||||
pageSize: 10, // 页面大小
|
||||
sort: "startTime",
|
||||
order: "desc", // 默认排序方式
|
||||
},
|
||||
columns: [ // 表单
|
||||
setupFlag: false, //默认不请求设置
|
||||
columns: [
|
||||
// 表单
|
||||
{
|
||||
title: "活动名称",
|
||||
key: "promotionName",
|
||||
|
@ -227,14 +159,27 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
clickTabPane(name) {
|
||||
|
||||
if (name == "setup") {
|
||||
|
||||
this.setupFlag = true;
|
||||
} else {
|
||||
this.setupFlag = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化信息
|
||||
init() {
|
||||
this.getDataList();
|
||||
},
|
||||
// 点击分页
|
||||
changePage(v) {
|
||||
this.searchForm.pageNumber = v - 1;
|
||||
this.searchForm.pageNumber = v;
|
||||
this.getDataList();
|
||||
this.clearSelectAll();
|
||||
},
|
||||
// 设置每页大小
|
||||
changePageSize(v) {
|
||||
this.searchForm.pageSize = v;
|
||||
this.getDataList();
|
||||
|
@ -287,6 +232,7 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
// 获取数据集合
|
||||
getDataList() {
|
||||
this.loading = true;
|
||||
if (this.selectDate && this.selectDate[0] && this.selectDate[1]) {
|
||||
|
@ -313,7 +259,7 @@ export default {
|
|||
</script>
|
||||
<style lang="scss">
|
||||
@import "@/styles/table-common.scss";
|
||||
.mr_5{
|
||||
.mr_5 {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div>
|
||||
<Form :model="form" :label-width="120">
|
||||
<FormItem label="每日场次设置">
|
||||
<Row :gutter="16" class="row">
|
||||
<Col class="time-item" @click.native="handleClickTime(item,index)" v-for="(item,index) in this.times" :key="index" span="3">
|
||||
<div class="time" :class="{'active':item.check}">{{item.time}}:00</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem label="秒杀规则">
|
||||
<Input type="textarea" :autosize="{minRows: 4,}" v-model="form.seckillRule" placeholder="申请规则" clearable style="width: 360px; margin-left:10px" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="foot-btn">
|
||||
<Button @click="closeCurrentPage" style="margin-right: 5px">返回</Button>
|
||||
<Button type="primary" :loading="submitLoading" @click="handleSubmit">提交</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!-- 选择时间 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from "@/api/index";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
submitLoading: false,
|
||||
selectedTime: [],
|
||||
times: [], //时间集合 1-24点
|
||||
form: {
|
||||
seckillRule: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 关闭当前页面
|
||||
*/
|
||||
closeCurrentPage() {
|
||||
this.$store.commit("removeTag", "manager-seckill-add");
|
||||
localStorage.pageOpenedList = JSON.stringify(
|
||||
this.$store.state.app.pageOpenedList
|
||||
);
|
||||
this.$router.go(-1);
|
||||
},
|
||||
/**
|
||||
* 提交秒杀信息
|
||||
*/
|
||||
async handleSubmit() {
|
||||
let hours = this.times
|
||||
.filter((item) => {
|
||||
return item.check;
|
||||
})
|
||||
.map((item) => {
|
||||
return item.time;
|
||||
})
|
||||
.join(",");
|
||||
|
||||
let result = await setSetting("SECKILL_SETTING", {
|
||||
seckillRule: this.form.seckillRule,
|
||||
hours,
|
||||
});
|
||||
if (result.success) {
|
||||
this.$Message.success("设置成功!");
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化当前信息
|
||||
*/
|
||||
async init() {
|
||||
let result = await getSetting("SECKILL_SETTING");
|
||||
if (result.success) {
|
||||
this.form.seckillRule = result.result.seckillRule;
|
||||
this.times=[]
|
||||
for (let i = 0; i < 24; i++) {
|
||||
// 将数据拆出
|
||||
if (result.result.hours) {
|
||||
let way = result.result.hours.split(",");
|
||||
way.forEach((hours) => {
|
||||
if (hours == i) {
|
||||
this.times.push({
|
||||
time: i,
|
||||
check: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!this.times[i]) {
|
||||
this.times.push({
|
||||
time: i,
|
||||
check: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选中时间
|
||||
*/
|
||||
handleClickTime(val, index) {
|
||||
val.check = !val.check;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.row {
|
||||
width: 50%;
|
||||
}
|
||||
.foot-btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.time-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.active {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
color: #fff;
|
||||
background: $theme_color !important;
|
||||
}
|
||||
.time {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: 0.35s;
|
||||
border-radius: 0.8em;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
background: #f3f5f7;
|
||||
height: 100%;
|
||||
}
|
||||
.time-item {
|
||||
height: 50px;
|
||||
margin: 8px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
|
@ -224,7 +224,6 @@ import memberLayout from "@/views/member/list/index";
|
|||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { getCategoryTree } from "@/api/goods";
|
||||
import { shopDetail, shopAdd, shopEdit, getShopByMemberId } from "@/api/shops";
|
||||
import * as filters from "@/utils/filters";
|
||||
import uploadPicInput from "@/views/my-components/lili/upload-pic-input";
|
||||
import region from "@/views/lili-components/region";
|
||||
import liliMap from "@/views/my-components/map/index";
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
<Scroll :on-reach-bottom="memberSearchEdge">
|
||||
|
||||
<div dis-hover v-for="(item, index) in members" :key="index" class="scroll-card">
|
||||
<Button class="btns" :class="{'active':item.___selected}" @click="moveMember(index,item)" style="width: 100%;text-align: left">
|
||||
<Button class="btns" :class="{'active':item.____selected}" @click="moveMember(index,item)" style="width: 100%;text-align: left">
|
||||
<span v-if="item.mobile" class="mobile">
|
||||
{{item.mobile}}
|
||||
</span>
|
||||
|
@ -501,8 +501,8 @@ export default {
|
|||
|
||||
|
||||
this.members.forEach((item,index)=>{
|
||||
if(item.___selected && item.mobile == val.mobile){
|
||||
item.___selected = false
|
||||
if(item.____selected && item.mobile == val.mobile){
|
||||
item.____selected = false
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -543,7 +543,7 @@ export default {
|
|||
this.$Message.error("当前用户暂无手机号绑定");
|
||||
return false;
|
||||
}
|
||||
item.___selected = true;
|
||||
item.____selected = true;
|
||||
if (this.alreadyCheck.length == 0) {
|
||||
this.alreadyCheck.push(item.mobile);
|
||||
this.alreadyCheckShow.push(item);
|
||||
|
@ -581,7 +581,7 @@ export default {
|
|||
this.loading = false;
|
||||
if (res.success) {
|
||||
res.result.records.forEach((item) => {
|
||||
item.___selected = false;
|
||||
item.____selected = false;
|
||||
this.members.push(item);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 dandavis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,123 +0,0 @@
|
|||
# download
|
||||
========
|
||||
|
||||
## Summary
|
||||
---------
|
||||
The download() function is used to trigger a file download from JavaScript.
|
||||
|
||||
It specifies the contents and name of a new file placed in the browser's download directory. The input can be a URL, String, Blob, or Typed Array of data, or via a dataURL representing the file's data as base64 or url-encoded string. No matter the input format, download() saves a file using the specified file name and mime information in the same manner as a server using a Content-Disposition HTTP header.
|
||||
|
||||
## Getting and Using
|
||||
---------
|
||||
|
||||
### Via NPM/Bower
|
||||
`npm install downloadjs` <br />
|
||||
`bower install downloadjs`
|
||||
|
||||
`require("downloadjs")(data, strFileName, strMimeType);`
|
||||
|
||||
### Simple global `download` function via `<script>` include
|
||||
download(data, strFileName, strMimeType);
|
||||
|
||||
### Included via AMD
|
||||
require(['path/to/file'], function(download) {
|
||||
download(data, strFileName, strMimeType);
|
||||
});
|
||||
|
||||
|
||||
### Parameters
|
||||
---------
|
||||
* **data** - The Blob, File, String, or dataURL containing the soon-to-be File's contents.
|
||||
* **strFileName** - The name of the file to be created. Note that older browsers (like FF3.5, Ch5) don't honor the file name you provide, instead they automatically name the downloaded file.
|
||||
* **strMimeType** - The MIME content-type of the file to download. While optional, it helps the browser present friendlier information about the download to the user, encouraging them to accept the download.
|
||||
|
||||
|
||||
|
||||
## Example Usage
|
||||
---------
|
||||
|
||||
|
||||
### Plain Text
|
||||
#### text string - [live demo](http://pagedemos.com/hw24em95rsfq/output/)
|
||||
download("hello world", "dlText.txt", "text/plain");
|
||||
|
||||
#### text dataURL - [live demo](http://pagedemos.com/r9ywm98s6b29/output/)
|
||||
download("data:text/plain,hello%20world", "dlDataUrlText.txt", "text/plain");
|
||||
|
||||
#### text blob - [live demo](http://pagedemos.com/ckcah2vp8kza/output/)
|
||||
download(new Blob(["hello world"]), "dlTextBlob.txt", "text/plain");
|
||||
|
||||
#### text url - [live demo](http://pagedemos.com/pz6hkyqutjtw/output/)
|
||||
download("/robots.txt");
|
||||
|
||||
|
||||
#### text UInt8 Array - [live demo](http://pagedemos.com/zuyk46wbkktq/output/)
|
||||
var str= "hello world", arr= new Uint8Array(str.length);
|
||||
str.split("").forEach(function(a,b){
|
||||
arr[b]=a.charCodeAt();
|
||||
});
|
||||
|
||||
download( arr, "textUInt8Array.txt", "text/plain" );
|
||||
|
||||
### HTML
|
||||
#### html string - [live demo](http://pagedemos.com/k7rwq7msu3eb/output/)
|
||||
download(document.documentElement.outerHTML, "dlHTML.html", "text/html");
|
||||
|
||||
#### html Blob - [live demo](http://pagedemos.com/bxehm2fdf3g4/output/)
|
||||
download(new Blob(["hello world".bold()]), "dlHtmlBlob.html", "text/html");
|
||||
|
||||
#### ajax callback - [live demo](http://pagedemos.com/arr2ym74aw8t/output/)
|
||||
(note that callback mode won't work on vanilla ajax or with binary files)
|
||||
|
||||
$.ajax({
|
||||
url: "/download.html",
|
||||
success: download.bind(true, "text/html", "dlAjaxCallback.html")
|
||||
});
|
||||
|
||||
|
||||
### Binary Files
|
||||
#### image from URL - [live demo](http://pagedemos.com/yvvmxbjrwq7u/output/)
|
||||
download("/diff6.png");
|
||||
|
||||
#### Image via ajax for custom filename - [live demo](http://pagedemos.com/v2848zfgwrju/output/)
|
||||
var x=new XMLHttpRequest();
|
||||
x.open( "GET", "/diff6.png" , true);
|
||||
x.responseType="blob";
|
||||
x.onload= function(e){download(e.target.response, "awesomesauce.png", "image/png");};
|
||||
x.send();
|
||||
|
||||
|
||||
## Compatibility
|
||||
---------
|
||||
download.js works with a wide range of devices and browsers.
|
||||
|
||||
You can expect it to work for the vast majority of your users, with some common-sense limits:
|
||||
|
||||
* Devices without file systems like iPhone, iPad, Wii, et al. have nowhere to save the file to, sorry.
|
||||
* Android support starts at 4.2 for the built-in browser, though chrome 36+ and firefox 20+ on android 2.3+ work well.
|
||||
* Devices without Blob support won't be able to download Blobs or TypedArrays
|
||||
* Legacy devices (no a[download]) support can only download a few hundred kilobytes of data, and can't give the file a custom name.
|
||||
* Devices without window.URL support can only download a couple megabytes of data
|
||||
* IE versions of 9 and before are NOT supported because the don't support a[download] or dataURL frame locations.
|
||||
|
||||
|
||||
## FAQ
|
||||
---------
|
||||
|
||||
* `Can I tell when a download is done/canceled?` No.
|
||||
* `How can I style the temporary download link?` Define CSS class styles for `.download-js-link`.
|
||||
* `What's up with Safari?` I don't know either but pull requests that improve the situation are welcome.
|
||||
* `Why is my binary file corrupted?` Likely: an incorrect MIME or using jQuery ajax, which has no bin support.
|
||||
* `How big of files work?` Depends, try yourself: [File Echo Demo](http://pagedemos.com/gqs6hbmjcpem/)... I do a 1GB dl routinely on a thinkpad...
|
||||
|
||||
|
||||
## Change Log (v4.1)
|
||||
---------
|
||||
* 2008 :: landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
||||
* 2012 :: added named files via a[download], msSaveBlob() for IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
||||
* 2014 :: added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support
|
||||
* 2015 :: converted to amd/commonJS module with browser-friendly fallback
|
||||
* 2015 :: 4.1 added direct URL downloading via a single URL argument.
|
||||
* 2016 :: 4.2 added large dataURL support, a more semantic codebase, and hidden temp links
|
||||
* 2017 :: added support for empty dataURLs
|
||||
* 20XX :: ???? Considering Zip, Tar, and other multi-file outputs, Blob.prototype.download option, and more, stay tuned folks.
|
|
@ -1,167 +0,0 @@
|
|||
//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
|
||||
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
||||
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
||||
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
|
||||
// v4 adds AMD/UMD, commonJS, and plain browser support
|
||||
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
|
||||
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
|
||||
// https://github.com/rndme/download
|
||||
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define([], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory();
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.download = factory();
|
||||
}
|
||||
}(this, function () {
|
||||
|
||||
return function download(data, strFileName, strMimeType) {
|
||||
|
||||
var self = window, // this script is only for browsers anyway...
|
||||
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
|
||||
mimeType = strMimeType || defaultMime,
|
||||
payload = data,
|
||||
url = !strFileName && !strMimeType && payload,
|
||||
anchor = document.createElement("a"),
|
||||
toString = function(a){return String(a);},
|
||||
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
|
||||
fileName = strFileName || "download",
|
||||
blob,
|
||||
reader;
|
||||
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
|
||||
|
||||
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
||||
payload=[payload, mimeType];
|
||||
mimeType=payload[0];
|
||||
payload=payload[1];
|
||||
}
|
||||
|
||||
|
||||
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
|
||||
fileName = url.split("/").pop().split("?")[0];
|
||||
anchor.href = url; // assign href prop to temp anchor
|
||||
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
|
||||
var ajax=new XMLHttpRequest();
|
||||
ajax.open( "GET", url, true);
|
||||
ajax.responseType = 'blob';
|
||||
ajax.onload= function(e){
|
||||
download(e.target.response, fileName, defaultMime);
|
||||
};
|
||||
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
|
||||
return ajax;
|
||||
} // end if valid url?
|
||||
} // end if url?
|
||||
|
||||
|
||||
//go ahead and download dataURLs right away
|
||||
if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
|
||||
|
||||
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
|
||||
payload=dataUrlToBlob(payload);
|
||||
mimeType=payload.type || defaultMime;
|
||||
}else{
|
||||
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
|
||||
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
|
||||
saver(payload) ; // everyone else can save dataURLs un-processed
|
||||
}
|
||||
|
||||
}else{//not data url, is it a string with special needs?
|
||||
if(/([\x80-\xff])/.test(payload)){
|
||||
var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
|
||||
for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
|
||||
payload=new myBlob([tempUiArr], {type: mimeType});
|
||||
}
|
||||
}
|
||||
blob = payload instanceof myBlob ?
|
||||
payload :
|
||||
new myBlob([payload], {type: mimeType}) ;
|
||||
|
||||
|
||||
function dataUrlToBlob(strUrl) {
|
||||
var parts= strUrl.split(/[:;,]/),
|
||||
type= parts[1],
|
||||
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
|
||||
binData= decoder( parts.pop() ),
|
||||
mx= binData.length,
|
||||
i= 0,
|
||||
uiArr= new Uint8Array(mx);
|
||||
|
||||
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
|
||||
|
||||
return new myBlob([uiArr], {type: type});
|
||||
}
|
||||
|
||||
function saver(url, winMode){
|
||||
|
||||
if ('download' in anchor) { //html5 A[download]
|
||||
anchor.href = url;
|
||||
anchor.setAttribute("download", fileName);
|
||||
anchor.className = "download-js-link";
|
||||
anchor.innerHTML = "downloading...";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
setTimeout(function() {
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
|
||||
}, 66);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle non-a[download] safari as best we can:
|
||||
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
|
||||
if(/^data:/.test(url)) url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
if(!window.open(url)){ // popup blocked, offer direct download:
|
||||
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//do iframe dataURL download (old ch+FF):
|
||||
var f = document.createElement("iframe");
|
||||
document.body.appendChild(f);
|
||||
|
||||
if(!winMode && /^data:/.test(url)){ // force a mime that will download:
|
||||
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
}
|
||||
f.src=url;
|
||||
setTimeout(function(){ document.body.removeChild(f); }, 333);
|
||||
|
||||
}//end saver
|
||||
|
||||
|
||||
|
||||
|
||||
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
|
||||
return navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
|
||||
if(self.URL){ // simple fast and modern way using Blob and URL:
|
||||
saver(self.URL.createObjectURL(blob), true);
|
||||
}else{
|
||||
// handle non-Blob()+non-URL browsers:
|
||||
if(typeof blob === "string" || blob.constructor===toString ){
|
||||
try{
|
||||
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
|
||||
}catch(y){
|
||||
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
|
||||
}
|
||||
}
|
||||
|
||||
// Blob but not URL support:
|
||||
reader=new FileReader();
|
||||
reader.onload=function(e){
|
||||
saver(this.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
return true;
|
||||
}; /* end download() */
|
||||
}));
|
|
@ -1,2 +0,0 @@
|
|||
//download.js v4.2, by dandavis; 2008-2017. [MIT] see http://danml.com/download.html for tests/usage
|
||||
;(function(r,l){"function"==typeof define&&define.amd?define([],l):"object"==typeof exports?module.exports=l():r.download=l()})(this,function(){return function l(a,e,k){function q(a){var h=a.split(/[:;,]/);a=h[1];var h=("base64"==h[2]?atob:decodeURIComponent)(h.pop()),d=h.length,b=0,c=new Uint8Array(d);for(b;b<d;++b)c[b]=h.charCodeAt(b);return new f([c],{type:a})}function m(a,b){if("download"in d)return d.href=a,d.setAttribute("download",n),d.className="download-js-link",d.innerHTML="downloading...",d.style.display="none",document.body.appendChild(d),setTimeout(function(){d.click(),document.body.removeChild(d),!0===b&&setTimeout(function(){g.URL.revokeObjectURL(d.href)},250)},66),!0;if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent))return/^data:/.test(a)&&(a="data:"+a.replace(/^data:([\w\/\-\+]+)/,"application/octet-stream")),!window.open(a)&&confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")&&(location.href=a),!0;var c=document.createElement("iframe");document.body.appendChild(c),!b&&/^data:/.test(a)&&(a="data:"+a.replace(/^data:([\w\/\-\+]+)/,"application/octet-stream")),c.src=a,setTimeout(function(){document.body.removeChild(c)},333)}var g=window,b=k||"application/octet-stream",c=!e&&!k&&a,d=document.createElement("a");k=function(a){return String(a)};var f=g.Blob||g.MozBlob||g.WebKitBlob||k,n=e||"download",f=f.call?f.bind(g):Blob;"true"===String(this)&&(a=[a,b],b=a[0],a=a[1]);if(c&&2048>c.length&&(n=c.split("/").pop().split("?")[0],d.href=c,-1!==d.href.indexOf(c))){var p=new XMLHttpRequest;return p.open("GET",c,!0),p.responseType="blob",p.onload=function(a){l(a.target.response,n,"application/octet-stream")},setTimeout(function(){p.send()},0),p}if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(a)){if(!(2096103.424<a.length&&f!==k))return navigator.msSaveBlob?navigator.msSaveBlob(q(a),n):m(a);a=q(a),b=a.type||"application/octet-stream"}else if(/([\x80-\xff])/.test(a)){e=0;var c=new Uint8Array(a.length),t=c.length;for(e;e<t;++e)c[e]=a.charCodeAt(e);a=new f([c],{type:b})}a=a instanceof f?a:new f([a],{type:b});if(navigator.msSaveBlob)return navigator.msSaveBlob(a,n);if(g.URL)m(g.URL.createObjectURL(a),!0);else{if("string"==typeof a||a.constructor===k)try{return m("data:"+b+";base64,"+g.btoa(a))}catch(h){return m("data:"+b+","+encodeURIComponent(a))}b=new FileReader,b.onload=function(a){m(this.result)},b.readAsDataURL(a)}return!0}});
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"name": "downloadjs",
|
||||
"main": "download.js",
|
||||
"version": "1.4.7",
|
||||
"description": "file downloading using client-side javascript",
|
||||
"keywords": [
|
||||
"files",
|
||||
"dataURL",
|
||||
"blob",
|
||||
"download"
|
||||
],
|
||||
"homepage": "http://danml.com/download.html",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "dandavis",
|
||||
"email": "rndme@users.noreply.github.com",
|
||||
"url": "http://danml.com/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rndme/download.git"
|
||||
},
|
||||
"bugs": {
|
||||
"email": "rndme@users.noreply.github.com"
|
||||
},
|
||||
"files": [
|
||||
"download.js",
|
||||
"download.min.js"
|
||||
],
|
||||
"npmName": "downloadjs",
|
||||
"npmFileMap": [
|
||||
{
|
||||
"basePath": "/",
|
||||
"files": [
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"__npminstall_done": "Thu Jun 03 2021 11:07:53 GMT+0800 (中国标准时间)",
|
||||
"_from": "downloadjs@1.4.7",
|
||||
"_resolved": "https://registry.nlark.com/downloadjs/download/downloadjs-1.4.7.tgz"
|
||||
}
|
|
@ -1,360 +0,0 @@
|
|||
<template>
|
||||
<div :id="idName" @click="generate">
|
||||
<slot> Download {{ name }} </slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import download from "downloadjs";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// mime type [xls, csv]
|
||||
type: {
|
||||
type: String,
|
||||
default: "xls",
|
||||
},
|
||||
// Json to download
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
// fields inside the Json Object that you want to export
|
||||
// if no given, all the properties in the Json are exported
|
||||
fields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// this prop is used to fix the problem with other components that use the
|
||||
// variable fields, like vee-validate. exportFields works exactly like fields
|
||||
exportFields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// Use as fallback when the row has no field values
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
// Title(s) for the data, could be a string or an array of strings (multiple titles)
|
||||
header: {
|
||||
default: null,
|
||||
},
|
||||
// Footer(s) for the data, could be a string or an array of strings (multiple footers)
|
||||
footer: {
|
||||
default: null,
|
||||
},
|
||||
// filename to export
|
||||
name: {
|
||||
type: String,
|
||||
default: "data.xls",
|
||||
},
|
||||
fetch: {
|
||||
type: Function,
|
||||
},
|
||||
meta: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
worksheet: {
|
||||
type: String,
|
||||
default: "Sheet1",
|
||||
},
|
||||
//event before generate was called
|
||||
beforeGenerate: {
|
||||
type: Function,
|
||||
},
|
||||
//event before download pops up
|
||||
beforeFinish: {
|
||||
type: Function,
|
||||
},
|
||||
// Determine if CSV Data should be escaped
|
||||
escapeCsv: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// long number stringify
|
||||
stringifyLongNum: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// unique identifier
|
||||
idName() {
|
||||
var now = new Date().getTime();
|
||||
return "export_" + now;
|
||||
},
|
||||
|
||||
downloadFields() {
|
||||
if (this.fields) return this.fields;
|
||||
|
||||
if (this.exportFields) return this.exportFields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async generate() {
|
||||
if (typeof this.beforeGenerate === "function") {
|
||||
await this.beforeGenerate();
|
||||
}
|
||||
let data = this.data;
|
||||
if (typeof this.fetch === "function" || !data) data = await this.fetch();
|
||||
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let json = this.getProcessedJson(data, this.downloadFields);
|
||||
if (this.type === "html") {
|
||||
// this is mainly for testing
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name.replace(".xls", ".html"),
|
||||
"text/html"
|
||||
);
|
||||
} else if (this.type === "csv") {
|
||||
return this.export(
|
||||
this.jsonToCSV(json),
|
||||
this.name.replace(".xls", ".csv"),
|
||||
"application/csv"
|
||||
);
|
||||
}
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name,
|
||||
"application/vnd.ms-excel"
|
||||
);
|
||||
},
|
||||
/*
|
||||
Use downloadjs to generate the download link
|
||||
*/
|
||||
export: async function (data, filename, mime) {
|
||||
let blob = this.base64ToBlob(data, mime);
|
||||
if (typeof this.beforeFinish === "function") await this.beforeFinish();
|
||||
download(blob, filename, mime);
|
||||
},
|
||||
/*
|
||||
jsonToXLS
|
||||
---------------
|
||||
Transform json data into an xml document with MS Excel format, sadly
|
||||
it shows a prompt when it opens, that is a default behavior for
|
||||
Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
|
||||
*/
|
||||
jsonToXLS(data) {
|
||||
let xlsTemp =
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
|
||||
let xlsData = "<thead>";
|
||||
const colspan = Object.keys(data[0]).length;
|
||||
let _self = this;
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
xlsData += this.parseExtraData(
|
||||
header,
|
||||
'<tr><th colspan="' + colspan + '">${data}</th></tr>'
|
||||
);
|
||||
}
|
||||
|
||||
//Fields
|
||||
xlsData += "<tr>";
|
||||
for (let key in data[0]) {
|
||||
xlsData += "<th>" + key + "</th>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
xlsData += "</thead>";
|
||||
|
||||
//Data
|
||||
xlsData += "<tbody>";
|
||||
data.map(function (item, index) {
|
||||
xlsData += "<tr>";
|
||||
for (let key in item) {
|
||||
xlsData +=
|
||||
"<td>" +
|
||||
_self.preprocessLongNum(
|
||||
_self.valueReformattedForMultilines(item[key])
|
||||
) +
|
||||
"</td>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
});
|
||||
xlsData += "</tbody>";
|
||||
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
xlsData += "<tfoot>";
|
||||
xlsData += this.parseExtraData(
|
||||
this.footer,
|
||||
'<tr><td colspan="' + colspan + '">${data}</td></tr>'
|
||||
);
|
||||
xlsData += "</tfoot>";
|
||||
}
|
||||
|
||||
return xlsTemp
|
||||
.replace("${table}", xlsData)
|
||||
.replace("${worksheet}", this.worksheet);
|
||||
},
|
||||
/*
|
||||
jsonToCSV
|
||||
---------------
|
||||
Transform json data into an CSV file.
|
||||
*/
|
||||
jsonToCSV(data) {
|
||||
let _self = this;
|
||||
var csvData = [];
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
csvData.push(this.parseExtraData(header, "${data}\r\n"));
|
||||
}
|
||||
|
||||
//Fields
|
||||
for (let key in data[0]) {
|
||||
csvData.push(key);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
//Data
|
||||
data.map(function (item) {
|
||||
for (let key in item) {
|
||||
let escapedCSV = item[key] + "";
|
||||
// Escaped CSV data to string to avoid problems with numbers or other types of values
|
||||
// this is controlled by the prop escapeCsv
|
||||
if (_self.escapeCsv) {
|
||||
escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
|
||||
if (escapedCSV.match(/[,"\n]/)) {
|
||||
escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
|
||||
}
|
||||
}
|
||||
csvData.push(escapedCSV);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
});
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
|
||||
}
|
||||
return csvData.join("");
|
||||
},
|
||||
/*
|
||||
getProcessedJson
|
||||
---------------
|
||||
Get only the data to export, if no fields are set return all the data
|
||||
*/
|
||||
getProcessedJson(data, header) {
|
||||
let keys = this.getKeys(data, header);
|
||||
let newData = [];
|
||||
let _self = this;
|
||||
data.map(function (item, index) {
|
||||
let newItem = {};
|
||||
for (let label in keys) {
|
||||
let property = keys[label];
|
||||
newItem[label] = _self.getValue(property, item);
|
||||
}
|
||||
newData.push(newItem);
|
||||
});
|
||||
|
||||
return newData;
|
||||
},
|
||||
getKeys(data, header) {
|
||||
if (header) {
|
||||
return header;
|
||||
}
|
||||
|
||||
let keys = {};
|
||||
for (let key in data[0]) {
|
||||
keys[key] = key;
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
/*
|
||||
parseExtraData
|
||||
---------------
|
||||
Parse title and footer attribute to the csv format
|
||||
*/
|
||||
parseExtraData(extraData, format) {
|
||||
let parseData = "";
|
||||
if (Array.isArray(extraData)) {
|
||||
for (var i = 0; i < extraData.length; i++) {
|
||||
if (extraData[i])
|
||||
parseData += format.replace("${data}", extraData[i]);
|
||||
}
|
||||
} else {
|
||||
parseData += format.replace("${data}", extraData);
|
||||
}
|
||||
return parseData;
|
||||
},
|
||||
|
||||
getValue(key, item) {
|
||||
const field = typeof key !== "object" ? key : key.field;
|
||||
let indexes = typeof field !== "string" ? [] : field.split(".");
|
||||
let value = this.defaultValue;
|
||||
|
||||
if (!field) value = item;
|
||||
else if (indexes.length > 1)
|
||||
value = this.getValueFromNestedItem(item, indexes);
|
||||
else value = this.parseValue(item[field]);
|
||||
|
||||
if (key.hasOwnProperty("callback"))
|
||||
value = this.getValueFromCallback(value, key.callback);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/*
|
||||
convert values with newline \n characters into <br/>
|
||||
*/
|
||||
valueReformattedForMultilines(value) {
|
||||
if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
|
||||
else return value;
|
||||
},
|
||||
preprocessLongNum(value) {
|
||||
if (this.stringifyLongNum) {
|
||||
if (String(value).startsWith("0x")) {
|
||||
return value;
|
||||
}
|
||||
if (!isNaN(value) && value != "") {
|
||||
if (value > 99999999999 || value < 0.0000000000001) {
|
||||
return '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getValueFromNestedItem(item, indexes) {
|
||||
let nestedItem = item;
|
||||
for (let index of indexes) {
|
||||
if (nestedItem) nestedItem = nestedItem[index];
|
||||
}
|
||||
return this.parseValue(nestedItem);
|
||||
},
|
||||
|
||||
getValueFromCallback(item, callback) {
|
||||
if (typeof callback !== "function") return this.defaultValue;
|
||||
const value = callback(item);
|
||||
return this.parseValue(value);
|
||||
},
|
||||
parseValue(value) {
|
||||
return value || value === 0 || typeof value === "boolean"
|
||||
? value
|
||||
: this.defaultValue;
|
||||
},
|
||||
base64ToBlob(data, mime) {
|
||||
let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
|
||||
let bstr = atob(base64);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8ClampedArray(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
},
|
||||
}, // end methods
|
||||
};
|
||||
</script>
|
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Pooya Parsa <pooya@pi0.ir>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,321 +0,0 @@
|
|||
# JSON to Excel for VUE 2
|
||||
|
||||
Download your JSON data as an Excel file directly from the browser. This component is based on the solution proposed on [this thread](https://stackoverflow.com/questions/17142427/javascript-to-export-html-table-to-excel)
|
||||
|
||||
### Important! Extra prompt in Microsoft Excel
|
||||
|
||||
**The method implemented in this component uses HTML tables to draw the .xls files, Microsoft Excel no longer recognize HTML as native content so a warning message will be displayed before opening the file. The content will be rendered perfectly but the message can't be avoided.**
|
||||
|
||||
## Getting started
|
||||
|
||||
Get the package:
|
||||
|
||||
```bash
|
||||
npm install vue-json-excel
|
||||
```
|
||||
|
||||
Register JsonExcel in your vue app entry point:
|
||||
|
||||
```js
|
||||
import Vue from "vue";
|
||||
import JsonExcel from "vue-json-excel";
|
||||
|
||||
Vue.component("downloadExcel", JsonExcel);
|
||||
```
|
||||
|
||||
In your template
|
||||
|
||||
```html
|
||||
<download-excel :data="json_data">
|
||||
Download Data
|
||||
<img src="download_icon.png" />
|
||||
</download-excel>
|
||||
```
|
||||
|
||||
## Props List
|
||||
|
||||
| Name | Type | Description | Default |
|
||||
| :--------------------------- | :----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: |
|
||||
| data | Array | Data to be exported. |
|
||||
| fields | Object | Fields inside the JSON Object that you want to export. If none provided, all properties in the JSON will be exported. |
|
||||
| export-fields (exportFields) | Object | Used to fix the problem with other components that use the variable fields, like vee-validate. exportFields works exactly like fields |
|
||||
| type | string | Mime type [xls, csv] | xls |
|
||||
| name | string | File name to export. | data.xls |
|
||||
| header | string/Array | Title(s) for the data. Can be a string (one title) or an array of strings (multiple titles). |
|
||||
| title(deprecated) | string/Array | same as header, title is maintained for retro-compatibility purposes but its use is not recommended due to the conflict with the HTML5 title attribute. |
|
||||
| footer | string/Array | Footer(s) for the data. Can be a string (one footer) or an array of strings (multiple footers). |
|
||||
| default-value (defaultValue) | string | Use as fallback when the row has no field values. | '' |
|
||||
| worksheet | string | Name of the worksheet tab. | 'Sheet1' |
|
||||
| fetch | Function | Callback to fetch data before download, if it's set it runs immediately after mouse pressed and before download process.<br>IMPORTANT: only works if no data prop is defined. |
|
||||
| before-generate | Function | Callback to call a method right before the generate / fetch data, eg:show loading progress |
|
||||
| before-finish | Function | Callback to call a method right before the download box pops out, eg:hide loading progress |
|
||||
| stringifyLongNum | Boolean | stringify long number and decimal(solve the problem of loss of digital accuracy), default: false |
|
||||
| escapeCsv | Boolean | This escapes CSV values in order to fix some excel problems with number fields. But this will wrap every csv data with **="** and **"**, to avoid that you have to set this prop to false. default: True |
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Vue from "vue";
|
||||
import JsonExcel from "vue-json-excel";
|
||||
|
||||
Vue.component("downloadExcel", JsonExcel);
|
||||
|
||||
const app = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
json_fields: {
|
||||
"Complete name": "name",
|
||||
City: "city",
|
||||
Telephone: "phone.mobile",
|
||||
"Telephone 2": {
|
||||
field: "phone.landline",
|
||||
callback: (value) => {
|
||||
return `Landline Phone - ${value}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
json_data: [
|
||||
{
|
||||
name: "Tony Peña",
|
||||
city: "New York",
|
||||
country: "United States",
|
||||
birthdate: "1978-03-15",
|
||||
phone: {
|
||||
mobile: "1-541-754-3010",
|
||||
landline: "(541) 754-3010",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Thessaloniki",
|
||||
city: "Athens",
|
||||
country: "Greece",
|
||||
birthdate: "1987-11-23",
|
||||
phone: {
|
||||
mobile: "+1 855 275 5071",
|
||||
landline: "(2741) 2621-244",
|
||||
},
|
||||
},
|
||||
],
|
||||
json_meta: [
|
||||
[
|
||||
{
|
||||
key: "charset",
|
||||
value: "utf-8",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In your HTML call it like
|
||||
|
||||
```html
|
||||
<download-excel
|
||||
class="btn btn-default"
|
||||
:data="json_data"
|
||||
:fields="json_fields"
|
||||
worksheet="My Worksheet"
|
||||
name="filename.xls"
|
||||
>
|
||||
Download Excel (you can customize this with html code!)
|
||||
</download-excel>
|
||||
```
|
||||
|
||||
REQUIRED
|
||||
|
||||
- json_data: Contains the data you want to export.
|
||||
- json_fields: You can select what fields to export. Specify nested data and assign labels to the fields. The key is the label, the value is the JSON field. This will export the field data 'as is'. If you need to customize the the exported data you can define a callback function. Thanks to @gucastiliao.
|
||||
|
||||
```js
|
||||
let json_fields = {
|
||||
// regular field (exported data 'as is')
|
||||
fieldLabel: attributeName, // nested attribute supported
|
||||
// callback function for data formatting
|
||||
anotherFieldLabel: {
|
||||
field: anotherAttributeName, // nested attribute supported
|
||||
callback: (value) => {
|
||||
return `formatted value ${value}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
json_fields is a object that represents which columns will be exported. If no object is provided, the component will be use the first object in your data array to extract the keys as columns names. Json field example:
|
||||
|
||||
```
|
||||
:export-fields="{
|
||||
'Human friendly name': '_name_field_from_json',
|
||||
'user's last name': '_last_name_text'
|
||||
}"
|
||||
```
|
||||
|
||||
## Export CSV
|
||||
|
||||
To export JSON as a CSV file, just add the prop `type` with a value of "csv":
|
||||
|
||||
```html
|
||||
<download-excel
|
||||
class="btn btn-default"
|
||||
:data="json_data"
|
||||
:fields="json_fields"
|
||||
type="csv"
|
||||
name="filename.xls"
|
||||
>
|
||||
Download CSV (you can customize this with html code!)
|
||||
</download-excel>
|
||||
```
|
||||
|
||||
## Multi-line values will appear in a single cell
|
||||
|
||||
A single text value in the data that contains newline characters will appear as a single cell in Excel. This avoids the undesired behavior of multi-line values getting split into multiple cells that must be merged before using data filters and pivot tables.
|
||||
|
||||
For example:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<json-excel :data="dataForExcel" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import JsonExcel from "@/components/JsonExcel";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
JsonExcel,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
dataForExcel: [
|
||||
{ colA: "Hello", colB: "World" },
|
||||
{
|
||||
colA: "Multi-line",
|
||||
/* Multi-line value: */
|
||||
colB:
|
||||
"This is a long paragraph\nwith multiple lines\nthat should show in a single cell.",
|
||||
},
|
||||
{ colA: "Another", colB: "Regular cell" },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
![Example of Excel showing multi-line cell](example-multi-line.png)
|
||||
|
||||
## Fetch Data on Demand
|
||||
|
||||
In case you need to fetch data from the server, you could use the fetch prop that allows you to define a callback function that is executed when your user click the download button. This function has to return a JSON value containing the data to export. A basic use case is:
|
||||
|
||||
```js
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<hr>
|
||||
<h2>Fetch Example</h2>
|
||||
<downloadexcel
|
||||
class = "btn"
|
||||
:fetch = "fetchData"
|
||||
:fields = "json_fields"
|
||||
:before-generate = "startDownload"
|
||||
:before-finish = "finishDownload">
|
||||
Download Excel
|
||||
</downloadexcel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import downloadexcel from "vue-json-excel";
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
downloadexcel,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
json_fields: {
|
||||
'Complete name': 'name',
|
||||
'Date': 'date',
|
||||
},
|
||||
}
|
||||
}, //data
|
||||
methods:{
|
||||
async fetchData(){
|
||||
const response = await axios.get('https://holidayapi.com/v1/holidays?key=a4b2083b-1577-4acd-9408-6e529996b129&country=US&year=2017&month=09');
|
||||
console.log(response);
|
||||
return response.data.holidays;
|
||||
},
|
||||
startDownload(){
|
||||
alert('show loading');
|
||||
},
|
||||
finishDownload(){
|
||||
alert('hide loading');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
```
|
||||
|
||||
## Using callbacks
|
||||
|
||||
when using callback functions in the fields description, you have three option to retrieve data:
|
||||
|
||||
- **field: 'path.to.nested.property'** you can retrieve a specific value using the nested property notation.
|
||||
|
||||
```js
|
||||
json_fields: {
|
||||
'Complete name': 'name',
|
||||
'City': 'city',
|
||||
'Telephone': 'phone.mobile',
|
||||
'Telephone 2' : {
|
||||
field: 'phone.landline',
|
||||
callback: (value) => {
|
||||
return `Landline Phone - ${value}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
- **field: 'define.nested.object'** you can retrieve a nested object too.
|
||||
|
||||
```js
|
||||
json_fields: {s
|
||||
'Complete name': 'name',
|
||||
'City': 'city',
|
||||
'Telephone': 'phone.mobile',
|
||||
'Telephone 2' : {
|
||||
field: 'phone',
|
||||
callback: (value) => {
|
||||
return `Landline Phone - ${value.landline}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
- Or **get the whole row** if field is undefined.
|
||||
|
||||
```js
|
||||
json_fields: {
|
||||
'Complete name': 'name',
|
||||
'City': 'city',
|
||||
'Telephone': 'phone.mobile',
|
||||
'Telephone 2' : {
|
||||
callback: (value) => {
|
||||
return `Landline Phone - ${value.phone.landline}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
#### Status
|
||||
|
||||
This project is in an early stage of development. Any contribution is welcome :D
|
|
@ -1,654 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
var download = createCommonjsModule(function (module, exports) {
|
||||
//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
|
||||
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
||||
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
||||
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
|
||||
// v4 adds AMD/UMD, commonJS, and plain browser support
|
||||
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
|
||||
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
|
||||
// https://github.com/rndme/download
|
||||
|
||||
(function (root, factory) {
|
||||
{
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory();
|
||||
}
|
||||
}(commonjsGlobal, function () {
|
||||
|
||||
return function download(data, strFileName, strMimeType) {
|
||||
|
||||
var self = window, // this script is only for browsers anyway...
|
||||
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
|
||||
mimeType = strMimeType || defaultMime,
|
||||
payload = data,
|
||||
url = !strFileName && !strMimeType && payload,
|
||||
anchor = document.createElement("a"),
|
||||
toString = function(a){return String(a);},
|
||||
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
|
||||
fileName = strFileName || "download",
|
||||
blob,
|
||||
reader;
|
||||
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
|
||||
|
||||
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
||||
payload=[payload, mimeType];
|
||||
mimeType=payload[0];
|
||||
payload=payload[1];
|
||||
}
|
||||
|
||||
|
||||
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
|
||||
fileName = url.split("/").pop().split("?")[0];
|
||||
anchor.href = url; // assign href prop to temp anchor
|
||||
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
|
||||
var ajax=new XMLHttpRequest();
|
||||
ajax.open( "GET", url, true);
|
||||
ajax.responseType = 'blob';
|
||||
ajax.onload= function(e){
|
||||
download(e.target.response, fileName, defaultMime);
|
||||
};
|
||||
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
|
||||
return ajax;
|
||||
} // end if valid url?
|
||||
} // end if url?
|
||||
|
||||
|
||||
//go ahead and download dataURLs right away
|
||||
if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
|
||||
|
||||
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
|
||||
payload=dataUrlToBlob(payload);
|
||||
mimeType=payload.type || defaultMime;
|
||||
}else {
|
||||
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
|
||||
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
|
||||
saver(payload) ; // everyone else can save dataURLs un-processed
|
||||
}
|
||||
|
||||
}else {//not data url, is it a string with special needs?
|
||||
if(/([\x80-\xff])/.test(payload)){
|
||||
var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
|
||||
for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
|
||||
payload=new myBlob([tempUiArr], {type: mimeType});
|
||||
}
|
||||
}
|
||||
blob = payload instanceof myBlob ?
|
||||
payload :
|
||||
new myBlob([payload], {type: mimeType}) ;
|
||||
|
||||
|
||||
function dataUrlToBlob(strUrl) {
|
||||
var parts= strUrl.split(/[:;,]/),
|
||||
type= parts[1],
|
||||
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
|
||||
binData= decoder( parts.pop() ),
|
||||
mx= binData.length,
|
||||
i= 0,
|
||||
uiArr= new Uint8Array(mx);
|
||||
|
||||
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
|
||||
|
||||
return new myBlob([uiArr], {type: type});
|
||||
}
|
||||
|
||||
function saver(url, winMode){
|
||||
|
||||
if ('download' in anchor) { //html5 A[download]
|
||||
anchor.href = url;
|
||||
anchor.setAttribute("download", fileName);
|
||||
anchor.className = "download-js-link";
|
||||
anchor.innerHTML = "downloading...";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
setTimeout(function() {
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
|
||||
}, 66);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle non-a[download] safari as best we can:
|
||||
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
|
||||
if(/^data:/.test(url)) url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
if(!window.open(url)){ // popup blocked, offer direct download:
|
||||
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//do iframe dataURL download (old ch+FF):
|
||||
var f = document.createElement("iframe");
|
||||
document.body.appendChild(f);
|
||||
|
||||
if(!winMode && /^data:/.test(url)){ // force a mime that will download:
|
||||
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
}
|
||||
f.src=url;
|
||||
setTimeout(function(){ document.body.removeChild(f); }, 333);
|
||||
|
||||
}//end saver
|
||||
|
||||
|
||||
|
||||
|
||||
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
|
||||
return navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
|
||||
if(self.URL){ // simple fast and modern way using Blob and URL:
|
||||
saver(self.URL.createObjectURL(blob), true);
|
||||
}else {
|
||||
// handle non-Blob()+non-URL browsers:
|
||||
if(typeof blob === "string" || blob.constructor===toString ){
|
||||
try{
|
||||
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
|
||||
}catch(y){
|
||||
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
|
||||
}
|
||||
}
|
||||
|
||||
// Blob but not URL support:
|
||||
reader=new FileReader();
|
||||
reader.onload=function(e){
|
||||
saver(this.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
return true;
|
||||
}; /* end download() */
|
||||
}));
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
var script = {
|
||||
props: {
|
||||
// mime type [xls, csv]
|
||||
type: {
|
||||
type: String,
|
||||
default: "xls",
|
||||
},
|
||||
// Json to download
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
// fields inside the Json Object that you want to export
|
||||
// if no given, all the properties in the Json are exported
|
||||
fields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// this prop is used to fix the problem with other components that use the
|
||||
// variable fields, like vee-validate. exportFields works exactly like fields
|
||||
exportFields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// Use as fallback when the row has no field values
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
// Title(s) for the data, could be a string or an array of strings (multiple titles)
|
||||
header: {
|
||||
default: null,
|
||||
},
|
||||
// Footer(s) for the data, could be a string or an array of strings (multiple footers)
|
||||
footer: {
|
||||
default: null,
|
||||
},
|
||||
// filename to export
|
||||
name: {
|
||||
type: String,
|
||||
default: "data.xls",
|
||||
},
|
||||
fetch: {
|
||||
type: Function,
|
||||
},
|
||||
meta: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
worksheet: {
|
||||
type: String,
|
||||
default: "Sheet1",
|
||||
},
|
||||
//event before generate was called
|
||||
beforeGenerate: {
|
||||
type: Function,
|
||||
},
|
||||
//event before download pops up
|
||||
beforeFinish: {
|
||||
type: Function,
|
||||
},
|
||||
// Determine if CSV Data should be escaped
|
||||
escapeCsv: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// long number stringify
|
||||
stringifyLongNum: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// unique identifier
|
||||
idName() {
|
||||
var now = new Date().getTime();
|
||||
return "export_" + now;
|
||||
},
|
||||
|
||||
downloadFields() {
|
||||
if (this.fields) return this.fields;
|
||||
|
||||
if (this.exportFields) return this.exportFields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async generate() {
|
||||
if (typeof this.beforeGenerate === "function") {
|
||||
await this.beforeGenerate();
|
||||
}
|
||||
let data = this.data;
|
||||
if (typeof this.fetch === "function" || !data) data = await this.fetch();
|
||||
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let json = this.getProcessedJson(data, this.downloadFields);
|
||||
if (this.type === "html") {
|
||||
// this is mainly for testing
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name.replace(".xls", ".html"),
|
||||
"text/html"
|
||||
);
|
||||
} else if (this.type === "csv") {
|
||||
return this.export(
|
||||
this.jsonToCSV(json),
|
||||
this.name.replace(".xls", ".csv"),
|
||||
"application/csv"
|
||||
);
|
||||
}
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name,
|
||||
"application/vnd.ms-excel"
|
||||
);
|
||||
},
|
||||
/*
|
||||
Use downloadjs to generate the download link
|
||||
*/
|
||||
export: async function (data, filename, mime) {
|
||||
let blob = this.base64ToBlob(data, mime);
|
||||
if (typeof this.beforeFinish === "function") await this.beforeFinish();
|
||||
download(blob, filename, mime);
|
||||
},
|
||||
/*
|
||||
jsonToXLS
|
||||
---------------
|
||||
Transform json data into an xml document with MS Excel format, sadly
|
||||
it shows a prompt when it opens, that is a default behavior for
|
||||
Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
|
||||
*/
|
||||
jsonToXLS(data) {
|
||||
let xlsTemp =
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
|
||||
let xlsData = "<thead>";
|
||||
const colspan = Object.keys(data[0]).length;
|
||||
let _self = this;
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
xlsData += this.parseExtraData(
|
||||
header,
|
||||
'<tr><th colspan="' + colspan + '">${data}</th></tr>'
|
||||
);
|
||||
}
|
||||
|
||||
//Fields
|
||||
xlsData += "<tr>";
|
||||
for (let key in data[0]) {
|
||||
xlsData += "<th>" + key + "</th>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
xlsData += "</thead>";
|
||||
|
||||
//Data
|
||||
xlsData += "<tbody>";
|
||||
data.map(function (item, index) {
|
||||
xlsData += "<tr>";
|
||||
for (let key in item) {
|
||||
xlsData +=
|
||||
"<td>" +
|
||||
_self.preprocessLongNum(
|
||||
_self.valueReformattedForMultilines(item[key])
|
||||
) +
|
||||
"</td>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
});
|
||||
xlsData += "</tbody>";
|
||||
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
xlsData += "<tfoot>";
|
||||
xlsData += this.parseExtraData(
|
||||
this.footer,
|
||||
'<tr><td colspan="' + colspan + '">${data}</td></tr>'
|
||||
);
|
||||
xlsData += "</tfoot>";
|
||||
}
|
||||
|
||||
return xlsTemp
|
||||
.replace("${table}", xlsData)
|
||||
.replace("${worksheet}", this.worksheet);
|
||||
},
|
||||
/*
|
||||
jsonToCSV
|
||||
---------------
|
||||
Transform json data into an CSV file.
|
||||
*/
|
||||
jsonToCSV(data) {
|
||||
let _self = this;
|
||||
var csvData = [];
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
csvData.push(this.parseExtraData(header, "${data}\r\n"));
|
||||
}
|
||||
|
||||
//Fields
|
||||
for (let key in data[0]) {
|
||||
csvData.push(key);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
//Data
|
||||
data.map(function (item) {
|
||||
for (let key in item) {
|
||||
let escapedCSV = item[key] + "";
|
||||
// Escaped CSV data to string to avoid problems with numbers or other types of values
|
||||
// this is controlled by the prop escapeCsv
|
||||
if (_self.escapeCsv) {
|
||||
escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
|
||||
if (escapedCSV.match(/[,"\n]/)) {
|
||||
escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
|
||||
}
|
||||
}
|
||||
csvData.push(escapedCSV);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
});
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
|
||||
}
|
||||
return csvData.join("");
|
||||
},
|
||||
/*
|
||||
getProcessedJson
|
||||
---------------
|
||||
Get only the data to export, if no fields are set return all the data
|
||||
*/
|
||||
getProcessedJson(data, header) {
|
||||
let keys = this.getKeys(data, header);
|
||||
let newData = [];
|
||||
let _self = this;
|
||||
data.map(function (item, index) {
|
||||
let newItem = {};
|
||||
for (let label in keys) {
|
||||
let property = keys[label];
|
||||
newItem[label] = _self.getValue(property, item);
|
||||
}
|
||||
newData.push(newItem);
|
||||
});
|
||||
|
||||
return newData;
|
||||
},
|
||||
getKeys(data, header) {
|
||||
if (header) {
|
||||
return header;
|
||||
}
|
||||
|
||||
let keys = {};
|
||||
for (let key in data[0]) {
|
||||
keys[key] = key;
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
/*
|
||||
parseExtraData
|
||||
---------------
|
||||
Parse title and footer attribute to the csv format
|
||||
*/
|
||||
parseExtraData(extraData, format) {
|
||||
let parseData = "";
|
||||
if (Array.isArray(extraData)) {
|
||||
for (var i = 0; i < extraData.length; i++) {
|
||||
if (extraData[i])
|
||||
parseData += format.replace("${data}", extraData[i]);
|
||||
}
|
||||
} else {
|
||||
parseData += format.replace("${data}", extraData);
|
||||
}
|
||||
return parseData;
|
||||
},
|
||||
|
||||
getValue(key, item) {
|
||||
const field = typeof key !== "object" ? key : key.field;
|
||||
let indexes = typeof field !== "string" ? [] : field.split(".");
|
||||
let value = this.defaultValue;
|
||||
|
||||
if (!field) value = item;
|
||||
else if (indexes.length > 1)
|
||||
value = this.getValueFromNestedItem(item, indexes);
|
||||
else value = this.parseValue(item[field]);
|
||||
|
||||
if (key.hasOwnProperty("callback"))
|
||||
value = this.getValueFromCallback(value, key.callback);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/*
|
||||
convert values with newline \n characters into <br/>
|
||||
*/
|
||||
valueReformattedForMultilines(value) {
|
||||
if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
|
||||
else return value;
|
||||
},
|
||||
preprocessLongNum(value) {
|
||||
if (this.stringifyLongNum) {
|
||||
if (String(value).startsWith("0x")) {
|
||||
return value;
|
||||
}
|
||||
if (!isNaN(value) && value != "") {
|
||||
if (value > 99999999999 || value < 0.0000000000001) {
|
||||
return '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getValueFromNestedItem(item, indexes) {
|
||||
let nestedItem = item;
|
||||
for (let index of indexes) {
|
||||
if (nestedItem) nestedItem = nestedItem[index];
|
||||
}
|
||||
return this.parseValue(nestedItem);
|
||||
},
|
||||
|
||||
getValueFromCallback(item, callback) {
|
||||
if (typeof callback !== "function") return this.defaultValue;
|
||||
const value = callback(item);
|
||||
return this.parseValue(value);
|
||||
},
|
||||
parseValue(value) {
|
||||
return value || value === 0 || typeof value === "boolean"
|
||||
? value
|
||||
: this.defaultValue;
|
||||
},
|
||||
base64ToBlob(data, mime) {
|
||||
let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
|
||||
let bstr = atob(base64);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8ClampedArray(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
},
|
||||
}, // end methods
|
||||
};
|
||||
|
||||
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
|
||||
/* server only */
|
||||
, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
||||
if (typeof shadowMode !== 'boolean') {
|
||||
createInjectorSSR = createInjector;
|
||||
createInjector = shadowMode;
|
||||
shadowMode = false;
|
||||
} // Vue.extend constructor export interop.
|
||||
|
||||
|
||||
var options = typeof script === 'function' ? script.options : script; // render functions
|
||||
|
||||
if (template && template.render) {
|
||||
options.render = template.render;
|
||||
options.staticRenderFns = template.staticRenderFns;
|
||||
options._compiled = true; // functional template
|
||||
|
||||
if (isFunctionalTemplate) {
|
||||
options.functional = true;
|
||||
}
|
||||
} // scopedId
|
||||
|
||||
|
||||
if (scopeId) {
|
||||
options._scopeId = scopeId;
|
||||
}
|
||||
|
||||
var hook;
|
||||
|
||||
if (moduleIdentifier) {
|
||||
// server build
|
||||
hook = function hook(context) {
|
||||
// 2.3 injection
|
||||
context = context || // cached call
|
||||
this.$vnode && this.$vnode.ssrContext || // stateful
|
||||
this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional
|
||||
// 2.2 with runInNewContext: true
|
||||
|
||||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
||||
context = __VUE_SSR_CONTEXT__;
|
||||
} // inject component styles
|
||||
|
||||
|
||||
if (style) {
|
||||
style.call(this, createInjectorSSR(context));
|
||||
} // register component module identifier for async chunk inference
|
||||
|
||||
|
||||
if (context && context._registeredComponents) {
|
||||
context._registeredComponents.add(moduleIdentifier);
|
||||
}
|
||||
}; // used by ssr in case component is cached and beforeCreate
|
||||
// never gets called
|
||||
|
||||
|
||||
options._ssrRegister = hook;
|
||||
} else if (style) {
|
||||
hook = shadowMode ? function () {
|
||||
style.call(this, createInjectorShadow(this.$root.$options.shadowRoot));
|
||||
} : function (context) {
|
||||
style.call(this, createInjector(context));
|
||||
};
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
if (options.functional) {
|
||||
// register for functional component in vue file
|
||||
var originalRender = options.render;
|
||||
|
||||
options.render = function renderWithStyleInjection(h, context) {
|
||||
hook.call(context);
|
||||
return originalRender(h, context);
|
||||
};
|
||||
} else {
|
||||
// inject component registration as beforeCreate hook
|
||||
var existing = options.beforeCreate;
|
||||
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
||||
}
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
var normalizeComponent_1 = normalizeComponent;
|
||||
|
||||
/* script */
|
||||
const __vue_script__ = script;
|
||||
|
||||
/* template */
|
||||
var __vue_render__ = function() {
|
||||
var _vm = this;
|
||||
var _h = _vm.$createElement;
|
||||
var _c = _vm._self._c || _h;
|
||||
return _c(
|
||||
"div",
|
||||
{ attrs: { id: _vm.idName }, on: { click: _vm.generate } },
|
||||
[_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])],
|
||||
2
|
||||
)
|
||||
};
|
||||
var __vue_staticRenderFns__ = [];
|
||||
__vue_render__._withStripped = true;
|
||||
|
||||
/* style */
|
||||
const __vue_inject_styles__ = undefined;
|
||||
/* scoped */
|
||||
const __vue_scope_id__ = undefined;
|
||||
/* module identifier */
|
||||
const __vue_module_identifier__ = undefined;
|
||||
/* functional template */
|
||||
const __vue_is_functional_template__ = false;
|
||||
/* style inject */
|
||||
|
||||
/* style inject SSR */
|
||||
|
||||
|
||||
|
||||
var JsonExcel = normalizeComponent_1(
|
||||
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
|
||||
__vue_inject_styles__,
|
||||
__vue_script__,
|
||||
__vue_scope_id__,
|
||||
__vue_is_functional_template__,
|
||||
__vue_module_identifier__,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
module.exports = JsonExcel;
|
|
@ -1,652 +0,0 @@
|
|||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
var download = createCommonjsModule(function (module, exports) {
|
||||
//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
|
||||
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
||||
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
||||
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
|
||||
// v4 adds AMD/UMD, commonJS, and plain browser support
|
||||
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
|
||||
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
|
||||
// https://github.com/rndme/download
|
||||
|
||||
(function (root, factory) {
|
||||
{
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory();
|
||||
}
|
||||
}(commonjsGlobal, function () {
|
||||
|
||||
return function download(data, strFileName, strMimeType) {
|
||||
|
||||
var self = window, // this script is only for browsers anyway...
|
||||
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
|
||||
mimeType = strMimeType || defaultMime,
|
||||
payload = data,
|
||||
url = !strFileName && !strMimeType && payload,
|
||||
anchor = document.createElement("a"),
|
||||
toString = function(a){return String(a);},
|
||||
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
|
||||
fileName = strFileName || "download",
|
||||
blob,
|
||||
reader;
|
||||
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
|
||||
|
||||
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
||||
payload=[payload, mimeType];
|
||||
mimeType=payload[0];
|
||||
payload=payload[1];
|
||||
}
|
||||
|
||||
|
||||
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
|
||||
fileName = url.split("/").pop().split("?")[0];
|
||||
anchor.href = url; // assign href prop to temp anchor
|
||||
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
|
||||
var ajax=new XMLHttpRequest();
|
||||
ajax.open( "GET", url, true);
|
||||
ajax.responseType = 'blob';
|
||||
ajax.onload= function(e){
|
||||
download(e.target.response, fileName, defaultMime);
|
||||
};
|
||||
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
|
||||
return ajax;
|
||||
} // end if valid url?
|
||||
} // end if url?
|
||||
|
||||
|
||||
//go ahead and download dataURLs right away
|
||||
if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
|
||||
|
||||
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
|
||||
payload=dataUrlToBlob(payload);
|
||||
mimeType=payload.type || defaultMime;
|
||||
}else {
|
||||
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
|
||||
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
|
||||
saver(payload) ; // everyone else can save dataURLs un-processed
|
||||
}
|
||||
|
||||
}else {//not data url, is it a string with special needs?
|
||||
if(/([\x80-\xff])/.test(payload)){
|
||||
var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
|
||||
for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
|
||||
payload=new myBlob([tempUiArr], {type: mimeType});
|
||||
}
|
||||
}
|
||||
blob = payload instanceof myBlob ?
|
||||
payload :
|
||||
new myBlob([payload], {type: mimeType}) ;
|
||||
|
||||
|
||||
function dataUrlToBlob(strUrl) {
|
||||
var parts= strUrl.split(/[:;,]/),
|
||||
type= parts[1],
|
||||
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
|
||||
binData= decoder( parts.pop() ),
|
||||
mx= binData.length,
|
||||
i= 0,
|
||||
uiArr= new Uint8Array(mx);
|
||||
|
||||
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
|
||||
|
||||
return new myBlob([uiArr], {type: type});
|
||||
}
|
||||
|
||||
function saver(url, winMode){
|
||||
|
||||
if ('download' in anchor) { //html5 A[download]
|
||||
anchor.href = url;
|
||||
anchor.setAttribute("download", fileName);
|
||||
anchor.className = "download-js-link";
|
||||
anchor.innerHTML = "downloading...";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
setTimeout(function() {
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
|
||||
}, 66);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle non-a[download] safari as best we can:
|
||||
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
|
||||
if(/^data:/.test(url)) url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
if(!window.open(url)){ // popup blocked, offer direct download:
|
||||
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//do iframe dataURL download (old ch+FF):
|
||||
var f = document.createElement("iframe");
|
||||
document.body.appendChild(f);
|
||||
|
||||
if(!winMode && /^data:/.test(url)){ // force a mime that will download:
|
||||
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
}
|
||||
f.src=url;
|
||||
setTimeout(function(){ document.body.removeChild(f); }, 333);
|
||||
|
||||
}//end saver
|
||||
|
||||
|
||||
|
||||
|
||||
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
|
||||
return navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
|
||||
if(self.URL){ // simple fast and modern way using Blob and URL:
|
||||
saver(self.URL.createObjectURL(blob), true);
|
||||
}else {
|
||||
// handle non-Blob()+non-URL browsers:
|
||||
if(typeof blob === "string" || blob.constructor===toString ){
|
||||
try{
|
||||
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
|
||||
}catch(y){
|
||||
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
|
||||
}
|
||||
}
|
||||
|
||||
// Blob but not URL support:
|
||||
reader=new FileReader();
|
||||
reader.onload=function(e){
|
||||
saver(this.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
return true;
|
||||
}; /* end download() */
|
||||
}));
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
var script = {
|
||||
props: {
|
||||
// mime type [xls, csv]
|
||||
type: {
|
||||
type: String,
|
||||
default: "xls",
|
||||
},
|
||||
// Json to download
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
// fields inside the Json Object that you want to export
|
||||
// if no given, all the properties in the Json are exported
|
||||
fields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// this prop is used to fix the problem with other components that use the
|
||||
// variable fields, like vee-validate. exportFields works exactly like fields
|
||||
exportFields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// Use as fallback when the row has no field values
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
// Title(s) for the data, could be a string or an array of strings (multiple titles)
|
||||
header: {
|
||||
default: null,
|
||||
},
|
||||
// Footer(s) for the data, could be a string or an array of strings (multiple footers)
|
||||
footer: {
|
||||
default: null,
|
||||
},
|
||||
// filename to export
|
||||
name: {
|
||||
type: String,
|
||||
default: "data.xls",
|
||||
},
|
||||
fetch: {
|
||||
type: Function,
|
||||
},
|
||||
meta: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
worksheet: {
|
||||
type: String,
|
||||
default: "Sheet1",
|
||||
},
|
||||
//event before generate was called
|
||||
beforeGenerate: {
|
||||
type: Function,
|
||||
},
|
||||
//event before download pops up
|
||||
beforeFinish: {
|
||||
type: Function,
|
||||
},
|
||||
// Determine if CSV Data should be escaped
|
||||
escapeCsv: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// long number stringify
|
||||
stringifyLongNum: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// unique identifier
|
||||
idName() {
|
||||
var now = new Date().getTime();
|
||||
return "export_" + now;
|
||||
},
|
||||
|
||||
downloadFields() {
|
||||
if (this.fields) return this.fields;
|
||||
|
||||
if (this.exportFields) return this.exportFields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async generate() {
|
||||
if (typeof this.beforeGenerate === "function") {
|
||||
await this.beforeGenerate();
|
||||
}
|
||||
let data = this.data;
|
||||
if (typeof this.fetch === "function" || !data) data = await this.fetch();
|
||||
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let json = this.getProcessedJson(data, this.downloadFields);
|
||||
if (this.type === "html") {
|
||||
// this is mainly for testing
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name.replace(".xls", ".html"),
|
||||
"text/html"
|
||||
);
|
||||
} else if (this.type === "csv") {
|
||||
return this.export(
|
||||
this.jsonToCSV(json),
|
||||
this.name.replace(".xls", ".csv"),
|
||||
"application/csv"
|
||||
);
|
||||
}
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name,
|
||||
"application/vnd.ms-excel"
|
||||
);
|
||||
},
|
||||
/*
|
||||
Use downloadjs to generate the download link
|
||||
*/
|
||||
export: async function (data, filename, mime) {
|
||||
let blob = this.base64ToBlob(data, mime);
|
||||
if (typeof this.beforeFinish === "function") await this.beforeFinish();
|
||||
download(blob, filename, mime);
|
||||
},
|
||||
/*
|
||||
jsonToXLS
|
||||
---------------
|
||||
Transform json data into an xml document with MS Excel format, sadly
|
||||
it shows a prompt when it opens, that is a default behavior for
|
||||
Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
|
||||
*/
|
||||
jsonToXLS(data) {
|
||||
let xlsTemp =
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
|
||||
let xlsData = "<thead>";
|
||||
const colspan = Object.keys(data[0]).length;
|
||||
let _self = this;
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
xlsData += this.parseExtraData(
|
||||
header,
|
||||
'<tr><th colspan="' + colspan + '">${data}</th></tr>'
|
||||
);
|
||||
}
|
||||
|
||||
//Fields
|
||||
xlsData += "<tr>";
|
||||
for (let key in data[0]) {
|
||||
xlsData += "<th>" + key + "</th>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
xlsData += "</thead>";
|
||||
|
||||
//Data
|
||||
xlsData += "<tbody>";
|
||||
data.map(function (item, index) {
|
||||
xlsData += "<tr>";
|
||||
for (let key in item) {
|
||||
xlsData +=
|
||||
"<td>" +
|
||||
_self.preprocessLongNum(
|
||||
_self.valueReformattedForMultilines(item[key])
|
||||
) +
|
||||
"</td>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
});
|
||||
xlsData += "</tbody>";
|
||||
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
xlsData += "<tfoot>";
|
||||
xlsData += this.parseExtraData(
|
||||
this.footer,
|
||||
'<tr><td colspan="' + colspan + '">${data}</td></tr>'
|
||||
);
|
||||
xlsData += "</tfoot>";
|
||||
}
|
||||
|
||||
return xlsTemp
|
||||
.replace("${table}", xlsData)
|
||||
.replace("${worksheet}", this.worksheet);
|
||||
},
|
||||
/*
|
||||
jsonToCSV
|
||||
---------------
|
||||
Transform json data into an CSV file.
|
||||
*/
|
||||
jsonToCSV(data) {
|
||||
let _self = this;
|
||||
var csvData = [];
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
csvData.push(this.parseExtraData(header, "${data}\r\n"));
|
||||
}
|
||||
|
||||
//Fields
|
||||
for (let key in data[0]) {
|
||||
csvData.push(key);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
//Data
|
||||
data.map(function (item) {
|
||||
for (let key in item) {
|
||||
let escapedCSV = item[key] + "";
|
||||
// Escaped CSV data to string to avoid problems with numbers or other types of values
|
||||
// this is controlled by the prop escapeCsv
|
||||
if (_self.escapeCsv) {
|
||||
escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
|
||||
if (escapedCSV.match(/[,"\n]/)) {
|
||||
escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
|
||||
}
|
||||
}
|
||||
csvData.push(escapedCSV);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
});
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
|
||||
}
|
||||
return csvData.join("");
|
||||
},
|
||||
/*
|
||||
getProcessedJson
|
||||
---------------
|
||||
Get only the data to export, if no fields are set return all the data
|
||||
*/
|
||||
getProcessedJson(data, header) {
|
||||
let keys = this.getKeys(data, header);
|
||||
let newData = [];
|
||||
let _self = this;
|
||||
data.map(function (item, index) {
|
||||
let newItem = {};
|
||||
for (let label in keys) {
|
||||
let property = keys[label];
|
||||
newItem[label] = _self.getValue(property, item);
|
||||
}
|
||||
newData.push(newItem);
|
||||
});
|
||||
|
||||
return newData;
|
||||
},
|
||||
getKeys(data, header) {
|
||||
if (header) {
|
||||
return header;
|
||||
}
|
||||
|
||||
let keys = {};
|
||||
for (let key in data[0]) {
|
||||
keys[key] = key;
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
/*
|
||||
parseExtraData
|
||||
---------------
|
||||
Parse title and footer attribute to the csv format
|
||||
*/
|
||||
parseExtraData(extraData, format) {
|
||||
let parseData = "";
|
||||
if (Array.isArray(extraData)) {
|
||||
for (var i = 0; i < extraData.length; i++) {
|
||||
if (extraData[i])
|
||||
parseData += format.replace("${data}", extraData[i]);
|
||||
}
|
||||
} else {
|
||||
parseData += format.replace("${data}", extraData);
|
||||
}
|
||||
return parseData;
|
||||
},
|
||||
|
||||
getValue(key, item) {
|
||||
const field = typeof key !== "object" ? key : key.field;
|
||||
let indexes = typeof field !== "string" ? [] : field.split(".");
|
||||
let value = this.defaultValue;
|
||||
|
||||
if (!field) value = item;
|
||||
else if (indexes.length > 1)
|
||||
value = this.getValueFromNestedItem(item, indexes);
|
||||
else value = this.parseValue(item[field]);
|
||||
|
||||
if (key.hasOwnProperty("callback"))
|
||||
value = this.getValueFromCallback(value, key.callback);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/*
|
||||
convert values with newline \n characters into <br/>
|
||||
*/
|
||||
valueReformattedForMultilines(value) {
|
||||
if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
|
||||
else return value;
|
||||
},
|
||||
preprocessLongNum(value) {
|
||||
if (this.stringifyLongNum) {
|
||||
if (String(value).startsWith("0x")) {
|
||||
return value;
|
||||
}
|
||||
if (!isNaN(value) && value != "") {
|
||||
if (value > 99999999999 || value < 0.0000000000001) {
|
||||
return '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getValueFromNestedItem(item, indexes) {
|
||||
let nestedItem = item;
|
||||
for (let index of indexes) {
|
||||
if (nestedItem) nestedItem = nestedItem[index];
|
||||
}
|
||||
return this.parseValue(nestedItem);
|
||||
},
|
||||
|
||||
getValueFromCallback(item, callback) {
|
||||
if (typeof callback !== "function") return this.defaultValue;
|
||||
const value = callback(item);
|
||||
return this.parseValue(value);
|
||||
},
|
||||
parseValue(value) {
|
||||
return value || value === 0 || typeof value === "boolean"
|
||||
? value
|
||||
: this.defaultValue;
|
||||
},
|
||||
base64ToBlob(data, mime) {
|
||||
let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
|
||||
let bstr = atob(base64);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8ClampedArray(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
},
|
||||
}, // end methods
|
||||
};
|
||||
|
||||
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
|
||||
/* server only */
|
||||
, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
||||
if (typeof shadowMode !== 'boolean') {
|
||||
createInjectorSSR = createInjector;
|
||||
createInjector = shadowMode;
|
||||
shadowMode = false;
|
||||
} // Vue.extend constructor export interop.
|
||||
|
||||
|
||||
var options = typeof script === 'function' ? script.options : script; // render functions
|
||||
|
||||
if (template && template.render) {
|
||||
options.render = template.render;
|
||||
options.staticRenderFns = template.staticRenderFns;
|
||||
options._compiled = true; // functional template
|
||||
|
||||
if (isFunctionalTemplate) {
|
||||
options.functional = true;
|
||||
}
|
||||
} // scopedId
|
||||
|
||||
|
||||
if (scopeId) {
|
||||
options._scopeId = scopeId;
|
||||
}
|
||||
|
||||
var hook;
|
||||
|
||||
if (moduleIdentifier) {
|
||||
// server build
|
||||
hook = function hook(context) {
|
||||
// 2.3 injection
|
||||
context = context || // cached call
|
||||
this.$vnode && this.$vnode.ssrContext || // stateful
|
||||
this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional
|
||||
// 2.2 with runInNewContext: true
|
||||
|
||||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
||||
context = __VUE_SSR_CONTEXT__;
|
||||
} // inject component styles
|
||||
|
||||
|
||||
if (style) {
|
||||
style.call(this, createInjectorSSR(context));
|
||||
} // register component module identifier for async chunk inference
|
||||
|
||||
|
||||
if (context && context._registeredComponents) {
|
||||
context._registeredComponents.add(moduleIdentifier);
|
||||
}
|
||||
}; // used by ssr in case component is cached and beforeCreate
|
||||
// never gets called
|
||||
|
||||
|
||||
options._ssrRegister = hook;
|
||||
} else if (style) {
|
||||
hook = shadowMode ? function () {
|
||||
style.call(this, createInjectorShadow(this.$root.$options.shadowRoot));
|
||||
} : function (context) {
|
||||
style.call(this, createInjector(context));
|
||||
};
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
if (options.functional) {
|
||||
// register for functional component in vue file
|
||||
var originalRender = options.render;
|
||||
|
||||
options.render = function renderWithStyleInjection(h, context) {
|
||||
hook.call(context);
|
||||
return originalRender(h, context);
|
||||
};
|
||||
} else {
|
||||
// inject component registration as beforeCreate hook
|
||||
var existing = options.beforeCreate;
|
||||
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
||||
}
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
var normalizeComponent_1 = normalizeComponent;
|
||||
|
||||
/* script */
|
||||
const __vue_script__ = script;
|
||||
|
||||
/* template */
|
||||
var __vue_render__ = function() {
|
||||
var _vm = this;
|
||||
var _h = _vm.$createElement;
|
||||
var _c = _vm._self._c || _h;
|
||||
return _c(
|
||||
"div",
|
||||
{ attrs: { id: _vm.idName }, on: { click: _vm.generate } },
|
||||
[_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])],
|
||||
2
|
||||
)
|
||||
};
|
||||
var __vue_staticRenderFns__ = [];
|
||||
__vue_render__._withStripped = true;
|
||||
|
||||
/* style */
|
||||
const __vue_inject_styles__ = undefined;
|
||||
/* scoped */
|
||||
const __vue_scope_id__ = undefined;
|
||||
/* module identifier */
|
||||
const __vue_module_identifier__ = undefined;
|
||||
/* functional template */
|
||||
const __vue_is_functional_template__ = false;
|
||||
/* style inject */
|
||||
|
||||
/* style inject SSR */
|
||||
|
||||
|
||||
|
||||
var JsonExcel = normalizeComponent_1(
|
||||
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
|
||||
__vue_inject_styles__,
|
||||
__vue_script__,
|
||||
__vue_scope_id__,
|
||||
__vue_is_functional_template__,
|
||||
__vue_module_identifier__,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
export default JsonExcel;
|
|
@ -1,660 +0,0 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.JsonExcel = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
var download = createCommonjsModule(function (module, exports) {
|
||||
//download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage
|
||||
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
||||
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
||||
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
|
||||
// v4 adds AMD/UMD, commonJS, and plain browser support
|
||||
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
|
||||
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
|
||||
// https://github.com/rndme/download
|
||||
|
||||
(function (root, factory) {
|
||||
{
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory();
|
||||
}
|
||||
}(commonjsGlobal, function () {
|
||||
|
||||
return function download(data, strFileName, strMimeType) {
|
||||
|
||||
var self = window, // this script is only for browsers anyway...
|
||||
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
|
||||
mimeType = strMimeType || defaultMime,
|
||||
payload = data,
|
||||
url = !strFileName && !strMimeType && payload,
|
||||
anchor = document.createElement("a"),
|
||||
toString = function(a){return String(a);},
|
||||
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
|
||||
fileName = strFileName || "download",
|
||||
blob,
|
||||
reader;
|
||||
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
|
||||
|
||||
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
||||
payload=[payload, mimeType];
|
||||
mimeType=payload[0];
|
||||
payload=payload[1];
|
||||
}
|
||||
|
||||
|
||||
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
|
||||
fileName = url.split("/").pop().split("?")[0];
|
||||
anchor.href = url; // assign href prop to temp anchor
|
||||
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
|
||||
var ajax=new XMLHttpRequest();
|
||||
ajax.open( "GET", url, true);
|
||||
ajax.responseType = 'blob';
|
||||
ajax.onload= function(e){
|
||||
download(e.target.response, fileName, defaultMime);
|
||||
};
|
||||
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
|
||||
return ajax;
|
||||
} // end if valid url?
|
||||
} // end if url?
|
||||
|
||||
|
||||
//go ahead and download dataURLs right away
|
||||
if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){
|
||||
|
||||
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
|
||||
payload=dataUrlToBlob(payload);
|
||||
mimeType=payload.type || defaultMime;
|
||||
}else {
|
||||
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
|
||||
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
|
||||
saver(payload) ; // everyone else can save dataURLs un-processed
|
||||
}
|
||||
|
||||
}else {//not data url, is it a string with special needs?
|
||||
if(/([\x80-\xff])/.test(payload)){
|
||||
var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length;
|
||||
for(i;i<mx;++i) tempUiArr[i]= payload.charCodeAt(i);
|
||||
payload=new myBlob([tempUiArr], {type: mimeType});
|
||||
}
|
||||
}
|
||||
blob = payload instanceof myBlob ?
|
||||
payload :
|
||||
new myBlob([payload], {type: mimeType}) ;
|
||||
|
||||
|
||||
function dataUrlToBlob(strUrl) {
|
||||
var parts= strUrl.split(/[:;,]/),
|
||||
type= parts[1],
|
||||
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
|
||||
binData= decoder( parts.pop() ),
|
||||
mx= binData.length,
|
||||
i= 0,
|
||||
uiArr= new Uint8Array(mx);
|
||||
|
||||
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
|
||||
|
||||
return new myBlob([uiArr], {type: type});
|
||||
}
|
||||
|
||||
function saver(url, winMode){
|
||||
|
||||
if ('download' in anchor) { //html5 A[download]
|
||||
anchor.href = url;
|
||||
anchor.setAttribute("download", fileName);
|
||||
anchor.className = "download-js-link";
|
||||
anchor.innerHTML = "downloading...";
|
||||
anchor.style.display = "none";
|
||||
document.body.appendChild(anchor);
|
||||
setTimeout(function() {
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
|
||||
}, 66);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle non-a[download] safari as best we can:
|
||||
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
|
||||
if(/^data:/.test(url)) url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
if(!window.open(url)){ // popup blocked, offer direct download:
|
||||
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//do iframe dataURL download (old ch+FF):
|
||||
var f = document.createElement("iframe");
|
||||
document.body.appendChild(f);
|
||||
|
||||
if(!winMode && /^data:/.test(url)){ // force a mime that will download:
|
||||
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
}
|
||||
f.src=url;
|
||||
setTimeout(function(){ document.body.removeChild(f); }, 333);
|
||||
|
||||
}//end saver
|
||||
|
||||
|
||||
|
||||
|
||||
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
|
||||
return navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
|
||||
if(self.URL){ // simple fast and modern way using Blob and URL:
|
||||
saver(self.URL.createObjectURL(blob), true);
|
||||
}else {
|
||||
// handle non-Blob()+non-URL browsers:
|
||||
if(typeof blob === "string" || blob.constructor===toString ){
|
||||
try{
|
||||
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
|
||||
}catch(y){
|
||||
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
|
||||
}
|
||||
}
|
||||
|
||||
// Blob but not URL support:
|
||||
reader=new FileReader();
|
||||
reader.onload=function(e){
|
||||
saver(this.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
return true;
|
||||
}; /* end download() */
|
||||
}));
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
var script = {
|
||||
props: {
|
||||
// mime type [xls, csv]
|
||||
type: {
|
||||
type: String,
|
||||
default: "xls",
|
||||
},
|
||||
// Json to download
|
||||
data: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
// fields inside the Json Object that you want to export
|
||||
// if no given, all the properties in the Json are exported
|
||||
fields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// this prop is used to fix the problem with other components that use the
|
||||
// variable fields, like vee-validate. exportFields works exactly like fields
|
||||
exportFields: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
// Use as fallback when the row has no field values
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
// Title(s) for the data, could be a string or an array of strings (multiple titles)
|
||||
header: {
|
||||
default: null,
|
||||
},
|
||||
// Footer(s) for the data, could be a string or an array of strings (multiple footers)
|
||||
footer: {
|
||||
default: null,
|
||||
},
|
||||
// filename to export
|
||||
name: {
|
||||
type: String,
|
||||
default: "data.xls",
|
||||
},
|
||||
fetch: {
|
||||
type: Function,
|
||||
},
|
||||
meta: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
worksheet: {
|
||||
type: String,
|
||||
default: "Sheet1",
|
||||
},
|
||||
//event before generate was called
|
||||
beforeGenerate: {
|
||||
type: Function,
|
||||
},
|
||||
//event before download pops up
|
||||
beforeFinish: {
|
||||
type: Function,
|
||||
},
|
||||
// Determine if CSV Data should be escaped
|
||||
escapeCsv: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// long number stringify
|
||||
stringifyLongNum: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// unique identifier
|
||||
idName() {
|
||||
var now = new Date().getTime();
|
||||
return "export_" + now;
|
||||
},
|
||||
|
||||
downloadFields() {
|
||||
if (this.fields) return this.fields;
|
||||
|
||||
if (this.exportFields) return this.exportFields;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async generate() {
|
||||
if (typeof this.beforeGenerate === "function") {
|
||||
await this.beforeGenerate();
|
||||
}
|
||||
let data = this.data;
|
||||
if (typeof this.fetch === "function" || !data) data = await this.fetch();
|
||||
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let json = this.getProcessedJson(data, this.downloadFields);
|
||||
if (this.type === "html") {
|
||||
// this is mainly for testing
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name.replace(".xls", ".html"),
|
||||
"text/html"
|
||||
);
|
||||
} else if (this.type === "csv") {
|
||||
return this.export(
|
||||
this.jsonToCSV(json),
|
||||
this.name.replace(".xls", ".csv"),
|
||||
"application/csv"
|
||||
);
|
||||
}
|
||||
return this.export(
|
||||
this.jsonToXLS(json),
|
||||
this.name,
|
||||
"application/vnd.ms-excel"
|
||||
);
|
||||
},
|
||||
/*
|
||||
Use downloadjs to generate the download link
|
||||
*/
|
||||
export: async function (data, filename, mime) {
|
||||
let blob = this.base64ToBlob(data, mime);
|
||||
if (typeof this.beforeFinish === "function") await this.beforeFinish();
|
||||
download(blob, filename, mime);
|
||||
},
|
||||
/*
|
||||
jsonToXLS
|
||||
---------------
|
||||
Transform json data into an xml document with MS Excel format, sadly
|
||||
it shows a prompt when it opens, that is a default behavior for
|
||||
Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
|
||||
*/
|
||||
jsonToXLS(data) {
|
||||
let xlsTemp =
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
|
||||
let xlsData = "<thead>";
|
||||
const colspan = Object.keys(data[0]).length;
|
||||
let _self = this;
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
xlsData += this.parseExtraData(
|
||||
header,
|
||||
'<tr><th colspan="' + colspan + '">${data}</th></tr>'
|
||||
);
|
||||
}
|
||||
|
||||
//Fields
|
||||
xlsData += "<tr>";
|
||||
for (let key in data[0]) {
|
||||
xlsData += "<th>" + key + "</th>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
xlsData += "</thead>";
|
||||
|
||||
//Data
|
||||
xlsData += "<tbody>";
|
||||
data.map(function (item, index) {
|
||||
xlsData += "<tr>";
|
||||
for (let key in item) {
|
||||
xlsData +=
|
||||
"<td>" +
|
||||
_self.preprocessLongNum(
|
||||
_self.valueReformattedForMultilines(item[key])
|
||||
) +
|
||||
"</td>";
|
||||
}
|
||||
xlsData += "</tr>";
|
||||
});
|
||||
xlsData += "</tbody>";
|
||||
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
xlsData += "<tfoot>";
|
||||
xlsData += this.parseExtraData(
|
||||
this.footer,
|
||||
'<tr><td colspan="' + colspan + '">${data}</td></tr>'
|
||||
);
|
||||
xlsData += "</tfoot>";
|
||||
}
|
||||
|
||||
return xlsTemp
|
||||
.replace("${table}", xlsData)
|
||||
.replace("${worksheet}", this.worksheet);
|
||||
},
|
||||
/*
|
||||
jsonToCSV
|
||||
---------------
|
||||
Transform json data into an CSV file.
|
||||
*/
|
||||
jsonToCSV(data) {
|
||||
let _self = this;
|
||||
var csvData = [];
|
||||
|
||||
//Header
|
||||
const header = this.header || this.$attrs.title;
|
||||
if (header) {
|
||||
csvData.push(this.parseExtraData(header, "${data}\r\n"));
|
||||
}
|
||||
|
||||
//Fields
|
||||
for (let key in data[0]) {
|
||||
csvData.push(key);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
//Data
|
||||
data.map(function (item) {
|
||||
for (let key in item) {
|
||||
let escapedCSV = item[key] + "";
|
||||
// Escaped CSV data to string to avoid problems with numbers or other types of values
|
||||
// this is controlled by the prop escapeCsv
|
||||
if (_self.escapeCsv) {
|
||||
escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
|
||||
if (escapedCSV.match(/[,"\n]/)) {
|
||||
escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
|
||||
}
|
||||
}
|
||||
csvData.push(escapedCSV);
|
||||
csvData.push(",");
|
||||
}
|
||||
csvData.pop();
|
||||
csvData.push("\r\n");
|
||||
});
|
||||
//Footer
|
||||
if (this.footer != null) {
|
||||
csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
|
||||
}
|
||||
return csvData.join("");
|
||||
},
|
||||
/*
|
||||
getProcessedJson
|
||||
---------------
|
||||
Get only the data to export, if no fields are set return all the data
|
||||
*/
|
||||
getProcessedJson(data, header) {
|
||||
let keys = this.getKeys(data, header);
|
||||
let newData = [];
|
||||
let _self = this;
|
||||
data.map(function (item, index) {
|
||||
let newItem = {};
|
||||
for (let label in keys) {
|
||||
let property = keys[label];
|
||||
newItem[label] = _self.getValue(property, item);
|
||||
}
|
||||
newData.push(newItem);
|
||||
});
|
||||
|
||||
return newData;
|
||||
},
|
||||
getKeys(data, header) {
|
||||
if (header) {
|
||||
return header;
|
||||
}
|
||||
|
||||
let keys = {};
|
||||
for (let key in data[0]) {
|
||||
keys[key] = key;
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
/*
|
||||
parseExtraData
|
||||
---------------
|
||||
Parse title and footer attribute to the csv format
|
||||
*/
|
||||
parseExtraData(extraData, format) {
|
||||
let parseData = "";
|
||||
if (Array.isArray(extraData)) {
|
||||
for (var i = 0; i < extraData.length; i++) {
|
||||
if (extraData[i])
|
||||
parseData += format.replace("${data}", extraData[i]);
|
||||
}
|
||||
} else {
|
||||
parseData += format.replace("${data}", extraData);
|
||||
}
|
||||
return parseData;
|
||||
},
|
||||
|
||||
getValue(key, item) {
|
||||
const field = typeof key !== "object" ? key : key.field;
|
||||
let indexes = typeof field !== "string" ? [] : field.split(".");
|
||||
let value = this.defaultValue;
|
||||
|
||||
if (!field) value = item;
|
||||
else if (indexes.length > 1)
|
||||
value = this.getValueFromNestedItem(item, indexes);
|
||||
else value = this.parseValue(item[field]);
|
||||
|
||||
if (key.hasOwnProperty("callback"))
|
||||
value = this.getValueFromCallback(value, key.callback);
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/*
|
||||
convert values with newline \n characters into <br/>
|
||||
*/
|
||||
valueReformattedForMultilines(value) {
|
||||
if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
|
||||
else return value;
|
||||
},
|
||||
preprocessLongNum(value) {
|
||||
if (this.stringifyLongNum) {
|
||||
if (String(value).startsWith("0x")) {
|
||||
return value;
|
||||
}
|
||||
if (!isNaN(value) && value != "") {
|
||||
if (value > 99999999999 || value < 0.0000000000001) {
|
||||
return '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
getValueFromNestedItem(item, indexes) {
|
||||
let nestedItem = item;
|
||||
for (let index of indexes) {
|
||||
if (nestedItem) nestedItem = nestedItem[index];
|
||||
}
|
||||
return this.parseValue(nestedItem);
|
||||
},
|
||||
|
||||
getValueFromCallback(item, callback) {
|
||||
if (typeof callback !== "function") return this.defaultValue;
|
||||
const value = callback(item);
|
||||
return this.parseValue(value);
|
||||
},
|
||||
parseValue(value) {
|
||||
return value || value === 0 || typeof value === "boolean"
|
||||
? value
|
||||
: this.defaultValue;
|
||||
},
|
||||
base64ToBlob(data, mime) {
|
||||
let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
|
||||
let bstr = atob(base64);
|
||||
let n = bstr.length;
|
||||
let u8arr = new Uint8ClampedArray(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
},
|
||||
}, // end methods
|
||||
};
|
||||
|
||||
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier
|
||||
/* server only */
|
||||
, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
||||
if (typeof shadowMode !== 'boolean') {
|
||||
createInjectorSSR = createInjector;
|
||||
createInjector = shadowMode;
|
||||
shadowMode = false;
|
||||
} // Vue.extend constructor export interop.
|
||||
|
||||
|
||||
var options = typeof script === 'function' ? script.options : script; // render functions
|
||||
|
||||
if (template && template.render) {
|
||||
options.render = template.render;
|
||||
options.staticRenderFns = template.staticRenderFns;
|
||||
options._compiled = true; // functional template
|
||||
|
||||
if (isFunctionalTemplate) {
|
||||
options.functional = true;
|
||||
}
|
||||
} // scopedId
|
||||
|
||||
|
||||
if (scopeId) {
|
||||
options._scopeId = scopeId;
|
||||
}
|
||||
|
||||
var hook;
|
||||
|
||||
if (moduleIdentifier) {
|
||||
// server build
|
||||
hook = function hook(context) {
|
||||
// 2.3 injection
|
||||
context = context || // cached call
|
||||
this.$vnode && this.$vnode.ssrContext || // stateful
|
||||
this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional
|
||||
// 2.2 with runInNewContext: true
|
||||
|
||||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
||||
context = __VUE_SSR_CONTEXT__;
|
||||
} // inject component styles
|
||||
|
||||
|
||||
if (style) {
|
||||
style.call(this, createInjectorSSR(context));
|
||||
} // register component module identifier for async chunk inference
|
||||
|
||||
|
||||
if (context && context._registeredComponents) {
|
||||
context._registeredComponents.add(moduleIdentifier);
|
||||
}
|
||||
}; // used by ssr in case component is cached and beforeCreate
|
||||
// never gets called
|
||||
|
||||
|
||||
options._ssrRegister = hook;
|
||||
} else if (style) {
|
||||
hook = shadowMode ? function () {
|
||||
style.call(this, createInjectorShadow(this.$root.$options.shadowRoot));
|
||||
} : function (context) {
|
||||
style.call(this, createInjector(context));
|
||||
};
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
if (options.functional) {
|
||||
// register for functional component in vue file
|
||||
var originalRender = options.render;
|
||||
|
||||
options.render = function renderWithStyleInjection(h, context) {
|
||||
hook.call(context);
|
||||
return originalRender(h, context);
|
||||
};
|
||||
} else {
|
||||
// inject component registration as beforeCreate hook
|
||||
var existing = options.beforeCreate;
|
||||
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
||||
}
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
var normalizeComponent_1 = normalizeComponent;
|
||||
|
||||
/* script */
|
||||
const __vue_script__ = script;
|
||||
|
||||
/* template */
|
||||
var __vue_render__ = function() {
|
||||
var _vm = this;
|
||||
var _h = _vm.$createElement;
|
||||
var _c = _vm._self._c || _h;
|
||||
return _c(
|
||||
"div",
|
||||
{ attrs: { id: _vm.idName }, on: { click: _vm.generate } },
|
||||
[_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])],
|
||||
2
|
||||
)
|
||||
};
|
||||
var __vue_staticRenderFns__ = [];
|
||||
__vue_render__._withStripped = true;
|
||||
|
||||
/* style */
|
||||
const __vue_inject_styles__ = undefined;
|
||||
/* scoped */
|
||||
const __vue_scope_id__ = undefined;
|
||||
/* module identifier */
|
||||
const __vue_module_identifier__ = undefined;
|
||||
/* functional template */
|
||||
const __vue_is_functional_template__ = false;
|
||||
/* style inject */
|
||||
|
||||
/* style inject SSR */
|
||||
|
||||
|
||||
|
||||
var JsonExcel = normalizeComponent_1(
|
||||
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
|
||||
__vue_inject_styles__,
|
||||
__vue_script__,
|
||||
__vue_scope_id__,
|
||||
__vue_is_functional_template__,
|
||||
__vue_module_identifier__,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
return JsonExcel;
|
||||
|
||||
})));
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
|
@ -1 +0,0 @@
|
|||
../../_downloadjs@1.4.7@downloadjs
|
|
@ -1,47 +0,0 @@
|
|||
{
|
||||
"name": "vue-json-excel",
|
||||
"version": "0.3.0",
|
||||
"description": "Download your JSON as an excel or CSV file directly from the browser",
|
||||
"main": "dist/vue-json-excel.umd.js",
|
||||
"module": "dist/vue-json-excel.esm.js",
|
||||
"scripts": {
|
||||
"build:dist": "rollup -c ./rollup.config.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jecovier/vue-json-excel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"vuejs",
|
||||
"vue2",
|
||||
"Excel",
|
||||
"xls",
|
||||
"csv",
|
||||
"json",
|
||||
"export",
|
||||
"json excel",
|
||||
"download",
|
||||
"component"
|
||||
],
|
||||
"author": "Jose Javier Espinoza",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jecovier/vue-json-excel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/jecovier/vue-json-excel#readme",
|
||||
"dependencies": {
|
||||
"downloadjs": "^1.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rollup": "^1.7.4",
|
||||
"rollup-plugin-commonjs": "^9.2.2",
|
||||
"rollup-plugin-node-resolve": "^4.0.1",
|
||||
"rollup-plugin-vue": "^4.7.2",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"__npminstall_done": "Thu Jun 03 2021 11:07:53 GMT+0800 (中国标准时间)",
|
||||
"_from": "vue-json-excel@0.3.0",
|
||||
"_resolved": "https://registry.nlark.com/vue-json-excel/download/vue-json-excel-0.3.0.tgz"
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import vue from 'rollup-plugin-vue';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
|
||||
export default {
|
||||
input: 'JsonExcel.vue',
|
||||
output: [
|
||||
{
|
||||
format: 'cjs',
|
||||
file: 'dist/vue-json-excel.cjs.js'
|
||||
},
|
||||
{
|
||||
format: 'esm',
|
||||
file: 'dist/vue-json-excel.esm.js'
|
||||
},
|
||||
{
|
||||
name: 'JsonExcel',
|
||||
format: 'umd',
|
||||
file: 'dist/vue-json-excel.umd.js'
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
vue(),
|
||||
commonjs(),
|
||||
resolve()
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
_downloadjs@1.4.7@downloadjs
|
|
@ -1 +0,0 @@
|
|||
_vue-json-excel@0.3.0@vue-json-excel
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"vue-json-excel": "^0.3.0"
|
||||
}
|
||||
}
|
|
@ -21,11 +21,11 @@ export default {
|
|||
common: 'https://common-api.pickmall.cn',
|
||||
buyer: 'https://buyer-api.pickmall.cn',
|
||||
seller: 'https://store-api.pickmall.cn',
|
||||
manager: 'https://admin-api.pickmall.cn'
|
||||
// common: 'http://192.168.0.103:8890',
|
||||
// buyer: 'http://192.168.0.103:8888',
|
||||
// seller: 'http://192.168.0.103:8889',
|
||||
// manager: 'http://192.168.0.103:8887'
|
||||
manager: 'https://admin-api.pickmall.cn',
|
||||
common: 'http://192.168.0.109:8890',
|
||||
buyer: 'http://192.168.0.109:8888',
|
||||
seller: 'http://192.168.0.109:8889',
|
||||
manager: 'http://192.168.0.109:8887'
|
||||
},
|
||||
api_prod: {
|
||||
common: 'https://common-api.pickmall.cn',
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<template slot-scope="{ row,index }" slot="action">
|
||||
<Button v-if="params.auditStatus == 99" type="primary" @click="()=>{liveGoodsData.splice(index,1)}">删除</Button>
|
||||
<Button v-if="params.auditStatus != 99 && !reviewed" ghost type="primary" @click="()=>{$router.push({path:'/goods-operation-edit',query:{id:row.goodsId}})}">查看</Button>
|
||||
<Button v-if="reviewed" :type="row.__selected ? 'primary' : ''" @click="selectedLiveGoods(row,index)">{{row.__selected ? '已':''}}选择</Button>
|
||||
<Button v-if="reviewed" :type="row.___selected ? 'primary' : ''" @click="selectedLiveGoods(row,index)">{{row.___selected ? '已':''}}选择</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="flex">
|
||||
|
@ -181,7 +181,7 @@ export default {
|
|||
this.liveGoodsData.forEach((item, index) => {
|
||||
val.forEach((callback) => {
|
||||
if (item.id == callback.id) {
|
||||
this.$set(this.liveGoodsData[index], "__selected", true);
|
||||
this.$set(this.liveGoodsData[index], "___selected", true);
|
||||
// this.selectedGoods.push(item);
|
||||
}
|
||||
});
|
||||
|
@ -208,15 +208,15 @@ export default {
|
|||
* 回调参数补充
|
||||
*/
|
||||
selectedLiveGoods(val, index) {
|
||||
if (!val.__selected) {
|
||||
val.__selected = true;
|
||||
this.$set(this.liveGoodsData[index], "__selected", true);
|
||||
if (!val.___selected) {
|
||||
val.___selected = true;
|
||||
this.$set(this.liveGoodsData[index], "___selected", true);
|
||||
this.selectedGoods.push(this.liveGoodsData[index]);
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
val.__selected = false;
|
||||
val.___selected = false;
|
||||
|
||||
this.$set(this.liveGoodsData[index], "__selected", true);
|
||||
this.$set(this.liveGoodsData[index], "___selected", true);
|
||||
this.selectedGoods.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,8 +64,8 @@ export default {
|
|||
return {
|
||||
// 时间
|
||||
|
||||
uvs: "", // 访客数
|
||||
pvs: "", // 浏览量
|
||||
uvs: 0, // 访客数
|
||||
pvs: 0, // 浏览量
|
||||
|
||||
dateList: [
|
||||
// 日期选择列表
|
||||
|
@ -198,8 +198,8 @@ export default {
|
|||
if (res.result) {
|
||||
this.data = res.result;
|
||||
res.result.forEach((item) => {
|
||||
this.uvs += item.uvNum;
|
||||
this.pvs += item.pvNum;
|
||||
this.uvs += parseInt(item.uvNum);
|
||||
this.pvs += parseInt(item.pvNum);
|
||||
});
|
||||
|
||||
if (!this.orderChart) {
|
||||
|
|
Loading…
Reference in New Issue