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

Initial commit

parents
Pipeline #772 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
import { Controller, Context } from 'egg';
import { trim, omitBy } from 'lodash';
export default class EditorController extends Controller {
public async home(ctx: Context) {
await ctx.renderClient('editor.js', {});
}
public async save(ctx: Context) {
const pageInfo = ctx.request.body;
const result = await ctx.model.PageInfo.create(pageInfo);
ctx.body = ctx.helper.ok(result);
}
public async update(ctx: Context) {
const pageInfo = ctx.request.body;
const result = await ctx.model.PageInfo.update(pageInfo, {where: { id: +pageInfo.id }});
ctx.body = ctx.helper.ok(result);
}
public async get(ctx: Context) {
const pageInfo = await ctx.model.PageInfo.findOne({where: { id: +ctx.params.pageId }});
ctx.body = ctx.helper.ok(pageInfo);
}
public async getList(ctx: Context) {
const { type, pageSize, pageNo, author, pageName, isPublish } = ctx.query;
const { like } = ctx.model.Sequelize.Op;
let where = omitBy({
author: author && { like: `%${author}%`},
pageName: pageName && { like: `%${pageName}%`},
isPublish
}, v => !trim(v));
if (type === 'list') {
where = { ...where, isPublish: 1 };
} else if (type === 'my') {
where = { ...where, author: 'congmin.hao' };
} else if (type === 'template') {
where = { ...where, isTemplate: 1 };
}
const { count: total, rows: data } = await ctx.model.PageInfo.findAndCountAll({ where, limit: +pageSize || 10,
offset: (+pageNo - 1) * +pageSize || 0 });
ctx.body = ctx.helper.ok({ total, data });
}
}
\ 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);
}
};
};
/**
* @desc 页面信息表
*/
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, AllowNull } from 'sequelize-typescript';
@Table({
modelName: 'page_info',
freezeTableName: true
})
export class PageInfo extends Model<PageInfo> {
@PrimaryKey
@AutoIncrement
@Column({
type: DataType.INTEGER(11)
})
id: number;
@Column({
field: 'page_data',
type: DataType.TEXT
})
page: string;
@Column({
field: 'page_name',
type: DataType.STRING(32)
})
pageName: string;
@Column({
field: 'page_describe',
type: DataType.STRING(255)
})
pageDescribe: string;
@Column({
type: DataType.INTEGER(1)
})
enable: number;
@AllowNull(false)
@Column({
type: DataType.STRING(32)
})
author: string;
@Column({
type: DataType.STRING(32),
field: 'cover_image'
})
coverImage: string;
@Column({
type: DataType.INTEGER(1),
field: 'is_template'
})
isTemplate: number;
@Column({
type: DataType.INTEGER(1),
field: 'is_publish'
})
isPublish: number;
@Column({
type: DataType.INTEGER(1),
field: 'show_download'
})
showDownload: number;
}
export default () => PageInfo;
\ No newline at end of file
import { Application } from 'egg';
export default (application: Application) => {
const { router, controller } = application;
// router.post('/admin/api/article/list', controller.admin.list);
// router.post('/admin/api/article/add', controller.admin.add);
// router.post('/admin/api/article/del', controller.admin.del);
// router.get('/admin/api/article/:id', controller.admin.detail);
router.post('/editor/save', controller.editor.save);
router.post('/editor/update', controller.editor.update);
router.get('/editor/get/list', controller.editor.getList);
router.get('/editor/get/:pageId', controller.editor.get);
// router.get('/', controller.admin.login);
// router.get('/admin', controller.admin.home);
// router.get('/admin/*', controller.admin.home);
router.get('/editor', controller.editor.home);
router.get('/editor/*', controller.editor.home);
router.get('/activity/:id', controller.activity.home);
};
\ No newline at end of file
import DB from '../lib/db/base';
declare module 'egg' {
interface Application {
db: DB;
}
interface Context {
db: DB;
}
}
\ No newline at end of file
## egg规范view目录, 保证view文件夹存在, 否则app.config.view.root为空, 编译服务器文件会存放到该目录.
\ No newline at end of file
import http from '../service/http.service';
import config from '../config';
const testReq = 0;
export default {
getPageList(params) {
return http.get(config.apiHost + 'editor/get/list', { params });
},
getPageById(params) {
return http.get(`/editor/get/${params.pageId}`);
},
updatePage(params) {
return http.post(`/editor/update`, params);
},
savePage(params) {
return http.post(`/editor/save`, params);
}
};
\ 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
<template>
<div class="top-bar" v-if="isShowTopBar">
<img class="top-bar-left" src="@/asset/images/xyqb@2x.png" />
<div class="top-bar-right">
<a href="https://s.xyqb.com/m">
<button class="top-bar-button">打开APP</button>
</a>
<img @click="isShowTopBar = false" class="top-bar-close" src="@/asset/images/close@2x.png" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShowTopBar: true
}
}
}
</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;
font-size: 0;
a {
margin-right: 15px;
.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 { Vue, Component, Prop } from 'vue-property-decorator';
// import cherryUi from '../../../../../node_modules/@qg/cherry-ui/src/index';
import cherryUi from '@qg/cherry-ui';
import '@qg/cherry-ui/dist/cherry.css';
Vue.use(cherryUi);
@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">
<link rel="stylesheet" href="">
</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;
}
@media screen and (min-width: 768Px) {
html {
font-size: 37.5Px;
}
body {
max-width: 375Px;
max-height: 667Px;
margin: 0 auto !important;
}
}
</style>
<script lang="ts" src="./index.ts"></script>
\ No newline at end of file
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import iView from 'iview';
import cherryUi from '@qg/cherry-ui';
import VueContextMenu from '@editor/component/Contextmenu';
import 'iview/dist/styles/iview.css';
import '@qg/cherry-ui/dist/cherry.css';
Vue.use(iView);
Vue.use(cherryUi);
Vue.use(VueContextMenu);
@Component({
name: 'Layout',
})
export default class Layout extends Vue {
activeName: string = 'list';
get isDashboard() {
return this.activeName === 'detail';
}
@Watch('$route', { immediate: true })
onRouteChange(to) {
this.activeName = to.name;
}
select(name) {
this.$router.push({
name
});
}
created() {
console.log('>>EASY_ENV_IS_NODE create', EASY_ENV_IS_NODE);
}
}
\ No newline at end of file
<template>
<div id="app" class="layout" >
<Row type="flex" class="layout-container" v-if="!isDashboard">
<i-col :span="3" class="layout-menu-left">
<Menu :active-name="activeName" theme="dark" width="auto" :open-names="['1']" accordion @on-select="select">
<div class="layout-logo-left">低代码平台</div>
<Menu-item name="list">作品列表</Menu-item>
<Menu-item name="my">我的作品</Menu-item>
<Menu-item name="template">创意模板</Menu-item>
</Menu>
</i-col>
<i-col :span="21" class="layout-menu-right">
<div class="layout-header">
</div>
<div class="layout-content">
<slot></slot>
</div>
<div class="layout-copy">
2014-2020 &copy; QuantGroup
</div>
</i-col>
</Row>
<slot v-if="isDashboard"></slot>
</div>
</template>
<style lang="less">
#app {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ivu-spin-fix {
z-index: 1000;
background-color: rgba(0, 0, 0, 0.8);
}
.layout {
border: 1px solid #d7dde4;
background: #f5f7f9;
position: relative;
border-radius: 4px;
overflow: auto;
height: 100%;
}
.layout-container {
height: 100%;
}
.layout-breadcrumb {
padding: 10px 15px 0;
}
.layout-content {
flex: 1;
margin: 15px;
overflow: auto;
background: #fff;
border-radius: 4px;
}
.layout-content-main {
padding: 10px;
}
.layout-copy {
text-align: center;
padding: 10px 0 20px;
color: #9ea7b4;
}
.layout-menu-left {
background: #464c5b;
}
.layout-menu-right {
display: flex;
flex-direction: column;
}
.layout-header {
height: 45px;
line-height: 45px;
background: #fff;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
.layout-ceiling-main {
float: right;
margin-right: 15px;
}
.layout-logo-left {
width: 90%;
height: 30px;
color: #fff;
font-size: 18px;
text-align: center;
background: #5b6270;
border-radius: 3px;
margin: 15px auto;
}
.layout-ceiling-main a {
color: #9ba7b5;
}
.ivu-col {
transition: width 0.2s ease-in-out;
}
.router-fade-enter-active,
.router-fade-leave-active {
transition: opacity 0.3s;
}
.router-fade-enter,
.router-fade-leave-active {
opacity: 0;
}
body {
/deep/ .cr-popup {
z-index: 1111;
}
}
</style>
<script lang="ts" src="./index.ts"></script>
\ No newline at end of file
export default {
apiHost: `http://localhost:7001/`,
qiniuHost: `https://appsync.lkbang.net/`,
};
import prod from './prod.config';
import dev from './dev.config';
export default process.env.EGG_SERVER_ENV === 'prod' ? prod : dev;
export default {
apiHost: `http://localhost:7001/`,
qiniuHost: `https://appsync.lkbang.net/`,
};
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 => {
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;
type PlainObject<T = any> = { [key: string]: T };
\ No newline at end of file
<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:;"
:class="['login-form-send', { 'not-allow': noAllowSend || showCount }]"
ref="sendVcode"
>
{{ captchaBtnText }}
</a>
</template>
</cr-field>
<cr-button :color="btnColor" shape='circle' block native-type="submit" class="login-form-submit">
{{ btnTxt }}
</cr-button>
</cr-form>
</template>
<script>
// import sa from 'sa-sdk-javascript';
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.js';
import { getParameterByName } from '@/service/utils.service.ts';
// import './jy/jy.js';
const validOptions = [
[
{ required: true, message: "请填写您的手机号" },
{ validator: isPhone, message: "请填写正确的手机号码" }
],
[{ required: true, message: "请填写验证码" }]
];
let timer = null;
let sa = null;
export default {
name: "KALoginForm",
props: {
btnTxt: {
type: String,
default: "登录"
},
btnColor: {
type: String,
default: '#000'
},
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}秒` : '获取验证码';
}
},
mounted() {
console.log('mounted');
require('./jy/jy.js');
sa = () => import('sa-sdk-javascript');
this.verifyMode();
},
methods: {
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(res) {
var vm = this;
initGeetest(
{
width: "100%",
gt: res.gt,
challenge: res.challenge,
offline: !res.success,
product: "bind"
},
function(captchaObj) {
console.log(this);
vm.$refs.sendVcode.addEventListener('click', function() {
if (!isPhone(vm.formData.phoneNo)) {
vm.$notify({ type: "warning", message: "请填写正确的手机号码" });
return;
}
if (vm.showCount) return;
captchaObj.verify();
});
// captchaObj.appendTo("#captcha-box");
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() {
this.$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: "短信验证码已发送,请注意查收" });
} catch(e) {
console.log(e);
} 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() {
const regcode = getParameterByName('registerFrom') || '';
let le_is_login_success = false;
try {
const { phoneNo, verifyCode } = this.formData;
const { token, uuid } = await Api.fastLogin(phoneNo, verifyCode, null, this.uniqueKey);
Storage.set('token', token);
Cookies.set('phoneNo', phoneNo);
if (uuid) {
Cookies.set('userid', uuid);
sa.login(user.uuid);
}
le_is_login_success = true;
} catch(e) {
console.log(e);
} finally {
sa.track('LoginEvent', {
le_login_action: '注册/登录',
le_login_type: '验证码登录',
le_is_login_success,
le_channel_code: regcode,
});
}
this.goNext();
},
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 });
}
},
};
</script>
<style lang="less">
@import './jy/jy.less';
// @import "../style/var.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 {
border-radius: 8px;
background-color: #eaeaea;
color: #999;
padding: 5px 10px 4px;
}
&-send {
background-color: #f00;
color: #fff;
&.not-allow {
background-color: #eaeaea;
color: #999;
}
}
// &-submit {
// margin-top: 20px !important;
// height: 48px !important;
// border-radius: 8px !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;
}
export const basicComponents = [
{
eleName: 'freedom-container',
title: '自由容器',
config: [
{
key: 'title',
name: '标题',
type: 'Input'
},
{
key: 'showHeader',
name: '显示标题',
type: 'Checkbox'
},
{
key: 'backgroundImage',
name: '背景图片',
type: 'Upload'
},
],
value: {
title: '活动详情'
showHeader: false,
backgroundImage: 'https://static.q-gp.com/new-xyqb-user-ui/public/static/img/landing-bg.6aa8a19.png';
},
commonStyle: {}
},
{
eleName: 'cr-button',
title: '按钮',
config: [
{
key: 'color',
name: '按钮颜色',
type: 'ColorPicker'
},
{
key: 'text',
name: '按钮文案',
type: 'Input'
}
],
value: {
color: '#07c160',
text: '按钮1'
},
commonStyle: {
position: 'absolute',
top: 5,
left: 5
}
},
{
eleName: 'cr-field',
title: '输入框',
config: [
{
key: 'placeholder',
name: '提示信息',
type: 'Input'
},
{
key: 'label',
name: 'label文案',
type: 'Input'
}
],
value: {
placeholder: '请输入',
label: 'label'
},
commonStyle: {
position: 'absolute',
top: 5,
left: 5
}
},
{
eleName: 'cr-image',
title: '图片',
config: [
{
key: 'width',
name: '宽度',
type: 'Input'
},
{
key: 'height',
name: '高度',
type: 'Input'
},
{
key: 'src',
name: '图片',
type: 'Upload'
},
],
value: {
width: '190px',
height: '140px',
src: 'https://appsync.lkbang.net/Fs0qmUsMry39AjHDf_W-qgn8XEy6',
fit: 'contain'
},
commonStyle: {
position: 'absolute',
top: 5,
left: 5
}
}
];
export const businessComponents = [
{
eleName: 'login-form',
title: 'KA登陆表单',
h: 4,
config: [
{
key: 'btnTxt',
name: '按钮文案',
type: 'Input'
},
{
key: 'btnColor',
name: '按钮颜色',
type: 'ColorPicker'
},
{
key: 'registerFrom',
name: '渠道号',
type: 'Input'
},
],
value: {
btnTxt: '登陆',
btnColor: '#ee0a24',
registerFrom: ''
},
commonStyle: {
position: 'absolute',
top: 5,
left: 5
}
}
];
\ No newline at end of file
<template>
<span class="text">
{{text}}
<button>xxx</button>
<span class="vue-draggable-handle"></span>
</span>
</template>
<style>
.vue-draggable-handle {
position: absolute;
width: 20px;
height: 20px;
top: 0;
left: 0;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat;
background-position: bottom right;
padding: 0 8px 8px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: pointer;
}
</style>
<script>
export default {
name: "CustomDragElement",
props: {
text : {
type: String,
default: "x",
},
},
data: function() {
return {
}
},
mounted: function() {
console.log("### " + this.text + " ready!");
},
}
</script>
\ 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>
<template>
<span class="text">
{{text}}
</span>
</template>
<style>
</style>
<script>
export default {
name: "TestElement",
props: {
text : {
type: String,
default: "x",
},
},
data: function() {
return {
}
},
mounted: function() {
console.log("### " + this.text + " ready!");
},
}
</script>
\ No newline at end of file
'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 } from 'vue-property-decorator';
import eleConfig from '../../../editor/utils/config';
import FreedomContainer from '../../../editor/component/FreedomContainer/index.vue';
// import schameConfig from '@qg/cherry-ui/src/button/schame.js';
// import components from '@qg/cherry-ui/src/index.js';
import { kebabCase } from 'lodash';
// import {GridLayout, GridItem} from 'vue-grid-layout';
import GridLayout from '../../component/components/GridLayout.vue';
import GridItem from '../../component/components/GridItem.vue';
import LoginForm from '@/lib/Form/index.vue';
import TopBar from '@/component/TopBar';
import {
Getter
} from 'vuex-class';
@Component({ components: { FreedomContainer, GridLayout, GridItem, TopBar, LoginForm }, name: 'Activity'})
export default class Activity extends Vue {
@Getter('pageData') pageData;
// pageData = {elements: [{name: 'cr-field', point: {x: 0, y: 0, w: 12, h: 1, i: '1', moved: false}}, {name: 'cr-button', point: {x: 0, y: 1, w: 12, h: 1, i: '0', moved: false}, schame: [{key: 'color', name: '按钮颜色', type: 'ColorPicker'}, {key: 'text', name: '按钮文案', type: 'Input'}], props: {tag: 'button', type: 'default', size: 'normal', color: '#07c160', text: '按钮1', shape: 'square', nativeType: 'button', loadingSize: '20px'}, commonStyle: {position: 'absolute', top: 5, left: 5}}, {name: 'freedom-container', point: {x: 0, y: 2, w: 12, h: 5, i: '2', moved: false}, child: []}]};
isLayoutComReady = false;
get layout() {
console.log('this.pageData', this.pageData);
return this.pageData && this.pageData.elements.map(v => v.point) || [];
}
fetchApi(options) {
const { store, route } = options;
const { pageId } = route.params;
return store.dispatch('getPageDate', { pageId });
}
createStyle({h}) {
return EASY_ENV_IS_NODE ? {
height: `${h * 50 + Math.max(0, h - 1) * 10}px`,
// marginTop: '10px'
} : {};
}
}
\ No newline at end of file
<template>
<div class="activity">
<TopBar />
<grid-layout
:layout.sync="layout"
:isDraggable="false"
:isResizable="false"
:col-num="12"
:row-height="55.59"
:margin="[0, 0]"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item :style="createStyle(item.point)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i">
<component class="Dcmc-panel-com" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less">
html,
body,
#app {
height: 100%;
/deep/ .cr-popup {
.cr-notify {
font-size: 14px;
}
}
}
.activity {
width: 100%;
height: 100%;
min-height: 100%;
overflow-y: scroll;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
// transform: translateY(-10px);
transition-property: none;
.vue-grid-item {
transition-property: none;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
}
}
}
</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>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</Layout>
</template>
<script lang="ts" src="./index.ts"></script>
import {Component, Vue, Prop, Watch} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash';
import Upload from '@editor/component/DynamicForm/component/Upload/index.vue';
@Component({ components: { Upload }, name: 'BasicPageForm' })
export default class DynamicForm extends Vue {
@Getter('pageInfo') pageData;
@Prop(Boolean) value;
showPopup: boolean = false;
formCustom: object = {};
ruleCustom: object = {
pageName: [
{ required: true, message: '请输入页面名称', trigger: 'blur' }
]
};
@Watch('pageData', { immediate: true })
onPageDataChange(newVal) {
const { pageName, pageDescribe, coverImage, showDownload, isPublish } = this.pageData;
this.formCustom = { pageName, pageDescribe, coverImage, showDownload: !!showDownload, isPublish: !!isPublish };
}
@Watch('value')
onValueChange(val) {
console.log('onValueChange', val);
this.showPopup = val;
}
handleSubmit(type) {
this.$refs.formCustom.validate((valid) => {
if (valid) {
this.$emit('submit', type, { ...this.formCustom, showDownload: this.formCustom.showDownload ? 1 : 0, isPublish: this.formCustom.isPublish ? 1 : 0 });
}
});
},
change(val) {
this.$emit('input', val);
}
}
\ No newline at end of file
<template>
<Modal v-model="showPopup" width="380" @on-visible-change="change" class-name='basic-form'>
<Form ref="formCustom" :model="formCustom" :rules="ruleCustom" :label-width="80" label-position="left">
<FormItem label="页面名称" prop="pageName">
<Input v-model="formCustom.pageName" placeholder="请输入页面名称"></Input>
</FormItem>
<FormItem label="页面描述" prop="pageDescribe">
<Input v-model="formCustom.pageDescribe" type="textarea" placeholder="请输入页面描述" :rows="3"></Input>
</FormItem>
<FormItem label="页面封面" prop="coverImage">
<Upload v-model="formCustom.coverImage" />
</FormItem>
<FormItem label="引导下载" prop="showDownload">
<Checkbox v-model="formCustom.showDownload"></Checkbox>
</FormItem>
<FormItem label="是否发布" prop="isPublish">
<Checkbox v-model="formCustom.isPublish"></Checkbox>
</FormItem>
</Form>
<div slot="footer">
<Button type="success" @click="handleSubmit('save')">保存</Button>
<Button type="info" @click="handleSubmit('preview')">保存并预览</Button>
</div>
</Modal>
</template>
<script lang="ts" src="./index.ts">
// import Upload from '@editor/component/DynamicForm/component/Upload/index.vue';
// export default {
// name: 'BasicPageForm',
// components: {
// Upload,
// },
// props: {
// value: Boolean,
// defaultForm: Object
// },
// data() {
// return {
// showPopup: false,
// formCustom: {
// pageName: '',
// pageDescribe: '',
// coverImage: 'http://desk.fd.zol-img.com.cn/g5/M00/00/07/ChMkJ1ZqMb2IWITEAAbRDaofaNIAAGBHwO3hh0ABtEl380.jpg',
// showDownload: false,
// isPublish: false,
// ...this.defaultForm
// },
// ruleCustom: {
// pageName: [
// { required: true, message: '请输入页面名称', trigger: 'blur' }
// ]
// },
// }
// },
// watch: {
// value(val) {
// this.showPopup = val;
// },
// },
// methods: {
// handleSubmit(type) {
// this.$refs.formCustom.validate((valid) => {
// if (valid) {
// this.$emit('submit', type, this.formCustom);
// }
// });
// },
// change(val) {
// this.$emit('input', val);
// }
// }
// }
</script>
\ No newline at end of file
<template>
<div></div>
</template>
<script>
import Vue from "vue";
import { getElementsByClassName } from "../util";
import { COMPONENT_NAME } from "../constant";
export default {
data() {
return {
items: [],
position: {
x: 0,
y: 0
},
style: {
zIndex: 2,
minWidth: 150
},
mainMenuInstance: null,
customClass: null,
mouseListening: false
};
},
mounted() {
const SubmenuConstructor = Vue.component(COMPONENT_NAME);
this.mainMenuInstance = new SubmenuConstructor();
this.mainMenuInstance.items = this.items;
this.mainMenuInstance.commonClass = {
menu: 'menu',
menuItem: 'menu_item',
clickableMenuItem: 'menu_item__clickable',
unclickableMenuItem: 'menu_item__unclickable'
};
this.mainMenuInstance.position = {
x: this.position.x,
y: this.position.y,
width: 0,
height: 0
};
this.mainMenuInstance.style.minWidth = this.style.minWidth;
this.mainMenuInstance.style.zIndex = this.style.zIndex;
this.mainMenuInstance.customClass = this.customClass;
this.mainMenuInstance.$mount();
document.body.appendChild(this.mainMenuInstance.$el);
this.addListener();
},
destroyed() {
this.removeListener();
if (this.mainMenuInstance) {
this.mainMenuInstance.close();
}
},
methods: {
mousewheelListener() {
this.$destroy();
},
mouseDownListener(e) {
let el = e.target;
const menus = getElementsByClassName('menu');
while (!menus.find(m => m === el) && el.parentElement) {
el = el.parentElement;
}
if (!menus.find(m => m === el)) {
this.$destroy();
}
},
mouseClickListener(e) {
let el = e.target;
const menus = getElementsByClassName('menu');
const menuItems = getElementsByClassName('menu_item');
const unclickableMenuItems = getElementsByClassName(
'menu_item__unclickable'
);
while (
!menus.find(m => m === el) &&
!menuItems.find(m => m === el) &&
el.parentElement
) {
el = el.parentElement;
}
if (menuItems.find(m => m === el)) {
if (e.button !== 0 || unclickableMenuItems.find(m => m === el)) {
return;
}
this.$destroy();
return;
}
if (!menus.find(m => m === el)) {
this.$destroy();
}
},
addListener() {
if (!this.mouseListening) {
document.addEventListener("click", this.mouseClickListener);
document.addEventListener("mousedown", this.mouseDownListener);
document.addEventListener("mousewheel", this.mousewheelListener);
this.mouseListening = true;
}
},
removeListener() {
if (this.mouseListening) {
document.removeEventListener("click", this.mouseClickListener);
document.removeEventListener("mousedown", this.mouseDownListener);
document.removeEventListener("mousewheel", this.mousewheelListener);
this.mouseListening = false;
}
}
}
};
</script>
<style module>
.menu,
.menu_item,
.menu_item__clickable,
.menu_item__unclickable {
box-sizing: border-box;
}
</style>
\ No newline at end of file
<template>
<transition name="contextmenu-submenu-fade">
<div
ref="menu"
:class="[commonClass.menu, 'menu', customClass]"
:style="{left: style.left + 'px', top: style.top + 'px', minWidth: style.minWidth + 'px', zIndex: style.zIndex}"
v-if="visible"
@contextmenu="(e)=>e.preventDefault()"
>
<div class="menu_body">
<template v-for="(item,index) of items">
<template v-if="!item.hidden">
<div
:class="[
commonClass.menuItem, commonClass.unclickableMenuItem,
'menu_item', 'menu_item__disabled',
item.divided?'menu_item__divided':null
]"
:key="index"
v-if="item.disabled"
>
<div class="menu_item_icon" v-if="hasIcon">
<i :class="item.icon" v-if="item.icon"></i>
</div>
<span class="menu_item_label">{{item.label}}</span>
<div class="menu_item_expand_icon"></div>
</div>
<div
:class="[
commonClass.menuItem, commonClass.unclickableMenuItem,
'menu_item', 'menu_item__available',
activeSubmenu.index===index? 'menu_item_expand':null,
item.divided?'menu_item__divided':null
]"
:key="index"
@mouseenter="($event)=>enterItem($event,item,index)"
v-else-if="item.children"
>
<div class="menu_item_icon" v-if="hasIcon">
<i :class="item.icon" v-if="item.icon"></i>
</div>
<span class="menu_item_label">{{item.label}}</span>
<div class="menu_item_expand_icon"></div>
</div>
<div
:class="[
commonClass.menuItem, commonClass.clickableMenuItem,
'menu_item', 'menu_item__available',
item.divided?'menu_item__divided':null
]"
:key="index"
@mouseenter="($event)=>enterItem($event,item,index)"
@click="itemClick(item)"
v-else
>
<div class="menu_item_icon" v-if="hasIcon">
<i :class="item.icon" v-if="item.icon"></i>
</div>
<span class="menu_item_label">{{item.label}}</span>
<div class="menu_item_expand_icon"></div>
</div>
</template>
</template>
</div>
</div>
</transition>
</template>
<script>
import Vue from "vue";
import {
SUBMENU_X_OFFSET,
SUBMENU_Y_OFFSET,
SUBMENU_OPEN_TREND_LEFT,
SUBMENU_OPEN_TREND_RIGHT,
COMPONENT_NAME
} from "../constant";
export default {
name: COMPONENT_NAME,
data() {
return {
commonClass: {
menu: null,
menuItem: null,
clickableMenuItem: null,
unclickableMenuItem: null
},
activeSubmenu: {
index: null,
instance: null
},
items: [],
position: {
x: 0,
y: 0,
width: 0,
height: 0
},
style: {
left: 0,
top: 0,
zIndex: 2,
minWidth: 150
},
customClass: null,
visible: false,
hasIcon: false,
openTrend: SUBMENU_OPEN_TREND_RIGHT
};
},
mounted() {
this.visible = true;
for (let item of this.items) {
if (item.icon) {
this.hasIcon = true;
break;
}
}
this.$nextTick(() => {
const windowWidth = document.documentElement.clientWidth;
const windowHeight = document.documentElement.clientHeight;
const menu = this.$refs.menu;
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
(this.openTrend === SUBMENU_OPEN_TREND_LEFT
? this.leftOpen
: this.rightOpen)(windowWidth, windowHeight, menuWidth);
this.style.top = this.position.y;
if (this.position.y + menuHeight > windowHeight) {
if (this.position.height === 0) {
this.style.top = this.position.y - menuHeight;
} else {
this.style.top = windowHeight - menuHeight;
}
}
});
},
methods: {
leftOpen(windowWidth, windowHeight, menuWidth) {
this.style.left = this.position.x - menuWidth;
this.openTrend = SUBMENU_OPEN_TREND_LEFT;
if (this.style.left < 0) {
this.openTrend = SUBMENU_OPEN_TREND_RIGHT;
if (this.position.width === 0) {
this.style.left = 0;
} else {
this.style.left = this.position.x + this.position.width;
}
}
},
rightOpen(windowWidth, windowHeight, menuWidth) {
this.style.left = this.position.x + this.position.width;
this.openTrend = SUBMENU_OPEN_TREND_RIGHT;
if (this.style.left + menuWidth > windowWidth) {
this.openTrend = SUBMENU_OPEN_TREND_LEFT;
if (this.position.width === 0) {
this.style.left = windowWidth - menuWidth;
} else {
this.style.left = this.position.x - menuWidth;
}
}
},
enterItem(e, item, index) {
if (!this.visible) {
return;
}
if (this.activeSubmenu.instance) {
if (this.activeSubmenu.index === index) {
return;
} else {
this.activeSubmenu.instance.close();
this.activeSubmenu.instance = null;
this.activeSubmenu.index = null;
}
}
if (!item.children) {
return;
}
const menuItemClientRect = e.target.getBoundingClientRect();
const SubmenuConstructor = Vue.component(COMPONENT_NAME);
this.activeSubmenu.index = index;
this.activeSubmenu.instance = new SubmenuConstructor();
this.activeSubmenu.instance.items = item.children;
this.activeSubmenu.instance.openTrend = this.openTrend;
this.activeSubmenu.instance.commonClass = this.commonClass;
this.activeSubmenu.instance.position = {
x: menuItemClientRect.x + SUBMENU_X_OFFSET,
y: menuItemClientRect.y + SUBMENU_Y_OFFSET,
width: menuItemClientRect.width - 2 * SUBMENU_X_OFFSET,
height: menuItemClientRect.width
};
this.activeSubmenu.instance.style.minWidth =
typeof item.minWidth === "number" ? item.minWidth : this.style.minWidth;
this.activeSubmenu.instance.style.zIndex = this.style.zIndex;
this.activeSubmenu.instance.customClass =
typeof item.customClass === "string"
? item.customClass
: this.customClass;
this.activeSubmenu.instance.$mount();
document.body.appendChild(this.activeSubmenu.instance.$el);
},
itemClick(item) {
if (!this.visible) {
return;
}
if (
item &&
!item.disabled &&
!item.hidden &&
typeof item.onClick === "function"
) {
return item.onClick();
}
},
close() {
this.visible = false;
if (this.activeSubmenu.instance) {
this.activeSubmenu.instance.close();
}
this.$nextTick(() => {
this.$destroy();
});
}
}
};
</script>
<style scoped>
.menu {
position: fixed;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
background: #fff;
border-radius: 4px;
padding: 8px 0;
}
.menu_body {
display: block;
}
.menu_item {
list-style: none;
line-height: 32px;
padding: 0 16px;
margin: 0;
font-size: 13px;
outline: 0;
display: flex;
align-items: center;
transition: 0.2s;
border-bottom: 1px solid #00000000;
}
.menu_item__divided {
border-bottom-color: #ebeef5;
}
.menu_item .menu_item_icon {
margin-right: 8px;
width: 13px;
}
.menu_item .menu_item_label {
flex: 1;
}
.menu_item .menu_item_expand_icon {
margin-left: 16px;
font-size: 6px;
width: 10px;
}
.menu_item__available {
color: #606266;
cursor: pointer;
}
.menu_item__available:hover {
background: #ecf5ff;
color: #409eff;
}
.menu_item__disabled {
color: #c0c4cc;
cursor: not-allowed;
}
.menu_item_expand {
background: #ecf5ff;
color: #409eff;
}
</style>
<style>
.contextmenu-submenu-fade-enter-active,
.contextmenu-submenu-fade-leave-active {
transition: opacity 0.1s;
}
.contextmenu-submenu-fade-enter,
.contextmenu-submenu-fade-leave-to {
opacity: 0;
}
</style>
\ No newline at end of file
import { Component, Vue, Prop } from 'vue-property-decorator';
import components from '@qg/cherry-ui/src/index.js';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import { kebabCase } from 'lodash';
import { chunk, flatten } from 'lodash';
@Component({ name: 'DynamicComponent' })
export default class DynamicComponent extends Vue {
@Prop({ default: () => ([]), type: Array }) data;
eleConfig: array = chunk(this.data, 2);
created() {
console.log(this.eleConfig, flatten(this.eleConfig));
}
dragstart(event, eleName) {
this.$emit('dragstart');
const eleConfig = flatten(this.eleConfig).find(config => config.eleName === eleName);
const props = this.getProps(eleName);
if (eleName === 'freedom-container') {
event.dataTransfer.setData('text', JSON.stringify({
name: eleName,
point: {x: 0, y: 2, w: 12, h: 5, i: '0'},
child: [],
schame: eleConfig.config,
props: {...props, ...eleConfig.value},
commonStyle: eleConfig.commonStyle
}));
} else {
event.dataTransfer.setData('text', JSON.stringify({
name: eleName,
point: {x: 0, y: 0, w: 12, h: eleConfig.h || 1, i: '0'},
schame: eleConfig.config,
props: {...props, ...eleConfig.value},
commonStyle: eleConfig.commonStyle
}));
}
console.log('getData', event.dataTransfer.getData('text'));
event.dataTransfer.effectAllowed = 'copyMove';
}
dragend() {
this.$emit('dragend');
console.log('dragend');
}
getProps(eleName) {
const props = {};
for (const key of Object.keys(components)) {
const component = components[key];
if (kebabCase(component.name) === eleName && component.props) {
for (const prop of Object.keys(component.props)) {
props[prop] = ['Object', 'Array'].includes(component.props[prop].type.name) ? component.props[prop].default && component.props[prop].default() : component.props[prop].default;
}
}
}
console.log(props);
return props;
}
}
\ No newline at end of file
<template>
<div class="dynamic">
<Row v-for="(arr, index) in eleConfig" :key="index" class="dynamic-row" type="flex" justify="space-between">
<Col span="10" v-for="config in arr" :key="config.eleName+index">
<Card draggable="true" @dragend.native="dragend" @dragstart.native="dragstart($event, config.eleName)">
<p slot="title">{{ config.title }}</p>
<div class="dynamic-row-card">
<img
src="http://desk.fd.zol-img.com.cn/g5/M00/00/07/ChMkJ1ZqMb2IWITEAAbRDaofaNIAAGBHwO3hh0ABtEl380.jpg"
/>
</div>
</Card>
</Col>
</Row>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less">
.dynamic {
margin-top: 10px;
&-row {
margin-bottom: 10px;
p {
text-align: center;
}
&-card {
display: flex;
justify-content: center;
img {
width: 64px;
object-fit: fill;
-webkit-user-drag: none;
}
}
}
}
</style>
<template>
<div class="upload">
<div class="upload-img" v-if="value">
<img :src="value">
<div class="upload-img-cover">
<Icon type="ios-trash-outline" @click.native="handleRemove()"></Icon>
</div>
</div>
<div id="upload">
<div id="upload_pic">
<Icon type="camera" size="20"></Icon>
</div>
</div>
</div>
</template>
<script>
import '@/service/qiniu.service';
import config from '@/config';
export default {
props: {
value: String,
},
methods: {
handleRemove () {
this.$emit('input', '');
this.$emit('on-change');
},
uploadQiniu() {
var uploader = Qiniu.uploader({
runtimes: 'html5', // 上传模式,依次退化
browse_button: 'upload_pic', // 上传选择的点选按钮,**必需**
uptoken_url: 'https://opapi.xyqb.com/upload/getToken',
// uptoken_url: `${config.apiHost}/upload/getToken`, //Ajax请求upToken的Url,**强烈建议设置**(服务端提供)
save_key: true, // 默认 false。若在服务端生成uptoken的上传策略中指定了 `sava_key`,则开启,SDK会忽略对key的处理
domain: config.qiniuHost, // bucket 域名,下载资源时用到,**必需**
get_new_uptoken: false, // 设置上传文件的时候是否每次都重新获取新的token
container: 'upload', // 上传区域DOM ID,默认是browser_button的父元素,
max_file_size: '10mb', // 最大文件体积限制
max_retries: 3, // 上传失败最大重试次数
chunk_size: '4mb', // 分块上传时,每片的体积
auto_start: true, // 选择文件后自动上传,若关闭需要自己绑定事件触发上传
init: {
FileUploaded: (up, file, info) => {
console.log(this);
var res = JSON.parse(info);
if (res.businessCode === '0001') {
this.$Notice.error({
title: res.msg,
desc: '',
});
} else {
this.$Notice.success({
title: '上传成功',
desc: '',
});
this.$emit('input', config.qiniuHost + res.hash);
this.$emit('on-change', config.qiniuHost + res.hash);
// this.url = config.qiniuHost + res.hash;
}
},
},
});
},
},
mounted () {
this.uploadQiniu();
}
}
</script>
<style lang="less" scoped="">
.upload {
display: flex;
align-items: center;
&-img {
display: inline-block;
width: 60px;
height: 60px;
text-align: center;
line-height: 60px;
border: 1px solid transparent;
border-radius: 4px;
overflow: hidden;
background: #fff;
position: relative;
box-shadow: 0 1px 1px rgba(0,0,0,.2);
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
&-cover {
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,.6);
i {
margin: 0 2px;
color: #fff;
font-size: 20px;
cursor: pointer;
}
}
&:hover {
.upload-img-cover {
display: block;
}
}
}
#upload {
text-align: center;
border: 1px dashed;
width: 56px;
height:56px;
line-height: 56px;
#upload_pic {
}
}
}
</style>
\ No newline at end of file
import {Component, Mixins, Prop, Watch} from 'vue-property-decorator';
import { Getter, State } from 'vuex-class';
import { reduce, ceil, subtract, divide } from 'lodash';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import Upload from './component/Upload/index.vue';
@Component({ components: { Upload }, name: 'DynamicForm' })
export default class DynamicForm extends Mixins(ContextMenu) {
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex;
@Getter('pageData') pageData;
@Prop({type: Object, default: () => ({ schame: { config: [], value: [] }, props: {} })}) curElement;
form: object = {};
@Watch('curElement', { immediate: true, deep: true })
onElementChange(newVal) {
this.form = reduce(newVal.schame, (obj, param) => {
obj[param.key] = newVal.props[param.key];
return obj;
}, {});
}
get eleName() {
let result = '';
if (!this.curChildIndex && this.curChildIndex !== 0) {
result = this.pageData?.elements[this.curEleIndex]?.name ?? '';
}
return result;
}
change() {
this.$emit('modProps', this.form);
}
resizedEvent(h, w) {
const elements = this.pageData.elements[this.curEleIndex];
console.log('resizedEvent', this.curEleIndex, elements);
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...elements, point: { ...elements.point, w: w ?? elements.point.w, h: h ?? elements.point.h } } });
}
getStyle(oElement, sName) {
const result = oElement.currentStyle ? oElement.currentStyle[sName] : getComputedStyle(oElement, null)[sName];
return result.includes('px') ? result.slice(0, -2) : result;
}
changeAlignType(type) {
const freedomBody = document.querySelector('.freedom-body');
const curElement = (freedomBody as Element).children[this.curChildIndex];
const [ containerW, containerH ] = [this.getStyle(freedomBody, 'width'), this.getStyle(freedomBody, 'height')];
const [ eleW, eleH ] = [this.getStyle(curElement, 'width'), this.getStyle(curElement, 'height')];
const elements = this.pageData.elements[this.curEleIndex].child[this.curChildIndex];
let { left, top } = elements.commonStyle;
switch (type) {
case 'top':
top = 0; break;
case 'right':
left = subtract(containerW, eleW); break;
case 'bottom':
top = subtract(containerH, eleH); break;
case 'left':
left = 0; break;
case 'vertical':
top = ceil(divide(subtract(containerH, eleH), 2), 2); break;
case 'horizontal':
left = ceil(divide(subtract(containerW, eleW), 2), 2); break;
default: break;
}
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...elements, commonStyle: { ...elements.commonStyle, left, top } } });
}
}
\ No newline at end of file
<template>
<div class="dynamic-form">
<template>
<h4>组件属性</h4>
<Form ref="formCustom" :label-width="60" :model="form">
<FormItem :label="item.name" :key="index" v-for="(item, index) in curElement.schame">
<component :is="item.type" v-model="form[item.key]" @on-change="change" />
</FormItem>
</Form>
</template>
<template v-if="eleName === 'freedom-container' || curChildIndex || curChildIndex === 0">
<h4>基础样式</h4>
<Form ref="formCustom" :label-width="60">
<FormItem label="尺寸" v-if="eleName === 'freedom-container'">
<Tooltip placement="top" content="全屏">
<Button type="ghost" icon="arrow-resize" @click="resizedEvent(12, 12)"></Button>
</Tooltip>
<Tooltip placement="top" content="宽100%">
<Button type="ghost" icon="arrow-swap" @click="resizedEvent(null, 12)"></Button>
</Tooltip>
<Tooltip placement="top" content="高100%">
<Button type="ghost" icon="arrow-swap" @click="resizedEvent(12, null)" ></Button>
</Tooltip>
</FormItem>
<FormItem label="定位" v-if="curChildIndex || curChildIndex === 0">
<Tooltip placement="top" content="上对齐">
<Button type="ghost" icon="arrow-up-c" @click="changeAlignType('top')"></Button>
<!-- <Button @click="changeAlignType('top')">上对齐</Button> -->
</Tooltip>
<Tooltip placement="top" content="右对齐">
<Button type="ghost" icon="arrow-right-c" @click="changeAlignType('right')"></Button>
<!-- <Button @click="changeAlignType('right')">右对齐</Button> -->
</Tooltip>
<Tooltip placement="top" content="下对齐">
<Button type="ghost" icon="arrow-down-c" @click="changeAlignType('bottom')"></Button>
<!-- <Button @click="changeAlignType('bottom')">下对齐</Button> -->
</Tooltip>
<Tooltip placement="top" content="左对齐">
<Button type="ghost" icon="arrow-left-c" @click="changeAlignType('left')"></Button>
<!-- <Button @click="changeAlignType('left')">左对齐</Button> -->
</Tooltip>
<Tooltip placement="top" content="垂直居中">
<Button type="ghost" icon="android-film" @click="changeAlignType('vertical')"></Button>
<!-- <Button @click="changeAlignType('vertical')">垂直居中</Button> -->
</Tooltip>
<Tooltip placement="top" content="水平居中">
<Button type="ghost" icon="android-film" @click="changeAlignType('horizontal')"></Button>
<!-- <Button @click="changeAlignType('horizontal')">水平居中</Button> -->
</Tooltip>
</FormItem>
</Form>
</template>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less">
.dynamic-form {
padding: 0 15px;
h4 {
padding: 10px 0;
margin-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
/deep/ .ivu-form-item-label {
font-size: 14px;
}
/deep/ .ivu-form-item-content {
.ivu-tooltip {
margin-right: 6px;
.ivu-btn-ghost {
padding: 2px 6px;
display: flex;
align-items: center;
justify-content: center;
.ivu-icon {
font-size: 20px;
}
}
&:last-child {
.ivu-icon {
transform: rotate(90deg);
}
}
}
}
}
</style>
\ No newline at end of file
import { Component, Prop, Mixins } from 'vue-property-decorator';
import LoginForm from '@/lib/Form/index.vue';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import { cloneDeep } from 'lodash';
import { Action, Mutation } from 'vuex-class';
@Component({ components: { LoginForm }, name: 'FreedomContainer' })
export default class FreedomContainer extends Mixins(ContextMenu) {
@Action('setDragable') setDragable;
@Prop({type: Object, default: () => ({ child: [] })}) childItem;
@Prop({type: Number, default: 0}) containerIndex;
@Prop({ type: Boolean, default: true }) showHeader;
@Prop(String) title;
@Prop(String) backgroundImage;
mousedown(childIndex, event) {
this.setDragable(false);
const childItem = cloneDeep(this.childItem);
const { top: startTop, left: startLeft } = childItem.child[childIndex].commonStyle;
const { clientY, clientX } = event;
const move = moveEvent => {
moveEvent.stopPropagation();
moveEvent.preventDefault();
const top = moveEvent.clientY - clientY + startTop;
const left = moveEvent.clientX - clientX + startLeft;
this.updatePageInfo({ containerIndex: this.containerIndex, childIndex, data: { ...childItem.child[childIndex], commonStyle: {...childItem.child[childIndex].commonStyle, top, left} } });
};
const up = () => {
this.setDragable(true);
document.removeEventListener('mousemove', move, true);
document.removeEventListener('mouseup', up, true);
};
document.addEventListener('mousemove', move, true);
document.addEventListener('mouseup', up, true);
}
transformStyle(styleObj) {
const style = {};
for (const key of Object.keys(styleObj)) {
style[key] = typeof styleObj[key] !== 'string' ? `${styleObj[key]}px` : styleObj[key];
if (key === 'backgroundImage') {
style.backgroundImage = `url(${style.backgroundImage})`;
}
}
return style;
}
handleElementClick(curEleIndex, curChildIndex) {
console.log('handleElementClick', curEleIndex, curChildIndex, this.childItem);
this.$emit('handleElementClick', curEleIndex, curChildIndex);
}
home() {
console.log('主页');
},
deletedata() {
console.log('delete!');
}
}
\ No newline at end of file
<template>
<div class="freedom" @click.stop="handleElementClick(containerIndex)">
<header v-if="showHeader">{{ title }}</header>
<div :class="['freedom-body', { 'freedom-body_full': showHeader }]" :style="{background: `url(${backgroundImage}) no-repeat 0 0 / cover`}">
<component v-for="(item, index) in childItem.child" :style="transformStyle(item.commonStyle)" :is="item.name" :key="index" @click.stop.native="handleElementClick(containerIndex, index)" @mousedown.native.stop="mousedown(index, $event)" v-bind="item.props" @contextmenu.native.prevent.stop="show($event, containerIndex, index)"></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
# qg-table 查询表单
### 引入
```js
import QGTable from './index.vue';
export default{
components: {
QGTable,
},
}
```
## 代码演示
### 基础用法
`QGTable`需要传入自定义查询条件和表格列
```html
<QGTable
:searchCondition="renderSearchCondition"
:columns="columns"
:request="query"
:toolBar="gettoolBar"
/>
```
### 自定义查询条件
通过`searchCondition`属性可以渲染自定义查询条件
每个查询条件包含`lable``type``key``value``rules`属性
```html
[
{label: '状态',type: 'select', key: 'select', option: statusList, value: 1, rules:{required: true, message: '请输入'}},
{label: '标签',type: 'input', key: 'input', rules: {required:true,trigger: 'blur'}},
{label: '时间', type: 'date', key:'time'}
],
```
### 表格列
通过`columns`属性控制表格展示的列
```html
columns: [{
title: '',
key: ''
}]
```
### 自定义查询操作
通过`request`属性,将改属性是一个方法,默认参数是查询的字端数据,可自定义处理查询的数据,接口处理完毕后需返回表单展示的数据
`data`是表格展示的数据,`total`是分页总数
```html
requestFn(data){
console.log(data);
return {
data: [{},{}],
total: 100,
}
},
```
### 自定义按钮
设置`toolBar`控制自定展示按钮
```html
gettoolBar(){
return [<Button type="primary" onClick={() => this.newElement}>新增</Button>]
},
```
## API
### QGTable Props
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|------|
| columns | 表格列 | *Array* | `-` |
| request | 查询接口方法 | *Function* | `-` |
| toolBar | 自定义按钮 | *Function* | `-` |
### columns
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|------|
| title | 条件标题 | *string* | - |
| value | 默认查询值 | *number \| string* | - |
| formType | 表单类型可选值为 `input``select``date`| *string* | - |
| key | 条件的key | *string* | - |
| rules | 查询条件的校验规则 | *array* | - |
| number | 查询条件的是否为数字 | *boolean* | - |
| valueEnum | 选择框的枚举值 | *object* | - |
| align | 是否居中 | *object* | 'center' |
| hideSearch | 不作为查询条件展示 | *boolean* | false |
| hideTable | 不在列表中展示 | *boolean* | false |
<template>
<div class="tableComStyle">
<div class="searchFormStyle qg-table">
<Form inline ref="searchForm" :rules="searchCondition.rules || {}" :model="searchForm" class="inline">
<FormItem :key="getKey(item.key)" :prop="getKey(item.key)" :rules="item.rules" :label="item.label" v-for="item in searchCondition">
<Input v-model="searchForm[item.key]" placeholder="" class="comWidth" v-if="item.type === 'input'" clearable />
<Select v-model="searchForm[item.key]" class="comWidth" filterable v-else-if="item.type === 'select'" clearable>
<Option :value="isNum(value, item.number)" :key="value" v-for="(label,value) in item.option" :label="label" />
</Select>
<DatePicker v-else class="timeWidth" type="datetimerange" placeholder="" v-model="searchForm[item.key]"></DatePicker>
</FormItem>
<FormItem class="btnGroupStyle">
<div>
<Button class="btnStyle" @click="reset()">重置</Button>
<Button type="primary" class="btnStyle" @click="query()">查询</Button>
</div>
</FormItem>
</Form>
</div>
<div class="tableGroupStyle">
<div class="toolBarStyle">
<h3>查询数据</h3>
<div>
<Button type="primary" class="btnStyle" @click="newEvent()">新增</Button>
<slot></slot>
</div>
</div>
<Table :columns="renderColumns" :data="tableData" class="tableStyle"></Table>
<Page
:total="total"
v-if="total > 0"
show-elevator
show-sizer
class="pageStyle"
:current="searchForm.pageNo"
:pageSize="searchForm.pageSize"
@on-change="changePageNo"
@on-page-size-change="changePageSize"
/>
</div>
</div>
</template>
<script>
export default {
props: {
columns: Array,
request: Function,
toolBar: Function,
},
data() {
return {
tableData: [],
total: 0,
searchForm: {
pageSize: 10,
pageNo: 1,
},
searchCondition: [],
};
},
watch: {
searchCondition: {
handler: function(newVal = []) {
newVal.forEach(item => {
this.$set(this.searchForm, item.key, item.value || null)
});
},
immediate: true,
},
},
computed: {
renderColumns: function() {
return this.handleColumns(this.columns)
}
},
methods: {
changePageNo(page) {
this.query(page);
},
changePageSize(size) {
this.searchForm.pageSize = size;
this.query();
},
async query(page = 1) {
this.$refs.searchForm.validate(async val => {
if (val) {
this.searchForm.pageNo = page;
const getQueryData = (await this.request(this.searchForm)) || {};
this.tableData = getQueryData.data || [];
this.total = getQueryData.total || 0;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
isArray(key) {
return Array.isArray(key);
},
getKey(key) {
return (this.isArray(key) && key[0]) || key;
},
reset() {
this.resetForm('searchForm');
this.searchForm.pageSize = 10;
this.searchForm.pageNo = 1;
},
newEvent(e) {
this.$emit('newBtnClick', e);
},
handleColumns(col=[]) {
const data = []
col.forEach(item => {
item.align = item.align || 'center';
if (item.valueEnum&&!item.render) {
item.render = (h, params) => {
return h('div', item.valueEnum[params.row[item.key]])
}
}
if (!item.hideSearch && item.key !== 'action' && !item.type){
this.searchCondition.push({label: item.title, type: item.formType, rules: item.rules, value: item.value, option: item.valueEnum, key: item.key, number: item.number})
}
if (!item.hideTable) {
data.push(item)
}
})
return data
},
isNum(value, number) {
return number ? +value : value;
}
},
mounted() {
this.$nextTick(this.query);
},
};
</script>
<style scoped lang="less">
// @import '../../styles/comment.less';
@padding: 25px;
.tableComStyle {
background: #f5f5f5;
padding: 15px;
min-height: 1000px;
.searchFormStyle {
text-align: left;
clear: both;
background-color: #ffffff;
padding: @padding;
min-height: 70px;
.labelStyle {
font-weight: bold !important;
display: inline-block;
}
// & @{deep} .ivu-form-item .ivu-form-item-label {
// .labelStyle;
// }
// & @{deep} .ivu-form-item-content {
// .labelStyle;
// }
.required:before {
content: '* ';
color: #ed3f14;
}
}
.btnStyle {
margin-right: 10px;
}
.tableStyle {
margin-top: 15px;
}
.comWidth {
width: 200px;
}
.timeWidth {
width: 320px;
}
.btnGroupStyle {
text-align: left;
float: right;
}
.inline {
display: inline-block;
}
.tableGroupStyle {
background: #ffffff;
padding: @padding;
margin-top: 15px;
.toolBarStyle {
display: flex;
justify-content: space-between;
align-items: center;
}
.pageStyle {
margin-top: 20px;
width: 100%;
text-align: right;
}
}
}
</style>
<style lang="less">
.qg-table {
.ivu-form-item-label {
font-weight: bold;
display: inline-block;
}
.ivu-form-item-content {
display: inline-block;
}
}
</style>
<template>
<div>
<QGTable
:columns="columns"
:request="query"
:toolBar="gettoolBar"
>
<Button>新增</Button>
</QGTable>
</div>
</template>
<script>
import QGTable from './index.vue';
const statusList = [{label: '有效', value: 1},{label: '无效',value: 2}];
const showTitle = function(){}
const type = ''
export default {
components: {
QGTable,
},
data(){
return {
search: {
time: [],
},
type: '',
title: '',
templateList: [],
total: 0,
modal: false,
form: {},
value: null,
formModule: [{}],
columns: [
{
type: 'index',
title: '序号',
},
{
key: 'componentName',
title: '模版ID',
formType: 'input',
value: '11',
},
{
key: 'templateType',
title: '模版名称',
formType: 'input',
hideTable: true,
},
{
key: 'page',
title: '模版类型',
formType: 'select',
value: 1,
number: true,
valueEnum: {
1: '好的',
2: '行动',
3: '可以的',
},
render: (h, params) => {
return h('div', showTitle('page', type, params));
},
},
{
key: 'terminal',
title: '内容',
},
{
key: 'status',
title: '状态',
render: (h, params) => {
return h('div', showTitle('status', statusList, params));
},
},
{
key: 'time',
title: '发布时间',
render: (h, params) => {
return h('div', `${(params.row.startTime && params.row.startTime) || ''}-${(params.row.endTime && params.row.endTime) || ''}`);
},
},
{
key: 'action',
title: '操作',
width: 200,
render: (h, params) => {
const props = {
type: 'primary',
};
const style = {
display: 'inline-block',
margin: '5px',
};
const isShow = status => {
if (Array.isArray(status)) {
return status.includes(params.row.status) ? 'inline-block' : 'none';
}
return params.row.status === status ? 'inline-block' : 'none';
};
const btnArr = [
h(
'Button',
{
props,
style: {
...style,
display: isShow([1, 3]),
},
on: {
click: () => {
console.log('查看');
},
},
},
'查看'
),
h(
'Button',
{
props,
style: {
...style,
display: isShow(2),
},
on: {
click: () => {
console.log('修改!')
},
},
},
'修改'
),
h(
'Poptip',
{
props: {
confirm: true,
transfer: true,
title: '确认启用?',
},
style: {
display: isShow(2),
},
on: {
'on-ok': () => {
console.log('启用')
},
},
},
[
h(
'Button',
{
props,
style,
},
'启用'
),
]
),
h(
'Poptip',
{
props: {
confirm: true,
transfer: true,
title: '确认删除?',
},
style: {
...style,
display: isShow(1),
},
on: {
'on-ok': () => {
console.log('删除')
},
},
},
[
h(
'Button',
{
props,
},
'删除'
),
]
),
];
return h(
'div',
{
style: {
textAlign: 'left',
},
},
btnArr
);
},
},
],
}
},
methods: {
gettoolBar(){
return [<Button type="primary" onClick={() => this.newElement}>新增</Button>]
},
requestFn(data){
console.log(data);
return {
data: [{page: 1},{}],
total: 100,
}
},
query(data) {
console.log(data);
return {
data: [{page:1}, {page:2}],
total: 100,
}
},
},
mounted(){}
}
</script>
\ No newline at end of file
'use strict';
import App from '@framework/app';
import createStore from '../store/index';
import createRouter from './router/index';
import entry from '@editor/view/home/index.vue';
export default new App({ entry, createStore, createRouter }).bootstrap();
\ No newline at end of file
import {Component, Vue } from 'vue-property-decorator';
import { Mutation } from 'vuex-class';
@Component({ name: 'ContextMenu' })
export class ContextMenu extends Vue {
@Mutation('COPY_OR_DELETE_PAGE_INFO') updatePageData;
@Mutation('UPDATE_PAGE_INFO') updatePageInfo;
show(event, containerIndex, childIndex) {
console.log('删除', event, containerIndex, childIndex);
this.$contextmenu({
items: [
{
label: '复制',
onClick: () => {
this.updatePageData({type: 'copy', containerIndex, childIndex});
console.log('复制');
}
},
{
label: '删除',
onClick: () => {
console.log('删除', event, containerIndex, childIndex);
this.updatePageData({type: 'delete', containerIndex, childIndex});
// console.log('删除');
}
},
],
event,
minWidth: 100
});
}
}
\ No newline at end of file
import Vue from 'vue';
import VueRouter from 'vue-router';
import Dashboard from '../view/dashboard/index.vue';
import PageList from '../view/pageList/index.vue';
import myPage from '../view/myPage/index.vue';
import template from '../view/template/index.vue';
Vue.use(VueRouter);
export default function createRouter() {
return new VueRouter({
mode: 'history',
base: '/editor/',
routes: [
{
path: '/',
redirect: 'list'
},
{
name: 'detail',
path: '/detail/:pageId?',
component: Dashboard
},
{
name: 'list',
path: '/list',
component: PageList
},
{
name: 'my',
path: '/my',
component: myPage
},
{
name: 'template',
path: '/template',
component: template
},
]
});
}
import { kebabCase } from 'lodash';
import { Getter, Action, State, Mutation } from 'vuex-class';
import { Mixins, Component, Watch } from 'vue-property-decorator';
import DynamicComponent from '@editor/component/DynamicComponent/index.vue';
import VueGridLayout from 'vue-grid-layout';
import FreedomContainer from '../../component/FreedomContainer/index.vue';
import DynamicForm from '../../component/DynamicForm/index.vue';
import components from '@qg/cherry-ui/src/index.js';
import LoginForm from '@/lib/Form/index.vue';
import { ContextMenu } from '@editor/mixins/contextMenu.mixin';
import BasicPageForm from '@editor/component/BasicPageForm/index.vue';
import { basicComponents, businessComponents } from '@/lib/config';
@Component({components: { DynamicComponent, FreedomContainer, DynamicForm, GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem, LoginForm, BasicPageForm }, name: 'DashBoard'})
export default class DashBoard extends Mixins(ContextMenu) {
@Mutation('ADD_ELEMENTS') addElements;
@Mutation('SET_CUR_ELE_INDEX') setCurEleIndex;
@Mutation('SET_CUR_CHILD_INDEX') setCurChildIndex;
@Action('resetPageData') resetPageData;
@Action('savePageData') savePageData;
@Action('getPageDate') getPageDate;
@Getter('pageId') pageId;
@Getter('pageData') pageData;
@State(state => state.editor.draggable) draggable;
@State(state => state.editor.curEleIndex) curEleIndex;
@State(state => state.editor.curChildIndex) curChildIndex;
activeName: string = '1';
activeTab: string = '0';
isCollapsed: boolean = false;
isDragIn: boolean = false;
isDraging: boolean = false;
// curEleIndex: number | null = null;
// curChildIndex: number | null = null;
resources: object = { basicComponents, businessComponents };
showSubmitPopup: boolean = false;
// todo:
// 1. grid拖拽进去后的顺序
// 2. init height
created() {
const { pageId } = this.$route.params;
if (pageId) {
this.getPageDate({ pageId });
} else {
this.resetPageData();
}
}
mounted() {
this.$nextTick(() => {
this.activeTab = '2';
});
console.log(this.activeTab);
}
get layout() {
return this.pageData.elements.map(v => v.point);
}
get curElement() {
let element = {};
if (this.curEleIndex !== null) {
if (this.curChildIndex !== null) {
element = this.pageData.elements[this.curEleIndex].child[this.curChildIndex];
} else {
element = this.pageData.elements[this.curEleIndex];
}
}
return element;
}
// 选择组件库
selectMaterial(val: string) {
this.activeName = val;
}
async save(type, pageConfig) {
if (!type) {
this.showSubmitPopup = true;
} else {
this.pageData.elements.sort((a, b) => a.point.y - b.point.y);
const { pageName, pageDescribe, coverImage, showDownload, isPublish } = pageConfig;
const pageInfo = { page: JSON.stringify(this.pageData), author: 'congmin.hao', isPublish, pageName, pageDescribe, coverImage, showDownload };
if (+this.pageId) { pageInfo.id = this.pageId; }
await this.savePageData(pageInfo);
this.showSubmitPopup = false;
if (type === 'preview') {
window.open(`http://localhost:7001/activity/${this.pageId}`);
}
}
}
exit() {
this.$router.back();
}
handleElementClick(curEleIndex = null, curChildIndex = null) {
console.log(curEleIndex, curChildIndex);
this.toggle(false);
this.setCurEleIndex(curEleIndex);
this.setCurChildIndex(curChildIndex);
}
toggle(val) {
this.isCollapsed = val;
}
dragstart() {
this.isDraging = true;
}
dragend() {
this.isDraging = false;
}
dragenter() {
console.log('dragenter');
this.isDragIn = true;
}
dragleave() {
console.log('dragleave');
this.isDragIn = false;
}
dragover(event) {
if (event.target.classList.contains('freedom')) {
event.dataTransfer.dropEffect = 'move';
} else {
event.dataTransfer.dropEffect = 'copy';
}
}
modProps(props) {
console.log('modProps');
if (this.curEleIndex !== null) {
if (this.curChildIndex !== null) {
this.updatePageInfo({ containerIndex: this.curEleIndex, childIndex: this.curChildIndex, data: { ...this.pageData.elements[this.curEleIndex].child[this.curChildIndex], props } });
} else {
this.updatePageInfo({ containerIndex: this.curEleIndex, data: { ...this.pageData.elements[this.curEleIndex], props } });
}
}
}
drops(event) {
console.log(event.target);
this.isDragIn = false;
this.isCollapsed = false;
const data = JSON.parse(event.dataTransfer.getData('text'));
event.dataTransfer.clearData();
console.log({...data, i: this.pageData.elements.length}, this.pageData.elements[event.target.dataset.index]);
if (event.target.classList.contains('freedom')) {
this.addElements({ containerIndex: event.target.dataset.index, data });
} else {
this.addElements({ data: {...data, point: { ...data.point, i: this.pageData.elements.length}} });
}
}
getProps(eleName) {
const props = {};
for (const key of Object.keys(components)) {
const component = components[key];
if (kebabCase(component.name) === eleName && component.props) {
console.log(key, component.props);
for (const prop of Object.keys(component.props)) {
props[prop] = [Object, Array].includes(component.props[prop].type) ? component.props[prop].default() : component.props[prop].default;
}
}
}
console.log(props);
return props;
}
resizedEvent(i, h, w) {
const index = this.pageData.elements.findIndex(ele => ele.point.i === i);
this.updatePageInfo({ containerIndex: index, data: { ...this.pageData.elements[index], point: { ...this.pageData.elements[index].point, w, h } } });
}
}
\ No newline at end of file
<template>
<Row class="dashboard" v-cloak>
<Row class="dashboard-header" type="flex" align="middle">
<Col span="8">
<h3>低代码开发平台</h3>
</Col>
<Col span="6">
<Row type="flex" justify="end">
<Col span="7" @click.native="save()">
<Icon type="ionic"></Icon>
<span>保存</span>
</Col>
<Col span="7" @click.native="exit">
<Icon type="monitor"></Icon>
<span>退出</span>
</Col>
</Row>
</Col>
</Row>
<Row class="dashboard-container">
<Col class="dashboard-container-left" span="6">
<Menu class="Dc-left-menu" active-name="1" @on-select="selectMaterial">
<MenuItem name="1">
<Icon type="ios-paper"></Icon>
</MenuItem>
<MenuItem name="2">
<Icon type="ios-people"></Icon>
</MenuItem>
<MenuItem name="3">
<Icon type="stats-bars"></Icon>
</MenuItem>
</Menu>
<div class="Dc-left-content">
<div v-show="activeName === '1'">
<h4>基础库</h4>
<dynamic-component :data="resources.basicComponents" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '2'">
<h4>业务库</h4>
<dynamic-component :data="resources.businessComponents" @dragstart="dragstart" @dragend="dragend"></dynamic-component>
</div>
<div v-show="activeName === '3'">
<h4>模板</h4>
</div>
</div>
</Col>
<Col class="dashboard-container-middle" span="18">
<Row class="Dc-middle-row">
<Col :span="isCollapsed ? 24 : 16" class="Dc-middle-container" @click.native="toggle(true)">
<div :class="[{'Dcm-container-panel_in': isDragIn, 'Dcm-container-panel_draging': isDraging}, 'Dcm-container-panel']" @dragover.prevent @dragenter="dragenter" @dragover="dragover"
@dragleave="dragleave" @drop="drops">
<grid-layout
:layout.sync="layout"
:col-num="12"
:row-height="55.59"
:margin="[0, 0]"
:is-draggable="draggable"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item @click.native.stop="handleElementClick(index, null)" v-for="(item, index) in pageData.elements"
:x="item.point.x"
:y="item.point.y"
:w="item.point.w"
:h="item.point.h"
:i="item.point.i"
:key="item.point.i"
@contextmenu.native.prevent="show($event, index)"
@resized="resizedEvent">
<component class="Dcmc-panel-com" @handleElementClick="handleElementClick" :data-index="index" :containerIndex="index" :childItem="item" :is="item.name" :key="index" v-bind="item.props"></component>
</grid-item>
</grid-layout>
</div>
</Col>
<Col span="8" :class="[{'Dcm-sider_none': isCollapsed}, 'Dc-middle-sider']">
<Tabs class="Dc-middle-editing" type="card">
<TabPane label="属性">
<dynamic-form :curElement="curElement" @modProps="modProps"></dynamic-form>
</TabPane>
<TabPane label="事件">事件</TabPane>
<TabPane label="页面设置">页面设置</TabPane>
</Tabs>
</Col>
</Row>
</Col>
</Row>
<BasicPageForm v-model="showSubmitPopup" @submit="save" />
</Row>
</template>
<style lang="less">
.tabs-position() {
/deep/ .ivu-tabs-nav-scroll {
display: flex;
justify-content: center;
}
}
[v-cloak] {
display: none;
}
.dashboard {
min-width: 1280px;
height: 100% !important;
&-header {
position: relative;
padding: 0 16px;
height: 48px !important;
background: #fff;
box-shadow: 0 2px 3px 0 hsla(0,0%,39.2%,.06);
font-size: 16px;
z-index: 901;
}
&-container {
height: calc(100% - 48px) !important;
&-left {
display: flex !important;
height: 100%;
.Dc-left-menu {
height: 100%;
width: 65px !important;
i {
font-size: 18px !important;
}
}
.Dc-left-content {
flex: 1;
padding: 10px;
}
.tabs-position();
}
&-middle {
height: 100%;
background: #f0f2f5;
.Dc-middle-row {
height: 100%;
.Dc-middle-container {
overflow: scroll;
height: 100%;
opacity: 1;
transition: width ease-in-out 0.5s;
.Dcm-container-panel {
margin: 30px auto;
width: 375px;
height: 667px;
min-height: 667px;
overflow-y: scroll;
background-color: rgb(244, 244, 244);
box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2);
/deep/ .vue-grid-layout {
// transform: translateY(-10px);
.vue-grid-item {
display: flex;
justify-content: center;
align-items: center;
background: #fff;
overflow: hidden;
}
}
.Dcmc-panel-com {
// height: 38px;
}
// &_in {
// opacity: 0.7;
// }
&_draging {
.Dcmc-panel-com {
* {
pointer-events: none;
}
}
opacity: 0.7;
}
}
}
.Dc-middle-sider {
height: 100%;
background: #fff;
transition: width ease-in-out 0.5s;
.Dc-middle-editing {
height: 100%;
min-width: 320px;
.tabs-position();
}
}
.Dcm-sider_none {
width: 0;
}
}
}
}
}
</style>
<script lang="ts" src="./index.ts"></script>
import { Vue, Component, Emit } from 'vue-property-decorator';
import Layout from 'component/layout/editor/index.vue';
@Component({
components: {
Layout
}
})
export default class Home extends Vue {}
\ No newline at end of file
<template>
<Layout>
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
</Layout>
</template>
<script lang="ts" src="./index.ts"></script>
<template>
<div>
<QGTable
:columns="columns"
:request="query"
@newBtnClick="addPage"
>
</QGTable>
</div>
</template>
<script>
import editorApi from '@api/editor.api';
import QGTable from '@editor/component/QgTable/index.vue';
export default {
components: {
QGTable,
},
data(){
return {
columns: [
{
key: 'id',
title: '编号',
formType: 'input',
hideSearch: true
},
{
key: 'pageName',
title: '名称',
formType: 'input',
},
{
key: 'coverImage',
title: '封面',
hideSearch: true,
render: (h, params) => {
return h(
'div',
{
style: {
textAlign: 'left',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '10px 0'
},
},
[h(
'img',{
attrs: {
src: params.row.coverImage,
},
style: {
width: '60px',
height: '60px'
}
}
)]
);
},
},
{
key: 'isPublish',
title: '是否发布',
formType: 'select',
number: true,
valueEnum: {
0: '',
1: '',
}
},
{
key: 'isTemplate',
title: '是否模板',
formType: 'select',
number: true,
hideSearch: true,
valueEnum: {
0: '',
1: '',
}
},
{
key: 'action',
title: '操作',
width: 200,
render: (h, params) => {
const props = {
type: 'primary',
};
const style = {
display: 'inline-block',
margin: '5px',
};
const btnArr = [
h(
'Button',
{
props,
style: {
...style,
},
on: {
click: () => {
console.log(params);
this.$router.push(`/detail/${params.row.id}`);
},
},
},
'修改'
),
h(
'Poptip',
{
props: {
confirm: true,
transfer: true,
title: '确认删除?',
},
style: {
...style,
},
on: {
'on-ok': () => {
console.log('删除')
},
},
},
[
h(
'Button',
{
props,
},
'删除'
),
]
),
];
return h(
'div',
{
style: {
textAlign: 'center',
},
},
btnArr
);
},
},
],
}
},
methods: {
async query(data) {
console.log(data);
return editorApi.getPageList({ type: 'my', ...data });
},
addPage() {
this.$router.push('/detail');
}
},
mounted(){}
}
</script>
\ No newline at end of file
<template>
<div>
<QGTable
:columns="columns"
:request="query"
>
</QGTable>
</div>
</template>
<script>
import editorApi from '@api/editor.api';
import QGTable from '@editor/component/QgTable/index.vue';
export default {
components: {
QGTable,
},
data(){
return {
columns: [
{
key: 'id',
title: '编号',
formType: 'input',
hideSearch: true
},
{
key: 'author',
title: '作者',
formType: 'input',
},
{
key: 'coverImage',
title: '封面',
hideSearch: true,
render: (h, params) => {
return h(
'div',
{
style: {
textAlign: 'left',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '10px 0'
},
},
[h(
'img',{
attrs: {
src: params.row.coverImage,
},
style: {
width: '60px',
height: '60px'
}
}
)]
);
},
},
{
key: 'isTemplate',
title: '是否模板',
formType: 'select',
number: true,
hideSearch: true,
valueEnum: {
0: '',
1: '',
}
},
],
}
},
methods: {
async query(data) {
return editorApi.getPageList({ type: 'list', ...data });
},
},
mounted(){}
}
</script>
\ No newline at end of file
<template>
<div>
<QGTable
:columns="columns"
:request="query"
:toolBar="gettoolBar"
>
</QGTable>
</div>
</template>
<script>
import editorApi from '@api/editor.api';
import QGTable from '@editor/component/QgTable/index.vue';
export default {
components: {
QGTable,
},
data(){
return {
columns: [
{
key: 'id',
title: '编号',
formType: 'input',
hideSearch: true
},
{
key: 'author',
title: '作者',
formType: 'input',
},
{
key: 'coverImage',
title: '封面',
hideSearch: true
},
{
key: 'isTemplate',
title: '是否模板',
formType: 'select',
number: true,
hideSearch: true,
valueEnum: {
0: '',
1: '',
}
},
],
}
},
methods: {
gettoolBar(){
return [<Button type="primary" onClick={() => this.newElement}>新增</Button>]
},
requestFn(data){
console.log(data);
return {
data: [{page: 1},{}],
total: 100,
}
},
async query(data) {
return editorApi.getPageList({ type: 'template' });
},
},
mounted(){}
}
</script>
\ 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 http from '../../../../service/http.service';
import api from '@/api/editor.api';
import { Module, GetterTree, ActionTree, MutationTree } from 'vuex';
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
} from './type';
import RootState from '../../state';
import EditorState, { PageInfo, defaultState, Page } 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> = {
async savePageData({ commit }, condition) {
if (condition.id) {
// await http.post(`/editor/update`, condition);
await api.updatePage(condition);
commit(SET_PAGE_INFO, { ...condition, page: JSON.parse(condition.page as string) });
} else {
// const res = await http.post(`/editor/save`, condition);
const res = await api.savePage(condition);
const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) });
}
},
async getPageDate({ commit }, condition) {
// const res = await http.get(`/editor/get/${condition.pageId}`);
const res = await api.getPageById(condition);
const { page, ...rest } = res as PageInfo;
commit(SET_PAGE_INFO, { ...rest, page: JSON.parse(page as string) });
},
setDragable({ commit }, condition) {
commit(SET_DRAGABLE, condition);
},
resetPageData({ commit }, condition) {
const { pageInfo, curEleIndex, curChildIndex } = { ...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.draggable = data;
},
[SET_CUR_ELE_INDEX](state, curEleIndex) {
state.curEleIndex = curEleIndex;
},
[SET_CUR_CHILD_INDEX](state, curChildIndex) {
state.curChildIndex = curChildIndex;
},
[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);
}
} else if (type === 'copy') {
let eleCopyed = {};
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);
}
},
[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 = { ...defaultState }) {
this.state = initState;
}
}
\ No newline at end of file
export interface Page {
elements: any[];
}
export interface PageInfo {
id?: number;
page?: Page | string;
enable?: number;
author?: string;
coverImage?: string;
isTemplate?: number;
isPublish?: number;
}
export default interface EditorState {
pageInfo: PageInfo;
draggable: boolean;
curEleIndex: number | null;
curChildIndex: number | null;
}
export const defaultState = {
draggable: true,
curEleIndex: null,
curChildIndex: null,
pageInfo: {
id: 0,
pageName: '',
pageDescribe: '',
coverImage: 'http://desk.fd.zol-img.com.cn/g5/M00/00/07/ChMkJ1ZqMb2IWITEAAbRDaofaNIAAGBHwO3hh0ABtEl380.jpg',
showDownload: false,
isPublish: false,
page: {
elements: [],
}
}
};
\ 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';
\ No newline at end of file
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 axios from 'axios';
import basicConfig from '../config';
import localStorage from './localStorage.service';
// import { Notify } from '@qg/cherry-ui';
const ERR_MESSAGE_MAP = {
status: {
400: '错误请求',
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求错误,未找到该资源',
405: '请求方法未允许',
408: '请求超时',
500: '服务器端出错',
501: '网络未实现',
502: '网络错误',
503: '服务不可用',
504: '网络超时',
505: 'http版本不支持该请求'
}
};
const CancelToken = axios.CancelToken;
const pending = {};
const testReq = 0;
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 => {
testReq++;
console.log('config.url', config.url, testReq, JSON.stringify(pending));
// beforeRequest();
// 发起请求时,取消掉当前正在进行的相同请求
if (pending[config.url as string]) {
pending[config.url as string]('取消重复请求');
}
config.cancelToken = new CancelToken(c => (pending[config.url as string] = c));
// 添加token
const token = localStorage.get('Token');
if (token) {
config.headers['x-auth-token'] = token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器即异常处理
instance.interceptors.response.use(
response => {
console.log('response.data.code', JSON.stringify(response && response.data));
if (response && response.config) {
let notifyType = 'danger';
// afterRequest();
delete pending[response.config.url as string];
if (response.data.code === '0000' && response.data.businessCode === '0000') { 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);
} else {
console.log('response.data.code1', JSON.stringify(response));
}
},
async err => {
console.log('err.response', JSON.stringify(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);
}
// 错误提示
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();
}
};
// 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;
}
{
"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
declare var window: Window;
declare var EASY_ENV_IS_NODE: boolean;
interface Window {
__INITIAL_STATE__: any;
}
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">
</head>
<body>
<div id="app"><!--vue-ssr-outlet--></div>
<script src="https://cdn.staticfile.org/plupload/2.1.2/plupload.full.min.js"></script>
</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
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.sequelize = {
dialect: 'mysql',
username: 'qa',
password: 'qatest',
host: '172.17.5.9',
port: 31024,
database: 'low_code',
};
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()
};
return exports;
};
/**
* production
*
* prod + default(override)
*/
import { Application, EggAppConfig } from 'egg';
export default (appInfo: EggAppConfig) => {
const exports: any = {};
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'
},
sequelize: {
enable: true,
package: 'egg-sequelize-ts'
}
};
\ 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": "egg-vue-typescript-boilerplate",
"version": "4.0.3",
"description": "Egg + Vue + TypeScript Server Side Render(SSR) 服务端渲染骨架项目",
"scripts": {
"start": "egg-scripts start --port 7001 --workers 4",
"backend": "nohup egg-scripts start --port 7001 --workers 4 &",
"dev": "egg-bin dev -r egg-ts-helper/register",
"debug": "egg-bin debug -r egg-ts-helper/register",
"build": "npm run tsc && easy build -s",
"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",
"@hubcarl/json-typescript-mapper": "^2.0.0",
"@qg/cherry-ui": "^2.5.27",
"axios": "^0.18.1",
"egg": "^2.3.0",
"egg-cors": "^2.1.1",
"egg-scripts": "^2.10.0",
"egg-sequelize-ts": "^0.2.3-0",
"egg-view-vue-ssr": "^3.0.5",
"egg-webpack": "^4.4.7",
"egg-webpack-vue": "^2.0.0",
"element-ui": "^2.0.8",
"extend": "~3.0.0",
"font-awesome": "^4.7.0",
"iview": "^2.9.0",
"js-cookie": "^2.2.1",
"lodash": "^4.17.4",
"lodash-id": "^0.14.0",
"lowdb": "^1.0.0",
"mockjs": "^1.0.1-beta3",
"moment": "^2.17.1",
"mysql2": "^2.2.5",
"postcss-px2rem": "^0.3.0",
"qs": "^6.9.4",
"sa-sdk-javascript": "^1.15.27",
"sequelize": "^4.38.0",
"sequelize-typescript": "^0.6.6",
"shortid": "^2.2.8",
"showdown": "^1.8.6",
"simplemde": "^1.11.2",
"vue": "^2.6.12",
"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": {
"@babel/plugin-syntax-jsx": "^7.12.1",
"@easy-team/easywebpack-cli": "^4.0.0",
"@easy-team/easywebpack-vue": "^4.0.0",
"@types/lodash": "^4.14.117",
"@types/lowdb": "^1.0.6",
"@types/node": "^10.12.0",
"@types/shortid": "^0.0.29",
"babel-plugin-transform-vue-jsx": "^4.0.1",
"cz-conventional-changelog": "^2.1.0",
"egg-bin": "^4.9.0",
"egg-scripts": "^2.10.0",
"egg-ts-helper": "^1.13.0",
"imagemin-webpack-plugin": "^2.4.2",
"less": "^3.12.2",
"less-loader": "^7.1.0",
"node-tool-utils": "^1.1.1",
"ts-loader": "^5.3.0",
"ts-node": "^7.0.1",
"tslint": "^5.9.1",
"tslint-eslint-rules": "^5.4.0",
"tslint-loader": "^3.5.3",
"typescript": "^3.9.2"
},
"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]
},
"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';
import ExportEditor from '../../../app/controller/editor';
declare module 'egg' {
interface IController {
activity: ExportActivity;
editor: ExportEditor;
}
}
// 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 ExportPageInfo from '../../../app/model/pageInfo';
declare module 'egg' {
interface IModel {
PageInfo: ReturnType<typeof ExportPageInfo>;
}
}
// 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-sequelize-ts';
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;
sequelize?: EggPluginItem;
cors?: EggPluginItem;
webpack?: EggPluginItem;
webpackvue?: EggPluginItem;
}
}
\ No newline at end of file
'use strict';
const path = require('path');
const resolve = filepath => path.resolve(__dirname, filepath);
module.exports = {
entry: {
// 'admin/login': 'app/web/page/admin/login/login.vue',
// 'admin/home': 'app/web/page/admin/home/index.ts',
'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 => {
if (moduleName.includes('cherry-ui') || moduleName.includes('@interactjs')) {
console.log(moduleName);
}
return /cherry-ui/.test(moduleName) || /@interactjs/.test(moduleName);
}]
},
module:{
rules:[
{ babel: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui'), resolve('node_modules/@interactjs')],
exclude: []
}
},
{
vue: {
include: [resolve('app/web'), resolve('node_modules/@qg/cherry-ui')],
exclude: []
}
},
{ ts: {
exclude: []
}
},
{ less: true },
{ tslint: { options: { fix: true } } },
]
},
plugins: [
{
copy: [{
from: 'app/web/asset',
to: 'asset'
}]
}
],
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