Commit 47eca88c authored by 徐光星's avatar 徐光星

Merge branch 'feat/v1.8-batch-1' into 'master'

Feat/v1.8 batch 1

See merge request !90
parents 63c387e4 f67fabcb
......@@ -73,13 +73,14 @@ export default class EditorController extends Controller {
}
public async getList(ctx: Context) {
const { type, pageSize, pageNo, author, pageName, isPublish, pageDescribe } = ctx.query;
const { type, pageSize, pageNo, author, pageName, isPublish, pageDescribe, uuid } = ctx.query;
const { like } = ctx.model.Sequelize.Op;
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);
......@@ -126,4 +127,7 @@ export default class EditorController extends Controller {
const pageInfo = await ctx.model.PageInfo.update({ enable: 0 }, {where: { id: +ctx.params.pageId, tenantId: ctx.headers['qg-tenant-id'] }});
ctx.body = ctx.helper.ok(pageInfo);
}
public async getServerTime(ctx: Context) {
ctx.body = ctx.helper.ok(new Date().getTime());
}
}
\ No newline at end of file
......@@ -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)
})
......
......@@ -14,6 +14,7 @@ export default (application: Application) => {
router.post('/editor/clearcache', controller.editor.clearCache);
router.get('/editor/get/list', controller.editor.getList);
router.get('/editor/get/template', controller.editor.getTemplateList);
router.get('/editor/getServerTime', controller.editor.getServerTime);
router.get('/editor/get/:uuid', controller.editor.get);
router.delete('/editor/:pageId', controller.editor.delete);
router.get('/editor/login', controller.editor.login);
......
......@@ -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>
......
<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="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="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>
......
......@@ -17,6 +17,7 @@ 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 BaseSelect from './component/BaseSelect/index.vue';
import ComponentSelect from './component/ComponentSelect/index.vue';
import FormList from './component/FormList/index.vue';
......@@ -25,6 +26,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 +54,9 @@ const allComponentsMap = getAllScheme();
SwiperListSelector,
GoodsSwiperSelector,
GoodsChannelTypeSelector,
GoodsPromotionTypeSelector
GoodsPromotionTypeSelector,
FontWeightSelector,
GoodsResourceSelector
}, 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 || ''">
......
......@@ -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,8 +99,20 @@ 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; }
pageInfo.redirectUrl = pageInfo.redirectUrl.trim();
if (pageInfo.redirectUrl.trim()) {
if (pageInfo.redirectUrl.indexOf('https://') != 0 && pageInfo.redirectUrl.indexOf('xyqb://') != 0) {
// 校验活动结束地址链接配置是否满足https和xyqb协议
this.$Notice.error({
title: '链接地址错误或域名不支持,请重新输入',
desc: '',
});
this.showSubmitPopup = false;
return;
}
}
await this.savePageData({ pageInfo, pageData: this.pageData });
if (this.uuid) { await this.getPageDate({ pageId: this.uuid }); }
this.showSubmitPopup = false;
......
......@@ -35,6 +35,12 @@ export default {
title: '描述',
formType: 'input',
},
{
key: 'uuid',
title: 'uuid',
formType: 'input',
hideTable: true,
},
{
key: 'author',
title: '作者',
......
......@@ -59,6 +59,9 @@ export interface PageInfo {
uuid?: string;
tenantId?: number;
diversion?: string;
redirectUrl?: string;
validStartTime?: string;
validEndTime?: string;
}
export default interface EditorState {
......@@ -83,6 +86,9 @@ export const defaultState = {
isPublish: false,
isTemplate: false,
uuid: '',
validEndTime: '',
validStartTime: '',
redirectUrl: '',
page: {
commonStyle: {
backgroundColor: '#f7f8fa',
......
......@@ -2176,9 +2176,9 @@
}
},
"@qg/citrus-ui": {
"version": "0.3.55",
"resolved": "http://npmprivate.quantgroups.com/@qg%2fcitrus-ui/-/citrus-ui-0.3.55.tgz",
"integrity": "sha512-c/TnAe/oTURSk7jR+k9Ql/WFwO2/KmZ9pD9KwG2q/m2iWaecAf73u5lyBhtGyyqnJv0V/nyGtfYsC+/HiFcgHw==",
"version": "0.3.56",
"resolved": "http://npmprivate.quantgroups.com/@qg%2fcitrus-ui/-/citrus-ui-0.3.56.tgz",
"integrity": "sha512-BHwB5HNBOsP3z8qqLOGEceW+2ps0cfPYehZ+me1Y1lTcC9ap+8GZDExzO7FsV1MlYU2Y/E1R3+4ORFSs0PnE0g==",
"requires": {
"@better-scroll/core": "^2.1.1",
"@qg/cherry-ui": "^2.23.9",
......
......@@ -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.55",
"@qg/citrus-ui": "0.3.56",
"@riophae/vue-treeselect": "^0.4.0",
"@types/lodash": "^4.14.117",
"@types/node": "^10.12.0",
......
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