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

Initial commit

parents
Pipeline #818 canceled with stages
*.js linguist-language=javascript
*.css linguist-language=javascript
*.html linguist-language=javascript
\ No newline at end of file
.DS_Store
.happypack/
node_modules/
npm-debug.log
.idea/
dist
static
public
private
run
*.iml
artifacts.json
*tmp
_site
logs
app/**/*.js
config/plugin.local.js
config/plugin.js
config/config.*.js
index.js
config/manifest.json
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
{
"version": "1.0.0",
"configurations": [
{
"name": "Launch Egg Vue",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"windows": { "runtimeExecutable": "npm.cmd" },
"runtimeArgs": [ "run", "debug" ],
"console": "integratedTerminal",
"protocol": "auto",
"restart": true,
"port": 9229,
"autoAttachChildProcesses": true
}
]
}
\ No newline at end of file
{
"files.exclude": {
"USE_GITIGNORE": true,
"**/*.js": {
"when": "$(basename).ts"
}
},
"path-intellisense.mappings": {
"lib": "${workspaceRoot}/app/web/lib",
"asset": "${workspaceRoot}/app/web/asset",
"component": "${workspaceRoot}/app/web/component",
"page": "${workspaceRoot}/app/web/page",
"store": "${workspaceRoot}/app/web/store",
},
"typescript.tsdk": "node_modules/typescript/lib"
}
\ No newline at end of file
# egg-vue-typescript-boilerplate
基于 Egg + Vue + Webpack SSR 服务端渲染和 CSR 前端渲染工程骨架项目。
Single Page Application Isomorphic Example for Egg + Vue, Front-End and Node of The Application are Written in TypeScript.
## Document
- https://www.yuque.com/easy-team/egg-vue
- https://www.yuque.com/easy-team/easywebpack
- https://easyjs.cn
## QuickStart
- Development
```bash
$ npm install -g easywebpack-cli
$ easy init
$ npm install
$ npm run dev
$ open http://localhost:7001
```
- Publish
```bash
npm run tsc
npm run build
npm start
```
## Features
- ✔︎ Single Page Application, Support Vue Server Side Render and Client Side Render Modes, Rendering Cache, Automatic Downgrade
- ✔︎ Front-End and Node of The Application are Written in TypeScript, Use `vue-property-decorator` and `vuex-class`
- ✔︎ Build with Webpack + TypeScript, Auto Building, Hot Reload, Code Splitting, High Speed, Performance Optimization
## Rendering
- Front-End TypeScript
![Front-End TypeScript](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-front-end.png?raw=true)
- Node TypeScript
![Node TypeScript](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-node.png?raw=true)
- UI ScreenShot
![UI ScreenShot](https://github.com/easy-team/egg-vue-typescript-boilerplate/blob/master/docs/images/vue-admin-ui.png?raw=true)
## TypeScript
- https://github.com/kaorun343/vue-property-decorator
- https://github.com/ktsn/vuex-class
## License
[MIT](LICENSE)
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
export function ok(data) {
if (typeof data === 'string') {
return {
code: '0001',
businessCode: '0000',
message: data,
};
}
return {
code: '0000',
businessCode: '0000',
data,
};
}
export function fail({ message = '服务异常', code = '0001', businessCode = '0001' }) {
return {
code,
businessCode,
message,
};
}
import * as path from 'path';
import * as util from 'util';
import { Context } from 'egg';
export default () => {
const skipExt = [ '.png', '.jpeg', '.jpg', '.ico', '.gif' ];
return async function access(ctx: Context, next: any) {
const start = new Date().getTime();
await next();
const rs: number = Math.ceil(new Date().getTime() - start);
ctx.set('X-Response-Time', String(rs));
const ext = path.extname(ctx.url).toLocaleLowerCase();
const isSkip = skipExt.indexOf(ext) !== -1 && ctx.status < 400;
if (!isSkip) {
const ip = ctx.get('X-Real-IP') || ctx.ip;
const port = ctx.get('X-Real-Port');
const protocol = ctx.protocol.toUpperCase();
const method = ctx.method;
const url = ctx.url;
const status = ctx.status;
const length = ctx.length || '-';
const referrer = ctx.get('referrer') || '-';
const ua = ctx.get('user-agent') || '-';
const serverTime = ctx.response.get('X-Server-Response-Time') || '-';
const message = util.format('[access] %s:%s - %s %s %s/%s %s %s %s %s %s',
ip, port, method, url, protocol, status, length, referrer, rs, serverTime, ua);
ctx.logger.info(message);
}
};
};
import { Application } from 'egg';
export default (application: Application) => {
const { router, controller } = application;
const healthMessage = { message: 'service is running...' };
router.get('/tech/health/check', ctx => {
ctx.body = healthMessage;
});
router.get('/activity/:id', controller.activity.home);
};
\ No newline at end of file
## egg规范view目录, 保证view文件夹存在, 否则app.config.view.root为空, 编译服务器文件会存放到该目录.
\ No newline at end of file
module.exports = {
// Sentry options are required
organization: 'sentry',
project: 'quantum-blocks',
apiKey: '2e7e0678d81d4542b09cdbc35812f1cbb07a4993cd13435ba49d9511be11ec95',
// Release version name/hash is required
release: 'quantum-blocks@0.0.1',
baseSentryURL: 'http://sentry.quantgroups.com/api/0',
deleteAfterCompile: true,
include: /js\//, // 只上传js和map文件
ignore: ['node_modules'],
suppressErrors: true,
filenameTransform(filename) {
return '~/' + filename;
},
};
import http from '../service/http.service';
import config from '../config';
import axios from 'axios';
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');
},
getUpToken() {
return axios.get(`${config.opapiHost}/upload/getToken`);
},
uploadBase64(base64Str, name, token) {
return axios.post(`${config.qiniuUpHost}/putb64/-1/key/${name}`, base64Str, {
headers: {
'content-type': 'application/octet-stream',
'Authorization': token,
}
});
}
};
\ 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);
},
// 商品组或专题查询
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.post(`${config.kdspHost}/api/kdsp/coupon/list`, params, {
// hideToken: true,
// headers: {
// 'x-auth-token': '97466ed6-ec03-452e-be8f-763a8ffafefe'
// }
// });
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, {
// todo: header里二者参数需特殊处理
headers: {
'x-user-terminal': 'H5',
'vccChannel': '',
}
});
},
};
\ No newline at end of file
import qs from 'qs';
import http from '../service/http.service';
import config from '../config';
const { opapiHost } = config;
export function getCaptcha() {
return `${opapiHost}/user/captcha`;
}
export function login(params) {
return http.post(`${opapiHost}/backStms/prologue/login`, qs.stringify(params), {
accessToken: true
});
}
export function getUserInfo() {
return http.get(`${opapiHost}/backStms/oauth/currentuserinfo`, {
accessToken: true
});
}
\ 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
import { Vue, Component, Prop } from 'vue-property-decorator';
import Raven from 'raven-js';
import RavenVue from 'raven-js/plugins/vue';
import { release } from '@/.sentryclirc';
import '@/service/qg.service';
// 初始化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 {
@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;
isNode: boolean = EASY_ENV_IS_NODE;
created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
}
}
\ No newline at end of file
<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
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-vcc2.liangkebang.net/',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=default`,
opapiHost: `https://opapi-vcc2.liangkebang.net`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
kdspHost: 'https://kdsp-api-vcc2.liangkebang.net',
};
import prod from './prod.config';
import dev from './dev.config';
import test from './test.config';
// export default process.env.NODE_ENV === 'test' ? test : process.env.NODE_ENV === 'production' ? prod : dev;
export default process.env.SENTRY_ENV ? test : process.env.NODE_ENV === 'production' ? prod : dev;
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default {
apiHost: `https://quantum-blocks.q-gp.com/`,
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`,
};
const protocol = EASY_ENV_IS_BROWSER ? window.location.protocol : 'https';
export default {
apiHost: `https://quantum-vcc2.liangkebang.net/`,
opApiHost: 'https://opapi-vcc2.liangkebang.net',
qiniuHost: `https://appsync.lkbang.net/`,
shenceUrl: `${protocol}//bn.xyqb.com/sa?project=production`,
qiniuUpHost: `${protocol}//up-z0.qiniup.com`,
};
import Vue from 'vue';
import { sync } from 'vuex-router-sync';
export default class App {
config: any;
constructor(config) {
this.config = config;
}
bootstrap() {
console.log('EASY_ENV_IS_NODE', EASY_ENV_IS_NODE, EASY_ENV_IS_BROWSER);
if (EASY_ENV_IS_NODE) {
return this.server();
}
return this.client();
}
create(initState) {
const { entry, createStore, createRouter } = this.config;
console.log('initState', initState);
const store = createStore(initState);
const router = createRouter();
sync(store, router);
return {
router,
store,
render: h => { // not use ...entry, why ?
return h(entry);
},
};
}
fetch(vm): Promise<any> {
const { store, router } = vm;
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents) {
return Promise.reject('No Match Component');
}
return Promise.all(
matchedComponents.map((component: any) => {
const options = component.options;
if (options && options.methods && options.methods.fetchApi) {
return options.methods.fetchApi.call(component, { store, router, route: router.currentRoute });
}
return null;
})
);
}
client() {
Vue.prototype.$http = require('axios');
const vm = this.create(window.__INITIAL_STATE__);
vm.router.afterEach(() => {
this.fetch(vm);
});
const app = new Vue(vm);
const root = document.getElementById('app');
const hydrate = root.childNodes.length > 0;
app.$mount('#app', hydrate);
return app;
}
server() {
return context => {
// console.log('server', context.state);
const vm = this.create(context.state);
const { store, router } = vm;
router.push(context.state.url);
return new Promise((resolve, reject) => {
router.onReady(() => {
this.fetch(vm).then(() => {
context.state = store.state;
return resolve(new Vue(vm));
});
});
});
};
}
}
\ No newline at end of file
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
<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="coupon.id" v-for="coupon 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.startDate} - ${coupon.endDate}`}}</p>
</div>
<cr-button class="Gi_one-right" type="primary" @click="getCoupon(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="coupon.id" v-for="coupon 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="getCoupon(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) {
const { coupons } = await operationApi.getCoupons({ couponIds: newVal.join(',') });
if(coupons && coupons.length) this.list = coupons;
// console.log('coupons', this.list);
},
immediate: true
}
},
methods: {
async getCoupon(coupon) {
if (!coupon.pickupAble) {
await operationApi.pickupCoupon({ couponId: coupon.couponId });
this.$toast.success('领取成功');
} else {
window.location.href = coupon.navUrl;
}
},
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="top-bar">
<img class="top-bar-left" :src="leftImg" />
<div class="top-bar-right">
<a :href="href">
<button class="top-bar-button">打开APP</button>
</a>
<img class="top-bar-close" @click="close" src="./images/close@2x.png" />
</div>
</div>
</template>
<script>
export default {
name: "DownloadGuide",
props: {
href: String,
leftImg: String
},
methods: {
close() {
this.$emit('delete');
}
}
}
</script>
<style lang="less" scoped>
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
background-color: #d7e6ff;
padding: 0 20px 0 15px;
.top-bar-left {
width: 92px;
height: 24px;
}
.top-bar-right {
display: flex;
align-items: center;
a {
margin-right: 15px;
font-size: 0;
.top-bar-button {
width: 70px;
height: 24px;
line-height: 24px;
border-radius: 24px !important;
background-color: #5573eb !important;
font-size: 13px;
}
}
.top-bar-close {
width: 20px;
height: 20px;
}
}
}
</style>
\ No newline at end of file
import Http from '@/service/http.service.ts';
import Cookies from '@/service/cookieStorage.service.ts';
import localStorage from '@/service/localStorage.service.ts';
import qs from 'qs';
// const ApiBaseUrl = 'http://passportapi.q-gp.com';
const ApiBaseUrl = 'https://passportapi-fe.liangkebang.net';
export default {
getCaptcha() {
return Http.post(ApiBaseUrl + '/api/captcha');
},
sendVcode(phone, captcha, captchaId) {
const params = {
phoneNo: phone,
captchaId,
captchaValue: captcha,
};
return Http.post(ApiBaseUrl + '/api/sms/send_login_code_new_forH5', qs.stringify(params));
},
sendGtVcode(gt) {
return Http.post(ApiBaseUrl + '/api/sms/send_login_code_new_forH5', qs.stringify(gt));
},
fastLogin(phone, vcode, clickId, uniqueKey, registerFrom = 1) {
const headers = {
Authorization: 'Verification ' + btoa(phone + ':' + vcode),
};
const params = {
channelId: 1,
createdFrom: registerFrom,
key: 'xyqb',
btRegisterChannelId: '',
dimension: '',
click_id: clickId,
uniqueKey,
};
return Http.post(ApiBaseUrl + '/user/login/fastV1', qs.stringify(params), {
headers,
});
},
// 判断极验还是qg
verifyMode(clientType, phoneNo) {
return Http.post(ApiBaseUrl + '/api/captcha/new', { clientType, phoneNo });
},
goNext(params) {
return Http.post(ApiBaseUrl + '/platform/api/page/next', qs.stringify(params));
}
};
<template>
<cr-form
class="login-form"
ref="loginForm"
show-error
validate-trigger="onBlur"
@submit="login"
@failed="onFormFailed"
>
<cr-field
v-model="formData.phoneNo"
name="phoneNo"
type="tel"
label=""
placeholder="请填写您的手机号"
:rules="validOptions[0]"
/>
<cr-field
v-if="captchaShow"
class="send-sms-field"
v-model="formData.captcha"
name="captcha"
type="digit"
label=""
placeholder="请填写图片验证码"
:rules="validOptions[1]"
>
<template slot="button">
<img :src="captchaUrl" @click="getCaptcha()" class="captchaUrl" />
</template>
</cr-field>
<cr-field
class="send-sms-field"
v-model="formData.verifyCode"
name="verifyCode"
type="tel"
label=""
placeholder="请填写验证码"
:rules="validOptions[1]"
>
<template slot="button">
<a
href="javascript:;"
:style="vcStyles"
:class="['login-form-send', { 'not-allow': noAllowSend || showCount }]"
ref="sendVcode"
>
{{ captchaBtnText }}
</a>
</template>
</cr-field>
<cr-button :color="btnColor" :style="btnStyles" shape='circle' block native-type="submit" class="login-form-submit">
{{ btnTxt }}
</cr-button>
</cr-form>
</template>
<script>
import Vue from 'vue';
import Storage from '@/service/localStorage.service.ts';
import Cookies from '@/service/cookieStorage.service.ts';
import { isPhone } from '@/service/utils.service.ts';
import Api from './api/api.ts';
import config from '@/config/index';
const validOptions = [
[
{ required: true, message: "请填写您的手机号" },
{ validator: isPhone, message: "请填写正确的手机号码" }
],
[{ required: true, message: "请填写验证码" }]
];
let timer = null;
let sa = null;
export default {
name: "KALoginForm",
props: {
btnTxt: String,
btnColor: {
type: String,
default: '#000'
},
btnImage: String,
vcBgColor: String,
registerFrom: String,
},
data() {
return {
showCount: false,
count: null,
captchaUrl: "",
captchaShow: false,
gtShow: false,
captchaId: "",
gt: {
clientType: "h5",
verifyType: "gt",
geetest_challenge: "",
geetest_validate: "",
geetest_seccode: "",
phoneNo: ""
},
uniqueKey: "",
formData: {
phoneNo: "",
verifyCode: "",
captcha: ""
},
validOptions
};
},
computed: {
noAllowSend() {
return !isPhone(this.formData.phoneNo);
},
captchaBtnText() {
return this.showCount ? `剩余${this.count}秒` : '获取验证码';
},
btnStyles() {
return this.btnImage ? {
background: `url(${this.btnImage}) no-repeat 0 0 / 100%`,
} : {};
},
vcStyles() {
return { backgroundColor: this.vcBgColor };
}
},
mounted() {
require('./jy/jy.ts');
this.initSa();
this.verifyMode();
},
methods: {
initSa() {
sa = require('sa-sdk-javascript');
//神策数据埋点
sa.init({
server_url: config.shenceUrl,
heatmap: {
//是否开启点击图,默认 default 表示开启,自动采集 $WebClick 事件,可以设置 'not_collect' 表示关闭
clickmap:'default',
//是否开启触达注意力图,默认 default 表示开启,自动采集 $WebStay 事件,可以设置 'not_collect' 表示关闭
scroll_notice_map:'default',
//判断外层是否有 App 的 SDK ,如果有的话,会往 App 的 SDK 发数据。如果没有,就正常发送数据。
// use_app_track: true,
},
show_log: process.env.NODE_ENV !== 'production'
});
//添加公共属性
sa.registerPage({platformType:'H5'});
this.$nextTick(() => {
sa.quick("autoTrackSinglePage");
});
},
async verifyMode() {
const res = await Api.verifyMode(this.gt.clientType, this.formData.phoneNo);
this.uniqueKey = res?.uniqueKey;
if (res?.verifyType === 'gt') {
this.getJyParam(res);
} else {
this.getCaptcha();
}
},
getJyParam({ gt, challenge, success }) {
var vm = this;
initGeetest(
{
width: "100%",
gt: gt,
challenge: challenge,
offline: !success,
product: "bind"
},
function(captchaObj) {
vm.$refs.sendVcode.addEventListener('click', function() {
if (!isPhone(vm.formData.phoneNo)) {
vm.$notify({ type: "warning", message: "请填写正确的手机号码" });
return;
}
if (vm.showCount) return;
captchaObj.verify();
});
captchaObj.onSuccess(function() {
var result = captchaObj.getValidate();
vm.gt.geetest_challenge = result.geetest_challenge;
vm.gt.geetest_validate = result.geetest_validate;
vm.gt.geetest_seccode = result.geetest_seccode;
vm.getCode(captchaObj);
});
captchaObj.onError(function() {
vm.$toast("尝试过多,请点击重试");
});
}
);
},
async getCaptcha() {
this.captchaShow = true;
const res = await Api.getCaptcha();
({ image: this.captchaUrl, imageId: this.captchaId } = res);
},
async getCode(captchaObj) {
if (!isPhone(this.formData.phoneNo)) {
this.$notify({ type: "warning", message: "请填写正确的手机号码" });
return;
}
if (timer) return;
try {
await this.sendCode();
this.smsCount();
this.$notify({ type: "success", message: "短信验证码已发送,请注意查收" });
} finally {
this.resetJyState(captchaObj);
}
},
async sendCode() {
const { phoneNo, captcha } = this.formData;
if (this.captchaShow) {
await Api.sendVcode(phoneNo, captcha, this.captchaId);
} else {
await Api.sendGtVcode({ ...this.gt, phoneNo: phoneNo });
}
},
smsCount() {
this.showCount = true;
this.count = 60;
timer = setInterval(() => {
if (this.count > 0 && this.count <= 60) {
this.count--;
} else {
this.showCount = false;
clearInterval(timer);
timer = null;
}
}, 1000);
},
resetJyState(captchaObj) {
if(captchaObj) captchaObj.reset();
},
async login() {
let le_is_login_success = false;
try {
const { phoneNo, verifyCode } = this.formData;
const user = await Api.fastLogin(phoneNo, verifyCode, null, this.uniqueKey, this.registerFrom);
this.saveInfo(user);
le_is_login_success = true;
this.goNext();
} finally {
sa.track('LoginEvent', {
le_login_action: '注册/登录',
le_login_type: '验证码登录',
le_is_login_success,
le_channel_code: this.registerFrom,
});
}
},
async goNext() {
const extData = { redirectURL: null, productId: 1 };
const params = {
key: 'xyqb',
target: 'cashTarget4',
currentPage: 'login'
};
const { transition } = await Api.goNext(params);
window.location.replace(
transition + '?phoneNo=' + Storage.get('phoneNo') + '&protocol=' + window.location.protocol + '&token=' + Storage.get('token') + (extData ? '&extData=' + btoa(JSON.stringify(extData)) : '')
);
},
onFormFailed(errorInfo) {
const { errors } = errorInfo;
this.$notify({ type: "warning", message: errors[0].message });
},
saveInfo({ token, phoneNo, uuid }) {
Storage.set('token', token);
Cookies.set('phoneNo', phoneNo);
if (uuid) {
Cookies.set('userid', uuid);
sa.login(uuid);
}
}
},
};
</script>
<style lang="less">
@import './jy/jy.less';
.login-form {
padding: 0 20px;
width: 100%;
.cr-field--error-message {
display: none;
}
.cr-cell {
&__value {
overflow: initial;
}
&-group {
&::after {
display: none;
}
}
background-color: #fff;
border-radius: 8px;
margin-bottom: 12px;
align-items: center;
padding: 12.5px 16px;
&::after {
display: none;
}
&__title {
color: #333333;
display: flex;
align-items: center;
}
}
&-btn {
width: 90px;
position: relative;
}
&-send {
padding: 5px 10px 4px;
border-radius: 8px;
background-color: #97d5ff;
color: #fff;
&.not-allow {
background-color: #eaeaea !important;
color: #999;
}
}
&-submit {
height: 50px !important;
border-radius: 8px !important;
border: 0 !important;
}
}
.send-sms-field {
padding: 8px 6px 8px 15px !important;
}
</style>
//覆盖弹出框 滑动模块形式
.geetest_wind.geetest_panel .geetest_panel_box.geetest_panelshowslide{
width: 278px !important;
height: 285px !important;
}
.geetest_holder.geetest_mobile.geetest_ant.geetest_popup .geetest_popup_box {
width: 278px !important;
height: 285px !important;
max-width:none !important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_slider .geetest_slider_track .geetest_slider_tip {
font-size: 14px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel .geetest_copyright .geetest_copyright_tip {
font-size: 11px!important;
}
.geetest_holder.geetest_wind .geetest_radar_btn, .geetest_holder.geetest_wind .geetest_success_btn {
height: 44px!important;
}
.geetest_holder.geetest_wind .geetest_radar_tip, .geetest_holder.geetest_wind .geetest_success_radar_tip {
height: 44px!important;
line-height: 44px!important;
font-size: 14px!important;
// padding: 0 0 0 46px!important;
}
.geetest_holder.geetest_wind {
height: 44px!important;
min-width: 260px!important;
}
.geetest_holder.geetest_wind .geetest_logo, .geetest_holder.geetest_wind .geetest_success_logo {
width: 20px!important;
height: 20px!important;
top: 11px!important;
right: 12px!important;
}
.geetest_holder.geetest_wind .geetest_wait .geetest_wait_dot {
width: 20px!important;
height: 20px!important;
margin: 2px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result.geetest_fail {
height: 24px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result {
height: 24px!important;
bottom: -25px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_showTip {
bottom:0px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_widget .geetest_window .geetest_result .geetest_result_content {
font-size: 14px!important;
text-indent: 16px!important;
line-height: 24px!important;
height: 24px!important;
bottom: -25px!important;
}
// 点击形式
.geetest_wind.geetest_panel .geetest_panel_box.geetest_panelshowclick {
width: 320px !important;
height: 410px !important;
}
.geetest_holder.geetest_silver.geetest_popup .geetest_popup_box {
min-width: 210px!important;
max-width: 348px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips {
font-size: 16px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips .geetest_tip_content {
font-size: 16px!important;
height: 20px!important;
}
.geetest_holder.geetest_silver .geetest_head .geetest_tips .geetest_tip_img {
width: 116px!important;
height: 40px!important;
right: -116px!important;
top: -10px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_small {
height: 23px!important;
margin-top: -11.5px!important;
}
.geetest_holder.geetest_silver .geetest_panel a.geetest_close, .geetest_holder.geetest_silver .geetest_panel a.geetest_refresh, .geetest_holder.geetest_silver .geetest_panel a.geetest_feedback{
height: 23px!important;
width: 23px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_commit .geetest_commit_tip {
margin-top: -10px!important;
height: 20px!important;
line-height: 20px!important;
font-size: 16px!important;
letter-spacing: 2px!important;
}
.geetest_holder.geetest_silver .geetest_panel .geetest_commit {
border-radius: 2px!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_result_tip {
bottom: -29px!important;
height: 28px!important;
text-indent: 9px!important;
font-size: 16px!important;
line-height: 29px!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_result_tip.geetest_up {
bottom:0px!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel .geetest_copyright {
display: none!important;
}
.geetest_holder.geetest_mobile.geetest_ant .geetest_panel a.geetest_feedback {
display: none!important;
}
.geetest_holder.geetest_wind.geetest_radar_error .geetest_logo {
display: none!important;
}
.geetest_holder.geetest_silver .geetest_panel a.geetest_feedback {
display: none!important;
}
.geetest_holder.geetest_wind .geetest_logo, .geetest_holder.geetest_wind .geetest_success_logo {
display: none!important;
}
.geetest_holder.geetest_silver .geetest_table_box .geetest_window .geetest_item .geetest_big_mark .geetest_mark_no {
// height: 24px!important;
// line-height: 24px!important;
font-size: 18px!important;
}
.geetest_holder.geetest_wind .geetest_radar_tip, .geetest_holder.geetest_wind .geetest_success_radar_tip {
padding: 0!important;
}
// tslint:disable
/* initGeetest 1.0.0
* 用于加载id对应的验证码库,并支持宕机模式
* 暴露 initGeetest 进行验证码的初始化
* 一般不需要用户进行修改
*/
(function(global, factory) {
'use strict';
if (typeof module === 'object' && typeof module.exports === 'object') {
// CommonJS
module.exports = global.document ?
factory(global, true) :
function(w) {
if (!w.document) {
throw new Error('Geetest requires a window with a document');
}
return factory(w);
};
} else {
factory(global);
}
})(typeof window !== 'undefined' ? window : this, function(window, noGlobal) {
'use strict';
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
const document = window.document;
const Math = window.Math;
const head = document.getElementsByTagName('head')[0];
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each(process) {
const _obj = this._obj;
for (const k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
}
};
function Config(config) {
const self = this;
new _Object(config)._each(function(key, value) {
self[key] = value;
});
}
Config.prototype = {
api_server: 'api.geetest.com',
protocol: 'http://',
type_path: '/gettype.php',
fallback_config: {
slide: {
static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'],
type: 'slide',
slide: '/static/js/geetest.0.0.0.js'
},
fullpage: {
static_servers: ['static.geetest.com', 'dn-staticdown.qbox.me'],
type: 'fullpage',
fullpage: '/static/js/fullpage.0.0.0.js'
}
},
_get_fallback_config() {
const self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else if (self.new_captcha) {
return self.fallback_config.fullpage;
} else {
return self.fallback_config.slide;
}
},
_extend(obj) {
const self = this;
new _Object(obj)._each(function(key, value) {
self[key] = value;
});
}
};
const isNumber = function(value) {
return (typeof value === 'number');
};
const isString = function(value) {
return (typeof value === 'string');
};
const isBoolean = function(value) {
return (typeof value === 'boolean');
};
const isObject = function(value) {
return (typeof value === 'object' && value !== null);
};
const isFunction = function(value) {
return (typeof value === 'function');
};
const callbacks = {};
const status = {};
const random = function() {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
const loadScript = function(url, cb) {
const script = document.createElement('script');
script.charset = 'UTF-8';
script.async = true;
script.onerror = function() {
cb(true);
};
let loaded = false;
script.onload = script.onreadystatechange = function() {
if (!loaded &&
(!script.readyState ||
'loaded' === script.readyState ||
'complete' === script.readyState)) {
loaded = true;
setTimeout(function() {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
};
const normalizeDomain = function(domain) {
return domain.replace(/^https?:\/\/|\/$/g, '');
};
const normalizePath = function(path) {
path = path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
const normalizeQuery = function(query) {
if (!query) {
return '';
}
let q = '?';
new _Object(query)._each(function(key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
const makeURL = function(protocol, domain, path, query) {
domain = normalizeDomain(domain);
let url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
const load = function(protocol, domains, path, query, cb) {
const tryRequest = function(at) {
const url = makeURL(protocol, domains[at], path, query);
loadScript(url, function(err) {
if (err) {
if (at >= domains.length - 1) {
cb(true);
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
});
};
tryRequest(0);
};
const jsonp = function(domains, path, config, callback) {
if (isObject(config.getLib)) {
config._extend(config.getLib);
callback(config);
return;
}
if (config.offline) {
callback(config._get_fallback_config());
return;
}
const cb = 'geetest_' + random();
window[cb] = function(data) {
if (data.status === 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
callback(config._get_fallback_config());
}
window[cb] = undefined;
try {
delete window[cb];
} catch (e) {
}
};
load(config.protocol, domains, path, {
gt: config.gt,
callback: cb
}, function(err) {
if (err) {
callback(config._get_fallback_config());
}
});
};
const throwError = function(errorType, config) {
const errors = {
networkError: '网络错误'
};
if (typeof config.onError === 'function') {
config.onError(errors[errorType]);
} else {
throw new Error(errors[errorType]);
}
};
const detect = function() {
return !!window.Geetest;
};
if (detect()) {
status.slide = 'loaded';
}
const initGeetest = function(userConfig, callback) {
const config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
jsonp([config.api_server || config.apiserver], config.type_path, config, function(newConfig) {
const type = newConfig.type;
const init = function() {
config._extend(newConfig);
callback(new window.Geetest(config));
};
callbacks[type] = callbacks[type] || [];
const s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
load(config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function(err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config);
} else {
status[type] = 'loaded';
const cbs = callbacks[type];
for (let i = 0, len = cbs.length; i < len; i = i + 1) {
const cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
}
});
} else if (s === 'loaded') {
init();
} else if (s === 'fail') {
throwError('networkError', config);
} else if (s === 'loading') {
callbacks[type].push(init);
}
});
};
window.initGeetest = initGeetest;
return initGeetest;
});
<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 { skuId, skuNum, skuSource } = goods;
const params = { skuId, skuNum, skuSource, type: 1 }
await operationApi.addShopCart(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 Goods from '../Goods/index.vue';
import Coupon from '../Coupon/index.vue';
export default {
name: 'goods-tabs',
components: {
Goods,
Coupon
},
props: {
list: {
type: Array,
default: () => []
},
childItem: {
type: Object,
default: () => ({ child: [] })
},
activeColor: {
type: String,
default: '#323233'
},
inactiveColor: {
type: String,
default: '#646566'
},
underlineColor: {
type: String,
default: '#1989fa'
}
},
data() {
return {
}
},
computed: {
tabs() {
const list = this.list.map(tab => {
const rs = { ...tab };
this.childItem?.child.forEach(ele => {
if (tab.componentId === ele.id) Object.assign(rs, tab, ele);
})
return rs;
});
// console.log('tabs', this.childItem.child, list);
return list;
}
},
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
},
loop: Boolean,
autoplay: Boolean,
animation: Boolean
},
data() {
const vm = this;
let isEven = true;
let lastEvenProgress = 0;
let lastProgress = 0;
return {
showSwiper: true,
// style: {},
swiperOptions: {
loop: this.loop,
slidesPerView: 4,
slidesPerColumn: this.slidesPerColumn,
spaceBetween: 8,
observer:true,
observeParents:true,
watchSlidesProgress: true,
autoplay: this.autoplay,
on: {
progress: function() {
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 {
transition: 'all .2s cubic-bezier(.4, 0, .2, 1)'
};
}
},
watch: {
slidesPerColumn(newVal) {
this.refreshSwiper('slidesPerColumn', newVal);
},
autoplay(newVal) {
this.refreshSwiper('autoplay', newVal);
},
loop(newVal) {
this.refreshSwiper('loop', newVal);
},
},
methods: {
go(url) {
window.location.href = url;
},
refreshSwiper(key, val) {
this.swiperOptions[key] = val;
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
if (this.editor) setTimeout(() =>this.editor.adjustHeight(), 0);
}
},
}
</script>
<style lang="less" scoped>
.guide-cube {
width: 100%;
padding: 12px;
&-swiper {
width: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
width: 81px;
height: 81px;
/deep/ .cr-image {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="marquee">
<div class="marquee-swiper" v-if='showSwiper' ref="mySwiper" v-swiper:mySwiper="swiperOptions">
<div class="swiper-wrapper">
<div class="swiper-slide" :key="index" v-for="(item, index) in list">
<a class="swiper-slide-a" @click="go(item.link)">
<span :style="styles">{{item.text}}</span>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue";
import 'swiper/dist/css/swiper.css';
if (EASY_ENV_IS_BROWSER) {
const VueAwesomeSwiper = require('vue-awesome-swiper/dist/ssr')
Vue.use(VueAwesomeSwiper)
}
export default {
inject: ['editor'],
props: {
list: {
type: Array,
default: () => ([])
},
fontColor: {
type: String,
default: '#666666'
}
},
data() {
return {
showSwiper: true,
swiperOptions: {
loop: true,
observer:true,
observeParents:true,
direction: 'vertical',
allowTouchMove: false,
autoplay: true
}
}
},
computed: {
swiper() {
return this.$refs.mySwiper.swiper
},
styles() {
return {
color: this.fontColor
}
}
},
watch: {
list: {
deep: true,
handler() {
this.refreshSwiper();
}
}
},
methods: {
go(link) {
window.location.href = link;
},
refreshSwiper() {
console.log('refreshSwiper');
this.showSwiper= false;
this.$nextTick(() => this.showSwiper= true);
}
}
}
</script>
<style lang="less" scoped>
.marquee {
width: 100%;
height: 30px;
border-radius: 8px;
background-color: #D8D8D8;
.swiper-container {
height: 100%;
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
a {
display: flex;
justify-content: center;
align-items: center;
}
span {
font-size: 14px;
line-height: 20px;
color: #666666;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="placeholder" :style="style"></div>
</template>
<script>
export default {
props: {
height: Number,
},
computed: {
style() {
return {
height: `${this.height}px`
}
}
}
}
</script>
<style lang="less" scoped>
.placeholder {
min-height: 10px;
}
</style>
\ No newline at end of file
// export const basicComponents = require('@qg/cherry-ui/md/index.json');
// export const kaLoginForm = require('@qg/citrus-ui/md/index.json');
export const basicComponents = [
{
eleName: 'freedom-container',
title: '自由容器',
config: [
// {
// key: 'backgroundImage',
// name: '背景图片',
// type: 'Upload'
// },
// {
// key: 'backgroundColor',
// name: '背景颜色',
// type: 'ColorSelector'
// },
],
value: {
// backgroundImage: 'http://activitystatic.q-gp.com/landing-bg.png';
// backgroundColor: '#fff'
},
commonStyle: {}
},
{
eleName: 'cr-button',
title: 'Button 按钮',
config: [
{
key: 'type',
name: '类型',
desc: '类型,可选值为',
options: [
'default',
'primary',
'info',
'danger',
'waring',
'success'
],
type: 'select'
},
{
key: 'size',
name: '尺寸',
desc: '尺寸,可选值为',
options: [
'normal',
'large',
'small',
'mini'
],
type: 'select'
},
{
key: 'color',
name: '颜色',
desc: '按钮颜色,支持传入',
type: 'ColorSelector'
},
{
key: 'icon',
name: '图标',
desc: '左侧',
type: 'text'
},
{
key: 'block',
name: '块级元素',
desc: '是否为块级元素',
type: 'checkbox'
},
{
key: 'shape',
name: '形状',
desc: '按钮形状,可选值为',
options: [
'square',
'circle'
],
type: 'select'
},
{
key: 'disabled',
name: '禁用',
desc: '是否禁用按钮',
type: 'checkbox'
},
{
key: 'plain',
name: '朴素按钮',
desc: '朴素按钮',
type: 'checkbox'
},
{
key: 'hairline',
name: '细边框',
desc: '细边框',
type: 'checkbox'
},
{
key: 'loading',
name: '加载中',
desc: '是否显示为加载状态',
type: 'checkbox'
},
{
key: 'loadingText',
name: '加载文字',
desc: '加载状态提示文字',
type: 'text'
},
{
key: 'loadingType',
name: '加载图标类型',
desc: '',
options: [
'circular',
'spinner'
],
type: 'select'
},
{
key: 'loadingSize',
name: '加载图标大小',
desc: '加载图标大小',
type: 'text'
}
],
value: {
type: 'default',
size: 'normal',
color: '',
icon: '',
block: false,
shape: 'square',
disabled: false,
plain: false,
hairline: false,
loading: false,
loadingText: '',
loadingType: 'circular',
loadingSize: '20px'
},
commonStyle: {}
},
{
eleName: 'cr-image',
title: '图片',
config: [
{
key: 'width',
name: '宽度',
type: 'text'
},
{
key: 'height',
name: '高度',
type: 'text'
},
{
key: 'src',
name: '图片',
type: 'Upload'
},
],
value: {
width: '5.067rem',
height: '3.733rem',
src: 'https://appsync.lkbang.net/Fs0qmUsMry39AjHDf_W-qgn8XEy6',
fit: 'contain',
},
commonStyle: {}
},
{
eleName: 'cr-nav-bar',
title: 'NavBar 导航栏',
config: [
{
key: 'title',
name: '标题',
desc: '标题',
propType: 'string',
options: [],
type: 'text'
},
{
key: 'leftText',
name: '左侧文案',
desc: '左侧文案',
propType: 'string',
options: [],
type: 'text'
},
{
key: 'rightText',
name: '右侧文案',
desc: '右侧文案',
default: '',
options: [
'info',
'danger',
'primary',
'warning'
],
type: 'text'
},
{
key: 'leftArrow',
name: '左侧箭头',
desc: '是否显示左侧箭头',
propType: 'boolean',
options: [],
type: 'checkbox'
},
{
key: 'border',
name: '边框',
desc: '是否显示下边框',
propType: 'boolean',
options: [],
type: 'checkbox'
},
{
key: 'fixed',
name: '固定导航栏',
desc: '是否固定导航栏',
propType: 'boolean',
options: [],
type: 'checkbox'
},
{
key: 'zIndex',
name: 'z-index',
desc: '元素z-index',
propType: [
'number',
'string'
],
options: [],
type: 'text'
}
],
value: {
title: '',
leftText: '返回',
rightText: '',
leftArrow: true,
border: true,
fixed: true,
zIndex: '9999'
},
commonStyle: {}
}
];
export const businessComponents = [
// kaLoginForm,
{
eleName: 'cs-ka-login-form',
title: 'KA登陆表单',
h: 224,
config: [
{
key: 'btnTxt',
name: '按钮文案',
type: 'text'
},
{
key: 'btnColor',
name: '按钮颜色',
type: 'ColorSelector'
},
{
key: 'btnImage',
name: '按钮图片',
type: 'Upload'
},
{
key: 'vcBgColor',
name: '验证码背景色(可点击态)',
type: 'ColorSelector'
},
{
key: 'registerFrom',
name: '渠道号',
type: 'text'
},
],
value: {
btnTxt: '',
btnColor: '#ee0a24',
btnImage: 'http://activitystatic.q-gp.com/landing-submit.png',
registerFrom: '',
vcBgColor: '#97d5ff'
},
commonStyle: {}
},
{
eleName: 'download-guide',
title: '下载引导',
h: 40,
config: [
{
key: 'href',
name: '跳转链接',
type: 'text'
},
{
key: 'leftImg',
name: 'logo',
type: 'Upload'
}
],
value: {
href: 'https://s.xyqb.com/4',
leftImg: 'http://activitystatic.q-gp.com/xyqb%402x.png'
},
commonStyle: {}
},
{
eleName: 'goods-tabs',
title: '商品导航',
config: [
{
title: '标签录入',
children: [
{
key: 'list',
name: '添加菜单',
type: 'FormList'
formControl: [
{
key: 'tabTitle',
name: '名称',
type: 'text'
},
{
key: 'componentId',
name: '组件',
type: 'ComponentSelect', // componentSelect
}
]
}
]
},
{
title: '标签样式',
children: [
{
key: 'activeColor',
name: '选中态文字颜色',
type: 'ColorSelector'
},
{
key: 'inactiveColor',
name: '默认态文字颜色',
type: 'ColorSelector'
},
{
key: 'underlineColor',
name: '下划线颜色',
type: 'ColorSelector'
},
]
}
],
value: {
list: [{
tabTitle: 'tab1',
componentId: '1'
}, {
tabTitle: 'tab2',
componentId: '2'
}],
activeColor: '#323233',
inactiveColor: '#646566',
underlineColor: '#1989fa',
},
commonStyle: {}
},
{
eleName: 'Goods',
title: '商品',
config: [
{
title: '商品录入',
children: [{
key: 'column',
name: '商品列数',
type: 'ColumnSelector'
},
{
key: 'goods',
name: '选择商品',
type: 'GoodsTableModal'
formControl: [
{
key: 'goodsNumber',
name: '商品数量',
type: 'number'
}
]
}]
},
{
title: '专题推荐',
children: [
{
key: 'showSpecial',
name: '显示专题',
type: 'checkbox'
},
{
key: 'specialUrl',
name: '专题图片',
type: 'Upload'
},
{
key: 'specialLink',
name: '专题链接',
type: 'text'
},
]
}
],
value: {
goods: {
type: 'goodsGroup',
ids: []
},
goodsNumber: 3,
column: 2,
showSpecial: true,
specialUrl: '',
specialLink: ''
},
commonStyle: {}
},
{
eleName: 'Coupon',
title: '优惠券',
config: [
{
title: '优惠券录入',
children: [
{
key: 'column',
name: '优惠券列数',
type: 'ColumnSelector'
},
{
key: 'couponsList',
name: '选择优惠券',
type: 'CouponTableModal'
formControl: [
{
key: 'couponsNumber',
name: '优惠券数量',
type: 'number'
}
]
},
],
},
{
title: '优惠券样式',
children: [
{
key: 'bgImage',
name: '优惠券背景图片',
type: 'Upload'
},
{
key: 'bgColor',
name: '优惠券背景颜色',
type: 'ColorSelector'
},
]
}
],
value: {
couponsList: [],
couponsNumber: 3,
column: 3,
bgColor: '',
bgImage: '',
},
commonStyle: {}
},
{
eleName: 'Advertisement',
title: '广告',
config: [
{
key: 'column',
name: '广告列数',
type: 'ColumnSelector'
},
{
key: 'list',
name: '添加广告',
type: 'FormList'
formControl: [
{
key: 'img',
name: '图片',
type: 'Upload'
},
{
key: 'link',
name: '链接',
type: 'text', // componentSelect
}
]
}
],
value: {
list: [{
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}],
column: 2
}
},
{
eleName: 'Placeholder',
title: '楼层间隔',
config: [
{
key: 'height',
name: '间隔高度',
type: 'Slider'
},
],
value: {
height: 10
}
},
{
eleName: 'GuideCube',
title: '导购魔方',
config: [
{
key: 'slidesPerColumn',
name: '行数',
type: 'InputNumber'
},
{
key: 'loop',
name: '循环',
type: 'checkbox'
},
{
key: 'autoplay',
name: '自动播放',
type: 'checkbox'
},
{
key: 'animation',
name: '切换动画',
type: 'checkbox'
},
{
key: 'list',
name: '添加导购',
type: 'FormList'
formControl: [
{
key: 'img',
name: '图片',
type: 'Upload'
},
{
key: 'link',
name: '链接',
type: 'text', // componentSelect
}
]
}
],
value: {
list: [{
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}, {
img: 'http://activitystatic.q-gp.com/landing-bg.png',
link: 'http://activitystatic.q-gp.com'
}],
slidesPerColumn: 1,
loop: false,
autoplay: false,
animation: false
}
},
{
eleName: 'CustomMarquee',
title: '跑马灯',
config: [
{
key: 'fontColor',
name: '字体颜色',
type: 'ColorSelector',
},
{
key: 'list',
name: '添加数据',
type: 'FormList'
formControl: [
{
key: 'text',
name: '文案',
type: 'text'
},
{
key: 'link',
name: '链接',
type: 'text'
},
]
}
],
value: {
fontColor: '#666666',
list: [{
text: '请输入文案'
}, {
text: '请输入文案'
}]
}
},
];
\ No newline at end of file
import { Component, Prop, Vue, Mixins } from 'vue-property-decorator';
import { Action, Mutation, State, Getter } from 'vuex-class';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import CustomMarquee from '@/lib/Marquee/index.vue';
import { resizeDiv } from '@/service/utils.service';
@Component({ components: { CustomMarquee }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.curChildIndex) curChildIndex;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
@Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop(String) backgroundImage;
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">
<component :class="['freedom-body-item', { 'Fb-item_selected': curChildIndex === index }]" v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle, 'container')" :is="item.name" :key="index" v-bind="item.props"></component>
</div>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.freedom {
height: 100%;
width: 100%;
header {
width: 100%;
height: 48px;
line-height: 48px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
&-body {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
&_full {
height: calc(100% - 48px);
}
}
}
</style>
\ No newline at end of file
<template>
<div ref="item"
class="vue-grid-item"
:class="classObj"
:style="style"
>
<slot></slot>
<span v-if="resizableAndNotStatic" ref="handle" :class="resizableHandleClass"></span>
<!--<span v-if="draggable" ref="dragHandle" class="vue-draggable-handle"></span>-->
</div>
</template>
<style>
.vue-grid-item {
transition: all 200ms ease;
transition-property: left, top, right;
/* add right for rtl */
}
.vue-grid-item.no-touch {
-ms-touch-action: none;
touch-action: none;
}
.vue-grid-item.cssTransforms {
transition-property: transform;
left: 0;
right: auto;
}
.vue-grid-item.cssTransforms.render-rtl {
left: auto;
right: 0;
}
.vue-grid-item.resizing {
opacity: 0.6;
z-index: 3;
}
.vue-grid-item.vue-draggable-dragging {
transition:none;
z-index: 3;
}
.vue-grid-item.vue-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.vue-grid-item > .vue-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=');
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
}
.vue-grid-item > .vue-rtl-resizable-handle {
bottom: 0;
left: 0;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAuMDAwMDAwMDAwMDAwMDAyIiBoZWlnaHQ9IjEwLjAwMDAwMDAwMDAwMDAwMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDwhLS0gQ3JlYXRlZCB3aXRoIE1ldGhvZCBEcmF3IC0gaHR0cDovL2dpdGh1Yi5jb20vZHVvcGl4ZWwvTWV0aG9kLURyYXcvIC0tPgogPGc+CiAgPHRpdGxlPmJhY2tncm91bmQ8L3RpdGxlPgogIDxyZWN0IGZpbGw9Im5vbmUiIGlkPSJjYW52YXNfYmFja2dyb3VuZCIgaGVpZ2h0PSIxMiIgd2lkdGg9IjEyIiB5PSItMSIgeD0iLTEiLz4KICA8ZyBkaXNwbGF5PSJub25lIiBvdmVyZmxvdz0idmlzaWJsZSIgeT0iMCIgeD0iMCIgaGVpZ2h0PSIxMDAlIiB3aWR0aD0iMTAwJSIgaWQ9ImNhbnZhc0dyaWQiPgogICA8cmVjdCBmaWxsPSJ1cmwoI2dyaWRwYXR0ZXJuKSIgc3Ryb2tlLXdpZHRoPSIwIiB5PSIwIiB4PSIwIiBoZWlnaHQ9IjEwMCUiIHdpZHRoPSIxMDAlIi8+CiAgPC9nPgogPC9nPgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxsaW5lIGNhbnZhcz0iI2ZmZmZmZiIgY2FudmFzLW9wYWNpdHk9IjEiIHN0cm9rZS1saW5lY2FwPSJ1bmRlZmluZWQiIHN0cm9rZS1saW5lam9pbj0idW5kZWZpbmVkIiBpZD0ic3ZnXzEiIHkyPSItNzAuMTc4NDA3IiB4Mj0iMTI0LjQ2NDE3NSIgeTE9Ii0zOC4zOTI3MzciIHgxPSIxNDQuODIxMjg5IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSJub25lIi8+CiAgPGxpbmUgc3Ryb2tlPSIjNjY2NjY2IiBzdHJva2UtbGluZWNhcD0idW5kZWZpbmVkIiBzdHJva2UtbGluZWpvaW49InVuZGVmaW5lZCIgaWQ9InN2Z181IiB5Mj0iOS4xMDY5NTciIHgyPSIwLjk0NzI0NyIgeTE9Ii0wLjAxODEyOCIgeDE9IjAuOTQ3MjQ3IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiLz4KICA8bGluZSBzdHJva2UtbGluZWNhcD0idW5kZWZpbmVkIiBzdHJva2UtbGluZWpvaW49InVuZGVmaW5lZCIgaWQ9InN2Z183IiB5Mj0iOSIgeDI9IjEwLjA3MzUyOSIgeTE9IjkiIHgxPSItMC42NTU2NCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiM2NjY2NjYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+);
background-position: bottom left;
padding-left: 3px;
background-repeat: no-repeat;
background-origin: content-box;
cursor: sw-resize;
right: auto;
}
.vue-grid-item.disable-userselect {
user-select: none;
}
</style>
<script>
import {setTopLeft, setTopRight, setTransformRtl, setTransform} from './helpers/utils';
import {getControlPosition, createCoreData} from './helpers/draggableUtils';
import {getColsFromBreakpoint} from './helpers/responsiveUtils';
import {getDocumentDir} from "./helpers/DOM";
// var eventBus = require('./eventBus');
import '@interactjs/auto-start'
import '@interactjs/actions/drag'
import '@interactjs/actions/resize'
import '@interactjs/modifiers'
import '@interactjs/dev-tools'
import interact from '@interactjs/interact'
export default {
name: "GridItem",
props: {
/*cols: {
type: Number,
required: true
},*/
/*containerWidth: {
type: Number,
required: true
},
rowHeight: {
type: Number,
required: true
},
margin: {
type: Array,
required: true
},
maxRows: {
type: Number,
required: true
},*/
isDraggable: {
type: Boolean,
required: false,
default: null
},
isResizable: {
type: Boolean,
required: false,
default: null
},
/*useCssTransforms: {
type: Boolean,
required: true
},
*/
static: {
type: Boolean,
required: false,
default: false
},
minH: {
type: Number,
required: false,
default: 1
},
minW: {
type: Number,
required: false,
default: 1
},
maxH: {
type: Number,
required: false,
default: Infinity
},
maxW: {
type: Number,
required: false,
default: Infinity
},
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
},
w: {
type: Number,
required: true
},
h: {
type: Number,
required: true
},
i: {
required: true
},
dragIgnoreFrom: {
type: String,
required: false,
default: 'a, button'
},
dragAllowFrom: {
type: String,
required: false,
default: null
},
resizeIgnoreFrom: {
type: String,
required: false,
default: 'a, button'
},
},
inject: ["eventBus", "layout"],
data: function () {
return {
cols: 1,
containerWidth: 100,
rowHeight: 30,
margin: [10, 10],
maxRows: Infinity,
draggable: null,
resizable: null,
useCssTransforms: true,
useStyleCursor: true,
isDragging: false,
dragging: null,
isResizing: false,
resizing: null,
lastX: NaN,
lastY: NaN,
lastW: NaN,
lastH: NaN,
style: {},
rtl: false,
dragEventSet: false,
resizeEventSet: false,
previousW: null,
previousH: null,
previousX: null,
previousY: null,
innerX: this.x,
innerY: this.y,
innerW: this.w,
innerH: this.h
}
},
created () {
let self = this;
// Accessible refernces of functions for removing in beforeDestroy
self.updateWidthHandler = function (width) {
self.updateWidth(width);
};
self.compactHandler = function (layout) {
self.compact(layout);
};
self.setDraggableHandler = function (isDraggable) {
if (self.isDraggable === null) {
self.draggable = isDraggable;
}
};
self.setResizableHandler = function (isResizable) {
if (self.isResizable === null) {
self.resizable = isResizable;
}
};
self.setRowHeightHandler = function (rowHeight) {
self.rowHeight = rowHeight;
};
self.setMaxRowsHandler = function (maxRows) {
self.maxRows = maxRows;
};
self.directionchangeHandler = () => {
this.rtl = getDocumentDir() === 'rtl';
this.compact();
};
self.setColNum = (colNum) => {
self.cols = parseInt(colNum);
}
this.eventBus.$on('updateWidth', self.updateWidthHandler);
this.eventBus.$on('compact', self.compactHandler);
this.eventBus.$on('setDraggable', self.setDraggableHandler);
this.eventBus.$on('setResizable', self.setResizableHandler);
this.eventBus.$on('setRowHeight', self.setRowHeightHandler);
this.eventBus.$on('setMaxRows', self.setMaxRowsHandler);
this.eventBus.$on('directionchange', self.directionchangeHandler);
this.eventBus.$on('setColNum', self.setColNum)
this.rtl = getDocumentDir() === 'rtl';
},
beforeDestroy: function(){
let self = this;
//Remove listeners
this.eventBus.$off('updateWidth', self.updateWidthHandler);
this.eventBus.$off('compact', self.compactHandler);
this.eventBus.$off('setDraggable', self.setDraggableHandler);
this.eventBus.$off('setResizable', self.setResizableHandler);
this.eventBus.$off('setRowHeight', self.setRowHeightHandler);
this.eventBus.$off('setMaxRows', self.setMaxRowsHandler);
this.eventBus.$off('directionchange', self.directionchangeHandler);
this.eventBus.$off('setColNum', self.setColNum);
if (this.interactObj) {
this.interactObj.unset() // destroy interact intance
}
},
mounted: function () {
if (this.layout.responsive && this.layout.lastBreakpoint) {
this.cols = getColsFromBreakpoint(this.layout.lastBreakpoint, this.layout.cols);
} else {
this.cols = this.layout.colNum;
}
this.rowHeight = this.layout.rowHeight;
this.containerWidth = this.layout.width !== null ? this.layout.width : 100;
this.margin = this.layout.margin !== undefined ? this.layout.margin : [10, 10];
this.maxRows = this.layout.maxRows;
if (this.isDraggable === null) {
this.draggable = this.layout.isDraggable;
} else {
this.draggable = this.isDraggable;
}
if (this.isResizable === null) {
this.resizable = this.layout.isResizable;
} else {
this.resizable = this.isResizable;
}
this.useCssTransforms = this.layout.useCssTransforms;
this.useStyleCursor = this.layout.useStyleCursor;
this.createStyle();
},
watch: {
isDraggable: function () {
this.draggable = this.isDraggable;
},
static: function () {
this.tryMakeDraggable();
this.tryMakeResizable();
},
draggable: function () {
this.tryMakeDraggable();
},
isResizable: function () {
this.resizable = this.isResizable;
},
resizable: function () {
this.tryMakeResizable();
},
rowHeight: function () {
this.createStyle();
this.emitContainerResized();
},
cols: function () {
this.tryMakeResizable();
this.createStyle();
this.emitContainerResized();
},
containerWidth: function () {
this.tryMakeResizable();
this.createStyle();
this.emitContainerResized();
},
x: function (newVal) {
this.innerX = newVal;
this.createStyle();
},
y: function (newVal) {
this.innerY = newVal;
this.createStyle();
},
h: function (newVal) {
this.innerH = newVal
this.createStyle();
// this.emitContainerResized();
},
w: function (newVal) {
this.innerW = newVal;
this.createStyle();
// this.emitContainerResized();
},
renderRtl: function () {
// console.log("### renderRtl");
this.tryMakeResizable();
this.createStyle();
},
minH: function () {
this.tryMakeResizable();
},
maxH: function () {
this.tryMakeResizable();
},
minW: function () {
this.tryMakeResizable();
},
maxW: function () {
this.tryMakeResizable();
},
"$parent.margin": function (margin) {
if (!margin || (margin[0] == this.margin[0] && margin[1] == this.margin[1])) {
return;
}
this.margin = margin.map(m => Number(m));
this.createStyle();
this.emitContainerResized();
},
},
computed: {
classObj() {
return {
'vue-resizable' : this.resizableAndNotStatic,
'static': this.static,
'resizing' : this.isResizing,
'vue-draggable-dragging' : this.isDragging,
'cssTransforms' : this.useCssTransforms,
'render-rtl' : this.renderRtl,
'disable-userselect': this.isDragging,
'no-touch': this.isAndroid && this.draggableOrResizableAndNotStatic
}
},
resizableAndNotStatic(){
return this.resizable && !this.static;
},
draggableOrResizableAndNotStatic(){
return (this.draggable || this.resizable) && !this.static;
},
isAndroid() {
return true;
// return navigator.userAgent.toLowerCase().indexOf("android") !== -1;
},
renderRtl() {
return (this.layout.isMirrored) ? !this.rtl : this.rtl;
},
resizableHandleClass() {
if (this.renderRtl) {
return 'vue-resizable-handle vue-rtl-resizable-handle';
} else {
return 'vue-resizable-handle';
}
}
},
methods: {
createStyle: function () {
if (this.x + this.w > this.cols) {
this.innerX = 0;
this.innerW = (this.w > this.cols) ? this.cols : this.w
} else {
this.innerX = this.x;
this.innerW = this.w;
}
let pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
if (this.isDragging) {
pos.top = this.dragging.top;
// Add rtl support
if (this.renderRtl) {
pos.right = this.dragging.left;
} else {
pos.left = this.dragging.left;
}
}
if (this.isResizing) {
pos.width = this.resizing.width;
pos.height = this.resizing.height;
}
let style;
// CSS Transforms support (default)
if (this.useCssTransforms) {
// Add rtl support
if (this.renderRtl) {
style = setTransformRtl(pos.top, pos.right, pos.width, pos.height);
} else {
style = setTransform(pos.top, pos.left, pos.width, pos.height);
}
} else { // top,left (slow)
// Add rtl support
if (this.renderRtl) {
style = setTopRight(pos.top, pos.right, pos.width, pos.height);
} else {
style = setTopLeft(pos.top, pos.left, pos.width, pos.height);
}
}
this.style = style;
},
emitContainerResized() {
// this.style has width and height with trailing 'px'. The
// resized event is without them
let styleProps = {};
for (let prop of ['width', 'height']) {
let val = this.style[prop];
let matches = val.match(/^(\d+)px$/);
if (! matches)
return;
styleProps[prop] = matches[1];
}
this.$emit("container-resized", this.i, this.h, this.w, styleProps.height, styleProps.width);
},
handleResize: function (event) {
if (this.static) return;
const position = getControlPosition(event);
// Get the current drag point from the event. This is used as the offset.
if (position == null) return; // not possible but satisfies flow
const {x, y} = position;
const newSize = {width: 0, height: 0};
let pos;
switch (event.type) {
case "resizestart": {
this.previousW = this.innerW;
this.previousH = this.innerH;
pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
newSize.width = pos.width;
newSize.height = pos.height;
this.resizing = newSize;
this.isResizing = true;
break;
}
case "resizemove": {
// console.log("### resize => " + event.type + ", lastW=" + this.lastW + ", lastH=" + this.lastH);
const coreEvent = createCoreData(this.lastW, this.lastH, x, y);
if (this.renderRtl) {
newSize.width = this.resizing.width - coreEvent.deltaX;
} else {
newSize.width = this.resizing.width + coreEvent.deltaX;
}
newSize.height = this.resizing.height + coreEvent.deltaY;
///console.log("### resize => " + event.type + ", deltaX=" + coreEvent.deltaX + ", deltaY=" + coreEvent.deltaY);
this.resizing = newSize;
break;
}
case "resizeend": {
//console.log("### resize end => x=" +this.innerX + " y=" + this.innerY + " w=" + this.innerW + " h=" + this.innerH);
pos = this.calcPosition(this.innerX, this.innerY, this.innerW, this.innerH);
newSize.width = pos.width;
newSize.height = pos.height;
// console.log("### resize end => " + JSON.stringify(newSize));
this.resizing = null;
this.isResizing = false;
break;
}
}
// Get new WH
pos = this.calcWH(newSize.height, newSize.width);
if (pos.w < this.minW) {
pos.w = this.minW;
}
if (pos.w > this.maxW) {
pos.w = this.maxW;
}
if (pos.h < this.minH) {
pos.h = this.minH;
}
if (pos.h > this.maxH) {
pos.h = this.maxH;
}
if (pos.h < 1) {
pos.h = 1;
}
if (pos.w < 1) {
pos.w = 1;
}
this.lastW = x;
this.lastH = y;
if (this.innerW !== pos.w || this.innerH !== pos.h) {
this.$emit("resize", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
if (event.type === "resizeend" && (this.previousW !== this.innerW || this.previousH !== this.innerH)) {
this.$emit("resized", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
this.eventBus.$emit("resizeEvent", event.type, this.i, this.innerX, this.innerY, pos.h, pos.w);
},
handleDrag(event) {
if (this.static) return;
if (this.isResizing) return;
const position = getControlPosition(event);
// Get the current drag point from the event. This is used as the offset.
if (position === null) return; // not possible but satisfies flow
const {x, y} = position;
// let shouldUpdate = false;
let newPosition = {top: 0, left: 0};
switch (event.type) {
case "dragstart": {
this.previousX = this.innerX;
this.previousY = this.innerY;
let parentRect = event.target.offsetParent.getBoundingClientRect();
let clientRect = event.target.getBoundingClientRect();
if (this.renderRtl) {
newPosition.left = (clientRect.right - parentRect.right) * -1;
} else {
newPosition.left = clientRect.left - parentRect.left;
}
newPosition.top = clientRect.top - parentRect.top;
this.dragging = newPosition;
this.isDragging = true;
break;
}
case "dragend": {
if (!this.isDragging) return;
let parentRect = event.target.offsetParent.getBoundingClientRect();
let clientRect = event.target.getBoundingClientRect();
// Add rtl support
if (this.renderRtl) {
newPosition.left = (clientRect.right - parentRect.right) * -1;
} else {
newPosition.left = clientRect.left - parentRect.left;
}
newPosition.top = clientRect.top - parentRect.top;
// console.log("### drag end => " + JSON.stringify(newPosition));
// console.log("### DROP: " + JSON.stringify(newPosition));
this.dragging = null;
this.isDragging = false;
// shouldUpdate = true;
break;
}
case "dragmove": {
const coreEvent = createCoreData(this.lastX, this.lastY, x, y);
// Add rtl support
if (this.renderRtl) {
newPosition.left = this.dragging.left - coreEvent.deltaX;
} else {
newPosition.left = this.dragging.left + coreEvent.deltaX;
}
newPosition.top = this.dragging.top + coreEvent.deltaY;
// console.log("### drag => " + event.type + ", x=" + x + ", y=" + y);
// console.log("### drag => " + event.type + ", deltaX=" + coreEvent.deltaX + ", deltaY=" + coreEvent.deltaY);
// console.log("### drag end => " + JSON.stringify(newPosition));
this.dragging = newPosition;
break;
}
}
// Get new XY
let pos;
if (this.renderRtl) {
pos = this.calcXY(newPosition.top, newPosition.left);
} else {
pos = this.calcXY(newPosition.top, newPosition.left);
}
this.lastX = x;
this.lastY = y;
if (this.innerX !== pos.x || this.innerY !== pos.y) {
this.$emit("move", this.i, pos.x, pos.y);
}
if (event.type === "dragend" && (this.previousX !== this.innerX || this.previousY !== this.innerY)) {
this.$emit("moved", this.i, pos.x, pos.y);
}
this.eventBus.$emit("dragEvent", event.type, this.i, pos.x, pos.y, this.innerH, this.innerW);
},
calcPosition: function (x, y, w, h) {
const colWidth = this.calcColWidth();
// add rtl support
let out;
if (this.renderRtl) {
out = {
right: Math.round(colWidth * x + (x + 1) * this.margin[0]),
top: Math.round(this.rowHeight * y + (y + 1) * this.margin[1]),
// 0 * Infinity === NaN, which causes problems with resize constriants;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * this.margin[0]),
height: h === Infinity ? h : Math.round(this.rowHeight * h + Math.max(0, h - 1) * this.margin[1])
};
} else {
out = {
left: Math.round(colWidth * x + (x + 1) * this.margin[0]),
top: Math.round(this.rowHeight * y + (y + 1) * this.margin[1]),
// 0 * Infinity === NaN, which causes problems with resize constriants;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * this.margin[0]),
height: h === Infinity ? h : Math.round(this.rowHeight * h + Math.max(0, h - 1) * this.margin[1])
};
}
return out;
},
/**
* Translate x and y coordinates from pixels to grid units.
* @param {Number} top Top position (relative to parent) in pixels.
* @param {Number} left Left position (relative to parent) in pixels.
* @return {Object} x and y in grid units.
*/
// TODO check if this function needs change in order to support rtl.
calcXY(top, left) {
const colWidth = this.calcColWidth();
// left = colWidth * x + margin * (x + 1)
// l = cx + m(x+1)
// l = cx + mx + m
// l - m = cx + mx
// l - m = x(c + m)
// (l - m) / (c + m) = x
// x = (left - margin) / (coldWidth + margin)
let x = Math.round((left - this.margin[0]) / (colWidth + this.margin[0]));
let y = Math.round((top - this.margin[1]) / (this.rowHeight + this.margin[1]));
// Capping
x = Math.max(Math.min(x, this.cols - this.innerW), 0);
y = Math.max(Math.min(y, this.maxRows - this.innerH), 0);
return {x, y};
},
// Helper for generating column width
calcColWidth() {
const colWidth = (this.containerWidth - (this.margin[0] * (this.cols + 1))) / this.cols;
// console.log("### COLS=" + this.cols + " COL WIDTH=" + colWidth + " MARGIN " + this.margin[0]);
return colWidth;
},
/**
* Given a height and width in pixel values, calculate grid units.
* @param {Number} height Height in pixels.
* @param {Number} width Width in pixels.
* @return {Object} w, h as grid units.
*/
calcWH(height, width) {
const colWidth = this.calcColWidth();
// width = colWidth * w - (margin * (w - 1))
// ...
// w = (width + margin) / (colWidth + margin)
let w = Math.round((width + this.margin[0]) / (colWidth + this.margin[0]));
let h = Math.round((height + this.margin[1]) / (this.rowHeight + this.margin[1]));
// Capping
w = Math.max(Math.min(w, this.cols - this.innerX), 0);
h = Math.max(Math.min(h, this.maxRows - this.innerY), 0);
return {w, h};
},
updateWidth: function (width, colNum) {
this.containerWidth = width;
if (colNum !== undefined && colNum !== null) {
this.cols = colNum;
}
},
compact: function () {
this.createStyle();
},
tryMakeDraggable: function(){
const self = this;
if (this.interactObj === null || this.interactObj === undefined) {
this.interactObj = interact(this.$refs.item);
if (!this.useStyleCursor) {
this.interactObj.styleCursor(false);
}
}
if (this.draggable && !this.static) {
const opts = {
ignoreFrom: this.dragIgnoreFrom,
allowFrom: this.dragAllowFrom
};
this.interactObj.draggable(opts);
/*this.interactObj.draggable({allowFrom: '.vue-draggable-handle'});*/
if (!this.dragEventSet) {
this.dragEventSet = true;
this.interactObj.on('dragstart dragmove dragend', function (event) {
self.handleDrag(event);
});
}
} else {
this.interactObj.draggable({
enabled: false
});
}
},
tryMakeResizable: function(){
const self = this;
if (this.interactObj === null || this.interactObj === undefined) {
this.interactObj = interact(this.$refs.item);
if (!this.useStyleCursor) {
this.interactObj.styleCursor(false);
}
}
if (this.resizable && !this.static) {
let maximum = this.calcPosition(0,0,this.maxW, this.maxH);
let minimum = this.calcPosition(0,0, this.minW, this.minH);
// console.log("### MAX " + JSON.stringify(maximum));
// console.log("### MIN " + JSON.stringify(minimum));
const opts = {
preserveAspectRatio: true,
// allowFrom: "." + this.resizableHandleClass.trim().replace(" ", "."),
edges: {
left: false,
right: "." + this.resizableHandleClass.trim().replace(" ", "."),
bottom: "." + this.resizableHandleClass.trim().replace(" ", "."),
top: false
},
ignoreFrom: this.resizeIgnoreFrom,
restrictSize: {
min: {
height: minimum.height,
width: minimum.width
},
max: {
height: maximum.height,
width: maximum.width
}
}
};
this.interactObj.resizable(opts);
if (!this.resizeEventSet) {
this.resizeEventSet = true;
this.interactObj
.on('resizestart resizemove resizeend', function (event) {
self.handleResize(event);
});
}
} else {
this.interactObj.resizable({
enabled: false
});
}
},
autoSize: function() {
// ok here we want to calculate if a resize is needed
this.previousW = this.innerW;
this.previousH = this.innerH;
let newSize=this.$slots.default[0].elm.getBoundingClientRect();
let pos = this.calcWH(newSize.height, newSize.width);
if (pos.w < this.minW) {
pos.w = this.minW;
}
if (pos.w > this.maxW) {
pos.w = this.maxW;
}
if (pos.h < this.minH) {
pos.h = this.minH;
}
if (pos.h > this.maxH) {
pos.h = this.maxH;
}
if (pos.h < 1) {
pos.h = 1;
}
if (pos.w < 1) {
pos.w = 1;
}
// this.lastW = x; // basically, this is copied from resizehandler, but shouldn't be needed
// this.lastH = y;
if (this.innerW !== pos.w || this.innerH !== pos.h) {
this.$emit("resize", this.i, pos.h, pos.w, newSize.height, newSize.width);
}
if (this.previousW !== pos.w || this.previousH !== pos.h) {
this.$emit("resized", this.i, pos.h, pos.w, newSize.height, newSize.width);
this.eventBus.$emit("resizeEvent", "resizeend", this.i, this.innerX, this.innerY, pos.h, pos.w);
}
}
},
}
</script>
<template>
<div ref="item" class="vue-grid-layout" :style="mergedStyle">
<slot></slot>
<grid-item class="vue-grid-placeholder"
v-show="isDragging"
:x="placeholder.x"
:y="placeholder.y"
:w="placeholder.w"
:h="placeholder.h"
:i="placeholder.i"></grid-item>
</div>
</template>
<style>
.vue-grid-layout {
position: relative;
transition: height 200ms ease;
}
</style>
<script>
import Vue from 'vue';
const elementResizeDetectorMaker = require("element-resize-detector");
import {bottom, compact, getLayoutItem, moveElement, validateLayout, cloneLayout, getAllCollisions} from './helpers/utils';
import {getBreakpointFromWidth, getColsFromBreakpoint, findOrGenerateResponsiveLayout} from "./helpers/responsiveUtils";
//var eventBus = require('./eventBus');
import GridItem from './GridItem.vue'
import {addWindowEventListener, removeWindowEventListener} from "./helpers/DOM";
export default {
name: "GridLayout",
provide() {
return {
eventBus: null,
layout: this
}
},
components: {
GridItem,
},
props: {
// If true, the container height swells and contracts to fit contents
autoSize: {
type: Boolean,
default: true
},
colNum: {
type: Number,
default: 12
},
rowHeight: {
type: Number,
default: 150
},
maxRows: {
type: Number,
default: Infinity
},
margin: {
type: Array,
default: function () {
return [10, 10];
}
},
isDraggable: {
type: Boolean,
default: true
},
isResizable: {
type: Boolean,
default: true
},
isMirrored: {
type: Boolean,
default: false
},
useCssTransforms: {
type: Boolean,
default: true
},
verticalCompact: {
type: Boolean,
default: true
},
layout: {
type: Array,
required: true,
},
responsive: {
type: Boolean,
default: false
},
responsiveLayouts: {
type: Object,
default: function() {
return {};
}
},
breakpoints:{
type: Object,
default: function(){return{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
},
cols:{
type: Object,
default: function(){return{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }},
},
preventCollision: {
type: Boolean,
default: false
},
useStyleCursor: {
type: Boolean,
default: true
}
},
data: function () {
return {
width: null,
mergedStyle: {},
lastLayoutLength: 0,
isDragging: false,
placeholder: {
x: 0,
y: 0,
w: 0,
h: 0,
i: -1
},
layouts: {}, // array to store all layouts from different breakpoints
lastBreakpoint: null, // store last active breakpoint
originalLayout: null, // store original Layout
};
},
created () {
const self = this;
// Accessible refernces of functions for removing in beforeDestroy
self.resizeEventHandler = function(eventType, i, x, y, h, w) {
self.resizeEvent(eventType, i, x, y, h, w);
};
self.dragEventHandler = function(eventType, i, x, y, h, w) {
self.dragEvent(eventType, i, x, y, h, w);
};
self._provided.eventBus = new Vue();
self.eventBus = self._provided.eventBus;
self.eventBus.$on('resizeEvent', self.resizeEventHandler);
self.eventBus.$on('dragEvent', self.dragEventHandler);
self.$emit('layout-created', self.layout);
},
beforeDestroy: function(){
//Remove listeners
this.eventBus.$off('resizeEvent', this.resizeEventHandler);
this.eventBus.$off('dragEvent', this.dragEventHandler);
this.eventBus.$destroy();
removeWindowEventListener("resize", this.onWindowResize);
this.erd.uninstall(this.$refs.item);
},
beforeMount: function() {
this.$emit('layout-before-mount', this.layout);
},
mounted: function() {
this.$emit('layout-mounted', this.layout);
this.$nextTick(function () {
validateLayout(this.layout);
this.originalLayout = this.layout;
const self = this;
this.$nextTick(function() {
self.onWindowResize();
self.initResponsiveFeatures();
//self.width = self.$el.offsetWidth;
addWindowEventListener('resize', self.onWindowResize);
compact(self.layout, self.verticalCompact);
self.$emit('layout-updated',self.layout)
self.updateHeight();
self.$nextTick(function () {
this.erd = elementResizeDetectorMaker({
strategy: "scroll", //<- For ultra performance.
// See https://github.com/wnr/element-resize-detector/issues/110 about callOnAdd.
callOnAdd: false,
});
this.erd.listenTo(self.$refs.item, function () {
self.onWindowResize();
});
});
});
});
},
watch: {
width: function (newval, oldval) {
const self = this;
this.$nextTick(function () {
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
if (oldval === null) {
/*
If oldval == null is when the width has never been
set before. That only occurs when mouting is
finished, and onWindowResize has been called and
this.width has been changed the first time after it
got set to null in the constructor. It is now time
to issue layout-ready events as the GridItems have
their sizes configured properly.
The reason for emitting the layout-ready events on
the next tick is to allow for the newly-emitted
updateWidth event (above) to have reached the
children GridItem-s and had their effect, so we're
sure that they have the final size before we emit
layout-ready (for this GridLayout) and
item-layout-ready (for the GridItem-s).
This way any client event handlers can reliably
invistigate stable sizes of GridItem-s.
*/
this.$nextTick(() => {
this.$emit('layout-ready', self.layout);
});
}
this.updateHeight();
});
},
layout: function () {
this.layoutUpdate();
},
colNum: function (val) {
this.eventBus.$emit("setColNum", val);
},
rowHeight: function() {
this.eventBus.$emit("setRowHeight", this.rowHeight);
},
isDraggable: function() {
this.eventBus.$emit("setDraggable", this.isDraggable);
},
isResizable: function() {
this.eventBus.$emit("setResizable", this.isResizable);
},
responsive() {
if (!this.responsive) {
this.$emit('update:layout', this.originalLayout);
this.eventBus.$emit("setColNum", this.colNum);
}
this.onWindowResize();
},
maxRows: function() {
this.eventBus.$emit("setMaxRows", this.maxRows);
},
margin() {
this.updateHeight();
}
},
methods: {
layoutUpdate() {
if (this.layout !== undefined && this.originalLayout !== null) {
if (this.layout.length !== this.originalLayout.length) {
// console.log("### LAYOUT UPDATE!", this.layout.length, this.originalLayout.length);
let diff = this.findDifference(this.layout, this.originalLayout);
if (diff.length > 0){
// console.log(diff);
if (this.layout.length > this.originalLayout.length) {
this.originalLayout = this.originalLayout.concat(diff);
} else {
this.originalLayout = this.originalLayout.filter(obj => {
return !diff.some(obj2 => {
return obj.i === obj2.i;
});
});
}
}
this.lastLayoutLength = this.layout.length;
this.initResponsiveFeatures();
}
compact(this.layout, this.verticalCompact);
this.eventBus.$emit("updateWidth", this.width);
this.updateHeight();
this.$emit('layout-updated',this.layout)
}
},
updateHeight: function () {
this.mergedStyle = {
height: this.containerHeight()
};
},
onWindowResize: function () {
if (this.$refs !== null && this.$refs.item !== null && this.$refs.item !== undefined) {
this.width = this.$refs.item.offsetWidth;
}
this.eventBus.$emit("resizeEvent");
},
containerHeight: function () {
if (!this.autoSize) return;
// console.log("bottom: " + bottom(this.layout))
// console.log("rowHeight + margins: " + (this.rowHeight + this.margin[1]) + this.margin[1])
const containerHeight = bottom(this.layout) * (this.rowHeight + this.margin[1]) + this.margin[1] + 'px';
return containerHeight;
},
dragEvent: function (eventName, id, x, y, h, w) {
//console.log(eventName + " id=" + id + ", x=" + x + ", y=" + y);
let l = getLayoutItem(this.layout, id);
//GetLayoutItem sometimes returns null object
if (l === undefined || l === null){
l = {x:0, y:0}
}
if (eventName === "dragmove" || eventName === "dragstart") {
this.placeholder.i = id;
this.placeholder.x = l.x;
this.placeholder.y = l.y;
this.placeholder.w = w;
this.placeholder.h = h;
this.$nextTick(function() {
this.isDragging = true;
});
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
} else {
this.$nextTick(function() {
this.isDragging = false;
});
}
// Move the element to the dragged location.
this.layout = moveElement(this.layout, l, x, y, true, this.preventCollision);
compact(this.layout, this.verticalCompact);
// needed because vue can't detect changes on array element properties
this.eventBus.$emit("compact");
this.updateHeight();
if (eventName === 'dragend') this.$emit('layout-updated', this.layout);
},
resizeEvent: function (eventName, id, x, y, h, w) {
let l = getLayoutItem(this.layout, id);
//GetLayoutItem sometimes return null object
if (l === undefined || l === null){
l = {h:0, w:0}
}
let hasCollisions;
if (this.preventCollision) {
const collisions = getAllCollisions(this.layout, { ...l, w, h }).filter(
layoutItem => layoutItem.i !== l.i
);
hasCollisions = collisions.length > 0;
// If we're colliding, we need adjust the placeholder.
if (hasCollisions) {
// adjust w && h to maximum allowed space
let leastX = Infinity,
leastY = Infinity;
collisions.forEach(layoutItem => {
if (layoutItem.x > l.x) leastX = Math.min(leastX, layoutItem.x);
if (layoutItem.y > l.y) leastY = Math.min(leastY, layoutItem.y);
});
if (Number.isFinite(leastX)) l.w = leastX - l.x;
if (Number.isFinite(leastY)) l.h = leastY - l.y;
}
}
if (!hasCollisions) {
// Set new width and height.
l.w = w;
l.h = h;
}
if (eventName === "resizestart" || eventName === "resizemove") {
this.placeholder.i = id;
this.placeholder.x = x;
this.placeholder.y = y;
this.placeholder.w = l.w;
this.placeholder.h = l.h;
this.$nextTick(function() {
this.isDragging = true;
});
//this.$broadcast("updateWidth", this.width);
this.eventBus.$emit("updateWidth", this.width);
} else {
this.$nextTick(function() {
this.isDragging = false;
});
}
if (this.responsive) this.responsiveGridLayout();
compact(this.layout, this.verticalCompact);
this.eventBus.$emit("compact");
this.updateHeight();
if (eventName === 'resizeend') this.$emit('layout-updated', this.layout);
},
// finds or generates new layouts for set breakpoints
responsiveGridLayout(){
let newBreakpoint = getBreakpointFromWidth(this.breakpoints, this.width);
let newCols = getColsFromBreakpoint(newBreakpoint, this.cols);
// save actual layout in layouts
if(this.lastBreakpoint != null && !this.layouts[this.lastBreakpoint])
this.layouts[this.lastBreakpoint] = cloneLayout(this.layout);
// Find or generate a new layout.
let layout = findOrGenerateResponsiveLayout(
this.originalLayout,
this.layouts,
this.breakpoints,
newBreakpoint,
this.lastBreakpoint,
newCols,
this.verticalCompact
);
// Store the new layout.
this.layouts[newBreakpoint] = layout;
if (this.lastBreakpoint !== newBreakpoint) {
this.$emit('breakpoint-changed', newBreakpoint, layout);
}
// new prop sync
this.$emit('update:layout', layout);
this.lastBreakpoint = newBreakpoint;
this.eventBus.$emit("setColNum", getColsFromBreakpoint(newBreakpoint, this.cols));
},
// clear all responsive layouts
initResponsiveFeatures(){
// clear layouts
this.layouts = Object.assign({}, this.responsiveLayouts);
},
// find difference in layouts
findDifference(layout, originalLayout){
//Find values that are in result1 but not in result2
let uniqueResultOne = layout.filter(function(obj) {
return !originalLayout.some(function(obj2) {
return obj.i === obj2.i;
});
});
//Find values that are in result2 but not in result1
let uniqueResultTwo = originalLayout.filter(function(obj) {
return !layout.some(function(obj2) {
return obj.i === obj2.i;
});
});
//Combine the two arrays of unique entries#
return uniqueResultOne.concat(uniqueResultTwo);
}
},
}
</script>
let currentDir = 'auto';
// let currentDir = "auto";
function hasDocument() {
return (typeof document !== 'undefined');
}
function hasWindow() {
return (typeof window !== 'undefined');
}
export function getDocumentDir() {
if (!hasDocument()) {
return currentDir;
}
const direction = (typeof document.dir !== 'undefined') ?
document.dir :
document.getElementsByTagName('html')[0].getAttribute('dir');
return direction;
}
export function setDocumentDir(dir) {
// export function setDocumentDir(dir){
if (!hasDocument) {
currentDir = dir;
return;
}
const html = document.getElementsByTagName('html')[0];
html.setAttribute('dir', dir);
}
export function addWindowEventListener(event, callback) {
if (!hasWindow) {
callback();
return;
}
window.addEventListener(event, callback);
}
export function removeWindowEventListener(event, callback) {
if (!hasWindow) {
return;
}
window.removeEventListener(event, callback);
}
// Get {x, y} positions from event.
export function getControlPosition(e) {
return offsetXYFromParentOf(e);
}
// Get from offsetParent
export function offsetXYFromParentOf(evt) {
const offsetParent = evt.target.offsetParent || document.body;
const offsetParentRect = evt.offsetParent === document.body ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();
const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left;
const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top;
/*const x = Math.round(evt.clientX + offsetParent.scrollLeft - offsetParentRect.left);
const y = Math.round(evt.clientY + offsetParent.scrollTop - offsetParentRect.top);*/
return {x, y};
}
// Create an data object exposed by <DraggableCore>'s events
export function createCoreData(lastX, lastY, x, y) {
// State changes are often (but not always!) async. We want the latest value.
const isStart = !isNum(lastX);
if (isStart) {
// If this is our first move, use the x and y as last coords.
return {
deltaX: 0, deltaY: 0,
lastX: x, lastY: y,
x, y
};
} else {
// Otherwise calculate proper values.
return {
deltaX: x - lastX, deltaY: y - lastY,
lastX, lastY,
x, y
};
}
}
function isNum(num) {
return typeof num === 'number' && !isNaN(num);
}
// @flow
import {cloneLayout, compact, correctBounds} from './utils';
// import type {Layout} from './utils';
// export type ResponsiveLayout = {lg, md, sm, xs, xxs};
// type Breakpoint = string;
// type Breakpoints = {lg, md, sm, xs, xxs};
/**
* Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint).
*
* @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...})
* @param {Number} width Screen width.
* @return {String} Highest breakpoint that is less than width.
*/
export function getBreakpointFromWidth(breakpoints, width) {
const sorted = sortBreakpoints(breakpoints);
let matching = sorted[0];
for (let i = 1, len = sorted.length; i < len; i++) {
const breakpointName = sorted[i];
if (width > breakpoints[breakpointName]) { matching = breakpointName; }
}
return matching;
}
/**
* Given a breakpoint, get the # of cols set for it.
* @param {String} breakpoint Breakpoint name.
* @param {Object} cols Map of breakpoints to cols.
* @return {Number} Number of cols.
*/
export function getColsFromBreakpoint(breakpoint, cols) {
if (!cols[breakpoint]) {
throw new Error('ResponsiveGridLayout: `cols` entry for breakpoint ' + breakpoint + ' is missing!');
}
return cols[breakpoint];
}
/**
* Given existing layouts and a new breakpoint, find or generate a new layout.
*
* This finds the layout above the new one and generates from it, if it exists.
*
* @param {Array} orgLayout Original layout.
* @param {Object} layouts Existing layouts.
* @param {Array} breakpoints All breakpoints.
* @param {String} breakpoint New breakpoint.
* @param {String} breakpoint Last breakpoint (for fallback).
* @param {Number} cols Column count at new breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} New layout.
*/
export function findOrGenerateResponsiveLayout(orgLayout, layouts, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
if (layouts[breakpoint]) { return cloneLayout(layouts[breakpoint]); }
// Find or generate the next layout
let layout = orgLayout;
const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
export function generateResponsiveLayout(layout, breakpoints,
breakpoint, lastBreakpoint,
cols, verticalCompact) {
// If it already exists, just return it.
/*if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]);
// Find or generate the next layout
let layout = layouts[lastBreakpoint];*/
/*const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}*/
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols}), verticalCompact);
}
/**
* Given breakpoints, return an array of breakpoints sorted by width. This is usually
* e.g. ['xxs', 'xs', 'sm', ...]
*
* @param {Object} breakpoints Key/value pair of breakpoint names to widths.
* @return {Array} Sorted breakpoints.
*/
export function sortBreakpoints(breakpoints) {
const keys = Object.keys(breakpoints);
return keys.sort(function(a, b) {
return breakpoints[a] - breakpoints[b];
});
}
// tslint:disable
// @flow
// export type LayoutItemRequired = {w, h, x, y, i};
// export type LayoutItem = LayoutItemRequired &
// {minW?, minH?, maxW?, maxH?,
// moved?, static?,
// isDraggable?, isResizable?};
// export type Layout = Array<LayoutItem>;
// export type Position = {left, top, width, height};
/*
export type DragCallbackData = {
node: HTMLElement,
x, y,
deltaX, deltaY,
lastX, lastY
};
*/
// export type DragEvent = {e: Event} & DragCallbackData;
// export type Size = {width, height};
// export type ResizeEvent = {e: Event, node: HTMLElement, size: Size};
// const isProduction = process.env.NODE_ENV === 'production';
/**
* Return the bottom coordinate of the layout.
*
* @param {Array} layout Layout array.
* @return {Number} Bottom coordinate.
*/
export function bottom(layout) {
let max = 0, bottomY;
for (let i = 0, len = layout.length; i < len; i++) {
bottomY = layout[i]. y + layout[i].h;
if (bottomY > max) { max = bottomY; }
}
return max;
}
export function cloneLayout(layout) {
const newLayout = Array(layout.length);
for (let i = 0, len = layout.length; i < len; i++) {
newLayout[i] = cloneLayoutItem(layout[i]);
}
return newLayout;
}
// Fast path to cloning, since this is monomorphic
export function cloneLayoutItem(layoutItem) {
/*return {
w.w, h.h, x.x, y.y, i.i,
minW.minW, maxW.maxW, minH.minH, maxH.maxH,
moved(layoutItem.moved), static(layoutItem.static),
// These can be null
isDraggable.isDraggable, isResizable.isResizable
};*/
return JSON.parse(JSON.stringify(layoutItem));
}
/**
* Given two layoutitems, check if they collide.
*
* @return {Boolean} True if colliding.
*/
export function collides(l1, l2) {
if (l1 === l2) { return false; } // same element
if (l1.x + l1.w <= l2.x) { return false; } // l1 is left of l2
if (l1.x >= l2.x + l2.w) { return false; } // l1 is right of l2
if (l1.y + l1.h <= l2.y) { return false; } // l1 is above l2
if (l1.y >= l2.y + l2.h) { return false; } // l1 is below l2
return true; // boxes overlap
}
/**
* Given a layout, compact it. This involves going down each y coordinate and removing gaps
* between items.
*
* @param {Array} layout Layout.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} Compacted Layout.
*/
export function compact(layout, verticalCompact) {
// Statics go in the compareWith array right away so items flow around them.
const compareWith = getStatics(layout);
// We go through the items by row and column.
const sorted = sortLayoutItemsByRowCol(layout);
// Holding for new items.
const out = Array(layout.length);
for (let i = 0, len = sorted.length; i < len; i++) {
let l = sorted[i];
// Don't move static elements
if (!l.static) {
l = compactItem(compareWith, l, verticalCompact);
// Add to comparison array. We only collide with items before this one.
// Statics are already in this array.
compareWith.push(l);
}
// Add to output array to make sure they still come out in the right order.
out[layout.indexOf(l)] = l;
// Clear moved flag, if it exists.
l.moved = false;
}
return out;
}
/**
* Compact an item in the layout.
*/
export function compactItem(compareWith, l, verticalCompact) {
if (verticalCompact) {
// Move the element up as far as it can go without colliding.
while (l.y > 0 && !getFirstCollision(compareWith, l)) {
l.y--;
}
}
// Move it down, and keep moving it down if it's colliding.
let collides;
while ((collides = getFirstCollision(compareWith, l))) {
l.y = collides.y + collides.h;
}
return l;
}
/**
* Given a layout, make sure all elements fit within its bounds.
*
* @param {Array} layout Layout array.
* @param {Number} bounds Number of columns.
*/
export function correctBounds(layout, bounds) {
const collidesWith = getStatics(layout);
for (let i = 0, len = layout.length; i < len; i++) {
const l = layout[i];
// Overflows right
if (l.x + l.w > bounds.cols) { l.x = bounds.cols - l.w; }
// Overflows left
if (l.x < 0) {
l.x = 0;
l.w = bounds.cols;
}
if (!l.static) { collidesWith.push(l); } else {
// If this is static and collides with other statics, we must move it down.
// We have to do something nicer than just letting them overlap.
while (getFirstCollision(collidesWith, l)) {
l.y++;
}
}
}
return layout;
}
/**
* Get a layout item by ID. Used so we can override later on if necessary.
*
* @param {Array} layout Layout array.
* @param {String} id ID
* @return {LayoutItem} Item at ID.
*/
export function getLayoutItem(layout, id) {
for (let i = 0, len = layout.length; i < len; i++) {
if (layout[i].i === id) { return layout[i]; }
}
}
/**
* Returns the first item this layout collides with.
* It doesn't appear to matter which order we approach this from, although
* perhaps that is the wrong thing to do.
*
* @param {Object} layoutItem Layout item.
* @return {Object|undefined} A colliding layout item, or undefined.
*/
export function getFirstCollision(layout, layoutItem) {
for (let i = 0, len = layout.length; i < len; i++) {
if (collides(layout[i], layoutItem)) { return layout[i]; }
}
}
export function getAllCollisions(layout, layoutItem) {
return layout.filter((l) => collides(l, layoutItem));
}
/**
* Get all static elements.
* @param {Array} layout Array of layout objects.
* @return {Array} Array of static layout items..
*/
export function getStatics(layout) {
// return [];
return layout.filter((l) => l.static);
}
/**
* Move an element. Responsible for doing cascading movements of other elements.
*
* @param {Array} layout Full layout to modify.
* @param {LayoutItem} l element to move.
* @param {Number} [x] X position in grid units.
* @param {Number} [y] Y position in grid units.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is
* being dragged/resized by th euser.
*/
export function moveElement(layout, l, x, y, isUserAction, preventCollision) {
if (l.static) { return layout; }
// Short-circuit if nothing to do.
// if (l.y === y && l.x === x) return layout;
const oldX = l.x;
const oldY = l.y;
const movingUp = y && l.y > y;
// This is quite a bit faster than extending the object
if (typeof x === 'number') { l.x = x; }
if (typeof y === 'number') { l.y = y; }
l.moved = true;
// If this collides with anything, move it.
// When doing this comparison, we have to sort the items we compare with
// to ensure, in the case of multiple collisions, that we're getting the
// nearest collision.
let sorted = sortLayoutItemsByRowCol(layout);
if (movingUp) { sorted = sorted.reverse(); }
const collisions = getAllCollisions(sorted, l);
if (preventCollision && collisions.length) {
l.x = oldX;
l.y = oldY;
l.moved = false;
return layout;
}
// Move each item that collides away from this element.
for (let i = 0, len = collisions.length; i < len; i++) {
const collision = collisions[i];
// console.log('resolving collision between', l.i, 'at', l.y, 'and', collision.i, 'at', collision.y);
// Short circuit so we can't infinite loop
if (collision.moved) { continue; }
// This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
if (l.y > collision.y && l.y - collision.y > collision.h / 4) { continue; }
// Don't move static items - we have to move *this* element away
if (collision.static) {
layout = moveElementAwayFromCollision(layout, collision, l, isUserAction);
} else {
layout = moveElementAwayFromCollision(layout, l, collision, isUserAction);
}
}
return layout;
}
/**
* This is where the magic needs to happen - given a collision, move an element away from the collision.
* We attempt to move it up if there's room, otherwise it goes below.
*
* @param {Array} layout Full layout to modify.
* @param {LayoutItem} collidesWith Layout item we're colliding with.
* @param {LayoutItem} itemToMove Layout item we're moving.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is being dragged/resized
* by the user.
*/
export function moveElementAwayFromCollision(layout, collidesWith,
itemToMove, isUserAction) {
const preventCollision = false; // we're already colliding
// If there is enough space above the collision to put this element, move it there.
// We only do this on the main collision as this can get funky in cascades and cause
// unwanted swapping behavior.
if (isUserAction) {
// Make a mock item so we don't modify the item here, only modify in moveElement.
const fakeItem = {
x: itemToMove.x,
y: itemToMove.y,
w: itemToMove.w,
h: itemToMove.h,
i: '-1'
};
fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0);
if (!getFirstCollision(layout, fakeItem)) {
return moveElement(layout, itemToMove, undefined, fakeItem.y, preventCollision);
}
}
// Previously this was optimized to move below the collision directly, but this can cause problems
// with cascading moves, as an item may actually leapflog a collision and cause a reversal in order.
return moveElement(layout, itemToMove, undefined, itemToMove.y + 1, preventCollision);
}
/**
* Helper to convert a number to a percentage string.
*
* @param {Number} num Any number
* @return {String} That number as a percentage.
*/
export function perc(num) {
return num * 100 + '%';
}
export function setTransform(top, left, width, height) {
// Replace unitless items with px
const translate = 'translate3d(' + left + 'px,' + top + 'px, 0)';
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Just like the setTransform method, but instead it will return a negative value of right.
*
* @param top
* @param right
* @param width
* @param height
* @returns {{transform, WebkitTransform, MozTransform, msTransform, OTransform, width, height, position}}
*/
export function setTransformRtl(top, right, width, height) {
// Replace unitless items with px
const translate = 'translate3d(' + right * -1 + 'px,' + top + 'px, 0)';
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
export function setTopLeft(top, left, width, height) {
return {
top: top + 'px',
left: left + 'px',
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Just like the setTopLeft method, but instead, it will return a right property instead of left.
*
* @param top
* @param right
* @param width
* @param height
* @returns {{top, right, width, height, position}}
*/
export function setTopRight(top, right, width, height) {
return {
top: top + 'px',
right: right + 'px',
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
/**
* Get layout items sorted from top left to right and down.
*
* @return {Array} Array of layout objects.
* @return {Array} Layout, sorted static items first.
*/
export function sortLayoutItemsByRowCol(layout) {
return [].concat(layout).sort(function(a, b) {
if (a.y === b.y && a.x === b.x) {
return 0;
}
if (a.y > b.y || (a.y === b.y && a.x > b.x)) {
return 1;
}
return -1;
});
}
/**
* Generate a layout using the initialLayout and children as a template.
* Missing entries will be added, extraneous ones will be truncated.
*
* @param {Array} initialLayout Layout passed in through props.
* @param {String} breakpoint Current responsive breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout vertically.
* @return {Array} Working layout.
*/
/*
export function synchronizeLayoutWithChildren(initialLayout, children: Array<React.Element>|React.Element,
cols, verticalCompact) {
// ensure 'children' is always an array
if (!Array.isArray(children)) {
children = [children];
}
initialLayout = initialLayout || [];
// Generate one layout item per child.
let layout = [];
for (let i = 0, len = children.length; i < len; i++) {
let newItem;
const child = children[i];
// Don't overwrite if it already exists.
const exists = getLayoutItem(initialLayout, child.key || "1" /!* FIXME satisfies Flow *!/);
if (exists) {
newItem = exists;
} else {
const g = child.props._grid;
// Hey, this item has a _grid property, use it.
if (g) {
if (!isProduction) {
validateLayout([g], 'ReactGridLayout.children');
}
// Validated; add it to the layout. Bottom 'y' possible is the bottom of the layout.
// This allows you to do nice stuff like specify {y: Infinity}
if (verticalCompact) {
newItem = cloneLayoutItem({...g, y: Math.min(bottom(layout), g.y), i: child.key});
} else {
newItem = cloneLayoutItem({...g, y: g.y, i: child.key});
}
}
// Nothing provided: ensure this is added to the bottom
else {
newItem = cloneLayoutItem({w: 1, h: 1, x: 0, y: bottom(layout), i: child.key || "1"});
}
}
layout[i] = newItem;
}
// Correct the layout.
layout = correctBounds(layout, {cols: cols});
layout = compact(layout, verticalCompact);
return layout;
}
*/
/**
* Validate a layout. Throws errors.
*
* @param {Array} layout Array of layout items.
* @param {String} [contextName] Context name for errors.
* @throw {Error} Validation error.
*/
export function validateLayout(layout, contextName) {
contextName = contextName || 'Layout';
const subProps = ['x', 'y', 'w', 'h'];
if (!Array.isArray(layout)) { throw new Error(contextName + ' must be an array!'); }
for (let i = 0, len = layout.length; i < len; i++) {
const item = layout[i];
for (let j = 0; j < subProps.length; j++) {
if (typeof item[subProps[j]] !== 'number') {
throw new Error('VueGridLayout: ' + contextName + '[' + i + '].' + subProps[j] + ' must be a number!');
}
}
if (item.i && typeof item.i !== 'string') {
// number is also ok, so comment the error
// TODO confirm if commenting the line below doesn't cause unexpected problems
// throw new Error('VueGridLayout: ' + contextName + '[' + i + '].i must be a string!');
}
if (item.static !== undefined && typeof item.static !== 'boolean') {
throw new Error('VueGridLayout: ' + contextName + '[' + i + '].static must be a boolean!');
}
}
}
// Flow can't really figure this out, so we just use Object
export function autoBindHandlers(el, fns) {
fns.forEach((key) => el[key] = el[key].bind(el));
}
/**
* Convert a JS object to CSS string. Similar to React's output of CSS.
* @param obj
* @returns {string}
*/
export function createMarkup(obj) {
const keys = Object.keys(obj);
if (!keys.length) { return ''; }
let i, len = keys.length;
let result = '';
for (i = 0; i < len; i++) {
const key = keys[i];
const val = obj[key];
result += hyphenate(key) + ':' + addPx(key, val) + ';';
}
return result;
}
/* The following list is defined in React's core */
export let IS_UNITLESS = {
animationIterationCount: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
flexOrder: true,
gridRow: true,
gridColumn: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related properties
fillOpacity: true,
stopOpacity: true,
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
};
/**
* Will add px to the end of style values which are Numbers.
* @param name
* @param value
* @returns {*}
*/
export function addPx(name, value) {
if (typeof value === 'number' && !IS_UNITLESS[ name ]) {
return value + 'px';
} else {
return value;
}
}
/**
* Hyphenate a camelCase string.
*
* @param {String} str
* @return {String}
*/
export let hyphenateRE = /([a-z\d])([A-Z])/g;
export function hyphenate(str) {
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
export function findItemInArray(array, property, value) {
for (let i = 0; i < array.length; i++) {
if (array[i][property] == value) {
return true;
}
}
return false;
}
export function findAndRemove(array, property, value) {
array.forEach(function(result, index) {
if (result[property] === value) {
// Remove from array
array.splice(index, 1);
}
});
}
'use strict';
import App from '../../framework/app';
import createStore from '../store/index';
import createRouter from './router/index';
import entry from './view/home/index.vue';
export default new App({ entry, createStore, createRouter }).bootstrap();
\ No newline at end of file
import Vue from 'vue';
import VueRouter from 'vue-router';
import Activity from '../view/activity/index.vue';
Vue.use(VueRouter);
export default function createRouter() {
return new VueRouter({
mode: 'history',
routes: [
{
path: '/activity/:pageId',
component: Activity
}
]
});
}
import { Vue, Component, Watch, Provide, Mixins } from 'vue-property-decorator';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import GridLayout from '../../component/VueGridLayout/GridLayout.vue';
import GridItem from '../../component/VueGridLayout/GridItem.vue';
// import LoginForm from '@/lib/Form/index.vue';
import DownloadGuide from '@/lib/DownloadGuide/index.vue';
import { Getter, State, Mutation } from 'vuex-class';
import GuideCube from '@/lib/GuideCube/index.vue';
import GoodsTabs from '@/lib/GoodsTabs/index.vue';
import Coupon from '@/lib/Coupon/index.vue';
import Advertisement from '@/lib/Advertisement/index.vue';
import TransformStyleMixin from '@/page/mixins/transformStyle.mixin';
import { getStyle } from '@/service/utils.service';
@Component({ components: { FreedomContainer, GridLayout, GridItem, DownloadGuide, GoodsTabs, GuideCube, Advertisement, Coupon }, name: 'Activity'})
export default class Activity extends Mixins(TransformStyleMixin) {
@Getter('pageData') pageData;
@State(state => state.editor.pageInfo.pageName) pageName;
@State(state => state.editor.gridLayout.rowHeight) rowHeight;
@Provide('editor');
isLayoutComReady = false;
showBackTop = false;
targetEle: HTMLElement | null = null;
get layout() {
return this.pageData && this.pageData.elements.map(v => v.point) || [];
}
@Watch('pageName', { immediate: true })
onPageNameChange(newVal) {
if (EASY_ENV_IS_BROWSER && newVal) {
document.title = newVal;
// 如果是 iOS 设备,则使用如下 hack 的写法实现页面标题的更新
if (navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
const hackIframe = document.createElement('iframe');
hackIframe.style.display = 'none';
hackIframe.src = '/public/fixIosTitle.html?r=' + Math.random();
document.body.appendChild(hackIframe);
setTimeout(() => {
document.body.removeChild(hackIframe);
}, 300);
}
}
}
mounted() {
this.targetEle = document.querySelector('body');
this.showBackTop = true;
const timer = setTimeout(() => {
this.modfiTabsStyle();
}, 500);
this.$once('hook:beforeDestroy', () => {
clearTimeout(timer);
});
}
fetchApi(options) {
const { store, route } = options;
const { pageId } = route.params;
console.log('fetchApi', route);
return store.dispatch('getPageDate', { pageId });
}
createStyle({h}) {
return EASY_ENV_IS_NODE ? {
height: `${h * this.rowHeight}px`,
} : {};
}
modfiTabsStyle() {
const tabsEle = document.querySelector('.tabs');
if (tabsEle) {
const gridItemEle = tabsEle?.parentNode;
if (gridItemEle?.classList.contains('vue-grid-item')) {
// 处理transform
const transform = getStyle(gridItemEle, 'transform');
const transformY = transform.split('(')[1].split(')')[0].split(',')[5];
gridItemEle.style.transform = 'none';
gridItemEle.style.top = `${transformY}px`;
// 处理backgroundColor
const backgroundColor = getStyle(tabsEle, 'backgroundColor');
const crTabs = tabsEle.childNodes[0];
crTabs.style.backgroundColor = backgroundColor;
const stickyEle = crTabs?.childNodes[0];
if (stickyEle?.classList.contains('cr-sticky') && stickyEle?.childNodes) {
stickyEle.childNodes[0]?.style.backgroundColor = backgroundColor;
}
}
}
}
}
\ No newline at end of file
<template>
<div class="activity" :style="transformStyle(pageData.commonStyle)">
<grid-layout
:layout.sync="layout"
:isDraggable="false"
:isResizable="false"
:col-num="12"
:row-height="rowHeight"
:margin="[0, 0]"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :style="createStyle(item.point)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i">
<component :style="transformStyle(item.commonStyle)" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
<cr-back-top v-if="showBackTop && pageData.props.showBackTop" />
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
html,
body,
#app {
height: 100%;
/deep/ .cr-popup {
.cr-notify {
font-size: 14px;
}
}
}
.activity {
width: 100%;
height: 100%;
min-height: 100%;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
min-height: 667px;
// transform: translateY(-10px);
transition-property: none;
.vue-grid-item {
transition-property: none;
display: flex;
justify-content: center;
align-items: center;
&>*:first-child {
height: 100%;
}
}
}
}
</style>
import { Vue, Component, Emit } from 'vue-property-decorator';
import Layout from 'component/layout/activity/index.vue';
@Component({
components: {
Layout
}
})
export default class Home extends Vue {}
\ No newline at end of file
<template>
<Layout>
<router-view></router-view>
</Layout>
</template>
<script lang="ts" src="./index.ts"></script>
import { Component, Vue } from 'vue-property-decorator';
import { transformStyle } from '@/service/utils.service';
@Component({ name: 'TransformStyleMixin' })
export default class TransformStyleMixin extends Vue {
transformStyle(styleObj, element) {
// console.log('transformStyle', styleObj, element);
const style = {};
if (!styleObj) { return style; }
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(2)}rem`;
} else {
style[key] = styleObj[key]?.includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(2)}rem` : styleObj[key];
}
if (key === 'backgroundImage' && style.backgroundImage) {
// style.backgroundImage = `url(${style.backgroundImage})`;
style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
return style;
}
}
\ No newline at end of file
'use strict';
import Vue from 'vue';
import Vuex from 'vuex';
import RootState from './state';
import Editor from './modules/editor';
Vue.use(Vuex);
export default function createStore(initState: any = {}) {
const { title, url, origin, locale, csrf, editor } = initState;
console.log('store', title, url, origin, locale, csrf, editor);
const state = { title, url, origin, locale, csrf };
return new Vuex.Store<RootState>({
state,
modules: {
editor: new Editor(editor)
}
});
}
\ No newline at end of file
import api from '@/api/editor.api';
import { Module, GetterTree, ActionTree, MutationTree } from 'vuex';
import Vue from 'vue';
import {
SET_PAGE_INFO,
SET_DRAGABLE,
COPY_OR_DELETE_PAGE_INFO,
UPDATE_PAGE_INFO,
ADD_ELEMENTS,
SET_CUR_ELE_INDEX,
SET_CUR_CHILD_INDEX,
RESET_PAGE_DATA,
SET_TEMPLATE_LIST,
SET_PAGE_DATA,
UPDATE_COMMON_STYLE,
UPDATE_PAGE_STYLE,
UPDATE_PAGE_PROPS
} from './type';
import RootState from '../../state';
import EditorState, { PageInfo, defaultState, Page, PageElement } from './state';
export default class EditorModule implements Module<EditorState, RootState> {
state: EditorState;
getters: GetterTree<EditorState, RootState> = {
pageData(state) {
return state.pageInfo.page;
},
pageId(state) {
return state.pageInfo.id;
},
pageInfo(state) {
return state.pageInfo;
}
};
actions: ActionTree<EditorState, RootState> = {
// pageInfo: 处理过的页面数据 - activity, pageData: 未处理的数据 - editor
async savePageData({ commit }, { pageInfo, pageData}) {
if (pageInfo.id) {
await api.updatePage(pageInfo);
commit(SET_PAGE_INFO, { ...pageInfo, page: pageData });
} else {
const res = await api.savePage(pageInfo);
const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: pageData });
}
},
async getPageDate({ commit }, condition) {
const res = await api.getPageById(condition);
const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) });
},
async getTemplateList({ commit }) {
const res = await api.getTemplateList({ type: 'template' });
const handledData = res.map((v, idx) => ({ eleName: `template${idx}`, title: v.pageName, coverImage: v.coverImage, page: v.page }));
// todo 解析数据
commit(SET_TEMPLATE_LIST, handledData);
},
async setTemplateInfo({commit}, condition) {
const res = await api.getPageById(condition);
const { page } = res as PageInfo;
const { pageInfo } = JSON.parse(JSON.stringify(defaultState));
commit(SET_PAGE_INFO, { ...pageInfo, page: JSON.parse(page as string) });
},
setDragable({ commit }, condition) {
commit(SET_DRAGABLE, condition);
},
resetPageData({ commit }, condition) {
const { pageInfo, curEleIndex, curChildIndex } = JSON.parse(JSON.stringify(defaultState));
commit(SET_PAGE_INFO, pageInfo);
commit(SET_CUR_ELE_INDEX, curEleIndex);
commit(SET_CUR_CHILD_INDEX, curChildIndex);
}
};
mutations: MutationTree<EditorState> = {
[SET_PAGE_INFO](state, data) {
state.pageInfo = data;
},
[SET_DRAGABLE](state, data) {
state.gridLayout.draggable = data;
},
[SET_CUR_ELE_INDEX](state, curEleIndex) {
state.curEleIndex = curEleIndex;
},
[SET_CUR_CHILD_INDEX](state, curChildIndex) {
state.curChildIndex = curChildIndex;
},
[SET_TEMPLATE_LIST](state, data) {
state.templateList = data;
},
[SET_PAGE_DATA](state, data) {
state.pageInfo.page = data;
},
[COPY_OR_DELETE_PAGE_INFO](state, { type, containerIndex, childIndex }) {
const page = (state.pageInfo.page as Page).elements;
if (type === 'delete') {
if (childIndex || childIndex === 0) {
page[containerIndex].child.splice(childIndex, 1);
} else {
page.splice(containerIndex, 1);
}
state.curEleIndex = null;
state.curChildIndex = null;
} else if (type === 'copy') {
let eleCopyed = {} as PageElement;
if (childIndex || childIndex === 0) {
eleCopyed = page[containerIndex].child[childIndex];
const { left, top } = eleCopyed.commonStyle;
page[containerIndex].child.push({ ...eleCopyed, commonStyle: { ...eleCopyed.commonStyle, left: left + 10, top: top + 10 } });
} else {
eleCopyed = page[containerIndex];
page.push({ ...eleCopyed, point: { ...eleCopyed.point, i: page.length }});
}
}
},
[UPDATE_PAGE_INFO](state, { containerIndex, childIndex, data }) {
const page = (state.pageInfo.page as Page).elements;
if (childIndex || childIndex === 0) {
page[containerIndex].child.splice(childIndex, 1, data);
} else {
page.splice(containerIndex, 1, data);
}
},
[UPDATE_COMMON_STYLE](state, {containerIndex, childIndex, data}) {
const page = (state.pageInfo.page as Page).elements;
if (childIndex || childIndex === 0) {
Vue.set(page[containerIndex].child[childIndex], 'commonStyle', data);
} else {
Vue.set(page[containerIndex], 'commonStyle', data);
}
},
[UPDATE_PAGE_STYLE](state, { data }) {
(state.pageInfo.page as Page).commonStyle = data;
},
[UPDATE_PAGE_PROPS](state, { data }) {
(state.pageInfo.page as Page).props = data;
},
[ADD_ELEMENTS](state, { containerIndex, data }) {
const page = (state.pageInfo.page as Page).elements;
if (containerIndex || containerIndex === 0) {
page[containerIndex].child.push(data);
} else {
page.push(data);
}
},
};
constructor(initState: EditorState = JSON.parse(JSON.stringify(defaultState))) {
this.state = initState;
}
}
\ No newline at end of file
interface Schame {
key: string;
name: string;
type: string;
}
interface Point {
x: number;
y: number;
w: number;
h: number;
i: number | string;
responsive: boolean; // 自定义属性, 表示是否根据背景图片调整宽高
}
interface CommonStyle {
left: number;
top: number;
}
interface GridLayout {
draggable: boolean;
rowHeight: number;
}
export interface PageElement {
name: string;
title: string;
schame: Schame[];
props: object;
point: Point;
commonStyle: CommonStyle;
child: PageElement[];
}
export interface Page {
commonStyle: CommonStyle;
props: object;
elements: PageElement[];
}
export interface PageInfo {
id?: number;
page?: Page | string;
enable?: number;
author?: string;
coverImage?: string;
isTemplate?: number;
isPublish?: number | boolean;
}
export const defaultState = {
curEleIndex: null,
curChildIndex: null,
pageInfo: {
id: 0,
pageName: '',
pageDescribe: '',
coverImage: 'http://activitystatic.q-gp.com/low_code.jpg',
isPublish: false,
page: {
commonStyle: {
backgroundColor: '#f7f8fa',
backgroundImage: ''
},
props: {
showBackTop: false
},
elements: [],
}
},
templateList: [],
gridLayout: {
draggable: true,
rowHeight: 1,
colNum: 375
},
};
export default interface EditorState {
pageInfo: PageInfo;
draggable: boolean;
curEleIndex: number | null;
curChildIndex: number | null;
templateList: any[];
rowHeight: number;
gridLayout: GridLayout;
}
\ No newline at end of file
'use strict';
export const SET_PAGE_INFO = 'SET_PAGE_INFO';
export const SET_DRAGABLE = 'SET_DRAGABLE';
export const COPY_OR_DELETE_PAGE_INFO = 'COPY_OR_DELETE_PAGE_INFO';
export const UPDATE_PAGE_INFO = 'UPDATE_PAGE_INFO';
export const ADD_ELEMENTS = 'ADD_ELEMENTS';
export const SET_CUR_ELE_INDEX = 'SET_CUR_ELE_INDEX';
export const SET_CUR_CHILD_INDEX = 'SET_CUR_CHILD_INDEX';
export const RESET_PAGE_DATA = 'RESET_PAGE_DATA';
export const SET_TEMPLATE_LIST = 'SET_TEMPLATE_LIST';
export const SET_PAGE_DATA = 'SET_PAGE_DATA';
export const UPDATE_COMMON_STYLE = 'UPDATE_COMMON_STYLE';
export const UPDATE_PAGE_STYLE = 'UPDATE_PAGE_STYLE';
export const UPDATE_PAGE_PROPS = 'UPDATE_PAGE_PROPS';
export default interface RootState {
origin: string;
csrf: string;
}
\ No newline at end of file
// tslint:disable
const cookies = require('js-cookie');
const option = {
domain: EASY_ENV_IS_BROWSER && window.location.host.indexOf('.q-gp.com') >= 0 ? '.q-gp.com' : '',
expires: 365,
};
const Cookies = {
get(key) {
const result = cookies.get(key) || '';
try {
return JSON.parse(result);
} catch (e) {
return result;
}
},
set(key, value) {
return cookies.set(key, value, option);
},
clear() {
return Object.keys(cookies.get()).forEach(function(cookie) {
Cookies.remove(cookie, option);
});
},
remove(key) {
return cookies.remove(key, option);
},
};
module.exports = Cookies;
import Vue from 'vue';
export default new Vue();
\ No newline at end of file
import axios from 'axios';
import basicConfig from '../config';
import localStorage from './localStorage.service';
// import { Notify } from '@qg/cherry-ui';
import Notify from '@qg/cherry-ui/src/notify';
const ERR_MESSAGE_MAP = {
status: {
400: '错误请求',
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求错误,未找到该资源',
405: '请求方法未允许',
408: '请求超时',
500: '服务器端出错',
501: '网络未实现',
502: '网络错误',
503: '服务不可用',
504: '网络超时',
505: 'http版本不支持该请求'
}
};
const CancelToken = axios.CancelToken;
const pending = {};
let reqNum = 0;
axios.defaults.baseURL = basicConfig.apiHost;
axios.defaults.timeout = 30000;
axios.defaults.xsrfHeaderName = 'x-csrf-token';
axios.defaults.xsrfCookieName = 'csrfToken';
const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds));
// 体验优化暂时下掉loading
function beforeRequest() {
reqNum++;
// store.dispatch("setIsLoading", true);
}
function afterRequest() {
clearRequest();
// setTimeout(clearRequest, 300);
}
function clearRequest() {
reqNum--;
if (reqNum <= 0) {
// store.dispatch("setIsLoading", false);
}
}
const instance = axios.create();
// 请求拦截器
instance.interceptors.request.use(
config => {
// op-api识别API使用
config.headers['X-Requested-With'] = 'XMLHttpRequest';
// beforeRequest();
// 发起请求时,取消掉当前正在进行的相同请求
if (pending[config.url as string]) {
pending[config.url as string]('取消重复请求');
}
config.cancelToken = new CancelToken(c => (pending[(config.url + JSON.stringify(config.data)) as string] = c));
// 添加token
const token = localStorage.get('token');
if (token && !config.hideToken) {
config.headers['X-Auth-Token'] = token;
if (config.accessToken) { config.headers['Access-Token'] = token; }
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器即异常处理
instance.interceptors.response.use(
response => {
let notifyType = 'danger';
// afterRequest();
delete pending[response.config.url as string];
const codeArr = ['0000', 2000];
if (codeArr.includes(response.data.code)) { return response.data.data; }
// 后端返回异常信息时提出警告
if (response.data.code && response.data.msg) { notifyType = 'warning'; }
Notify({
type: notifyType,
message: response.data.msg || '后端服务异常',
duration: notifyType === 'warning' ? 6000 : 3000
});
if (response.data.code === '40100') {
localStorage.remove('Token');
}
return Promise.reject(response.data);
},
async err => {
// afterRequest();
// 判断是否取消请求
if (err?.message === '取消重复请求') {
const res = {
status: 409,
data: {}
};
return Promise.resolve(res);
}
// 提示网络错误时尝试重试
if (
err.message === 'Network Error' &&
(!err.config?.retryCount || err.config?.retryCount <= 3)
) {
if (!err.config?.retryCount) { err.config.retryCount = 1; }
err.config.retryCount++;
await wait(3000);
return axios.request(err.config);
}
if (err.response) {
switch (err.response.status) {
case 401:
window.location.href = `${window.location.origin}/editor/login`;
return;
}
}
// 错误提示
let message = '';
if (err.response) {
message = ERR_MESSAGE_MAP.status[err.response.status] || `连接错误${err.response.status}`;
} else {
message = '连接到服务器失败';
}
Notify({ type: 'danger', message });
return Promise.reject(err);
}
);
export default instance;
export default {
get(key) {
const result = EASY_ENV_IS_BROWSER && window.localStorage.getItem(key);
try {
return JSON.parse(result as string);
} catch (e) {
return result;
}
},
set(key, value) {
if (value === undefined) { return; }
const toString = Object.prototype.toString;
if (toString.call(value) === '[object Array]' || toString.call(value) === '[object Object]') {
value = JSON.stringify(value);
}
return EASY_ENV_IS_BROWSER && window.localStorage.setItem(key, value);
},
remove(key) {
return EASY_ENV_IS_BROWSER && window.localStorage.removeItem(key);
},
clear() {
return EASY_ENV_IS_BROWSER && window.localStorage.clear();
}
};
import Vue from 'vue';
// import {
// Button,
// Image,
// Icon,
// Cell,
// CellGroup,
// Row,
// Col,
// Dialog,
// Popup,
// Overlay,
// Divider,
// Loading,
// Picker,
// NavBar,
// Field,
// Checkbox,
// CardList,
// List,
// Form,
// Sticky,
// Tab,
// Tabs,
// Notify,
// Swipe,
// SwipeItem,
// Toast,
// BackTop
// } from '@qg/cherry-ui';
// import { KaLoginForm } from '@qg/citrus-ui';
import Button from '@qg/cherry-ui/src/button';
import Image from '@qg/cherry-ui/src/image';
import Field from '@qg/cherry-ui/src/field';
import Form from '@qg/cherry-ui/src/form';
import Tab from '@qg/cherry-ui/src/tab';
import Tabs from '@qg/cherry-ui/src/tabs';
import BackTop from '@qg/cherry-ui/src/back-top';
import Notify from '@qg/cherry-ui/src/notify';
import Toast from '@qg/cherry-ui/src/toast';
Vue.use(Button);
Vue.use(Image);
// Vue.use(Cell);
// Vue.use(CellGroup);
// Vue.use(Row);
// Vue.use(Col);
// Vue.use(Popup);
// Vue.use(Divider);
// Vue.use(Picker);
Vue.use(Field);
// Vue.use(Dialog);
// Vue.use(Checkbox);
// Vue.use(Icon);
// Vue.use(Sticky);
// Vue.use(Overlay);
Vue.use(Form);
// Vue.use(CardList);
// Vue.use(Loading);
// Vue.use(List);
Vue.use(Tab);
Vue.use(Tabs);
// Vue.use(Toast);
// Vue.use(Swipe);
// Vue.use(SwipeItem);
// Vue.use(Swipe);
Vue.use(BackTop);
// Vue.use(KaLoginForm);
Vue.prototype.$notify = Notify;
Vue.prototype.$toast = Toast;
// tslint:disable
/*!
* qiniu-js-sdk v1.0.19
*
* Copyright 2015 by Qiniu
* Released under GPL V2 License.
*
* GitHub: http://github.com/qiniu/js-sdk
*
* Date: 2017-3-
*/
/* global plupload ,mOxie */
/* global ActiveXObject */
/* exported Qiniu */
/* exported QiniuJsSDK */
/*eslint-disable*/
(function(global) {
/**
* Creates new cookie or removes cookie with negative expiration
* @param key The key or identifier for the store
* @param value Contents of the store
* @param exp Expiration - creation defaults to 30 days
*/
function createCookie(key, value, exp) {
const date = new Date();
date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000);
const expires = '; expires=' + date.toGMTString();
document.cookie = key + '=' + value + expires + '; path=/';
}
/**
* Returns contents of cookie
* @param key The key or identifier for the store
*/
function readCookie(key) {
const nameEQ = key + '=';
const ca = document.cookie.split(';');
for (let i = 0, max = ca.length; i < max; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) === 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
}
// if current browser is not support localStorage
// use cookie to make a polyfill
if (!window.localStorage) {
window.localStorage = {
setItem(key, value) {
createCookie(key, value, 30);
},
getItem(key) {
return readCookie(key);
},
removeItem(key) {
createCookie(key, '', -1);
},
};
}
function QiniuJsSDK() {
const that = this;
/**
* detect IE version
* if current browser is not IE
* it will return false
* else
* it will return version of current IE browser
* @return {Number|Boolean} IE version or false
*/
this.detectIEVersion = function() {
let v = 4;
const div = document.createElement('div');
const all = div.getElementsByTagName('i');
while (((div.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->'), all[0])) {
v++;
}
return v > 4 ? v : false;
};
const logger = {
MUTE: 0,
FATA: 1,
ERROR: 2,
WARN: 3,
INFO: 4,
DEBUG: 5,
TRACE: 6,
level: 0,
};
function log(type, args) {
const header = '[qiniu-js-sdk][' + type + ']';
let msg = header;
for (let i = 0; i < args.length; i++) {
if (typeof args[i] === 'string') {
msg += ' ' + args[i];
} else {
msg += ' ' + that.stringifyJSON(args[i]);
}
}
if (that.detectIEVersion()) {
// http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9
// var log = Function.prototype.bind.call(console.log, console);
// log.apply(console, args);
console.log(msg);
} else {
args.unshift(header);
console.log.apply(console, args);
}
if (document.getElementById('qiniu-js-sdk-log')) {
document.getElementById('qiniu-js-sdk-log').innerHTML += '<p>' + msg + '</p>';
}
}
function makeLogFunc(code) {
const func = code.toLowerCase();
logger[func] = function() {
// logger[func].history = logger[func].history || [];
// logger[func].history.push(arguments);
if (window.console && window.console.log && logger.level >= logger[code]) {
const args = Array.prototype.slice.call(arguments);
log(func, args);
}
};
}
for (const property in logger) {
if (logger.hasOwnProperty(property) && typeof logger[property] === 'number' && !logger.hasOwnProperty(property.toLowerCase())) {
makeLogFunc(property);
}
}
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
/**
* qiniu upload urls
* 'qiniuUploadUrls' is used to change target when current url is not avaliable
* @type {Array}
*/
const qiniuUploadUrls = ['http://upload.qiniu.com', 'http://up.qiniu.com'];
const qiniuUpHosts = {
http: ['http://upload.qiniu.com', 'http://up.qiniu.com'],
https: ['https://up.qbox.me'],
};
let changeUrlTimes = 0;
/**
* reset upload url
* if current page protocal is https
* it will always return 'https://up.qbox.me'
* else
* it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply
*/
this.resetUploadUrl = function() {
const hosts = window.location.protocol === 'https:' ? qiniuUpHosts.https : qiniuUpHosts.http;
const i = changeUrlTimes % hosts.length;
qiniuUploadUrl = hosts[i];
changeUrlTimes++;
logger.debug('resetUploadUrl: ' + qiniuUploadUrl);
};
// this.resetUploadUrl();
/**
* is image
* @param {String} url of a file
* @return {Boolean} file is a image or not
*/
this.isImage = function(url) {
url = url.split(/[?#]/)[0];
return /\.(png|jpg|jpeg|gif|bmp)$/i.test(url);
};
/**
* get file extension
* @param {String} filename
* @return {String} file extension
* @example
* input: test.txt
* output: txt
*/
this.getFileExtension = function(filename) {
const tempArr = filename.split('.');
let ext;
if (tempArr.length === 1 || (tempArr[0] === '' && tempArr.length === 2)) {
ext = '';
} else {
ext = tempArr.pop().toLowerCase(); // get the extension and make it lower-case
}
return ext;
};
/**
* encode string by utf8
* @param {String} string to encode
* @return {String} encoded string
*/
this.utf8_encode = function(argString) {
// http://kevin.vanzonneveld.net
// + original by: Webtoolkit.info (http://www.webtoolkit.info/)
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + improved by: sowberry
// + tweaked by: Jack
// + bugfixed by: Onno Marsman
// + improved by: Yves Sucaet
// + bugfixed by: Onno Marsman
// + bugfixed by: Ulrich
// + bugfixed by: Rafal Kukawski
// + improved by: kirilloid
// + bugfixed by: kirilloid
// * example 1: this.utf8_encode('Kevin van Zonneveld');
// * returns 1: 'Kevin van Zonneveld'
if (argString === null || typeof argString === 'undefined') {
return '';
}
const string = argString + ''; // .replace(/\r\n/g, '\n').replace(/\r/g, '\n');
let utftext = '';
let start;
let end;
let stringl = 0;
start = end = 0;
stringl = string.length;
for (let n = 0; n < stringl; n++) {
let c1 = string.charCodeAt(n);
let enc = null;
if (c1 < 128) {
end++;
} else if (c1 > 127 && c1 < 2048) {
enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128);
} else if ((c1 & 0xf800) ^ (0xd800 > 0)) {
enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128);
} else {
// surrogate pairs
if ((c1 & 0xfc00) ^ (0xd800 > 0)) {
throw new RangeError('Unmatched trail surrogate at ' + n);
}
const c2 = string.charCodeAt(++n);
if ((c2 & 0xfc00) ^ (0xdc00 > 0)) {
throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
}
c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000;
enc = String.fromCharCode((c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128);
}
if (enc !== null) {
if (end > start) {
utftext += string.slice(start, end);
}
utftext += enc;
start = end = n + 1;
}
}
if (end > start) {
utftext += string.slice(start, stringl);
}
return utftext;
};
this.base64_decode = function(data) {
// http://kevin.vanzonneveld.net
// + original by: Tyler Akins (http://rumkin.com)
// + improved by: Thunder.m
// + input by: Aman Gupta
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Onno Marsman
// + bugfixed by: Pellentesque Malesuada
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + input by: Brett Zamir (http://brett-zamir.me)
// + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
// * returns 1: 'Kevin van Zonneveld'
// mozilla has this native
// - but breaks in 2.0.0.12!
// if (typeof this.window['atob'] == 'function') {
// return atob(data);
// }
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let o1;
let o2;
let o3;
let h1;
let h2;
let h3;
let h4;
let bits;
let i = 0;
let ac = 0;
let dec = '';
const tmp_arr = [];
if (!data) {
return data;
}
data += '';
do {
// unpack four hexets into three octets using index points in b64
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4;
o1 = (bits >> 16) & 0xff;
o2 = (bits >> 8) & 0xff;
o3 = bits & 0xff;
if (h3 === 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 === 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
dec = tmp_arr.join('');
return dec;
};
/**
* encode data by base64
* @param {String} data to encode
* @return {String} encoded data
*/
this.base64_encode = function(data) {
// http://kevin.vanzonneveld.net
// + original by: Tyler Akins (http://rumkin.com)
// + improved by: Bayron Guevara
// + improved by: Thunder.m
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Pellentesque Malesuada
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// - depends on: this.utf8_encode
// * example 1: this.base64_encode('Kevin van Zonneveld');
// * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
// mozilla has this native
// - but breaks in 2.0.0.12!
// if (typeof this.window['atob'] == 'function') {
// return atob(data);
// }
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let o1;
let o2;
let o3;
let h1;
let h2;
let h3;
let h4;
let bits;
let i = 0;
let ac = 0;
let enc = '';
const tmp_arr = [];
if (!data) {
return data;
}
data = this.utf8_encode(data + '');
do {
// pack three octets into four hexets
o1 = data.charCodeAt(i++);
o2 = data.charCodeAt(i++);
o3 = data.charCodeAt(i++);
bits = (o1 << 16) | (o2 << 8) | o3;
h1 = (bits >> 18) & 0x3f;
h2 = (bits >> 12) & 0x3f;
h3 = (bits >> 6) & 0x3f;
h4 = bits & 0x3f;
// use hexets to index into b64, and append result to encoded string
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
switch (data.length % 3) {
case 1:
enc = enc.slice(0, -2) + '==';
break;
case 2:
enc = enc.slice(0, -1) + '=';
break;
}
return enc;
};
/**
* encode string in url by base64
* @param {String} string in url
* @return {String} encoded string
*/
this.URLSafeBase64Encode = function(v) {
v = this.base64_encode(v);
return v.replace(/\//g, '_').replace(/\+/g, '-');
};
this.URLSafeBase64Decode = function(v) {
v = v.replace(/_/g, '/').replace(/-/g, '+');
return this.base64_decode(v);
};
// TODO: use mOxie
/**
* craete object used to AJAX
* @return {Object}
*/
this.createAjax = function(argument) {
let xmlhttp = {};
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
}
return xmlhttp;
};
// TODO: enhance IE compatibility
/**
* parse json string to javascript object
* @param {String} json string
* @return {Object} object
*/
this.parseJSON = function(data) {
// Attempt to parse using the native JSON parser first
if (window.JSON && window.JSON.parse) {
return window.JSON.parse(data);
}
// var rx_one = /^[\],:{}\s]*$/,
// rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
// rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
// rx_four = /(?:^|:|,)(?:\s*\[)+/g,
const rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
// var json;
let text = String(data);
rx_dangerous.lastIndex = 0;
if (rx_dangerous.test(text)) {
text = text.replace(rx_dangerous, function(a) {
return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// todo 使用一下判断,增加安全性
// if (
// rx_one.test(
// text
// .replace(rx_two, '@')
// .replace(rx_three, ']')
// .replace(rx_four, '')
// )
// ) {
// return eval('(' + text + ')');
// }
return eval('(' + text + ')');
};
/**
* parse javascript object to json string
* @param {Object} object
* @return {String} json string
*/
this.stringifyJSON = function(obj) {
// Attempt to parse using the native JSON parser first
if (window.JSON && window.JSON.stringify) {
return window.JSON.stringify(obj);
}
switch (typeof obj) {
case 'string':
return '"' + obj.replace(/(["\\])/g, '\\$1') + '"';
case 'array':
return '[' + obj.map(that.stringifyJSON).join(',') + ']';
case 'object':
if (obj instanceof Array) {
const strArr = [];
const len = obj.length;
for (let i = 0; i < len; i++) {
strArr.push(that.stringifyJSON(obj[i]));
}
return '[' + strArr.join(',') + ']';
} else if (obj === null) {
return 'null';
} else {
const string = [];
for (const property in obj) {
if (obj.hasOwnProperty(property)) {
string.push(that.stringifyJSON(property) + ':' + that.stringifyJSON(obj[property]));
}
}
return '{' + string.join(',') + '}';
}
break;
case 'number':
return obj;
case false:
return obj;
case 'boolean':
return obj;
}
};
/**
* trim space beside text
* @param {String} untrimed string
* @return {String} trimed string
*/
this.trim = function(text) {
return text === null ? '' : text.replace(/^\s+|\s+$/g, '');
};
/**
* create a uploader by QiniuJsSDK
* @param {object} options to create a new uploader
* @return {object} uploader
*/
this.uploader = function(op) {
/** ******** inner function define start **********/
// according the different condition to reset chunk size
// and the upload strategy according with the chunk size
// when chunk size is zero will cause to direct upload
// see the statement binded on 'BeforeUpload' event
const reset_chunk_size = function() {
const ie = that.detectIEVersion();
let BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size;
// case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true
const isSpecialSafari =
(mOxie.Env.browser === 'Safari' && mOxie.Env.version <= 5 && mOxie.Env.os === 'Windows' && mOxie.Env.osVersion === '7') ||
(mOxie.Env.browser === 'Safari' && mOxie.Env.os === 'iOS' && mOxie.Env.osVersion === '7');
// case IE 9-,chunk_size is not empty and flash is included in runtimes
// set op.chunk_size to zero
// if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) {
if (ie && ie < 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) {
// link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not
// when plupload chunk_size setting is't null ,it cause bug in ie8/9 which runs flash runtimes (not support html5) .
op.chunk_size = 0;
} else if (isSpecialSafari) {
// win7 safari / iOS7 safari have bug when in chunk upload mode
// reset chunk_size to 0
// disable chunk in special version safari
op.chunk_size = 0;
} else {
BLOCK_BITS = 20;
MAX_CHUNK_SIZE = 4 << BLOCK_BITS; // 4M
chunk_size = plupload.parseSize(op.chunk_size);
if (chunk_size > MAX_CHUNK_SIZE) {
op.chunk_size = MAX_CHUNK_SIZE;
}
// qiniu service max_chunk_size is 4m
// reset chunk_size to max_chunk_size(4m) when chunk_size > 4m
}
// if op.chunk_size set 0 will be cause to direct upload
};
const getHosts = function(hosts) {
const result = [];
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
if (host.indexOf('-H') === 0) {
result.push(host.split(' ')[2]);
} else {
result.push(host);
}
}
return result;
};
const getPutPolicy = function(uptoken) {
const segments = uptoken.split(':');
const ak = segments[0];
const putPolicy = that.parseJSON(that.URLSafeBase64Decode(segments[2]));
putPolicy.ak = ak;
if (putPolicy.scope.indexOf(':') >= 0) {
putPolicy.bucket = putPolicy.scope.split(':')[0];
putPolicy.key = putPolicy.scope.split(':')[1];
} else {
putPolicy.bucket = putPolicy.scope;
}
return putPolicy;
};
const getUpHosts = function(uptoken) {
const putPolicy = getPutPolicy(uptoken);
// var uphosts_url = "//uc.qbox.me/v1/query?ak="+ak+"&bucket="+putPolicy.scope;
// IE 9- is not support protocal relative url
const uphosts_url = window.location.protocol + '//uc.qbox.me/v1/query?ak=' + putPolicy.ak + '&bucket=' + putPolicy.bucket;
logger.debug('putPolicy: ', putPolicy);
logger.debug('get uphosts from: ', uphosts_url);
const ie = that.detectIEVersion();
let ajax;
if (ie && ie <= 9) {
ajax = new mOxie.XMLHttpRequest();
mOxie.Env.swf_url = op.flash_swf_url;
} else {
ajax = that.createAjax();
}
ajax.open('GET', uphosts_url, false);
const onreadystatechange = function() {
logger.debug('ajax.readyState: ', ajax.readyState);
if (ajax.readyState === 4) {
logger.debug('ajax.status: ', ajax.status);
if (ajax.status < 400) {
const res = that.parseJSON(ajax.responseText);
qiniuUpHosts.http = getHosts(res.http.up);
qiniuUpHosts.https = getHosts(res.https.up);
logger.debug('get new uphosts: ', qiniuUpHosts);
that.resetUploadUrl();
} else {
logger.error('get uphosts error: ', ajax.responseText);
}
}
};
if (ie && ie <= 9) {
ajax.bind('readystatechange', onreadystatechange);
} else {
ajax.onreadystatechange = onreadystatechange;
}
ajax.send();
// ajax.send();
// if (ajax.status < 400) {
// var res = that.parseJSON(ajax.responseText);
// qiniuUpHosts.http = getHosts(res.http.up);
// qiniuUpHosts.https = getHosts(res.https.up);
// logger.debug("get new uphosts: ", qiniuUpHosts);
// that.resetUploadUrl();
// } else {
// logger.error("get uphosts error: ", ajax.responseText);
// }
};
const getUptoken = function(file) {
if (!that.token || (op.uptoken_url && that.tokenInfo.isExpired())) {
return getNewUpToken(file);
} else {
return that.token;
}
};
// getNewUptoken maybe called at Init Event or BeforeUpload Event
// case Init Event, the file param of getUptken will be set a null value
// if op.uptoken has value, set uptoken with op.uptoken
// else if op.uptoken_url has value, set uptoken from op.uptoken_url
// else if op.uptoken_func has value, set uptoken by result of op.uptoken_func
const getNewUpToken = function(file) {
if (op.uptoken) {
that.token = op.uptoken;
} else if (op.uptoken_url) {
logger.debug('get uptoken from: ', that.uptoken_url);
// TODO: use mOxie
const ajax = that.createAjax();
ajax.open('GET', that.uptoken_url, false);
ajax.setRequestHeader('If-Modified-Since', '0');
// ajax.onreadystatechange = function() {
// if (ajax.readyState === 4 && ajax.status === 200) {
// var res = that.parseJSON(ajax.responseText);
// that.token = res.uptoken;
// }
// };
ajax.send();
if (ajax.status === 200) {
const res = that.parseJSON(ajax.responseText);
that.token = res.uptoken;
const segments = that.token.split(':');
const putPolicy = that.parseJSON(that.URLSafeBase64Decode(segments[2]));
if (!that.tokenMap) {
that.tokenMap = {};
}
const getTimestamp = function(time) {
return Math.ceil(time.getTime() / 1000);
};
const serverTime = getTimestamp(new Date(ajax.getResponseHeader('date')));
const clientTime = getTimestamp(new Date());
that.tokenInfo = {
serverDelay: clientTime - serverTime,
deadline: putPolicy.deadline,
isExpired() {
const leftTime = this.deadline - getTimestamp(new Date()) + this.serverDelay;
return leftTime < 600;
},
};
logger.debug('get new uptoken: ', that.token);
logger.debug('get token info: ', that.tokenInfo);
} else {
logger.error('get uptoken error: ', ajax.responseText);
}
} else if (op.uptoken_func) {
logger.debug('get uptoken from uptoken_func');
that.token = op.uptoken_func(file);
logger.debug('get new uptoken: ', that.token);
} else {
logger.error('one of [uptoken, uptoken_url, uptoken_func] settings in options is required!');
}
if (that.token) {
getUpHosts(that.token);
}
return that.token;
};
// get file key according with the user passed options
const getFileKey = function(up, file, func) {
// WARNING
// When you set the key in putPolicy by "scope": "bucket:key"
// You should understand the risk of override a file in the bucket
// So the code below that automatically get key from uptoken has been commented
// var putPolicy = getPutPolicy(that.token)
// if (putPolicy.key) {
// logger.debug("key is defined in putPolicy.scope: ", putPolicy.key)
// return putPolicy.key
// }
let key = '';
let unique_names = false;
if (!op.save_key) {
unique_names = up.getOption && up.getOption('unique_names');
unique_names = unique_names || (up.settings && up.settings.unique_names);
if (unique_names) {
const ext = that.getFileExtension(file.name);
key = ext ? file.id + '.' + ext : file.id;
} else if (typeof func === 'function') {
key = func(up, file);
} else {
key = file.name;
}
}
return key;
};
/** ******** inner function define end **********/
if (op.log_level) {
logger.level = op.log_level;
}
if (!op.domain) {
throw new Error('domain setting in options is required!');
}
if (!op.browse_button) {
throw new Error('browse_button setting in options is required!');
}
if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) {
throw new Error('one of [uptoken, uptoken_url, uptoken_func] settings in options is required!');
}
logger.debug('init uploader start');
logger.debug('environment: ', mOxie.Env);
logger.debug('userAgent: ', navigator.userAgent);
const option = {};
// hold the handler from user passed options
const _Error_Handler = op.init && op.init.Error;
const _FileUploaded_Handler = op.init && op.init.FileUploaded;
// replace the handler for intercept
op.init.Error = function() {};
op.init.FileUploaded = function() {};
that.uptoken_url = op.uptoken_url;
that.token = '';
that.key_handler = typeof op.init.Key === 'function' ? op.init.Key : '';
this.domain = op.domain;
// TODO: ctx is global in scope of a uploader instance
// this maybe cause error
let ctx = '';
const speedCalInfo = {
isResumeUpload: false,
resumeFilesize: 0,
startTime: '',
currentTime: '',
};
reset_chunk_size();
logger.debug('invoke reset_chunk_size()');
logger.debug('op.chunk_size: ', op.chunk_size);
const defaultSetting = {
url: qiniuUploadUrl,
multipart_params: {
token: '',
},
};
const ie = that.detectIEVersion();
// case IE 9-
// add accept in multipart params
if (ie && ie <= 9) {
defaultSetting.multipart_params.accept = 'text/plain; charset=utf-8';
logger.debug('add accept text/plain in multipart params');
}
// compose options with user passed options and default setting
plupload.extend(option, op, defaultSetting);
logger.debug('option: ', option);
// create a new uploader with composed options
const uploader = new plupload.Uploader(option);
logger.debug('new plupload.Uploader(option)');
// bind getNewUpToken to 'Init' event
uploader.bind('Init', function(up, params) {
logger.debug('Init event activated');
// if op.get_new_uptoken is not true
// invoke getNewUptoken when uploader init
// else
// getNewUptoken everytime before a new file upload
if (!op.get_new_uptoken) {
getNewUpToken(null);
}
// getNewUpToken(null);
});
logger.debug('bind Init event');
// bind 'FilesAdded' event
// when file be added and auto_start has set value
// uploader will auto start upload the file
uploader.bind('FilesAdded', function(up, files) {
logger.debug('FilesAdded event activated');
let auto_start = up.getOption && up.getOption('auto_start');
auto_start = auto_start || (up.settings && up.settings.auto_start);
logger.debug('auto_start: ', auto_start);
logger.debug('files: ', files);
// detect is iOS
const is_ios = function() {
if (mOxie.Env.OS.toLowerCase() === 'ios') {
return true;
} else {
return false;
}
};
// if current env os is iOS change file name to [time].[ext]
if (is_ios()) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const ext = that.getFileExtension(file.name);
file.name = file.id + '.' + ext;
}
}
if (auto_start) {
setTimeout(function() {
up.start();
logger.debug('invoke up.start()');
}, 0);
// up.start();
// plupload.each(files, function(i, file) {
// up.start();
// logger.debug("invoke up.start()")
// logger.debug("file: ", file);
// });
}
up.refresh(); // Reposition Flash/Silverlight
});
logger.debug('bind FilesAdded event');
// bind 'BeforeUpload' event
// intercept the process of upload
// - prepare uptoken
// - according the chunk size to make differnt upload strategy
// - resume upload with the last breakpoint of file
uploader.bind('BeforeUpload', function(up, file) {
logger.debug('BeforeUpload event activated');
// add a key named speed for file object
file.speed = file.speed || 0;
ctx = '';
if (op.get_new_uptoken) {
getNewUpToken(file);
}
const directUpload = function(up, file, func) {
speedCalInfo.startTime = new Date().getTime();
let multipart_params_obj;
if (op.save_key) {
multipart_params_obj = {
token: that.token,
};
} else {
multipart_params_obj = {
key: getFileKey(up, file, func),
token: that.token,
};
}
const ie = that.detectIEVersion();
// case IE 9-
// add accept in multipart params
if (ie && ie <= 9) {
multipart_params_obj.accept = 'text/plain; charset=utf-8';
logger.debug('add accept text/plain in multipart params');
}
logger.debug('directUpload multipart_params_obj: ', multipart_params_obj);
const x_vars = op.x_vars;
if (x_vars !== undefined && typeof x_vars === 'object') {
for (const x_key in x_vars) {
if (x_vars.hasOwnProperty(x_key)) {
if (typeof x_vars[x_key] === 'function') {
multipart_params_obj['x:' + x_key] = x_vars[x_key](up, file);
} else if (typeof x_vars[x_key] !== 'object') {
multipart_params_obj['x:' + x_key] = x_vars[x_key];
}
}
}
}
up.setOption({
url: qiniuUploadUrl,
multipart: true,
chunk_size: is_android_weixin_or_qq() ? op.max_file_size : undefined,
multipart_params: multipart_params_obj,
});
};
// detect is weixin or qq inner browser
const is_android_weixin_or_qq = function() {
const ua = navigator.userAgent.toLowerCase();
if ((ua.match(/MicroMessenger/i) || mOxie.Env.browser === 'QQBrowser' || ua.match(/V1_AND_SQ/i)) && mOxie.Env.OS.toLowerCase() === 'android') {
return true;
} else {
return false;
}
};
let chunk_size = up.getOption && up.getOption('chunk_size');
chunk_size = chunk_size || (up.settings && up.settings.chunk_size);
logger.debug('uploader.runtime: ', uploader.runtime);
logger.debug('chunk_size: ', chunk_size);
// TODO: flash support chunk upload
if ((uploader.runtime === 'html5' || uploader.runtime === 'flash') && chunk_size) {
if (file.size < chunk_size || is_android_weixin_or_qq()) {
logger.debug('directUpload because file.size < chunk_size || is_android_weixin_or_qq()');
// direct upload if file size is less then the chunk size
directUpload(up, file, that.key_handler);
} else {
// TODO: need a polifill to make it work in IE 9-
// ISSUE: if file.name is existed in localStorage
// but not the same file maybe cause error
let localFileInfo = localStorage.getItem(file.name);
let blockSize = chunk_size;
if (localFileInfo) {
// TODO: although only the html5 runtime will enter this statement
// but need uniform way to make convertion between string and json
localFileInfo = that.parseJSON(localFileInfo);
const now = new Date().getTime();
const before = localFileInfo.time || 0;
const aDay = 24 * 60 * 60 * 1000; // milliseconds of one day
// if the last upload time is within one day
// will upload continuously follow the last breakpoint
// else
// will reupload entire file
if (now - before < aDay) {
if (localFileInfo.percent !== 100) {
if (file.size === localFileInfo.total) {
// TODO: if file.name and file.size is the same
// but not the same file will cause error
file.percent = localFileInfo.percent;
file.loaded = localFileInfo.offset;
ctx = localFileInfo.ctx;
// set speed info
speedCalInfo.isResumeUpload = true;
speedCalInfo.resumeFilesize = localFileInfo.offset;
// set block size
if (localFileInfo.offset + blockSize > file.size) {
blockSize = file.size - localFileInfo.offset;
}
} else {
// remove file info when file.size is conflict with file info
localStorage.removeItem(file.name);
}
} else {
// remove file info when upload percent is 100%
// avoid 499 bug
localStorage.removeItem(file.name);
}
} else {
// remove file info when last upload time is over one day
localStorage.removeItem(file.name);
}
}
speedCalInfo.startTime = new Date().getTime();
const multipart_params_obj = {};
const ie = that.detectIEVersion();
// case IE 9-
// add accept in multipart params
if (ie && ie <= 9) {
multipart_params_obj.accept = 'text/plain; charset=utf-8';
logger.debug('add accept text/plain in multipart params');
}
// TODO: to support bput
// http://developer.qiniu.com/docs/v6/api/reference/up/bput.html
up.setOption({
url: qiniuUploadUrl + '/mkblk/' + blockSize,
multipart: false,
chunk_size,
required_features: 'chunks',
headers: {
Authorization: 'UpToken ' + getUptoken(file),
},
multipart_params: multipart_params_obj,
});
}
} else {
logger.debug('directUpload because uploader.runtime !== \'html5\' || uploader.runtime !== \'flash\' || !chunk_size');
// direct upload if runtime is not html5
directUpload(up, file, that.key_handler);
}
});
logger.debug('bind BeforeUpload event');
// bind 'UploadProgress' event
// calculate upload speed
uploader.bind('UploadProgress', function(up, file) {
logger.trace('UploadProgress event activated');
speedCalInfo.currentTime = new Date().getTime();
const timeUsed = speedCalInfo.currentTime - speedCalInfo.startTime; // ms
let fileUploaded = file.loaded || 0;
if (speedCalInfo.isResumeUpload) {
fileUploaded = file.loaded - speedCalInfo.resumeFilesize;
}
file.speed = ((fileUploaded / timeUsed) * 1000).toFixed(0) || 0; // unit: byte/s
});
logger.debug('bind UploadProgress event');
// bind 'ChunkUploaded' event
// store the chunk upload info and set next chunk upload url
uploader.bind('ChunkUploaded', function(up, file, info) {
logger.debug('ChunkUploaded event activated');
logger.debug('file: ', file);
logger.debug('info: ', info);
const res = that.parseJSON(info.response);
logger.debug('res: ', res);
// ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...'
ctx = ctx ? ctx + ',' + res.ctx : res.ctx;
const leftSize = info.total - info.offset;
let chunk_size = up.getOption && up.getOption('chunk_size');
chunk_size = chunk_size || (up.settings && up.settings.chunk_size);
if (leftSize < chunk_size) {
up.setOption({
url: qiniuUploadUrl + '/mkblk/' + leftSize,
});
logger.debug('up.setOption url: ', qiniuUploadUrl + '/mkblk/' + leftSize);
}
up.setOption({
headers: {
Authorization: 'UpToken ' + getUptoken(file),
},
});
localStorage.setItem(
file.name,
that.stringifyJSON({
ctx,
percent: file.percent,
total: info.total,
offset: info.offset,
time: new Date().getTime(),
})
);
});
logger.debug('bind ChunkUploaded event');
let retries = qiniuUploadUrls.length;
// if error is unkown switch upload url and retry
const unknow_error_retry = function(file) {
if (retries-- > 0) {
setTimeout(function() {
that.resetUploadUrl();
file.status = plupload.QUEUED;
uploader.stop();
uploader.start();
}, 0);
return true;
} else {
retries = qiniuUploadUrls.length;
return false;
}
};
// bind 'Error' event
// check the err.code and return the errTip
uploader.bind(
'Error',
(function(_Error_Handler) {
return function(up, err) {
logger.error('Error event activated');
logger.error('err: ', err);
let errTip = '';
const file = err.file;
if (file) {
switch (err.code) {
case plupload.FAILED:
errTip = '上传失败。请稍后再试。';
break;
case plupload.FILE_SIZE_ERROR:
let max_file_size = up.getOption && up.getOption('max_file_size');
max_file_size = max_file_size || (up.settings && up.settings.max_file_size);
errTip = '浏览器最大可上传' + max_file_size + '。更大文件请使用命令行工具。';
break;
case plupload.FILE_EXTENSION_ERROR:
errTip = '文件验证失败。请稍后重试。';
break;
case plupload.HTTP_ERROR:
if (err.response === '') {
// Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE
errTip = err.message || '未知网络错误。';
if (!unknow_error_retry(file)) {
return;
}
break;
}
let errorObj = that.parseJSON(err.response);
let errorText = errorObj.error;
switch (err.status) {
case 400:
errTip = '请求报文格式错误。';
break;
case 401:
errTip = '客户端认证授权失败。请重试或提交反馈。';
break;
case 405:
errTip = '客户端请求错误。请重试或提交反馈。';
break;
case 579:
errTip = '资源上传成功,但回调失败。';
break;
case 599:
errTip = '网络连接异常。请重试或提交反馈。';
if (!unknow_error_retry(file)) {
return;
}
break;
case 614:
errTip = '文件已存在。';
try {
errorObj = that.parseJSON(errorObj.error);
errorText = errorObj.error || 'file exists';
} catch (e) {
errorText = errorObj.error || 'file exists';
}
break;
case 631:
errTip = '指定空间不存在。';
break;
case 701:
errTip = '上传数据块校验出错。请重试或提交反馈。';
break;
default:
errTip = '未知错误。';
if (!unknow_error_retry(file)) {
return;
}
break;
}
errTip = errTip + '(' + err.status + '' + errorText + ')';
break;
case plupload.SECURITY_ERROR:
errTip = '安全配置错误。请联系网站管理员。';
break;
case plupload.GENERIC_ERROR:
errTip = '上传失败。请稍后再试。';
break;
case plupload.IO_ERROR:
errTip = '上传失败。请稍后再试。';
break;
case plupload.INIT_ERROR:
errTip = '网站配置错误。请联系网站管理员。';
uploader.destroy();
break;
default:
errTip = err.message + err.details;
if (!unknow_error_retry(file)) {
return;
}
break;
}
if (_Error_Handler) {
_Error_Handler(up, err, errTip);
}
}
up.refresh(); // Reposition Flash/Silverlight
};
})(_Error_Handler)
);
logger.debug('bind Error event');
// bind 'FileUploaded' event
// intercept the complete of upload
// - get downtoken from downtoken_url if bucket is private
// - invoke mkfile api to compose chunks if upload strategy is chunk upload
uploader.bind(
'FileUploaded',
(function(_FileUploaded_Handler) {
return function(up, file, info) {
logger.debug('FileUploaded event activated');
logger.debug('file: ', file);
logger.debug('info: ', info);
const last_step = function(up, file, info) {
if (op.downtoken_url) {
// if op.dowontoken_url is not empty
// need get downtoken before invoke the _FileUploaded_Handler
const ajax_downtoken = that.createAjax();
ajax_downtoken.open('POST', op.downtoken_url, true);
ajax_downtoken.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
ajax_downtoken.onreadystatechange = function() {
if (ajax_downtoken.readyState === 4) {
if (ajax_downtoken.status === 200) {
let res_downtoken;
try {
res_downtoken = that.parseJSON(ajax_downtoken.responseText);
} catch (e) {
throw new Error('invalid json format');
}
const info_extended = {};
plupload.extend(info_extended, that.parseJSON(info), res_downtoken);
if (_FileUploaded_Handler) {
_FileUploaded_Handler(up, file, that.stringifyJSON(info_extended));
}
} else {
uploader.trigger('Error', {
status: ajax_downtoken.status,
response: ajax_downtoken.responseText,
file,
code: plupload.HTTP_ERROR,
});
}
}
};
ajax_downtoken.send('key=' + that.parseJSON(info).key + '&domain=' + op.domain);
} else if (_FileUploaded_Handler) {
_FileUploaded_Handler(up, file, info);
}
};
const res = that.parseJSON(info.response);
ctx = ctx || res.ctx;
// if ctx is not empty
// that means the upload strategy is chunk upload
// befroe the invoke the last_step
// we need request the mkfile to compose all uploaded chunks
// else
// invalke the last_step
logger.debug('ctx: ', ctx);
if (ctx) {
let key = '';
logger.debug('save_key: ', op.save_key);
if (!op.save_key) {
key = getFileKey(up, file, that.key_handler);
key = key ? '/key/' + that.URLSafeBase64Encode(key) : '';
}
const fname = '/fname/' + that.URLSafeBase64Encode(file.name);
logger.debug('op.x_vars: ', op.x_vars);
const x_vars = op.x_vars;
let x_val = '';
let x_vars_url = '';
if (x_vars !== undefined && typeof x_vars === 'object') {
for (const x_key in x_vars) {
if (x_vars.hasOwnProperty(x_key)) {
if (typeof x_vars[x_key] === 'function') {
x_val = that.URLSafeBase64Encode(x_vars[x_key](up, file));
} else if (typeof x_vars[x_key] !== 'object') {
x_val = that.URLSafeBase64Encode(x_vars[x_key]);
}
x_vars_url += '/x:' + x_key + '/' + x_val;
}
}
}
const url = qiniuUploadUrl + '/mkfile/' + file.size + key + fname + x_vars_url;
const ie = that.detectIEVersion();
let ajax;
if (ie && ie <= 9) {
ajax = new mOxie.XMLHttpRequest();
mOxie.Env.swf_url = op.flash_swf_url;
} else {
ajax = that.createAjax();
}
ajax.open('POST', url, true);
ajax.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
ajax.setRequestHeader('Authorization', 'UpToken ' + that.token);
const onreadystatechange = function() {
logger.debug('ajax.readyState: ', ajax.readyState);
if (ajax.readyState === 4) {
localStorage.removeItem(file.name);
let info;
if (ajax.status === 200) {
info = ajax.responseText;
logger.debug('mkfile is success: ', info);
last_step(up, file, info);
} else {
info = {
status: ajax.status,
response: ajax.responseText,
file,
code: -200,
responseHeaders: ajax.getAllResponseHeaders(),
};
logger.debug('mkfile is error: ', info);
uploader.trigger('Error', info);
}
}
};
if (ie && ie <= 9) {
ajax.bind('readystatechange', onreadystatechange);
} else {
ajax.onreadystatechange = onreadystatechange;
}
ajax.send(ctx);
logger.debug('mkfile: ', url);
} else {
last_step(up, file, info.response);
}
};
})(_FileUploaded_Handler)
);
logger.debug('bind FileUploaded event');
// init uploader
uploader.init();
logger.debug('invoke uploader.init()');
logger.debug('init uploader end');
return uploader;
};
/**
* get url by key
* @param {String} key of file
* @return {String} url of file
*/
this.getUrl = function(key) {
if (!key) {
return false;
}
key = encodeURI(key);
let domain = this.domain;
if (domain.slice(domain.length - 1) !== '/') {
domain = domain + '/';
}
return domain + key;
};
/**
* invoke the imageView2 api of Qiniu
* @param {Object} api params
* @param {String} key of file
* @return {String} url of processed image
*/
this.imageView2 = function(op, key) {
if (!/^\d$/.test(op.mode)) {
return false;
}
const mode = op.mode;
const w = op.w || '';
const h = op.h || '';
const q = op.q || '';
const format = op.format || '';
if (!w && !h) {
return false;
}
let imageUrl = 'imageView2/' + mode;
imageUrl += w ? '/w/' + w : '';
imageUrl += h ? '/h/' + h : '';
imageUrl += q ? '/q/' + q : '';
imageUrl += format ? '/format/' + format : '';
if (key) {
imageUrl = this.getUrl(key) + '?' + imageUrl;
}
return imageUrl;
};
/**
* invoke the imageMogr2 api of Qiniu
* @param {Object} api params
* @param {String} key of file
* @return {String} url of processed image
*/
this.imageMogr2 = function(op, key) {
const auto_orient = op['auto-orient'] || '';
const thumbnail = op.thumbnail || '';
const strip = op.strip || '';
const gravity = op.gravity || '';
const crop = op.crop || '';
const quality = op.quality || '';
const rotate = op.rotate || '';
const format = op.format || '';
const blur = op.blur || '';
// Todo check option
let imageUrl = 'imageMogr2';
imageUrl += auto_orient ? '/auto-orient' : '';
imageUrl += thumbnail ? '/thumbnail/' + thumbnail : '';
imageUrl += strip ? '/strip' : '';
imageUrl += gravity ? '/gravity/' + gravity : '';
imageUrl += quality ? '/quality/' + quality : '';
imageUrl += crop ? '/crop/' + crop : '';
imageUrl += rotate ? '/rotate/' + rotate : '';
imageUrl += format ? '/format/' + format : '';
imageUrl += blur ? '/blur/' + blur : '';
if (key) {
imageUrl = this.getUrl(key) + '?' + imageUrl;
}
return imageUrl;
};
/**
* invoke the watermark api of Qiniu
* @param {Object} api params
* @param {String} key of file
* @return {String} url of processed image
*/
this.watermark = function(op, key) {
const mode = op.mode;
if (!mode) {
return false;
}
let imageUrl = 'watermark/' + mode;
if (mode === 1) {
const image = op.image || '';
if (!image) {
return false;
}
imageUrl += image ? '/image/' + this.URLSafeBase64Encode(image) : '';
} else if (mode === 2) {
const text = op.text ? op.text : '';
const font = op.font ? op.font : '';
const fontsize = op.fontsize ? op.fontsize : '';
const fill = op.fill ? op.fill : '';
if (!text) {
return false;
}
imageUrl += text ? '/text/' + this.URLSafeBase64Encode(text) : '';
imageUrl += font ? '/font/' + this.URLSafeBase64Encode(font) : '';
imageUrl += fontsize ? '/fontsize/' + fontsize : '';
imageUrl += fill ? '/fill/' + this.URLSafeBase64Encode(fill) : '';
} else {
// Todo mode3
return false;
}
const dissolve = op.dissolve || '';
const gravity = op.gravity || '';
const dx = op.dx || '';
const dy = op.dy || '';
imageUrl += dissolve ? '/dissolve/' + dissolve : '';
imageUrl += gravity ? '/gravity/' + gravity : '';
imageUrl += dx ? '/dx/' + dx : '';
imageUrl += dy ? '/dy/' + dy : '';
if (key) {
imageUrl = this.getUrl(key) + '?' + imageUrl;
}
return imageUrl;
};
/**
* invoke the imageInfo api of Qiniu
* @param {String} key of file
* @return {Object} image info
*/
this.imageInfo = function(key) {
if (!key) {
return false;
}
const url = this.getUrl(key) + '?imageInfo';
const xhr = this.createAjax();
let info;
const that = this;
xhr.open('GET', url, false);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
info = that.parseJSON(xhr.responseText);
}
};
xhr.send();
return info;
};
/**
* invoke the exif api of Qiniu
* @param {String} key of file
* @return {Object} image exif
*/
this.exif = function(key) {
if (!key) {
return false;
}
const url = this.getUrl(key) + '?exif';
const xhr = this.createAjax();
let info;
const that = this;
xhr.open('GET', url, false);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
info = that.parseJSON(xhr.responseText);
}
};
xhr.send();
return info;
};
/**
* invoke the exif or imageInfo api of Qiniu
* according with type param
* @param {String} ['exif'|'imageInfo']type of info
* @param {String} key of file
* @return {Object} image exif or info
*/
this.get = function(type, key) {
if (!key || !type) {
return false;
}
if (type === 'exif') {
return this.exif(key);
} else if (type === 'imageInfo') {
return this.imageInfo(key);
}
return false;
};
/**
* invoke api of Qiniu like a pipeline
* @param {Array of Object} params of a series api call
* each object in array is options of api which name is set as 'fop' property
* each api's output will be next api's input
* @param {String} key of file
* @return {String|Boolean} url of processed image
*/
this.pipeline = function(arr, key) {
const isArray = Object.prototype.toString.call(arr) === '[object Array]';
let option;
let errOp;
let imageUrl = '';
if (isArray) {
for (let i = 0, len = arr.length; i < len; i++) {
option = arr[i];
if (!option.fop) {
return false;
}
switch (option.fop) {
case 'watermark':
imageUrl += this.watermark(option) + '|';
break;
case 'imageView2':
imageUrl += this.imageView2(option) + '|';
break;
case 'imageMogr2':
imageUrl += this.imageMogr2(option) + '|';
break;
default:
errOp = true;
break;
}
if (errOp) {
return false;
}
}
if (key) {
imageUrl = this.getUrl(key) + '?' + imageUrl;
const length = imageUrl.length;
if (imageUrl.slice(length - 1) === '|') {
imageUrl = imageUrl.slice(0, length - 1);
}
}
return imageUrl;
}
return false;
};
}
const Qiniu = new QiniuJsSDK();
global.Qiniu = Qiniu;
global.QiniuJsSDK = QiniuJsSDK;
})(window);
export const chunk = function(array, size) {
const result = [];
let [start, end] = [null, null];
for (let i = 0; i < Math.ceil(array.length / size); i++) {
start = i * size;
end = start + size;
result.push(array.slice(start, end));
}
return result;
};
export const getParameterByName = function(name, url) {
if (!url) {
url = window.location.href;
}
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
// 判断输入内容是否为空
export function isNull(str) {
return str === undefined || str.length === 0 || str === null;
}
// 判断手机号
export function isPhone(str) {
if (!isNull(str)) {
const reg = /^1[3456789]\d{9}$/;
return reg.test(str);
}
return false;
}
export function resizeDiv(imgUrl, clientHeight = 0, clientWidth = 0, callback) {
const img = new Image();
clientHeight = clientHeight || document.body.clientHeight;
clientWidth = clientWidth || document.body.clientWidth;
img.onload = function() {
const imgRealHeight = clientWidth * img.height / img.width;
console.log(img.width, img.height, imgRealHeight, clientHeight);
if (imgRealHeight > clientWidth) {
callback(imgRealHeight);
}
};
img.src = imgUrl;
}
const DK = {t: 'n', b: 's', l: 'w', r: 'e'}; // 上下左右 对应的 东南西北
export const convertPointStyle = (point, defaultStyle, directionKey = DK) => {
const pos = defaultStyle;
const height = pos.height;
const width = pos.width;
const hasT = /t/.test(point);
const hasB = /b/.test(point);
const hasL = /l/.test(point);
const hasR = /r/.test(point);
let newLeft = 0;
let newTop = 0;
if (point.length === 2) {
newLeft = hasL ? 0 : width;
newTop = hasT ? 0 : height;
} else {
// !#zh 上下点,宽度固定在中间
if (hasT || hasB) {
newLeft = width / 2 - 4;
newTop = hasT ? 0 : height;
}
// !#zh 左右点,高度固定在中间
if (hasL || hasR) {
newLeft = hasL ? 0 : width;
newTop = height / 2 - 4;
}
}
const style = {
marginLeft: (hasL || hasR) ? '-4px' : 0,
marginTop: (hasT || hasB) ? '-4px' : 0,
left: `${newLeft}px`,
top: `${newTop}px`,
cursor: point.split('').reverse().map(m => directionKey[m]).join('') + '-resize'
};
return style;
};
export const getStyle = function(oElement, sName) {
const result = oElement.currentStyle ? oElement.currentStyle[sName] : getComputedStyle(oElement, null)[sName];
return result.includes('px') ? result.slice(0, -2) : result;
};
export const validateType = function(obj) {
const class2type = {};
'Array Date RegExp Object Error'.split(' ').forEach(e => {
class2type[`[object ${e}]`] = e.toLowerCase();
});
if (obj == null) { return String(obj); }
return typeof obj === 'object' ? class2type[Object.prototype.toString.call(obj)] || 'object' : typeof obj;
};
export const transformStyle = function(styleObj = {}) {
// console.log('transformStyle', styleObj);
const style = {};
for (const key of Object.keys(styleObj)) {
if ( typeof styleObj[key] === 'number') {
style[key] = `${(styleObj[key] / 37.5).toFixed(3)}rem`;
} else {
style[key] = styleObj[key].includes('px') ? `${(+(styleObj[key].slice(0, -2)) / 37.5).toFixed(3)}rem` : styleObj[key];
}
if (key === 'backgroundImage' && style.backgroundImage) {
style.background = `url(${style.backgroundImage}) no-repeat 0 0 / cover`;
}
}
return style;
};
{
"extends": "../../config/tsconfig.json",
"compilerOptions": {
"target": "es5",
"module": "esnext",
"sourceMap": true,
"lib": [
"es6",
"dom",
"es2017",
"esnext"
],
"baseUrl": ".",
"paths": {
"@asset/*": ["asset/*"],
"@component/*": ["component/*"],
"@framework/*": ["framework/*"],
"@store/*": ["page/store/*"],
"@router/*": ["page/admin/home/router/*"],
"@view/*": ["page/admin/home/view/*"]
}
},
"include": [
"./**/*.ts",
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
\ No newline at end of file
import { AxiosRequestConfig } from 'axios';
declare var window: Window;
declare var EASY_ENV_IS_NODE: boolean;
interface Window {
__INITIAL_STATE__: any;
}
declare module 'axios' {
export interface AxiosRequestConfig {
accessToken?: boolean;
}
}
\ No newline at end of file
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en" style="font-size: 37.5px;">
<head>
<title>低代码平台</title>
<meta name="keywords">
<meta name="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">
<style>
html, body {
line-height: 1.15 !important;
}
</style>
</head>
<body>
<div id="app"><!--vue-ssr-outlet--></div>
</body>
</html>
\ No newline at end of file
module.exports = {
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": [
"last 2 versions",
"safari >= 7"
]
}
}
]
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-object-assign",
"@babel/plugin-syntax-jsx",
"transform-vue-jsx",
"@babel/plugin-transform-runtime"
],
"comments": false
};
\ No newline at end of file
#!/usr/bin/env node
"use strict";
const npm = require("npm");
npm.load(() => {
npm.run("start");
});
\ No newline at end of file
import { EggAppConfig } from 'egg';
import * as fs from 'fs';
import * as path from 'path';
export default (appInfo: EggAppConfig) => {
const config: any = {};
config.siteFile = {
'/favicon.ico': fs.readFileSync(path.join(appInfo.baseDir, 'app/web/asset/images/favicon.ico'))
};
config.view = {
cache: false
};
config.vuessr = {
layout: path.resolve(appInfo.baseDir, 'app/web/view/layout.html'),
renderOptions: {
basedir: path.join(appInfo.baseDir, 'app/view'),
},
};
config.logger = {
consoleLevel: 'DEBUG',
dir: path.join(appInfo.baseDir, 'logs')
};
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'public')
};
config.keys = '123456';
config.middleware = [
'access'
];
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
};
return config;
};
import { EggAppConfig } from 'egg';
import * as path from 'path';
import { getWebpackConfig } from '@easy-team/easywebpack-vue';
export default (appInfo: EggAppConfig) => {
const exports: any = {};
exports.static = {
maxAge: 0 // maxAge 缓存,默认 1 年
};
exports.development = {
// watchDirs: ['app/controller'], // 指定监视的目录(包括子目录),当目录下的文件变化的时候自动重载应用,路径从项目根目录开始写
ignoreDirs: ['app/web', 'public', 'config/manifest.json'] // 指定过滤的目录(包括子目录)
};
exports.logview = {
dir: path.join(appInfo.baseDir, 'logs')
};
exports.vuessr = {
injectCss: false
};
exports.webpack = {
webpackConfigList: getWebpackConfig(),
browser: 'http://localhost:7001/editor/list'
};
return exports;
};
/**
* production
*
* prod + default(override)
*/
import { Application, EggAppConfig } from 'egg';
export default (appInfo: EggAppConfig) => {
const exports: any = {};
exports.logger = {
dir: '/home/quant_group/logs',
};
const prodMysqlConfig = {
dialect: 'mysql',
username: 'low_code_w',
password: 'B2pRHuGMLBNybuKp',
host: 'low-code-db.quantgroups.com',
port: 3306,
database: 'low_code',
};
return exports;
};
import { Application, EggAppConfig } from 'egg';
export default (appInfo: EggAppConfig) => {
const exports: any = {};
return exports;
};
export default {
cors: {
package: 'egg-cors'
},
webpack: {
package: 'egg-webpack'
},
webpackvue : {
package: 'egg-webpack-vue'
}
};
\ No newline at end of file
export default {
vuessr: {
package: 'egg-view-vue-ssr'
}
};
\ No newline at end of file
{
"compilerOptions": {
/* Basic Options */
// "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
// "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation: */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": false, /* Enable strict checking of function types. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": false, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
\ No newline at end of file
{
"name": "quantum-blocks",
"version": "0.0.1",
"description": "低代码平台",
"scripts": {
"start": "egg-scripts start --port 80 --workers 1",
"stop": "egg-scripts stop",
"backend": "nohup egg-scripts start --port 7001 --workers 4",
"dev": "egg-bin dev -r egg-ts-helper/register",
"debug": "egg-bin debug -r egg-ts-helper/register",
"build": "easy build --devtool --size",
"tsc": "ets && tsc -p tsconfig.json",
"clean": "ets clean",
"kill": "easy kill",
"lint": "tslint --project . -c tslint.json",
"fix": "tslint --fix --project . -c tslint.json 'app/web/**/*{.ts}'",
"ii": "npm install --registry https://registry.npm.taobao.org",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"dependencies": {
"@better-scroll/core": "^2.0.5",
"@qg/cherry-ui": "^2.18.66",
"@qg/citrus-ui": "0.0.1-alpha.2",
"axios": "^0.21.1",
"babel-plugin-transform-vue-jsx": "^4.0.1",
"egg": "^2.3.0",
"egg-bin": "^4.9.0",
"egg-cors": "^2.1.1",
"egg-scripts": "^2.10.0",
"egg-ts-helper": "^1.13.0",
"egg-view-vue-ssr": "^3.0.5",
"egg-webpack": "^4.4.7",
"egg-webpack-vue": "^2.0.0",
"js-cookie": "^2.2.1",
"qs": "^6.9.4",
"raven-js": "^3.27.2",
"sa-sdk-javascript": "^1.15.27",
"vue": "^2.6.12",
"vue-awesome-swiper": "^3.1.3",
"vue-grid-layout": "^2.3.11",
"vue-property-decorator": "^7.2.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-class": "^0.3.1",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"typescript": "^3.9.2",
"@easy-team/easywebpack-cli": "^4.0.0",
"@easy-team/easywebpack-vue": "^4.0.0",
"webpack-sentry-plugin": "^1.16.0",
"postcss-px2rem": "^0.3.0",
"less": "^3.12.2",
"less-loader": "^7.1.0",
"tslint": "^5.9.1",
"tslint-eslint-rules": "^5.4.0",
"tslint-loader": "^3.5.3",
"ts-loader": "^5.3.0",
"ts-node": "^7.0.1",
"@babel/plugin-syntax-jsx": "^7.12.1",
"@types/node": "^10.12.0"
},
"egg": {
"typescript": true
},
"engines": {
"node": ">=8.0.0"
},
"ci": {
"version": "8, 10"
},
"repository": {
"type": "git",
"url": "git+https://github.com/easy-team/egg-vue-typescript-boilerplate.git"
},
"author": "hubcarl@126.com",
"license": "MIT",
"homepage": "https://github.com/easy-team/egg-vue-typescript-boilerplate.git",
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
module.exports = {
plugins: {
"postcss-px2rem": {
remUnit: 37.5
}
}
};
{
"extends": "./config/tsconfig.json",
"compileOnSave": true,
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"strictNullChecks": false
},
"include": [
"index.ts",
"app/**/*.ts",
"config/**/*.ts",
"mock/**/*.ts",
"test/**/*.ts"
],
"exclude": [
"public",
"app/web",
"app/public",
"app/view",
"node_modules"
]
}
\ No newline at end of file
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-eslint-rules"
],
"rules": {
"member-access": false,
"ordered-imports": false,
"trailing-comma": false,
"quotemark": [true, "single", "jsx-double"],
"eofline": false,
"object-literal-sort-keys": false,
"interface-name": false,
"arrow-parens": false,
"no-console": false,
"max-line-length": false,
"only-arrow-functions": false,
"interface-over-type-literal": false,
"ter-indent": [ true, 2],
"no-var-requires": false
},
"rulesDirectory": ["app"]
}
\ No newline at end of file
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
import ExportActivity from '../../../app/controller/activity';
declare module 'egg' {
interface IController {
activity: ExportActivity;
}
}
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
import ExtendIHelper from '../../../app/extend/helper';
type ExtendIHelperType = typeof ExtendIHelper;
declare module 'egg' {
interface IHelper extends ExtendIHelperType { }
}
\ No newline at end of file
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
export * from 'egg';
export as namespace Egg;
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
import ExportAccess from '../../../app/middleware/access';
declare module 'egg' {
interface IMiddleware {
access: typeof ExportAccess;
}
}
// This file is created by egg-ts-helper@1.25.8
// Do not modify this file!!!!!!!!!
import 'egg';
import 'egg-onerror';
import 'egg-session';
import 'egg-i18n';
import 'egg-watcher';
import 'egg-multipart';
import 'egg-security';
import 'egg-development';
import 'egg-logrotator';
import 'egg-schedule';
import 'egg-static';
import 'egg-jsonp';
import 'egg-view';
import 'egg-view-vue-ssr';
import 'egg-cors';
import 'egg-webpack';
import 'egg-webpack-vue';
import { EggPluginItem } from 'egg';
declare module 'egg' {
interface EggPlugin {
onerror?: EggPluginItem;
session?: EggPluginItem;
i18n?: EggPluginItem;
watcher?: EggPluginItem;
multipart?: EggPluginItem;
security?: EggPluginItem;
development?: EggPluginItem;
logrotator?: EggPluginItem;
schedule?: EggPluginItem;
static?: EggPluginItem;
jsonp?: EggPluginItem;
view?: EggPluginItem;
vuessr?: EggPluginItem;
cors?: EggPluginItem;
webpack?: EggPluginItem;
webpackvue?: EggPluginItem;
}
}
\ No newline at end of file
'use strict';
const path = require('path');
const SentryPlugin = require("webpack-sentry-plugin");
const resolve = filepath => path.resolve(__dirname, filepath);
const SentryConfig = require("./app/web/.sentryclirc.ts");
module.exports = {
entry: {
// 'login': 'app/web/page/login/index.vue',
// 'editor': 'app/web/page/editor/index.ts',
'activity': 'app/web/page/activity/index.ts',
},
resolve: {
alias:{
'@': resolve('app/web'),
'@api': resolve('app/web/api'),
'@asset': resolve('app/web/asset'),
'@framework': resolve('app/web/framework'),
'@component': resolve('app/web/component'),
'@store': resolve('app/web/page/store'),
// '@router': resolve('app/web/page/admin/home/router'),
// '@view': resolve('app/web/page/admin/home/view'),
// '@editor': resolve('app/web/page/editor')
}
},
nodeExternals: {
whitelist: [ moduleName => {
return /cherry-ui/.test(moduleName) || /citrus-ui/.test(moduleName) || /@interactjs/.test(moduleName);
}]
},
module:{
rules:[
{ babel: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@interactjs'), resolve('node_modules/@qg/citrus-ui')],
exclude: []
}
},
{
vue: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@qg/citrus-ui')],
exclude: []
}
},
{ ts: true },
{ less: true },
{ tslint: { options: { fix: true } } },
]
},
plugins: [
{
copy: [{
from: 'app/web/asset',
to: 'asset'
}]
},
{ imagemini: false },
{
sentry: {
env: ['prod'],
name: 'webpack-sentry-plugin',
args: SentryConfig
}
},
{
define:{
'process.env': {
SENTRY_ENV: JSON.stringify(process.env.SENTRY_ENV)
}
}
}
],
optimization: {
namedModules: true,
namedChunks: true,
runtimeChunk: {
name: 'runtime'
},
splitChunks:
{
name: false,
chunks: 'all',
cacheGroups:
{
default: false,
axios: {
name: 'axios', // 分离组件库
priority: 23,
chunks: 'all',
test: /[\\/]node_modules[\\/]axios[\\/]/,
enforce: true
},
vuex: {
name: 'vuex', // 分离组件库
priority: 23,
chunks: 'all',
test: /[\\/]node_modules[\\/]vuex[\\/]/,
enforce: true
},
saSdk: {
name: "chunk-sa-sdk", // split cherryUI into a single package
priority: 22, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]sa-sdk-javascript[\\/]/, // in order to adapt to cnpm
enforce: true
},
interactjs: {
name: 'interactjs', // 分离组件库
priority: 21,
chunks: 'all',
test: /[\\/]node_modules[\\/]@interactjs[\\/]/,
enforce: true
},
elementResizeDetector: {
name: 'elementResizeDetector', // 分离组件库
priority: 20,
chunks: 'all',
test: /[\\/]node_modules[\\/]element-resize-detector[\\/]/,
enforce: true
},
ravenJs: {
name: 'ravenJs', // 分离组件库
priority: 19,
chunks: 'all',
test: /[\\/]node_modules[\\/]raven-js[\\/]/,
enforce: true
},
swiper: {
name: 'swiper', // 分离组件库
priority: 19,
chunks: 'all',
test: /[\\/]node_modules[\\/]swiper[\\/]/,
enforce: true
},
// cherryUI: {
// name: 'chunk-cherryUI', // 分离组件库
// priority: 18,
// chunks: 'all',
// test: /[\\/]node_modules[\\/]@qg[\\/]cherry-ui[\\/]/,
// enforce: true
// },
vue: {
name: 'chunk-vue', // 分离组件库
priority: 17,
test: /[\\/]node_modules[\\/]vue[\\/]/,
enforce: true
},
common: {
name:'chunk-common',
minChunks: 3,
priority: 3,
reuseExistingChunk: true,
chunks (chunk) {
return ['login','editor','activity'].includes(chunk.name);
},
enforce: true
},
vendors:
{
name: 'common',
chunks: 'all',
minChunks: 3,
priority: 2,
test: /node_modules/,
reuseExistingChunk: true
},
styles:
{
name: 'common',
chunks: 'all',
minChunks: 3,
priority: 1,
test: /\.(css|less|scss|stylus)$/,
priority: 50,
}
}
}
},
devtool:'source-map',
customize(webpackConfig){
// 此外 webpackConfig 为原生生成的 webpack config,可以进行自定义处理
return webpackConfig;
}
};
\ No newline at end of file
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