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/*
!app/view/layout.html
!app/view/README.md
!app/view/.gitkeep
package-lock.json
yarn.lock
*.log
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.
......
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 { trim, omitBy } from 'lodash';
import { v1 as uuidv1 } from 'uuid';
export default class EditorController extends Controller {
......@@ -13,18 +14,28 @@ export default class EditorController extends Controller {
public async save(ctx: Context) {
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);
}
public async update(ctx: Context) {
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);
}
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);
}
......@@ -37,7 +48,7 @@ export default class EditorController extends Controller {
pageDescribe: pageDescribe && { like: `%${pageDescribe}%`},
isPublish,
enable: 1
}, v => !trim(v));
}, v => !v);
if (type === 'list') {
where = { ...where, isPublish: 1 };
} else if (type === 'my') {
......
......@@ -4,7 +4,7 @@
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull } from 'sequelize-typescript';
@Table({
modelName: 'page_info',
modelName: 'page_config_info',
freezeTableName: true
})
......@@ -35,6 +35,18 @@ export class PageInfo extends Model<PageInfo> {
})
pageDescribe: string;
@Column({
field: 'page_keywords',
type: DataType.STRING(255)
})
pageKeywords: string;
@Column({
field: 'uuid',
type: DataType.UUID
})
uuid: string;
@Column({
type: DataType.INTEGER(1)
})
......
......@@ -13,10 +13,10 @@ export default (application: Application) => {
router.post('/editor/update', controller.editor.update);
router.get('/editor/get/list', controller.editor.getList);
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.get('/editor/login', controller.editor.login);
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 iView from 'iview';
import Raven from 'raven-js';
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';
import { Vue } from 'vue-property-decorator';
import micro from './micro/index.vue';
import single from './single/index.vue';
Vue.use(iView);
Vue.use(VueContextMenu);
Vue.directive('track', {});
// 初始化sentry
if (process.env.SENTRY_ENV !== 'test' && process.env.NODE_ENV === 'production') {
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
export default window.__POWERED_BY_QIANKUN__ ? micro : single;
\ 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 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.use(VueContextMenu);
// 初始化sentry
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({
name: 'Layout'
name: 'Layout',
})
export default class Layout extends Vue {
@Prop({ type: String, default: 'egg' }) title?: string;
@Prop({ type: String, default: 'Vue TypeScript Framework, Server Side Render' }) description?: string;
@Prop({ type: String, default: 'Vue,TypeScript,Isomorphic' }) keywords?: string;
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
});
}
isNode: boolean = EASY_ENV_IS_NODE;
logOut() {
localStorage.clear();
window.location.href = '/editor/login';
}
created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
......
......@@ -27,9 +27,9 @@
<slot v-if="isDashboard"></slot>
</div>
</template>
<style lang="less">
<style lang="less" scoped>
#app {
position: absolute;
position: absolute;
top: 0;
left: 0;
width: 100%;
......@@ -60,7 +60,6 @@
.layout-content {
flex: 1;
margin: 15px;
overflow: auto;
background: #fff;
border-radius: 4px;
......@@ -71,6 +70,7 @@
}
.layout-copy {
height: 45px;
text-align: center;
padding: 10px 0 20px;
color: #9ea7b4;
......@@ -100,6 +100,7 @@
.layout-logo-left {
width: 90%;
height: 30px;
line-height: 30px;
color: #fff;
font-size: 18px;
text-align: center;
......
......@@ -17,9 +17,6 @@
<div v-else-if="!isNode" id="app"><slot></slot></div>
</template>
<style>
html {
font-size: 10vw;
}
#app {
position: absolute;
top: 0;
......
......@@ -2,8 +2,13 @@ const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'http';
export default {
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/`,
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`,
};
// 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';
export default {
apiHost: `https://quantum-blocks.q-gp.com/`,
h5Host: 'https://quantum-h5.q-gp.com/',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`,
......
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
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/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
opapiHost: `${protocol}//opapi.q-gp.com`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
......@@ -62,6 +62,7 @@ export default class App {
server() {
return context => {
// console.log('server', context.state);
const vm = this.create(context.state);
const { store, router } = vm;
router.push(context.state.url);
......
declare var EASY_ENV_IS_NODE: boolean;
declare var EASY_ENV_IS_BROWSER: boolean;
type PlainObject<T = any> = { [key: string]: T };
\ No newline at end of file
declare var __webpack_public_path__: string;
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
This diff is collapsed.
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
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];
});
}
'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
}
]
});
}
This diff is collapsed.
This diff is collapsed.
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
<template>
<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">
<Input v-model="formCustom.pageName" placeholder="请输入页面名称"></Input>
</FormItem>
<FormItem label="页面描述" prop="pageDescribe">
<Input v-model="formCustom.pageDescribe" type="textarea" placeholder="请输入页面描述" :rows="3"></Input>
</FormItem>
<FormItem label="页面关键字" prop="pageKeywords">
<Input v-model="formCustom.pageKeywords" type="textarea" placeholder="请输入页面关键字" :rows="3"></Input>
</FormItem>
<FormItem label="是否发布" prop="isPublish">
<Checkbox v-model="formCustom.isPublish"></Checkbox>
<i-switch v-model="formCustom.isPublish"></i-switch>
</FormItem>
<FormItem label="是否模板" prop="isTemplate" v-if="formCustom.isPublish">
<Checkbox v-model="formCustom.isTemplate"></Checkbox>
<FormItem label="设为模板" prop="isTemplate" v-if="formCustom.isPublish">
<i-switch v-model="formCustom.isTemplate"></i-switch>
</FormItem>
</Form>
<div slot="footer">
<Button type="success" @click="handleSubmit('save')">保存</Button>
<Button type="info" @click="handleSubmit('preview')">保存并预览</Button>
<Button type="success" :loading="loadingSave" @click="handleSubmit('save')">保存</Button>
<Button type="info" :loading="loadingPreview" @click="handleSubmit('preview')">保存并预览</Button>
</div>
</Modal>
</template>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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