Commit e79407ce authored by 郝聪敏's avatar 郝聪敏

feature: v2.0提交

parent 21769f8c
import http from '../service/http.service';
import config from '../config';
export default {
getPageList(params) {
return http.get('editor/get/list', { params });
},
getPageById(params) {
return http.get(`editor/get/${params.pageId}`);
},
delPageById(pageId) {
return http.delete(`editor/${pageId}`);
},
updatePage(params) {
return http.post(`editor/update`, params);
},
savePage(params) {
return http.post(`editor/save`, params);
},
getTemplateList() {
return http.get('editor/get/template');
},
// 商品列表-查询
skuInfo(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/activity/activity-goods/sku-info/list`, params, {
accessToken: true
});
},
// 获取商品类目
categoryQuery() {
return http.get(`${config.opapiHost}/kdspOp/api/kdsp/op/rear-category/query/all`, {
accessToken: true
});
},
// 优惠券搜索列表
couponList(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/op/coupon/list`, params, {
accessToken: true
});
},
// 商品管理-商品专题列表_分页
specialPage(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/activity/activity-goods/special/list-page`, params, {
accessToken: true
});
},
};
\ No newline at end of file
......@@ -4,6 +4,6 @@ export default {
apiHost: `http://localhost:7001/`,
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`,
opapiHost: `${protocol}//opapi.q-gp.com`,
opapiHost: `https://opapi-vcc2.liangkebang.net`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
......@@ -2,8 +2,8 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default {
apiHost: `https://quantum-fe.liangkebang.net/`,
opApiHost: 'https://opapi-fe.liangkebang.net',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
<template>
<div class="ad" :class="['ad', { 'ad_one': column === 1, 'ad_two': column === 2, 'ad_three': column === 3 }]" :id="id">
<a :href="item.link" v-for="(item, index) in list" :key="index">
<cr-image :src="item.img" height="2.61rem" width="100%"></cr-image>
</a>
</div>
</template>
<script>
export default {
props: {
column: {
type: Number,
default: 2
},
list: {
type: Array,
default: () => ([])
}
}
}
</script>
<style lang="less" scoped>
.image-width(@width) {
a {
width: @width;
}
}
.ad {
display: flex;
justify-content: space-between;
margin: 0 12px;
&_one {
.image-width(100%);
}
&_two {
.image-width(49%);
}
&_three {
.image-width(32%);
}
}
</style>
\ No newline at end of file
<template>
<div class="goods" v-if="column === 1">
<div class="goods-item_one" :style="style" :key="goods.id" v-for="goods in couponList">
<div class="Gi_one-left">
<p>¥<span>{{ goods.couponAmt || 0 }}</span></p>
<p>{{ `满${goods.couponAmt || 0}元减${goods.limitAmt || 0}元` }}</p>
</div>
<div class="Gi_one-middle">
<p>{{ goods.name }}</p>
<p>2020.8.4-2020.9.4</p>
</div>
<cr-button class="Gi_one-right" type="primary">点击领取</cr-button>
<div class="goods-item-mask">
<p>已抢空</p>
</div>
</div>
</div>
<div v-else :class="['goods', {'goods_two': column === 2, 'goods_multiple': column === 3}]">
<div class="goods-item" :style="style" :key="goods.id" v-for="goods in couponList">
<p class="goods-item-title">{{ goods.name }}</p>
<p class="goods-item-amount">¥<span>{{ goods.couponAmt || 0 }}</span></p>
<p class="goods-item-condition">{{ `满${goods.couponAmt || 0}元减${goods.limitAmt || 0}元` }}</p>
<cr-button shape="circle" type="primary" >
点击领取
</cr-button>
<div class="goods-item-mask">
<p>已抢空</p>
</div>
</div>
</div>
</template>
<script>
import operationApi from '@api/operation.api';
export default {
inject: ['editor'],
props: {
couponsList: Array,
couponsNumber: Number,
column: {
type: Number,
default: 3
},
bgImage: String,
bgColor: {
type: String,
default: '#fff'
}
},
data() {
return {
list: []
}
},
computed: {
couponList() {
if (this.editor) this.$nextTick(() =>this.editor.adjustHeight());
return this.list.slice(0, this.couponsNumber);
},
style() {
return {
background: `url(${this.bgImage}) no-repeat 0 0 / cover`,
backgroundColor: this.bgColor
}
}
},
watch: {
couponsList: {
handler: async function (newVal) {
for(let i = 0; i < newVal.length;i++) {
const res = await operationApi.couponList({ couponId: newVal[i] });
const couponInfoList = res.couponInfoList;
if(couponInfoList && couponInfoList.length) this.list.push(couponInfoList[0]);
}
console.log('watch', this.list);
},
immediate: true
}
}
}
</script>
<style lang="less" scoped>
::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.goods {
width: 100%;
padding: 0 12px;
&-item {
position: relative;
padding: 4px 8px;
display: inline-flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 88px;
text-align: center;
background: url('./images/coupon-bg@2x.png') no-repeat 0 0 / cover;
font-size: 12px;
line-height: 18px;
&:last-child {
margin-right: 0;
}
&-title {
color: #EC1500;
}
&-amount {
color: #EC1500;
span {
font-size: 20px;
line-height: 26px;
font-weight: 600;
}
}
&-condition {
color: #999999;
}
&-mask {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
opacity: 0.7;
p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 66px;
height: 66px;
line-height: 66px;
text-align: center;
border-radius: 50%;
background-color: #000;
color: #fff;
font-weight: 600;
}
}
}
&_multiple {
overflow-x: auto;
white-space: nowrap;
padding: 0 4px;
.goods-item {
width: 114px;
margin-right: 4px;
}
}
&_two {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.goods-item {
margin-bottom: 8px;
width: 170px;
&_empty {
padding: 0;
margin: 0;
height: 0;
}
}
}
&-item_one {
position: relative;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
.Gi_one-left {
position: relative;
padding: 8px 0;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 90px;
&:after {
content: "";
position: absolute;
right: 0;
height: 100%;
border-style: dashed;
border-width: 0 1px 0 0;
border-color: #EC3333;
transform:scaleX(.5);
}
p {
line-height: 18px;
color: #EC3333;
span {
font-size: 20px;
line-height: 26px;
font-weight: 600;
}
}
}
.Gi_one-middle {
flex: 1;
padding: 8px 0;
margin-left: 8px;
text-align: left;
p {
&:first-child {
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
&:last-child {
line-height: 24px;
}
}
}
.Gi_one-right {
width: 56px;
height: 80px !important;
}
}
// 组件库替换变量处理
/deep/ .cr-button--primary {
height: 20px;
line-height: 20px;
border: 0;
font-size: 12px;
background: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
}
}
</style>
\ No newline at end of file
<template>
<div class="container">
<cr-image width="100%" height="2.4rem" :src="specialRecommend" @click="go()" v-if="showSpecial"></cr-image>
<div class="goods" v-if="column === 1">
<div class="goods-item goods-item_one" v-for="goods in goodsList" :key="goods.id">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" src="https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg"></cr-image>
<div class="goods-item_one-right">
<p class="goods-item-title">商品名称商品名称商品名称商品商品名称商品商品名称商品名称商品名称商品商品名称商品商品名称商品名称商品名称商品商品名称商品</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥320.00</p>
<p>¥999.00</p>
</div>
<cr-button
shape="circle"
type="primary"
>
立即购买
</cr-button>
</div>
</div>
</div>
</div>
<div class="goods" v-else>
<div :class="['goods-item', {'goods-item_three': column === 3, 'goods-item_two': column === 2}]" v-for="goods in goodsList" :key="goods.id">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" src="https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg"></cr-image>
<p class="goods-item-title">商品名称商品名称商品名称商品商品名称商品</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥320.00</p>
<p>¥999.00</p>
</div>
<div class="Gi-bottom-right">
<cr-image width="0.45rem" height="0.45rem" :src="shoppingCart"></cr-image>
</div>
</div>
</div>
<div class="goods-item goods-item_three goods-item_empty" v-if="showEmpty"></div>
</div>
</div>
</template>
<script>
import shoppingCart from './images/shopping-cart@2x.png';
import specialRecommend from './images/special-recommend.png';
import operationApi from '@api/operation.api';
export default {
inject: ['editor'],
props: {
goods: {
type: Object,
default: () => ({
type: 'goodsGroup',
ids: []
})
},
goodsNumber: Number,
column: {
type: Number,
default: 2
},
showSpecial: {
type: Boolean,
default: true
},
specialUrl: String,
specialLink: String,
},
data() {
return {
list: [],
shoppingCart,
specialRecommend
}
},
computed: {
goodsList() {
if (this.editor) this.$nextTick(() =>this.editor.adjustHeight());
return this.list.slice(0, this.goodsNumber);
},
showEmpty() {
return this.column === 3 && this.goodsNumber % 3 === 2;
}
},
watch: {
goods: {
handler: async function (newVal) {
if(newVal.type === 'goodsGroup' && newVal.ids.length) {
const { records } = await operationApi.skuInfo({ brandId: newVal.ids[0] });
if(records && records.length) this.list = records;
}
console.log('watch', this.list);
},
immediate: true
}
},
methods: {
go() {
if (this.specialLink) {
window.location.href = this.specialLink;
}
}
}
}
</script>
<style lang="less" scoped>
.container {
margin: 0 12px;
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 4px;
font-size: 0;
&-item {
border-radius: 8px;
background: #fff;
margin-bottom: 4px;
overflow: hidden;
&-title {
font-size: 13px;
line-height: 18px;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: left;
}
&-bottom {
display: flex;
justify-content: space-between;
align-items: center;
.Gi-bottom-left {
p {
text-align: left;
&:first-child {
font-size: 15px;
color: #EC3333;
}
&:last-child {
font-size: 13px;
color: #999999;
text-decoration:line-through;
}
}
}
.Gi-bottom-right {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
background: #FFF3F3;
border-radius: 50%;
/deep/ .cr-image {
width: 18px !important;
height: 18px !important;
}
}
}
&_one {
display: flex;
width: 100%;
.goods-item-img {
width: 112px !important;
height: 109px !important;
}
&-right {
padding: 6px 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.Gi-bottom-left {
margin-right: 10px;
flex: 1;
display: flex;
justify-content: space-around;
align-items: flex-end;
}
// 组件库替换变量处理
/deep/ .cr-button--primary {
height: 24px;
line-height: 24px;
border: 0;
background: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
}
}
}
&_two {
width: 49%;
.goods-item-img {
width: 100% !important;
height: 180px !important;
}
.goods-item-title {
margin: 8px 8px;
}
.goods-item-bottom {
margin: 0 8px 6px;
}
}
&_three {
width: 32%;
.goods-item-img {
width: 100% !important;
height: 117px !important;
}
.goods-item-title {
margin: 8px 4px;
}
.goods-item-bottom {
margin: 0 4px 6px;
.goods-item-bottom {
}
}
}
&_empty {
height: 0;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="tabs">
<cr-tabs @change="tabsChange" animated :underline-color="underlineColor" :title-active-color="activeColor" :title-inactive-color="inactiveColor">
<cr-tab v-for="(tab, index) in tabs" :key="index" :name="index" :title="tab.tabTitle">
<component :is="tab.name" v-bind="tab.props" :style="transformStyle(tab.commonStyle)"></component>
</cr-tab>
</cr-tabs>
</div>
</template>
<script>
import Goods from '../Goods/index.vue';
import Coupon from '../Coupon/index.vue';
export default {
name: 'goods-tabs',
components: {
Goods,
Coupon
},
props: {
list: {
type: Array,
default: () => []
},
childItem: {
type: Object,
default: () => ({ child: [] })
},
activeColor: {
type: String,
default: '#323233'
},
inactiveColor: {
type: String,
default: '#646566'
},
underlineColor: {
type: String,
default: '#1989fa'
}
},
data() {
return {
}
},
computed: {
tabs() {
const list = this.list.map(tab => {
const rs = { ...tab };
this.childItem?.child.forEach(ele => {
if (tab.componentId === ele.id) Object.assign(rs, tab, ele);
})
return rs;
});
console.log('tabs', this.childItem.child, list);
return list;
}
},
methods: {
tabsChange(name) {
console.log('change', name);
},
transformStyle(styleObj = {}) {
// console.log('transformStyle', styleObj);
const style = {};
for (const key of Object.keys(styleObj)) {
style[key] = styleObj[key];
if (key === 'backgroundImage') {
style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
return style;
}
}
}
</script>
<style lang="less" scoped>
/deep/ .cr-tabs__nav {
background-color: transparent;
}
/deep/ .cr-swipe-item {
&:nth-child(odd) {
background-color: #66c6f2;
}
&:nth-child(2n) {
background-color: #39a9ed;
}
height: 500px;
}
</style>
\ No newline at end of file
<template>
<div class="guide-cube">
<swiper v-if="showSwiper" class="guide-cube-swiper" ref="mySwiper" :options="swiperOptions">
<swiper-slide :style="style" :class="{'swiper-slide_two': slidesPerColumn === 2, 'swiper-slide_one': slidesPerColumn === 1 }" :key="index" v-for="(item, index) in list" @click="go(item.link)">
<cr-image width="" height="" :src="item.img" fit="cover" :show-loading="false"></cr-image>
</swiper-slide>
</swiper>
</div>
</template>
<script>
import { swiper, swiperSlide } from 'vue-awesome-swiper';
import 'swiper/dist/css/swiper.css';
export default {
inject: ['editor'],
components: {
swiper,
swiperSlide,
},
props: {
list: {
type: Array,
default: () => ([])
},
slidesPerColumn: {
type: Number,
default: 1
},
loop: Boolean,
autoplay: Boolean
},
data() {
const vm = this;
return {
showSwiper: true,
style: {},
swiperOptions: {
loop: this.loop,
slidesPerView: 4,
slidesPerColumn: this.slidesPerColumn,
spaceBetween: 8,
observer:true,
observeParents:true,
watchSlidesProgress: true,
autoplay: this.autoplay,
on: {
progress: function() {
// console.log('memorys', vm.memorys);
const slidesLength = this.slides.length;
for (let i = 0; i < this.slides.length; i++) {
const slide = this.slides.eq(i);
let scale = Math.abs(this.slides[i].progress % 1).toFixed(10) * 0.2;
// 记录初始状态,用来复位
if (i === 0) this.slides[0].scale = scale;
if (!this.slides[0].scale) {
vm.memorys[i] = vm.memorys[i] ? 0 : 1;
scale = 0.2;
}
// 差别对待不同索引
if (vm.memorys[i]) {
scale = 1 - scale;
} else {
scale = 0.8 + scale;
}
slide.transform(`scale3d(${scale}, ${scale}, 1)`);
}
},
},
}
}
},
computed: {
swiper() {
return this.$refs.mySwiper.swiper
},
memorys() {
const length = this.list.length;
const baseNumber = this.slidesPerColumn === 1 ? 2 : 4;
return new Array(length).fill(0).map((v, i) => [0, 3].includes(i % baseNumber) ? 1 : 0);
}
},
watch: {
slidesPerColumn(newVal) {
this.refreshSwiper('slidesPerColumn', newVal);
},
autoplay(newVal) {
this.refreshSwiper('autoplay', newVal);
},
loop(newVal) {
this.refreshSwiper('loop', newVal);
},
},
mounted() {
this.style = {
transition: 'all .4s cubic-bezier(.4, 0, .2, 1)'
}
},
methods: {
go(url) {
window.location.href = url;
},
refreshSwiper(key, val) {
this.swiperOptions[key] = val;
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
if (this.editor) setTimeout(() =>this.editor.adjustHeight(), 0);
}
},
}
</script>
<style lang="less" scoped>
.guide-cube {
padding: 12px;
&-swiper {
width: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
width: 81px;
height: 81px;
/deep/ .cr-image {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="marquee">
<swiper class="marquee-swiper" ref="mySwiper" :options="swiperOptions">
<swiper-slide :key="index" v-for="(item, index) in list">
<span :style="styles">{{item.text}}</span>
</swiper-slide>
</swiper>
</div>
</template>
<script>
import { swiper, swiperSlide } from 'vue-awesome-swiper';
import 'swiper/dist/css/swiper.css';
export default {
inject: ['editor'],
components: {
swiper,
swiperSlide,
},
props: {
list: {
type: Array,
default: () => ([])
},
fontColor: {
type: String,
default: '#666666'
}
},
data() {
return {
swiperOptions: {
// loop: true,
observer:true,
observeParents:true,
direction: 'vertical',
allowTouchMove: false,
autoplay: true
}
}
},
computed: {
swiper() {
return this.$refs.mySwiper.swiper
},
styles() {
return {
color: this.fontColor
}
}
},
}
</script>
<style lang="less" scoped>
.marquee {
width: 100%;
height: 30px;
border-radius: 8px;
background-color: #D8D8D8;
.swiper-container {
height: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 14px;
line-height: 20px;
color: #666666;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="placeholder" :style="style"></div>
</template>
<script>
export default {
props: {
height: Number,
},
computed: {
style() {
return {
height: `${this.height}px`
}
}
}
}
</script>
<style lang="less" scoped>
.placeholder {
min-height: 10px;
}
</style>
\ No newline at end of file
This diff is collapsed.
import { cloneDeep } from 'lodash';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component, Prop, Vue, Mixins } from 'vue-property-decorator';
import { Action, Mutation, State, Getter } from 'vuex-class';
import LoginForm from '@/lib/Form/index.vue';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import { resizeDiv } from '@/service/utils.service';
@Component({ components: { LoginForm }, name: 'FreedomContainer' })
export default class FreedomContainer extends Vue {
@Component({ name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.curChildIndex) curChildIndex;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
......@@ -14,21 +13,6 @@ export default class FreedomContainer extends Vue {
@Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop(String) backgroundImage;
transformStyle(styleObj) {
const style = {};
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(3)}rem`;
} else {
style[key] = styleObj[key].includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(3)}rem` : styleObj[key];
}
if (key === 'backgroundImage') {
style.backgroundImage = `url(${style.backgroundImage})`;
}
}
return style;
}
mounted() {
// 根据背景图设置元素高度
const index = this.pageData?.elements?.findIndex(v => v.point?.responsive);
......
import { kebabCase } from 'lodash';
import { Vue, Component, Watch } from 'vue-property-decorator';
import { Vue, Component, Watch, Provide, Mixins } from 'vue-property-decorator';
import eleConfig from '../../../editor/utils/config';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import GridLayout from '../../component/VueGridLayout/GridLayout.vue';
import GridItem from '../../component/VueGridLayout/GridItem.vue';
import LoginForm from '@/lib/Form/index.vue';
// import LoginForm from '@/lib/Form/index.vue';
import DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { Getter, State, Mutation } from 'vuex-class';
import GuideCube from '@/lib/GuideCube/index.vue';
import GoodsTabs from '@/lib/GoodsTabs/index.vue';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
@Component({ components: { FreedomContainer, GridLayout, GridItem, LoginForm, DownloadGuide }, name: 'Activity'})
export default class Activity extends Vue {
@Component({ components: { FreedomContainer, GridLayout, GridItem, DownloadGuide, GoodsTabs, GuideCube }, name: 'Activity'})
export default class Activity extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.pageInfo.pageName) pageName;
@State(state => state.editor.gridLayout.rowHeight) rowHeight;
@Mutation('DEL_ELEMENTS') delPageInfo;
@Provide('editor');
isLayoutComReady = false;
......
<template>
<div class="activity">
<div class="activity" :style="transformStyle(pageData.commonStyle)">
<grid-layout
:layout.sync="layout"
:isDraggable="false"
......@@ -20,7 +20,7 @@
:h="item.point.h"
:i="item.point.i"
:key="item.point.i">
<component class="Dcmc-panel-com" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" @delete="delPageInfo(index)" v-bind="item.props"></component>
<component :style="transformStyle(item.commonStyle)" class="Dcmc-panel-com" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
</div>
......@@ -45,6 +45,7 @@
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
min-height: 667px;
// transform: translateY(-10px);
transition-property: none;
.vue-grid-item {
......
......@@ -2,6 +2,7 @@ import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import { kebabCase, chunk, flatten } from 'lodash';
import { State } from 'vuex-class';
import { v4 as uuid } from 'uuid';
@Component({ name: 'DynamicComponent' })
export default class DynamicComponent extends Vue {
......@@ -18,30 +19,26 @@ export default class DynamicComponent extends Vue {
dragstart(event, eleName) {
this.$emit('dragstart');
const eleConfig = flatten(this.eleConfig).find(config => config.eleName === eleName);
const id = uuid().slice(19);
const compontObj = {
id,
name: eleName,
title: eleConfig.title,
point: {x: 0, y: 0, w: this.colNum, h: 300, i: id},
schame: eleConfig.config,
props: {...eleConfig.value},
commonStyle: eleConfig.commonStyle
};
// const props = this.getProps(eleName);
if (eleName.includes('template')) {
event.dataTransfer.setData('text', JSON.stringify({
template: eleConfig.page
}));
} else if (eleName === 'freedom-container') {
event.dataTransfer.setData('text', JSON.stringify({
name: eleName,
title: eleConfig.title,
point: {x: 0, y: 2, w: this.colNum, h: eleConfig.h || 1, i: '0'},
child: [],
schame: eleConfig.config,
props: {...eleConfig.value},
commonStyle: eleConfig.commonStyle
}));
// 自由容器和商品导航组件有child属性
} else if (eleName === 'freedom-container' || eleName === 'goods-tabs') {
event.dataTransfer.setData('text', JSON.stringify({ ...compontObj, child: [] }));
} else {
event.dataTransfer.setData('text', JSON.stringify({
name: eleName,
title: eleConfig.title,
point: {x: 0, y: 0, w: this.colNum, h: eleConfig.h || 1, i: '0'},
schame: eleConfig.config,
props: {...eleConfig.value},
commonStyle: eleConfig.commonStyle
}));
event.dataTransfer.setData('text', JSON.stringify(compontObj));
}
event.dataTransfer.effectAllowed = 'copyMove';
}
......
......@@ -34,7 +34,7 @@
}
}
/deep/ .ivu-card {
.ivu-card {
&-head {
padding: 5px;
}
......
<template>
<div class="color-selector">
<Input class="color-selector-input" v-model="color" placeholder="请输入"></Input>
<ColorPicker v-model="color" />
<Input class="color-selector-input" v-model="color" placeholder="请输入" @on-change="change"></Input>
<ColorPicker v-model="color" @on-change="change" />
</div>
</template>
<script>
......@@ -14,8 +14,16 @@
color: this.value,
}
},
created() {
console.log(this.color);
},
watch: {
color(val) {
value(val) {
this.color = val;
}
},
methods: {
change(val) {
console.log('color', val);
this.$emit('input', val);
}
......
<template>
<div class="column-selector">
<Tooltip placement="top" content="通栏">
<Button type="ghost" icon="minus-round" @click="select(1)"></Button>
</Tooltip>
<Tooltip placement="top" content="两列">
<Button type="ghost" icon="ios-pause" @click="select(2)"></Button>
</Tooltip>
<Tooltip placement="top" content="三列">
<Button type="ghost" icon="navicon-round" @click="select(3)" ></Button>
</Tooltip>
</div>
</template>
<script>
export default {
props: {
value: Number,
},
data() {
return {
column: this.value,
}
},
watch: {
value(val) {
this.color = val;
}
},
methods: {
select(column) {
console.log('column-selector', column);
this.$emit('input', column);
}
}
}
</script>
<style lang="less">
</style>
\ No newline at end of file
import {Component, Mixins, Prop, Watch, Vue} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import EventBus from '@/service/eventBus.service';
@Component({ components: {}, name: 'ComponentSelect' })
export default class DynamicForm extends Vue {
@State(state => state.editor.curEleIndex) curEleIndex;
@Getter('pageData') pageData;
@Prop([String, Number]) value;
selected: string = this.value;
list: object[] = [];
@Watch('selected', { immediate: true })
onSelectedChange(val) {
this.$emit('input', val);
}
@Watch('curEleIndex', { immediate: true })
onCurEleIndexChange(val) {
this.updateOptions(val);
}
mounted() {
EventBus.$on('component-moved', () => {
this.updateOptions();
});
}
updateOptions(val = this.curEleIndex) {
if (val || val === 0) {
const pointY = this.pageData?.elements[val]?.point?.y;
this.list = this.pageData?.elements?.filter(v => pointY < v?.point?.y)?.map((element, index) => ({ id: element.id, label: element.title + '-' + element.id}));
console.log('curEleIndex', pointY, this.pageData?.elements?.filter(v => pointY < v?.point?.y), this.list);
}
}
}
\ No newline at end of file
<template>
<div class="select">
<Select v-model="selected">
<Option v-for="item in list" :value="item.id" :key="item.id">{{ item.label }}</Option>
</Select>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less">
.color-selector {
display: flex;
align-items: center;
justify-content: space-between;
&-input {
flex-basis: 150px;
}
}
</style>
\ No newline at end of file
const couponTypeList: object[] = [
{
id: 1,
name: '满减券',
},
{
id: 2,
name: '运费券',
},
];
const receiverTypeList: object[] = [
{
id: 1,
name: '主动领取',
},
{
id: 2,
name: '自动发放',
},
{
id: 3,
name: '不限',
},
];
const listStatus: object[] = [
{
id: 'WAIT_ON_LINE',
name: '待发布',
},
{
id: 'ON_LINE',
name: '已上架',
},
{
id: 'OFF_LINE',
name: '已下架',
},
];
const statusList: object[] = [
{
id: 1,
name: '待发布',
},
{
id: 2,
name: '已上架',
},
{
id: 3,
name: '已下架',
},
{
id: 4,
name: '已过期',
},
];
const columns: object[] = [
{
type: 'selection',
width: 60,
hideSearch: true
},
{
title: '批次id',
key: 'id',
hideSearch: true
},
{
title: '批次id',
hideTable: true,
key: 'couponId',
formType: 'input'
},
{
title: '优惠券名称',
key: 'name',
hideSearch: true
},
{
title: '优惠券类型',
key: 'couponType',
formType: 'select',
valueEnum: couponTypeList.reduce((pre, cur) => {
pre[cur.id] = cur.name;
return pre;
}, {}),
render: (h, params) => {
const obj = couponTypeList.find(item => item.id === params.row.couponType) || {};
return h('div', obj.name);
},
},
{
title: '优惠券面值',
key: 'couponAmt',
hideSearch: true,
},
{
title: '优惠券属性',
key: '',
hideSearch: true,
render: (h, params) => {
const obj = params.row.couponType === 1 ? `满${params.row.limitAmt}${params.row.couponAmt}元` : `满${params.row.limitAmt}可用`;
return h('div', obj);
},
},
{
title: '发行数量',
key: 'pushCount',
hideSearch: true,
},
{
title: '发行有效期',
key: 'receiverTime',
hideSearch: true
},
{
title: '使用有效期',
key: 'useTime',
hideSearch: true
},
{
title: '领取方式',
key: 'receiverType',
hideSearch: true,
render: (h, params) => {
const obj = receiverTypeList.find(item => item.id === params.row.receiverType) || {};
return h('div', obj.name);
},
},
{
title: '状态',
key: 'status',
hideSearch: true,
render: (h, params) => {
const obj = listStatus.find(item => item.id === params.row.status) || {};
return h('div', obj.name);
}
},
{
title: '状态',
key: 'status',
formType: 'select',
hideTable: true,
valueEnum: statusList.reduce((pre, cur) => {
pre[cur.id] = cur.name;
return pre;
}, {}),
}
];
export default columns;
\ No newline at end of file
import {Component, Prop, Watch, Vue} from 'vue-property-decorator';
import TableModal from '../TableModal/index.vue';
import couponColumn from './columns/coupon.column';
import { cloneDeep } from 'lodash';
import operationApi from '@api/operation.api';
@Component({ components: { TableModal }, name: 'CouponTableModal' })
export default class CouponTableModal extends Vue {
@Prop({ default: () => ([]), type: Array }) value;
@Prop({ default: () => ([]), type: Array }) formControl;
goods: object = cloneDeep(this.value);
table: object[] = [
{
title: '选择优惠券',
type: 'coupon',
multiple: true,
columns: couponColumn,
query: this.query
}
];
@Watch('goods')
onFormChange(newVal) {
this.$emit('input', newVal);
}
async query(data) {
const res = await operationApi.couponList(data);
const couponInfoList = res?.couponInfoList?.map(item => {
item.receiverTime = `${item.receiverStartTime.slice(0, 10)}--${item.receiverEndTime.slice(0, 10)}`;
item.useTime = item.useTimeStart ? `${item.useTimeStart}-${item.useTimeEnd}` : `自领取${item.receiverDaysValid}天后生效,有效天数${item.validDays}天`;
return item;
});
return { data: couponInfoList || [], total: res?.pageInfo?.totalCount };
}
}
\ No newline at end of file
<template>
<table-modal :table="table" :formControl="formControl" v-model="goods" title="优惠券"></table-modal>
</template>
<script lang="ts" src="./index.ts"></script>
<style></style>
\ No newline at end of file
<template>
<div class="form-list">
<Card class="form-list-card" :key="index" v-for="(item, index) in list">
<div slot='title' class="Fl-card-title">
<h4>项目{{index + 1}}</h4>
<a @click="del(index)">删除</a>
</div>
<Form :model="item" :label-width="80">
<FormItem :prop="`${keywords.key}`" :label="keywords.name" :key="idx" v-for="(keywords, idx) in formControl">
<component :is="getComponent(keywords.type)" :options="item.options" v-model="item[keywords.key]" />
</FormItem>
</Form>
</Card>
<div class="form-list-button">
<Button type="dashed" icon="plus-round" @click="add">添加项目</Button>
</div>
</div>
</template>
<script>
import ComponentSelect from '../ComponentSelect/index.vue';
import Upload from '../Upload/index.vue';
export default {
components: {
ComponentSelect,
Upload
},
props: {
value: {
type: Array,
default: () => []
},
formControl: {
type: Array,
default: () => []
},
name: String
},
data() {
return {
list: this.value,
}
},
watch: {
list(val) {
this.$emit('input', val);
},
value(val) {
this.list = val;
}
},
methods: {
getComponent(type) {
let result = type;
switch (type) {
case 'text':
result = 'Input';
break;
case 'select':
result = 'BaseSelect';
break;
case 'checkbox' :
result = 'Checkbox';
break;
case 'textarea' :
result = 'Textarea';
break;
case 'number' :
result = 'Number';
break;
}
return result;
},
add() {
const object = this.formControl.reduce((pre, cur) => {
pre[cur.key] = '';
return pre;
}, {})
this.list.push(object);
},
del(index) {
this.list.splice(index, 1);
}
}
}
</script>
<style lang="less" scoped>
.form-list {
/deep/ .ivu-card {
margin-bottom: 16px;
.ivu-form {
.ivu-form-item-label {
text-align: left;
}
}
}
h3 {
padding-bottom: 10px;
text-align: center;
}
&-card {
/deep/ .ivu-card-head{
background: #f0f2f5;
}
.Fl-card-title {
display: flex;
justify-content: space-between;
}
}
&-button {
margin-top: 20px;
text-align: center;
}
}
</style>
\ No newline at end of file
const commodityChannel: object[] = [
{
id: 1,
name: '自营',
},
{
id: 2,
name: '京东开普勒',
},
{
id: 3,
name: '京东联盟',
},
{
id: 4,
name: '众联 ',
},
{
id: 5,
name: '企业购',
},
];
const goodsStatus: object[] = [
{
id: 1,
name: '未上架',
},
{
id: 2,
name: '审核中',
},
{
id: 3,
name: '已上架',
},
{
id: 4,
name: '我方下架',
},
{
id: 5,
name: '三方下架',
},
];
const columns = function() {
return [
{
type: 'selection',
width: 60,
hideSearch: true
},
{
title: '商品ID',
key: 'id',
formType: 'input'
},
{
title: '商品图片',
key: 'imageUrl',
width: 130,
hideSearch: true,
render: (h, params) => {
return h('img', {
attrs: {
src: params.row.imageUrl,
width: 50,
height: 50
}
});
}
},
{
title: '商品名称',
key: 'skuName',
width: 155,
hideSearch: true
},
{
title: '商品渠道',
key: 'skuSource',
hideSearch: true,
render: (h, params) => {
const obj = commodityChannel.find(item => item.id === params.row.skuSource) || {};
return h('div', obj.name);
},
},
{
title: '销售价格',
key: 'salePrice',
hideSearch: true
},
{
title: '划线价格',
key: 'marketPrice',
hideSearch: true
},
{
title: '发布状态',
key: 'status',
formType: 'select',
valueEnum: goodsStatus.reduce((pre, cur) => {
pre[cur.id] = cur.name;
return pre;
}, {}),
render: (h, params) => {
const obj = goodsStatus.find(item => item.id === params.row.status) || {};
return h('div', obj.name);
},
},
{
title: '创建时间',
key: 'createdAt',
hideSearch: true
},
{
title: '发布时间',
key: 'pushedAt',
hideSearch: true
},
{
title: '下架时间',
key: 'offlineAt',
hideSearch: true
},
{
title: '标签',
hideSearch: true,
render: (h, params) => {
const labelName = [];
if (params.row.labelList) {
params.row.labelList.forEach(e => {
labelName.push(e.labelName);
});
}
return h('div', labelName.join(','));
},
},
{
title: '商品类目',
key: 'cid1',
formType: 'treeSelect',
hideTable: true,
valueEnum: this.getCategory
},
];
};
export default columns;
\ No newline at end of file
const groupColumns = function() {
return [
{
type: 'selection',
width: 60,
hideSearch: true
},
{
title: '专题ID',
key: 'id',
formType: 'input'
},
{
title: '专题名称',
key: 'name',
formType: 'input'
},
{
title: '有效期',
key: 'validityTime',
hideSearch: true,
render: (h, params) => {
const time = `${params.row.startTime}${params.row.endTime}`;
return h('div', time);
},
},
{
title: '标签',
key: 'labelName',
hideSearch: true,
render: (h, params) => {
const labelName = [];
params.row?.listLabel?.forEach(e => {
labelName.push(e.labelName);
});
return h('div', labelName.join(','));
},
},
{
title: '创建时间',
key: 'createdAt',
hideSearch: true,
}
];
};
export default groupColumns;
\ No newline at end of file
import {Component, Prop, Watch, Vue} from 'vue-property-decorator';
import TableModal from '../TableModal/index.vue';
import goodsColumn from './columns/goods.column';
import goodsGroupColumn from './columns/goodsGroup.column';
import { cloneDeep } from 'lodash';
import operationApi from '@api/operation.api';
@Component({ components: { TableModal }, name: 'GoodsTableModal' })
export default class GoodsTableModal extends Vue {
@Prop({ default: () => ({
type: 'goods',
ids: []
}), type: Object }) value;
@Prop({ default: () => ([]), type: Array }) formControl;
goods: object = cloneDeep(this.value);
table: object[] = [
{
title: '选择商品',
type: 'goods',
multiple: true,
columns: goodsColumn.call(this),
query: this.query
},
{
title: '选择商品组',
type: 'goodsGroup',
multiple: false,
columns: goodsGroupColumn.call(this),
query: this.queryGroup
}
];
@Watch('goods')
onFormChange(newVal) {
this.$emit('input', newVal);
}
async query(data) {
const { records, total } = await operationApi.skuInfo({ type: 'list', ...data });
records.forEach(record => {
if (this.goods.ids.some(v => v === record.id)) {
record._checked = true;
}
});
return { data: records || [], total };
},
async getCategory() {
function recursionData(data) {
const list = [];
data.forEach(item => {
const { categoryId: id, categoryName: label, categoryLevel: level, children } = item;
const itemData = { id, label, level };
if (item.children && item.children.length) { itemData.children = recursionData(children); }
list.push(itemData);
});
return list;
}
const res = await operationApi.categoryQuery();
return recursionData(res.level1List || []);
},
async queryGroup(data) {
const { records, total } = await operationApi.specialPage(data);
records.forEach(record => {
if (this.goods.ids.some(v => v === record.id)) {
record._checked = true;
}
});
return { data: records || [], total };
}
}
\ No newline at end of file
<template>
<table-modal :table="table" :formControl="formControl" v-model="goods" title="商品"></table-modal>
</template>
<script lang="ts" src="./index.ts"></script>
<style></style>
\ No newline at end of file
import {Component, Prop, Watch, Vue, Mixins} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { cloneDeep } from 'lodash';
import Number from '../Number/index.vue';
import QGTable from '@editor/component/QgTable/index.vue';
import { validateType } from '@/service/utils.service';
import operationApi from '@api/operation.api';
import DynamicFormMixin from '../mixins/dynamicForm.mixin';
@Component({ components: { Number, QGTable }, name: 'ComponentSelect' })
export default class DynamicForm extends Mixins(DynamicFormMixin) {
@State(state => state.editor.curEleIndex) curEleIndex;
@Getter('pageData') pageData;
@Prop({ default: () => ([]), type: Array }) formControl;
@Prop({ default: () => ([]), type: Array }) table;
@Prop([Object, Array]) value;
@Prop(String) title;
form: object = {};
modal: boolean = false;
selections: object[] = [];
activeName: number = 0;
get idsLength() {
if (validateType(this.value) === 'object') {
return this.value?.ids?.length;
}
return this.value?.length;
}
@Watch('curEleIndex', { immediate: true })
onElementChange(newVal) {
this.formControl.forEach(schame => {
this.$set(this.form, schame.key, this.pageData.elements[this.curEleIndex].props[schame.key]);
});
}
@Watch('form', { immediate: true, deep: true })
onFormChange(newVal) {
console.log('onFormChange', newVal);
let parent = this.$parent;
while (!parent.modProps) {
parent = parent.$parent;
}
parent.modProps(this.form, 'component');
}
add() {
this.modal = true;
}
selectionChange(selection) {
if (!this.table[this.activeName]?.multiple && selection.length > 1) {
return this.$Notice.warning({
title: '商品组只能单选'
});
}
this.selections = selection;
}
ok() {
if (this.table.length > 1) {
this.$emit('input', {
type: this.table[this.activeName].type,
ids: this.selections.map(v => v.id)
});
} else {
this.$emit('input', this.selections.map(v => v.id));
}
}
cancel() {
this.selections = [];
}
menuChange(name) {
this.activeName = name;
this.selections = [];
}
editText(type) {
let rs = '';
switch (type) {
case 'edit':
rs = `编辑${this.title}`;
break;
case 'add':
rs = `添加${this.title}`;
break;
case 'choose':
rs = `选择${this.title}`;
break;
}
return rs;
}
}
\ No newline at end of file
<template>
<div class="table-modal">
<Button v-if="!idsLength" type="dashed" icon="plus-round" @click="add">{{editText('add')}}</Button>
<Card v-else class="table-modal-card">
<div slot='title' class="Fl-card-title">
<a slot='title' @click="add()">{{editText('edit')}}</a>
</div>
<Form :model="form" :label-width="80">
<FormItem :prop="`${keywords.key}`" :label="keywords.name" :key="idx" v-for="(keywords, idx) in formControl">
<component :is="getComponent(keywords.type)" :options="keywords.options" v-model="form[keywords.key]" />
</FormItem>
</Form>
</Card>
<Modal
:width="1150"
v-model="modal"
@on-ok="ok"
@on-cancel="cancel"
class="table-modal-popup"
:title="editText('choose')">
<div class="Tm-popup-body" v-if="modal">
<Menu v-if="table.length > 1" width="90px" :active-name="activeName" @on-select="menuChange">
<MenuItem :name="index" :key="index" v-for="(item, index) in table">
<span>{{item.title}}</span>
</MenuItem>
</Menu>
<div class="Tmp-body-content">
<template v-for="(item, index) in table">
<div v-show="activeName === index" >
<QGTable
ref="qgTable"
:columns="item.columns"
:request="item.query"
:hideAdd="true"
:height="500"
@on-selection-change="selectionChange"
>
</QGTable>
</div>
</template>
</div>
</div>
</Modal>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.table-modal {
&-card {
/deep/ .ivu-card-head{
background: #f0f2f5;
}
/deep/ .ivu-form {
.ivu-form-item-label {
width: 100px !important;
text-align: left;
}
}
.Fl-card-title {
text-align: right;
}
}
&-popup {
/deep/ .tableComStyle {
min-height: 0;
// .tableGroupStyle {
// height: 500px;
// overflow: scroll;
// }
}
.Tm-popup-body {
display: flex;
.Tmp-body-content {
flex: 1;
}
/deep/ .ivu-menu-item {
text-align: center;
padding: 10px 0;
}
}
}
}
</style>
\ No newline at end of file
......@@ -72,6 +72,7 @@
.upload {
display: flex;
align-items: center;
width: 100%;
&-img {
display: inline-block;
width: 60px;
......
import {Component, Vue } from 'vue-property-decorator';
@Component({ name: 'dynamicFormMixin' })
export default class DynamicFormMixin extends Vue {
getComponent(type) {
let result = type;
switch (type) {
case 'text':
result = 'Input';
break;
case 'select':
result = 'BaseSelect';
break;
case 'checkbox' :
result = 'Checkbox';
break;
case 'textarea' :
result = 'Textarea';
break;
case 'number' :
result = 'Number';
break;
}
return result;
}
}
\ No newline at end of file
import {Component, Mixins, Prop, Watch} from 'vue-property-decorator';
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import { reduce, ceil, subtract, divide, pick, cloneDeep, pickBy } from 'lodash';
import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import DynamicFormMixin from './component/mixins/dynamicForm.mixin';
import Upload from './component/Upload/index.vue';
import ColorSelector from './component/ColorSelector/index.vue';
import BaseSelect from './component/BaseSelect/index.vue';
import ComponentSelect from './component/ComponentSelect/index.vue';
import FormList from './component/FormList/index.vue';
import GoodsTableModal from './component/GoodsTableModal/index.vue';
import CouponTableModal from './component/CouponTableModal/index.vue';
import Textarea from './component/Textarea/index.vue';
import Number from './component/Number/index.vue';
import ColumnSelector from './component/ColumnSelector/index.vue';
import { resizeDiv, getStyle } from '@/service/utils.service';
@Component({ components: { Upload, ColorSelector, BaseSelect, Textarea, Number }, name: 'DynamicForm' })
export default class DynamicForm extends Mixins(ContextMenu) {
@Component({ components: { Upload, ColorSelector, BaseSelect, FormList, Textarea, Number, ComponentSelect, GoodsTableModal, CouponTableModal, ColumnSelector }, name: 'DynamicForm' })
export default class DynamicForm extends Mixins(ContextMenuMixin, DynamicFormMixin) {
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex;
@Getter('pageData') pageData;
......@@ -26,43 +32,112 @@ export default class DynamicForm extends Mixins(ContextMenu) {
element = this.pageData.elements[this.curEleIndex];
}
}
console.log('curElement', element);
return element;
}
get point() {
return this.curEleIndex || this.curEleIndex === 0 ? this.pageData.elements[this.curEleIndex]?.point : { h: 0, w: 0 };
return this.curEleIndex || this.curEleIndex === 0 ? cloneDeep(this.pageData.elements[this.curEleIndex]?.point) : { h: 0, w: 0 };
}
get commonStyle() {
return (this.curEleIndex || this.curEleIndex === 0) && (this.curChildIndex || this.curChildIndex === 0) ? this.pageData.elements[this.curEleIndex].child[this.curChildIndex].commonStyle : { h: 0, w: 0 };
let rs = { backgroundColor: '', backgroundImage: '' };
if (this.curEleIndex || this.curEleIndex === 0) {
rs = cloneDeep({ ...rs, ...this.pageData.elements[this.curEleIndex].commonStyle });
if (this.curChildIndex || this.curChildIndex === 0) {
rs = cloneDeep({ ...rs, ...this.pageData.elements[this.curEleIndex].child[this.curChildIndex].commonStyle });
}
}
// console.log('commonStyle', rs);
return rs;
}
get hasGroup() {
return this.curElement?.schame?.some(v => v.title);
}
get curFormKey() {
const keys = [];
this.curElement.schame?.forEach(schame => {
if (schame.children) {
schame.children.forEach(child => {
keys.push(child.key);
});
} else {
keys.push(schame.key);
}
});
return keys;
}
get eleName() {
let result = '';
if (!this.curChildIndex && this.curChildIndex !== 0) {
result = this.pageData?.elements[this.curEleIndex]?.name ?? '';
}
return result;
}
// 监听curElement变化, 更新form
@Watch('curElement', { immediate: true, deep: true })
onElementChange(newVal) {
// curEleIndex选中时commonStyle为空, curChildIndex选中时commonStyle有值且不可设置背景
newVal?.schame?.forEach(schame => {
this.$set(this.form, schame.key, newVal.props[schame.key]);
if (schame.children) {
schame.children.forEach(child => {
this.$set(this.form, child.key, newVal.props[child.key]);
});
} else {
this.$set(this.form, schame.key, newVal.props[schame.key]);
}
});
console.log('curElement', newVal, this.form);
}
// 监听form变化, 更新pageData
@Watch('form', { immediate: true, deep: true })
onFormChange(newVal) {
this.$emit('modProps', this.form);
if (!Object.keys(newVal).length) { return; }
const params = pick(this.form, this.curFormKey);
console.log('form', newVal, params);
this.$nextTick(() => this.adjustHeight());
this.$emit('modProps', params, 'component');
}
get eleName() {
let result = '';
if (!this.curChildIndex && this.curChildIndex !== 0) {
result = this.pageData?.elements[this.curEleIndex]?.name ?? '';
updatePoint(value, key) {
// console.log('updatePoint', this.point);
const elements = this.pageData.elements[this.curEleIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, ...this.point } } });
}
updateStyle(value, key) {
console.log('updateCommonStyle', this.commonStyle);
const elements = this.pageData.elements[this.curEleIndex];
// this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, commonStyle: { ...elements.commonStyle, ...this.commonStyle } } });
this.updateCommonStyle({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: this.commonStyle });
}
getDataFromProps() {
if (this.curEleIndex || this.curEleIndex === 0) {
this.point = this.pageData.elements[this.curEleIndex]?.point || this.point;
this.commonStyle = Object.assign({}, initialCommonStyle, this.pageData.elements[this.curEleIndex].commonStyle);
if (this.curChildIndex || this.curChildIndex === 0) {
this.commonStyle = Object.assign({}, this.pageData.elements[this.curEleIndex].child[this.curChildIndex].commonStyle);
}
}
return result;
}
resizedEvent(h, w, responsive) {
const elements = this.pageData.elements[this.curEleIndex];
if (responsive) {
resizeDiv(this.form.backgroundImage, 667, 375, (imgHeight) => {
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: imgHeight ?? elements.point.h, responsive: true } } });
});
if (this.form.backgroundImage) {
resizeDiv(this.form.backgroundImage, 667, 375, (imgHeight) => {
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: imgHeight ?? elements.point.h, responsive: true } } });
});
} else if (elements.id) {
this.adjustHeight();
}
} else {
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: h ?? elements.point.h } } });
}
......@@ -97,32 +172,4 @@ export default class DynamicForm extends Mixins(ContextMenu) {
}
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...elements, commonStyle: { ...elements.commonStyle, left, top } } });
}
getComponent(type) {
let result = 'Input';
switch (type) {
case 'text':
result = 'Input';
break;
case 'select':
result = 'BaseSelect';
break;
case 'checkbox' :
result = 'Checkbox';
break;
case 'ColorSelector' :
result = 'ColorSelector';
break;
case 'textarea' :
result = 'Textarea';
break;
case 'number' :
result = 'Number';
break;
case 'Upload':
result = 'Upload';
break;
}
return result;
}
}
\ No newline at end of file
......@@ -2,16 +2,26 @@
<div class="dynamic-form">
<h2>{{curElement.title}}</h2>
<template>
<h4>组件属性</h4>
<Form class="dynamic-form-component" :label-width="80" :model="form">
<FormItem :label="item.name" :key="index" v-for="(item, index) in curElement.schame">
<component :is="getComponent(item.type)" :options="item.options" v-model="form[item.key]" />
</FormItem>
<h3 v-if="!hasGroup">组件属性</h3>
<template v-for="(item, index) in curElement.schame">
<div v-if="item.title">
<h3>{{ item.title }}</h3>
<FormItem :label="child.name" :key="child.key" v-for="child in item.children">
<component :is="getComponent(child.type)" :options="child.options" :formControl="child.formControl" v-model="form[child.key]" />
</FormItem>
</div>
<!-- <component v-else-if="item.formControl" :is="getComponent(item.type)" :options="item.options" :formControl="item.formControl" :name="item.name" v-model="form[item.key]" /> -->
<FormItem class="Df-component-formitem" v-else :label="item.name" >
<component :is="getComponent(item.type)" :options="item.options" :formControl="item.formControl" v-model="form[item.key]" />
</FormItem>
</template>
</Form>
</template>
<template>
<h4>基础样式</h4>
<!-- <h3>基础样式</h3> -->
<Form class="dynamic-form-basic" :label-width="80">
<h3>基础样式</h3>
<template v-if="curChildIndex || curChildIndex === 0">
<FormItem label="定位">
<Tooltip placement="top" content="上对齐">
......@@ -34,8 +44,8 @@
</Tooltip>
</FormItem>
<FormItem label="位置">
<InputNumber class="Df-basic-inputnumber" v-model="commonStyle.left"></InputNumber>
<InputNumber v-model="commonStyle.top"></InputNumber>
<InputNumber class="Df-basic-inputnumber" v-model="commonStyle.left" @on-change="updateStyle($event, 'left')"></InputNumber>
<InputNumber v-model="commonStyle.top" @on-change="updateStyle($event, 'top')"></InputNumber>
</FormItem>
<FormItem label="尺寸">
<Tooltip placement="top" content="全屏">
......@@ -49,16 +59,16 @@
</Tooltip>
</FormItem>
<FormItem label="宽高">
<InputNumber class="Df-basic-inputnumber" :max="375" :min="0" v-model="commonStyle.width"></InputNumber>
<InputNumber :max="667" :min="0" v-model="commonStyle.height"></InputNumber>
<InputNumber class="Df-basic-inputnumber" :max="375" :min="0" v-model="commonStyle.width" @on-change="updateStyle($event, 'width')"></InputNumber>
<InputNumber :max="667" :min="0" v-model="commonStyle.height" @on-change="updateStyle($event, 'height')"></InputNumber>
</FormItem>
</template>
<template v-if="(curEleIndex || curEleIndex === 0) && !curChildIndex && curChildIndex !== 0">
<FormItem label="尺寸">
<FormItem label="容器尺寸">
<Tooltip placement="top" content="全屏">
<Button type="ghost" icon="arrow-resize" @click="resizedEvent(667, 375)"></Button>
</Tooltip>
<Tooltip placement="top" content="根据背景图片自动调整宽高">
<Tooltip placement="top" content="根据背景图片或组件自动调整宽高">
<Button type="ghost" icon="image" @click="resizedEvent(667, 375, true)" ></Button>
</Tooltip>
<Tooltip placement="top" content="宽100%">
......@@ -68,11 +78,17 @@
<Button type="ghost" icon="arrow-swap" @click="resizedEvent(667, null)" ></Button>
</Tooltip>
</FormItem>
<FormItem label="宽高">
<InputNumber class="Df-basic-inputnumber" :max="375" :min="0" v-model="point.w"></InputNumber>
<InputNumber :max="667" :min="0" v-model="point.h"></InputNumber>
<FormItem label="容器宽高">
<InputNumber class="Df-basic-inputnumber" :max="375" :min="0" v-model="point.w" @on-change="updatePoint($event, 'w')"></InputNumber>
<InputNumber :max="667" :min="0" v-model="point.h" @on-change="updatePoint($event, 'h')"></InputNumber>
</FormItem>
</template>
<FormItem label="背景图片">
<Upload v-model="commonStyle.backgroundImage" @change="updateStyle($event, 'backgroundImage')"></Upload>
</FormItem>
<FormItem label="背景颜色">
<ColorSelector v-model="commonStyle.backgroundColor" @input="updateStyle($event, 'backgroundColor')"></ColorSelector>
</FormItem>
</Form>
</template>
</div>
......@@ -80,17 +96,21 @@
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.dynamic-form {
padding: 0 15px 16px;
padding: 0 0 16px;
background: #fff;
h2 {
padding: 17px 0;
border-bottom: 8px solid #F5F6FA;
text-align: center;
}
h4 {
h3 {
padding: 10px 0;
margin-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
&-component {
padding: 0 20px;
border-bottom: 8px solid #F5F6FA;
}
&-basic {
padding: 0 20px;
......
import {Component, Mixins, Prop, Watch} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash';
import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import Upload from '../DynamicForm/component/Upload/index.vue';
import ColorSelector from '../DynamicForm/component/ColorSelector/index.vue';
import BaseSelect from '../DynamicForm/component/BaseSelect/index.vue';
import Textarea from '../DynamicForm/component/Textarea/index.vue';
import Number from '../DynamicForm/component/Number/index.vue';
import { resizeDiv, getStyle } from '@/service/utils.service';
@Component({ components: { Upload, ColorSelector, BaseSelect, Textarea, Number }, name: 'DynamicForm' })
export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
@Getter('pageData') pageData;
title: string = '页面背景';
form: object = {};
schame: object[] = [
{
key: 'backgroundImage',
name: '背景图片',
type: 'Upload'
},
{
key: 'backgroundColor',
name: '背景颜色',
type: 'ColorSelector'
},
];
@Watch('pageData', { immediate: true, deep: true })
onElementChange(newVal) {
const keys = Object.keys(this.schame);
// this.form = {};
this.schame.forEach(schame => {
this.$set(this.form, schame.key, this.pageData?.commonStyle[schame.key]);
});
}
@Watch('form', { immediate: true, deep: true })
onFormChange(newVal) {
this.$emit('modProps', this.form, 'page');
}
// resizedChildEvent(type) {
// this.$emit('resizedChildEvent', type);
// // const containerEle = this.$refs.freedomContainer[this.curEleIndex];
// }
}
\ No newline at end of file
<template>
<div class="dynamic-form">
<h2>{{title}}</h2>
<Form class="dynamic-form-component" :label-width="80" :model="form">
<h3>组件属性</h3>
<template v-for="(item, index) in schame">
<FormItem class="Df-component-formitem" :label="item.name" >
<component :is="item.type" :options="item.options" v-model="form[item.key]" />
</FormItem>
</template>
</Form>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.dynamic-form {
background: #fff;
h2 {
padding: 17px 0;
border-bottom: 8px solid #F5F6FA;
text-align: center;
}
h3 {
padding: 10px 0;
margin-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
&-component {
padding: 0 20px;
border-bottom: 8px solid #F5F6FA;
}
&-basic {
padding: 0 20px;
.Df-basic-inputnumber {
margin-right: 10px;
}
}
/deep/ .ivu-form-item-label {
font-size: 14px;
}
/deep/ .ivu-input-number {
width: 60px;
}
/deep/ .ivu-form-item-content {
.ivu-tooltip {
margin-right: 6px;
.ivu-btn-ghost {
padding: 2px 6px;
display: flex;
align-items: center;
justify-content: center;
.ivu-icon {
font-size: 20px;
}
}
&:last-child {
.ivu-icon {
transform: rotate(90deg);
}
}
}
}
}
</style>
\ No newline at end of file
import { Component, Prop, Mixins, Watch } from 'vue-property-decorator';
import LoginForm from '@/lib/Form/index.vue';
// import LoginForm from '@/lib/Form/index.vue';
import DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import Marquee from '@/lib/Marquee/index.vue';
import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import { cloneDeep, pick, omit, throttle } from 'lodash';
import { Action, Mutation, State } from 'vuex-class';
import { Action, Mutation, State, Getter } from 'vuex-class';
import { convertPointStyle, getStyle } from '@/service/utils.service';
@Component({ components: { LoginForm, DownloadGuide }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(ContextMenu) {
@Component({ components: { DownloadGuide, Marquee }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(ContextMenuMixin, TransformStyleMixin) {
@Action('setDragable') setDragable;
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex;
@Getter('pageData') pageData;
@Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop({type: Number, default: 0}) containerIndex;
@Prop({ type: Boolean, default: false }) showHeader;
@Prop(String) backgroundImage;
@Prop({ type: String, default: '#fff' }) backgroundColor;
dots: object = {};
get styles() {
return {
// background: `url(${this.backgroundImage}) no-repeat 0 0 / cover`,
// backgroundColor: this.backgroundColor
};
}
mousedown(childIndex, event) {
this.setDragable(false);
const childItem = cloneDeep(this.childItem);
......@@ -42,37 +54,45 @@ export default class FreedomContainer extends Mixins(ContextMenu) {
document.addEventListener('mouseup', up, true);
}
transformStyle(styleObj, element) {
let style = {};
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(2)}rem`;
} else {
style[key] = styleObj[key]?.includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(2)}rem` : styleObj[key];
}
if (key === 'backgroundImage') {
style.backgroundImage = `url(${style.backgroundImage})`;
}
}
const transformFun = element === 'container' ? pick : omit;
style = transformFun(style, ['position', 'top', 'left']);
return style;
}
handleElementClick(curEleIndex, curChildIndex) {
this.$emit('handleElementClick', curEleIndex, curChildIndex);
}
get commonStyle() {
if ((this.curEleIndex || this.curEleIndex === 0 ) && (this.curChildIndex || this.curChildIndex === 0)) {
return this.pageData?.elements[this.curEleIndex]?.child[this.curChildIndex]?.commonStyle;
}
return null;
}
@Watch('curChildIndex')
onIndexChange(newVal) {
this.dots = {};
if (newVal || newVal === 0) {
// console.log('onIndexChange');
if (this.curChildIndex || this.curChildIndex === 0) {
this.setPointStyle();
}
}
@Watch('commonStyle')
onCommonStyleChange(newVal) {
// console.log('onCommonStyleChange', newVal);
if (newVal) {
this.setDotsStyle();
}
}
// 获取point计算后样式
setPointStyle() {
this.$nextTick(() => {
const [height, width] = this.getHW(this.curChildIndex);
const childEle = this.childItem.child[this.curChildIndex];
this.setDotsStyle();
this.updateCommonStyle({ containerIndex: this.containerIndex, childIndex: this.curChildIndex, data: { ...childEle.commonStyle, height: +height, width: +width }});
});
}
setDotsStyle() {
this.$nextTick(() => {
const points = ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b'];
const [height, width] = this.getHW(this.curChildIndex);
......@@ -80,8 +100,6 @@ export default class FreedomContainer extends Mixins(ContextMenu) {
pre[cur] = convertPointStyle(cur, {height, width});
return pre;
}, {});
const childEle = this.childItem.child[this.curChildIndex];
this.updateCommonStyle({ containerIndex: this.containerIndex, childIndex: this.curChildIndex, data: { ...childEle.commonStyle, height: +height, width: +width }});
});
}
......
<template>
<div class="freedom" :ref="`freedomContainer${containerIndex}`" @click.stop="handleElementClick(containerIndex)">
<div class="freedom-body" :style="{background: `url(${backgroundImage}) no-repeat 0 0 / cover`}">
<div v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle, 'container')" :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" :key="index" @click.stop="handleElementClick(containerIndex, index)" @mousedown.stop="mousedown(index, $event)" @contextmenu.prevent.stop="show($event, containerIndex, index)">
<div class="freedom-body" :style="styles" :data-index="containerIndex">
<div v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle, 'container')" :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" :key="index" @click.stop="handleElementClick(containerIndex, index)" @mousedown.stop.prevent="mousedown(index, $event)" @contextmenu.prevent.stop="show($event, containerIndex, index)">
<component ref="childComponent" :style="transformStyle(item.commonStyle, 'component')" :is="item.name" v-bind="item.props"></component>
<div class="freedom-body-dot"
v-for="(style, key) in dots[index]"
......
......@@ -7,7 +7,8 @@
<Select v-model="searchForm[item.key]" class="comWidth" filterable v-else-if="item.type === 'select'" clearable>
<Option :value="isNum(value, item.number)" :key="value" v-for="(label,value) in item.option" :label="label" />
</Select>
<DatePicker v-else class="timeWidth" type="datetimerange" placeholder="" v-model="searchForm[item.key]"></DatePicker>
<DatePicker v-else-if="item.type === 'date'" class="timeWidth" type="datetimerange" placeholder="" v-model="searchForm[item.key]"></DatePicker>
<treeselect v-else-if="item.type === 'treeSelect'" v-model.number="searchForm[item.key]" :multiple="false" :options="item.option" placeholder="请选择" :normalizer="normalizer" style="width:180px" />
</FormItem>
<FormItem class="btnGroupStyle">
<div>
......@@ -18,14 +19,14 @@
</Form>
</div>
<div class="tableGroupStyle">
<div class="toolBarStyle">
<div v-if="!hideAdd" class="toolBarStyle">
<h3>查询数据</h3>
<div>
<Button type="primary" class="btnStyle" @click="newEvent()">新增</Button>
<slot></slot>
</div>
</div>
<Table :columns="renderColumns" :data="tableData" class="tableStyle"></Table>
<Table :height="height" @on-selection-change="selectionChange" :columns="renderColumns" :data="tableData" class="tableStyle"></Table>
<Page
:total="total"
v-if="total > 0"
......@@ -41,14 +42,25 @@
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect';
import '@riophae/vue-treeselect/dist/vue-treeselect.css';
import { cloneDeep } from 'lodash';
export default {
components: { Treeselect },
props: {
columns: Array,
columns: {
type: Array,
default: () => ({})
},
hideAdd: Boolean,
request: Function,
toolBar: Function,
height: Number,
},
data() {
return {
cols: this.transformData(),
tableData: [],
total: 0,
searchForm: {
......@@ -70,7 +82,7 @@ export default {
},
computed: {
renderColumns: function() {
return this.handleColumns(this.columns)
return this.handleColumns(this.cols)
}
},
methods: {
......@@ -105,12 +117,16 @@ export default {
this.searchForm.pageSize = 10;
this.searchForm.pageNo = 1;
},
refresh() {
this.query();
},
newEvent(e) {
this.$emit('newBtnClick', e);
},
handleColumns(col=[]) {
const data = []
col.forEach(item => {
this.searchCondition = [];
col.forEach(item => {
item.align = item.align || 'center';
if (item.valueEnum&&!item.render) {
item.render = (h, params) => {
......@@ -128,6 +144,35 @@ export default {
},
isNum(value, number) {
return number ? +value : value;
},
selectionChange(selection) {
this.$emit('on-selection-change', selection);
},
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children;
}
return {
id: node.id || null,
label: node.label,
children: node.children,
};
},
transformData() {
return cloneDeep(this.columns).map(item => {
// 处理valueEnum为异步函数的情况
if (item.valueEnum && typeof item.valueEnum === 'function') {
item.valueEnum().then(v => {
this.cols.forEach(column => {
if (column.key === item.key) {
column.valueEnum = v;
}
})
});
item.valueEnum = [];
}
return item;
})
}
},
mounted() {
......@@ -148,16 +193,14 @@ export default {
background-color: #ffffff;
padding: @padding;
min-height: 70px;
font-size: 0;
.labelStyle {
font-weight: bold !important;
display: inline-block;
}
// & @{deep} .ivu-form-item .ivu-form-item-label {
// .labelStyle;
// }
// & @{deep} .ivu-form-item-content {
// .labelStyle;
// }
/deep/ .ivu-form {
margin-bottom: -24px;
}
.required:before {
content: '* ';
color: #ed3f14;
......
import {Component, Vue } from 'vue-property-decorator';
import { Mutation } from 'vuex-class';
import { getStyle } from '@/service/utils.service';
@Component({ name: 'ContextMenu' })
export class ContextMenu extends Vue {
@Component({ name: 'ContextMenuMixin' })
export default class ContextMenuMixin extends Vue {
@Mutation('COPY_OR_DELETE_PAGE_INFO') updatePageData;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@Mutation('UPDATE_COMMON_STYLE') updateCommonStyle;
......@@ -31,4 +32,13 @@ export class ContextMenu extends Vue {
minWidth: 100
});
}
adjustHeight() {
if (!this.curEleIndex && this.curEleIndex !== 0) { return; }
const elements = this.pageData.elements[this.curEleIndex];
const component = document.getElementById(elements.id);
const height = component ? getStyle(component, 'height') : 0;
console.log('adjustHeight', height);
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, h: +height || elements.point.h } } });
}
}
\ No newline at end of file
import {Component, Vue } from 'vue-property-decorator';
import { Mutation, Getter } from 'vuex-class';
import { cloneDeep } from 'lodash';
@Component({ name: 'GoodsTabsMixin' })
export default class GoodsTabsMixin extends Vue {
@Getter('pageData') pageData;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@Mutation('COPY_OR_DELETE_PAGE_INFO') updatePageData;
handleGoodsTabs() {
const goodsTabs = {};
const pageData = cloneDeep(this.pageData);
pageData.elements.forEach((element, idx) => {
if (element.name === 'goods-tabs' && element?.props?.list.length) {
const childs = [];
const childIndexs = [];
element.props.list.forEach((data, index) => {
const index = pageData.elements.findIndex(v => v.id === data.componentId);
if (index !== -1) {
childIndexs.push(index);
childs.push(pageData.elements[index]);
}
});
goodsTabs[element.id] = { idx, childs, childIndexs };
}
});
Object.keys(goodsTabs).forEach(key => {
const { idx, childIndexs, childs } = goodsTabs[key];
pageData.elements[idx].child = childs;
// 逆向循环
for (let i = childIndexs.length - 1; i >= 0; i--) {
pageData.elements.splice(childIndexs[i], 1);
}
});
return pageData;
}
parseGoodsTabs() {
const goodsTabs = {};
this.pageData.elements.forEach((element, idx) => {
if (element.name === 'goods-tabs' && element.child.length) {
goodsTabs[idx] = cloneDeep(element.child);
// 逆向循环
for (let i = element.child.length - 1; i >= 0; i--) {
this.updatePageData({ type: 'delete', containerIndex: idx, childIndex: i });
}
}
});
console.log('parseGoodsTabs', goodsTabs);
Object.keys(goodsTabs).forEach(key => {
goodsTabs[key].forEach((child, index) => {
// console.log('parseGoodsTabs', { containerIndex: +key + 1 + index, data: child });
this.updatePageInfo({ containerIndex: +key + 1 + index, data: child });
});
});
}
}
\ No newline at end of file
import { kebabCase, maxBy } from 'lodash';
import { Getter, Action, State, Mutation } from 'vuex-class';
import { Mixins, Component, Watch } from 'vue-property-decorator';
import { Mixins, Component, Watch, Provide } from 'vue-property-decorator';
import DynamicComponent from '@editor/component/DynamicComponent/index.vue';
import VueGridLayout from 'vue-grid-layout';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import DynamicForm from '../../component/DynamicForm/index.vue';
import LoginForm from '@/lib/Form/index.vue';
import DynamicPageForm from '../../component/DynamicPageForm/index.vue';
// import LoginForm from '@/lib/Form/index.vue';
import DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import GoodsTabs from '@/lib/GoodsTabs/index.vue';
import Goods from '@/lib/Goods/index.vue';
import Advertisement from '@/lib/Advertisement/index.vue';
import Coupon from '@/lib/Coupon/index.vue';
import Placeholder from '@/lib/Placeholder/index.vue';
import GuideCube from '@/lib/GuideCube/index.vue';
import Marquee from '@/lib/Marquee/index.vue';
import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import GoodsTabsMixin from '@editor/mixins/goodsTabs.mixin';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import BasicPageForm from '@editor/component/BasicPageForm/index.vue';
import { basicComponents, businessComponents } from '@/lib/config';
import config from '@/config/index';
import localStorage from '@/service/localStorage.service';
import EventBus from '@/service/eventBus.service';
@Component({components: { DynamicComponent, FreedomContainer, DynamicForm, GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem, LoginForm, DownloadGuide, BasicPageForm }, name: 'DashBoard'})
export default class DashBoard extends Mixins(ContextMenu) {
GridItem: VueGridLayout.GridItem, DownloadGuide, BasicPageForm, DynamicPageForm, GoodsTabs, Goods, Coupon, Advertisement, Placeholder, GuideCube, Marquee }, name: 'DashBoard'})
export default class DashBoard extends Mixins(ContextMenuMixin, GoodsTabsMixin, TransformStyleMixin) {
@Mutation('ADD_ELEMENTS') addElements;
@Mutation('SET_CUR_ELE_INDEX') setCurEleIndex;
@Mutation('SET_CUR_CHILD_INDEX') setCurChildIndex;
@Mutation('SET_PAGE_INFO') setPageInfo;
@Mutation('SET_PAGE_DATA') setPageData;
@Mutation('UPDATE_PAGE_STYLE') setPageStyle;
@Action('resetPageData') resetPageData;
@Action('savePageData') savePageData;
@Action('getPageDate') getPageDate;
......@@ -35,6 +47,8 @@ export default class DashBoard extends Mixins(ContextMenu) {
@State(state => state.editor.curChildIndex) curChildIndex;
@State(state => state.editor.templateList) templateList;
@Provide() editor = this;
activeName: string = '1';
isCollapsed: boolean = true;
isDragIn: boolean = false;
......@@ -48,10 +62,11 @@ export default class DashBoard extends Mixins(ContextMenu) {
console.log('env', process.env);
this.resetPageData();
if (pageId) {
this.getPageDate({ pageId });
await this.getPageDate({ pageId });
} else if (templateId) {
this.setTemplateInfo({ pageId: templateId });
await this.setTemplateInfo({ pageId: templateId });
}
this.parseGoodsTabs();
this.getTemplateList();
}
......@@ -70,10 +85,11 @@ export default class DashBoard extends Mixins(ContextMenu) {
this.showSubmitPopup = true;
} else {
this.pageData.elements.sort((a, b) => a.point.y - b.point.y);
const pageData = this.handleGoodsTabs();
const { pageName, pageDescribe, coverImage, isPublish, isTemplate } = pageConfig;
const pageInfo = { page: JSON.stringify(this.pageData), author: user?.account, isPublish, pageName, pageDescribe, coverImage, isTemplate };
const pageInfo = { page: JSON.stringify(pageData), author: user?.account, isPublish, pageName, pageDescribe, coverImage, isTemplate };
if (+this.pageId) { pageInfo.id = this.pageId; }
await this.savePageData(pageInfo);
await this.savePageData({ pageInfo, pageData: this.pageData });
this.showSubmitPopup = false;
if (type === 'preview') {
window.open(`${config.apiHost}activity/${this.pageId}`);
......@@ -93,10 +109,6 @@ export default class DashBoard extends Mixins(ContextMenu) {
}
toggle(val) {
if (val) {
this.setCurEleIndex(null);
this.setCurChildIndex(null);
}
this.isCollapsed = val;
}
......@@ -126,15 +138,19 @@ export default class DashBoard extends Mixins(ContextMenu) {
}
}
modProps(props) {
let currentEle = {};
if (this.curEleIndex !== null) {
if (this.curChildIndex !== null) {
currentEle = this.pageData.elements[this.curEleIndex].child[this.curChildIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...currentEle, props: { ...currentEle.props, ...props } } });
} else {
currentEle = this.pageData.elements[this.curEleIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...this.pageData.elements[this.curEleIndex], props: { ...currentEle.props, ...props } } });
modProps(props, ele) {
if (ele === 'page') {
this.setPageStyle({ data: props });
} else if (ele === 'component') {
let currentEle = {};
if (this.curEleIndex !== null) {
if (this.curChildIndex !== null) {
currentEle = this.pageData.elements[this.curEleIndex].child[this.curChildIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...currentEle, props: { ...currentEle.props, ...props } } });
} else {
currentEle = this.pageData.elements[this.curEleIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...this.pageData.elements[this.curEleIndex], props: { ...currentEle.props, ...props } } });
}
}
}
}
......@@ -150,7 +166,7 @@ export default class DashBoard extends Mixins(ContextMenu) {
this.setPageData(JSON.parse(data.template));
this.handleElementClick(null, null);
// freedom
} else if (event.target.classList.contains('freedom')) {
} else if (event.target.classList.contains('freedom-body')) {
const { y: curY } = this.pageData.elements[event.target.dataset.index].point;
const scrollTop = this.layout.reduce((pre, cur) => {
if (cur.y < curY) {
......@@ -162,18 +178,24 @@ export default class DashBoard extends Mixins(ContextMenu) {
this.handleElementClick(+event.target.dataset.index, this.pageData.elements[event.target.dataset.index].child.length - 1);
// component
} else {
const { i } = maxBy(this.layout, 'i') || {};
const y = Math.floor(top / this.rowHeight);
console.log('drops', i);
this.addElements({ data: {...data, point: { ...data.point, i: i || i === 0 ? String(+i + 1) : '0', y } });
this.addElements({ data: {...data, point: { ...data.point, y } });
this.handleElementClick(this.pageData.elements.length - 1, null);
}
// 调整组件高度
this.$nextTick(() => this.adjustHeight());
}
resizedEvent(i, h, w) {
const index = this.pageData.elements.findIndex(ele => ele.point.i === i);
this.updatePageInfo({ containerIndex: index, data: { ...this.pageData.elements[index], point: { ...this.pageData.elements[index].point, w, h } } });
}
movedEvent(i, newX, newY) {
EventBus.$emit('component-moved');
console.log('MOVED i=' + i + ', X=' + newX + ', Y=' + newY);
}
/**
* 调整自由容器子元素宽高及边框原点位置
* @param {[type]} type 尺寸类型
......
......@@ -60,6 +60,8 @@
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
:style="transformStyle(pageData.commonStyle)"
@click.native.stop="toggle(false)"
>
<grid-item @click.native.stop="handleElementClick(index, null)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
......@@ -70,8 +72,9 @@
:key="item.point.i"
@contextmenu.native.prevent="show($event, index)"
@resized="resizedEvent"
@moved="movedEvent"
:class="{'Dcmcp-item_selected': curEleIndex === index && curChildIndex === null}">
<component ref="container" class="Dcmcp-item-com" @handleElementClick="handleElementClick" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
<component ref="container" :style="transformStyle(item.commonStyle)" :id="item.id" class="Dcmcp-item-com" @handleElementClick="handleElementClick" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
</div>
......@@ -82,7 +85,9 @@
<dynamic-form @modProps="modProps" @resizedChildEvent="resizedChildEvent"></dynamic-form>
</TabPane>
<TabPane label="事件">事件</TabPane>
<TabPane label="页面设置">页面设置</TabPane>
<TabPane label="页面设置">
<dynamic-page-form @modProps="modProps" @resizedChildEvent="resizedChildEvent"></dynamic-page-form>
</TabPane>
</Tabs>
</Col>
</Row>
......@@ -91,7 +96,7 @@
<BasicPageForm v-model="showSubmitPopup" @submit="save" />
</Row>
</template>
<style lang="less">
<style lang="less" scoped>
.tabs-position() {
/deep/ .ivu-tabs-nav-scroll {
display: flex;
......@@ -152,16 +157,16 @@
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
min-height: 667px;
.vue-grid-item {
text-align: center;
background: #fff;
overflow: hidden;
&:hover {
border: 1px dashed #0c0c0c !important;
}
&>*:first-child {
height: 100%;
}
// &>*:first-child {
// height: 100%;
// }
.vue-resizable-handle {
z-index: 10000;
}
......@@ -189,10 +194,18 @@
height: 100%;
min-width: 320px;
.tabs-position();
background: #F5F6FA;
/deep/ .ivu-tabs-bar {
background: #fff;
margin-bottom: 0;
}
/deep/ .ivu-tabs-content {
height: calc(100% - 48px);
overflow-y: scroll;
overflow-x: hidden;
.ivu-tabs-tabpane {
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
}
}
}
......
import { Component, Vue } from 'vue-property-decorator';
import { pick, omit } from 'lodash';
import { transformStyle } from '@/service/utils.service';
@Component({ name: 'TransformStyleMixin' })
export default class TransformStyleMixin extends Vue {
transformStyle(styleObj, element) {
// console.log('transformStyle', styleObj, element);
let style = {};
if (!styleObj) { return style; }
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(2)}rem`;
} else {
style[key] = styleObj[key]?.includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(2)}rem` : styleObj[key];
}
if (key === 'backgroundImage') {
style.backgroundImage = `url(${style.backgroundImage})`;
}
}
const transformFun = element === 'container' ? pick : omit;
style = transformFun(style, ['position', 'top', 'left']);
return style;
}
}
\ No newline at end of file
import api from '@/api/editor.api';
import { Module, GetterTree, ActionTree, MutationTree } from 'vuex';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import {
SET_PAGE_INFO,
SET_DRAGABLE,
COPY_OR_DELETE_PAGE_INFO,
UPDATE_PAGE_INFO,
ADD_ELEMENTS,
DEL_ELEMENTS,
SET_CUR_ELE_INDEX,
SET_CUR_CHILD_INDEX,
RESET_PAGE_DATA,
SET_TEMPLATE_LIST,
SET_PAGE_DATA,
UPDATE_COMMON_STYLE,
UPDATE_PAGE_STYLE,
} from './type';
import RootState from '../../state';
......@@ -35,14 +36,15 @@ export default class EditorModule implements Module<EditorState, RootState> {
};
actions: ActionTree<EditorState, RootState> = {
async savePageData({ commit }, condition) {
if (condition.id) {
await api.updatePage(condition);
commit(SET_PAGE_INFO, { ...condition, page: JSON.parse(condition.page as string) });
// pageInfo: 处理过的页面数据 - activity, pageData: 未处理的数据 - editor
async savePageData({ commit }, { pageInfo, pageData}) {
if (pageInfo.id) {
await api.updatePage(pageInfo);
commit(SET_PAGE_INFO, { ...pageInfo, page: pageData });
} else {
const res = await api.savePage(condition);
const res = await api.savePage(pageInfo);
const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) });
commit(SET_PAGE_INFO, { ...rest, page: pageData });
}
},
async getPageDate({ commit }, condition) {
......@@ -125,11 +127,14 @@ export default class EditorModule implements Module<EditorState, RootState> {
[UPDATE_COMMON_STYLE](state, {containerIndex, childIndex, data}) {
const page = (state.pageInfo.page as Page).elements;
if (childIndex || childIndex === 0) {
page[containerIndex].child[childIndex].commonStyle = data;
Vue.set(page[containerIndex].child[childIndex], 'commonStyle', data);
} else {
page[containerIndex].commonStyle = data;
Vue.set(page[containerIndex], 'commonStyle', data);
}
},
[UPDATE_PAGE_STYLE](state, { data }) {
(state.pageInfo.page as Page).commonStyle = data;
},
[ADD_ELEMENTS](state, { containerIndex, data }) {
const page = (state.pageInfo.page as Page).elements;
if (containerIndex || containerIndex === 0) {
......@@ -138,10 +143,6 @@ export default class EditorModule implements Module<EditorState, RootState> {
page.push(data);
}
},
[DEL_ELEMENTS](state, { containerIndex }) {
const page = (state.pageInfo.page as Page).elements;
page.splice(containerIndex, 1);
},
};
constructor(initState: EditorState = cloneDeep(defaultState)) {
......
......@@ -34,6 +34,7 @@ export interface PageElement {
}
export interface Page {
commonStyle: CommonStyle;
elements: PageElement[];
}
......@@ -57,6 +58,10 @@ export const defaultState = {
coverImage: 'http://activitystatic.q-gp.com/low_code.jpg',
isPublish: false,
page: {
commonStyle: {
backgroundColor: '#f7f8fa',
backgroundImage: ''
},
elements: [],
}
},
......
......@@ -4,10 +4,10 @@ export const SET_DRAGABLE = 'SET_DRAGABLE';
export const COPY_OR_DELETE_PAGE_INFO = 'COPY_OR_DELETE_PAGE_INFO';
export const UPDATE_PAGE_INFO = 'UPDATE_PAGE_INFO';
export const ADD_ELEMENTS = 'ADD_ELEMENTS';
export const DEL_ELEMENTS = 'DEL_ELEMENTS';
export const SET_CUR_ELE_INDEX = 'SET_CUR_ELE_INDEX';
export const SET_CUR_CHILD_INDEX = 'SET_CUR_CHILD_INDEX';
export const RESET_PAGE_DATA = 'RESET_PAGE_DATA';
export const SET_TEMPLATE_LIST = 'SET_TEMPLATE_LIST';
export const SET_PAGE_DATA = 'SET_PAGE_DATA';
export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE';
\ No newline at end of file
export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE';
export const UPDATE_PAGE_STYLE = 'UPDATE_PAGE_STYLE';
\ No newline at end of file
import Vue from 'vue';
export default new Vue();
\ No newline at end of file
......@@ -53,12 +53,14 @@ const instance = axios.create();
// 请求拦截器
instance.interceptors.request.use(
config => {
// op-api识别API使用
config.headers['X-Requested-With'] = 'XMLHttpRequest';
// beforeRequest();
// 发起请求时,取消掉当前正在进行的相同请求
if (pending[config.url as string]) {
pending[config.url as string]('取消重复请求');
}
config.cancelToken = new CancelToken(c => (pending[config.url as string] = c));
config.cancelToken = new CancelToken(c => (pending[(config.url + JSON.stringify(config.data)) as string] = c));
// 添加token
const token = localStorage.get('token');
if (token) {
......@@ -118,6 +120,14 @@ instance.interceptors.response.use(
return axios.request(err.config);
}
if (err.response) {
switch (err.response.status) {
case 401:
window.location.href = `${window.location.origin}/editor/login`;
return;
}
}
// 错误提示
let message = '';
if (err.response) {
......
......@@ -21,9 +21,12 @@ import {
Form,
Sticky,
Tab,
Tabs
Tabs,
Notify,
Swipe,
SwipeItem
} from '@qg/cherry-ui';
// import { KaLoginForm } from '@qg/citrus-ui';
import { KaLoginForm } from '@qg/citrus-ui';
Vue.use(Button);
Vue.use(Image);
......@@ -44,7 +47,11 @@ Vue.use(Form);
// Vue.use(CardList);
// Vue.use(Loading);
// Vue.use(List);
// Vue.use(Tab);
// Vue.use(Tabs);
Vue.use(Tab);
Vue.use(Tabs);
// Vue.use(Swipe);
// Vue.use(SwipeItem);
// Vue.use(KaLoginForm);
Vue.use(KaLoginForm);
Vue.prototype.$notify = Notify;
......@@ -93,3 +93,28 @@ export const getStyle = function(oElement, sName) {
const result = oElement.currentStyle ? oElement.currentStyle[sName] : getComputedStyle(oElement, null)[sName];
return result.includes('px') ? result.slice(0, -2) : result;
};
export const validateType = function(obj) {
const class2type = {};
'Array Date RegExp Object Error'.split(' ').forEach(e => {
class2type[`[object ${e}]`] = e.toLowerCase();
});
if (obj == null) { return String(obj); }
return typeof obj === 'object' ? class2type[Object.prototype.toString.call(obj)] || 'object' : typeof obj;
};
export const transformStyle = function(styleObj = {}) {
// console.log('transformStyle', styleObj);
const style = {};
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(3)}rem`;
} else {
style[key] = styleObj[key].includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(3)}rem` : styleObj[key];
}
if (key === 'backgroundImage' && style.backgroundImage) {
style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
return style;
};
......@@ -25,22 +25,19 @@ module.exports = {
},
nodeExternals: {
whitelist: [ moduleName => {
if (moduleName.includes('cherry-ui') || moduleName.includes('@interactjs')) {
console.log(moduleName);
}
return /cherry-ui/.test(moduleName) || /@interactjs/.test(moduleName);
return /cherry-ui/.test(moduleName) || /citrus-ui/.test(moduleName) || /@interactjs/.test(moduleName);
}]
},
module:{
rules:[
{ babel: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@interactjs')],
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@interactjs'), resolve('node_modules/@qg/citrus-ui')],
exclude: []
}
},
{
vue: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui')],
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@qg/citrus-ui')],
exclude: []
}
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment