Commit 91ca76b1 authored by 徐光星's avatar 徐光星

Merge branch 'master' of git.quantgroup.cn:ui/quantum-blocks into feat/multiActivity

parents 676d4f0d d3661513
import { Controller, Context } from 'egg';
import _, { omitBy } from 'lodash';
import { v1 as uuidv1 } from 'uuid';
import { PageHistory } from '../model/pageHistory';
import * as path from 'path';
const { api } = require(path.resolve('./config/apollo.json'));
export default class EditorController extends Controller {
public async login(ctx: Context) {
......@@ -16,9 +19,11 @@ export default class EditorController extends Controller {
const pageInfo = ctx.request.body;
try {
const result = await ctx.model.PageInfo.create({ ...pageInfo, uuid: uuidv1().replace(/-/g, ''), tenantId: ctx.headers['qg-tenant-id']});
const { uuid, id } = result.dataValues;
await ctx.model.PageHistory.create({ ...pageInfo, uuid, pageId: id, tenantId: ctx.headers['qg-tenant-id']});
ctx.body = ctx.helper.ok(result);
await ctx.service.redis.del(`pageInfo:${result.dataValues.uuid}`);
await ctx.service.redis.del(`page:${result.dataValues.uuid}`);
await ctx.service.redis.del(`pageInfo:${uuid}`);
await ctx.service.redis.del(`page:${uuid}`);
} catch (error) {
if (error.message.indexOf('Data too long') > -1 && error.message.indexOf('page_data') > -1) {
error.message = '配置组件过多,请对组件进行删减';
......@@ -35,10 +40,14 @@ export default class EditorController extends Controller {
if (ctx.headers['qg-tenant-id']) {
pageInfo.tenantId = +ctx.headers['qg-tenant-id'];
}
const uuid = pageInfo.uuid;
try {
const result = await ctx.model.PageInfo.update(pageInfo, {where: { uuid: pageInfo.uuid, tenantId: ctx.headers['qg-tenant-id'] }});
await ctx.service.redis.del(`pageInfo:${pageInfo.uuid}`);
await ctx.service.redis.del(`page:${pageInfo.uuid}`);
const pageData = await ctx.model.PageInfo.findOne({where: { uuid }});
const pageId = pageData.id;
await ctx.model.PageHistory.create({ ...pageInfo, uuid, pageId, tenantId: ctx.headers['qg-tenant-id']});
await ctx.service.redis.del(`pageInfo:${uuid}`);
await ctx.service.redis.del(`page:${uuid}`);
ctx.body = ctx.helper.ok(result);
} catch (error) {
if (error.message.indexOf('Data too long') > -1 && error.message.indexOf('page_data') > -1) {
......@@ -51,10 +60,9 @@ export default class EditorController extends Controller {
public async get(ctx: Context) {
const { isEditor } = ctx.query;
let pageInfo = isEditor ? null : await ctx.service.redis.get(`pageInfo:${ctx.params.uuid}`);
console.log('redis', pageInfo);
if (!pageInfo) {
pageInfo = await ctx.model.PageInfo.findOne({where: { uuid: ctx.params.uuid }});
console.log('sql', pageInfo);
// console.log('sql', pageInfo);
if (ctx.query.lite && pageInfo) {
// 如果存在sheme移除掉
......@@ -73,13 +81,13 @@ export default class EditorController extends Controller {
}
public async getList(ctx: Context) {
const { type, pageSize, pageNo, author, pageName, isPublish, pageDescribe } = ctx.query;
const { like } = ctx.model.Sequelize.Op;
const { type, pageSize, pageNo, author, pageName, isPublish, pageDescribe, uuid } = ctx.query;
let where = omitBy({
author: author && { like: `%${author}%`},
pageName: pageName && { like: `%${pageName}%`},
pageDescribe: pageDescribe && { like: `%${pageDescribe}%`},
isPublish,
uuid,
tenantId: ctx.headers['qg-tenant-id'],
enable: 1
}, v => !v);
......@@ -90,8 +98,17 @@ export default class EditorController extends Controller {
} else if (type === 'template') {
where = { ...where, isTemplate: 1 };
}
const { count: total, rows: data } = await ctx.model.PageInfo.findAndCountAll({ where, limit: +pageSize || 10,
offset: (+pageNo - 1) * +pageSize || 0, order: [['updated_at', 'DESC']] });
const { count: total, rows: data } = await ctx.model.PageInfo.findAndCountAll({
where,
limit: +pageSize || 10,
offset: (+pageNo - 1) * +pageSize || 0,
order: [['updated_at', 'DESC']],
attributes: ['id', 'uuid', 'author', 'updated_at', 'isTemplate', 'coverImage', 'pageDescribe', 'pageName', 'tenantId'],
include: [PageHistory.scope('relative')]
});
data.map(item => {
item.pageHistory && item.pageHistory.reverse();
})
ctx.body = ctx.helper.ok({ total, data });
}
......@@ -124,6 +141,100 @@ export default class EditorController extends Controller {
public async delete(ctx: Context) {
const pageInfo = await ctx.model.PageInfo.update({ enable: 0 }, {where: { id: +ctx.params.pageId, tenantId: ctx.headers['qg-tenant-id'] }});
const pageData = await ctx.model.PageInfo.find({where: { id: +ctx.params.pageId, tenantId: ctx.headers['qg-tenant-id'] }});
await ctx.model.PageHistory.destroy({where: { pageId: pageData.dataValues.id }});
ctx.body = ctx.helper.ok(pageInfo);
}
public async getServerTime(ctx: Context) {
ctx.body = ctx.helper.ok(new Date().getTime());
}
// 恢复历史记录
public async recoverPageData(ctx: Context) {
const params = ctx.request.body;
const id = params.id;
const author = params.author;
if (!id) {
ctx.body = {};
return;
}
const pageData = await ctx.model.PageHistory.findOne({where: {id: +id, tenantId: ctx.headers['qg-tenant-id']}})
const data = Object.assign({}, pageData.dataValues);
// 记录当前操作人
data.author = author;
delete data.updatedAt;
delete data.createdAt;
// 更新主表行数据
const uuid = pageData.uuid;
const updateData = Object.assign({}, data);
delete updateData.uuid;
delete updateData.pageId;
delete updateData.id;
await ctx.model.PageInfo.update(updateData, {where: { uuid: uuid, tenantId: ctx.headers['qg-tenant-id'] }});
await ctx.service.redis.del(`pageInfo:${uuid}`);
await ctx.service.redis.del(`page:${uuid}`);
// 创建新历史记录
const historyData = Object.assign({}, data);
delete historyData.id;
await ctx.model.PageHistory.create(historyData);
ctx.body = ctx.helper.ok({});
}
// 获取历史记录页面预览信息
public async getHistoryPreviewData(ctx: Context) {
const pageInfo = await ctx.model.PageHistory.findOne({where: { id: ctx.params.id }});
// console.log('sql', pageInfo);
if (ctx.query.lite && pageInfo) {
// 如果存在sheme移除掉
const page = JSON.parse(pageInfo.page || []);
for (let i = 0; i < page.elements.length; i++) {
delete page.elements[i].schame
}
if (page.scheme) {
delete page.scheme;
}
pageInfo.page = JSON.stringify(page);
}
ctx.body = ctx.helper.ok(pageInfo);
}
public async getListForActivityCalendar(ctx: Context) {
let { exposureTime } = ctx.request.body;
exposureTime = exposureTime.replace(/-/g, '/').split(' ')[0]
console.log(exposureTime);
// let where:any = omitBy({
// // validStartTime: {
// // [ctx.model.Sequelize.Op.lte]: exposureTime
// // },
// // validEndTime: {
// // [ctx.model.Sequelize.Op.gte]: exposureTime
// // },
// enable: 1,
// isPublish: 1,
// tenantId: '560761',
// }, v => !v);
// where[ctx.model.Sequelize.fn('validStartTime', '%Y/%m/%d')] = {
// [ctx.model.Sequelize.Op.lte]: exposureTime
// }
// where[ctx.model.Sequelize.fn('validEndTime', '%Y/%m/%d')] = {
// [ctx.model.Sequelize.Op.gte]: exposureTime
// }
// const { rows: data } = await ctx.model.PageInfo.findAndCountAll({
// where,
// order: [['updated_at', 'DESC']],
// attributes: ['id', 'uuid', 'pageName', 'tenantId', 'validStartTime', 'validEndTime']
// });
const data = await ctx.model.query(
"SELECT `id`, `uuid`, `page_name` AS `pageName`, `tenant_id` AS `tenantId`, `validStartTime`, `validEndTime` FROM page_config_info WHERE DATE_FORMAT(`validStartTime`, '%Y/%m/%d') <= '"+ exposureTime +"' AND DATE_FORMAT(`validEndTime`, '%Y/%m/%d') >= '" + exposureTime + "' AND `page_config_info`.`enable` = 1 AND `page_config_info`.`is_publish` = 1 AND `page_config_info`.`tenant_id` = '560761'", {
type: ctx.model.QueryTypes.SELECT
}
)
const res = data.map(item => {
item.url = `${api.h5Host}/activity/${item.uuid}?tenantId=${item.tenantId}&vccToken={token}&appChannel={appChannel}`;
item.uuid && delete item.uuid;
item.tenantId && delete item.tenantId;
return item;
})
ctx.body = ctx.helper.ok(res);
}
}
\ No newline at end of file
/**
* @desc 页面历史记录表
*/
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull, ForeignKey, BelongsTo, DefaultScope, Scopes } from 'sequelize-typescript';
import { PageInfo } from './pageInfo';
@Scopes({
relative: {
attributes: ['id', 'author', 'updatedAt', 'createdAt']
}
})
@Table({
modelName: 'page_history',
freezeTableName: true
})
export class PageHistory extends Model<PageHistory> {
@PrimaryKey
@AutoIncrement
@Column({
type: DataType.INTEGER(11)
})
id: number;
@Column({
field: 'page_data',
type: DataType.TEXT
})
page: string;
@Column({
field: 'page_name',
type: DataType.STRING(32)
})
pageName: string;
@Column({
field: 'page_describe',
type: DataType.STRING(255)
})
pageDescribe: string;
@Column({
field: 'page_keywords',
type: DataType.STRING(255)
})
pageKeywords: string;
@Column({
field: 'uuid',
type: DataType.UUID
})
uuid: string;
// @BelongsTo(() => PageInfo)
// pageInfo: PageInfo;
@Column({
field: 'redirectUrl',
type: DataType.STRING(255),
get() {
const redirectUrl = this.getDataValue('redirectUrl');
return !redirectUrl ? '' : redirectUrl;
},
})
redirectUrl: string;
@Column({
field: 'validEndTime',
type: DataType.STRING(32),
get() {
const validEndTime = this.getDataValue('validEndTime');
return !validEndTime ? '' : validEndTime;
},
})
validEndTime: string;
@Column({
field: 'validStartTime',
type: DataType.STRING(32),
get() {
const validStartTime = this.getDataValue('validStartTime');
return !validStartTime ? '' : validStartTime;
},
})
validStartTime: string;
@Column({
type: DataType.INTEGER(1)
})
enable: number;
@AllowNull(false)
@Column({
type: DataType.STRING(32)
})
author: string;
@Column({
type: DataType.STRING(32),
field: 'cover_image'
})
coverImage: string;
@Column({
type: DataType.INTEGER(1),
field: 'is_template'
})
isTemplate: number;
@Column({
type: DataType.INTEGER(1),
field: 'is_publish'
})
isPublish: number;
@Column({
type: DataType.DATE,
field: 'updated_at',
get() {
const moment = require('moment');
const updatedAt = this.getDataValue('updatedAt');
return moment(updatedAt).utcOffset('+08:00').format('YYYY-MM-DD HH:mm:ss');
},
})
updatedAt: string;
@Column({
type: DataType.DATE,
field: 'created_at',
get() {
const moment = require('moment');
const createdAt = this.getDataValue('createdAt');
return moment(createdAt).format('YYYY-MM-DD HH:mm:ss');
},
})
createdAt: string;
@ForeignKey(() => PageInfo)
@Column({
field: 'page_id',
type: DataType.INTEGER(11)
})
pageId: number;
@Column({
type: DataType.INTEGER(11),
field: 'tenant_id',
})
tenantId: number;
}
export default () => PageHistory;
\ No newline at end of file
/**
* @desc 页面信息表
*/
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull } from 'sequelize-typescript';
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull, HasMany } from 'sequelize-typescript';
import { PageHistory } from './pageHistory';
@Table({
modelName: 'page_config_info',
freezeTableName: true
......@@ -47,6 +47,36 @@ export class PageInfo extends Model<PageInfo> {
})
uuid: string;
@Column({
field: 'redirectUrl',
type: DataType.STRING(255),
get() {
const redirectUrl = this.getDataValue('redirectUrl');
return !redirectUrl ? '' : redirectUrl;
},
})
redirectUrl: string;
@Column({
field: 'validEndTime',
type: DataType.STRING(32),
get() {
const validEndTime = this.getDataValue('validEndTime');
return !validEndTime ? '' : validEndTime;
},
})
validEndTime: string;
@Column({
field: 'validStartTime',
type: DataType.STRING(32),
get() {
const validStartTime = this.getDataValue('validStartTime');
return !validStartTime ? '' : validStartTime;
},
})
validStartTime: string;
@Column({
type: DataType.INTEGER(1)
})
......@@ -81,7 +111,7 @@ export class PageInfo extends Model<PageInfo> {
field: 'updated_at',
get() {
const moment = require('moment');
const updatedAt = this.getDataValue('updatedAt');
const updatedAt = this.getDataValue('updated_at');
return moment(updatedAt).format('YYYY-MM-DD HH:mm:ss');
},
})
......@@ -92,6 +122,10 @@ export class PageInfo extends Model<PageInfo> {
field: 'tenant_id',
})
tenantId: number;
@HasMany(() => PageHistory)
pageHistory: PageHistory[];
}
export default () => PageInfo;
\ No newline at end of file
......@@ -13,8 +13,12 @@ export default (application: Application) => {
router.post('/editor/update', controller.editor.update);
router.post('/editor/clearcache', controller.editor.clearCache);
router.get('/editor/get/list', controller.editor.getList);
router.post('/editor/getList/activityCalendar', controller.editor.getListForActivityCalendar);
router.get('/editor/get/template', controller.editor.getTemplateList);
router.get('/editor/getServerTime', controller.editor.getServerTime);
router.get('/editor/get/:uuid', controller.editor.get);
router.get('/editor/getHistoryPage/:id', controller.editor.getHistoryPreviewData);
router.post('/editor/recoverPageData', controller.editor.recoverPageData);
router.delete('/editor/:pageId', controller.editor.delete);
router.get('/editor/login', controller.editor.login);
router.get('/editor', controller.editor.home);
......
import { Subscription } from 'egg';
class clearInvalidPageHistoryRecords extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
cronOptions: {
tz: 'Asia/Shanghai'
},
immediate: false,
cron: '0 0 2 L * *', // 每月的最后一天凌晨2点整执行
type: 'all', // 指定所有的 worker 都需要执行
};
}
async subscribe() {
this.ctx.service.records.clearInvalidPageHistoryRecords();
}
}
module.exports = clearInvalidPageHistoryRecords;
\ No newline at end of file
......@@ -46,7 +46,7 @@ function deleteUrlParams(url, ref) {
}
}
export default class ArticeService extends Service {
export default class NavService extends Service {
private context: Context;
constructor(ctx: Context) {
super(ctx);
......
import { Context, Service } from 'egg';
export default class RecordsService extends Service {
private context: Context;
constructor(ctx: Context) {
super(ctx);
this.context = ctx;
}
async clearInvalidPageHistoryRecords() {
// 操作记录表直接删除一年以上的操作记录
const limitDate = new Date(new Date().getTime() - 365 * 24 * 3600 * 1000).toLocaleDateString().replace(/\//g, '-')
try {
this.context.logger.info(`开始删除页面历史记录,删除范围为日期在${limitDate}之前的所有记录`);
await this.context.model.PageHistory.destroy({
where: {
updated_at: {
$lte: new Date(limitDate)
}
}
});
this.context.logger.info(`页面历史记录删除成功,删除范围为日期在${limitDate}之前的所有记录`);
} catch (err) {
this.context.logger.info(`页面历史记录删除失败,失败原因: ${JSON.stringify(err)}`);
}
return;
}
}
......@@ -24,6 +24,9 @@ export default {
getTemplateList() {
return http.get('editor/get/template');
},
recoverPageData(params) {
return http.post(`editor/recoverPageData`, params);
},
getUpToken() {
return axios.get(`${config.opapiHost}/upload/getToken`);
},
......
......@@ -105,4 +105,9 @@ export default {
accessToken: true
});
},
getCouponModalList(params) {
return http.post(`${config.opapiHost}/kdspOp/api/kdsp/app/app-config-push/listAll`, params, {
accessToken: true
});
}
};
\ No newline at end of file
......@@ -134,6 +134,7 @@ export default {
border-radius: 0;
background: #f5f7f9;
width: 100%;
z-index: 1;
.layout-container {
height: 100%;
flex-flow: row;
......
......@@ -14,7 +14,7 @@ export default class DynamicForm extends Vue {
@Getter('pageData') pageData;
@State(state => state.tenant.cartAndShareBtn) cartAndShareBtn;
@Prop(Boolean) value;
validTime: array = [];
showPopup: boolean = false;
loadingSave: boolean = false;
loadingPreview: boolean = false;
......@@ -34,9 +34,11 @@ export default class DynamicForm extends Vue {
@Watch('pageData', { immediate: true })
onPageDataChange(newVal) {
const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate, page } = this.pageInfo;
const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate, page, validStartTime, validEndTime, redirectUrl } = this.pageInfo;
const { shareOpenMethod, shareCoverImage, diversion } = page.props;
this.formCustom = { pageName, pageDescribe, pageKeywords, coverImage, isPublish: !!isPublish, isTemplate: !!isTemplate, shareCoverImage, shareOpenMethod, diversion };
this.formCustom = { pageName, pageDescribe, pageKeywords, coverImage, isPublish: !!isPublish, isTemplate: !!isTemplate, shareCoverImage, shareOpenMethod, diversion, validStartTime, validEndTime, redirectUrl };
this.validTime = validStartTime && validEndTime ? [validStartTime, validEndTime] : [];
// `${validStartTime} - ${validEndTime}` : '';
}
@Watch('value')
......@@ -65,6 +67,17 @@ export default class DynamicForm extends Vue {
this.$emit('input', val);
}
validTimeChange(val) {
if (val.length && val[0] && val[1]) {
if (val[1].slice(-8) === '00:00:00') {
val[1] = val[1].replace('00:00:00', '23:59:59');
}
}
this.validTime = val;
this.formCustom.validStartTime = val[0] || '';
this.formCustom.validEndTime = val[1] || '';
}
preview() {
this.formCustom.pageName = this.formCustom.pageName || '未命名';
this.handleSubmit('preview');
......
<template>
<Modal v-model="showPopup" width="380" @on-visible-change="change" class-name='basic-form'>
<!-- {{formCustom}} -->
<Form @submit.native.prevent ref="formCustom" :model="formCustom" :rules="ruleCustom" :label-width="80" label-position="left">
<FormItem label="页面名称" prop="pageName">
<Input v-model="formCustom.pageName" placeholder="请输入页面名称"></Input>
......@@ -24,8 +25,14 @@
<FormItem label="是否发布" prop="isPublish">
<i-switch v-model="formCustom.isPublish"></i-switch>
</FormItem>
<FormItem label="活动有效期" prop="redirectUrl">
<DatePicker v-model="validTime" @on-change="validTimeChange" format="yyyy/MM/dd HH:mm:ss" type="datetimerange" placeholder="选填,留空默认长期有效" style="width: 100%"></DatePicker>
</FormItem>
<FormItem label="活动结束url" prop="redirectUrl">
<Input v-model="formCustom.redirectUrl" placeholder="选填,留空默认首页" :rows="3"></Input>
</FormItem>
<FormItem label="导流url" prop="diversion">
<Input v-model="formCustom.diversion" placeholder="可选,具体url请联系对应导流项目的产品或开发" :rows="3"></Input>
<Input v-model="formCustom.diversion" placeholder="选填,具体url请联系对应导流项目的产品或开发" :rows="3"></Input>
</FormItem>
<FormItem label="设为模板" prop="isTemplate" v-if="formCustom.isPublish">
<i-switch v-model="formCustom.isTemplate"></i-switch>
......
......@@ -9,6 +9,8 @@
<p slot="title">{{ config.title }}</p>
<div class="dynamic-row-card">
<img
data-html2canvas-ignore="true"
class="ignore-html2canvas-loop"
:src="config.coverImage || 'http://activitystatic.q-gp.com/low_code.jpg'"
/>
</div>
......@@ -26,6 +28,8 @@
<p slot="title">{{ config.title }}</p>
<div class="dynamic-row-card">
<img
data-html2canvas-ignore="true"
class="ignore-html2canvas-loop"
:src="config.coverImage || 'http://activitystatic.q-gp.com/low_code.jpg'"
/>
</div>
......
<template>
<div class="couponModalSeletor">
<Select v-model="modalId" filterable clearable multiple>
<Option v-for="item in list" :value="item.appConfigPushNo" :key="item.appConfigPushNo">{{ `id ${item.id} - ${item.name}` }}</Option>
</Select>
<!-- <Button type="primary" @click="save">保存</Button> -->
</div>
</template>
<script>
import operationApi from '@api/operation.api';
export default {
props: {
value: {
type: Array,
default: []
}
},
watch: {
value(val) {
this.modalId = val;
},
modalId(val) {
this.$emit('input', val);
}
},
components: {
},
data() {
return {
list: [],
modalId: []
}
},
created() {
this.modalId = this.value;
this.getCouponModalList();
},
methods: {
async getCouponModalList() {
const res = await operationApi.getCouponModalList({
elementType: 4, // 4为优惠券弹窗类型
enable: 'VALID'
});
const list = res.list.map(item => {
let obj = {};
obj.id = item.id.toString();
obj.appConfigPushNo = item.appConfigPushNo.toString();
obj.name = item.componentName;
return obj;
})
// console.log(list)
this.list = list;
}
}
}
</script>
<style scoped lang="less">
</style>
<template>
<div class="fontWeightSelectorContainer">
<span class="option" @click="select(item.id)" v-for="(item, index) in options" :key="index" :class="{selected: item.id == selected}">
{{item.label}}
</span>
<!-- <Button type="primary" @click="save">保存</Button> -->
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: "normal"
}
},
watch: {
value(val) {
this.selected = val;
},
selected(val) {
this.$emit('input', val);
}
},
methods: {
select(id) {
this.selected = id;
}
},
data() {
return {
options: [
{id: 'normal', label: '标准'},
{id: 'medium', label: '中粗'},
{id: 'bold', label: '加粗'}
],
selected: ''
}
},
created() {
this.selected = this.value;
}
}
</script>
<style scoped lang="less">
.option{
cursor: pointer;
border-radius: 4px;
line-height:normal;
display: inline-block;
padding: 3px 5px;
border: 1px solid #dddee1;
margin: 0 2px;
user-select: none;
&.selected, &:hover{
border-color: #57a3f3;
box-shadow: 0px 0px 0px 2px rgb(45 140 240 / 20%)
}
}
</style>
<template>
<div class="VerticalAdGoodsSelectorContainer">
<Input
v-model="data.ids"
type="textarea"
:autosize="{minRows: 3,maxRows: 10}"
placeholder="请输入skuNo,每个skuNo使用英文逗号隔开"
@on-blur="changeIds"
/>
<div class="goodsItem" v-for="(item, index) in data.data" :key="index">
<div class="title">
商品{{index + 1}}
</div>
<Form @submit.native.prevent ref="formCustom" :model="item" :label-width="90" label-position="left">
<FormItem label="sku_no" prop="skuNo">
<Input v-model="item.skuNo" disabled />
</FormItem>
<FormItem label="商品名称" prop="skuName">
<Input v-model="item.skuName" />
</FormItem>
<FormItem label="商品图片" prop="skuUrl">
<Upload v-model="item.skuUrl"></Upload>
</FormItem>
<FormItem label="商品横幅文案" prop="subTitle">
<Input v-model="item.subTitle" />
</FormItem>
</Form>
<Button type="error" long @click="del(index, item.skuNo)" class="deleteGood">删除</Button>
</div>
</div>
</template>
<script>
import operationApi from '@api/operation.api';
import Upload from '@editor/component/DynamicForm/component/Upload/index.vue';
export default {
props: {
value: {
type: Object,
default: () => {}
}
},
watch: {
value: {
handler(val) {
this.data = val;
}
},
data: {
deep: true,
handler(val) {
this.$emit('input', val);
}
}
},
components: {
Upload
},
data() {
return {
data: {}
}
},
created() {
this.data = JSON.parse(JSON.stringify(this.value));
},
methods: {
init() {
const data = this.value;
if (!data.ids) data.ids = '';
if (!data.data) data.data = [];
this.data = data;
},
async getGoods(params) {
let prevDataArr = this.data && this.data.data ? JSON.parse(JSON.stringify(this.data.data)) : [];
let skuToInfoMaps = {};
for (let i = 0; i < prevDataArr.length; i++) {
skuToInfoMaps[prevDataArr[i].skuNo] = prevDataArr[i];
}
const res = await operationApi.getGoods(params);
try {
let goodsList = res.skus || [];
let arr = [];
if (goodsList.length) {
for (let i = 0; i < goodsList.length; i++) {
let item = {};
item.skuName = goodsList[i].skuName;
item.salePrice = goodsList[i].salePrice;
item.skuNo = goodsList[i].skuNo;
item.skuUrl = goodsList[i].skuUrl;
item.status = goodsList[i].status;
if (skuToInfoMaps[goodsList[i].skuNo]) {
item = Object.assign({}, skuToInfoMaps[goodsList[i].skuNo])
};
arr.push(item);
}
this.$set(this.data, 'data', arr);
} else {
this.$set(this.data, 'data', []);
}
} catch (err) {
console.log(err);
this.$set(this.data, 'data', []);
}
// setTimeout(() => {
// this.$emit('adjust')
// }, 1000)
},
changeIds() {
const ids = this.data && this.data.ids ? this.data.ids.trim() : '';
this.data.ids = ids;
const skus = ids ? ids.split(',') : [];
if (skus.length) {
if (skus.length > 30) {
this.$toast('最多填入30个商品');
return;
}
this.getGoods({
skus: Array.from(new Set(skus))
});
} else {
this.$set(this.data, 'data', []);
}
},
del(index, skuNo) {
this.data.data.splice(index, 1)
const ids = this.data.ids ? this.data.ids.split(',') : [];
const idx = ids.indexOf(skuNo);
if (idx > -1 && ids.length) ids.splice(idx, 1);
this.data.ids = ids.join();
if (this.data.data.length == 0) this.data.ids = '';
}
}
}
</script>
<style scoped lang="less">
.goodsItem{
border: 1px solid #dddee1;
border-radius: 4px;
margin-top: 10px;
padding: 5px;
.title{
width: 100%;
height: 30px;
font-weight: bold;
}
}
</style>
<template>
<div class="goodsResourceSelectorContainer">
<span class="option" @click="select(item.id)" v-for="(item, index) in options" :key="index" :class="{selected: item.id == dataValue.type}">
{{item.label}}
</span>
<div v-if="dataValue.type == 'category'">
<Button type="primary" @click="add">选择品类</Button>
</div>
<div v-if="dataValue.type == 'brand'">
<Button type="primary" @click="add">选择品牌</Button>
</div>
<Modal v-model="showModal" :mask-closable="false" :title="modalTitle" width="600">
<div v-if="dataValue.type == 'category'">
<div class="search">
<Form class="searchFormStyle" inline>
<FormItem label="类目名称" prop="name">
<Input v-model="searchParams.name" placeholder="" />
</FormItem>
<FormItem label="类目编码" prop="name">
<Input v-model="searchParams.sceneId" placeholder="" />
</FormItem>
<Button type="primary" @click="searchList">查询</Button>
</Form>
</div>
<Table
@on-select="selectItem"
@on-select-cancel="cancelSelectItem"
:columns="tableColumn"
:data="tableData"
class="tableStyle"
height="600"
:loading="loading"
/>
<Page class="page" :total="total" @on-change="changePageNo" show-total></Page>
</div>
<div v-if="dataValue.type == 'brand'">
<div class="search">
<Form class="searchFormStyle" inline>
<FormItem label="品牌名称" prop="name">
<Input v-model="searchParams.name" placeholder="" />
</FormItem>
<FormItem label="品牌编码" prop="name">
<Input v-model="searchParams.sceneId" placeholder="" />
</FormItem>
<Button type="primary" @click="searchList">查询</Button>
</Form>
</div>
<Table
@on-select="selectItem"
@on-select-cancel="cancelSelectItem"
:columns="tableColumn"
:data="tableData"
class="tableStyle"
height="600"
:loading="loading"
/>
<Page class="page" :total="total" @on-change="changePageNo" show-total></Page>
</div>
<div slot="footer">
<Button @click="showModal = false">取消</Button>
<Button type="primary" @click="submitSelect">确认</Button>
</div>
</Modal>
</div>
</template>
<script>
import operationApi from '@api/operation.api';
const categoryCol = function() {
return [
{
type: 'selection',
width: 60,
align: 'center'
},
{
align: 'center',
title: '类目名称',
key: 'categoryName',
},
{
align: 'center',
title: '类目编码',
key: 'sceneId',
width: 180
},
]
}
const brandCol = function() {
return [
{
type: 'selection',
width: 60,
align: 'center'
},
{
align: 'center',
title: '品牌名称',
key: 'brandName',
},
{
align: 'center',
title: '品牌编码',
key: 'sceneId',
width: 120
},
{
align: 'center',
title: 'logo',
render: (h, params) => {
const row = params.row;
return h('div', {
style: {
padding: '5px 0'
}
}, [
h('img', {
attrs: {
src: row.imgUrl,
width: 80,
height: 80
},
})
]
)
},
width: 200
}
]
}
export default {
props: {
value: {
type: Object,
default() {
return {}
}
}
},
computed: {
modalTitle() {
return this.dataValue.type === 'category' ? '选择品类' : '选择品牌'
}
},
watch: {
value(val) {
console.log(val, '122')
this.dataValue = val;
},
dataValue: {
deep: true,
handler(val) {
this.$emit('input', val);
}
}
},
methods: {
submitSelect() {
// 提交选择
const targetKey = this.dataValue.type === 'category' ? 'categoryId' : 'brandId';
this.dataValue[targetKey] = this.dataValue.type === 'category' ? this.selectCategoryCollect.join() : this.selectBrandCollect.join();
this.dataValue.type === 'category' ? this.dataValue.brandId = '' : this.dataValue.categoryId = ''; // 清空非当前选项的id数据
this.showModal = false;
},
selectItem(arr, item) {
// 选择某一项
if (this.dataValue.type == 'category') {
this.selectCategoryCollect.push(String(item.sceneId));
console.log(this.selectCategoryCollect);
}
if (this.dataValue.type == 'brand') {
this.selectBrandCollect.push(String(item.sceneId));
console.log(this.selectBrandCollect);
}
},
cancelSelectItem(arr, item) {
// 取消选择某一项
if (this.dataValue.type == 'category') {
this.selectCategoryCollect.splice(this.selectCategoryCollect.indexOf(item.sceneId), 1)
console.log(this.selectCategoryCollect);
}
if (this.dataValue.type == 'brand') {
this.selectBrandCollect.splice(this.selectBrandCollect.indexOf(item.sceneId), 1)
console.log(this.selectBrandCollect);
}
},
add() {
this.showModal = true;
this.getData();
},
select(type) {
if (type == this.dataValue.type) return;
this.dataValue = {
type,
brandId: '',
categoryId: '',
}
this.selectCategoryCollect = [];
this.selectBrandCollect = [];
},
changePageNo(val) {
this.searchParams.pageNo = val;
this.getData();
},
searchList() {
this.searchParams.pageNo = 1;
this.searchParams.pageSize = 10;
this.getData();
},
async getData() {
let res;
this.loading = true;
this.tableData = [];
this.tableColumn = this.dataValue.type == 'category' ? categoryCol.bind(this)() : brandCol.bind(this)();
try {
if (this.dataValue.type == 'category') {
// 品类
res = await operationApi.getCategoryAndPriority({...this.searchParams, categoryLevel: 1});
} else {
// 品牌
res = await operationApi.getBrandAndPriority(this.searchParams);
}
this.$nextTick(() => {
this.loading = false
this.total = res.total || 0;
const list = res.records || [];
list.map(item => {
if (this.dataValue.type == 'category') {
if (this.selectCategoryCollect.indexOf(String(item.sceneId)) > -1) {
item._checked = true;
}
}
if (this.dataValue.type == 'brand') {
if (this.selectBrandCollect.indexOf(String(item.sceneId)) > -1) {
item._checked = true;
}
}
return item;
})
this.tableData = list;
})
} catch (e) {
this.loading = false;
}
},
},
data() {
return {
options: [
{id: 'all', label: '全部商品'},
{id: 'category', label: '按照品类'},
{id: 'brand', label: '按照品牌'}
],
dataValue: {},
showModal: false,
categoryData: [],
tableData: [],
tableColumn: [],
total: 0,
searchParams: {
pageSize: 10,
pageNo: 1,
name: '',
sceneId: ''
},
loading: false,
selectCategoryCollect: [], // 选择品类集合
selectBrandCollect: [] // 选择品牌集合
}
},
created() {
if (!Object.keys(this.value).length) {
// 默认值
this.dataValue = {
type: 'all',
brandId: '',
categoryId: '',
}
} else {
this.dataValue = this.value;
if (this.dataValue.type == 'category') {
this.selectCategoryCollect = this.dataValue.categoryId ? this.dataValue.categoryId.split(',') : [];
}
if (this.dataValue.type == 'brand') {
this.selectBrandCollect = this.dataValue.brandId ? this.dataValue.brandId.split(',') : [];
}
console.log(this.selectCategoryCollect, this.selectBrandCollect)
}
}
}
</script>
<style lang="less" scoped>
.option{
cursor: pointer;
border-radius: 4px;
line-height:normal;
display: inline-block;
padding: 3px 5px;
border: 1px solid #dddee1;
margin: 0 2px;
user-select: none;
&.selected, &:hover{
border-color: #57a3f3;
box-shadow: 0px 0px 0px 2px rgb(45 140 240 / 20%)
}
}
.searchFormStyle {
text-align: left;
clear: both;
background-color: #fff;
padding: 4px;
min-height: 70px;
font-size: 0;
display: flex;
align-items: center;
.btnGroupStyle{
button{
margin-right: 6px;
}
}
/deep/ .ivu-form-item{
margin-bottom: 0;
}
/deep/ .ivu-form-item-label {
font-weight: bold;
display: inline-block;
}
/deep/.ivu-form-item-content {
display: inline-block;
}
}
.page{
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
/deep/ th.ivu-table-column-center{
.ivu-checkbox-wrapper{
display: none;
}
}
</style>
\ No newline at end of file
<template>
<div class="seckillActivityContainer">
<Select v-model="activityInfoId" filterable>
<Option v-for="item in list" :value="item.id" :key="item.id">{{ `id ${item.id} - ${item.title}` }}</Option>
</Select>
<!-- <Button type="primary" @click="save">保存</Button> -->
</div>
</template>
<script>
import operationApi from '@api/operation.api';
export default {
props: {
value: {
type: String,
default: ''
}
},
watch: {
activityInfoId(val) {
this.$emit('input', val);
}
},
components: {
},
data() {
return {
list: [],
activityInfoId: ''
}
},
created() {
this.activityInfoId = this.value;
this.getActivityList();
console.log(this.value)
},
methods: {
async getActivityList() {
const res = await operationApi.getActivityList({
templateType: 1,
crowdType: [1],
userType:[]
});
for (let i = 0; i < res.length; i++) {
res[i].id = res[i].id.toString();
}
this.list = res;
}
}
}
</script>
<style scoped lang="less">
</style>
<template>
<div class="snapUpActivityContainer">
<div class="swiperStyleSeletorContainer">
<span class="option" @click="select(item.id)" v-for="(item, index) in options" :key="index" :class="{selected: item.id == selected}">
{{item.label}}
</span>
......
......@@ -7,6 +7,7 @@ import Upload from './component/Upload/index.vue';
import ColorSelector from './component/ColorSelector/index.vue';
import DiscountGoodsSelector from './component/DiscountGoodsSelector/index.vue';
import SnapUpActivitySelector from './component/SnapUpActivitySelector/index.vue';
import SeckillSelector from './component/SeckillSelector/index.vue';
import VerticalAdStyleSelector from './component/VerticalAdStyleSelector/index.vue';
import CategoryTypeSelector from './component/CategoryTypeSelector/index.vue';
import CategorySelector from './component/CategorySelector/index.vue';
......@@ -17,6 +18,8 @@ import GoodsSwiperSelector from './component/GoodsSwiperSelector/index.vue';
import DiscountGoodsSelectorV2 from './component/DiscountGoodsSelectorV2/index.vue';
import GoodsChannelTypeSelector from './component/GoodsChannelTypeSelector/index.vue';
import GoodsPromotionTypeSelector from './component/GoodsPromotionTypeSelector/index.vue';
import GoodsResourceSelector from './component/GoodsResourceSelector/index.vue';
import GoodsCubeSelector from './component/GoodsCubeSelector/index.vue';
import BaseSelect from './component/BaseSelect/index.vue';
import ComponentSelect from './component/ComponentSelect/index.vue';
import FormList from './component/FormList/index.vue';
......@@ -25,6 +28,7 @@ import CouponTableModal from './component/CouponTableModal/index.vue';
import Textarea from './component/Textarea/index.vue';
import Number from './component/Number/index.vue';
import ColumnSelector from './component/ColumnSelector/index.vue';
import FontWeightSelector from './component/FontWeightSelector/index.vue';
import { resizeDiv, getStyle } from '@/service/utils.service';
import { getAllScheme } from '../../../store/modules/editor/scheme';
import EventBus from '@service/eventBus.service';
......@@ -52,7 +56,11 @@ const allComponentsMap = getAllScheme();
SwiperListSelector,
GoodsSwiperSelector,
GoodsChannelTypeSelector,
GoodsPromotionTypeSelector
GoodsPromotionTypeSelector,
FontWeightSelector,
GoodsResourceSelector,
GoodsCubeSelector,
SeckillSelector
}, name: 'DynamicForm' })
export default class DynamicForm extends Mixins(ContextMenuMixin, DynamicFormMixin) {
@State(state => state.editor.curEleIndex) curEleIndex;
......
......@@ -10,7 +10,10 @@
<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" :limit-name="child.type === 'FormList' && curElement.name === 'cs-goods-tabs' ? 'cs-goods' : ''" v-model="form[child.key]" />
<!-- {{curElement.name}}
{{child.key}} -->
<InputNumber v-if="curElement.name == 'cs-guess-you-like' && child.key == 'rowNumber'" :max="10" :min="1" v-model="form[child.key]"></InputNumber>
<component v-else :is="getComponent(child.type)" :options="child.options" :formControl="child.formControl" :limit-name="child.type === 'FormList' && curElement.name === 'cs-goods-tabs' ? 'cs-goods' : ''" v-model="form[child.key]" />
</FormItem>
</div>
<FormItem class="Df-component-formitem" v-else :label="item.name" :title="item.desc || ''">
......
......@@ -4,6 +4,7 @@ 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 CouponModalSelector from '../DynamicForm/component/CouponModalSelector/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';
......@@ -12,7 +13,7 @@ import BackTopPicker from '../DynamicForm/component/BackTopPicker/index.vue';
import SwitchBtn from '../DynamicForm/component/SwitchBtn/index.vue';
import { SHOP_CART_CONFIG, DEFAULT_CONFIG } from '@service/staticData.service';
@Component({ components: { Upload, ColorSelector, BaseSelect, Textarea, Number, FormList, BackTopPicker, SwitchBtn }, name: 'DynamicPageForm' })
@Component({ components: { Upload, ColorSelector, BaseSelect, Textarea, Number, FormList, BackTopPicker, SwitchBtn, CouponModalSelector }, name: 'DynamicPageForm' })
export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
@Getter('pageData') pageData;
@State(state => state.tenant.cartAndShareBtn) cartAndShareBtn;
......@@ -32,6 +33,13 @@ export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
type: 'ColorSelector'
}
];
baseConfigSchema: object[] = [
{
key: 'pageNeedLogin',
name: '是否登录',
type: 'SwitchBtn'
},
]; ,
titleSchema: object[] = [
{
key: 'titleBgColor',
......@@ -158,6 +166,13 @@ export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
formDefault: {}
}
];
floatModal: object[] = [
{
key: 'couponModal',
name: '优惠券弹窗',
type: 'CouponModalSelector'
},
];
// 这块用于让被污染的pageInfo的page.props返回shareOpenMethod
otherProps: object[] = [
{
......@@ -171,7 +186,7 @@ export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
},
];
get propsSchema() {
return [...this.titleSchema, ...this.bottomSchema, ...this.floatSchema, ...this.otherProps];
return [...this.titleSchema, ...this.bottomSchema, ...this.floatSchema, ...this.otherProps, ...this.floatModal, ...this.baseConfigSchema];
}
@Watch('cartAndShareBtn', { immediate: true })
......@@ -189,6 +204,7 @@ export default class DynamicPageForm extends Mixins(ContextMenuMixin) {
this.propsSchema.forEach(schame => {
this.$set(this.propsForm, schame.key, this.pageData?.props?.[schame.key]);
});
// console.log(this.propsForm);
}
@Watch('commonStyleForm', { immediate: true, deep: true })
......
......@@ -3,6 +3,12 @@
<h2>{{title}}</h2>
<Form class="dynamic-form-component" :label-width="80" :model="propsForm" @submit.native.prevent>
<h3>基础属性</h3>
<h4>基础配置</h4>
<template v-for="(item, index) in baseConfigSchema">
<FormItem class="Df-component-formitem" :label="item.name" :key="'baseConfigSchema_' + index">
<component :is="item.type" :options="item.options" v-bind="item.props" v-model="propsForm[item.key]" :formControl="item.formControl" />
</FormItem>
</template>
<h4>标题栏</h4>
<template v-for="(item, index) in titleSchema">
<FormItem class="Df-component-formitem" :label="item.name" :key="'titleSchema_' + index">
......@@ -21,6 +27,12 @@
<component :is="item.type" :options="item.options" v-bind="item.props" v-model="propsForm[item.key]" :formDefault="item.formDefault" :formControl="item.formControl" />
</FormItem>
</template>
<h4>弹窗</h4>
<template v-for="(item, index) in floatModal">
<FormItem class="Df-component-formitem" :label="item.name" :key="'floatModal_' + index">
<component :is="item.type" :options="item.options" v-bind="item.props" v-model="propsForm[item.key]" :formDefault="item.formDefault" :formControl="item.formControl" />
</FormItem>
</template>
</Form>
<Form class="dynamic-form-component" :label-width="80" :model="commonStyleForm" @submit.native.prevent>
<h3>基础样式</h3>
......@@ -57,7 +69,7 @@
}
}
/deep/ .ivu-form-item-label {
font-size: 14px;
font-size: 12px;
}
/deep/ .ivu-input-number {
......
<template>
<div class="freedom" :ref="`freedomContainer${containerIndex}`" @click.stop="handleElementClick(containerIndex)">
<div class="freedom-body" :data-index="containerIndex">
<video width="100%" @loadeddata="videoOnload" ref="videoBackground" height="auto" style="z-index: -10; osition: absolute; top: 0; left: 0" v-if="childItem.props.freedomVideo" :src="childItem.props.freedomVideo" autoplay loop muted />
<div v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle, 'container')" :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" :key="index" @click.stop="handleElementClick(containerIndex, index)" @mousedown.stop.prevent="mousedown(index, $event)" @contextmenu.prevent.stop="show($event, containerIndex, index)">
<component ref="childComponent" :style="transformStyle(item.commonStyle, 'component')" :is="item.name" v-bind="item.props"></component>
<div class="freedom-body-dot"
......
<template>
<Modal width="700" v-model="show" title="历史记录" @on-visible-change="change">
<Table
:columns="columns"
:data="records"
class="tableStyle"
/>
<span slot="footer"></span>
</Modal>
</template>
<script>
import editorApi from '@api/editor.api';
import config from '@/config/index';
export default {
props: {
value: {
type: Boolean,
default: false
},
records: {
type: Array,
default() {
return []
}
}
},
watch: {
value(val) {
this.show = val;
}
},
data() {
return {
show: false,
columns: [
{
title: '版本',
align: 'center',
render: (h, params) => {
console.log(params);
return h('span', {
style: {
color: '#2d8cf0',
textDecoration: 'underline',
cursor: 'pointer'
},
on: {
'click': () => {
window.open(`${config.h5Host}/history/${params.row.id}?isPreview=1`)
},
},
}, `v${this.records.length - params.index}`)
}
},
{
title: '更新时间',
key: 'updatedAt',
align: 'center'
},
{
key: 'author',
title: '操作人',
align: 'center'
},
{
title: '操作',
align: 'center',
render: (h, params) => {
const props = {
type: 'primary',
};
const style = {
display: 'inline-block',
margin: '5px',
};
return h(
'Poptip',
{
props: {
confirm: true,
transfer: true,
title: '确认恢复到此版本?',
},
style: {
...style,
},
on: {
'on-ok': async () => {
await editorApi.recoverPageData({
id: params.row.id,
author: JSON.parse(localStorage.getItem('user')).account || ''
});
window.location.reload();
},
},
},
[
h(
'Button',
{
...props
},
'恢复'
),
]
)
}
}
]
}
},
created() {
this.show = this.value;
console.log(this.records, 123)
},
methods: {
change(val) {
if (!val) {
this.close();
}
},
close() {
this.$emit('close');
}
}
}
</script>
\ No newline at end of file
......@@ -82,15 +82,16 @@
/>
</template>
</div>
<recordsModal v-model="showRecordsModal" :records="records" @close="showRecordsModal = false" />
</div>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect';
import '@riophae/vue-treeselect/dist/vue-treeselect.css';
import { cloneDeep } from 'lodash';
import recordsModal from './components/records.vue'
export default {
components: { Treeselect },
components: { Treeselect, recordsModal },
props: {
columns: {
type: Array,
......@@ -114,7 +115,9 @@ export default {
},
showSelected: false, // todo
searchCondition: [],
selectedTab: '1'
selectedTab: '1',
records: [],
showRecordsModal: false
};
},
watch: {
......@@ -133,6 +136,11 @@ export default {
}
},
methods: {
showRecords(data) {
// 展示历史记录
this.records = data || [];
this.showRecordsModal = true;
},
changePageNo(page) {
this.query(page);
this.$emit('on-page-change', page);
......
......@@ -16,6 +16,9 @@ export default class GoodsTabsMixin extends Vue {
get hasSnapUpCom() {
return this.pageData.elements.some(item => item.name === 'cs-snap-up');
}
get hasSeckillCom() {
return this.pageData.elements.some(item => item.name === 'cs-seckill');
}
get hasGoodsNavCom() {
return this.pageData.elements.some(item => item.name === 'cs-goods-tabs');
}
......
......@@ -89,7 +89,7 @@ export default class DashBoard extends Mixins(ContextMenuMixin, GoodsTabsMixin,
pageData = this.handleComAchorScrollEnable();
pageData = this.handleGoodsTabs();
pageData = this.handleCouponIds(pageData);
const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate, shareCoverImage, shareOpenMethod, diversion } = pageConfig;
const { pageName, pageDescribe, pageKeywords, coverImage, isPublish, isTemplate, shareCoverImage, shareOpenMethod, diversion, validStartTime, validEndTime, redirectUrl } = pageConfig;
// !diversion shareCoverImage shareOpenMethod没有作为单独的sql字段存储下来,只是单纯的存储到的redis中
// 目前对此打了补丁,存放到page的props中
pageData.props.diversion = diversion;
......@@ -99,15 +99,30 @@ export default class DashBoard extends Mixins(ContextMenuMixin, GoodsTabsMixin,
// 如果存在sheme, 移除页面中的sacheme
delete pageData.scheme;
separateAllScheme(pageData.elements);
const pageInfo = { diversion, page: JSON.stringify(pageData), author: user?.account, isPublish, pageName, pageDescribe, pageKeywords, coverImage, isTemplate, shareCoverImage, shareOpenMethod } as pageInfo;
const pageInfo = { diversion, page: JSON.stringify(pageData), author: user?.account, isPublish, pageName, pageDescribe, pageKeywords, coverImage, isTemplate, shareCoverImage, shareOpenMethod, validStartTime, validEndTime, redirectUrl } as pageInfo;
if (this.uuid) { pageInfo.uuid = this.uuid; }
// 兼容老数据保存时redirectUrl为null的情况
pageInfo.redirectUrl = pageInfo.redirectUrl && typeof pageInfo.redirectUrl == 'string' ? pageInfo.redirectUrl.trim() : '';
if (pageInfo.redirectUrl.length) {
if (pageInfo.redirectUrl.indexOf('https://') != 0 && pageInfo.redirectUrl.indexOf('xyqb://') != 0) {
// 校验活动结束地址链接配置是否满足https和xyqb协议
this.$Notice.error({
title: '链接地址错误或域名不支持,请重新输入',
desc: '',
});
this.showSubmitPopup = false;
return;
}
}
// console.log({ pageInfo, pageData: this.pageData }, 0);
// return;
await this.savePageData({ pageInfo, pageData: this.pageData });
if (this.uuid) { await this.getPageDate({ pageId: this.uuid }); }
this.showSubmitPopup = false;
// 清除新增数据时的缓存
if (!isCreate) { this.removeDefaultCache(); }
if (type === 'preview') {
window.open(`${config.h5Host}/activity/${this.uuid}?tenantId=${this.pageInfo.tenantId}`);
window.open(`${config.h5Host}/activity/${this.uuid}?tenantId=${this.pageInfo.tenantId}&isPreview=1`);
} else {
this.$Notice.success({ title: '保存成功!' });
}
......@@ -258,6 +273,14 @@ export default class DashBoard extends Mixins(ContextMenuMixin, GoodsTabsMixin,
return;
}
}
if (el.data.name === 'cs-seckill') {
if (this.hasSeckillCom) {
this.$Notice.warning({
title: '秒杀组件目前只支持添加一个'
});
return;
}
}
// 添加多个goods-tabs会取消锚点滚动
if (el.data.name === 'cs-goods-tabs') {
if (this.hasGoodsNavCom) {
......
......@@ -35,6 +35,12 @@ export default {
title: '描述',
formType: 'input',
},
{
key: 'uuid',
title: 'uuid',
formType: 'input',
hideTable: true,
},
{
key: 'author',
title: '作者',
......@@ -76,7 +82,7 @@ export default {
title: '链接',
hideSearch: true,
render: (h, params) => {
return h('span', `${config.h5Host}/activity/${params.row.uuid}?tenantId=${params.row.tenantId}&vccToken={token}`)
return h('span', `${config.h5Host}/activity/${params.row.uuid}?tenantId=${params.row.tenantId}&vccToken={token}&appChannel={appChannel}`)
}
},
{
......@@ -117,7 +123,7 @@ export default {
},
on: {
click: () => {
window.open(`${config.h5Host}/activity/${params.row.uuid}?tenantId=${params.row.tenantId}`);
window.open(`${config.h5Host}/activity/${params.row.uuid}?tenantId=${params.row.tenantId}&isPreview=1`);
},
},
},
......@@ -139,6 +145,22 @@ export default {
},
'修改'
),
h(
'Button',
{
props,
style: {
...style,
display: params.row.pageHistory && params.row.pageHistory.length ? 'inline-block' : 'none'
},
on: {
click: () => {
this.$refs.qgTable.showRecords(params.row.pageHistory);
},
},
},
'历史记录'
),
h(
'Poptip',
{
......
......@@ -59,6 +59,30 @@ export interface PageInfo {
uuid?: string;
tenantId?: number;
diversion?: string;
redirectUrl?: string;
validStartTime?: string;
validEndTime?: string;
}
export interface PageHistory {
id?: number;
page?: Page | string;
enable?: number;
author?: string;
coverImage?: string;
shareCoverImage?: string; // 分享缩略图
shareOpenMethod?: string; // 分享后的打开方式
isTemplate?: number | boolean;
isPublish?: number | boolean;
pageName?: string;
pageDescribe?: string;
pageKeywords?: string;
uuid?: string;
tenantId?: number;
diversion?: string;
redirectUrl?: string;
validStartTime?: string;
validEndTime?: string;
}
export default interface EditorState {
......@@ -83,17 +107,22 @@ export const defaultState = {
isPublish: false,
isTemplate: false,
uuid: '',
validEndTime: '',
validStartTime: '',
redirectUrl: '',
page: {
commonStyle: {
backgroundColor: '#f7f8fa',
backgroundImage: '',
},
props: {
pageNeedLogin: false,
titleBgColor: '#fff',
titleUseUrl: false,
showPageBottomTip: true,
pageBottomTxt: '没有更多啦~',
pageBottomColor: '#333',
couponModal: [],
showBackTop: true,
btAttachVal: [
{
......
......@@ -2176,9 +2176,9 @@
}
},
"@qg/citrus-ui": {
"version": "0.3.54",
"resolved": "http://npmprivate.quantgroups.com/@qg%2fcitrus-ui/-/citrus-ui-0.3.54.tgz",
"integrity": "sha512-N4cFIBYIOVL3OCC67i/wZhlyFm2ZcJvSSLRdFT0KmiLSmd5rlJzDQxg26Sohc5v5yUtXH7g/qLHmYI1m1Uf2vw==",
"version": "0.3.64",
"resolved": "http://npmprivate.quantgroups.com/@qg%2fcitrus-ui/-/citrus-ui-0.3.64.tgz",
"integrity": "sha512-+jwT/+7St+ehknceeqE+gq7zybPRTfsFc3ozyHt3t1XnQR+U3SscgcGy3Z78DSvEgmZJ96mD9G/DJTIM6NGfqA==",
"requires": {
"@better-scroll/core": "^2.1.1",
"@qg/cherry-ui": "^2.23.9",
......@@ -2188,15 +2188,16 @@
"js-cookie": "^2.2.1",
"nanoid": "^3.3.4",
"sa-sdk-javascript": "^1.16.1",
"smoothscroll-polyfill": "^0.4.4",
"swiper": "^4.5.1",
"vuex": "^3.6.0",
"weixin-js-sdk": "^1.6.0"
},
"dependencies": {
"nanoid": {
"version": "3.3.4",
"resolved": "http://npmprivate.quantgroups.com/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
"version": "3.3.6",
"resolved": "http://npmprivate.quantgroups.com/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
}
}
},
......@@ -4836,9 +4837,9 @@
}
},
"base64-arraybuffer": {
"version": "0.2.0",
"resolved": "http://npmprivate.quantgroups.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
"version": "1.0.2",
"resolved": "http://npmprivate.quantgroups.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="
},
"base64-js": {
"version": "1.5.1",
......@@ -7147,42 +7148,6 @@
"resolved": "http://npmprivate.quantgroups.com/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "5.2.1",
"resolved": "http://npmprivate.quantgroups.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
"integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
"requires": {
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
"js-yaml": "^3.13.1",
"parse-json": "^4.0.0"
},
"dependencies": {
"import-fresh": {
"version": "2.0.0",
"resolved": "http://npmprivate.quantgroups.com/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "http://npmprivate.quantgroups.com/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"resolve-from": {
"version": "3.0.0",
"resolved": "http://npmprivate.quantgroups.com/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
}
}
},
"crc": {
"version": "3.8.0",
"resolved": "http://npmprivate.quantgroups.com/crc/-/crc-3.8.0.tgz",
......@@ -7403,11 +7368,11 @@
}
},
"css-line-break": {
"version": "1.1.1",
"resolved": "http://npmprivate.quantgroups.com/css-line-break/-/css-line-break-1.1.1.tgz",
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
"version": "2.1.0",
"resolved": "http://npmprivate.quantgroups.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"requires": {
"base64-arraybuffer": "^0.2.0"
"utrie": "^1.0.2"
}
},
"css-loader": {
......@@ -7506,6 +7471,42 @@
"cssnano-preset-default": "^4.0.8",
"is-resolvable": "^1.0.0",
"postcss": "^7.0.0"
},
"dependencies": {
"cosmiconfig": {
"version": "5.2.1",
"resolved": "http://npmprivate.quantgroups.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
"integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
"requires": {
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
"js-yaml": "^3.13.1",
"parse-json": "^4.0.0"
}
},
"import-fresh": {
"version": "2.0.0",
"resolved": "http://npmprivate.quantgroups.com/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "http://npmprivate.quantgroups.com/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"resolve-from": {
"version": "3.0.0",
"resolved": "http://npmprivate.quantgroups.com/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
}
}
},
"cssnano-preset-default": {
......@@ -12534,11 +12535,12 @@
}
},
"html2canvas": {
"version": "1.0.0-rc.7",
"resolved": "http://npmprivate.quantgroups.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz",
"integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==",
"version": "1.4.1",
"resolved": "http://npmprivate.quantgroups.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"requires": {
"css-line-break": "1.1.1"
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
}
},
"htmlparser2": {
......@@ -20658,6 +20660,42 @@
"requires": {
"cosmiconfig": "^5.0.0",
"import-cwd": "^2.0.0"
},
"dependencies": {
"cosmiconfig": {
"version": "5.2.1",
"resolved": "http://npmprivate.quantgroups.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
"integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
"requires": {
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
"js-yaml": "^3.13.1",
"parse-json": "^4.0.0"
}
},
"import-fresh": {
"version": "2.0.0",
"resolved": "http://npmprivate.quantgroups.com/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
}
},
"parse-json": {
"version": "4.0.0",
"resolved": "http://npmprivate.quantgroups.com/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1"
}
},
"resolve-from": {
"version": "3.0.0",
"resolved": "http://npmprivate.quantgroups.com/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
}
}
},
"postcss-loader": {
......@@ -22823,6 +22861,11 @@
"resolved": "http://npmprivate.quantgroups.com/smart-buffer/-/smart-buffer-4.1.0.tgz",
"integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw=="
},
"smoothscroll-polyfill": {
"version": "0.4.4",
"resolved": "http://npmprivate.quantgroups.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg=="
},
"snake-case": {
"version": "2.1.0",
"resolved": "http://npmprivate.quantgroups.com/snake-case/-/snake-case-2.1.0.tgz",
......@@ -23882,6 +23925,14 @@
"integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==",
"dev": true
},
"text-segmentation": {
"version": "1.0.3",
"resolved": "http://npmprivate.quantgroups.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"requires": {
"utrie": "^1.0.2"
}
},
"text-table": {
"version": "0.2.0",
"resolved": "http://npmprivate.quantgroups.com/text-table/-/text-table-0.2.0.tgz",
......@@ -24753,6 +24804,14 @@
"resolved": "http://npmprivate.quantgroups.com/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"utrie": {
"version": "1.0.2",
"resolved": "http://npmprivate.quantgroups.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"requires": {
"base64-arraybuffer": "^1.0.2"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "http://npmprivate.quantgroups.com/uuid/-/uuid-3.4.0.tgz",
......
......@@ -8,7 +8,7 @@
"test": "cross-env NODE_ENV=production EGG_SERVER_ENV=sit egg-scripts start --port 80 --workers 1",
"stop": "egg-scripts stop",
"backend": "nohup egg-scripts start --port 7001 --workers 4",
"dev": "cross-env NODE_ENV=test APOLLO_CLUSTER=k8s NAMESPACE=yxm npm run apollo && egg-bin dev -r egg-ts-helper/register --port 7002",
"dev": "cross-env NODE_ENV=test APOLLO_CLUSTER=k8s NAMESPACE=ds npm run apollo && egg-bin dev -r egg-ts-helper/register --port 7002",
"debug": "egg-bin debug -r egg-ts-helper/register",
"apollo": "node bin/apollo.js",
"build": "npm run tsc && cross-env NODE_ENV=production APOLLO_CLUSTER=3C npm run apollo && cross-env COS_ENV=production easy build --devtool",
......@@ -30,7 +30,7 @@
"@hubcarl/json-typescript-mapper": "^2.0.0",
"@qg/apollo-nodejs": "^2.1.2",
"@qg/cherry-ui": "2.23.10",
"@qg/citrus-ui": "0.3.54",
"@qg/citrus-ui": "0.3.64",
"@riophae/vue-treeselect": "^0.4.0",
"@types/lodash": "^4.14.117",
"@types/node": "^10.12.0",
......@@ -50,7 +50,7 @@
"egg-webpack": "^4.4.7",
"egg-webpack-vue": "^2.0.0",
"extend": "~3.0.0",
"html2canvas": "^1.0.0-rc.7",
"html2canvas": "1.4.1",
"iview": "^2.9.0",
"js-cookie": "^2.2.1",
"less": "^3.12.2",
......
......@@ -18,7 +18,8 @@
"only-arrow-functions": false,
"interface-over-type-literal": false,
"ter-indent": [ true, 2],
"no-var-requires": false
"no-var-requires": false,
"triple-equals": false
},
"rulesDirectory": ["app"]
}
\ No newline at end of file
......@@ -3,12 +3,14 @@
import 'egg';
import ExportNavigatorConfig from '../../../app/model/navigatorConfig';
import ExportPageHistory from '../../../app/model/pageHistory';
import ExportPageInfo from '../../../app/model/pageInfo';
import ExportTenantAuth from '../../../app/model/tenantAuth';
declare module 'egg' {
interface IModel {
NavigatorConfig: ReturnType<typeof ExportNavigatorConfig>;
PageHistory: ReturnType<typeof ExportPageHistory>;
PageInfo: ReturnType<typeof ExportPageInfo>;
TenantAuth: ReturnType<typeof ExportTenantAuth>;
}
......
......@@ -7,11 +7,13 @@ 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 ExportNavigator from '../../../app/service/navigator';
import ExportRecords from '../../../app/service/records';
import ExportRedis from '../../../app/service/redis';
declare module 'egg' {
interface IService {
navigator: AutoInstanceType<typeof ExportNavigator>;
records: AutoInstanceType<typeof ExportRecords>;
redis: AutoInstanceType<typeof ExportRedis>;
}
}
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