Commit 793c3d95 authored by 薛智杰's avatar 薛智杰

Merge branch 'feature/add_v2.0' into 'master'

Feature/add v2.0

See merge request !4
parents 21769f8c 263b5a99
...@@ -23,7 +23,6 @@ app/view/* ...@@ -23,7 +23,6 @@ app/view/*
!app/view/layout.html !app/view/layout.html
!app/view/README.md !app/view/README.md
!app/view/.gitkeep !app/view/.gitkeep
package-lock.json
yarn.lock yarn.lock
*.log *.log
coverage coverage
\ No newline at end of file
# egg-vue-typescript-boilerplate # quantum-blocks
基于 Egg + Vue + Webpack SSR 服务端渲染和 CSR 前端渲染工程骨架项目。 基于 Egg + Vue + Webpack SSR 服务端渲染和 CSR 前端渲染工程项目。
Single Page Application Isomorphic Example for Egg + Vue, Front-End and Node of The Application are Written in TypeScript. Single Page Application Isomorphic Example for Egg + Vue, Front-End and Node of The Application are Written in TypeScript.
......
import { Controller, Context } from 'egg';
export default class ActivityController extends Controller {
public async home(ctx: Context) {
await ctx.render('activity.js', { url: ctx.url });
}
}
\ No newline at end of file
import { Controller, Context } from 'egg'; import { Controller, Context } from 'egg';
import { trim, omitBy } from 'lodash'; import { trim, omitBy } from 'lodash';
import { v1 as uuidv1 } from 'uuid';
export default class EditorController extends Controller { export default class EditorController extends Controller {
...@@ -13,18 +14,28 @@ export default class EditorController extends Controller { ...@@ -13,18 +14,28 @@ export default class EditorController extends Controller {
public async save(ctx: Context) { public async save(ctx: Context) {
const pageInfo = ctx.request.body; const pageInfo = ctx.request.body;
const result = await ctx.model.PageInfo.create(pageInfo); const result = await ctx.model.PageInfo.create({ ...pageInfo, uuid: uuidv1().replace(/-/g, '')});
await ctx.service.redis.set(`pageInfo:${result.dataValues.uuid}`, result.dataValues);
ctx.body = ctx.helper.ok(result); ctx.body = ctx.helper.ok(result);
} }
public async update(ctx: Context) { public async update(ctx: Context) {
const pageInfo = ctx.request.body; const pageInfo = ctx.request.body;
const result = await ctx.model.PageInfo.update(pageInfo, {where: { id: +pageInfo.id }}); if (!pageInfo.uuid) {
pageInfo.uuid = uuidv1().replace(/-/g, '');
}
const result = await ctx.model.PageInfo.update(pageInfo, {where: { uuid: pageInfo.uuid }});
await ctx.service.redis.set(`pageInfo:${pageInfo.uuid}`, pageInfo);
await ctx.service.redis.del(`page:${pageInfo.uuid}`);
ctx.body = ctx.helper.ok(result); ctx.body = ctx.helper.ok(result);
} }
public async get(ctx: Context) { public async get(ctx: Context) {
const pageInfo = await ctx.model.PageInfo.findOne({where: { id: +ctx.params.pageId }}); let pageInfo = await ctx.service.redis.get(`pageInfo:${ctx.params.uuid}`);
if (!pageInfo) {
pageInfo = await ctx.model.PageInfo.findOne({where: { uuid: ctx.params.uuid }});
await ctx.service.redis.set(`pageInfo:${ctx.params.uuid}`, pageInfo);
}
ctx.body = ctx.helper.ok(pageInfo); ctx.body = ctx.helper.ok(pageInfo);
} }
...@@ -37,7 +48,7 @@ export default class EditorController extends Controller { ...@@ -37,7 +48,7 @@ export default class EditorController extends Controller {
pageDescribe: pageDescribe && { like: `%${pageDescribe}%`}, pageDescribe: pageDescribe && { like: `%${pageDescribe}%`},
isPublish, isPublish,
enable: 1 enable: 1
}, v => !trim(v)); }, v => !v);
if (type === 'list') { if (type === 'list') {
where = { ...where, isPublish: 1 }; where = { ...where, isPublish: 1 };
} else if (type === 'my') { } else if (type === 'my') {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull } from 'sequelize-typescript'; import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull } from 'sequelize-typescript';
@Table({ @Table({
modelName: 'page_info', modelName: 'page_config_info',
freezeTableName: true freezeTableName: true
}) })
...@@ -35,6 +35,18 @@ export class PageInfo extends Model<PageInfo> { ...@@ -35,6 +35,18 @@ export class PageInfo extends Model<PageInfo> {
}) })
pageDescribe: string; pageDescribe: string;
@Column({
field: 'page_keywords',
type: DataType.STRING(255)
})
pageKeywords: string;
@Column({
field: 'uuid',
type: DataType.UUID
})
uuid: string;
@Column({ @Column({
type: DataType.INTEGER(1) type: DataType.INTEGER(1)
}) })
......
...@@ -13,10 +13,10 @@ export default (application: Application) => { ...@@ -13,10 +13,10 @@ export default (application: Application) => {
router.post('/editor/update', controller.editor.update); router.post('/editor/update', controller.editor.update);
router.get('/editor/get/list', controller.editor.getList); router.get('/editor/get/list', controller.editor.getList);
router.get('/editor/get/template', controller.editor.getTemplateList); router.get('/editor/get/template', controller.editor.getTemplateList);
router.get('/editor/get/:pageId', controller.editor.get); router.get('/editor/get/:uuid', controller.editor.get);
router.delete('/editor/:pageId', controller.editor.delete); router.delete('/editor/:pageId', controller.editor.delete);
router.get('/editor/login', controller.editor.login); router.get('/editor/login', controller.editor.login);
router.get('/editor', controller.editor.home); router.get('/editor', controller.editor.home);
router.get('/editor/*', controller.editor.home); router.get('/editor/*', controller.editor.home);
router.get('/activity/:id', controller.activity.home); router.get('/*', controller.editor.home);
}; };
\ No newline at end of file
import { Context, Service } from 'egg';
export default class ArticeService extends Service {
private context: Context;
constructor(ctx: Context) {
super(ctx);
this.context = ctx;
}
async set(key, value, seconds = 1000 * 60 * 60 * 24) {
value = JSON.stringify(value);
if (this.context.app.redis) {
await this.context.app.redis.set(key, value, 'EX', seconds);
}
}
async get(key) {
if (this.context.app.redis) {
const data = await this.context.app.redis.get(key);
if (!data) {
return;
}
return JSON.parse(data);
}
}
async del(key) {
await this.context.app.redis.del(key);
}
}
declare module 'egg' {
interface Application {
redis: any;
}
}
\ No newline at end of file
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
});
},
// todo: 对外接口需提供x-auth-token
// 购物车-添加商品
addShopCart(params) {
return http.post(`${config.kdspHost}/api/kdsp/shop-cart/add-update`, params, {
hideToken: true,
headers: {
'x-auth-token': '7386386a-3a78-41f9-8584-14933823bf20'
}
});
},
// 商品组或专题查询
getGoods(params) {
return http.post(`${config.kdspHost}/api/kdsp/activity/activity-goods-special/skus`, params);
// return {
// skus: [
// {
// skuNo: '100014565800',
// skuName: '【自营】【自营】小米手机 陶瓷黑 8GB+128GB 官方标配',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 2.85,
// marketPrice: 293
// },
// {
// skuNo: '100014565820',
// skuName: '【自营】[自营][自营]0',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 264,
// marketPrice: 300
// },
// {
// skuNo: '100014565820',
// skuName: '【自营】[自营][自营]0',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 264,
// marketPrice: 300
// },
// {
// skuNo: '100014565800',
// skuName: '【自营】【自营】小米手机 陶瓷黑 8GB+128GB 官方标配',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 2.85,
// marketPrice: 293
// },
// {
// skuNo: '100014565820',
// skuName: '【自营】[自营][自营]0',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 264,
// marketPrice: 300
// },
// {
// skuNo: '100014565820',
// skuName: '【自营】[自营][自营]0',
// skuUrl: 'https://img14.360buyimg.com/n0/jfs/t1/141986/32/5318/98164/5f3236baE713fd239/5f2746db41f3e9c0.jpg',
// salePrice: 264,
// marketPrice: 300
// }
// ]
// };
},
// 根据id查询优惠券列表
getCoupons(params) {
return http.get(`${config.kdspHost}/api/kdsp/coupon/list`, {
params,
hideToken: true,
headers: {
'x-auth-token': '7386386a-3a78-41f9-8584-14933823bf20'
}
});
// return {
// coupons: [
// {
// id: 1,
// pickupId: 1,
// name: '3C数码会场',
// startDate: '2021-02-22',
// endDate: '2021-02-22',
// faceValue: 100,
// limitAmount: 300,
// limitDesc: '满300减100',
// couponCategory: 1,
// iconUrl: 'in esse',
// description: 'officia do',
// status: 19667180,
// navUrl: 'exercitation est',
// pickupAble: false,
// publishCountFinished: false
// },
// {
// id: 2,
// pickupId: 1,
// name: '3C数码会场',
// startDate: '2021-02-22',
// endDate: '2021-02-22',
// faceValue: 100,
// limitAmount: 300,
// limitDesc: '满300减100',
// couponCategory: 1,
// iconUrl: 'oc',
// description: 'minim dolore tempor',
// status: 63205995,
// navUrl: 'cupidat',
// pickupAble: true,
// publishCountFinished: false
// },
// {
// id: 3,
// pickupId: 1,
// name: '3C数码会场',
// startDate: '2021-02-22',
// endDate: '2021-02-22',
// faceValue: 100,
// limitAmount: 300,
// limitDesc: '满300减100',
// couponCategory: 1,
// iconUrl: 'eu fugiat commodo voluptate exercitation',
// description: 'nisi',
// status: 43999314,
// navUrl: 'commodo in reprehenderit',
// pickupAble: false,
// publishCountFinished: true
// },
// {
// id: 4,
// pickupId: 1,
// name: '3C数码会场',
// startDate: '2021-02-22',
// endDate: '2021-02-22',
// faceValue: 100,
// limitAmount: 300,
// limitDesc: '满300减100',
// couponCategory: 1,
// iconUrl: 'eu fugiat commodo voluptate exercitation',
// description: 'nisi',
// status: 43999314,
// navUrl: 'commodo in reprehenderit',
// pickupAble: false,
// publishCountFinished: true
// }
// ]
// };
},
// 领取优惠券
pickupCoupon(params) {
return http.post(`${config.kdspHost}/api/kdsp/coupon/pickup`, params, {
hideToken: true,
// todo: header里二者参数需特殊处理
headers: {
'x-user-terminal': 'H5',
'vccChannel': '',
'x-auth-token': ''
},
});
},
};
\ No newline at end of file
html,
body {
padding: 0;
margin: 0;
height: 100%;
}
* {
outline: 0;
margin: 0;
padding: 0;
border: none;
box-sizing: border-box;
}
html {
background-color: #f9f9f9;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
textarea,
input[type="email"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="password"],
input[type="url"] {
-webkit-appearance: none;
}
a {
text-decoration: none;
}
li {
list-style: none;
}
\ No newline at end of file
app/web/asset/images/favicon.ico

4.19 KB | W: | H:

app/web/asset/images/favicon.ico

2.3 KB | W: | H:

app/web/asset/images/favicon.ico
app/web/asset/images/favicon.ico
app/web/asset/images/favicon.ico
app/web/asset/images/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
<template>
<html v-if="isNode">
<head>
<title>{{title}}</title>
<meta name="keywords" :content="keywords">
<meta name="description" :content="description">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/public/asset/css/reset.css">
</head>
<body>
<div id="app"><slot></slot></div>
</body>
</html>
<div v-else-if="!isNode" id="app"><slot></slot></div>
</template>
<style>
html {
font-size: 10vw !important;
}
@media screen and (min-width: 768Px) {
html {
font-size: 37.5Px !important;
}
body {
max-width: 375Px;
max-height: 667Px;
margin: 0 auto !important;
}
}
</style>
<script lang="ts" src="./index.ts"></script>
\ No newline at end of file
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'; import { Vue } from 'vue-property-decorator';
import iView from 'iview'; import micro from './micro/index.vue';
import Raven from 'raven-js'; import single from './single/index.vue';
import RavenVue from 'raven-js/plugins/vue';
import { release } from '@/.sentryclirc';
import VueContextMenu from '@editor/component/Contextmenu/index';
import localStorage from '@/service/localStorage.service';
import '@/service/qg.service';
import 'iview/dist/styles/iview.css';
Vue.use(iView); Vue.directive('track', {});
Vue.use(VueContextMenu);
// 初始化sentry export default window.__POWERED_BY_QIANKUN__ ? micro : single;
if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') { \ No newline at end of file
Raven.config('http://0785298052fd46128e201f30ca649102@sentry.q-gp.com/64', {
release,
environment: 'production'
})
.addPlugin(RavenVue, Vue)
.install();
}
@Component({
name: 'Layout',
})
export default class Layout extends Vue {
activeName: string = 'list';
username: string = localStorage.get('user')?.name || '陌生人';
get isDashboard() {
return this.activeName === 'detail';
}
@Watch('$route', { immediate: true })
onRouteChange(to) {
this.activeName = to.name;
}
select(name) {
this.$router.push({
name
});
}
logOut() {
localStorage.clear();
window.location.href = '/editor/login';
}
created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
}
}
\ No newline at end of file
<template>
<div id="app" class="layout">
<Row type="flex" class="layout-container" v-if="!isDashboard">
<Sider collapsible :collapsed-width="71" style="z-index: 100" ref="sider" v-model="isCollapsed">
<Menu :active-name="activeName" theme="dark" width="auto" :open-names="openNames" accordion @on-select="select" :class="menuitemClasses" ref="menus">
<template v-for="(menu, index) in page">
<MenuItem style="background: #001529;" v-if="!isCollapsed" :name="index" :key="menu.name">
<Icon :type="menu.icon"></Icon>
<span class="layout-text">{{ menu.name }}</span>
</MenuItem>
<Tooltip v-else="isCollapsed" :key="menu.name" transfer :content="menu.name" placement="right">
<MenuItem :name="index" :class="[index == activeName ? 'sub-menu-selected' : 'sub-menu-not-selected']">
<Icon :type="menu.icon"></Icon>
<span class="layout-text">{{ menu.name }}</span>
</MenuItem>
</Tooltip>
</template>
</Menu>
</Sider>
<div class="right-content">
<div class="layout-content">
<Spin fix v-show="showLoading">
<Icon type="load-c" size="58" class="spin-icon-load"></Icon>
<div style="font-size: 20Px">Loading</div>
</Spin>
<transition name="router-fade" mode="out-in">
<router-view></router-view>
</transition>
</div>
<div class="layout-copy">
2014-2021 &copy; QuantGroup
</div>
</div>
</Row>
<slot v-else></slot>
</div>
</template>
<script>
export default {
data() {
return {
showLoading: false,
activeName: 0,
openNames: ['0'],
isCollapsed: false,
page: [{
name: '作品列表',
icon: 'ios-list',
path: 'list',
children: []
},
{
name: '我的草稿',
icon: 'ios-book',
path: 'my',
children: []
},
{
name: '创意模板',
icon: 'ios-compose',
path: 'template',
children: []
}]
}
},
methods: {
select(name) {
console.log('select', name);
const current = this.page[name];
this.$router.push({
name: current.path,
});
this.$nextTick(() => {
this.activeName = name;
this.$refs.menus.updateOpened();
this.$refs.menus.updateActiveName();
});
},
},
created() {
console.log('isDashboard', this.$route);
},
computed: {
menuitemClasses: function() {
return ['ivu-menu-dark', 'menu-item', this.isCollapsed ? 'collapsed-menu' : ''];
},
isDashboard() {
console.log('isDashboard', this.$route);
return this.$route.name === 'detail';
}
},
}
</script>
<style lang="less" scoped>
.layout {
position: absolute;
top: 60Px !important;
bottom: 0;
border: 0 !important;
border-radius: 0;
background: #f5f7f9;
width: 100%;
.layout-container {
height: 100%;
flex-flow: row;
.menu-item {
& > div {
display: block !important;
}
span {
line-height: 21Px;
display: inline-block;
overflow: hidden;
width: 100Px;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
transition: width 0.2s ease, opacity;
z-index: 100;
}
i {
transform: translateX(0);
transition: font-size 0.2s ease, transform 0.2s ease;
vertical-align: middle;
font-size: 20Px;
}
.sub-menu-selected {
color: #fff;
}
.sub-menu-not-selected {
color: rgba(255, 255, 255, 0.7) !important;
}
}
.collapsed-menu {
span {
width: 0;
}
}
.right-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
.layout-content {
flex: 1;
height: 100%;
overflow: auto;
background: #fff;
border-radius: 4Px;
}
.layout-copy {
text-align: center;
padding: 10Px 0 20Px;
color: #9ea7b4;
}
}
}
}
.spin-icon-load {
animation: ani-demo-spin 1s linear infinite;
}
@keyframes ani-demo-spin {
from {
transform: rotate(0deg);
}
50% {
transform: rotate(180deg);
}
to {
transform: rotate(360deg);
}
}
.router-fade-enter-active,
.router-fade-leave-active {
transition: opacity 0.3s;
}
.router-fade-enter,
.router-fade-leave-active {
opacity: 0;
}
</style>
<!-- <script lang="ts" src="./index.ts"></script> -->
import { Vue, Component, Prop } from 'vue-property-decorator'; import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import iView from 'iview';
import Raven from 'raven-js'; import Raven from 'raven-js';
import RavenVue from 'raven-js/plugins/vue'; import RavenVue from 'raven-js/plugins/vue';
import { release } from '@/.sentryclirc'; import { release } from '@/.sentryclirc';
import VueContextMenu from '@editor/component/Contextmenu/index';
import localStorage from '@/service/localStorage.service';
import '@/service/qg.service'; import '@/service/qg.service';
import 'iview/dist/styles/iview.css';
Vue.use(iView);
Vue.use(VueContextMenu);
// 初始化sentry // 初始化sentry
if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') { if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') {
...@@ -15,14 +22,31 @@ if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') ...@@ -15,14 +22,31 @@ if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production')
} }
@Component({ @Component({
name: 'Layout' name: 'Layout',
}) })
export default class Layout extends Vue { export default class Layout extends Vue {
@Prop({ type: String, default: 'egg' }) title?: string; activeName: string = 'list';
@Prop({ type: String, default: 'Vue TypeScript Framework, Server Side Render' }) description?: string; username: string = localStorage.get('user')?.name || '陌生人';
@Prop({ type: String, default: 'Vue,TypeScript,Isomorphic' }) keywords?: string;
get isDashboard() {
return this.activeName === 'detail';
}
@Watch('$route', { immediate: true })
onRouteChange(to) {
this.activeName = to.name;
}
select(name) {
this.$router.push({
name
});
}
isNode: boolean = EASY_ENV_IS_NODE; logOut() {
localStorage.clear();
window.location.href = '/editor/login';
}
created() { created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE); console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
<slot v-if="isDashboard"></slot> <slot v-if="isDashboard"></slot>
</div> </div>
</template> </template>
<style lang="less"> <style lang="less" scoped>
#app { #app {
position: absolute; position: absolute;
top: 0; top: 0;
...@@ -60,7 +60,6 @@ ...@@ -60,7 +60,6 @@
.layout-content { .layout-content {
flex: 1; flex: 1;
margin: 15px;
overflow: auto; overflow: auto;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
...@@ -71,6 +70,7 @@ ...@@ -71,6 +70,7 @@
} }
.layout-copy { .layout-copy {
height: 45px;
text-align: center; text-align: center;
padding: 10px 0 20px; padding: 10px 0 20px;
color: #9ea7b4; color: #9ea7b4;
...@@ -100,6 +100,7 @@ ...@@ -100,6 +100,7 @@
.layout-logo-left { .layout-logo-left {
width: 90%; width: 90%;
height: 30px; height: 30px;
line-height: 30px;
color: #fff; color: #fff;
font-size: 18px; font-size: 18px;
text-align: center; text-align: center;
......
...@@ -17,9 +17,6 @@ ...@@ -17,9 +17,6 @@
<div v-else-if="!isNode" id="app"><slot></slot></div> <div v-else-if="!isNode" id="app"><slot></slot></div>
</template> </template>
<style> <style>
html {
font-size: 10vw;
}
#app { #app {
position: absolute; position: absolute;
top: 0; top: 0;
......
...@@ -2,8 +2,13 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http'; ...@@ -2,8 +2,13 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http';
export default { export default {
apiHost: `http://localhost:7001/`, apiHost: `http://localhost:7001/`,
// apiHost: `http://192.168.28.199:7001/`,
// apiHost: 'https://quantum-blocks-vcc2.liangkebang.net/',
h5Host: 'https://quantum-h5-vcc2.liangkebang.net/',
qiniuHost: `https://appsync.lkbang.net/`, qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`, 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`, qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
// kdspHost: 'https://kdsp-api-vcc2.liangkebang.net',
kdspHost: 'https://talos-vcc2.liangkebang.net'
}; };
\ No newline at end of file
...@@ -2,6 +2,7 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https'; ...@@ -2,6 +2,7 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default { export default {
apiHost: `https://quantum-blocks.q-gp.com/`, apiHost: `https://quantum-blocks.q-gp.com/`,
h5Host: 'https://quantum-h5.q-gp.com/',
qiniuHost: `https://appsync.lkbang.net/`, qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`, shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`, opapiHost: `${protocol}//opapi.q-gp.com`,
......
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https'; const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default { export default {
apiHost: `https://quantum-fe.liangkebang.net/`, apiHost: `https://quantum-blocks-vcc2.liangkebang.net/`,
h5Host: 'https://quantum-h5-vcc2.liangkebang.net/',
opapiHost: 'https://opapi-vcc2.liangkebang.net',
qiniuHost: `https://appsync.lkbang.net/`, qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`, shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`, qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
}; };
...@@ -62,6 +62,7 @@ export default class App { ...@@ -62,6 +62,7 @@ export default class App {
server() { server() {
return context => { return context => {
// console.log('server', context.state);
const vm = this.create(context.state); const vm = this.create(context.state);
const { store, router } = vm; const { store, router } = vm;
router.push(context.state.url); router.push(context.state.url);
......
declare var EASY_ENV_IS_NODE: boolean; declare var EASY_ENV_IS_NODE: boolean;
declare var EASY_ENV_IS_BROWSER: boolean; declare var EASY_ENV_IS_BROWSER: boolean;
declare var __webpack_public_path__: string;
type PlainObject<T = any> = { [key: string]: T }; type PlainObject<T = any> = { [key: string]: T };
interface Window {
__INITIAL_STATE__: string;
__POWERED_BY_QIANKUN__: string;
__INJECTED_PUBLIC_PATH_BY_QIANKUN__: string;
}
\ No newline at end of file
<template>
<div :class="['ad', { 'ad_one': column === 1, 'ad_two': column === 2, 'ad_three': column === 3 }]">
<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="coupon" v-if="column === 1">
<div class="coupon-item_one" :style="style" :key="index" v-for="(coupon, index) in couponList">
<div class="Gi_one-left">
<p>¥<span>{{ coupon.faceValue || '-' }}</span></p>
<p>{{coupon.limitDesc}}</p>
</div>
<div class="Gi_one-middle">
<p>{{ coupon.name }}</p>
<p>{{ coupon.couponValidTime }}</p>
</div>
<cr-button class="Gi_one-right" type="primary" @click="pickupCoupon(coupon)">{{btnText(coupon.pickupAble)}}</cr-button>
<div class="coupon-item-mask" v-if="!coupon.pickupAble && coupon.publishCountFinished">
<p>已抢空</p>
</div>
</div>
</div>
<div v-else :class="['coupon', {'coupon_two': column === 2, 'coupon_multiple': column === 3}]">
<div class="coupon-item" :style="style" :key="index" v-for="(coupon, index) in couponList">
<p class="coupon-item-title">{{ coupon.name }}</p>
<p class="coupon-item-amount">¥<span>{{ coupon.faceValue || '-' }}</span></p>
<p class="coupon-item-amount">{{coupon.limitDesc}}</p>
<cr-button shape="circle" type="primary" @click="pickupCoupon(coupon)">
{{btnText(coupon.pickupAble)}}
</cr-button>
<div class="coupon-item-mask" v-if="!coupon.pickupAble && coupon.publishCountFinished">
<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: this.bgImage ? `url(${this.bgImage}) no-repeat 0 0 / cover` : '',
backgroundColor: this.bgColor
}
}
},
watch: {
couponsList: {
handler: async function (newVal) {
if(newVal) this.getCoupons(newVal);
},
immediate: true
}
},
methods: {
async pickupCoupon(coupon) {
if (!coupon.pickupAble) {
await operationApi.pickupCoupon({ couponId: coupon.id });
this.$toast.success('领取成功');
await this.getCoupons([coupon.id]);
} else {
window.location.href = coupon.navUrl;
}
},
async getCoupons(ids) {
const { coupons } = await operationApi.getCoupons({ couponIds: ids.join(',') });
if(coupons && coupons.length) this.list = coupons;
},
btnText(pickupAble) {
return pickupAble ? '去使用' : '立即领取';
}
}
}
</script>
<style lang="less" scoped>
::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.coupon {
width: 100%;
padding: 0 12px;
box-sizing: border-box;
font-size: 0;
&-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;
.coupon-item {
width: 114px;
margin-right: 4px;
}
}
&_two {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.coupon-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('special')" v-if="showSpecial"></cr-image>
<div class="goods" v-if="column === 1">
<div class="goods-item goods-item_one" @click="go('detail', goods.skuNo)" v-for="goods in goodsList" :key="goods.id">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" :src="goods.skuUrl"></cr-image>
<div class="goods-item_one-right">
<p class="goods-item-title">{{goods.skuName}}</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥{{goods.salePrice || '-'}}</p>
<p>¥{{goods.marketPrice || '-'}}</p>
</div>
<cr-button
shape="circle"
type="primary"
@click="go('detail', goods.skuNo)"
>
立即抢购
</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" @click="go('detail', goods.skuNo)">
<cr-image width="100%" height="3.15rem" class="goods-item-img" fit="contain" :src="goods.skuUrl"></cr-image>
<p class="goods-item-title">{{goods.skuName}}</p>
<div class="goods-item-bottom">
<div class="Gi-bottom-left">
<p>¥{{goods.salePrice || '-'}}</p>
<p>¥{{goods.marketPrice || '-'}}</p>
</div>
<div class="Gi-bottom-right" @click="addShopCart(goods)">
<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.ids.length) {
let records = [];
if(newVal.type === 'goodsGroup') {
({ skus: records } = await operationApi.getGoods({ specialId: newVal.ids[0] }));
} else if (newVal.type === 'goods') {
({ skus: records } = await operationApi.getGoods({ skus: newVal.ids }));
}
if(records.length) this.list = records;
}
// console.log('watch', this.list);
},
immediate: true
}
},
methods: {
go(type, params) {
if (type === 'special' && this.specialLink) {
window.location.href = this.specialLink;
} else if (type === 'goodsDetail') {
window.location.href = `xyqb://homepage/goodsdetail?skuNo=${params}`;
}
},
async addShopCart(goods) {
const { skuNo, skuSource } = goods;
const params = [{ skuId: skuNo, skuNum: 1, skuSource, type: 1 }];
await operationApi.addShopCart({ shopCartBaseList: params });
this.$toast.success('添加成功');
}
}
}
</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 {
height: 36px;
font-size: 12px;
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 {
display: flex;
flex-direction: column;
align-items: center;
p {
max-width: 55px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
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;
flex-direction: row;
align-items: flex-end;
p {
max-width: 55px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
// 组件库替换变量处理
/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 sticky ref="GoodsTabs" @change="tabsChange" animated :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 CsGoods from '../Goods/index.vue';
import CsCoupon from '../Coupon/index.vue';
export default {
name: 'goods-tabs',
components: {
CsGoods,
CsCoupon
},
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;
});
return list;
}
},
mounted() {
setTimeout(() => {
this.$refs.GoodsTabs.resize();
})
},
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.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">
<div v-if="showSwiper" class="guide-cube-swiper" ref="mySwiper" v-swiper:mySwiper="swiperOptions">
<div class="swiper-wrapper">
<div :style="style" :class="{'swiper-slide_two': slidesPerColumn === 2, 'swiper-slide_one': slidesPerColumn === 1, 'swiper-slide': true }" :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>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import 'swiper/dist/css/swiper.css';
if (EASY_ENV_IS_BROWSER) {
const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
Vue.use(VueAwesomeSwiper)
}
export default {
inject: ['editor'],
props: {
list: {
type: Array,
default: () => ([])
},
slidesPerColumn: {
type: Number,
default: 1
},
autoplay: Boolean,
animation: Boolean
},
data() {
const vm = this;
let isEven = true;
let lastEvenProgress = 0;
let lastProgress = 0;
return {
showSwiper: true,
swiperOptions: {
loop: this.slidesPerColumn === 1 ? true : false,
slidesPerView: 4,
slidesPerColumn: this.slidesPerColumn,
spaceBetween: 8,
observer:true,
observeParents:true,
watchSlidesProgress: true,
autoplay: this.autoplay,
on: {
progress: function() {
const slidesLength = this.slides.length;
if (!vm.animation) return;
let ratio = Math.abs(this.slides[0].progress % 1).toFixed(10) * 0.2;
if ([0, 1].includes(Math.abs(this.slides[0].progress % 2))) {
isEven = Math.abs(this.slides[0].progress % 2) === 0 ? true : false;
}
if (Math.abs(this.slides[0].progress - lastEvenProgress) > 1) {
lastEvenProgress = Math.floor(this.slides[0].progress);
isEven = !isEven;
}
const isLeft = lastProgress < this.slides[0].progress;
lastProgress = this.slides[0].progress;
for (let i = 0; i < this.slides.length; i++) {
const slide = this.slides.eq(i);
let scale = 1;
if (Math.abs(this.slides[0].progress % 2) === 0) {
scale = vm.memorys[i] ? 0.8 : 1;
} else if(Math.abs(this.slides[0].progress % 2) === 1) {
scale = vm.memorys[i] ? 1 : 0.8;
} else if (isEven) {
if (isLeft) {
if (vm.memorys[i]) {
scale = 0.8 + ratio;
} else {
scale = 1 - ratio;
}
} else {
if (vm.memorys[i]) {
scale = 1 - ratio;
} else {
scale = 0.8 + ratio;
}
}
} else if (!isEven) {
if (isLeft) {
if (vm.memorys[i]) {
scale = 1 - ratio;
} else {
scale = 0.8 + ratio;
}
} else {
if (vm.memorys[i]) {
scale = 0.8 + ratio;
} else {
scale = 1 - ratio;
}
}
}
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);
},
style() {
return this.animation ? {
transition: 'all .2s cubic-bezier(.4, 0, .2, 1)'
} : {
transform: 'none'
};
}
},
watch: {
slidesPerColumn(newVal) {
this.refreshSwiper('slidesPerColumn', newVal);
},
autoplay(newVal) {
this.refreshSwiper('autoplay', newVal);
},
loop(newVal) {
this.refreshSwiper('loop', newVal);
},
},
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 {
width: 100%;
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">
<div class="marquee-swiper" v-if='showSwiper' ref="mySwiper" v-swiper:mySwiper="swiperOptions">
<div class="swiper-wrapper">
<div class="swiper-slide" :key="index" v-for="(item, index) in list">
<a class="swiper-slide-a" @click="go(item.link)">
<span :style="styles">{{item.text}}</span>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import 'swiper/dist/css/swiper.css';
if (EASY_ENV_IS_BROWSER) {
const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
Vue.use(VueAwesomeSwiper)
}
export default {
inject: ['editor'],
props: {
list: {
type: Array,
default: () => ([])
},
fontColor: {
type: String,
default: '#666666'
}
},
data() {
return {
showSwiper: true,
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
}
}
},
watch: {
list: {
deep: true,
handler() {
this.refreshSwiper();
}
}
},
methods: {
go(link) {
window.location.href = link;
},
refreshSwiper() {
console.log('refreshSwiper');
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
}
}
}
</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;
a {
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
// export const basicComponents = require('@qg/cherry-ui/md/index.json'); const crComponents = require('@qg/cherry-ui/md/index.json');
// export const kaLoginForm = require('@qg/citrus-ui/md/index.json'); export const businessComponents = require('@qg/citrus-ui/md/index.json');
export const basicComponents = [ export const basicComponents = [
{ {
eleName: 'freedom-container', eleName: 'freedom-container',
coverImage: 'https://img.lkbang.net/citrus-ui/cherry-component-cover-free-container.png',
title: '自由容器', title: '自由容器',
h: 300,
config: [ config: [
{ // {
key: 'backgroundImage', // key: 'backgroundImage',
name: '背景图片', // name: '背景图片',
type: 'Upload' // type: 'Upload'
}, // },
], // {
value: { // key: 'backgroundColor',
backgroundImage: 'http://activitystatic.q-gp.com/landing-bg.png'; // name: '背景颜色',
}, // type: 'ColorSelector'
commonStyle: {} // },
},
{
h: 44,
eleName: 'cr-button',
title: 'Button 按钮',
config: [
{
key: 'type',
name: '类型',
desc: '类型,可选值为',
options: [
'default',
'primary',
'info',
'danger',
'waring',
'success'
],
type: 'select'
},
{
key: 'size',
name: '尺寸',
desc: '尺寸,可选值为',
options: [
'normal',
'large',
'small',
'mini'
],
type: 'select'
},
{
key: 'color',
name: '颜色',
desc: '按钮颜色,支持传入',
type: 'ColorSelector'
},
{
key: 'icon',
name: '图标',
desc: '左侧',
type: 'text'
},
{
key: 'block',
name: '块级元素',
desc: '是否为块级元素',
type: 'checkbox'
},
{
key: 'shape',
name: '形状',
desc: '按钮形状,可选值为',
options: [
'square',
'circle'
],
type: 'select'
},
{
key: 'disabled',
name: '禁用',
desc: '是否禁用按钮',
type: 'checkbox'
},
{
key: 'plain',
name: '朴素按钮',
desc: '朴素按钮',
type: 'checkbox'
},
{
key: 'hairline',
name: '细边框',
desc: '细边框',
type: 'checkbox'
},
{
key: 'loading',
name: '加载中',
desc: '是否显示为加载状态',
type: 'checkbox'
},
{
key: 'loadingText',
name: '加载文字',
desc: '加载状态提示文字',
type: 'text'
},
{
key: 'loadingType',
name: '加载图标类型',
desc: '',
options: [
'circular',
'spinner'
],
type: 'select'
},
{
key: 'loadingSize',
name: '加载图标大小',
desc: '加载图标大小',
type: 'text'
}
], ],
value: { value: {
type: 'default', // backgroundImage: 'http://activitystatic.q-gp.com/landing-bg.png';
size: 'normal', // backgroundColor: '#fff'
color: '',
icon: '',
block: false,
shape: 'square',
disabled: false,
plain: false,
hairline: false,
loading: false,
loadingText: '',
loadingType: 'circular',
loadingSize: '20px'
}, },
commonStyle: {} commonStyle: {}
}, },
{ ...crComponents
eleName: 'cr-image', // {
title: '图片', // eleName: 'cr-button',
h: 140, // title: 'Button 按钮',
config: [ // config: [
{ // {
key: 'width', // key: 'type',
name: '宽度', // name: '类型',
type: 'text' // desc: '类型,可选值为',
}, // options: [
{ // 'default',
key: 'height', // 'primary',
name: '高度', // 'info',
type: 'text' // 'danger',
}, // 'waring',
{ // 'success'
key: 'src', // ],
name: '图片', // type: 'select'
type: 'Upload' // },
}, // {
], // key: 'size',
value: { // name: '尺寸',
width: '5.067rem', // desc: '尺寸,可选值为',
height: '3.733rem', // options: [
src: 'https://appsync.lkbang.net/Fs0qmUsMry39AjHDf_W-qgn8XEy6', // 'normal',
fit: 'contain', // 'large',
}, // 'small',
commonStyle: {} // 'mini'
}, // ],
{ // type: 'select'
eleName: 'cr-nav-bar', // },
title: 'NavBar 导航栏', // {
h: 48, // key: 'color',
config: [ // name: '颜色',
{ // desc: '按钮颜色,支持传入',
key: 'title', // type: 'ColorSelector'
name: '标题', // },
desc: '标题', // {
propType: 'string', // key: 'icon',
options: [], // name: '图标',
type: 'text' // desc: '左侧',
}, // type: 'text'
{ // },
key: 'leftText', // {
name: '左侧文案', // key: 'block',
desc: '左侧文案', // name: '块级元素',
propType: 'string', // desc: '是否为块级元素',
options: [], // type: 'checkbox'
type: 'text' // },
}, // {
{ // key: 'shape',
key: 'rightText', // name: '形状',
name: '右侧文案', // desc: '按钮形状,可选值为',
desc: '右侧文案', // options: [
default: '', // 'square',
options: [ // 'circle'
'info', // ],
'danger', // type: 'select'
'primary', // },
'warning' // {
], // key: 'disabled',
type: 'text' // name: '禁用',
}, // desc: '是否禁用按钮',
{ // type: 'checkbox'
key: 'leftArrow', // },
name: '左侧箭头', // {
desc: '是否显示左侧箭头', // key: 'plain',
propType: 'boolean', // name: '朴素按钮',
options: [], // desc: '朴素按钮',
type: 'checkbox' // type: 'checkbox'
}, // },
{ // {
key: 'border', // key: 'hairline',
name: '边框', // name: '细边框',
desc: '是否显示下边框', // desc: '细边框',
propType: 'boolean', // type: 'checkbox'
options: [], // },
type: 'checkbox' // {
}, // key: 'loading',
{ // name: '加载中',
key: 'fixed', // desc: '是否显示为加载状态',
name: '固定导航栏', // type: 'checkbox'
desc: '是否固定导航栏', // },
propType: 'boolean', // {
options: [], // key: 'loadingText',
type: 'checkbox' // name: '加载文字',
}, // desc: '加载状态提示文字',
{ // type: 'text'
key: 'zIndex', // },
name: 'z-index', // {
desc: '元素z-index', // key: 'loadingType',
propType: [ // name: '加载图标类型',
'number', // desc: '',
'string' // options: [
], // 'circular',
options: [], // 'spinner'
type: 'text' // ],
} // type: 'select'
], // },
value: { // {
title: '', // key: 'loadingSize',
leftText: '返回', // name: '加载图标大小',
rightText: '', // desc: '加载图标大小',
leftArrow: true, // type: 'text'
border: true, // }
fixed: true, // ],
zIndex: '9999' // value: {
}, // type: 'default',
commonStyle: {} // size: 'normal',
} // color: '',
// icon: '',
// block: false,
// shape: 'square',
// disabled: false,
// plain: false,
// hairline: false,
// loading: false,
// loadingText: '',
// loadingType: 'circular',
// loadingSize: '20px'
// },
// commonStyle: {}
// },
// {
// eleName: 'cr-image',
// title: '图片',
// config: [
// {
// key: 'width',
// name: '宽度',
// type: 'text'
// },
// {
// key: 'height',
// name: '高度',
// type: 'text'
// },
// {
// key: 'src',
// name: '图片',
// type: 'Upload'
// },
// ],
// value: {
// width: '5.067rem',
// height: '3.733rem',
// src: 'https://appsync.lkbang.net/Fs0qmUsMry39AjHDf_W-qgn8XEy6',
// fit: 'contain',
// },
// commonStyle: {}
// },
// {
// eleName: 'cr-nav-bar',
// title: 'NavBar 导航栏',
// config: [
// {
// key: 'title',
// name: '标题',
// desc: '标题',
// propType: 'string',
// options: [],
// type: 'text'
// },
// {
// key: 'leftText',
// name: '左侧文案',
// desc: '左侧文案',
// propType: 'string',
// options: [],
// type: 'text'
// },
// {
// key: 'rightText',
// name: '右侧文案',
// desc: '右侧文案',
// default: '',
// options: [
// 'info',
// 'danger',
// 'primary',
// 'warning'
// ],
// type: 'text'
// },
// {
// key: 'leftArrow',
// name: '左侧箭头',
// desc: '是否显示左侧箭头',
// propType: 'boolean',
// options: [],
// type: 'checkbox'
// },
// {
// key: 'border',
// name: '边框',
// desc: '是否显示下边框',
// propType: 'boolean',
// options: [],
// type: 'checkbox'
// },
// {
// key: 'fixed',
// name: '固定导航栏',
// desc: '是否固定导航栏',
// propType: 'boolean',
// options: [],
// type: 'checkbox'
// },
// {
// key: 'zIndex',
// name: 'z-index',
// desc: '元素z-index',
// propType: [
// 'number',
// 'string'
// ],
// options: [],
// type: 'text'
// }
// ],
// value: {
// title: '',
// leftText: '返回',
// rightText: '',
// leftArrow: true,
// border: true,
// fixed: true,
// zIndex: '9999'
// },
// commonStyle: {}
// }
]; ];
export const businessComponents = [ // export const businessComponents = [
{ // kaLoginForm,
eleName: 'login-form', // // {
title: 'KA登陆表单', // // eleName: 'cs-ka-login-form',
h: 224, // // title: 'KA登陆表单',
config: [ // // h: 224,
{ // // config: [
key: 'btnTxt', // // {
name: '按钮文案', // // key: 'btnTxt',
type: 'text' // // name: '按钮文案',
}, // // type: 'text'
{ // // },
key: 'btnColor', // // {
name: '按钮颜色', // // key: 'btnColor',
type: 'ColorSelector' // // name: '按钮颜色',
}, // // type: 'ColorSelector'
{ // // },
key: 'btnImage', // // {
name: '按钮图片', // // key: 'btnImage',
type: 'Upload' // // name: '按钮图片',
}, // // type: 'Upload'
{ // // },
key: 'vcBgColor', // // {
name: '验证码背景色(可点击态)', // // key: 'vcBgColor',
type: 'ColorSelector' // // name: '验证码背景色(可点击态)',
}, // // type: 'ColorSelector'
{ // // },
key: 'registerFrom', // // {
name: '渠道号', // // key: 'registerFrom',
type: 'text' // // name: '渠道号',
}, // // type: 'text'
], // // },
value: { // // ],
btnTxt: '', // // value: {
btnColor: '#ee0a24', // // btnTxt: '',
btnImage: 'http://activitystatic.q-gp.com/landing-submit.png', // // btnColor: '#ee0a24',
registerFrom: '', // // btnImage: 'http://activitystatic.q-gp.com/landing-submit.png',
vcBgColor: '#97d5ff' // // registerFrom: '',
}, // // vcBgColor: '#97d5ff'
commonStyle: {} // // },
}, // // commonStyle: {}
{ // // },
eleName: 'download-guide', // // {
title: '下载引导', // // eleName: 'download-guide',
h: 40, // // title: '下载引导',
config: [ // // h: 40,
{ // // config: [
key: 'href', // // {
name: '跳转链接', // // key: 'href',
type: 'text' // // name: '跳转链接',
}, // // type: 'text'
{ // // },
key: 'leftImg', // // {
name: 'logo', // // key: 'leftImg',
type: 'Upload' // // name: 'logo',
} // // type: 'Upload'
], // // }
value: { // // ],
href: 'https://s.xyqb.com/4', // // value: {
leftImg: 'http://activitystatic.q-gp.com/xyqb%402x.png' // // href: 'https://s.xyqb.com/4',
}, // // leftImg: 'http://activitystatic.q-gp.com/xyqb%402x.png'
commonStyle: {} // // },
} // // commonStyle: {}
]; // // },
// // {
// // eleName: 'goods-tabs',
// // title: '商品导航',
// // config: [
// // {
// // title: '标签录入',
// // children: [
// // {
// // key: 'list',
// // name: '添加菜单',
// // type: 'FormList',
// // formControl: [
// // {
// // key: 'tabTitle',
// // name: '名称',
// // type: 'text'
// // },
// // {
// // key: 'componentId',
// // name: '组件',
// // type: 'ComponentSelect', // componentSelect
// // }
// // ]
// // }
// // ]
// // },
// // {
// // title: '标签样式',
// // children: [
// // {
// // key: 'activeColor',
// // name: '选中态文字颜色',
// // type: 'ColorSelector'
// // },
// // {
// // key: 'inactiveColor',
// // name: '默认态文字颜色',
// // type: 'ColorSelector'
// // },
// // {
// // key: 'underlineColor',
// // name: '下划线颜色',
// // type: 'ColorSelector'
// // },
// // ]
// // }
// // ],
// // value: {
// // list: [{
// // tabTitle: 'tab1',
// // componentId: '1'
// // }, {
// // tabTitle: 'tab2',
// // componentId: '2'
// // }],
// // activeColor: '#323233',
// // inactiveColor: '#646566',
// // underlineColor: '#1989fa',
// // },
// // commonStyle: {}
// // },
// // {
// // eleName: 'Goods',
// // title: '商品',
// // config: [
// // {
// // title: '商品录入',
// // children: [{
// // key: 'column',
// // name: '商品列数',
// // type: 'ColumnSelector'
// // },
// // {
// // key: 'goods',
// // name: '选择商品',
// // type: 'GoodsTableModal',
// // formControl: [
// // {
// // key: 'goodsNumber',
// // name: '商品数量',
// // type: 'number'
// // }
// // ]
// // }]
// // },
// // {
// // title: '专题推荐',
// // children: [
// // {
// // key: 'showSpecial',
// // name: '显示专题',
// // type: 'checkbox'
// // },
// // {
// // key: 'specialUrl',
// // name: '专题图片',
// // type: 'Upload'
// // },
// // {
// // key: 'specialLink',
// // name: '专题链接',
// // type: 'text'
// // },
// // ]
// // }
// // ],
// // value: {
// // goods: {
// // type: 'goodsGroup',
// // ids: []
// // },
// // goodsNumber: 3,
// // column: 2,
// // showSpecial: true,
// // specialUrl: '',
// // specialLink: ''
// // },
// // commonStyle: {}
// // },
// // {
// // eleName: 'Coupon',
// // title: '优惠券',
// // config: [
// // {
// // title: '优惠券录入',
// // children: [
// // {
// // key: 'column',
// // name: '优惠券列数',
// // type: 'ColumnSelector'
// // },
// // {
// // key: 'couponsList',
// // name: '选择优惠券',
// // type: 'CouponTableModal',
// // formControl: [
// // {
// // key: 'couponsNumber',
// // name: '优惠券数量',
// // type: 'number'
// // }
// // ]
// // },
// // ],
// // },
// // {
// // title: '优惠券样式',
// // children: [
// // {
// // key: 'bgImage',
// // name: '优惠券背景图片',
// // type: 'Upload'
// // },
// // {
// // key: 'bgColor',
// // name: '优惠券背景颜色',
// // type: 'ColorSelector'
// // },
// // ]
// // }
// // ],
// // value: {
// // couponsList: [],
// // couponsNumber: 3,
// // column: 3,
// // bgColor: '',
// // bgImage: '',
// // },
// // commonStyle: {}
// // },
// // {
// // eleName: 'Advertisement',
// // title: '广告',
// // config: [
// // {
// // key: 'column',
// // name: '广告列数',
// // type: 'ColumnSelector'
// // },
// // {
// // key: 'list',
// // name: '添加广告',
// // type: 'FormList',
// // formControl: [
// // {
// // key: 'img',
// // name: '图片',
// // type: 'Upload'
// // },
// // {
// // key: 'link',
// // name: '链接',
// // type: 'text', // componentSelect
// // }
// // ]
// // }
// // ],
// // value: {
// // list: [{
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }],
// // column: 2
// // }
// // },
// // {
// // eleName: 'Placeholder',
// // title: '楼层间隔',
// // config: [
// // {
// // key: 'height',
// // name: '间隔高度',
// // type: 'Slider'
// // },
// // ],
// // value: {
// // height: 10
// // }
// // },
// // {
// // eleName: 'GuideCube',
// // title: '导购魔方',
// // config: [
// // {
// // key: 'slidesPerColumn',
// // name: '行数',
// // type: 'InputNumber'
// // },
// // {
// // key: 'autoplay',
// // name: '自动播放',
// // type: 'checkbox'
// // },
// // {
// // key: 'animation',
// // name: '切换动画',
// // type: 'checkbox'
// // },
// // {
// // key: 'list',
// // name: '添加导购',
// // type: 'FormList',
// // formControl: [
// // {
// // key: 'img',
// // name: '图片',
// // type: 'Upload'
// // },
// // {
// // key: 'link',
// // name: '链接',
// // type: 'text', // componentSelect
// // }
// // ]
// // }
// // ],
// // value: {
// // list: [{
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }, {
// // img: 'http://activitystatic.q-gp.com/landing-bg.png',
// // link: 'http://activitystatic.q-gp.com'
// // }],
// // slidesPerColumn: 1,
// // autoplay: false,
// // animation: false
// // }
// // },
// // {
// // eleName: 'CustomMarquee',
// // title: '跑马灯',
// // config: [
// // {
// // key: 'fontColor',
// // name: '字体颜色',
// // type: 'ColorSelector',
// // },
// // {
// // key: 'list',
// // name: '添加数据',
// // type: 'FormList',
// // formControl: [
// // {
// // key: 'text',
// // name: '文案',
// // type: 'text'
// // },
// // {
// // key: 'link',
// // name: '链接',
// // type: 'text'
// // },
// // ]
// // }
// // ],
// // value: {
// // fontColor: '#666666',
// // list: [{
// // text: '请输入文案'
// // }, {
// // text: '请输入文案'
// // }]
// // }
// // },
// ];
\ No newline at end of file
import { cloneDeep } from 'lodash';
import { Component, Prop, Vue } 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 { resizeDiv } from '@/service/utils.service';
@Component({ components: { LoginForm }, name: 'FreedomContainer' })
export default class FreedomContainer extends Vue {
@Getter('pageData') pageData;
@State(state => state.editor.curChildIndex) curChildIndex;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@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);
const { props: { backgroundImage }, point } = this.pageData?.elements[index] || { props: {}};
if (backgroundImage) {
resizeDiv(backgroundImage, null, null, (height) => {
this.updatePageInfo({ containerIndex: index, data: { ...this.pageData?.elements[index], point: { ...point, h: height ?? point.h } } });
});
}
}
}
\ No newline at end of file
<template>
<div class="freedom">
<div class="freedom-body" :style="{background: `url(${backgroundImage}) no-repeat 0 0 / cover`}">
<component :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle)" :is="item.name" :key="index" v-bind="item.props"></component>
</div>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.freedom {
height: 100%;
width: 100%;
header {
width: 100%;
height: 48px;
line-height: 48px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
&-body {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
&_full {
height: calc(100% - 48px);
}
}
}
</style>
\ No newline at end of file
<template>
<div ref="item"
class="vue-grid-item"
:class="classObj"
:style="style"
>
<slot></slot>
<span v-if="resizableAndNotStatic" ref="handle" :class="resizableHandleClass"></span>
<!--<span v-if="draggable" ref="dragHandle" class="vue-draggable-handle"></span>-->
</div>
</template>
<style>
.vue-grid-item {
transition: all 200ms ease;
transition-property: left, top, right;
/* add right for rtl */
}
.vue-grid-item.no-touch {
-ms-touch-action: none;
touch-action: none;
}
.vue-grid-item.cssTransforms {
transition-property: transform;
left: 0;
right: auto;
}
.vue-grid-item.cssTransforms.render-rtl {
left: auto;
right: 0;
}
.vue-grid-item.resizing {
opacity: 0.6;
z-index: 3;
}
.vue-grid-item.vue-draggable-dragging {
transition:none;
z-index: 3;
}
.vue-grid-item.vue-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.vue-grid-item > .vue-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=');
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
}
.vue-grid-item > .vue-rtl-resizable-handle {
bottom: 0;
left: 0;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAuMDAwMDAwMDAwMDAwMDAyIiBoZWlnaHQ9IjEwLjAwMDAwMDAwMDAwMDAwMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDwhLS0gQ3JlYXRlZCB3aXRoIE1ldGhvZCBEcmF3IC0gaHR0cDovL2dpdGh1Yi5jb20vZHVvcGl4ZWwvTWV0aG9kLURyYXcvIC0tPgogPGc+CiAgPHRpdGxlPmJhY2tncm91bmQ8L3RpdGxlPgogIDxyZWN0IGZpbGw9Im5vbmUiIGlkPSJjYW52YXNfYmFja2dyb3VuZCIgaGVpZ2h0PSIxMiIgd2lkdGg9IjEyIiB5PSItMSIgeD0iLTEiLz4KICA8ZyBkaXNwbGF5PSJub25lIiBvdmVyZmxvdz0idmlzaWJsZSIgeT0iMCIgeD0iMCIgaGVpZ2h0PSIxMDAlIiB3aWR0aD0iMTAwJSIgaWQ9ImNhbnZhc0dyaWQiPgogICA8cmVjdCBmaWxsPSJ1cmwoI2dyaWRwYXR0ZXJuKSIgc3Ryb2tlLXdpZHRoPSIwIiB5PSIwIiB4PSIwIiBoZWlnaHQ9IjEwMCUiIHdpZHRoPSIxMDAlIi8+CiAgPC9nPgogPC9nPgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxsaW5lIGNhbnZhcz0iI2ZmZmZmZiIgY2FudmFzLW9wYWNpdHk9IjEiIHN0cm9rZS1saW5lY2FwPSJ1bmRlZmluZWQiIHN0cm9rZS1saW5lam9pbj0idW5kZWZpbmVkIiBpZD0ic3ZnXzEiIHkyPSItNzAuMTc4NDA3IiB4Mj0iMTI0LjQ2NDE3NSIgeTE9Ii0zOC4zOTI3MzciIHgxPSIxNDQuODIxMjg5IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSJub25lIi8+CiAgPGxpbmUgc3Ryb2tlPSIjNjY2NjY2IiBzdHJva2UtbGluZWNhcD0idW5kZWZpbmVkIiBzdHJva2UtbGluZWpvaW49InVuZGVmaW5lZCIgaWQ9InN2Z181IiB5Mj0iOS4xMDY5NTciIHgyPSIwLjk0NzI0NyIgeTE9Ii0wLjAxODEyOCIgeDE9IjAuOTQ3MjQ3IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiLz4KICA8bGluZSBzdHJva2UtbGluZWNhcD0idW5kZWZpbmVkIiBzdHJva2UtbGluZWpvaW49InVuZGVmaW5lZCIgaWQ9InN2Z183IiB5Mj0iOSIgeDI9IjEwLjA3MzUyOSIgeTE9IjkiIHgxPSItMC42NTU2NCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiM2NjY2NjYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+);
background-position: bottom left;
padding-left: 3px;
background-repeat: no-repeat;
background-origin: content-box;
cursor: sw-resize;
right: auto;
}
.vue-grid-item.disable-userselect {
user-select: none;
}
</style>
<script>
import {setTopLeft, setTopRight, setTransformRtl, setTransform} from './helpers/utils';
import {getControlPosition, createCoreData} from './helpers/draggableUtils';
import {getColsFromBreakpoint} from './helpers/responsiveUtils';
import {getDocumentDir} from "./helpers/DOM";
// var eventBus = require('./eventBus');
import '@interactjs/auto-start'
import '@interactjs/actions/drag'
import '@interactjs/actions/resize'
import '@interactjs/modifiers'
import '@interactjs/dev-tools'
import interact from '@interactjs/interact'
export default {
name: "GridItem",
props: {
/*cols: {
type: Number,
required: true
},*/
/*containerWidth: {
type: Number,
required: true
},
rowHeight: {
type: Number,
required: true
},
margin: {
type: Array,
required: true
},
maxRows: {
type: Number,
required: true
},*/
isDraggable: {
type: Boolean,
required: false,
default: null
},
isResizable: {
type: Boolean,
required: false,
default: null
},
/*useCssTransforms: {
type: Boolean,
required: true
},
*/
static: {
type: Boolean,
required: false,
default: false
},
minH: {
type: Number,
required: false,
default: 1
},
minW: {
type: Number,
required: false,
default: 1
},
maxH: {
type: Number,
required: false,
default: Infinity
},
maxW: {
type: Number,
required: false,
default: Infinity
},
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
},
w: {
type: Number,
required: true
},
h: {
type: Number,
required: true
},
i: {
required: true
},
dragIgnoreFrom: {
type: String,
required: false,
default: 'a, button'
},
dragAllowFrom: {
type: String,
required: false,
default: null
},
resizeIgnoreFrom: {
type: String,
required: false,
default: 'a, button'
},
},
inject: ["eventBus", "layout"],
data: function () {
return {
cols: 1,
containerWidth: 100,
rowHeight: 30,
margin: [10, 10],
maxRows: Infinity,
draggable: null,
resizable: null,
useCssTransforms: true,
useStyleCursor: true,
isDragging: false,
dragging: null,
isResizing: false,
resizing: null,
lastX: NaN,
lastY: NaN,
lastW: NaN,
lastH: NaN,
style: {},
rtl: false,
dragEventSet: false,
resizeEventSet: false,
previousW: null,
previousH: null,
previousX: null,
previousY: null,
innerX: this.x,
innerY: this.y,
innerW: this.w,
innerH: this.h
}
},
created () {
let self = this;
// Accessible refernces of functions for removing in beforeDestroy
self.updateWidthHandler = function (width) {
self.updateWidth(width);
};
self.compactHandler = function (layout) {
self.compact(layout);
};
self.setDraggableHandler = function (isDraggable) {
if (self.isDraggable === null) {
self.draggable = isDraggable;
}
};
self.setResizableHandler = function (isResizable) {
if (self.isResizable === null) {
self.resizable = isResizable;
}
};
self.setRowHeightHandler = function (rowHeight) {
self.rowHeight = rowHeight;
};
self.setMaxRowsHandler = function (maxRows) {
self.maxRows = maxRows;
};
self.directionchangeHandler = () => {
this.rtl = getDocumentDir() === 'rtl';
this.compact();
};
self.setColNum = (colNum) => {
self.cols = parseInt(colNum);
}
this.eventBus.$on('updateWidth', self.updateWidthHandler);
this.eventBus.$on('compact', self.compactHandler);
this.eventBus.$on('setDraggable', self.setDraggableHandler);
this.eventBus.$on('setResizable', self.setResizableHandler);
this.eventBus.$on('setRowHeight', self.setRowHeightHandler);
this.eventBus.$on('setMaxRows', self.setMaxRowsHandler);
this.eventBus.$on('directionchange', self.directionchangeHandler);
this.eventBus.$on('setColNum', self.setColNum)
this.rtl = getDocumentDir() === 'rtl';
},
beforeDestroy: function(){
let self = this;
//Remove listeners
this.eventBus.$off('updateWidth', self.updateWidthHandler);
this.eventBus.$off('compact', self.compactHandler);
this.eventBus.$off('setDraggable', self.setDraggableHandler);
this.eventBus.$off('setResizable', self.setResizableHandler);
this.eventBus.$off('setRowHeight', self.setRowHeightHandler);
this.eventBus.$off('setMaxRows', self.setMaxRowsHandler);
this.eventBus.$off('directionchange', self.directionchangeHandler);
this.eventBus.$off('setColNum', self.setColNum);
if (this.interactObj) {
this.interactObj.unset() // destroy interact intance
}
},
mounted: function () {
if (this.layout.responsive && this.layout.lastBreakpoint) {
this.cols = getColsFromBreakpoint(this.layout.lastBreakpoint, this.layout.cols);
} else {
this.cols = this.layout.colNum;
}
this.rowHeight = this.layout.rowHeight;
this.containerWidth = this.layout.width !== null ? this.layout.width : 100;
this.margin = this.layout.margin !== undefined ? this.layout.margin : [10, 10];
this.maxRows = this.layout.maxRows;
if (this.isDraggable === null) {
this.draggable = this.layout.isDraggable;
} else {
this.draggable = this.isDraggable;
}
if (this.isResizable === null) {
this.resizable = this.layout.isResizable;
} else {
this.resizable = this.isResizable;
}
this.useCssTransforms = this.layout.useCssTransforms;
this.useStyleCursor = this.layout.useStyleCursor;
this.createStyle();
},
watch: {
isDraggable: function () {
this.draggable = this.isDraggable;
},
static: function () {
this.tryMakeDraggable();
this.tryMakeResizable();
},
draggable: function () {
this.tryMakeDraggable();
},
isResizable: function () {
this.resizable = this.isResizable;
},
resizable: function () {
this.tryMakeResizable();
},
rowHeight: function () {
this.createStyle();
this.emitContainerResized();
},
cols: function () {
this.tryMakeResizable();
this.createStyle();
this.emitContainerResized();
},
containerWidth: function () {
this.tryMakeResizable();
this.createStyle();
this.emitContainerResized();
},
x: function (newVal) {
this.innerX = newVal;
this.createStyle();
},
y: function (newVal) {
this.innerY = newVal;
this.createStyle();
},
h: function (newVal) {
this.innerH = newVal
this.createStyle();
// this.emitContainerResized();
},
w: function (newVal) {
this.innerW = newVal;
this.createStyle();
// this.emitContainerResized();
},
renderRtl: function () {
// console.log("### renderRtl");
this.tryMakeResizable();
this.createStyle();
},
minH: function () {
this.tryMakeResizable();
},
maxH: function () {
this.tryMakeResizable();
},
minW: function () {
this.tryMakeResizable();
},
maxW: function () {
this.tryMakeResizable();
},
"$parent.margin": function (margin) {
if (!margin || (margin[0] == this.margin[0] && margin[1] == this.margin[1])) {
return;
}
this.margin = margin.map(m => Number(m));
this.createStyle();
this.emitContainerResized();
},
},
computed: {
classObj() {
return {
'vue-resizable' : this.resizableAndNotStatic,
'static': this.static,
'resizing' : this.isResizing,
'vue-draggable-dragging' : this.isDragging,
'cssTransforms' : this.useCssTransforms,
'render-rtl' : this.renderRtl,
'disable-userselect': this.isDragging,
'no-touch': this.isAndroid && this.draggableOrResizableAndNotStatic
}
},
resizableAndNotStatic(){
return this.resizable && !this.static;
},
draggableOrResizableAndNotStatic(){
return (this.draggable || this.resizable) && !this.static;
},
isAndroid() {
return true;
// return navigator.userAgent.toLowerCase().indexOf("android") !== -1;
},
renderRtl() {
return (this.layout.isMirrored) ? !this.rtl : this.rtl;
},
resizableHandleClass() {
if (this.renderRtl) {
return 'vue-resizable-handle vue-rtl-resizable-handle';
} else {
return 'vue-resizable-handle';
}
}
},
methods: {
createStyle: function () {
if (this.x + this.w > this.cols) {
this.innerX = 0;
this.innerW = (this.w > this.cols) ? this.cols : this.w
} else {
this.innerX = this.x;
this.innerW = this.w;
}
let pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
if (this.isDragging) {
pos.top = this.dragging.top;
// Add rtl support
if (this.renderRtl) {
pos.right = this.dragging.left;
} else {
pos.left = this.dragging.left;
}
}
if (this.isResizing) {
pos.width = this.resizing.width;
pos.height = this.resizing.height;
}
let style;
// CSS Transforms support (default)
if (this.useCssTransforms) {
// Add rtl support
if (this.renderRtl) {
style = setTransformRtl(pos.top, pos.right, pos.width, pos.height);
} else {
style = setTransform(pos.top, pos.left, pos.width, pos.height);
}
} else { // top,left (slow)
// Add rtl support
if (this.renderRtl) {
style = setTopRight(pos.top, pos.right, pos.width, pos.height);
} else {
style = setTopLeft(pos.top, pos.left, pos.width, pos.height);
}
}
this.style = style;
},
emitContainerResized() {
// this.style has width and height with trailing 'px'. The
// resized event is without them
let styleProps = {};
for (let prop of ['width', 'height']) {
let val = this.style[prop];
let matches = val.match(/^(\d+)px$/);
if (! matches)
return;
styleProps[prop] = matches[1];
}
this.$emit("container-resized", this.i, this.h, this.w, styleProps.height, styleProps.width);
},
handleResize: function (event) {
if (this.static) return;
const position = getControlPosition(event);
// Get the current drag point from the event. This is used as the offset.
if (position == null) return; // not possible but satisfies flow
const {x, y} = position;
const newSize = {width: 0, height: 0};
let pos;
switch (event.type) {
case "resizestart": {
this.previousW = this.innerW;
this.previousH = this.innerH;
pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
newSize.width = pos.width;
newSize.height = pos.height;
this.resizing = newSize;
this.isResizing = true;
break;
}
case "resizemove": {
// console.log("### resize => " + event.type + ", lastW=" + this.lastW + ", lastH=" + this.lastH);
const coreEvent = createCoreData(this.lastW, this.lastH, x, y);
if (this.renderRtl) {
newSize.width = this.resizing.width - coreEvent.deltaX;
} else {
newSize.width = this.resizing.width + coreEvent.deltaX;
}
newSize.height = this.resizing.height + coreEvent.deltaY;
///console.log("### resize => " + event.type + ", deltaX=" + coreEvent.deltaX + ", deltaY=" + coreEvent.deltaY);
this.resizing = newSize;
break;
}
case "resizeend": {
//console.log("### resize end => x=" +this.innerX + " y=" + this.innerY + " w=" + this.innerW + " h=" + this.innerH);
pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
newSize.width = pos.width;
newSize.height = pos.height;
// console.log("### resize end => " + JSON.stringify(newSize));
this.resizing = null;
this.isResizing = false;
break;
}
}
// Get new WH
pos = this.calcWH(newSize.height, newSize.width);
if (pos.w < this.minW) {
pos.w = this.minW;
}
if (pos.w > this.maxW) {
pos.w = this.maxW;
}
if (pos.h < this.minH) {
pos.h = this.minH;
}
if (pos.h > this.maxH) {
pos.h = this.maxH;
}
if (pos.h < 1) {
pos.h = 1;
}
if (pos.w < 1) {
pos.w = 1;
}
this.lastW = x;
this.lastH = y;
if (this.innerW !== pos.w || this.innerH !== pos.h) {
this.$emit("resize", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
if (event.type === "resizeend" && (this.previousW !== this.innerW || this.previousH !== this.innerH)) {
this.$emit("resized", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
this.eventBus.$emit("resizeEvent", event.type, this.i, this.innerX, this.innerY, pos.h, pos.w);
},
handleDrag(event) {
if (this.static) return;
if (this.isResizing) return;
const position = getControlPosition(event);
// Get the current drag point from the event. This is used as the offset.
if (position === null) return; // not possible but satisfies flow
const {x, y} = position;
// let shouldUpdate = false;
let newPosition = {top: 0, left: 0};
switch (event.type) {
case "dragstart": {
this.previousX = this.innerX;
this.previousY = this.innerY;
let parentRect = event.target.offsetParent.getBoundingClientRect();
let clientRect = event.target.getBoundingClientRect();
if (this.renderRtl) {
newPosition.left = (clientRect.right - parentRect.right) * -1;
} else {
newPosition.left = clientRect.left - parentRect.left;
}
newPosition.top = clientRect.top - parentRect.top;
this.dragging = newPosition;
this.isDragging = true;
break;
}
case "dragend": {
if (!this.isDragging) return;
let parentRect = event.target.offsetParent.getBoundingClientRect();
let clientRect = event.target.getBoundingClientRect();
// Add rtl support
if (this.renderRtl) {
newPosition.left = (clientRect.right - parentRect.right) * -1;
} else {
newPosition.left = clientRect.left - parentRect.left;
}
newPosition.top = clientRect.top - parentRect.top;
// console.log("### drag end => " + JSON.stringify(newPosition));
// console.log("### DROP: " + JSON.stringify(newPosition));
this.dragging = null;
this.isDragging = false;
// shouldUpdate = true;
break;
}
case "dragmove": {
const coreEvent = createCoreData(this.lastX, this.lastY, x, y);
// Add rtl support
if (this.renderRtl) {
newPosition.left = this.dragging.left - coreEvent.deltaX;
} else {
newPosition.left = this.dragging.left + coreEvent.deltaX;
}
newPosition.top = this.dragging.top + coreEvent.deltaY;
// console.log("### drag => " + event.type + ", x=" + x + ", y=" + y);
// console.log("### drag => " + event.type + ", deltaX=" + coreEvent.deltaX + ", deltaY=" + coreEvent.deltaY);
// console.log("### drag end => " + JSON.stringify(newPosition));
this.dragging = newPosition;
break;
}
}
// Get new XY
let pos;
if (this.renderRtl) {
pos = this.calcXY(newPosition.top, newPosition.left);
} else {
pos = this.calcXY(newPosition.top, newPosition.left);
}
this.lastX = x;
this.lastY = y;
if (this.innerX !== pos.x || this.innerY !== pos.y) {
this.$emit("move", this.i, pos.x, pos.y);
}
if (event.type === "dragend" && (this.previousX !== this.innerX || this.previousY !== this.innerY)) {
this.$emit("moved", this.i, pos.x, pos.y);
}
this.eventBus.$emit("dragEvent", event.type, this.i, pos.x, pos.y, this.innerH, this.innerW);
},
calcPosition: function (x, y, w, h) {
const colWidth = this.calcColWidth();
// add rtl support
let out;
if (this.renderRtl) {
out = {
right: Math.round(colWidth * x + (x + 1) * this.margin[0]),
top: Math.round(this.rowHeight * y + (y + 1) * this.margin[1]),
// 0 * Infinity === NaN, which causes problems with resize constriants;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * this.margin[0]),
height: h === Infinity ? h : Math.round(this.rowHeight * h + Math.max(0, h - 1) * this.margin[1])
};
} else {
out = {
left: Math.round(colWidth * x + (x + 1) * this.margin[0]),
top: Math.round(this.rowHeight * y + (y + 1) * this.margin[1]),
// 0 * Infinity === NaN, which causes problems with resize constriants;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * this.margin[0]),
height: h === Infinity ? h : Math.round(this.rowHeight * h + Math.max(0, h - 1) * this.margin[1])
};
}
return out;
},
/**
* Translate x and y coordinates from pixels to grid units.
* @param {Number} top Top position (relative to parent) in pixels.
* @param {Number} left Left position (relative to parent) in pixels.
* @return {Object} x and y in grid units.
*/
// TODO check if this function needs change in order to support rtl.
calcXY(top, left) {
const colWidth = this.calcColWidth();
// left = colWidth * x + margin * (x + 1)
// l = cx + m(x+1)
// l = cx + mx + m
// l - m = cx + mx
// l - m = x(c + m)
// (l - m) / (c + m) = x
// x = (left - margin) / (coldWidth + margin)
let x = Math.round((left - this.margin[0]) / (colWidth + this.margin[0]));
let y = Math.round((top - this.margin[1]) / (this.rowHeight + this.margin[1]));
// Capping
x = Math.max(Math.min(x, this.cols - this.innerW), 0);
y = Math.max(Math.min(y, this.maxRows - this.innerH), 0);
return {x, y};
},
// Helper for generating column width
calcColWidth() {
const colWidth = (this.containerWidth - (this.margin[0] * (this.cols + 1))) / this.cols;
// console.log("### COLS=" + this.cols + " COL WIDTH=" + colWidth + " MARGIN " + this.margin[0]);
return colWidth;
},
/**
* Given a height and width in pixel values, calculate grid units.
* @param {Number} height Height in pixels.
* @param {Number} width Width in pixels.
* @return {Object} w, h as grid units.
*/
calcWH(height, width) {
const colWidth = this.calcColWidth();
// width = colWidth * w - (margin * (w - 1))
// ...
// w = (width + margin) / (colWidth + margin)
let w = Math.round((width + this.margin[0]) / (colWidth + this.margin[0]));
let h = Math.round((height + this.margin[1]) / (this.rowHeight + this.margin[1]));
// Capping
w = Math.max(Math.min(w, this.cols - this.innerX), 0);
h = Math.max(Math.min(h, this.maxRows - this.innerY), 0);
return {w, h};
},
updateWidth: function (width, colNum) {
this.containerWidth = width;
if (colNum !== undefined && colNum !== null) {
this.cols = colNum;
}
},
compact: function () {
this.createStyle();
},
tryMakeDraggable: function(){
const self = this;
if (this.interactObj === null || this.interactObj === undefined) {
this.interactObj = interact(this.$refs.item);
if (!this.useStyleCursor) {
this.interactObj.styleCursor(false);
}
}
if (this.draggable && !this.static) {
const opts = {
ignoreFrom: this.dragIgnoreFrom,
allowFrom: this.dragAllowFrom
};
this.interactObj.draggable(opts);
/*this.interactObj.draggable({allowFrom: '.vue-draggable-handle'});*/
if (!this.dragEventSet) {
this.dragEventSet = true;
this.interactObj.on('dragstart dragmove dragend', function (event) {
self.handleDrag(event);
});
}
} else {
this.interactObj.draggable({
enabled: false
});
}
},
tryMakeResizable: function(){
const self = this;
if (this.interactObj === null || this.interactObj === undefined) {
this.interactObj = interact(this.$refs.item);
if (!this.useStyleCursor) {
this.interactObj.styleCursor(false);
}
}
if (this.resizable && !this.static) {
let maximum = this.calcPosition(0,0,this.maxW, this.maxH);
let minimum = this.calcPosition(0,0, this.minW, this.minH);
// console.log("### MAX " + JSON.stringify(maximum));
// console.log("### MIN " + JSON.stringify(minimum));
const opts = {
preserveAspectRatio: true,
// allowFrom: "." + this.resizableHandleClass.trim().replace(" ", "."),
edges: {
left: false,
right: "." + this.resizableHandleClass.trim().replace(" ", "."),
bottom: "." + this.resizableHandleClass.trim().replace(" ", "."),
top: false
},
ignoreFrom: this.resizeIgnoreFrom,
restrictSize: {
min: {
height: minimum.height,
width: minimum.width
},
max: {
height: maximum.height,
width: maximum.width
}
}
};
this.interactObj.resizable(opts);
if (!this.resizeEventSet) {
this.resizeEventSet = true;
this.interactObj
.on('resizestart resizemove resizeend', function (event) {
self.handleResize(event);
});
}
} else {
this.interactObj.resizable({
enabled: false
});
}
},
autoSize: function() {
// ok here we want to calculate if a resize is needed
this.previousW = this.innerW;
this.previousH = this.innerH;
let newSize=this.$slots.default[0].elm.getBoundingClientRect();
let pos = this.calcWH(newSize.height, newSize.width);
if (pos.w < this.minW) {
pos.w = this.minW;
}
if (pos.w > this.maxW) {
pos.w = this.maxW;
}
if (pos.h < this.minH) {
pos.h = this.minH;
}
if (pos.h > this.maxH) {
pos.h = this.maxH;
}
if (pos.h < 1) {
pos.h = 1;
}
if (pos.w < 1) {
pos.w = 1;
}
// this.lastW = x; // basically, this is copied from resizehandler, but shouldn't be needed
// this.lastH = y;
if (this.innerW !== pos.w || this.innerH !== pos.h) {
this.$emit("resize", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
if (this.previousW !== pos.w || this.previousH !== pos.h) {
this.$emit("resized", this.i, pos.h, pos.w, newSize.height, newSize.width);
this.eventBus.$emit("resizeEvent", "resizeend", this.i, this.innerX, this.innerY, pos.h, pos.w);
}
}
},
}
</script>
<template>
<div ref="item" class="vue-grid-layout" :style="mergedStyle">
<slot></slot>
<grid-item class="vue-grid-placeholder"
v-show="isDragging"
:x="placeholder.x"
:y="placeholder.y"
:w="placeholder.w"
:h="placeholder.h"
:i="placeholder.i"></grid-item>
</div>
</template>
<style>
.vue-grid-layout {
position: relative;
transition: height 200ms ease;
}
</style>
<script>
import Vue from 'vue';
const elementResizeDetectorMaker = require("element-resize-detector");
import {bottom, compact, getLayoutItem, moveElement, validateLayout, cloneLayout, getAllCollisions} from './helpers/utils';
import {getBreakpointFromWidth, getColsFromBreakpoint, findOrGenerateResponsiveLayout} from "./helpers/responsiveUtils";
//var eventBus = require('./eventBus');
import GridItem from './GridItem.vue'
import {addWindowEventListener, removeWindowEventListener} from "./helpers/DOM";
export default {
name: "GridLayout",
provide() {
return {
eventBus: null,
layout: this
}
},
components: {
GridItem,
},
props: {
// If true, the container height swells and contracts to fit contents
autoSize: {
type: Boolean,
default: true
},
colNum: {
type: Number,
default: 12
},
rowHeight: {
type: Number,
default: 150
},
maxRows: {
type: Number,
default: Infinity
},
margin: {
type: Array,
default: function () {
return [10, 10];
}
},
isDraggable: {
type: Boolean,
default: true
},
isResizable: {
type: Boolean,
default: true
},
isMirrored: {
type: Boolean,
default: false
},
useCssTransforms: {
type: Boolean,
default: true
},
verticalCompact: {
type: Boolean,
default: true
},
layout: {
type: Array,
required: true,
},
responsive: {
type: Boolean,
default: false
},
responsiveLayouts: {
type: Object,
default: function() {
return {};
}
},
breakpoints:{
type: Object,
default: function(){return{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
},
cols:{
type: Object,
default: function(){return{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }},
},
preventCollision: {
type: Boolean,
default: false
},
useStyleCursor: {
type: Boolean,
default: true
}
},
data: function () {
return {
width: null,
mergedStyle: {},
lastLayoutLength: 0,
isDragging: false,
placeholder: {
x: 0,
y: 0,
w: 0,
h: 0,
i: -1
},
layouts: {}, // array to store all layouts from different breakpoints
lastBreakpoint: null, // store last active breakpoint
originalLayout: null, // store original Layout
};
},
created () {
const self = this;
// Accessible refernces of functions for removing in beforeDestroy
self.resizeEventHandler = function(eventType, i, x, y, h, w) {
self.resizeEvent(eventType, i, x, y, h, w);
};
self.dragEventHandler = function(eventType, i, x, y, h, w) {
self.dragEvent(eventType, i, x, y, h, w);
};
self._provided.eventBus = new Vue();
self.eventBus = self._provided.eventBus;
self.eventBus.$on('resizeEvent', self.resizeEventHandler);
self.eventBus.$on('dragEvent', self.dragEventHandler);
self.$emit('layout-created', self.layout);
},
beforeDestroy: function(){
//Remove listeners
this.eventBus.$off('resizeEvent', this.resizeEventHandler);
this.eventBus.$off('dragEvent', this.dragEventHandler);
this.eventBus.$destroy();
removeWindowEventListener("resize", this.onWindowResize);
this.erd.uninstall(this.$refs.item);
},
beforeMount: function() {
this.$emit('layout-before-mount', this.layout);
},
mounted: function() {
this.$emit('layout-mounted', this.layout);
this.$nextTick(function () {
validateLayout(this.layout);
this.originalLayout = this.layout;
const self = this;
this.$nextTick(function() {
self.onWindowResize();
self.initResponsiveFeatures();
//self.width = self.$el.offsetWidth;
addWindowEventListener('resize', self.onWindowResize);
compact(self.layout, self.verticalCompact);
self.$emit('layout-updated',self.layout)
self.updateHeight();
self.$nextTick(function () {
this.erd = elementResizeDetectorMaker({
strategy: "scroll", //<- For ultra performance.
// See https://github.com/wnr/element-resize-detector/issues/110 about callOnAdd.
callOnAdd: false,
});
this.erd.listenTo(self.$refs.item, function () {
self.onWindowResize();
});
});
});
});
},
watch: {
width: function (newval, oldval) {
const self = this;
this.$nextTick(function () {
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
if (oldval === null) {
/*
If oldval == null is when the width has never been
set before. That only occurs when mouting is
finished, and onWindowResize has been called and
this.width has been changed the first time after it
got set to null in the constructor. It is now time
to issue layout-ready events as the GridItems have
their sizes configured properly.
The reason for emitting the layout-ready events on
the next tick is to allow for the newly-emitted
updateWidth event (above) to have reached the
children GridItem-s and had their effect, so we're
sure that they have the final size before we emit
layout-ready (for this GridLayout) and
item-layout-ready (for the GridItem-s).
This way any client event handlers can reliably
invistigate stable sizes of GridItem-s.
*/
this.$nextTick(() => {
this.$emit('layout-ready', self.layout);
});
}
this.updateHeight();
});
},
layout: function () {
this.layoutUpdate();
},
colNum: function (val) {
this.eventBus.$emit("setColNum", val);
},
rowHeight: function() {
this.eventBus.$emit("setRowHeight", this.rowHeight);
},
isDraggable: function() {
this.eventBus.$emit("setDraggable", this.isDraggable);
},
isResizable: function() {
this.eventBus.$emit("setResizable", this.isResizable);
},
responsive() {
if (!this.responsive) {
this.$emit('update:layout', this.originalLayout);
this.eventBus.$emit("setColNum", this.colNum);
}
this.onWindowResize();
},
maxRows: function() {
this.eventBus.$emit("setMaxRows", this.maxRows);
},
margin() {
this.updateHeight();
}
},
methods: {
layoutUpdate() {
if (this.layout !== undefined && this.originalLayout !== null) {
if (this.layout.length !== this.originalLayout.length) {
// console.log("### LAYOUT UPDATE!", this.layout.length, this.originalLayout.length);
let diff = this.findDifference(this.layout, this.originalLayout);
if (diff.length > 0){
// console.log(diff);
if (this.layout.length > this.originalLayout.length) {
this.originalLayout = this.originalLayout.concat(diff);
} else {
this.originalLayout = this.originalLayout.filter(obj => {
return !diff.some(obj2 => {
return obj.i === obj2.i;
});
});
}
}
this.lastLayoutLength = this.layout.length;
this.initResponsiveFeatures();
}
compact(this.layout, this.verticalCompact);
this.eventBus.$emit("updateWidth", this.width);
this.updateHeight();
this.$emit('layout-updated',this.layout)
}
},
updateHeight: function () {
this.mergedStyle = {
height: this.containerHeight()
};
},
onWindowResize: function () {
if (this.$refs !== null && this.$refs.item !== null && this.$refs.item !== undefined) {
this.width = this.$refs.item.offsetWidth;
}
this.eventBus.$emit("resizeEvent");
},
containerHeight: function () {
if (!this.autoSize) return;
// console.log("bottom: " + bottom(this.layout))
// console.log("rowHeight + margins: " + (this.rowHeight + this.margin[1]) + this.margin[1])
const containerHeight = bottom(this.layout) * (this.rowHeight + this.margin[1]) + this.margin[1] + 'px';
return containerHeight;
},
dragEvent: function (eventName, id, x, y, h, w) {
//console.log(eventName + " id=" + id + ", x=" + x + ", y=" + y);
let l = getLayoutItem(this.layout, id);
//GetLayoutItem sometimes returns null object
if (l === undefined || l === null){
l = {x:0, y:0}
}
if (eventName === "dragmove" || eventName === "dragstart") {
this.placeholder.i = id;
this.placeholder.x = l.x;
this.placeholder.y = l.y;
this.placeholder.w = w;
this.placeholder.h = h;
this.$nextTick(function() {
this.isDragging = true;
});
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
} else {
this.$nextTick(function() {
this.isDragging = false;
});
}
// Move the element to the dragged location.
this.layout = moveElement(this.layout, l, x, y, true, this.preventCollision);
compact(this.layout, this.verticalCompact);
// needed because vue can't detect changes on array element properties
this.eventBus.$emit("compact");
this.updateHeight();
if (eventName === 'dragend') this.$emit('layout-updated', this.layout);
},
resizeEvent: function (eventName, id, x, y, h, w) {
let l = getLayoutItem(this.layout, id);
//GetLayoutItem sometimes return null object
if (l === undefined || l === null){
l = {h:0, w:0}
}
let hasCollisions;
if (this.preventCollision) {
const collisions = getAllCollisions(this.layout, { ...l, w, h }).filter(
layoutItem => layoutItem.i !== l.i
);
hasCollisions = collisions.length > 0;
// If we're colliding, we need adjust the placeholder.
if (hasCollisions) {
// adjust w && h to maximum allowed space
let leastX = Infinity,
leastY = Infinity;
collisions.forEach(layoutItem => {
if (layoutItem.x > l.x) leastX = Math.min(leastX, layoutItem.x);
if (layoutItem.y > l.y) leastY = Math.min(leastY, layoutItem.y);
});
if (Number.isFinite(leastX)) l.w = leastX - l.x;
if (Number.isFinite(leastY)) l.h = leastY - l.y;
}
}
if (!hasCollisions) {
// Set new width and height.
l.w = w;
l.h = h;
}
if (eventName === "resizestart" || eventName === "resizemove") {
this.placeholder.i = id;
this.placeholder.x = x;
this.placeholder.y = y;
this.placeholder.w = l.w;
this.placeholder.h = l.h;
this.$nextTick(function() {
this.isDragging = true;
});
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
} else {
this.$nextTick(function() {
this.isDragging = false;
});
}
if (this.responsive) this.responsiveGridLayout();
compact(this.layout, this.verticalCompact);
this.eventBus.$emit("compact");
this.updateHeight();
if (eventName === 'resizeend') this.$emit('layout-updated', this.layout);
},
// finds or generates new layouts for set breakpoints
responsiveGridLayout(){
let newBreakpoint = getBreakpointFromWidth(this.breakpoints, this.width);
let newCols = getColsFromBreakpoint(newBreakpoint, this.cols);
// save actual layout in layouts
if(this.lastBreakpoint != null && !this.layouts[this.lastBreakpoint])
this.layouts[this.lastBreakpoint] = cloneLayout(this.layout);
// Find or generate a new layout.
let layout = findOrGenerateResponsiveLayout(
this.originalLayout,
this.layouts,
this.breakpoints,
newBreakpoint,
this.lastBreakpoint,
newCols,
this.verticalCompact
);
// Store the new layout.
this.layouts[newBreakpoint] = layout;
if (this.lastBreakpoint !== newBreakpoint) {
this.$emit('breakpoint-changed', newBreakpoint, layout);
}
// new prop sync
this.$emit('update:layout', layout);
this.lastBreakpoint = newBreakpoint;
this.eventBus.$emit("setColNum", getColsFromBreakpoint(newBreakpoint, this.cols));
},
// clear all responsive layouts
initResponsiveFeatures(){
// clear layouts
this.layouts = Object.assign({}, this.responsiveLayouts);
},
// find difference in layouts
findDifference(layout, originalLayout){
//Find values that are in result1 but not in result2
let uniqueResultOne = layout.filter(function(obj) {
return !originalLayout.some(function(obj2) {
return obj.i === obj2.i;
});
});
//Find values that are in result2 but not in result1
let uniqueResultTwo = originalLayout.filter(function(obj) {
return !layout.some(function(obj2) {
return obj.i === obj2.i;
});
});
//Combine the two arrays of unique entries#
return uniqueResultOne.concat(uniqueResultTwo);
}
},
}
</script>
let currentDir = 'auto';
// let currentDir = "auto";
function hasDocument() {
return (typeof document !== 'undefined');
}
function hasWindow() {
return (typeof window !== 'undefined');
}
export function getDocumentDir() {
if (!hasDocument()) {
return currentDir;
}
const direction = (typeof document.dir !== 'undefined') ?
document.dir :
document.getElementsByTagName('html')[0].getAttribute('dir');
return direction;
}
export function setDocumentDir(dir) {
// export function setDocumentDir(dir){
if (!hasDocument) {
currentDir = dir;
return;
}
const html = document.getElementsByTagName('html')[0];
html.setAttribute('dir', dir);
}
export function addWindowEventListener(event, callback) {
if (!hasWindow) {
callback();
return;
}
window.addEventListener(event, callback);
}
export function removeWindowEventListener(event, callback) {
if (!hasWindow) {
return;
}
window.removeEventListener(event, callback);
}
// Get {x, y} positions from event.
export function getControlPosition(e) {
return offsetXYFromParentOf(e);
}
// Get from offsetParent
export function offsetXYFromParentOf(evt) {
const offsetParent = evt.target.offsetParent || document.body;
const offsetParentRect = evt.offsetParent === document.body ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();
const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left;
const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top;
/*const x = Math.round(evt.clientX + offsetParent.scrollLeft - offsetParentRect.left);
const y = Math.round(evt.clientY + offsetParent.scrollTop - offsetParentRect.top);*/
return {x, y};
}
// Create an data object exposed by <DraggableCore>'s events
export function createCoreData(lastX, lastY, x, y) {
// State changes are often (but not always!) async. We want the latest value.
const isStart = !isNum(lastX);
if (isStart) {
// If this is our first move, use the x and y as last coords.
return {
deltaX: 0, deltaY: 0,
lastX: x, lastY: y,
x, y
};
} else {
// Otherwise calculate proper values.
return {
deltaX: x - lastX, deltaY: y - lastY,
lastX, lastY,
x, y
};
}
}
function isNum(num) {
return typeof num === 'number' && !isNaN(num);
}
// @flow
import {cloneLayout, compact, correctBounds} from './utils';
// import type {Layout} from './utils';
// export type ResponsiveLayout = {lg, md, sm, xs, xxs};
// type Breakpoint = string;
// type Breakpoints = {lg, md, sm, xs, xxs};
/**
* Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint).
*
* @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...})
* @param {Number} width Screen width.
* @return {String} Highest breakpoint that is less than width.
*/
export function getBreakpointFromWidth(breakpoints, width) {
const sorted = sortBreakpoints(breakpoints);
let matching = sorted[0];
for (let i = 1, len = sorted.length; i < len; i++) {
const breakpointName = sorted[i];
if (width > breakpoints[breakpointName]) { matching = breakpointName; }
}
return matching;
}
/**
* Given a breakpoint, get the # of cols set for it.
* @param {String} breakpoint Breakpoint name.
* @param {Object} cols Map of breakpoints to cols.
* @return {Number} Number of cols.
*/
export function getColsFromBreakpoint(breakpoint, cols) {
if (!cols[breakpoint]) {
throw new Error('ResponsiveGridLayout: `cols` entry for breakpoint ' + breakpoint + ' is missing!');
}
return cols[breakpoint];
}
/**
* Given existing layouts and a new breakpoint, find or generate a new layout.
*
* This finds the layout above the new one and generates from it, if it exists.
*
* @param {Array} orgLayout Original layout.
* @param {Object} layouts Existing layouts.
* @param {Array} breakpoints All breakpoints.
* @param {String} breakpoint New breakpoint.
* @param {String} breakpoint Last breakpoint (for fallback).
* @param {Number} cols Column count at new breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} New layout.
*/
export function findOrGenerateResponsiveLayout(orgLayout, layouts, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
if (layouts[breakpoint]) { return cloneLayout(layouts[breakpoint]); }
// Find or generate the next layout
let layout = orgLayout;
const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
export function generateResponsiveLayout(layout, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
/*if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]);
// Find or generate the next layout
let layout = layouts[lastBreakpoint];*/
/*const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}*/
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
/**
* Given breakpoints, return an array of breakpoints sorted by width. This is usually
* e.g. ['xxs', 'xs', 'sm', ...]
*
* @param {Object} breakpoints Key/value pair of breakpoint names to widths.
* @return {Array} Sorted breakpoints.
*/
export function sortBreakpoints(breakpoints) {
const keys = Object.keys(breakpoints);
return keys.sort(function(a, b) {
return breakpoints[a] - breakpoints[b];
});
}
// tslint:disable
// @flow
// export type LayoutItemRequired = {w, h, x, y, i};
// export type LayoutItem = LayoutItemRequired &
// {minW?, minH?, maxW?, maxH?,
// moved?, static?,
// isDraggable?, isResizable?};
// export type Layout = Array<LayoutItem>;
// export type Position = {left, top, width, height};
/*
export type DragCallbackData = {
node: HTMLElement,
x, y,
deltaX, deltaY,
lastX, lastY
};
*/
// export type DragEvent = {e: Event} & DragCallbackData;
// export type Size = {width, height};
// export type ResizeEvent = {e: Event, node: HTMLElement, size: Size};
// const isProduction = process.env.NODE_ENV === 'production';
/**
* Return the bottom coordinate of the layout.
*
* @param {Array} layout Layout array.
* @return {Number} Bottom coordinate.
*/
export function bottom(layout) {
let max = 0, bottomY;
for (let i = 0, len = layout.length; i < len; i++) {
bottomY = layout[i]. y + layout[i].h;
if (bottomY > max) { max = bottomY; }
}
return max;
}
export function cloneLayout(layout) {
const newLayout = Array(layout.length);
for (let i = 0, len = layout.length; i < len; i++) {
newLayout[i] = cloneLayoutItem(layout[i]);
}
return newLayout;
}
// Fast path to cloning, since this is monomorphic
export function cloneLayoutItem(layoutItem) {
/*return {
w.w, h.h, x.x, y.y, i.i,
minW.minW, maxW.maxW, minH.minH, maxH.maxH,
moved(layoutItem.moved), static(layoutItem.static),
// These can be null
isDraggable.isDraggable, isResizable.isResizable
};*/
return JSON.parse(JSON.stringify(layoutItem));
}
/**
* Given two layoutitems, check if they collide.
*
* @return {Boolean} True if colliding.
*/
export function collides(l1, l2) {
if (l1 === l2) { return false; } // same element
if (l1.x + l1.w <= l2.x) { return false; } // l1 is left of l2
if (l1.x >= l2.x + l2.w) { return false; } // l1 is right of l2
if (l1.y + l1.h <= l2.y) { return false; } // l1 is above l2
if (l1.y >= l2.y + l2.h) { return false; } // l1 is below l2
return true; // boxes overlap
}
/**
* Given a layout, compact it. This involves going down each y coordinate and removing gaps
* between items.
*
* @param {Array} layout Layout.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} Compacted Layout.
*/
export function compact(layout, verticalCompact) {
// Statics go in the compareWith array right away so items flow around them.
const compareWith = getStatics(layout);
// We go through the items by row and column.
const sorted = sortLayoutItemsByRowCol(layout);
// Holding for new items.
const out = Array(layout.length);
for (let i = 0, len = sorted.length; i < len; i++) {
let l = sorted[i];
// Don't move static elements
if (!l.static) {
l = compactItem(compareWith, l, verticalCompact);
// Add to comparison array. We only collide with items before this one.
// Statics are already in this array.
compareWith.push(l);
}
// Add to output array to make sure they still come out in the right order.
out[layout.indexOf(l)] = l;
// Clear moved flag, if it exists.
l.moved = false;
}
return out;
}
/**
* Compact an item in the layout.
*/
export function compactItem(compareWith, l, verticalCompact) {
if (verticalCompact) {
// Move the element up as far as it can go without colliding.
while (l.y > 0 && !getFirstCollision(compareWith, l)) {
l.y--;
}
}
// Move it down, and keep moving it down if it's colliding.
let collides;
while ((collides = getFirstCollision(compareWith, l))) {
l.y = collides.y + collides.h;
}
return l;
}
/**
* Given a layout, make sure all elements fit within its bounds.
*
* @param {Array} layout Layout array.
* @param {Number} bounds Number of columns.
*/
export function correctBounds(layout, bounds) {
const collidesWith = getStatics(layout);
for (let i = 0, len = layout.length; i < len; i++) {
const l = layout[i];
// Overflows right
if (l.x + l.w > bounds.cols) { l.x = bounds.cols - l.w; }
// Overflows left
if (l.x < 0) {
l.x = 0;
l.w = bounds.cols;
}
if (!l.static) { collidesWith.push(l); } else {
// If this is static and collides with other statics, we must move it down.
// We have to do something nicer than just letting them overlap.
while (getFirstCollision(collidesWith, l)) {
l.y++;
}
}
}
return layout;
}
/**
* Get a layout item by ID. Used so we can override later on if necessary.
*
* @param {Array} layout Layout array.
* @param {String} id ID
* @return {LayoutItem} Item at ID.
*/
export function getLayoutItem(layout, id) {
for (let i = 0, len = layout.length; i < len; i++) {
if (layout[i].i === id) { return layout[i]; }
}
}
/**
* Returns the first item this layout collides with.
* It doesn't appear to matter which order we approach this from, although
* perhaps that is the wrong thing to do.
*
* @param {Object} layoutItem Layout item.
* @return {Object|undefined} A colliding layout item, or undefined.
*/
export function getFirstCollision(layout, layoutItem) {
for (let i = 0, len = layout.length; i < len; i++) {
if (collides(layout[i], layoutItem)) { return layout[i]; }
}
}
export function getAllCollisions(layout, layoutItem) {
return layout.filter((l) => collides(l, layoutItem));
}
/**
* Get all static elements.
* @param {Array} layout Array of layout objects.
* @return {Array} Array of static layout items..
*/
export function getStatics(layout) {
// return [];
return layout.filter((l) => l.static);
}
/**
* Move an element. Responsible for doing cascading movements of other elements.
*
* @param {Array} layout Full layout to modify.
* @param {LayoutItem} l element to move.
* @param {Number} [x] X position in grid units.
* @param {Number} [y] Y position in grid units.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is
* being dragged/resized by th euser.
*/
export function moveElement(layout, l, x, y, isUserAction, preventCollision) {
if (l.static) { return layout; }
// Short-circuit if nothing to do.
// if (l.y === y && l.x === x) return layout;
const oldX = l.x;
const oldY = l.y;
const movingUp = y && l.y > y;
// This is quite a bit faster than extending the object
if (typeof x === 'number') { l.x = x; }
if (typeof y === 'number') { l.y = y; }
l.moved = true;
// If this collides with anything, move it.
// When doing this comparison, we have to sort the items we compare with
// to ensure, in the case of multiple collisions, that we're getting the
// nearest collision.
let sorted = sortLayoutItemsByRowCol(layout);
if (movingUp) { sorted = sorted.reverse(); }
const collisions = getAllCollisions(sorted, l);
if (preventCollision && collisions.length) {
l.x = oldX;
l.y = oldY;
l.moved = false;
return layout;
}
// Move each item that collides away from this element.
for (let i = 0, len = collisions.length; i < len; i++) {
const collision = collisions[i];
// console.log('resolving collision between', l.i, 'at', l.y, 'and', collision.i, 'at', collision.y);
// Short circuit so we can't infinite loop
if (collision.moved) { continue; }
// This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
if (l.y > collision.y && l.y - collision.y > collision.h / 4) { continue; }
// Don't move static items - we have to move *this* element away
if (collision.static) {
layout = moveElementAwayFromCollision(layout, collision, l, isUserAction);
} else {
layout = moveElementAwayFromCollision(layout, l, collision, isUserAction);
}
}
return layout;
}
/**
* This is where the magic needs to happen - given a collision, move an element away from the collision.
* We attempt to move it up if there's room, otherwise it goes below.
*
* @param {Array} layout Full layout to modify.
* @param {LayoutItem} collidesWith Layout item we're colliding with.
* @param {LayoutItem} itemToMove Layout item we're moving.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is being dragged/resized
* by the user.
*/
export function moveElementAwayFromCollision(layout, collidesWith,
itemToMove, isUserAction) {
const preventCollision = false; // we're already colliding
// If there is enough space above the collision to put this element, move it there.
// We only do this on the main collision as this can get funky in cascades and cause
// unwanted swapping behavior.
if (isUserAction) {
// Make a mock item so we don't modify the item here, only modify in moveElement.
const fakeItem = {
x: itemToMove.x,
y: itemToMove.y,
w: itemToMove.w,
h: itemToMove.h,
i: '-1'
};
fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0);
if (!getFirstCollision(layout, fakeItem)) {
return moveElement(layout, itemToMove, undefined, fakeItem.y, preventCollision);
}
}
// Previously this was optimized to move below the collision directly, but this can cause problems
// with cascading moves, as an item may actually leapflog a collision and cause a reversal in order.
return moveElement(layout, itemToMove, undefined, itemToMove.y + 1, preventCollision);
}
/**
* Helper to convert a number to a percentage string.
*
* @param {Number} num Any number
* @return {String} That number as a percentage.
*/
export function perc(num) {
return num * 100 + '%';
}
export function setTransform(top, left, width, height) {
// Replace unitless items with px
const translate = 'translate3d(' + left + 'px,' + top + 'px, 0)';
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Just like the setTransform method, but instead it will return a negative value of right.
*
* @param top
* @param right
* @param width
* @param height
* @returns {{transform, WebkitTransform, MozTransform, msTransform, OTransform, width, height, position}}
*/
export function setTransformRtl(top, right, width, height) {
// Replace unitless items with px
const translate = 'translate3d(' + right * -1 + 'px,' + top + 'px, 0)';
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
export function setTopLeft(top, left, width, height) {
return {
top: top + 'px',
left: left + 'px',
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Just like the setTopLeft method, but instead, it will return a right property instead of left.
*
* @param top
* @param right
* @param width
* @param height
* @returns {{top, right, width, height, position}}
*/
export function setTopRight(top, right, width, height) {
return {
top: top + 'px',
right: right + 'px',
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Get layout items sorted from top left to right and down.
*
* @return {Array} Array of layout objects.
* @return {Array} Layout, sorted static items first.
*/
export function sortLayoutItemsByRowCol(layout) {
return [].concat(layout).sort(function(a, b) {
if (a.y === b.y && a.x === b.x) {
return 0;
}
if (a.y > b.y || (a.y === b.y && a.x > b.x)) {
return 1;
}
return -1;
});
}
/**
* Generate a layout using the initialLayout and children as a template.
* Missing entries will be added, extraneous ones will be truncated.
*
* @param {Array} initialLayout Layout passed in through props.
* @param {String} breakpoint Current responsive breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout vertically.
* @return {Array} Working layout.
*/
/*
export function synchronizeLayoutWithChildren(initialLayout, children: Array<React.Element>|React.Element,
cols, verticalCompact) {
// ensure 'children' is always an array
if (!Array.isArray(children)) {
children = [children];
}
initialLayout = initialLayout || [];
// Generate one layout item per child.
let layout = [];
for (let i = 0, len = children.length; i < len; i++) {
let newItem;
const child = children[i];
// Don't overwrite if it already exists.
const exists = getLayoutItem(initialLayout, child.key || "1" /!* FIXME satisfies Flow *!/);
if (exists) {
newItem = exists;
} else {
const g = child.props._grid;
// Hey, this item has a _grid property, use it.
if (g) {
if (!isProduction) {
validateLayout([g], 'ReactGridLayout.children');
}
// Validated; add it to the layout. Bottom 'y' possible is the bottom of the layout.
// This allows you to do nice stuff like specify {y: Infinity}
if (verticalCompact) {
newItem = cloneLayoutItem({...g, y: Math.min(bottom(layout), g.y), i: child.key});
} else {
newItem = cloneLayoutItem({...g, y: g.y, i: child.key});
}
}
// Nothing provided: ensure this is added to the bottom
else {
newItem = cloneLayoutItem({w: 1, h: 1, x: 0, y: bottom(layout), i: child.key || "1"});
}
}
layout[i] = newItem;
}
// Correct the layout.
layout = correctBounds(layout, {cols: cols});
layout = compact(layout, verticalCompact);
return layout;
}
*/
/**
* Validate a layout. Throws errors.
*
* @param {Array} layout Array of layout items.
* @param {String} [contextName] Context name for errors.
* @throw {Error} Validation error.
*/
export function validateLayout(layout, contextName) {
contextName = contextName || 'Layout';
const subProps = ['x', 'y', 'w', 'h'];
if (!Array.isArray(layout)) { throw new Error(contextName + ' must be an array!'); }
for (let i = 0, len = layout.length; i < len; i++) {
const item = layout[i];
for (let j = 0; j < subProps.length; j++) {
if (typeof item[subProps[j]] !== 'number') {
throw new Error('VueGridLayout: ' + contextName + '[' + i + '].' + subProps[j] + ' must be a number!');
}
}
if (item.i && typeof item.i !== 'string') {
// number is also ok, so comment the error
// TODO confirm if commenting the line below doesn't cause unexpected problems
// throw new Error('VueGridLayout: ' + contextName + '[' + i + '].i must be a string!');
}
if (item.static !== undefined && typeof item.static !== 'boolean') {
throw new Error('VueGridLayout: ' + contextName + '[' + i + '].static must be a boolean!');
}
}
}
// Flow can't really figure this out, so we just use Object
export function autoBindHandlers(el, fns) {
fns.forEach((key) => el[key] = el[key].bind(el));
}
/**
* Convert a JS object to CSS string. Similar to React's output of CSS.
* @param obj
* @returns {string}
*/
export function createMarkup(obj) {
const keys = Object.keys(obj);
if (!keys.length) { return ''; }
let i, len = keys.length;
let result = '';
for (i = 0; i < len; i++) {
const key = keys[i];
const val = obj[key];
result += hyphenate(key) + ':' + addPx(key, val) + ';';
}
return result;
}
/* The following list is defined in React's core */
export let IS_UNITLESS = {
animationIterationCount: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
flexOrder: true,
gridRow: true,
gridColumn: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related properties
fillOpacity: true,
stopOpacity: true,
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
};
/**
* Will add px to the end of style values which are Numbers.
* @param name
* @param value
* @returns {*}
*/
export function addPx(name, value) {
if (typeof value === 'number' && !IS_UNITLESS[ name ]) {
return value + 'px';
} else {
return value;
}
}
/**
* Hyphenate a camelCase string.
*
* @param {String} str
* @return {String}
*/
export let hyphenateRE = /([a-z\d])([A-Z])/g;
export function hyphenate(str) {
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
export function findItemInArray(array, property, value) {
for (let i = 0; i < array.length; i++) {
if (array[i][property] == value) {
return true;
}
}
return false;
}
export function findAndRemove(array, property, value) {
array.forEach(function(result, index) {
if (result[property] === value) {
// Remove from array
array.splice(index, 1);
}
});
}
'use strict';
import App from '../../framework/app';
import createStore from '../store/index';
import createRouter from './router/index';
import entry from './view/home/index.vue';
export default new App({ entry, createStore, createRouter }).bootstrap();
\ No newline at end of file
import Vue from 'vue';
import VueRouter from 'vue-router';
import Activity from '../view/activity/index.vue';
Vue.use(VueRouter);
export default function createRouter() {
return new VueRouter({
mode: 'history',
routes: [
{
path: '/activity/:pageId',
component: Activity
}
]
});
}
import { kebabCase } from 'lodash';
import { Vue, Component, Watch } 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 DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { Getter, State, Mutation } from 'vuex-class';
@Component({ components: { FreedomContainer, GridLayout, GridItem, LoginForm, DownloadGuide }, name: 'Activity'})
export default class Activity extends Vue {
@Getter('pageData') pageData;
@State(state => state.editor.pageInfo.pageName) pageName;
@State(state => state.editor.gridLayout.rowHeight) rowHeight;
@Mutation('DEL_ELEMENTS') delPageInfo;
isLayoutComReady = false;
get layout() {
return this.pageData && this.pageData.elements.map(v => v.point) || [];
}
@Watch('pageName', { immediate: true })
onPageNameChange(newVal) {
if (EASY_ENV_IS_BROWSER && newVal) {
document.title = newVal;
// 如果是 iOS 设备,则使用如下 hack 的写法实现页面标题的更新
if (navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
const hackIframe = document.createElement('iframe');
hackIframe.style.display = 'none';
hackIframe.src = '/public/fixIosTitle.html?r=' + Math.random();
document.body.appendChild(hackIframe);
setTimeout(() => {
document.body.removeChild(hackIframe);
}, 300);
}
}
}
fetchApi(options) {
const { store, route } = options;
const { pageId } = route.params;
return store.dispatch('getPageDate', { pageId });
}
createStyle({h}) {
return EASY_ENV_IS_NODE ? {
height: `${h * this.rowHeight}px`,
} : {};
}
}
\ No newline at end of file
<template>
<div class="activity">
<grid-layout
:layout.sync="layout"
:isDraggable="false"
:isResizable="false"
:col-num="12"
:row-height="rowHeight"
:margin="[0, 0]"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :style="createStyle(item.point)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
: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>
</grid-item>
</grid-layout>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less">
html,
body,
#app {
height: 100%;
/deep/ .cr-popup {
.cr-notify {
font-size: 14px;
}
}
}
.activity {
width: 100%;
height: 100%;
min-height: 100%;
overflow-y: scroll;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
// transform: translateY(-10px);
transition-property: none;
.vue-grid-item {
transition-property: none;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
&>*:first-child {
height: 100%;
}
}
}
}
</style>
import { Vue, Component, Emit } from 'vue-property-decorator';
import Layout from 'component/layout/activity/index.vue';
@Component({
components: {
Layout
}
})
export default class Home extends Vue {}
\ No newline at end of file
import {Component, Vue, Prop, Watch} from 'vue-property-decorator'; import {Component, Vue, Prop, Watch} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class'; import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash'; import { reduce, ceil, subtract, divide } from 'lodash';
import Upload from '@editor/component/DynamicForm/component/Upload/index.vue';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import editorApi from '@api/editor.api';
import uuidv4 from 'uuid/v4'; import uuidv4 from 'uuid/v4';
import config from '@/config'; import Upload from '@editor/component/DynamicForm/component/Upload/index.vue';
import editorApi from '@api/editor.api';
import config from '@config/index';
import { getStyle } from '@service/utils.service';
@Component({ components: { Upload }, name: 'BasicPageForm' }) @Component({ components: { Upload }, name: 'BasicPageForm' })
export default class DynamicForm extends Vue { export default class DynamicForm extends Vue {
@Getter('pageInfo') pageData; @Getter('pageInfo') pageData;
@Getter('pageId') pageId;
@Prop(Boolean) value; @Prop(Boolean) value;
showPopup: boolean = false; showPopup: boolean = false;
formCustom: object = {}; loadingSave: boolean = false;
loadingPreview: boolean = false;
formCustom: PageInfo = {};
ruleCustom: object = { ruleCustom: object = {
pageName: [ pageName: [
{ required: true, message: '请输入页面名称', trigger: 'blur' } { required: true, message: '请输入页面名称', trigger: 'blur' }
...@@ -23,19 +25,26 @@ export default class DynamicForm extends Vue { ...@@ -23,19 +25,26 @@ export default class DynamicForm extends Vue {
@Watch('pageData', { immediate: true }) @Watch('pageData', { immediate: true })
onPageDataChange(newVal) { onPageDataChange(newVal) {
const { pageName, pageDescribe, coverImage, isPublish, isTemplate } = this.pageData; const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate } = this.pageData;
this.formCustom = { pageName, pageDescribe, coverImage, isPublish: !!isPublish, isTemplate: !!isTemplate }; this.formCustom = { pageName, pageDescribe, pageKeywords, coverImage, isPublish: !!isPublish, isTemplate: !!isTemplate };
} }
@Watch('value') @Watch('value')
onValueChange(val) { onValueChange(val) {
console.log('onValueChange', val); console.log('onValueChange', val);
this.showPopup = val; this.showPopup = val;
this.loadingSave = false;
this.loadingPreview = false;
} }
handleSubmit(type) { handleSubmit(type) {
this.$refs.formCustom.validate(async (valid) => { this.$refs.formCustom.validate(async (valid) => {
if (valid) { if (valid) {
if (type === 'save') {
this.loadingSave = true;
} else if (type === 'preview') {
this.loadingPreview = true;
}
await this.createCoverImage(); await this.createCoverImage();
this.$emit('submit', type, { ...this.formCustom, isPublish: this.formCustom.isPublish ? 1 : 0, isTemplate: this.formCustom.isTemplate ? 1 : 0 }); this.$emit('submit', type, { ...this.formCustom, isPublish: this.formCustom.isPublish ? 1 : 0, isTemplate: this.formCustom.isTemplate ? 1 : 0 });
} }
...@@ -46,13 +55,48 @@ export default class DynamicForm extends Vue { ...@@ -46,13 +55,48 @@ export default class DynamicForm extends Vue {
this.$emit('input', val); this.$emit('input', val);
} }
preview() {
this.formCustom.pageName = this.formCustom.pageName || '未命名';
this.handleSubmit('preview');
}
autoSave() {
this.formCustom.pageName = this.formCustom.pageName || '未命名';
this.handleSubmit('autoSave');
}
changeStyle(type) {
const gridEle = document.querySelector('.vue-grid-layout');
gridEle.childNodes.forEach(v => {
if (v.classList.contains('vue-grid-item') && !v.classList.contains('vue-grid-placeholder')) {
if (type === 'forward') {
const transform = getStyle(v, 'transform');
const transformY = transform.split('(')[1].split(')')[0].split(',')[5];
v.style.transform = 'none';
v.style.top = `${transformY}px`;
} else if (type === 'backward') {
const top = getStyle(v, 'top');
v.style.transform = `translate3d(0px, ${top}px, 0px)`;
v.style.top = 'auto';
}
}
});
}
async createCoverImage() { async createCoverImage() {
try {
const imgName = btoa(`coverImage-${uuidv4().substr(0, 8)}`); const imgName = btoa(`coverImage-${uuidv4().substr(0, 8)}`);
const gridEle = document.querySelector('.Dcm-container-panel'); const gridEle = document.querySelector('.Dcm-container-panel');
const canvas = await html2canvas(gridEle as HTMLElement, { useCORS: true }); this.changeStyle('forward');
const canvas = await html2canvas(gridEle as HTMLElement, { useCORS: true, allowTaint: true });
this.changeStyle('backward');
const base64 = canvas.toDataURL(); const base64 = canvas.toDataURL();
const { data: { uptoken } } = await editorApi.getUpToken(); const { data: { uptoken } } = await editorApi.getUpToken();
const { data: { key } } = await editorApi.uploadBase64(base64.split(',')[1], imgName, `UpToken ${uptoken}`); const { data: { key } } = await editorApi.uploadBase64(base64.split(',')[1], imgName, `UpToken ${uptoken}`);
this.formCustom.coverImage = config.qiniuHost + key; this.formCustom.coverImage = config.qiniuHost + key;
} catch (e) {
console.log(e);
}
} }
} }
\ No newline at end of file
<template> <template>
<Modal v-model="showPopup" width="380" @on-visible-change="change" class-name='basic-form'> <Modal v-model="showPopup" width="380" @on-visible-change="change" class-name='basic-form'>
<Form ref="formCustom" :model="formCustom" :rules="ruleCustom" :label-width="80" label-position="left"> <Form @submit.native.prevent ref="formCustom" :model="formCustom" :rules="ruleCustom" :label-width="80" label-position="left">
<FormItem label="页面名称" prop="pageName"> <FormItem label="页面名称" prop="pageName">
<Input v-model="formCustom.pageName" placeholder="请输入页面名称"></Input> <Input v-model="formCustom.pageName" placeholder="请输入页面名称"></Input>
</FormItem> </FormItem>
<FormItem label="页面描述" prop="pageDescribe"> <FormItem label="页面描述" prop="pageDescribe">
<Input v-model="formCustom.pageDescribe" type="textarea" placeholder="请输入页面描述" :rows="3"></Input> <Input v-model="formCustom.pageDescribe" type="textarea" placeholder="请输入页面描述" :rows="3"></Input>
</FormItem> </FormItem>
<FormItem label="页面关键字" prop="pageKeywords">
<Input v-model="formCustom.pageKeywords" type="textarea" placeholder="请输入页面关键字" :rows="3"></Input>
</FormItem>
<FormItem label="是否发布" prop="isPublish"> <FormItem label="是否发布" prop="isPublish">
<Checkbox v-model="formCustom.isPublish"></Checkbox> <i-switch v-model="formCustom.isPublish"></i-switch>
</FormItem> </FormItem>
<FormItem label="是否模板" prop="isTemplate" v-if="formCustom.isPublish"> <FormItem label="设为模板" prop="isTemplate" v-if="formCustom.isPublish">
<Checkbox v-model="formCustom.isTemplate"></Checkbox> <i-switch v-model="formCustom.isTemplate"></i-switch>
</FormItem> </FormItem>
</Form> </Form>
<div slot="footer"> <div slot="footer">
<Button type="success" @click="handleSubmit('save')">保存</Button> <Button type="success" :loading="loadingSave" @click="handleSubmit('save')">保存</Button>
<Button type="info" @click="handleSubmit('preview')">保存并预览</Button> <Button type="info" :loading="loadingPreview" @click="handleSubmit('preview')">保存并预览</Button>
</div> </div>
</Modal> </Modal>
</template> </template>
......
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'; import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
import FreedomContainer from '../../component/FreedomContainer/index.vue'; import FreedomContainer from '../../component/FreedomContainer/index.vue';
import { kebabCase, chunk, flatten } from 'lodash'; import { kebabCase, chunk, flatten, cloneDeep } from 'lodash';
import { State } from 'vuex-class'; import { State } from 'vuex-class';
import { v4 as uuid } from 'uuid';
@Component({ name: 'DynamicComponent' }) @Component({ name: 'DynamicComponent' })
export default class DynamicComponent extends Vue { export default class DynamicComponent extends Vue {
@State(state => state.editor.gridLayout.colNum) colNum; @State(state => state.editor.gridLayout.colNum) colNum;
@Prop({ default: () => ([]), type: Array }) data; @Prop({ default: () => ([]), type: Array }) data;
@Prop(String) type;
eleConfig: array = []; eleConfig: object[] = [];
eleConfigMap: object = {};
activeName: string = '';
@Watch('data', { immediate: true }) @Watch('data', { immediate: true })
onDataChange(newVal) { onDataChange(newVal) {
if (this.type === 'business') {
this.eleConfigMap = newVal.reduce((pre, cur, index) => {
if (index === 0) { this.activeName = cur.name; }
const children = chunk(cur.children, 2);
pre[cur.name] = { ...cur, children};
return pre;
}, {});
} else {
this.eleConfig = chunk(newVal, 2); this.eleConfig = chunk(newVal, 2);
} }
}
dragstart(event, eleName) { dragstart(event, eleName) {
this.$emit('dragstart'); this.$emit('dragstart');
const eleConfig = flatten(this.eleConfig).find(config => config.eleName === eleName); const id = uuid().slice(19);
// const props = this.getProps(eleName); const { compontObj, page } = this.genrateCom(eleName);
if (eleName.includes('template')) { if (eleName.includes('template')) {
event.dataTransfer.setData('text', JSON.stringify({ event.dataTransfer.setData('text', JSON.stringify({
template: eleConfig.page template: 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 { } else {
event.dataTransfer.setData('text', JSON.stringify({ event.dataTransfer.setData('text', JSON.stringify(compontObj));
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.effectAllowed = 'copyMove'; event.dataTransfer.effectAllowed = 'copyMove';
} }
dragend() { dragend() {
this.$emit('dragend'); this.$emit('dragend');
console.log('dragend'); }
genrateCom(eleName) {
const bussinessEleConfig = Object.values(this.eleConfigMap).reduce((pre, cur) => pre.concat(cur.children), []);
const rawEleConfig = this.type === 'business' ? bussinessEleConfig : this.eleConfig;
const eleConfig = flatten(rawEleConfig).find(cfg => cfg.eleName === eleName);
const { title, config, value, commonStyle, page } = cloneDeep(eleConfig);
const id = uuid().slice(19);
const pointH = eleName === 'freedom-container' ? 360 : 105;
return {
compontObj: {
id,
name: eleName,
title,
point: {x: 0, y: 0, w: this.colNum, h: pointH, i: id},
schame: config,
props: {...value},
commonStyle
},
page
};
}
addEle(eleName) {
// console.log('addEle', eleName);
const { compontObj, page } = this.genrateCom(eleName);
if (eleName.includes('template')) {
this.$emit('addEle', { template: page });
} else if (eleName === 'freedom-container' || eleName === 'goods-tabs') {
this.$emit('addEle', { ...compontObj, child: [] });
} else {
this.$emit('addEle', compontObj);
}
} }
// getProps(eleName) { // getProps(eleName) {
......
<template> <template>
<div class="dynamic"> <div class="dynamic">
<template v-if="type === 'business'">
<Tabs size="small">
<TabPane :label="item.title" v-for="(item, key) in eleConfigMap" :key="key">
<Row v-for="(arr, index) in item.children" :key="index" class="dynamic-row" type="flex" justify="space-between">
<Col span="11" v-for="config in arr" :key="config.eleName+index">
<Card @dblclick.native="addEle(config.eleName)" draggable="true" @dragend.native="dragend" @dragstart.native="dragstart($event, config.eleName)">
<p slot="title">{{ config.title }}</p>
<div class="dynamic-row-card">
<img
:src="config.coverImage || 'http://activitystatic.q-gp.com/low_code.jpg'"
/>
</div>
<span class="dynamic-row-popup">拖拽或双击</span>
</Card>
</Col>
</Row>
</TabPane>
</Tabs>
</template>
<template v-else>
<Row v-for="(arr, index) in eleConfig" :key="index" class="dynamic-row" type="flex" justify="space-between"> <Row v-for="(arr, index) in eleConfig" :key="index" class="dynamic-row" type="flex" justify="space-between">
<Col span="10" v-for="config in arr" :key="config.eleName+index"> <Col span="11" v-for="config in arr" :key="config.eleName+index">
<Card draggable="true" @dragend.native="dragend" @dragstart.native="dragstart($event, config.eleName)"> <Card @dblclick.native="addEle(config.eleName)" draggable="true" @dragend.native="dragend" @dragstart.native="dragstart($event, config.eleName)">
<p slot="title">{{ config.title }}</p> <p slot="title">{{ config.title }}</p>
<div class="dynamic-row-card"> <div class="dynamic-row-card">
<img <img
src="http://activitystatic.q-gp.com/low_code.jpg" :src="config.coverImage || 'http://activitystatic.q-gp.com/low_code.jpg'"
/> />
</div> </div>
<span class="dynamic-row-popup">拖拽或双击</span>
</Card> </Card>
</Col> </Col>
</Row> </Row>
</template>
</div> </div>
</template> </template>
<script lang="ts" src="./index.ts"></script> <script lang="ts" src="./index.ts"></script>
<style lang="less"> <style lang="less" scoped>
.dynamic { .dynamic {
margin-top: 10px;
&-row { &-row {
margin-bottom: 10px; margin-bottom: 8px;
p { p {
text-align: center; text-align: left;
} }
&-card { &-card {
...@@ -34,15 +55,45 @@ ...@@ -34,15 +55,45 @@
} }
} }
/deep/ .ivu-card { /deep/.ivu-card {
position: relative;
overflow: hidden;
&-head { &-head {
padding: 5px; padding: 5px;
} }
&-body { &-body {
padding: 10px; padding: 0;
} }
.dynamic-row-popup {
position: absolute;
bottom: 0;
left: 0;
height: 16px;
width: 100%;
line-height: 16px;
font-size: 12px;
background: #2E8CF0;
text-align: center;
color: #fff;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
&:hover {
box-shadow: 0 0 14px 0 rgba(46,140,240,0.20);
border: 1px solid #2E8CF0 !important;
.dynamic-row-popup {
transform: translateY(0);
} }
} }
} }
}
/deep/ .ivu-tabs-tab {
font-size: 14px;
}
}
</style> </style>
<template> <template>
<div class="color-selector"> <div class="color-selector">
<Input class="color-selector-input" v-model="color" placeholder="请输入"></Input> <Input class="color-selector-input" v-model="color" placeholder="请输入" @input="change"></Input>
<ColorPicker v-model="color" /> <ColorPicker placement="bottom-end" v-model="color" @on-active-change="change($event)" @on-change="change($event)" />
</div> </div>
</template> </template>
<script> <script>
...@@ -14,8 +14,16 @@ ...@@ -14,8 +14,16 @@
color: this.value, color: this.value,
} }
}, },
created() {
console.log(this.color);
},
watch: { watch: {
color(val) { value(val) {
this.color = val;
}
},
methods: {
change(val) {
console.log('color', val); console.log('color', val);
this.$emit('input', val); this.$emit('input', val);
} }
...@@ -28,7 +36,8 @@ ...@@ -28,7 +36,8 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
&-input { &-input {
flex-basis: 150px; width: 100px !important;
flex-basis: 100px !important;
} }
} }
</style> </style>
\ No newline at end of file
<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" scoped>
</style>
\ No newline at end of file
const couponTypeList: object[] = [
{
id: 1,
name: '满减券',
},
{
id: 2,
name: '运费券',
},
{
id: 21,
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: '领取方式',
formType: 'select',
key: 'receiverType',
valueEnum: receiverTypeList.reduce((pre, cur) => {
pre[cur.id] = cur.name;
return pre;
}, {}),
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;
coupon: object[] = cloneDeep(this.value);
templates: object[] = [];
table: object[] = [
{
title: '选择优惠券',
type: 'coupon',
multiple: true,
columns: couponColumn,
query: this.query
}
];
@Watch('coupon')
onFormChange(newVal) {
this.$emit('input', newVal);
}
async query(data) {
// receiverType 领取方式 1:主动领取 2:自动发放 3:不限
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.validHours
? `自领取后有效${item.validHours}小时`
: `自领取${item.receiverDaysValid}天后生效,有效天数${item.validDays}天`;
// item.useTime = item.useTimeStart ? `${item.useTimeStart}-${item.useTimeEnd}` : `自领取${item.receiverDaysValid}天后生效,有效天数${item.validDays}天`;
const selections = this.templates.length ? this.templates : this.coupon;
console.log('query', selections, this.templates, this.coupon);
if (selections.some(v => v === item.id)) {
item._checked = true;
}
return item;
});
return { data: couponInfoList || [], total: res?.pageInfo?.totalCount };
}
}
\ No newline at end of file
<template>
<table-modal :table="table" :templates.sync="templates" :formControl="formControl" v-model="coupon" 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 @submit.native.prevent :model="item" :label-width="50">
<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;
}
}
&:hover {
box-shadow: 0 0 14px 0 rgba(46,140,240,0.20);
border: 1px solid #2E8CF0 !important;
}
}
h3 {
padding-bottom: 10px;
text-align: center;
}
&-card {
/deep/ .ivu-card-head{
background: #f0f2f5;
}
/deep/ .ivu-card-body {
padding: 10px !important;
}
.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;
templates: object = {};
goods: object = cloneDeep(this.value);
table: object[] = [
{
title: '选择商品',
type: 'goods',
key: 'skuNo',
multiple: true,
columns: goodsColumn.call(this),
query: this.query
},
{
title: '选择商品组',
type: 'goodsGroup',
key: 'id',
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 });
const selections = this.templates?.ids ? this.templates.ids : this.goods.ids;
console.log('query', selections, this.templates.ids);
records?.forEach(record => {
if (selections.some(v => v === record.skuNo)) {
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);
const selections = this.templates?.ids ? this.templates.ids : this.goods.ids;
console.log('query', selections, this.templates.ids);
records?.forEach(record => {
if (selections.some(v => v === record.id)) {
record._checked = true;
}
});
return { data: records || [], total };
}
}
\ No newline at end of file
<template> <template>
<Layout> <table-modal :table="table" :templates.sync="templates" :formControl="formControl" v-model="goods" title="商品"></table-modal>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</Layout>
</template> </template>
<script lang="ts" src="./index.ts"></script> <script lang="ts" src="./index.ts"></script>
<style></style>
\ No newline at end of file
import {Component, Prop, Watch, Vue, Mixins, Emit} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { cloneDeep, uniqBy } 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;
@Prop([Object, Array]) templates;
form: object = {};
modal: boolean = false;
selections: object[] = [];
activeName: number = 0;
isSelected: boolean = false;
get idsLength() {
return this.getLength('value');
}
getTableData() {
const tableData = this.$refs?.[`qgTable${this.activeName}`]?.[0]?.tableData || [];
console.log('get tableData', tableData);
return this.isSelected ? tableData : [];
}
changePageNo() {
this.isSelected = false;
}
// 获取除本页之外的默认值
getDefaultIds() {
const tableData = this.getTableData();
let defaultIds = this.value;
if (validateType(this.value) === 'object') {
defaultIds = this.value.type === this.table[this.activeName].type ? this.value?.ids : [];
}
let filterIds = [];
if (this.table.length > 1) {
filterIds = tableData.map(v => v[this.table[this.activeName].key]);
} else {
filterIds = tableData.map(v => v.id);
}
const rs = this.isSelected ? defaultIds.filter(v => !filterIds.includes(v)) : defaultIds;
return rs;
}
// 获取本页的选中值
getSelectionsIds() {
if (this.table.length > 1) {
return this.selections.map(v => v[this.table[this.activeName].key]);
} else {
return this.selections.map(v => v.id);
}
}
// 获取本页之外的临时值
getTemplateIds() {
let filterIds = [];
let templates = [];
const tableData = this.getTableData();
if (this.table.length > 1) {
filterIds = tableData.map(v => v[this.table[this.activeName].key]);
templates = this.templates.ids;
} else {
filterIds = tableData.map(v => v.id);
templates = this.templates;
}
const rs = this.isSelected ? templates.filter(v => !filterIds.includes(v)) : templates;
return rs;
}
getLength(type) {
const rs = type === 'templates' ? this.templates : this.value;
if (validateType(this.templates) === 'object') {
return rs?.ids?.length;
}
return rs?.length;
}
// 当table取消全选时的默认值
getDefaultSelections() {
let rs = [];
if (this.table.length > 1) {
const key = this.table[this.activeName].key;
rs = [{ [key]: -1 }];
} else {
rs = [{ id: -1 }];
}
return rs;
}
setTemplates(ids = []) {
let rs = null;
if (this.table.length > 1) {
rs = {
type: this.table[this.activeName].type,
ids
};
} else {
rs = ids;
}
return rs;
}
@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) {
let parent = this.$parent;
while (!parent.modProps) {
parent = parent.$parent;
}
parent.modProps(this.form, 'component');
}
@Watch('value', { immediate: true })
onValueChange(newVal) {
if (newVal && this.table.length > 1) {
this.activeName = this.table.findIndex(v => v.type === newVal.type);
}
}
@Emit('update:templates')
selectionChange(selection) {
if (!this.table[this.activeName]?.multiple && selection.length > 1) {
this.$Notice.warning({
title: '商品组只能单选'
});
return this.templates;
}
this.isSelected = true;
this.selections = selection.length ? selection : this.getDefaultSelections();
const ids = Array.from(new Set([...this.getSelectionsIds(), ...(this.getLength('templates') ? this.getTemplateIds() : this.getDefaultIds())]));
console.log('selectionChange', ids, selection);
const rs = this.setTemplates(ids);
return rs;
}
add() {
this.modal = true;
}
ok() {
let ids = [...this.getSelectionsIds(), ...(this.getLength('templates') ? this.getTemplateIds() : this.getDefaultIds())];
// ids为空时表示未选择
if (!ids.length) { return; }
// 当前页selections为空
ids = ids.filter(v => v !== -1);
this.selections = [];
this.isSelected = false;
// console.log('commit', selections);
if (this.table.length > 1) {
this.$emit('update:templates', {});
this.$emit('input', {
type: this.table[this.activeName].type,
ids: Array.from(new Set(ids))
});
} else {
this.$emit('update:templates', this.setTemplates());
this.$emit('input', Array.from(new Set(ids)));
}
}
@Emit('update:templates')
cancel() {
this.selections = [];
this.isSelected = false;
let rs = null;
if (this.table.length > 1) {
rs = {};
} else {
rs = [];
}
return rs;
}
async menuChange(name) {
// console.log('menuChange', `qgTable${name}`, this.$refs);
await this.$refs[`qgTable${name}`][0].query();
this.activeName = name;
this.$emit('update:templates', this.setTemplates());
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 @submit.native.prevent :model="form" :label-width="90">
<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${index}`"
:columns="item.columns"
:request="item.query"
:hideAdd="true"
:height="500"
@on-selection-change="selectionChange"
@on-page-change="changePageNo"
>
</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;
justify-content: center;
}
}
}
}
</style>
\ No newline at end of file
<template> <template>
<div>
<div class="upload"> <div class="upload">
<div class="upload-img" v-if="value"> <div class="upload-img" v-if="img">
<img :src="value"> <img :src="img">
<div class="upload-img-cover"> <div class="upload-img-cover">
<Icon type="ios-trash-outline" @click.native="handleRemove()"></Icon> <Icon type="ios-trash-outline" @click.native="handleRemove()"></Icon>
</div> </div>
</div> </div>
<div id="upload"> <div :id="containerId" class="upload-container">
<div id="upload_pic"> <Progress v-if="percent" vertical :percent="percent"></Progress>
<div :id="id">
<Icon type="camera" size="20"></Icon> <Icon type="camera" size="20"></Icon>
</div> </div>
</div> </div>
</div> </div>
<Input v-model="img"></Input>
</div>
</template> </template>
<script> <script>
import '@/service/qiniu.service'; import '@/service/qiniu.service';
import config from '@/config'; import config from '@/config';
import uuidv4 from 'uuid/v4';
// const id = uuidv4().substr(0, 8);
export default { export default {
props: { props: {
value: String, value: String,
}, },
data() {
return {
id: uuidv4().substr(0, 8),
percent: 0,
img: this.value
}
},
methods: { methods: {
handleRemove () { handleRemove () {
this.$emit('input', ''); this.img = '';
this.$emit('on-change'); this.$emit('input', this.img);
this.$emit('change', this.img);
}, },
uploadQiniu() { uploadQiniu() {
var uploader = Qiniu.uploader({ var uploader = Qiniu.uploader({
runtimes: 'html5', // 上传模式,依次退化 runtimes: 'html5', // 上传模式,依次退化
browse_button: 'upload_pic', // 上传选择的点选按钮,**必需** browse_button: this.id, // 上传选择的点选按钮,**必需**
uptoken_url: 'https://opapi.xyqb.com/upload/getToken', uptoken_url: 'https://opapi.xyqb.com/upload/getToken',
// uptoken_url: `${config.apiHost}/upload/getToken`, //Ajax请求upToken的Url,**强烈建议设置**(服务端提供) // uptoken_url: `${config.apiHost}/upload/getToken`, //Ajax请求upToken的Url,**强烈建议设置**(服务端提供)
save_key: true, // 默认 false。若在服务端生成uptoken的上传策略中指定了 `sava_key`,则开启,SDK会忽略对key的处理 save_key: true, // 默认 false。若在服务端生成uptoken的上传策略中指定了 `sava_key`,则开启,SDK会忽略对key的处理
domain: config.qiniuHost, // bucket 域名,下载资源时用到,**必需** domain: config.qiniuHost, // bucket 域名,下载资源时用到,**必需**
get_new_uptoken: false, // 设置上传文件的时候是否每次都重新获取新的token get_new_uptoken: false, // 设置上传文件的时候是否每次都重新获取新的token
container: 'upload', // 上传区域DOM ID,默认是browser_button的父元素, container: this.containerId, // 上传区域DOM ID,默认是browser_button的父元素,
max_file_size: '10mb', // 最大文件体积限制 max_file_size: '10mb', // 最大文件体积限制
max_retries: 3, // 上传失败最大重试次数 max_retries: 3, // 上传失败最大重试次数
chunk_size: '4mb', // 分块上传时,每片的体积 chunk_size: '4mb', // 分块上传时,每片的体积
auto_start: true, // 选择文件后自动上传,若关闭需要自己绑定事件触发上传 auto_start: true, // 选择文件后自动上传,若关闭需要自己绑定事件触发上传
init: { init: {
FileUploaded: (up, file, info) => { FileUploaded: (up, file, info) => {
console.log(this); // console.log(this);
this.percent = 0;
var res = JSON.parse(info); var res = JSON.parse(info);
if (res.businessCode === '0001') { if (res.businessCode === '0001') {
this.$Notice.error({ this.$Notice.error({
...@@ -54,24 +70,54 @@ ...@@ -54,24 +70,54 @@
title: '上传成功', title: '上传成功',
desc: '', desc: '',
}); });
this.$emit('input', config.qiniuHost + res.hash); this.img = config.qiniuHost + res.hash;
this.$emit('on-change', config.qiniuHost + res.hash); this.$emit('input', this.img);
this.$emit('change', this.img);
// this.url = config.qiniuHost + res.hash; // this.url = config.qiniuHost + res.hash;
} }
}, },
BeforeUpload: (up, file) => {
// var chunk_size = plupload.parseSize(this.getOption('chunk_size'));
this.percent = 0;
},
UploadProgress: (up, file) => {
// var chunk_size = plupload.parseSize(this.getOption('chunk_size'));
this.percent = file.percent;
console.log('UploadProgress', file.percent);
},
Error: (up, err, errTip) => {
this.percent = 0;
}
}, },
}); });
}, },
}, },
computed: {
containerId() {
return `container_${this.id}`;
}
},
watch: {
value: {
handler(newVal) {
this.img = newVal;
},
immediate: true
}
},
mounted () { mounted () {
this.uploadQiniu(); this.uploadQiniu();
} }
} }
</script> </script>
<style lang="less" scoped=""> <style lang="less" scoped>
.upload { .upload {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10px;
width: 100%;
overflow: hidden;
&-img { &-img {
display: inline-block; display: inline-block;
width: 60px; width: 60px;
...@@ -111,15 +157,20 @@ ...@@ -111,15 +157,20 @@
} }
} }
#upload { &-container {
text-align: center; text-align: center;
border: 1px dashed; border: 1px dashed;
width: 56px; width: 56px;
height:56px; height: 56px;
line-height: 56px; line-height: 56px;
#upload_pic { #upload_pic {
} }
/deep/ .ivu-progress {
box-sizing: content-box;
height: 50px;
line-height: 50px;
}
} }
} }
</style> </style>
\ No newline at end of file
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 { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash'; import { reduce, ceil, subtract, divide, pick, cloneDeep, pickBy } from 'lodash';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin'; import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import DynamicFormMixin from './component/mixins/dynamicForm.mixin';
import Upload from './component/Upload/index.vue'; import Upload from './component/Upload/index.vue';
import ColorSelector from './component/ColorSelector/index.vue'; import ColorSelector from './component/ColorSelector/index.vue';
import BaseSelect from './component/BaseSelect/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 Textarea from './component/Textarea/index.vue';
import Number from './component/Number/index.vue'; import Number from './component/Number/index.vue';
import ColumnSelector from './component/ColumnSelector/index.vue';
import { resizeDiv, getStyle } from '@/service/utils.service'; import { resizeDiv, getStyle } from '@/service/utils.service';
import EventBus from '@service/eventBus.service';
@Component({ components: { Upload, ColorSelector, BaseSelect, Textarea, Number }, name: 'DynamicForm' }) @Component({ components: { Upload, ColorSelector, BaseSelect, FormList, Textarea, Number, ComponentSelect, GoodsTableModal, CouponTableModal, ColumnSelector }, name: 'DynamicForm' })
export default class DynamicForm extends Mixins(ContextMenu) { export default class DynamicForm extends Mixins(ContextMenuMixin, DynamicFormMixin) {
@State(state => state.editor.curEleIndex) curEleIndex; @State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex; @State(state => state.editor.curChildIndex) curChildIndex;
@Getter('pageData') pageData; @Getter('pageData') pageData;
form: object = {}; form: object = {};
styleSchame: object = {
curEle: [
{
label: '定位',
list: [
{
content: '上对齐',
icon: 'arrow-up-c',
args: 'top'
},
{
content: '右对齐',
icon: 'arrow-right-c',
args: 'right'
},
{
content: '下对齐',
icon: 'arrow-down-c',
args: 'bottom'
},
{
content: '左对齐',
icon: 'arrow-left-c',
args: 'left'
},
{
content: '垂直居中',
icon: 'android-film',
args: 'vertical'
},
{
content: '水平居中',
icon: 'android-film',
args: 'horizontal'
},
]
},
{
label: '位置'
},
{
label: '尺寸',
list: [
{
content: '全屏',
icon: 'arrow-resize',
args: 'full'
},
{
content: '宽100%',
icon: 'arrow-swap',
args: 'width'
},
{
content: '高100%',
icon: 'arrow-swap',
args: 'height'
},
]
},
{
label: '宽高'
},
{
label: '背景图片'
},
{
label: '背景颜色'
}
],
curChild: [
{
label: '容器尺寸',
list: [
{
content: '全屏',
icon: 'arrow-resize',
args: [667, 375],
},
{
content: '根据背景图片调整宽高',
icon: 'image',
args: [667, 375, 'image'],
},
{
content: '根据元素默认尺寸调整宽高',
icon: 'stop',
args: ['', '', 'element'],
},
{
content: '宽100%',
icon: 'arrow-swap',
args: [null, 375]
},
{
content: '高100%',
icon: 'arrow-swap',
args: [667, null]
}
]
},
{
label: '容器宽高'
},
{
label: '背景图片'
},
{
label: '背景颜色'
}
]
};
get curElement() { get curElement() {
return this.getCurElement();
}
get point() {
return this.curEleIndex || this.curEleIndex === 0 ? cloneDeep(this.pageData.elements[this.curEleIndex]?.point) : { h: 0, w: 0 };
}
get commonStyle() {
let rs = { backgroundColor: '', backgroundImage: '' };
if (this.curEleIndex || this.curEleIndex === 0) {
if (this.curChildIndex || this.curChildIndex === 0) {
rs = cloneDeep({ ...rs, ...this.pageData.elements[this.curEleIndex].child[this.curChildIndex].commonStyle });
} else {
rs = cloneDeep({ ...rs, ...this.pageData.elements[this.curEleIndex].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;
}
get isSelected() {
return this.curChildIndex || this.curChildIndex === 0 || this.curEleIndex || this.curEleIndex === 0;
}
get childSelected() {
return this.curChildIndex || this.curChildIndex === 0;
}
get parentSelected() {
return (this.curEleIndex || this.curEleIndex === 0) && !this.curChildIndex && this.curChildIndex !== 0;
}
@Watch('curEleIndex', { immediate: true })
onCurEleIndexChange(newVal) {
this.setForm(newVal, 'curEleIndex');
}
@Watch('curChildIndex', { immediate: true })
onCurChildIndexChange(newVal) {
this.setForm(newVal, 'curChildIndex');
}
getCurElement() {
let element = {}; let element = {};
if (this.curEleIndex !== null) { if (this.curEleIndex !== null) {
if (this.curChildIndex !== null && this.pageData.elements[this.curEleIndex]) { if (this.curChildIndex !== null && this.pageData.elements[this.curEleIndex]) {
...@@ -29,50 +217,77 @@ export default class DynamicForm extends Mixins(ContextMenu) { ...@@ -29,50 +217,77 @@ export default class DynamicForm extends Mixins(ContextMenu) {
return element; return element;
} }
get point() { setForm(newVal, type) {
return this.curEleIndex || this.curEleIndex === 0 ? this.pageData.elements[this.curEleIndex]?.point : { h: 0, w: 0 }; this.form = { id: `${type}${newVal}` };
const element = this.getCurElement();
element?.schame?.forEach(schame => {
if (schame.children) {
schame.children.forEach(child => {
this.$set(this.form, child.key, element.props[child.key]);
});
} else {
this.$set(this.form, schame.key, element.props[schame.key]);
} }
});
get commonStyle() { // console.log('setForm', this.form);
return (this.curEleIndex || this.curEleIndex === 0) && (this.curChildIndex || this.curChildIndex === 0) ? this.pageData.elements[this.curEleIndex].child[this.curChildIndex].commonStyle : { h: 0, w: 0 };
} }
@Watch('curElement', { immediate: true, deep: true }) // 监听form变化, 更新pageData
onElementChange(newVal) { @Watch('form', { immediate: true, deep: true })
newVal?.schame?.forEach(schame => { onFormChange(newVal, oldVal) {
this.$set(this.form, schame.key, newVal.props[schame.key]); // console.log('onFormChange', newVal?.id, oldVal?.id);
if (newVal?.id !== oldVal?.id) { return; }
const params = pick(this.form, this.curFormKey);
this.$nextTick(() => {
const element = this.pageData.elements[this.curEleIndex];
// swiper refresh 后会自动调整
if (element.name !== 'cs-guide-cube') {
this.adjustHeight();
}
}); });
this.$emit('modProps', params, 'component');
} }
@Watch('form', { immediate: true, deep: true }) updatePoint(value, key) {
onFormChange(newVal) { // console.log('updatePoint', this.point);
this.$emit('modProps', this.form); const elements = this.pageData.elements[this.curEleIndex];
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, ...this.point } } });
} }
get eleName() { updateStyle(value, key) {
let result = ''; // console.log('updateCommonStyle', this.commonStyle);
if (!this.curChildIndex && this.curChildIndex !== 0) { const elements = this.pageData.elements[this.curEleIndex];
result = this.pageData?.elements[this.curEleIndex]?.name ?? ''; // 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) { resizedEvent(h, w, responsive) {
const elements = this.pageData.elements[this.curEleIndex]; const elements = this.pageData.elements[this.curEleIndex];
if (responsive) { if (responsive) {
resizeDiv(this.form.backgroundImage, 667, 375, (imgHeight) => { if (responsive === 'image' && elements.commonStyle.backgroundImage) {
// if (elements.name === 'freedom-container') {
resizeDiv(elements.commonStyle.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 } } }); this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: imgHeight ?? elements.point.h, responsive: true } } });
}); });
// }
} else if (responsive === 'element' && elements.id) {
this.adjustHeight();
}
} else { } else {
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: h ?? elements.point.h } } }); this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: h ?? elements.point.h } } });
} }
} }
resizedChildEvent(type) {
this.$emit('resizedChildEvent', type);
// const containerEle = this.$refs.freedomContainer[this.curEleIndex];
}
changeAlignType(type) { changeAlignType(type) {
const freedomBody = document.querySelector('.freedom-body'); const freedomBody = document.querySelector('.freedom-body');
const curElement = (freedomBody as Element).children[this.curChildIndex]; const curElement = (freedomBody as Element).children[this.curChildIndex];
...@@ -93,36 +308,10 @@ export default class DynamicForm extends Mixins(ContextMenu) { ...@@ -93,36 +308,10 @@ export default class DynamicForm extends Mixins(ContextMenu) {
top = ceil(divide(subtract(containerH, eleH), 2), 2); break; top = ceil(divide(subtract(containerH, eleH), 2), 2); break;
case 'horizontal': case 'horizontal':
left = ceil(divide(subtract(containerW, eleW), 2), 2); break; left = ceil(divide(subtract(containerW, eleW), 2), 2); break;
case 'full': case 'width': case 'height':
EventBus.$emit('resizedChildEvent', type); break;
default: break; default: break;
} }
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...elements, commonStyle: { ...elements.commonStyle, left, top } } }); 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
<template> <template>
<div class="dynamic-form"> <div class="dynamic-form" v-if="isSelected">
<h2>{{curElement.title}}</h2> <div class="dynamic-form-header">
<template> <h2>{{ curElement.title }}</h2>
<h4>组件属性</h4> <div>{{ `组件ID: ${curElement.id}` }}</div>
<Form class="dynamic-form-component" :label-width="80" :model="form"> </div>
<FormItem :label="item.name" :key="index" v-for="(item, index) in curElement.schame"> <Form class="dynamic-form-component" @submit.native.prevent :label-width="110" :model="form" v-if="curElement.schame && curElement.schame.length">
<component :is="getComponent(item.type)" :options="item.options" v-model="form[item.key]" /> <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="curElement.id + 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>
<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> </FormItem>
</template>
</Form> </Form>
<Form class="dynamic-form-basic" @submit.native.prevent :label-width="110">
<h3>基础样式</h3>
<template v-if="childSelected">
<template v-for="item in styleSchame.curEle">
<FormItem :label="item.label">
<template v-if="item.label === '宽高'">
<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>
</template> </template>
<template> <template v-else-if="item.label === '位置'">
<h4>基础样式</h4> <InputNumber class="Df-basic-inputnumber" v-model="commonStyle.left" @on-change="updateStyle($event, 'left')"></InputNumber>
<Form class="dynamic-form-basic" :label-width="80"> <InputNumber v-model="commonStyle.top" @on-change="updateStyle($event, 'top')"></InputNumber>
<template v-if="curChildIndex || curChildIndex === 0"> </template>
<FormItem label="定位"> <upload v-else-if="item.label === '背景图片'" v-model="commonStyle.backgroundImage" @change="updateStyle($event, 'backgroundImage')"></upload>
<Tooltip placement="top" content="上对齐"> <ColorSelector v-else-if="item.label === '背景颜色'" v-model="commonStyle.backgroundColor" @input="updateStyle($event, 'backgroundColor')"></ColorSelector>
<Button type="ghost" icon="arrow-up-c" @click="changeAlignType('top')"></Button> <template v-else>
</Tooltip> <Tooltip placement="top" :content="child.content" v-for="child in item.list" :key="child.content">
<Tooltip placement="top" content="右对齐"> <Button type="ghost" :icon="child.icon" @click="changeAlignType(child.args)"></Button>
<Button type="ghost" icon="arrow-right-c" @click="changeAlignType('right')"></Button>
</Tooltip>
<Tooltip placement="top" content="下对齐">
<Button type="ghost" icon="arrow-down-c" @click="changeAlignType('bottom')"></Button>
</Tooltip>
<Tooltip placement="top" content="左对齐">
<Button type="ghost" icon="arrow-left-c" @click="changeAlignType('left')"></Button>
</Tooltip>
<Tooltip placement="top" content="垂直居中">
<Button type="ghost" icon="android-film" @click="changeAlignType('vertical')"></Button>
</Tooltip>
<Tooltip placement="top" content="水平居中">
<Button type="ghost" icon="android-film" @click="changeAlignType('horizontal')"></Button>
</Tooltip>
</FormItem>
<FormItem label="位置">
<InputNumber class="Df-basic-inputnumber" v-model="commonStyle.left"></InputNumber>
<InputNumber v-model="commonStyle.top"></InputNumber>
</FormItem>
<FormItem label="尺寸">
<Tooltip placement="top" content="全屏">
<Button type="ghost" icon="arrow-resize" @click="resizedChildEvent('full')"></Button>
</Tooltip>
<Tooltip placement="top" content="宽100%">
<Button type="ghost" icon="arrow-swap" @click="resizedChildEvent('width')"></Button>
</Tooltip>
<Tooltip placement="top" content="高100%">
<Button type="ghost" icon="arrow-swap" @click="resizedChildEvent('height')" ></Button>
</Tooltip> </Tooltip>
</FormItem> </template>
<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>
</FormItem> </FormItem>
</template> </template>
<template v-if="(curEleIndex || curEleIndex === 0) && !curChildIndex && curChildIndex !== 0"> </template>
<FormItem label="尺寸"> <template v-if="parentSelected">
<Tooltip placement="top" content="全屏"> <template v-for="item in styleSchame.curChild">
<Button type="ghost" icon="arrow-resize" @click="resizedEvent(667, 375)"></Button> <FormItem :label="item.label">
</Tooltip> <template v-if="item.label === '容器宽高'">
<Tooltip placement="top" content="根据背景图片自动调整宽高"> <InputNumber class="Df-basic-inputnumber" :max="375" :min="0" v-model="point.w" @on-change="updatePoint($event, 'w')"></InputNumber>
<Button type="ghost" icon="image" @click="resizedEvent(667, 375, true)" ></Button> <InputNumber :max="667" :min="0" v-model="point.h" @on-change="updatePoint($event, 'h')"></InputNumber>
</Tooltip> </template>
<Tooltip placement="top" content="宽100%"> <upload v-else-if="item.label === '背景图片'" v-model="commonStyle.backgroundImage" @change="updateStyle($event, 'backgroundImage')"></upload>
<Button type="ghost" icon="arrow-swap" @click="resizedEvent(null, 375)"></Button> <ColorSelector v-else-if="item.label === '背景颜色'" v-model="commonStyle.backgroundColor" @input="updateStyle($event, 'backgroundColor')"></ColorSelector>
</Tooltip> <template v-else>
<Tooltip placement="top" content="高100%"> <Tooltip placement="top" :content="child.content" v-for="child in item.list" :key="child.content">
<Button type="ghost" icon="arrow-swap" @click="resizedEvent(667, null)" ></Button> <Button type="ghost" :icon="child.icon" @click="resizedEvent(...child.args)"></Button>
</Tooltip> </Tooltip>
</FormItem> </template>
<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> </FormItem>
</template> </template>
</Form>
</template> </template>
</Form>
</div> </div>
</template> </template>
<script lang="ts" src="./index.ts"></script> <script lang="ts" src="./index.ts"></script>
<style lang="less" scoped> <style lang="less" scoped>
.dynamic-form { .dynamic-form {
padding: 0 15px 16px; padding: 0 0 16px;
h2 { background: #fff;
&-header {
padding: 10px 0 5px;
border-bottom: 8px solid #F5F6FA;
text-align: center; text-align: center;
div {
color: #A1A6B3;
padding: 5px;
font-size: 12px;
} }
h4 { }
// h2 {
// padding: 17px 0;
// border-bottom: 8px solid #F5F6FA;
// text-align: center;
// }
h3 {
padding: 10px 0; padding: 10px 0;
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
&-component { &-component {
padding: 0 20px; padding: 0 20px;
border-bottom: 8px solid #F5F6FA;
} }
&-basic { &-basic {
padding: 0 20px; padding: 0 20px;
...@@ -98,15 +97,28 @@ ...@@ -98,15 +97,28 @@
margin-right: 10px; margin-right: 10px;
} }
} }
/deep/ .ivu-form-item-label {
font-size: 14px; /deep/ .ivu-form-item {
display: flex;
align-items: center;
&:before, &:after {
display: none;
}
.ivu-form-item-label {
width: 97px;
line-height: 18px;
font-size: 12px;
text-align: left;
} }
/deep/ .ivu-input-number { .ivu-input-number {
width: 60px; width: 60px;
} }
/deep/ .ivu-form-item-content { .ivu-form-item-content {
margin-left: 0 !important;
flex: 1;
.ivu-tooltip { .ivu-tooltip {
margin-right: 6px; margin-right: 6px;
.ivu-btn-ghost { .ivu-btn-ghost {
...@@ -126,4 +138,5 @@ ...@@ -126,4 +138,5 @@
} }
} }
} }
}
</style> </style>
\ No newline at end of file
import {Component, Vue, Prop, Watch, Emit} from 'vue-property-decorator';
import DynamicForm from '@editor/component/DynamicForm/index.vue';
import DynamicPageForm from '@editor/component/DynamicPageForm/index.vue';
// import EventBus from '@service/eventBus.service';
@Component({ components: { DynamicPageForm, DynamicForm }, name: 'RecordModal' })
export default class DynamicFormTabs extends Vue {
@Emit('modProps')
modProps(props, ele, type) {
// return props, ele, type;
}
// @Emit('resizedChildEvent')
resizedChildEvent(type) {
// EventBus.$emit('resizedChildEvent', type);
// return type;
}
}
\ No newline at end of file
<template>
<Tabs class="dynamic-form-tabs">
<TabPane label="组件设置">
<dynamic-form @modProps="modProps" @resizedChildEvent="resizedChildEvent"></dynamic-form>
</TabPane>
<!-- <TabPane label="事件">事件</TabPane> -->
<TabPane label="页面设置">
<dynamic-page-form @modProps="modProps" @resizedChildEvent="resizedChildEvent"></dynamic-page-form>
</TabPane>
</Tabs>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.tabs-position() {
/deep/ .ivu-tabs-nav-scroll {
display: flex;
justify-content: center;
}
}
.dynamic-form-tabs {
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);
.ivu-tabs-tabpane {
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
}
}
</style>
\ No newline at end of file
import {Component, Mixins, Prop, Watch} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide, cloneDeep, assign } 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 = '页面';
commonStyleForm: object = {};
propsForm: object = {};
commonStyleSchame: object[] = [
{
key: 'backgroundImage',
name: '背景图片',
type: 'Upload',
},
{
key: 'backgroundColor',
name: '背景颜色',
type: 'ColorSelector'
}
];
propsSchame: object[] = [
{
key: 'showBackTop',
name: '返回顶部',
type: 'checkbox'
}
];
@Watch('pageData', { immediate: true, deep: true })
onElementChange(newVal) {
this.commonStyleSchame.forEach(schame => {
this.$set(this.commonStyleForm, schame.key, this.pageData?.commonStyle[schame.key]);
});
this.propsSchame.forEach(schame => {
this.$set(this.propsForm, schame.key, this.pageData?.props?.[schame.key]);
});
}
@Watch('commonStyleForm', { immediate: true, deep: true })
onCommonStyleFormChange(newVal) {
this.$emit('modProps', newVal, 'page', 'commonStyle');
}
@Watch('propsForm', { immediate: true, deep: true })
onPropsFormChange(newVal) {
this.$emit('modProps', newVal, 'page', 'props');
}
}
\ No newline at end of file
<template>
<div class="dynamic-form">
<h2>{{title}}</h2>
<Form class="dynamic-form-component" :label-width="80" :model="propsForm" @submit.native.prevent>
<h3>基础属性</h3>
<template v-for="(item, index) in propsSchame">
<FormItem class="Df-component-formitem" :label="item.name" >
<component :is="item.type" :options="item.options" v-model="propsForm[item.key]" />
</FormItem>
</template>
</Form>
<Form class="dynamic-form-component" :label-width="80" :model="commonStyleForm" @submit.native.prevent>
<h3>基础样式</h3>
<template v-for="(item, index) in commonStyleSchame">
<FormItem class="Df-component-formitem" :label="item.name" >
<component :is="item.type" :options="item.options" v-model="commonStyleForm[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 { Component, Prop, Mixins, Watch } from 'vue-property-decorator';
import LoginForm from '@/lib/Form/index.vue'; import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import DownloadGuide from '@/lib/DownloadGuide/index.vue'; import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import { cloneDeep, pick, omit, throttle } from 'lodash'; 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'; import { convertPointStyle, getStyle } from '@/service/utils.service';
import EventBus from '@service/eventBus.service';
// import CsGuideCube from '@/lib/DownloadGuide/index.vue';
// import CsMarquee from '@/lib/Marquee/index.vue';
@Component({ components: { LoginForm, DownloadGuide }, name: 'FreedomContainer' }) @Component({ components: { }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(ContextMenu) { export default class FreedomContainer extends Mixins(ContextMenuMixin, TransformStyleMixin) {
@Action('setDragable') setDragable; @Action('setDragable') setDragable;
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex; @State(state => state.editor.curChildIndex) curChildIndex;
@Getter('pageData') pageData;
@Prop({type: Object, default: () => ({ child: [] })}) childItem; @Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop({type: Number, default: 0}) containerIndex; @Prop({type: Number, default: 0}) containerIndex;
@Prop({ type: Boolean, default: false }) showHeader; @Prop({ type: Boolean, default: false }) showHeader;
@Prop(String) backgroundImage; @Prop(String) backgroundImage;
@Prop({ type: String, default: '#fff' }) backgroundColor;
dots: object = {}; dots: object = {};
...@@ -42,51 +47,72 @@ export default class FreedomContainer extends Mixins(ContextMenu) { ...@@ -42,51 +47,72 @@ export default class FreedomContainer extends Mixins(ContextMenu) {
document.addEventListener('mouseup', up, true); 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) { handleElementClick(curEleIndex, curChildIndex) {
this.$emit('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') @Watch('curChildIndex')
onIndexChange(newVal) { onIndexChange(newVal) {
this.dots = {}; this.dots = {};
if (newVal || newVal === 0) { // console.log('onIndexChange');
if (this.curChildIndex || this.curChildIndex === 0 && this.containerIndex === this.curEleIndex) {
this.setPointStyle(); this.setPointStyle();
} }
} }
@Watch('commonStyle')
onCommonStyleChange(newVal) {
console.log('onCommonStyleChange', newVal);
if (newVal) {
this.setDotsStyle();
}
}
mounted() {
EventBus.$on('resizedChildEvent', (type) => {
if (this.curEleIndex === this.containerIndex) {
this.setChildSize(type);
this.setPointStyle();
}
});
}
// 获取point计算后样式 // 获取point计算后样式
setPointStyle() { setPointStyle() {
this.$nextTick(() => {
if (!this.curChildIndex && this.curChildIndex !== 0) { return; }
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(() => { this.$nextTick(() => {
const points = ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b']; const points = ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b'];
const [height, width] = this.getHW(this.curChildIndex); const [height, width] = this.getHW(this.curChildIndex);
this.dots[this.curChildIndex] = points.reduce((pre, cur) => { const dots = points.reduce((pre, cur) => {
pre[cur] = convertPointStyle(cur, {height, width}); pre[cur] = convertPointStyle(cur, {height, width});
return pre; return pre;
}, {}); }, {});
const childEle = this.childItem.child[this.curChildIndex]; this.$set(this.dots, this.curChildIndex, dots);
this.updateCommonStyle({ containerIndex: this.containerIndex, childIndex: this.curChildIndex, data: { ...childEle.commonStyle, height: +height, width: +width }}); // console.log('setDotsStyle', this.dots[this.curChildIndex]);
}); });
} }
getHW(index) { getHW(index) {
// console.log('getHW', index);
if (!this.$refs.childComponent || !this.$refs.childComponent.length) { return [0, 0]; }
const childComponent = this.$refs.childComponent[index]; const childComponent = this.$refs.childComponent[index];
if (!childComponent || !childComponent.$el) { return [0, 0]; }
return [getStyle(childComponent.$el, 'height'), getStyle(childComponent.$el, 'width')]; return [getStyle(childComponent.$el, 'height'), getStyle(childComponent.$el, 'width')];
} }
...@@ -147,6 +173,7 @@ export default class FreedomContainer extends Mixins(ContextMenu) { ...@@ -147,6 +173,7 @@ export default class FreedomContainer extends Mixins(ContextMenu) {
pos.height = newHeight > 0 ? newHeight : 0; pos.height = newHeight > 0 ? newHeight : 0;
pos.left = +left + (hasL ? disX : 0); pos.left = +left + (hasL ? disX : 0);
pos.top = +top + (hasT ? disY : 0); pos.top = +top + (hasT ? disY : 0);
if (!this.containerIndex && this.containerIndex !== 0 || !this.curChildIndex && this.curChildIndex !== 0) { return; }
this.updateCommonStyle({ containerIndex: this.containerIndex, childIndex: this.curChildIndex, data: pos }); this.updateCommonStyle({ containerIndex: this.containerIndex, childIndex: this.curChildIndex, data: pos });
this.setPointStyle(); this.setPointStyle();
}, 100); }, 100);
......
<template> <template>
<div class="freedom" :ref="`freedomContainer${containerIndex}`" @click.stop="handleElementClick(containerIndex)"> <div class="freedom" :ref="`freedomContainer${containerIndex}`" @click.stop="handleElementClick(containerIndex)">
<div class="freedom-body" :style="{background: `url(${backgroundImage}) no-repeat 0 0 / cover`}"> <div class="freedom-body" :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="mousedown(index, $event)" @contextmenu.prevent.stop="show($event, containerIndex, index)"> <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> <component ref="childComponent" :style="transformStyle(item.commonStyle, 'component')" :is="item.name" v-bind="item.props"></component>
<div class="freedom-body-dot" <div class="freedom-body-dot"
v-for="(style, key) in dots[index]" v-for="(style, key) in dots[index]"
...@@ -15,6 +15,18 @@ ...@@ -15,6 +15,18 @@
</template> </template>
<script lang="ts" src="./index.ts"></script> <script lang="ts" src="./index.ts"></script>
<style lang="less" scoped> <style lang="less" scoped>
.border() {
&:before {
position: absolute;
content: '';
border: 3px solid #2d8cf0 !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1
}
}
.freedom { .freedom {
height: 100%; height: 100%;
width: 100%; width: 100%;
...@@ -31,9 +43,21 @@ ...@@ -31,9 +43,21 @@
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
&-item { &-item {
position: absolute;
z-index: 101;
font-size: 0; font-size: 0;
&:after {
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 100%;
z-index: 3;
}
&:hover { &:hover {
border: 1px dashed #0c0c0c !important; .border();
} }
} }
&-dot { &-dot {
...@@ -46,7 +70,7 @@ ...@@ -46,7 +70,7 @@
z-index: 1001; z-index: 1001;
} }
.Fb-item_selected { .Fb-item_selected {
border: 1px dashed #0c0c0c !important; .border();
} }
} }
.activity { .activity {
......
import {Component, Vue, Prop, Watch, Emit} from 'vue-property-decorator';
import DynamicComponent from '@editor/component/DynamicComponent/index.vue';
import { basicComponents, businessComponents } from '@lib/config';
import { State } from 'vuex-class';
@Component({ components: { DynamicComponent }, name: 'RecordModal' })
export default class DynamicForm extends Vue {
@State(state => state.editor.templateList) templateList!: any[];
activeName: string = '1';
resources: object = { basicComponents, businessComponents };
// 选择组件库
selectMaterial(val: string) {
this.activeName = val;
}
@Emit('update:isDraging')
dragstart() {
return true;
}
@Emit('update:isDraging')
dragend() {
return false;
}
@Emit('addEle')
addEle(data) {
return data;
}
}
\ No newline at end of file
<template>
<div class="material">
<Menu class="Dc-left-menu" active-name="1" @on-select="selectMaterial">
<MenuItem name="1">
<Icon type="android-apps"></Icon>
<span>基础库</span>
</MenuItem>
<MenuItem name="2">
<Icon type="android-apps"></Icon>
<span>业务库</span>
</MenuItem>
<MenuItem name="3">
<Icon type="android-apps"></Icon>
<span>模版</span>
</MenuItem>
</Menu>
<div class="Dc-left-content">
<div v-show="activeName === '1'">
<dynamic-component type="basic" :data="resources.basicComponents" @addEle="addEle" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '2'">
<dynamic-component type="business" :data="resources.businessComponents" @addEle="addEle" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '3'">
<dynamic-component type="template" :data="templateList" @addEle="addEle" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
</div>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.tabs-position() {
/deep/ .ivu-tabs-nav-scroll {
display: flex;
justify-content: center;
}
}
.material {
display: flex !important;
height: 100%;
.Dc-left-menu {
height: 100%;
width: 77px !important;
.ivu-menu-item {
padding-left: 0;
padding-right: 0;
display: flex;
flex-direction: column;
align-items: center;
i {
font-size: 18px !important;
}
span {
font-size: 14px;
line-height: 22px;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
user-select: none;
}
}
}
.Dc-left-content {
flex: 1;
padding: 8px;
background: #fff;
overflow-y: scroll;
}
.tabs-position();
}
</style>
\ No newline at end of file
import { Component, Vue, Prop, Watch, Emit, Mixins } from 'vue-property-decorator';
import { Getter, Action, State, Mutation } from 'vuex-class';
import type { PageInfo, Page, GridLayout } from '@store/modules/editor/state';
import TransformStyleMixin from '@page/mixins/transformStyle.mixin';
import FreedomContainer from '@editor/component/FreedomContainer/index.vue';
@Component({ components: { FreedomContainer }, name: 'OperationPanel' })
export default class OperationPanel extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.gridLayout) gridLayout?: GridLayout;
@State(state => state.editor.curEleIndex) curEleIndex!: number | null;
@State(state => state.editor.curChildIndex) curChildIndex!: number | null;
@Prop(Boolean) isDraging;
@Prop(Boolean) isDragIn;
get layout() {
return this.pageData.elements.map(v => v.point);
}
@Emit('update:isDragIn')
dragenter() {
return true;
}
@Emit('update:isDragIn')
dragleave() {
return false;
}
@Emit('dragover')
dragover(event) {
//
}
@Emit('drops')
drops(event) {
//
}
@Emit('handleElementClick')
handleElementClick(curEleIndex?: number | null, curChildIndex?: number | null) {
//
}
@Emit('show')
show(event, containerIndex, childIndex) {
//
}
@Emit('resizedEvent')
resizedEvent(i, h, w) {
//
}
@Emit('movedEvent')
movedEvent(i, newX, newY) {
//
}
@Emit('toggle')
toggle(val) {
//
}
}
\ No newline at end of file
<template>
<div :class="[{'Dcm-container-panel_in': isDragIn, 'Dcm-container-panel_draging': isDraging}, 'Dcm-container-panel']" @dragover.prevent @dragenter="dragenter" @dragover="dragover"
@dragleave="dragleave" @drop="drops">
<grid-layout
:layout.sync="layout"
:col-num="gridLayout.colNum"
:row-height="gridLayout.rowHeight"
:margin="[0, 0]"
:is-draggable="gridLayout.draggable"
:is-resizable="true"
: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"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i + index"
@contextmenu.native.prevent="show($event, index)"
@resized="resizedEvent"
@moved="movedEvent"
:style="transformStyle(item.commonStyle, item.name)"
:class="{'Dcmcp-item_selected': curEleIndex === index && curChildIndex === null}">
<component ref="container" :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>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.border() {
&:before {
position: absolute;
content: '';
border: 3px solid #2d8cf0 !important;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1
}
}
.Dcm-container-panel {
margin: 30px auto;
width: 375px;
height: 667px;
min-height: 667px;
overflow-y: scroll;
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 {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
&:after {
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 100%;
z-index: 100;
}
&:hover {
.border();
}
.vue-resizable-handle {
z-index: 10000;
}
}
.Dcmcp-item_selected {
.border();
}
}
&_draging {
.Dcmcp-item-com {
* {
pointer-events: none;
}
}
opacity: 0.7;
}
}
</style>
\ No newline at end of file
<template> <template>
<div class="tableComStyle"> <div class="tableComStyle">
<div class="searchFormStyle qg-table"> <div class="searchFormStyle qg-table">
<Form inline ref="searchForm" :rules="searchCondition.rules || {}" :model="searchForm" class="inline"> <Form @submit.native.prevent inline ref="searchForm" :rules="searchCondition.rules || {}" :model="searchForm" class="inline">
<FormItem :key="getKey(item.key)" :prop="getKey(item.key)" :rules="item.rules" :label="item.label" v-for="item in searchCondition"> <FormItem :key="getKey(item.key)" :prop="getKey(item.key)" :rules="item.rules" :label="item.label" v-for="item in searchCondition">
<Input v-model="searchForm[item.key]" placeholder="" class="comWidth" v-if="item.type === 'input'" clearable /> <Input v-model="searchForm[item.key]" placeholder="" class="comWidth" v-if="item.type === 'input'" clearable />
<Select v-model="searchForm[item.key]" class="comWidth" filterable v-else-if="item.type === 'select'" clearable> <Select ref="searchSelect" 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" /> <Option :value="isNum(value, item.number)" :key="value" v-for="(label,value) in item.option" :label="label" />
</Select> </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>
<FormItem class="btnGroupStyle"> <FormItem class="btnGroupStyle">
<div> <div>
...@@ -18,14 +19,14 @@ ...@@ -18,14 +19,14 @@
</Form> </Form>
</div> </div>
<div class="tableGroupStyle"> <div class="tableGroupStyle">
<div class="toolBarStyle"> <div v-if="!hideAdd" class="toolBarStyle">
<h3>查询数据</h3> <h3>查询数据</h3>
<div> <div>
<Button type="primary" class="btnStyle" @click="newEvent()">新增</Button> <Button type="primary" class="btnStyle" @click="newEvent()">新增</Button>
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<Table :columns="renderColumns" :data="tableData" class="tableStyle"></Table> <Table :height="height" @on-select-cancel="selectionCancel" @on-selection-change="selectionChange" :columns="renderColumns" :data="tableData" class="tableStyle"></Table>
<Page <Page
:total="total" :total="total"
v-if="total > 0" v-if="total > 0"
...@@ -41,14 +42,25 @@ ...@@ -41,14 +42,25 @@
</div> </div>
</template> </template>
<script> <script>
import Treeselect from '@riophae/vue-treeselect';
import '@riophae/vue-treeselect/dist/vue-treeselect.css';
import { cloneDeep } from 'lodash';
export default { export default {
components: { Treeselect },
props: { props: {
columns: Array, columns: {
type: Array,
default: () => ([])
},
hideAdd: Boolean,
request: Function, request: Function,
toolBar: Function, toolBar: Function,
height: Number,
}, },
data() { data() {
return { return {
cols: this.transformData(),
tableData: [], tableData: [],
total: 0, total: 0,
searchForm: { searchForm: {
...@@ -66,16 +78,17 @@ export default { ...@@ -66,16 +78,17 @@ export default {
}); });
}, },
immediate: true, immediate: true,
}, }
}, },
computed: { computed: {
renderColumns: function() { renderColumns: function() {
return this.handleColumns(this.columns) return this.handleColumns(this.cols)
} }
}, },
methods: { methods: {
changePageNo(page) { changePageNo(page) {
this.query(page); this.query(page);
this.$emit('on-page-change', page);
}, },
changePageSize(size) { changePageSize(size) {
this.searchForm.pageSize = size; this.searchForm.pageSize = size;
...@@ -93,6 +106,10 @@ export default { ...@@ -93,6 +106,10 @@ export default {
}, },
resetForm(formName) { resetForm(formName) {
this.$refs[formName].resetFields(); this.$refs[formName].resetFields();
// 处理select bug
this.$refs.searchSelect.forEach(selectEle => {
selectEle.setQuery(null);
});
}, },
isArray(key) { isArray(key) {
return Array.isArray(key); return Array.isArray(key);
...@@ -105,11 +122,15 @@ export default { ...@@ -105,11 +122,15 @@ export default {
this.searchForm.pageSize = 10; this.searchForm.pageSize = 10;
this.searchForm.pageNo = 1; this.searchForm.pageNo = 1;
}, },
refresh() {
this.query();
},
newEvent(e) { newEvent(e) {
this.$emit('newBtnClick', e); this.$emit('newBtnClick', e);
}, },
handleColumns(col=[]) { handleColumns(col=[]) {
const data = [] const data = []
this.searchCondition = [];
col.forEach(item => { col.forEach(item => {
item.align = item.align || 'center'; item.align = item.align || 'center';
if (item.valueEnum&&!item.render) { if (item.valueEnum&&!item.render) {
...@@ -128,6 +149,38 @@ export default { ...@@ -128,6 +149,38 @@ export default {
}, },
isNum(value, number) { isNum(value, number) {
return number ? +value : value; return number ? +value : value;
},
selectionChange(selection) {
this.$emit('on-selection-change', selection);
},
selectionCancel(selection, row) {
this.$emit('on-select-cancel', selection, row);
},
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() { mounted() {
...@@ -139,25 +192,24 @@ export default { ...@@ -139,25 +192,24 @@ export default {
// @import '../../styles/comment.less'; // @import '../../styles/comment.less';
@padding: 25px; @padding: 25px;
.tableComStyle { .tableComStyle {
height: 100%;
background: #f5f5f5; background: #f5f5f5;
padding: 15px; padding: 15px;
min-height: 1000px; // min-height: 1000px;
.searchFormStyle { .searchFormStyle {
text-align: left; text-align: left;
clear: both; clear: both;
background-color: #ffffff; background-color: #ffffff;
padding: @padding; padding: @padding;
min-height: 70px; min-height: 70px;
font-size: 0;
.labelStyle { .labelStyle {
font-weight: bold !important; font-weight: bold !important;
display: inline-block; display: inline-block;
} }
// & @{deep} .ivu-form-item .ivu-form-item-label { /deep/ .ivu-form {
// .labelStyle; margin-bottom: -24px;
// } }
// & @{deep} .ivu-form-item-content {
// .labelStyle;
// }
.required:before { .required:before {
content: '* '; content: '* ';
color: #ed3f14; color: #ed3f14;
......
import {Component, Vue, Prop, Watch, Emit} from 'vue-property-decorator';
@Component({ name: 'RecordModal' })
export default class RecordModal extends Vue {
@Prop(Boolean) value;
showPopup: boolean = false;
@Watch('value', { immediate: true })
onValueChange(val) {
this.showPopup = val;
}
@Emit('ok')
ok() {
// return;
}
@Emit('cancel')
cancel() {
// return;
}
}
\ No newline at end of file
<template>
<Modal
v-model="showPopup"
@on-ok="ok"
@on-cancel="cancel"
width="360">
<p slot="header" style="color:#2d8cf0;text-align:center">
<Icon type="information-circled"></Icon>
<span>恢复提醒</span>
</p>
<div style="text-align:center">
<p>已检测到您上次编辑但未保存, 是否恢复?</p>
</div>
</Modal>
</template>
<script lang="ts" src="./index.ts"></script>
\ No newline at end of file
'use strict'; // tslint:disable
import Vue from 'vue';
import { sync } from 'vuex-router-sync';
import App from '@framework/app'; import App from '@framework/app';
import createStore from '../store/index'; import createStore from '@store/index';
import createRouter from './router/index'; import createRouter from './router/index';
import entry from '@editor/view/home/index.vue'; import entry from '@editor/view/home/index.vue';
export default new App({ entry, createStore, createRouter }).bootstrap(); import './public-path';
\ No newline at end of file
const create = function(initState) {
console.log('initState', initState);
const store = createStore(initState);
const router = createRouter();
sync(store, router);
return {
router,
store,
render: h => {
return h(entry);
},
};
};
const client = function() {
const vm = create(window.__INITIAL_STATE__);
const app = new Vue(vm);
const root = document.getElementById('app');
const hydrate = (<HTMLElement>root).childNodes.length > 0;
app.$mount('#app', hydrate);
};
export async function bootstrap() {}
export async function mount() {
setTimeout(() => {
client();
});
}
export async function unmount() {}
window.__POWERED_BY_QIANKUN__ || mount();
import { Component, Vue } from 'vue-property-decorator';
import { Getter, Mutation } from 'vuex-class';
import { isEqual } from 'lodash';
import localStorage from '@service/localStorage.service';
@Component({ name: 'AutoSaveMixin' })
export default class AutoSaveMixin extends Vue {
@Getter('pageInfo') pageInfo;
@Mutation('SET_PAGE_INFO') setPageInfo;
showRecordModal: boolean = false;
account: string = localStorage.get('user')?.account;
hasCompared: boolean = false;
timer: NodeJS.Timeout | null = null;
showRecord() {
// console.log('showRecord');
const record = localStorage.get(`${this.account}-${this.pageInfo.id}`);
if (record && !isEqual(record, this.pageInfo)) {
this.showRecordModal = true;
// console.log('showRecord', record, this.pageInfo);
return;
}
this.hasCompared = true;
}
autoSave() {
const interval = 5 * 1000;
this.timer = setInterval(() => {
// console.log('autoSave');
if (this.account && this.pageInfo && this.hasCompared) {
// console.log('autoSave in', this.pageInfo);
localStorage.set(`${this.account}-${this.pageInfo.id}`, this.pageInfo);
}
}, interval);
this.$once('hook:beforeDestroy', () => {
this.clearTimer();
});
}
removeDefaultCache(id = 0) {
this.clearTimer();
localStorage.remove(`${this.account}-${id}`);
this.autoSave();
}
clearTimer() {
clearInterval(this.timer as NodeJS.Timeout);
this.timer = null;
}
recover() {
// console.log('recover');
const record = localStorage.get(`${this.account}-${this.pageInfo.id}`);
this.setPageInfo(record);
this.hasCompared = true;
}
cancel() {
// console.log('cancel');
this.hasCompared = true;
}
}
\ No newline at end of file
import {Component, Vue } from 'vue-property-decorator'; import {Component, Vue } from 'vue-property-decorator';
import { Mutation } from 'vuex-class'; import { Mutation } from 'vuex-class';
import { getStyle } from '@/service/utils.service';
@Component({ name: 'ContextMenu' }) @Component({ name: 'ContextMenuMixin' })
export class ContextMenu extends Vue { export default class ContextMenuMixin extends Vue {
@Mutation('COPY_OR_DELETE_PAGE_INFO') updatePageData; @Mutation('COPY_OR_DELETE_PAGE_INFO') updatePageData;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo; @Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@Mutation('UPDATE_COMMON_STYLE') updateCommonStyle; @Mutation('UPDATE_COMMON_STYLE') updateCommonStyle;
...@@ -23,7 +24,6 @@ export class ContextMenu extends Vue { ...@@ -23,7 +24,6 @@ export class ContextMenu extends Vue {
onClick: () => { onClick: () => {
console.log('删除', event, containerIndex, childIndex); console.log('删除', event, containerIndex, childIndex);
this.updatePageData({type: 'delete', containerIndex, childIndex}); this.updatePageData({type: 'delete', containerIndex, childIndex});
// console.log('删除');
} }
}, },
], ],
...@@ -31,4 +31,13 @@ export class ContextMenu extends Vue { ...@@ -31,4 +31,13 @@ export class ContextMenu extends Vue {
minWidth: 100 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 === 'cs-goods-tabs' && element?.props?.list.length) {
const childs = [];
const childIndexs = [];
const idList = element.props.list.map(v => v.componentId);
if (idList.some(v => !v)) {
throw new Error('商品导航组件不可为空');
}
if (idList.length && idList.length !== Array.from(new Set(idList)).length) {
throw new Error('商品导航不可重复选择组件');
}
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 };
}
});
let indexs = [];
Object.keys(goodsTabs).forEach(key => {
const { idx, childIndexs, childs } = goodsTabs[key];
pageData.elements[idx].child = childs;
indexs = [...indexs, ...childIndexs];
});
indexs = indexs.sort((a, b) => a - b);
// 逆向循环
for (let i = indexs.length - 1; i >= 0; i--) {
pageData.elements.splice(indexs[i], 1);
}
return pageData;
}
parseGoodsTabs() {
const goodsTabs = {};
this.pageData.elements.forEach((element, idx) => {
if (element.name === 'cs-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
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
...@@ -2,24 +2,16 @@ import Vue from 'vue'; ...@@ -2,24 +2,16 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import Dashboard from '../view/dashboard/index.vue'; import Dashboard from '../view/dashboard/index.vue';
import PageList from '../view/pageList/index.vue'; import PageList from '../view/pageList/index.vue';
import myPage from '../view/myPage/index.vue'; import MyPage from '../view/myPage/index.vue';
import template from '../view/template/index.vue'; import Template from '../view/template/index.vue';
import localStorage from '@/service/localStorage.service'; import localStorage from '@/service/localStorage.service';
Vue.use(VueRouter); Vue.use(VueRouter);
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err);
};
VueRouter.prototype.replace = function push(location) {
return originalPush.call(this, location).catch(err => err);
};
export default function createRouter() { export default function createRouter() {
const router = new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'history',
base: '/editor/', base: window.__POWERED_BY_QIANKUN__ ? '/micro/quantum-blocks' : '/editor',
routes: [ routes: [
{ {
path: '/', path: '/',
...@@ -28,22 +20,26 @@ export default function createRouter() { ...@@ -28,22 +20,26 @@ export default function createRouter() {
{ {
name: 'detail', name: 'detail',
path: '/detail/:pageId?', path: '/detail/:pageId?',
component: () => import('../view/dashboard/index.vue') component: Dashboard
// component: () => import('../view/dashboard/index.vue')
}, },
{ {
name: 'list', name: 'list',
path: '/list', path: '/list',
component: () => import('../view/pageList/index.vue') component: PageList
// component: () => import('../view/pageList/index.vue')
}, },
{ {
name: 'my', name: 'my',
path: '/my', path: '/my',
component: () => import('../view/myPage/index.vue') component: MyPage
// component: () => import('../view/myPage/index.vue')
}, },
{ {
name: 'template', name: 'template',
path: '/template', path: '/template',
component: () => import('../view/template/index.vue') component: Template
// component: () => import('../view/template/index.vue')
}, },
] ]
}); });
......
import { kebabCase, maxBy } from 'lodash'; import { kebabCase, maxBy, matches } from 'lodash';
import { Getter, Action, State, Mutation } from 'vuex-class'; 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 VueGridLayout from 'vue-grid-layout';
import FreedomContainer from '../../component/FreedomContainer/index.vue'; import config from '@config/index';
import DynamicForm from '../../component/DynamicForm/index.vue'; import TransformStyleMixin from '@page/mixins/transformStyle.mixin';
import LoginForm from '@/lib/Form/index.vue'; import ContextMenuMixin from '@editor/mixins/contextMenu.mixin';
import DownloadGuide from '@/lib/DownloadGuide/index.vue'; import GoodsTabsMixin from '@editor/mixins/goodsTabs.mixin';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin'; import AutoSaveMixin from '@editor/mixins/autoSave.mixin';
import BasicPageForm from '@editor/component/BasicPageForm/index.vue'; import BasicPageFormModal from '@editor/component/BasicPageFormModal/index.vue';
import { basicComponents, businessComponents } from '@/lib/config'; import RecordModal from '@editor/component/RecordModal/index.vue';
import config from '@/config/index'; import MaterialMenu from '@editor/component/MaterialMenu/index.vue';
import localStorage from '@/service/localStorage.service'; import DynamicFormTabs from '@editor/component/DynamicFormTabs/index.vue';
import localStorage from '@service/localStorage.service';
@Component({components: { DynamicComponent, FreedomContainer, DynamicForm, GridLayout: VueGridLayout.GridLayout, import EventBus from '@service/eventBus.service';
GridItem: VueGridLayout.GridItem, LoginForm, DownloadGuide, BasicPageForm }, name: 'DashBoard'}) import { getStyle } from '@service/utils.service';
export default class DashBoard extends Mixins(ContextMenu) { import OperationPanel from '@editor/component/OperationPanel/index.vue';
import type { PageInfo, Page, GridLayout } from '../../../store/modules/editor/state';
@Component({components: { GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem, BasicPageFormModal, RecordModal, MaterialMenu, DynamicFormTabs, OperationPanel }, name: 'DashBoard'})
export default class DashBoard extends Mixins(ContextMenuMixin, GoodsTabsMixin, TransformStyleMixin, AutoSaveMixin) {
@Mutation('ADD_ELEMENTS') addElements; @Mutation('ADD_ELEMENTS') addElements;
@Mutation('SET_CUR_ELE_INDEX') setCurEleIndex; @Mutation('SET_CUR_ELE_INDEX') setCurEleIndex;
@Mutation('SET_CUR_CHILD_INDEX') setCurChildIndex; @Mutation('SET_CUR_CHILD_INDEX') setCurChildIndex;
@Mutation('SET_PAGE_INFO') setPageInfo; @Mutation('SET_PAGE_INFO') setPageInfo;
@Mutation('SET_PAGE_DATA') setPageData; @Mutation('SET_PAGE_DATA') setPageData;
@Mutation('UPDATE_PAGE_STYLE') setPageStyle;
@Mutation('UPDATE_PAGE_PROPS') setPageProps;
@Action('resetPageData') resetPageData; @Action('resetPageData') resetPageData;
@Action('savePageData') savePageData; @Action('savePageData') savePageData;
@Action('getPageDate') getPageDate; @Action('getPageDate') getPageDate;
@Action('getTemplateList') getTemplateList; @Action('getTemplateList') getTemplateList;
@Action('setTemplateInfo') setTemplateInfo; @Action('setTemplateInfo') setTemplateInfo;
@Getter('pageId') pageId; @Getter('uuid') uuid!: string;
@Getter('pageData') pageData; @Getter('pageData') pageData;
@State(state => state.editor.gridLayout.draggable) draggable; @State(state => state.editor.gridLayout) gridLayout?: GridLayout;
@State(state => state.editor.gridLayout.rowHeight) rowHeight; @State(state => state.editor.curEleIndex) curEleIndex!: number | null;
@State(state => state.editor.gridLayout.colNum) colNum; @State(state => state.editor.curChildIndex) curChildIndex!: number | null;
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex; @Provide() editor = this;
@State(state => state.editor.templateList) templateList;
activeName: string = '1';
isCollapsed: boolean = true; isCollapsed: boolean = true;
isDragIn: boolean = false; isDragIn: boolean = false;
isDraging: boolean = false; isDraging: boolean = false;
resources: object = { basicComponents, businessComponents };
showSubmitPopup: boolean = false; showSubmitPopup: boolean = false;
inTheSave: boolean = false;
async created() { async created() {
const { pageId, templateId } = this.$route.params; const { pageId, templateId } = this.$route.params;
console.log(pageId, templateId);
console.log('env', process.env);
this.resetPageData(); this.resetPageData();
if (pageId) { if (pageId) {
this.getPageDate({ pageId }); await this.getPageDate({ pageId });
} else if (templateId) { } else if (templateId) {
this.setTemplateInfo({ pageId: templateId }); await this.setTemplateInfo({ pageId: templateId });
} }
this.getTemplateList(); this.getTemplateList();
// 历史记录
this.showRecord();
this.autoSave();
} }
get layout() { beforeRouteLeave(to, from, next) {
return this.pageData.elements.map(v => v.point); console.log('beforeRouteLeave');
} }
// 选择组件库 get layout() {
selectMaterial(val: string) { return this.pageData.elements.map(v => v.point);
this.activeName = val;
} }
async save(type, pageConfig) { async save(type: 'preview' | 'save', pageConfig) {
try {
const user = localStorage.get('user'); const user = localStorage.get('user');
const isCreate = !!this.uuid;
if (!type) { if (!type) {
this.showSubmitPopup = true; this.showSubmitPopup = true;
} else { } else {
this.pageData.elements.sort((a, b) => a.point.y - b.point.y); this.pageData.elements.sort((a, b) => a.point.y - b.point.y);
const { pageName, pageDescribe, coverImage, isPublish, isTemplate } = pageConfig; // 处理商品标签组件
const pageInfo = { page: JSON.stringify(this.pageData), author: user?.account, isPublish, pageName, pageDescribe, coverImage, isTemplate }; const pageData = this.handleGoodsTabs();
if (+this.pageId) { pageInfo.id = this.pageId; } const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate } = pageConfig;
await this.savePageData(pageInfo); const pageInfo = { page: JSON.stringify(pageData), author: user?.account, isPublish, pageName, pageDescribe, pageKeywords, coverImage, isTemplate } as pageInfo;
if (this.uuid) { pageInfo.uuid = this.uuid; }
await this.savePageData({ pageInfo, pageData: this.pageData });
if (this.uuid) { await this.getPageDate({ pageId: this.uuid }); }
this.showSubmitPopup = false; this.showSubmitPopup = false;
// 清除新增数据时的缓存
if (!isCreate) { this.removeDefaultCache(); }
if (type === 'preview') { if (type === 'preview') {
window.open(`${config.apiHost}activity/${this.pageId}`); window.open(`${config.h5Host}activity/${this.uuid}`);
} else {
this.$Notice.success({ title: '保存成功!' });
} }
} }
} catch (e) {
this.showSubmitPopup = false;
this.$Notice.error({ title: e?.message || '出现未知错误!' });
} finally {
this.inTheSave = false;
}
}
async preview() {
this.inTheSave = true;
(this.$refs.basicPageForm as any).preview();
} }
exit() { exit() {
this.$router.back(); this.$router.back();
} }
handleElementClick(curEleIndex = null, curChildIndex = null) { handleElementClick(curEleIndex?: number | null, curChildIndex?: number | null) {
console.log('handleElementClick - DashBoard', curEleIndex, curChildIndex); // console.log('handleElementClick - DashBoard', curEleIndex, curChildIndex);
this.toggle(false); this.toggle(false);
this.setCurEleIndex(curEleIndex); this.setCurEleIndex(curEleIndex);
this.setCurChildIndex(curChildIndex); this.setCurChildIndex(curChildIndex);
} }
toggle(val) { toggle(val) {
if (val) {
this.setCurEleIndex(null);
this.setCurChildIndex(null);
}
this.isCollapsed = val; this.isCollapsed = val;
} }
dragstart() {
this.isDraging = true;
}
dragend() {
this.isDraging = false;
}
dragenter() { dragenter() {
console.log('dragenter');
this.isDragIn = true; this.isDragIn = true;
} }
dragleave() { dragleave() {
console.log('dragleave');
this.isDragIn = false; this.isDragIn = false;
} }
dragover(event) { dragover(event) {
if (event.target.classList.contains('freedom')) { if (event?.target?.children?.[0]?.classList.contains('freedom')) {
event.dataTransfer.dropEffect = 'move'; event.dataTransfer.dropEffect = 'move';
} else { } else {
event.dataTransfer.dropEffect = 'copy'; event.dataTransfer.dropEffect = 'copy';
} }
} }
modProps(props) { modProps(props, ele, type) {
if (ele === 'page') {
if (type === 'commonStyle') {
this.setPageStyle({ data: props });
} else if (type === 'props') {
this.setPageProps({ data: props });
}
} else if (ele === 'component') {
let currentEle = {}; let currentEle = {};
if (this.curEleIndex !== null) { if (this.curEleIndex !== null) {
if (this.curChildIndex !== null) { if (this.curChildIndex !== null) {
...@@ -138,48 +156,69 @@ export default class DashBoard extends Mixins(ContextMenu) { ...@@ -138,48 +156,69 @@ export default class DashBoard extends Mixins(ContextMenu) {
} }
} }
} }
}
drops(event) { drops(event) {
this.isDragIn = false; this.isDragIn = false;
this.isCollapsed = false; this.isCollapsed = false;
const data = JSON.parse(event.dataTransfer.getData('text')); const data = JSON.parse(event.dataTransfer.getData('text'));
const { layerX: left, layerY: top } = event; const { layerX: left, layerY: top } = event;
const target = event?.target?.children?.[0]?.children?.[0] || {};
event.dataTransfer.clearData(); event.dataTransfer.clearData();
// template // template
if (data.template) { if (data.template) {
this.setPageData(JSON.parse(data.template)); this.setPageData(JSON.parse(data.template));
this.handleElementClick(null, null); this.handleElementClick(null, null);
// freedom // freedom
} else if (event.target.classList.contains('freedom')) { } else if (target?.classList?.contains('freedom-body')) {
const { y: curY } = this.pageData.elements[event.target.dataset.index].point; const { y: curY } = this.pageData.elements[target?.dataset?.index].point;
const scrollTop = this.layout.reduce((pre, cur) => { const scrollTop = this.layout.reduce((pre, cur) => {
if (cur.y < curY) { if (cur.y < curY) {
return pre + cur.h * this.rowHeight; return pre + cur.h * this.gridLayout.rowHeight;
} }
return pre; return pre;
}, 0); }, 0);
this.addElements({ containerIndex: event.target.dataset.index, data: { ...data, commonStyle: { position: 'absolute', left, top: top - scrollTop } } }); this.addElements({ containerIndex: target?.dataset?.index, data: { ...data, commonStyle: { position: 'absolute', left, top: top - scrollTop } } });
this.handleElementClick(+event.target.dataset.index, this.pageData.elements[event.target.dataset.index].child.length - 1); this.handleElementClick(+target?.dataset?.index, this.pageData.elements[target?.dataset?.index].child.length - 1);
// component // component
} else { } else {
const { i } = maxBy(this.layout, 'i') || {}; const y = Math.floor(top / this.gridLayout.rowHeight);
const y = Math.floor(top / this.rowHeight); this.addElements({ data: {...data, point: { ...data.point, y } }});
console.log('drops', i); this.handleElementClick(this.pageData.elements.length - 1, null);
this.addElements({ data: {...data, point: { ...data.point, i: i || i === 0 ? String(+i + 1) : '0', y } }); }
// 调整组件高度
this.$nextTick(() => this.adjustHeight());
}
addEle(data) {
if (data.template) {
this.setPageData(JSON.parse(data.template));
this.handleElementClick(null, null);
} else {
const y = getStyle(document.querySelector('.vue-grid-layout'), 'height');
this.addElements({ data: {...data, point: { ...data.point, y } }});
this.handleElementClick(this.pageData.elements.length - 1, null); this.handleElementClick(this.pageData.elements.length - 1, null);
} }
// 调整组件高度
this.$nextTick(() => this.adjustHeight());
} }
resizedEvent(i, h, w) { resizedEvent(i, h, w) {
const index = this.pageData.elements.findIndex(ele => ele.point.i === i); 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 } } }); 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');
}
/** /**
* 调整自由容器子元素宽高及边框原点位置 * 调整自由容器子元素宽高及边框原点位置
* @param {[type]} type 尺寸类型 * @param {[type]} type 尺寸类型
*/ */
resizedChildEvent(type) { resizedChildEvent(type) {
const containerEle = this.$refs.container[this.curEleIndex]; const containerEle = this.$refs[`freedomContainer${containerIndex}`];
// const containerEle = this.$refs.container[(this.curEleIndex as number)];
containerEle.setChildSize(type); containerEle.setChildSize(type);
containerEle.setPointStyle(); containerEle.setPointStyle();
} }
......
<template> <template>
<Row class="dashboard" v-cloak> <Row class="dashboard">
<Row class="dashboard-header" type="flex" align="middle"> <Row class="dashboard-header" type="flex" align="middle">
<Col span="8"> <Col span="20" offset="2">
<h3>低代码开发平台</h3> <Button @click="preview()">预览</Button>
<Button type="primary" @click="save()">保存</Button>
</Col> </Col>
<Col span="6"> <Col span="2">
<Row type="flex" justify="end"> <Button @click="exit">取消</Button>
<Col span="7" @click.native="save()">
<Icon type="ionic"></Icon>
<span>保存</span>
</Col>
<Col span="7" @click.native="exit">
<Icon type="monitor"></Icon>
<span>返回</span>
</Col>
</Row>
</Col> </Col>
</Row> </Row>
<Row class="dashboard-container"> <Row class="dashboard-container">
<Col class="dashboard-container-left" span="6"> <Col class="dashboard-container-left" span="7">
<Menu class="Dc-left-menu" active-name="1" @on-select="selectMaterial"> <material-menu @addEle='addEle' :isDraging.sync="isDraging" />
<MenuItem name="1">
<Icon type="ios-paper"></Icon>
</MenuItem>
<MenuItem name="2">
<Icon type="ios-people"></Icon>
</MenuItem>
<MenuItem name="3">
<Icon type="stats-bars"></Icon>
</MenuItem>
</Menu>
<div class="Dc-left-content">
<div v-show="activeName === '1'">
<h2>基础库</h2>
<dynamic-component :data="resources.basicComponents" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '2'">
<h2>业务库</h2>
<dynamic-component :data="resources.businessComponents" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '3'">
<h2>模板</h2>
<dynamic-component :data="templateList" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
</div>
</Col> </Col>
<Col class="dashboard-container-middle" span="18"> <Col class="dashboard-container-middle" span="17">
<Row class="Dc-middle-row"> <Row class="Dc-middle-row">
<Col :span="isCollapsed ? 24 : 16" class="Dc-middle-container" @click.native="toggle(true)"> <Col :span="isCollapsed ? 24 : 16" class="Dc-middle-container" @click.native="toggle(true)">
<div :class="[{'Dcm-container-panel_in': isDragIn, 'Dcm-container-panel_draging': isDraging}, 'Dcm-container-panel']" @dragover.prevent @dragenter="dragenter" @dragover="dragover" <OperationPanel :isDragIn.sync="isDragIn" :isDraging="isDraging" @dragover="dragover" @drops="drops" @handleElementClick="handleElementClick" @show="show" @resizedEvent="resizedEvent" @movedEvent="movedEvent" @toggle="toggle" />
@dragleave="dragleave" @drop="drops">
<grid-layout
:layout.sync="layout"
:col-num="colNum"
:row-height="rowHeight"
:margin="[0, 0]"
:is-draggable="draggable"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item @click.native.stop="handleElementClick(index, null)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i"
@contextmenu.native.prevent="show($event, index)"
@resized="resizedEvent"
: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>
</grid-item>
</grid-layout>
</div>
</Col> </Col>
<Col span="8" :class="[{'Dcm-sider_none': isCollapsed}, 'Dc-middle-sider']"> <Col span="8" :class="[{'Dcm-sider_none': isCollapsed}, 'Dc-middle-sider']">
<Tabs class="Dc-middle-editing" type="card"> <DynamicFormTabs @modProps="modProps" @resizedChildEvent="resizedChildEvent" />
<TabPane label="属性">
<dynamic-form @modProps="modProps" @resizedChildEvent="resizedChildEvent"></dynamic-form>
</TabPane>
<TabPane label="事件">事件</TabPane>
<TabPane label="页面设置">页面设置</TabPane>
</Tabs>
</Col> </Col>
</Row> </Row>
</Col> </Col>
</Row> </Row>
<BasicPageForm v-model="showSubmitPopup" @submit="save" /> <BasicPageFormModal ref="basicPageForm" v-model="showSubmitPopup" @submit="save" />
<RecordModal ref="recordModal" v-model="showRecordModal" @ok="recover" @cancel="cancel" />
<Spin size="large" fix v-if="inTheSave"></Spin>
</Row> </Row>
</template> </template>
<style lang="less"> <style lang="less" scoped>
.tabs-position() { @font-face {
/deep/ .ivu-tabs-nav-scroll { font-weight: normal;
display: flex; font-family: 'cr-icon';
justify-content: center; font-style: normal;
} font-display: auto;
} src:
[v-cloak] { url('https://misc.lkbang.net/fonts/cr-icon.b0d72989.woff2') format('woff2'),
display: none; url('https://misc.lkbang.net/fonts/cr-icon.71b60fa6.woff') format('woff'),
url('https://misc.lkbang.net/fonts/cr-icon.2dca9b1b.ttf') format('truetype');
} }
.dashboard { .dashboard {
min-width: 1280px; min-width: 1280px;
...@@ -112,26 +51,20 @@ ...@@ -112,26 +51,20 @@
box-shadow: 0 2px 3px 0 hsla(0,0%,39.2%,.06); box-shadow: 0 2px 3px 0 hsla(0,0%,39.2%,.06);
font-size: 16px; font-size: 16px;
z-index: 901; z-index: 901;
.ivu-col {
text-align: center;
button {
width: 100px;
&:nth-child(2) {
margin-left: 40px;
} }
&-container {
height: calc(100% - 48px) !important;
&-left {
display: flex !important;
height: 100%;
.Dc-left-menu {
height: 100%;
width: 65px !important;
i {
font-size: 18px !important;
} }
} }
.Dc-left-content {
flex: 1;
padding: 10px;
background: #fff;
overflow-y: scroll;
} }
.tabs-position(); &-container {
height: calc(100% - 48px);
&-left {
height: 100%;
} }
&-middle { &-middle {
height: 100%; height: 100%;
...@@ -143,58 +76,20 @@ ...@@ -143,58 +76,20 @@
height: 100%; height: 100%;
opacity: 1; opacity: 1;
transition: width ease-in-out 0.5s; transition: width ease-in-out 0.5s;
.Dcm-container-panel { .Dcm-container-header {
margin: 30px auto; margin: 20px 0 50px 0;
width: 375px; .ivu-col {
height: 667px; font-size: 20px;
min-height: 667px;
overflow-y: scroll;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
.vue-grid-item {
text-align: center; text-align: center;
background: #fff;
overflow: hidden;
&:hover {
border: 1px dashed #0c0c0c !important;
}
&>*:first-child {
height: 100%;
}
.vue-resizable-handle {
z-index: 10000;
}
}
.Dcmcp-item_selected {
border: 1px dashed #0c0c0c !important;
}
}
&_draging {
.Dcmcp-item-com {
* {
pointer-events: none;
}
}
opacity: 0.7;
} }
} }
} }
.Dc-middle-sider { .Dc-middle-sider {
padding-top: 10px;
height: 100%; height: 100%;
background: #fff; background: #fff;
transition: width ease-in-out 0.5s; transition: width ease-in-out 0.5s;
overflow: hidden; overflow: hidden;
.Dc-middle-editing {
height: 100%;
min-width: 320px;
.tabs-position();
/deep/ .ivu-tabs-content {
height: calc(100% - 48px);
overflow-y: scroll;
overflow-x: hidden;
}
}
} }
.Dcm-sider_none { .Dcm-sider_none {
position: absolute; position: absolute;
...@@ -204,6 +99,10 @@ ...@@ -204,6 +99,10 @@
} }
} }
} }
/deep/ .ivu-spin-fix {
z-index: 10001;
}
} }
</style> </style>
<script lang="ts" src="./index.ts"></script> <script lang="ts" src="./index.ts"></script>
import { Vue, Component, Emit } from 'vue-property-decorator'; import { Vue, Component, Emit } from 'vue-property-decorator';
import Layout from 'component/layout/editor/index.vue'; import Layout from 'component/layout/editor/index';
@Component({ @Component({
components: { components: {
......
<template> <template>
<div>
<QGTable <QGTable
ref="qgTable" ref="qgTable"
:columns="columns" :columns="columns"
...@@ -7,7 +6,6 @@ ...@@ -7,7 +6,6 @@
@newBtnClick="addPage" @newBtnClick="addPage"
> >
</QGTable> </QGTable>
</div>
</template> </template>
<script> <script>
import editorApi from '@api/editor.api'; import editorApi from '@api/editor.api';
...@@ -96,7 +94,7 @@ export default { ...@@ -96,7 +94,7 @@ export default {
on: { on: {
click: () => { click: () => {
console.log(params); console.log(params);
this.$router.push(`/detail/${params.row.id}`); this.$router.push(`/detail/${params.row.uuid}`);
}, },
}, },
}, },
......
<template> <template>
<div>
<QGTable <QGTable
ref="qgTable" ref="qgTable"
:columns="columns" :columns="columns"
...@@ -7,7 +6,6 @@ ...@@ -7,7 +6,6 @@
@newBtnClick="addPage" @newBtnClick="addPage"
> >
</QGTable> </QGTable>
</div>
</template> </template>
<script> <script>
import editorApi from '@api/editor.api'; import editorApi from '@api/editor.api';
...@@ -77,7 +75,7 @@ export default { ...@@ -77,7 +75,7 @@ export default {
title: '链接', title: '链接',
hideSearch: true, hideSearch: true,
render: (h, params) => { render: (h, params) => {
return h('span', `${config.apiHost}activity/${params.row.id}`) return h('span', `${config.h5Host}activity/${params.row.uuid}`)
} }
}, },
{ {
...@@ -119,7 +117,7 @@ export default { ...@@ -119,7 +117,7 @@ export default {
on: { on: {
click: () => { click: () => {
console.log(params); console.log(params);
this.$router.push(`/detail/${params.row.id}`); this.$router.push(`/detail/${params.row.uuid}`);
}, },
}, },
}, },
......
<template> <template>
<div>
<QGTable <QGTable
:columns="columns" :columns="columns"
:request="query" :request="query"
@newBtnClick="addPage" @newBtnClick="addPage"
> >
</QGTable> </QGTable>
</div>
</template> </template>
<script> <script>
import editorApi from '@api/editor.api'; import editorApi from '@api/editor.api';
...@@ -77,7 +75,7 @@ export default { ...@@ -77,7 +75,7 @@ export default {
title: '链接', title: '链接',
hideSearch: true, hideSearch: true,
render: (h, params) => { render: (h, params) => {
return h('span', `${config.apiHost}activity/${params.row.id}`) return h('span', `${config.h5Host}activity/${params.row.uuid}`)
} }
}, },
{ {
...@@ -106,7 +104,7 @@ export default { ...@@ -106,7 +104,7 @@ export default {
}, },
on: { on: {
click: () => { click: () => {
this.$router.push({ name: 'detail', params: { templateId: params.row.id } }); this.$router.push({ name: 'detail', params: { templateId: params.row.uuid } });
}, },
}, },
}, },
......
import { Component, Vue } from 'vue-property-decorator';
import { pick, omit, omitBy } 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 = {
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover'
};
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) {
style.backgroundImage = `url(${style.backgroundImage})`;
// style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
if (element !== 'container') {
style = omit(style, ['position', 'top', 'left']);
}
style = omitBy(style, v => !v);
return style;
}
}
\ No newline at end of file
...@@ -8,7 +8,7 @@ Vue.use(Vuex); ...@@ -8,7 +8,7 @@ Vue.use(Vuex);
export default function createStore(initState: any = {}) { export default function createStore(initState: any = {}) {
const { title, url, origin, locale, csrf, editor } = initState; const { title, url, origin, locale, csrf, editor } = initState;
console.log('store', title, url, origin, locale, csrf, editor); // console.log('store', title, url, origin, locale, csrf, editor);
const state = { title, url, origin, locale, csrf }; const state = { title, url, origin, locale, csrf };
return new Vuex.Store<RootState>({ return new Vuex.Store<RootState>({
state, state,
......
import api from '@/api/editor.api'; import api from '@/api/editor.api';
import { Module, GetterTree, ActionTree, MutationTree } from 'vuex'; import { Module, GetterTree, ActionTree, MutationTree } from 'vuex';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import Vue from 'vue';
import { import {
SET_PAGE_INFO, SET_PAGE_INFO,
SET_DRAGABLE, SET_DRAGABLE,
COPY_OR_DELETE_PAGE_INFO, COPY_OR_DELETE_PAGE_INFO,
UPDATE_PAGE_INFO, UPDATE_PAGE_INFO,
ADD_ELEMENTS, ADD_ELEMENTS,
DEL_ELEMENTS,
SET_CUR_ELE_INDEX, SET_CUR_ELE_INDEX,
SET_CUR_CHILD_INDEX, SET_CUR_CHILD_INDEX,
RESET_PAGE_DATA, RESET_PAGE_DATA,
SET_TEMPLATE_LIST, SET_TEMPLATE_LIST,
SET_PAGE_DATA, SET_PAGE_DATA,
UPDATE_COMMON_STYLE, UPDATE_COMMON_STYLE,
UPDATE_PAGE_STYLE,
UPDATE_PAGE_PROPS
} from './type'; } from './type';
import RootState from '../../state'; import RootState from '../../state';
...@@ -23,32 +25,58 @@ export default class EditorModule implements Module<EditorState, RootState> { ...@@ -23,32 +25,58 @@ export default class EditorModule implements Module<EditorState, RootState> {
state: EditorState; state: EditorState;
getters: GetterTree<EditorState, RootState> = { getters: GetterTree<EditorState, RootState> = {
pageData(state) { pageData(state: EditorState): Page {
return state.pageInfo.page; return state.pageInfo.page as Page;
}, },
pageId(state) { pageId(state: EditorState): number | undefined {
return state.pageInfo.id; return state.pageInfo.id;
}, },
pageInfo(state) { uuid(state: EditorState): string | undefined {
return state.pageInfo.uuid;
},
pageInfo(state: EditorState): PageInfo {
return state.pageInfo; return state.pageInfo;
} }
}; };
actions: ActionTree<EditorState, RootState> = { actions: ActionTree<EditorState, RootState> = {
async savePageData({ commit }, condition) { // pageInfo: 处理过的页面数据 - activity, pageData: 未处理的数据 - editor
if (condition.id) { async savePageData({ commit }, { pageInfo, pageData}: { pageInfo: PageInfo, pageData: Page }) {
await api.updatePage(condition); if (pageInfo.uuid) {
commit(SET_PAGE_INFO, { ...condition, page: JSON.parse(condition.page as string) }); await api.updatePage(pageInfo);
commit(SET_PAGE_INFO, { ...pageInfo, page: pageData });
} else { } else {
const res = await api.savePage(condition); const res = await api.savePage(pageInfo);
const { page, ...rest } = res as 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) { async getPageDate({ commit }, condition: number) {
const res = await api.getPageById(condition); const res = await api.getPageById(condition);
const { page, ...rest } = res as PageInfo; const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) }); const pageObj = JSON.parse(page as string);
commit(SET_PAGE_INFO, { ...rest, page: pageObj });
const goodsTabs = {};
(pageObj as Page).elements.forEach((element, idx) => {
if (element.name === 'cs-goods-tabs' && element?.child?.length) {
goodsTabs[idx] = cloneDeep(element.child);
// 逆向循环
for (let i = element.child.length - 1; i >= 0; i--) {
commit(COPY_OR_DELETE_PAGE_INFO, { type: 'delete', containerIndex: idx, childIndex: i });
// this.updatePageData({ type: 'delete', containerIndex: idx, childIndex: i });
}
}
});
let tempIndex = 0;
Object.keys(goodsTabs).forEach(key => {
goodsTabs[key].forEach((child, index) => {
commit(UPDATE_PAGE_INFO, { containerIndex: +key + 1 + index + tempIndex, data: child, type: 'add' });
// console.log('parseGoodsTabs', { containerIndex: +key + 1 + index, data: child });
// this.updatePageInfo({ containerIndex: +key + 1 + index, data: child });
});
tempIndex++;
});
}, },
async getTemplateList({ commit }) { async getTemplateList({ commit }) {
const res = await api.getTemplateList({ type: 'template' }); const res = await api.getTemplateList({ type: 'template' });
...@@ -114,10 +142,12 @@ export default class EditorModule implements Module<EditorState, RootState> { ...@@ -114,10 +142,12 @@ export default class EditorModule implements Module<EditorState, RootState> {
} }
} }
}, },
[UPDATE_PAGE_INFO](state, { containerIndex, childIndex, data }) { [UPDATE_PAGE_INFO](state, { containerIndex, childIndex, data, type }) {
const page = (state.pageInfo.page as Page).elements; const page = (state.pageInfo.page as Page).elements;
if (childIndex || childIndex === 0) { if (childIndex || childIndex === 0) {
page[containerIndex].child.splice(childIndex, 1, data); page[containerIndex].child.splice(childIndex, 1, data);
} else if (type === 'add') {
page.splice(containerIndex, 0, data);
} else { } else {
page.splice(containerIndex, 1, data); page.splice(containerIndex, 1, data);
} }
...@@ -125,11 +155,17 @@ export default class EditorModule implements Module<EditorState, RootState> { ...@@ -125,11 +155,17 @@ export default class EditorModule implements Module<EditorState, RootState> {
[UPDATE_COMMON_STYLE](state, {containerIndex, childIndex, data}) { [UPDATE_COMMON_STYLE](state, {containerIndex, childIndex, data}) {
const page = (state.pageInfo.page as Page).elements; const page = (state.pageInfo.page as Page).elements;
if (childIndex || childIndex === 0) { if (childIndex || childIndex === 0) {
page[containerIndex].child[childIndex].commonStyle = data; Vue.set(page[containerIndex].child[childIndex], 'commonStyle', data);
} else { } else {
page[containerIndex].commonStyle = data; Vue.set(page[containerIndex], 'commonStyle', data);
} }
}, },
[UPDATE_PAGE_STYLE](state, { data }) {
(state.pageInfo.page as Page).commonStyle = data;
},
[UPDATE_PAGE_PROPS](state, { data }) {
(state.pageInfo.page as Page).props = data;
},
[ADD_ELEMENTS](state, { containerIndex, data }) { [ADD_ELEMENTS](state, { containerIndex, data }) {
const page = (state.pageInfo.page as Page).elements; const page = (state.pageInfo.page as Page).elements;
if (containerIndex || containerIndex === 0) { if (containerIndex || containerIndex === 0) {
...@@ -138,10 +174,6 @@ export default class EditorModule implements Module<EditorState, RootState> { ...@@ -138,10 +174,6 @@ export default class EditorModule implements Module<EditorState, RootState> {
page.push(data); page.push(data);
} }
}, },
[DEL_ELEMENTS](state, { containerIndex }) {
const page = (state.pageInfo.page as Page).elements;
page.splice(containerIndex, 1);
},
}; };
constructor(initState: EditorState = cloneDeep(defaultState)) { constructor(initState: EditorState = cloneDeep(defaultState)) {
......
...@@ -14,13 +14,16 @@ interface Point { ...@@ -14,13 +14,16 @@ interface Point {
} }
interface CommonStyle { interface CommonStyle {
left: number; left?: number;
top: number; top?: number;
backgroundColor?: string;
backgroundImage?: string;
} }
interface GridLayout { export interface GridLayout {
draggable: boolean; draggable: boolean;
rowHeight: number; rowHeight: number;
colNum: number;
} }
export interface PageElement { export interface PageElement {
...@@ -34,6 +37,8 @@ export interface PageElement { ...@@ -34,6 +37,8 @@ export interface PageElement {
} }
export interface Page { export interface Page {
commonStyle: CommonStyle;
props: object;
elements: PageElement[]; elements: PageElement[];
} }
...@@ -43,8 +48,20 @@ export interface PageInfo { ...@@ -43,8 +48,20 @@ export interface PageInfo {
enable?: number; enable?: number;
author?: string; author?: string;
coverImage?: string; coverImage?: string;
isTemplate?: number; isTemplate?: number | boolean;
isPublish?: number | boolean; isPublish?: number | boolean;
pageName?: string;
pageDescribe?: string;
pageKeywords?: string;
uuid?: string;
}
export default interface EditorState {
pageInfo: PageInfo;
curEleIndex: number | null;
curChildIndex: number | null;
templateList: any[];
gridLayout: GridLayout;
} }
export const defaultState = { export const defaultState = {
...@@ -54,9 +71,19 @@ export const defaultState = { ...@@ -54,9 +71,19 @@ export const defaultState = {
id: 0, id: 0,
pageName: '', pageName: '',
pageDescribe: '', pageDescribe: '',
pageKeywords: '',
coverImage: 'http://activitystatic.q-gp.com/low_code.jpg', coverImage: 'http://activitystatic.q-gp.com/low_code.jpg',
isPublish: false, isPublish: false,
isTemplate: false,
uuid: '',
page: { page: {
commonStyle: {
backgroundColor: '#f7f8fa',
backgroundImage: ''
},
props: {
showBackTop: true,
},
elements: [], elements: [],
} }
}, },
...@@ -67,13 +94,3 @@ export const defaultState = { ...@@ -67,13 +94,3 @@ export const defaultState = {
colNum: 375 colNum: 375
}, },
}; };
export default interface EditorState {
pageInfo: PageInfo;
draggable: boolean;
curEleIndex: number | null;
curChildIndex: number | null;
templateList: any[];
rowHeight: number;
gridLayout: GridLayout;
}
\ No newline at end of file
...@@ -4,10 +4,11 @@ export const SET_DRAGABLE = 'SET_DRAGABLE'; ...@@ -4,10 +4,11 @@ export const SET_DRAGABLE = 'SET_DRAGABLE';
export const COPY_OR_DELETE_PAGE_INFO = 'COPY_OR_DELETE_PAGE_INFO'; export const COPY_OR_DELETE_PAGE_INFO = 'COPY_OR_DELETE_PAGE_INFO';
export const UPDATE_PAGE_INFO = 'UPDATE_PAGE_INFO'; export const UPDATE_PAGE_INFO = 'UPDATE_PAGE_INFO';
export const ADD_ELEMENTS = 'ADD_ELEMENTS'; 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_ELE_INDEX = 'SET_CUR_ELE_INDEX';
export const SET_CUR_CHILD_INDEX = 'SET_CUR_CHILD_INDEX'; export const SET_CUR_CHILD_INDEX = 'SET_CUR_CHILD_INDEX';
export const RESET_PAGE_DATA = 'RESET_PAGE_DATA'; export const RESET_PAGE_DATA = 'RESET_PAGE_DATA';
export const SET_TEMPLATE_LIST = 'SET_TEMPLATE_LIST'; export const SET_TEMPLATE_LIST = 'SET_TEMPLATE_LIST';
export const SET_PAGE_DATA = 'SET_PAGE_DATA'; export const SET_PAGE_DATA = 'SET_PAGE_DATA';
export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE'; export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE';
export const UPDATE_PAGE_STYLE = 'UPDATE_PAGE_STYLE';
export const UPDATE_PAGE_PROPS = 'UPDATE_PAGE_PROPS';
import Vue from 'vue';
export default new Vue();
\ No newline at end of file
import axios from 'axios'; import axios from 'axios';
import basicConfig from '../config'; import basicConfig from '../config';
import localStorage from './localStorage.service'; import localStorage from './localStorage.service';
import { Notify } from '@qg/cherry-ui'; import Notify from '@qg/cherry-ui/src/notify';
const ERR_MESSAGE_MAP = { const ERR_MESSAGE_MAP = {
status: { status: {
...@@ -25,8 +25,8 @@ const pending = {}; ...@@ -25,8 +25,8 @@ const pending = {};
let reqNum = 0; let reqNum = 0;
axios.defaults.baseURL = basicConfig.apiHost; axios.defaults.baseURL = basicConfig.apiHost;
axios.defaults.timeout = 30000; axios.defaults.timeout = 30000;
axios.defaults.xsrfHeaderName = 'x-csrf-token'; // axios.defaults.xsrfHeaderName = 'x-csrf-token';
axios.defaults.xsrfCookieName = 'csrfToken'; // axios.defaults.xsrfCookieName = 'csrfToken';
const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds)); const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds));
...@@ -48,17 +48,48 @@ function clearRequest() { ...@@ -48,17 +48,48 @@ function clearRequest() {
} }
} }
let isRefreshing = true;
let subscriber: Array<() => void> = [];
const addSubscriber = callback => subscriber.push(callback);
const onAccessTokenFetched = () => {
subscriber.forEach(callback => callback());
subscriber = [];
};
const refreshtoken = async () => {
return instance.get(`${basicConfig.opapiHost}/backStms/oauth/refreshtoken?refreshtoken=${localStorage.get('refreshToken')}`);
};
/*
* 刷新token逻辑
*/
const refreshRequest = async options => {
const promise = new Promise(resolve => addSubscriber(() => resolve(instance.request(options))));
if (isRefreshing) {
isRefreshing = false;
await wait(500);
const data = await refreshtoken();
localStorage.set('token', data.accessToken);
localStorage.set('refreshToken', data.refreshToken);
onAccessTokenFetched();
isRefreshing = true;
}
return promise;
};
const instance = axios.create(); const instance = axios.create();
// 请求拦截器 // 请求拦截器
instance.interceptors.request.use( instance.interceptors.request.use(
config => { config => {
// op-api识别API使用
config.headers['X-Requested-With'] = 'XMLHttpRequest';
// beforeRequest(); // beforeRequest();
// 发起请求时,取消掉当前正在进行的相同请求 // 发起请求时,取消掉当前正在进行的相同请求
if (pending[config.url as string]) { if (pending[config.url as string]) {
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 // 添加token
const token = localStorage.get('token'); const token = localStorage.get('token');
if (token) { if (token) {
...@@ -83,6 +114,10 @@ instance.interceptors.response.use( ...@@ -83,6 +114,10 @@ instance.interceptors.response.use(
const codeArr = ['0000', 2000]; const codeArr = ['0000', 2000];
if (codeArr.includes(response.data.code)) { return response.data.data; } if (codeArr.includes(response.data.code)) { return response.data.data; }
if (response.data.code === '0001' && response.config.url.includes('oauth/refreshtoken')) {
window.location.href = `${window.location.origin}/${window.__POWERED_BY_QIANKUN__ ? 'access' : 'editor'}/login`;
}
// 后端返回异常信息时提出警告 // 后端返回异常信息时提出警告
if (response.data.code && response.data.msg) { notifyType = 'warning'; } if (response.data.code && response.data.msg) { notifyType = 'warning'; }
...@@ -118,6 +153,18 @@ instance.interceptors.response.use( ...@@ -118,6 +153,18 @@ instance.interceptors.response.use(
return axios.request(err.config); return axios.request(err.config);
} }
if (err.response) {
switch (err.response.status) {
case 401:
if (!err.response.config.url.includes('oauth/refreshtoken')) {
return refreshRequest(err.response.config);
} else {
window.location.href = `${window.location.origin}/${window.__POWERED_BY_QIANKUN__ ? 'access' : 'editor'}/login`;
}
return;
}
}
// 错误提示 // 错误提示
let message = ''; let message = '';
if (err.response) { if (err.response) {
......
import Vue from 'vue'; import Vue from 'vue';
import { // import {
Button, // Button,
Image, // Image,
Icon, // Icon,
Cell, // Cell,
CellGroup, // CellGroup,
Row, // Row,
Col, // Col,
Dialog, // Dialog,
Popup, // Popup,
Overlay, // Overlay,
Divider, // Divider,
Loading, // Loading,
Picker, // Picker,
NavBar, // NavBar,
Field, // Field,
Checkbox, // Checkbox,
CardList, // CardList,
List, // List,
Form, // Form,
Sticky, // Sticky,
Tab, // Tab,
Tabs // Tabs,
} from '@qg/cherry-ui'; // Notify,
// import { KaLoginForm } from '@qg/citrus-ui'; // Swipe,
// SwipeItem,
// Toast,
// BackTop
// } from '@qg/cherry-ui';
import Button from '@qg/cherry-ui/src/button';
import Image from '@qg/cherry-ui/src/image';
import Field from '@qg/cherry-ui/src/field';
import Form from '@qg/cherry-ui/src/form';
import Tab from '@qg/cherry-ui/src/tab';
import Tabs from '@qg/cherry-ui/src/tabs';
import BackTop from '@qg/cherry-ui/src/back-top';
import Notify from '@qg/cherry-ui/src/notify';
import Toast from '@qg/cherry-ui/src/toast';
import Text from '@qg/cherry-ui/src/text';
import Cell from '@qg/cherry-ui/src/cell';
import Checkbox from '@qg/cherry-ui/src/checkbox';
import CountDown from '@qg/cherry-ui/src/count-down';
import Divider from '@qg/cherry-ui/src/divider';
import Icon from '@qg/cherry-ui/src/icon';
import NavBar from '@qg/cherry-ui/src/nav-bar';
import NoticeBar from '@qg/cherry-ui/src/notice-bar';
import Progress from '@qg/cherry-ui/src/progress';
import Tag from '@qg/cherry-ui/src/tag';
import citrusUi from '@qg/citrus-ui';
Vue.use(Button); Vue.use(Button);
Vue.use(Image); Vue.use(Image);
// Vue.use(Cell); Vue.use(Cell);
// Vue.use(CellGroup); Vue.use(Divider);
// Vue.use(Row);
// Vue.use(Col);
// Vue.use(Popup);
// Vue.use(Divider);
// Vue.use(Picker);
Vue.use(Field); Vue.use(Field);
// Vue.use(Dialog); Vue.use(Checkbox);
// Vue.use(Checkbox); Vue.use(Icon);
// Vue.use(Icon);
// Vue.use(Sticky);
// Vue.use(Overlay);
Vue.use(Form); Vue.use(Form);
// Vue.use(CardList); Vue.use(Tab);
// Vue.use(Loading); Vue.use(Tabs);
// Vue.use(List); Vue.use(BackTop);
// Vue.use(Tab); Vue.use(Text);
// Vue.use(Tabs); Vue.use(CountDown);
Vue.use(NavBar);
Vue.use(NoticeBar);
Vue.use(Progress);
Vue.use(Tag);
Vue.use(citrusUi);
// Vue.use(KaLoginForm); Vue.prototype.$notify = Notify;
Vue.prototype.$toast = Toast;
...@@ -46,9 +46,9 @@ export function resizeDiv(imgUrl, clientHeight = 0, clientWidth = 0, callback) { ...@@ -46,9 +46,9 @@ export function resizeDiv(imgUrl, clientHeight = 0, clientWidth = 0, callback) {
img.onload = function() { img.onload = function() {
const imgRealHeight = clientWidth * img.height / img.width; const imgRealHeight = clientWidth * img.height / img.width;
console.log(img.width, img.height, imgRealHeight, clientHeight); console.log(img.width, img.height, imgRealHeight, clientHeight);
if (imgRealHeight > clientWidth) { // if (imgRealHeight > clientWidth) {
callback(imgRealHeight); callback(imgRealHeight);
} // }
}; };
img.src = imgUrl; img.src = imgUrl;
} }
...@@ -93,3 +93,28 @@ export const getStyle = function(oElement, sName) { ...@@ -93,3 +93,28 @@ export const getStyle = function(oElement, sName) {
const result = oElement.currentStyle ? oElement.currentStyle[sName] : getComputedStyle(oElement, null)[sName]; const result = oElement.currentStyle ? oElement.currentStyle[sName] : getComputedStyle(oElement, null)[sName];
return result.includes('px') ? result.slice(0, -2) : result; 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;
};
...@@ -12,12 +12,17 @@ ...@@ -12,12 +12,17 @@
], ],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@asset/*": ["asset/*"],
"@component/*": ["component/*"], "@component/*": ["component/*"],
"@framework/*": ["framework/*"], "@framework/*": ["framework/*"],
"@store/*": ["page/store/*"], "@store/*": ["page/store/*"],
"@router/*": ["page/admin/home/router/*"], "@router/*": ["page/admin/home/router/*"],
"@view/*": ["page/admin/home/view/*"] "@view/*": ["page/admin/home/view/*"],
"@editor/*": ["page/editor/*"],
"@config/*": ["config/*"],
"@page/*": ["page/*"],
"@lib/*": ["lib/*"],
"@service/*": ["service/*"],
"@api/*": ["api/*"],
} }
}, },
"include": [ "include": [
......
...@@ -9,6 +9,11 @@ interface Window { ...@@ -9,6 +9,11 @@ interface Window {
declare module 'axios' { declare module 'axios' {
export interface AxiosRequestConfig { export interface AxiosRequestConfig {
accessToken?: boolean; accessToken?: string;
}
export interface AxiosResponse {
accessToken?: string;
refreshToken?: string;
} }
} }
\ No newline at end of file
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" style="font-size: 37.5px;"> <html lang="en" >
<head> <head>
<title>低代码平台</title> <title>低代码平台</title>
<meta name="keywords"> <meta name="keywords">
...@@ -7,8 +7,13 @@ ...@@ -7,8 +7,13 @@
<meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/public/asset/css/reset.css"> <link href="https://activitystatic.lkbang.net/iview/2.9.0/styles/iview.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://activitystatic.lkbang.net/swiper/4.5.1/css/swiper.min.css">
<style> <style>
html {
font-size: 37.5Px !important;
}
html, body { html, body {
line-height: 1.15 !important; line-height: 1.15 !important;
} }
...@@ -16,6 +21,13 @@ ...@@ -16,6 +21,13 @@
</head> </head>
<body> <body>
<div id="app"><!--vue-ssr-outlet--></div> <div id="app"><!--vue-ssr-outlet--></div>
<script src="https://cdn.staticfile.org/plupload/2.1.2/plupload.full.min.js"></script> <script src="https://activitystatic.lkbang.net/plupload/2.1.2/plupload.full.min.js"></script>
<script src="https://activitystatic.lkbang.net/vue/2.6.12/vue.js"></script>
<script src="https://activitystatic.lkbang.net/iview/2.9.0/iview.min.js"></script>
<script src="https://activitystatic.lkbang.net/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://activitystatic.lkbang.net/vuex/3.4.0/vuex.min.js"></script>
<script src="https://activitystatic.lkbang.net/sa-sdk-javascript/1.15.16/sensorsdata.min.js"></script>
<script src="https://activitystatic.lkbang.net/swiper/4.5.1/swiper.min.js"></script>
<script src="https://activitystatic.lkbang.net/axios/0.19.2/axios.min.js"></script>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
"use strict"; "use strict";
const npm = require("npm"); const npm = require("npm");
npm.load(() => { npm.load(() => {
if (process.env.NODE_ENV === 'production') {
npm.run("start"); npm.run("start");
} else {
npm.run("test");
}
}); });
\ No newline at end of file
...@@ -36,5 +36,16 @@ export default (appInfo: EggAppConfig) => { ...@@ -36,5 +36,16 @@ export default (appInfo: EggAppConfig) => {
'access' 'access'
]; ];
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
};
config.security = {
csrf: {
enable: false
}
};
return config; return config;
}; };
...@@ -33,6 +33,18 @@ export default (appInfo: EggAppConfig) => { ...@@ -33,6 +33,18 @@ export default (appInfo: EggAppConfig) => {
exports.sequelize = localMysqlConfig; exports.sequelize = localMysqlConfig;
exports.redis = {
default: {
keyPrefix: appInfo.name + ':',
},
client: {
port: 31565, // Redis port
host: '172.17.5.13', // Redis host
password: '',
db: 0
}
};
exports.webpack = { exports.webpack = {
webpackConfigList: getWebpackConfig(), webpackConfigList: getWebpackConfig(),
browser: 'http://localhost:7001/editor/list' browser: 'http://localhost:7001/editor/list'
......
...@@ -35,5 +35,31 @@ export default (appInfo: EggAppConfig) => { ...@@ -35,5 +35,31 @@ export default (appInfo: EggAppConfig) => {
exports.sequelize = process.env.NAMESPACE ? localMysqlConfig : prodMysqlConfig; exports.sequelize = process.env.NAMESPACE ? localMysqlConfig : prodMysqlConfig;
const prodRedisConfig = {
default: {
keyPrefix: appInfo.name + ':',
},
client: {
port: 6379, // Redis port
host: 'front-redis.quantgroups.com', // Redis host
password: 'ln4^ESq80j4nrTMZ',
db: 0
}
};
const localRedisConfig = {
default: {
keyPrefix: appInfo.name + ':',
},
client: {
port: 31565, // Redis port
host: '172.17.5.13', // Redis host
password: '',
db: 0
}
};
exports.redis = process.env.NAMESPACE ? localRedisConfig: prodRedisConfig;
return exports; return exports;
}; };
export default { export default {
cors: {
package: 'egg-cors'
},
webpack: { webpack: {
package: 'egg-webpack' package: 'egg-webpack'
}, },
......
export default { export default {
cors: {
package: 'egg-cors'
},
vuessr: { vuessr: {
package: 'egg-view-vue-ssr' package: 'egg-view-vue-ssr'
}, },
sequelize: { sequelize: {
enable: true, enable: true,
package: 'egg-sequelize-ts' package: 'egg-sequelize-ts'
},
redis: {
enable: true,
package: 'egg-redis'
} }
}; };
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"description": "低代码平台", "description": "低代码平台",
"scripts": { "scripts": {
"start": "egg-scripts start --port 9050 --workers 1", "start": "egg-scripts start --port 9050 --workers 1",
"test": "egg-scripts start --port 80 --workers 1",
"stop": "egg-scripts stop", "stop": "egg-scripts stop",
"backend": "nohup egg-scripts start --port 7001 --workers 4", "backend": "nohup egg-scripts start --port 7001 --workers 4",
"dev": "egg-bin dev -r egg-ts-helper/register", "dev": "egg-bin dev -r egg-ts-helper/register",
...@@ -13,7 +14,7 @@ ...@@ -13,7 +14,7 @@
"clean": "ets clean", "clean": "ets clean",
"kill": "easy kill", "kill": "easy kill",
"lint": "tslint --project . -c tslint.json", "lint": "tslint --project . -c tslint.json",
"fix": "tslint --fix --project . -c tslint.json 'app/web/**/*{.ts}'", "fix": "tslint --fix --project . -c tslint.json 'app/**/*{.ts}'",
"ii": "npm install --registry https://registry.npm.taobao.org", "ii": "npm install --registry https://registry.npm.taobao.org",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}, },
...@@ -23,17 +24,19 @@ ...@@ -23,17 +24,19 @@
"@easy-team/easywebpack-cli": "^4.0.0", "@easy-team/easywebpack-cli": "^4.0.0",
"@easy-team/easywebpack-vue": "^4.0.0", "@easy-team/easywebpack-vue": "^4.0.0",
"@hubcarl/json-typescript-mapper": "^2.0.0", "@hubcarl/json-typescript-mapper": "^2.0.0",
"@qg/cherry-ui": "^2.18.66", "@qg/cherry-ui": "^2.20.6",
"@qg/citrus-ui": "0.0.1-alpha.2", "@qg/citrus-ui": "0.0.63",
"@riophae/vue-treeselect": "^0.4.0",
"@types/lodash": "^4.14.117", "@types/lodash": "^4.14.117",
"@types/node": "^10.12.0", "@types/node": "^10.12.0",
"@types/validator": "^4.5.26", "@types/validator": "^4.5.26",
"axios": "^0.18.1", "axios": "^0.21.1",
"babel-plugin-transform-vue-jsx": "^4.0.1", "babel-plugin-transform-vue-jsx": "^4.0.1",
"cz-conventional-changelog": "^2.1.0", "cz-conventional-changelog": "^2.1.0",
"egg": "^2.3.0", "egg": "^2.3.0",
"egg-bin": "^4.9.0", "egg-bin": "^4.9.0",
"egg-cors": "^2.1.1", "egg-cors": "^2.1.1",
"egg-redis": "^2.4.0",
"egg-scripts": "^2.10.0", "egg-scripts": "^2.10.0",
"egg-sequelize-ts": "^0.2.3-0", "egg-sequelize-ts": "^0.2.3-0",
"egg-ts-helper": "^1.13.0", "egg-ts-helper": "^1.13.0",
......
...@@ -2,12 +2,10 @@ ...@@ -2,12 +2,10 @@
// Do not modify this file!!!!!!!!! // Do not modify this file!!!!!!!!!
import 'egg'; import 'egg';
import ExportActivity from '../../../app/controller/activity';
import ExportEditor from '../../../app/controller/editor'; import ExportEditor from '../../../app/controller/editor';
declare module 'egg' { declare module 'egg' {
interface IController { interface IController {
activity: ExportActivity;
editor: ExportEditor; editor: ExportEditor;
} }
} }
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
type AnyClass = new (...args: any[]) => any;
type AnyFunc<T = any> = (...args: any[]) => T;
type CanExportFunc = AnyFunc<Promise<any>> | AnyFunc<IterableIterator<any>>;
type AutoInstanceType<T, U = T extends CanExportFunc ? T : T extends AnyFunc ? ReturnType<T> : T> = U extends AnyClass ? InstanceType<U> : U;
import ExportRedis from '../../../app/service/redis';
declare module 'egg' {
interface IService {
redis: AutoInstanceType<typeof ExportRedis>;
}
}
...@@ -14,9 +14,10 @@ import 'egg-schedule'; ...@@ -14,9 +14,10 @@ import 'egg-schedule';
import 'egg-static'; import 'egg-static';
import 'egg-jsonp'; import 'egg-jsonp';
import 'egg-view'; import 'egg-view';
import 'egg-cors';
import 'egg-view-vue-ssr'; import 'egg-view-vue-ssr';
import 'egg-sequelize-ts'; import 'egg-sequelize-ts';
import 'egg-cors'; import 'egg-redis';
import 'egg-webpack'; import 'egg-webpack';
import 'egg-webpack-vue'; import 'egg-webpack-vue';
import { EggPluginItem } from 'egg'; import { EggPluginItem } from 'egg';
...@@ -34,9 +35,10 @@ declare module 'egg' { ...@@ -34,9 +35,10 @@ declare module 'egg' {
static?: EggPluginItem; static?: EggPluginItem;
jsonp?: EggPluginItem; jsonp?: EggPluginItem;
view?: EggPluginItem; view?: EggPluginItem;
cors?: EggPluginItem;
vuessr?: EggPluginItem; vuessr?: EggPluginItem;
sequelize?: EggPluginItem; sequelize?: EggPluginItem;
cors?: EggPluginItem; redis?: EggPluginItem;
webpack?: EggPluginItem; webpack?: EggPluginItem;
webpackvue?: EggPluginItem; webpackvue?: EggPluginItem;
} }
......
...@@ -3,12 +3,18 @@ const path = require('path'); ...@@ -3,12 +3,18 @@ const path = require('path');
const SentryPlugin = require("webpack-sentry-plugin"); const SentryPlugin = require("webpack-sentry-plugin");
const resolve = filepath => path.resolve(__dirname, filepath); const resolve = filepath => path.resolve(__dirname, filepath);
const SentryConfig = require("./app/web/.sentryclirc.ts"); const SentryConfig = require("./app/web/.sentryclirc.ts");
let { name } = require('./package');
name = name.replace(/-/g, '');
module.exports = { module.exports = {
entry: { entry: {
'login': 'app/web/page/login/index.vue', 'login': 'app/web/page/login/index.vue',
'editor': 'app/web/page/editor/index.ts', 'editor': 'app/web/page/editor/index.ts',
'activity': 'app/web/page/activity/index.ts', },
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}, },
resolve: { resolve: {
alias:{ alias:{
...@@ -18,36 +24,32 @@ module.exports = { ...@@ -18,36 +24,32 @@ module.exports = {
'@framework': resolve('app/web/framework'), '@framework': resolve('app/web/framework'),
'@component': resolve('app/web/component'), '@component': resolve('app/web/component'),
'@store': resolve('app/web/page/store'), '@store': resolve('app/web/page/store'),
// '@router': resolve('app/web/page/admin/home/router'), '@editor': resolve('app/web/page/editor'),
// '@view': resolve('app/web/page/admin/home/view'), '@config': resolve('app/web/config'),
'@editor': resolve('app/web/page/editor') '@page': resolve('app/web/page'),
'@lib': resolve('app/web/lib'),
'@service': resolve('app/web/service'),
} }
}, },
nodeExternals: { nodeExternals: {
whitelist: [ moduleName => { whitelist: [ moduleName => {
if (moduleName.includes('cherry-ui') || moduleName.includes('@interactjs')) { return /cherry-ui/.test(moduleName) || /citrus-ui/.test(moduleName) || /@interactjs/.test(moduleName);
console.log(moduleName);
}
return /cherry-ui/.test(moduleName) || /@interactjs/.test(moduleName);
}] }]
}, },
module:{ module:{
rules:[ rules:[
{ babel: { { 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: [] exclude: []
} }
}, },
{ {
vue: { 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: []
}
},
{ ts: {
exclude: [] exclude: []
} }
}, },
{ ts: true },
{ less: true }, { less: true },
{ tslint: { options: { fix: true } } }, { tslint: { options: { fix: true } } },
] ]
...@@ -162,6 +164,15 @@ module.exports = { ...@@ -162,6 +164,15 @@ module.exports = {
} }
} }
}, },
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
// 'iview': 'iview',
'vuex': 'Vuex',
'axios': 'axios',
'sa-sdk-javascript': 'sensorsDataAnalytic201505',
'swiper': 'Swiper',
},
devtool:'source-map', devtool:'source-map',
customize(webpackConfig){ customize(webpackConfig){
// 此外 webpackConfig 为原生生成的 webpack config,可以进行自定义处理 // 此外 webpackConfig 为原生生成的 webpack config,可以进行自定义处理
......
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