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
This diff is collapsed.
This diff is collapsed.
<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>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
'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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment