Commit 79c591ea authored by fanxuehui's avatar fanxuehui

feat: init

parents
{
"presets": ["env", "react", "stage-1"],
"plugins": [
"react-hot-loader/babel",
"transform-runtime",
"transform-decorators-legacy",
["import", {
"libraryName": "antd"
}],
["import", {
"libraryName": "antd-mobile"
}]
]
}
\ No newline at end of file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
#server/
client/pages/pro/render
render/v1/libs
\ No newline at end of file
{
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 9,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"extends": "standard",
"plugins": [
"react"
],
"rules": {
"jsx-quotes": ["off", "prefer-single"],
"react/jsx-boolean-value": "error",
"react/jsx-curly-spacing": ["error", "never"],
"react/jsx-equals-spacing": ["error", "never"],
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-undef": "error",
"react/jsx-tag-spacing": ["error", { "beforeSelfClosing": "always" }],
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/self-closing-comp": "error",
"space-before-function-paren": "off",
"object-curly-spacing": "off"
}
}
\ No newline at end of file
node_modules
.tmp
dist
tmp
*.tmp.js
preview
.DS_Store
.vscode
\ No newline at end of file
registry = http://npm.dui88.com/
{
"printWidth": 80,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"requirePragma": false,
"insertPragma": false
}
# node
# Version v10.8
FROM harbor.dui88.com/library/node10:cat
MAINTAINER op@duiba.com.cn
ARG appname
ARG NODE_ENV
RUN echo "export LANG=en_US.UTF-8" && echo "Asia/Shanghai" > /etc/timezone
ENV LANG='en_US.UTF-8'
RUN mkdir /root/duiba-deploy/
ADD ./duiba-deploy /root/duiba-deploy/
WORKDIR /root/duiba-deploy/
#define entry point which will be run first when the container starts up
ENTRYPOINT node server
\ No newline at end of file
## 积木 - 可视化运营工具平台
让活动页面像搭积木一样简单便捷
## 本地开发时,可以将接口转发到测试环境
添加环境变量`proxy_api`可以转发接口,例:
```shell
proxy_api=http://jimu.tuiatest.cn npm run dev:client
```
或运行 npm 脚本,直接将接口转发到测试环境
```shell
npm run dev:proxy
```
## 本地开发时,可以只启动单个项目
添加环境变量 `DEV_PROJECT` 可以只启动单个项目(注:请不要在线上环境加这个环境变量)。
只启动 pro 项目时,可以这样:
```shell
DEV_PROJECT=pro npm run dev:proxy
```
## 本地调试 mock 数据
```shell
npm run dev
```
## 后端联调
```shell
# 请在修改build/server脚本中代理url(广告后端以及活动后端)之后,执行以下命令
proxy=true npm run dev
```
## 本地调试 dev 环境数据
```shell
# 起客户端并代理到服务端
NODE_ENV=dev node build/server
# 服务端转发到开发环境服务
NODE_ENV=dev BUILD_ENV=local nodemon --inspect server
```
ps:此系统需要从广告管理后台 -> 落地页管理 -> 推啊建站进入.
const path = require('path')
const webpack = require('webpack')
const TuiaAutoUpload = require('tuia-auto-upload')
const webpackConfig = require('./webpack.prod')
console.log(process.env.NODE_ENV)
webpack(webpackConfig, function(err, stats) {
if (err) throw err
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n'
)
;(async () => {
const uploader = await new TuiaAutoUpload({
dir: path.join(__dirname, '../dist/assets/'),
originDir: '/jimu-web/assets/'
})
await uploader.start()
})()
})
#!/usr/bin/env node
let CLIEngine = require('eslint').CLIEngine
let fs = require('fs')
let cli = new CLIEngine({
fix: true,
useEslintrc: true,
configFile: './.eslintrc'
})
let data = cli.executeOnFiles(['**/*.js', '**/*.jsx'])
let writeFile = function(filepath, content) {
return fs.writeFileSync(filepath, content)
}
data.results.forEach(function(ele) {
let { output, filePath } = ele
if (!output) return
let result = writeFile(filePath, output)
console.info(`格式化文件${filePath}${result ? '失败' : '成功'}`)
})
#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const {exec} = require('child_process')
const hooksDir = '../.git/hooks'
const hooksPath = path.join(hooksDir, 'pre-commit')
const examplePath = path.resolve(__dirname, './pre-commit')
const precommitPath = path.resolve(__dirname, hooksPath)
if (!fs.existsSync(precommitPath)) {
exec(`cp ${examplePath} ${precommitPath} `, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`)
}
fs.chmodSync(precommitPath, '777')
})
}
#!/bin/sh
jsfiles=$(git diff --cached --name-only --diff-filter=ACM "*.js" "*.jsx" | tr '\n' ' ')
[ -z "$jsfiles" ] && exit 0
echo "$jsfiles" | xargs ./node_modules/.bin/eslint --fix
RESULT=$?
[ $RESULT -ne 0 ] && exit 1
echo "$jsfiles" | xargs git add
exit 0
\ No newline at end of file
var proxyMiddleware = require('http-proxy-middleware')
const filter = function(pathname, req) {
return !(pathname.match('html') || pathname === '/')
}
function proxyAPI() {
const ipReg = /^((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d))))$/
let target = process.env.proxy_api
const ip = target.split(':')[0]
target = ipReg.test(ip) || ip === 'localhost' ? `http://${target}` : target
return proxyMiddleware(filter, {
target,
changeOrigin: true,
headers: {
referer: target
}
})
}
module.exports = {
proxyAPI
}
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const path = require('path')
const proxy = require('http-proxy-middleware')
const fs = require('fs')
const app = express()
const config = require('./webpack.dev.js')
const compiler = webpack(config)
const instance = webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
})
app.use(instance)
instance.waitUntilValid(() => {
require('opn')('http://localhost:3010/pro#/home', {
app: ['google chrome', '--incognito']
})
})
app.use(webpackHotMiddleware(compiler))
// 加载模板引擎
app.set('views', path.join(__dirname, '../dist/html'))
app.engine('.html', require('ejs').__express)
app.set('view engine', 'html')
app.use('/basics', function(req, res) {
res.sendFile(path.resolve(__dirname, '../dist/html/index.html'))
})
app.use('/pro', function(req, res) {
res.sendFile(path.resolve(__dirname, '../dist/html/pro.html'))
})
app.use('/render', function(req, res) {
res.setHeader('X-Frame-Options', 'allow-from *')
res.render('render-v1', {
env: process.env.NODE_ENV,
isTestLog: true
})
})
app.use('/render-v1', function(req, res) {
res.setHeader('X-Frame-Options', 'allow-from *')
res.render('render-v1', {
env: process.env.NODE_ENV,
isTestLog: true
})
})
app.use('/layers', function(req, res) {
res.sendFile(path.resolve(__dirname, '../dist/html/layers.html'))
})
app.use(
express.static('./', {
extensions: ['html']
})
)
if (process.env.proxy) {
app.use(
['/api', '/sso/ssoIndex'],
proxy({
target: 'http://localhost:3000'
})
)
// 代理到广告后台开发人员机器
// TODO换前缀名称
app.use(
'/java',
proxy({
target: 'http://172.16.47.169:1112',
// target: 'http://yapi.dui88.com/mock/66',
pathRewrite: {
'^/java': ''
},
changeOrigin: true
})
)
// 代理到活动后端开发人员机器
app.use(
'/activity',
proxy({
target: 'http://172.16.47.145:17792',
pathRewrite: {
'^/activity': ''
},
changeOrigin: true
})
)
} else if (process.env.proxy_api) {
// 将接口代理到对应环境
// 例如 `proxy_api=http://jimu.tuiatest.cn npm run dev:client` 可以将接口代理到测试环境
const { proxyAPI } = require('./proxyAPI')
console.log(`开启接口代理,代理到 ${process.env.proxy_api}`)
app.use(proxyAPI())
} else {
// 本地调开发环境
if (process.env.NODE_ENV === 'dev') {
app.use(
proxy('/', {
target: 'http://localhost:3000'
})
)
} else {
app.use(
proxy('/api/system/upload', {
target: 'http://localhost:3000'
})
)
// 本地mock数据
app.use((req, res) => {
const path = req.path
res.set('Content-Type', 'application/json')
console.log(path)
try {
/* eslint no-path-concat: "off" */
res.send(fs.readFileSync(__dirname + '/../mock' + path + '.json'))
} catch (e) {
res.send(
JSON.stringify({
code: '0',
desc: '无此mock数据',
data: {}
})
)
}
})
}
}
const port = 3010
app.listen(port, function() {
console.log(`Example app listening on http://localhost:${port}!\n`)
})
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin')
const path = require('path')
const { getHtmlWebpackPlugins } = require('./webpack.util')
module.exports = {
entry: {},
output: {
path: path.join(__dirname, '../dist')
},
module: {
rules: [
// {
// test: /\.(js|jsx)$/,
// enforce: 'pre',
// loader: 'eslint-loader',
// options: {
// fix: true
// }
// },
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader?limit=10240'
},
{
test: /\.(html|tpl)$/,
loader: 'html-loader'
},
{
test: /\.ejs$/,
loader: 'ejs-loader'
}
]
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
libs: path.join(__dirname, '../client/libs/'),
basics: path.join(__dirname, '../client/pages/basics/'),
pro: path.join(__dirname, '../client/pages/pro/'),
wp: path.join(__dirname, '../client/pages/pro/containers/workplace'),
assets: path.join(__dirname, '../client/assets/'),
components: path.join(__dirname, '../client/components/'),
render: path.join(__dirname, '../client/pages/pro/render/'),
root: path.join(__dirname, '../'),
layers: path.join(__dirname, '../client/pages/layers/'),
layersWp: path.join(
__dirname,
'../client/pages/layers/containers/workplace'
)
}
},
externals: {
swiper: 'Swiper'
},
plugins: [
new CleanWebpackPlugin([path.resolve(__dirname, '../dist')], {
root: path.resolve(__dirname, '../')
}),
...getHtmlWebpackPlugins(),
new HtmlWebpackHarddiskPlugin()
]
}
const webpack = require('webpack')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base.js')
const { getEntry, getStyle } = require('./webpack.util')
module.exports = merge(webpackBaseConfig, {
mode: 'development',
entry: getEntry(true),
devtool: 'inline-source-map',
module: {
rules: [].concat(getStyle(true))
},
devServer: {
contentBase: '../dist',
hot: true
},
output: {
publicPath: '/',
filename: 'assets/[hash:8].[name].js',
chunkFilename: 'assets/[contenthash:8].[name].js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
'process.env': {
JIMU_ENV: `'${process.env.NODE_ENV}'`
}
})
]
})
const webpack = require('webpack')
const merge = require('webpack-merge')
const webpackBaseConfig = require('./webpack.base.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const chalk = require('chalk')
const { getEntry, getStyle } = require('./webpack.util')
module.exports = merge(webpackBaseConfig, {
mode: 'production',
entry: getEntry(),
module: {
rules: [].concat(getStyle())
},
optimization: {
// minimize: false,
splitChunks: false
},
output: {
publicPath: '//yun.tuisnake.com/jimu-web/',
filename: 'assets/[contenthash:8].[name].js',
chunkFilename: 'assets/[contenthash:8].[name].js'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'assets/[contenthash:8].[name].css',
chunkFilename: 'assets/[contenthash:8].[name].css'
}),
new webpack.DefinePlugin({
'process.env': {
JIMU_ENV: `'${process.env.NODE_ENV}'`
}
}),
new ProgressBarPlugin({
format: chalk.yellow(
'打包中 [:bar] :current/:total :percent :elapseds :msg'
),
complete: '●',
incomplete: '○',
width: 20
})
]
})
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const getStyle = isDev => {
const specPath = [
/node_modules|admin.less|global.less/,
path.resolve(__dirname, '../client/pages/pro/styles'),
path.resolve(__dirname, '../client/pages/pro/render/styles'),
path.resolve(__dirname, '../client/pages/pro/render/entry.less'),
path.resolve(__dirname, '../render/v1/styles'),
path.resolve(__dirname, '../render/v1/index.less')
]
const remPath = [/\.rem\./]
const loader = isDev ? 'style-loader' : MiniCssExtractPlugin.loader
const cssSourceMap = !!isDev
const rule = [
{
test: /\.(le|c)ss/,
exclude: specPath.concat(remPath),
use: [
{
loader: loader
},
{
loader:
'css-loader?sourceMap&modules=true&localIdentName=[local]_[hash:base64:5]'
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
sourceMap: isDev
}
}
]
},
{
test: /\.(le|c)ss/,
include: specPath,
exclude: remPath,
use: [
{
loader: loader
},
{
loader: 'css-loader?sourceMap'
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
sourceMap: isDev
}
}
]
},
{
// 手机版CSS处理
test: /(\.less|\.css)$/,
include: remPath,
use: [
{ loader: loader },
{
loader:
'css-loader?sourceMap&modules=true&localIdentName=[local]_[hash:base64:5]'
},
{
loader: 'px2rem-loader',
options: {
remUnit: 200
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
sourceMap: cssSourceMap
}
}
]
}
]
return rule
}
const getEntry = isDev => {
let entry = {
basics: ['./client/pages/basics/index.js'],
// fixme
pro: ['./client/pages/pro/index.js']
}
if (isDev) {
const DEV_PROJECT = process.env.DEV_PROJECT
/**
* 当DEV_PROJECT位某个项目名时,webpack只对该项目进行打包
* 例如 `DEV_PROJECT=pro npm run dev:client` 只对pro项目进行打包
*/
if (DEV_PROJECT && entry[DEV_PROJECT]) {
entry = { [DEV_PROJECT]: entry[DEV_PROJECT] }
}
for (var i in entry) {
entry[i] = entry[i].concat('webpack-hot-middleware/client')
}
}
return entry
}
const HTML_WEBPACK_PLUGINS = {
basics: {
alwaysWriteToDisk: true,
chunks: ['basics'],
filename: 'html/index.html',
template: path.resolve(__dirname, '../client/pages/basics/index.html')
},
pro: {
alwaysWriteToDisk: true,
chunks: ['pro-render', 'pro'],
filename: 'html/pro.html',
template: path.resolve(__dirname, '../client/pages/pro/index.html')
},
render: {
JIMU_ENV: process.env.NODE_ENV,
alwaysWriteToDisk: true,
chunks: ['render'],
filename: 'html/render.html',
template: path.resolve(__dirname, '../client/pages/pro/render/entry.ejs')
}
}
const getHtmlWebpackPlugins = () => {
const DEV_PROJECT = process.env.DEV_PROJECT
/**
* 当DEV_PROJECT位某个项目名时,webpack只对该项目进行打包
* 例如 `DEV_PROJECT=pro npm run dev:client` 只对pro项目进行打包
*/
if (DEV_PROJECT && HTML_WEBPACK_PLUGINS[DEV_PROJECT]) {
return [new HtmlWebpackPlugin(HTML_WEBPACK_PLUGINS[DEV_PROJECT])]
} else {
return Object.values(HTML_WEBPACK_PLUGINS).map(htmlWebpackPluginConfig => {
return new HtmlWebpackPlugin(htmlWebpackPluginConfig)
})
}
}
module.exports = {
getEntry,
getStyle,
getHtmlWebpackPlugins
}
@import '~antd/dist/antd.less';
// @import '~antd/lib/style/index.less';
@import './theme.less';
@import './layout.less';
\ No newline at end of file
@import '~assets/styles/mixins.less';
@import './theme.less';
body,.ant-layout,.jimu-tools, .ant-layout-footer {
background-color: #EAEEF4;
}
.layout {
.ant-layout-header {
background-color: #fff;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 99;
}
.ant-layout-content{
min-height: 100%;
}
.ant-layout-footer{
margin-top: -70px;
}
.logo {
font-size: 24px;
height: 100%;
color: @primary-color;
margin-right: 60px;
float: left;
cursor: pointer;
span{
color: #333;
}
}
.user {
float: right;
.headimg {
display: inline-block;
vertical-align: top;
}
.logOut {
position: relative;
display: inline-block;
margin: 16px 0 16px 0;
.size(30, 30);
border-radius: 50%;
i {
.middle();
}
}
}
}
.jimu-tools {
padding-top: 20px;
height: 80px;
}
.jimu-bars {
float: right;
button {
margin-left: 8px;
}
}
.jimu-container {
padding: 10px;
background-color: #fff;
box-shadow: 0 0 10px #eee;
overflow-y: scroll;
}
.ant-pagination{
.ant-pagination-total-text{
// float: left;
}
}
\ No newline at end of file
//设置宽高
.size(@width: 1, @height: 1) {
width: @width*1px;
height: @height*1px;
}
//设置背景图片
.bgi(@src) {
background-image: url(@src);
background-repeat: no-repeat;
background-size: contain;
}
//设置段落
.text(@fontsize, @height: @fontsize, @lineheight: @height) {
font-size: @fontsize*1px;
height: @height*1px;
line-height: @lineheight*1px;
}
//清除浮动
.clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}
//垂直水平居中
.middle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
//水平居中
.center{
margin-left: auto;
margin-right: auto;
}
//绝对定位
.pos(@left:1,@top:1){
position: absolute;
left: @left*1px;
top: @top*1px;
}
.posr(@right:1,@top:1){
position: absolute;
top: @top*1px;
right: @right*1px;
}
.gray {
filter: grayscale(100%);
filter: gray;
}
.tooltip {
@width: 300;
@height: 80;
@border: 2;
@borderSize: 10;
display: inline-block;
.size(@width,@height);
background: #fffae1;
border: @border*1px solid #8b572a;
border-radius: 10px;
.text(30,@height);
color: #8b572a;
text-align: center;
position: absolute;
&:after,
&:before {
content: " ";
.size(0,0);
}
&:after {
position: absolute;
top: (@height - 2*@border)*1px;
left: (@width - @borderSize*2)/2*1px;
display: inline-block;
border: solid @borderSize*1px;
border-color: #fffae1 transparent transparent transparent;
z-index: 2;
}
&:before {
position: absolute;
top: (@height - 1.5*@border)*1px;
left: (@width - @borderSize*2 - @border*2)/2*1px;
display: inline-block;
border: solid (@borderSize+@border)*1px;
border-color: #8b572a transparent transparent transparent;
z-index: 1;
}
}
//h5容器
.container{
width: 750px;
.center;
}
\ No newline at end of file
@blue-6: #2D9AFF;
@red-6: #DC2324;
@green-6: #7ED321;
@gold-6: #FF8200;
@primary-color : @blue-6;
@info-color : @blue-6;
@success-color : @green-6;
@processing-color : @blue-6;
@error-color : @red-6;
@highlight-color : @red-6;
@warning-color : @gold-6;
@normal-color : #eaeaea;
import React, { Component } from 'react'
import { Modal } from 'antd'
import styles from './index.less'
import QRCode from 'qrcode.react'
import TuiaIcon from 'wp/constructors/fc/icon'
import cn from 'classnames'
export default class Preview extends Component {
constructor(props) {
super(props)
this.state = {
width: 375,
height: 667
}
}
show = () => {
this.setState({
modalVisible: true
})
}
cancel = () => {
this.setState({
modalVisible: false
})
}
changeView = (width, height) => {
this.setState({
width,
height
})
}
render() {
const { id, onCancel, title, content, url } = this.props
const { height, width } = this.state
const TriggerIns = content({
show: this.show
})
return (
<React.Fragment>
{TriggerIns}
<Modal
title={null}
footer={null}
centered
visible={this.state.modalVisible}
wrapClassName="previewH5Modal"
maskStyle={{
backgroundColor: 'rgba(0,0,0,.7)'
}}
width={800}
onCancel={onCancel}
destroyOnClose
>
<div className={styles['preview']}>
<div className={styles['left']}>
<div className={styles['frame-title']}>{title}</div>
<iframe
src={url}
frameBorder="0"
width={width}
height={height}
// style={{ width: '320px', height: '515px' }}
/>
</div>
<div className={styles['right']}>
{/* <div className={styles['instruction-title']}>行业标签</div>
<div className={styles['instruction-label']}>#幼儿教育#</div> */}
<div className={styles.mobile_wrapper}>
<div
className={cn(styles['mobile-model'], {
[styles['active']]: height === 667
})}
onClick={() => this.changeView(375, 667)}
>
<TuiaIcon type="icon-system_phone" />
<div>375*667(9:16)</div>
<div>主流机型</div>
</div>
<div
className={cn(styles['mobile-model'], {
[styles['active']]: height === 812
})}
onClick={() => this.changeView(375, 812)}
>
<TuiaIcon type="icon-system_phone1" />
<div>375*812(9:19.5)</div>
<div>刘海屏</div>
</div>
<div
className={cn(styles['mobile-model'], {
[styles['active']]: height === 562
})}
onClick={() => this.changeView(375, 562)}
>
<TuiaIcon type="icon-system_phone1" />
<div>375*562(2:3)</div>
<div>小屏</div>
</div>
</div>
<div className={styles.spin}></div>
<div className={styles['qrcode']}>
<QRCode value={url} size={160} />
</div>
<div className={styles['helper1']}>手机扫描二维码预览</div>
<div className={styles['helper2']}>
你也可以打开
<a href={url} target="_blank">
{' '}
新窗口预览
</a>
</div>
<div className={styles['close-btn']} onClick={this.cancel}>
关闭预览
</div>
<div className={styles['close-icon']}>
<div onClick={this.cancel}>
<TuiaIcon type="icon-system_Close" />
</div>
</div>
</div>
</div>
</Modal>
</React.Fragment>
)
}
}
@import '~pro/config/common.less';
.preview {
position: relative;
display: flex;
align-items: center;
.spin {
width: 100%;
height: 1px;
background-color: #ddd;
margin: 20px 0;
}
.left {
margin-right: 50px;
border: 1px solid rgba(229, 229, 229, 1);
border-radius: 4px;
box-shadow: 0px 4px 50px 0px rgba(0, 0, 0, 0.4);
background-color: rgba(250, 250, 250, 1);
.frame-title {
padding-left: 16px;
color: #333;
font-size: 14px;
line-height: 40px;
background-color: #fff;
}
}
.right {
position: relative;
width: 408px;
height: 537px;
padding: 16px 24px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
border-radius: 4px;
.mobile_wrapper {
display: flex;
align-items: center;
}
.mobile-model {
width: 100px;
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 4px;
box-shadow: 0px 0px 4px 0px #888;
margin: 0 10px 0 0;
cursor: pointer;
transition: all 0.1s ease;
i {
margin-bottom: 6px;
font-size: 31px;
color: #c2c2c2;
}
div {
line-height: 20px;
color: #333;
font-size: 12px;
}
div:first-of-type {
font-weight: bold;
}
}
.mobile-model.active {
background: rgba(255, 255, 255, 1);
box-shadow: 0px 0px 4px 0px rgba(45, 154, 255, 0.47);
border-radius: 4px;
border: 2px solid rgba(45, 154, 255, 1);
i {
color: @primary-color;
}
}
.instruction-title {
color: #666;
font-size: 12px;
line-height: 24px;
align-self: flex-start;
}
.instruction-label {
color: #333;
font-size: 14px;
line-height: 24px;
margin-bottom: 24px;
align-self: flex-start;
}
.qrcode {
width: 160px;
height: 160px;
}
.helper1 {
margin-top: 16px;
line-height: 24px;
color: #999;
font-size: 12px;
}
.helper2 {
line-height: 24px;
color: #999;
margin-bottom: 40px;
span {
color: #2d9aff;
cursor: pointer;
}
}
.close-btn {
width: 160px;
height: 40px;
background-color: rgba(45, 154, 255, 1);
border-radius: 4px;
line-height: 40px;
color: #fff;
font-weight: 600;
text-align: center;
cursor: pointer;
}
.close-icon {
position: absolute;
top: -70px;
right: 0;
width: 32px;
height: 70px;
z-index: 111;
div {
width: 32px;
height: 32px;
background-color: #999;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
z-index: 1;
position: relative;
cursor: pointer;
i {
line-height: 1;
font-size: 14px;
color: #fff;
}
}
}
.close-icon:before {
content: '';
display: block;
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
background-color: #999;
width: 2px;
}
}
}
import axios from 'axios'
import {
message
} from 'antd'
import util from './util'
const Axios = axios.create({
baseURL: '/api/'
})
const base = (opts) => {
opts = Object.assign({}, {
data: {},
params: {}
}, opts)
opts.data = util.filterNull(opts.data)
opts.params = util.filterNull(opts.params)
return new Promise((resolve, reject) => {
// message.loading('', 0)
Axios(opts)
.then(function (response) {
return response.data
})
.then(function (response) {
// message.destroy()
if (response.code === 0) {
resolve(response.data)
} else {
switch (response.code) {
case 100001:
window.location.href = '/basics#/login'
break
default:
reject(response.msg)
message.error(response.msg)
}
}
})
.catch(function (error) {
// message.destroy()
message.error('系统接口错误')
reject(error)
})
})
}
export default base
const util = {
copy: function (target, source) {
var newObj = Object.assign({}, target)
for (let x in source) {
if (util.checkType(source[x])) {
newObj[x] = util.copy(newObj[x], source[x])
} else {
newObj[x] = source[x]
}
}
return newObj
},
checkType: function (o, type) {
return Object.prototype.toString.call(o) === '[object ' + (type || 'Object') + ']'
},
filterNull: (obj) => {
if (!util.checkType(obj, 'Object')) return obj
let source = Object.assign({}, obj)
for (var i in source) {
if (!source[i]) delete source[i]
if (util.checkType(source[i])) {
return util.filterNull(source[i])
}
}
return source
},
// 复制剪切板
copyToClipboard: str => {
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
},
// 获取qs中指定参数
get: (name) => {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')
var r = window.location.search.substr(1).match(reg)
if (r != null) return unescape(r[2])
return null
},
// 对象中参数为对象或数组,则序列化
serialize: (source) => {
if (!util.checkType(source)) {
return source
}
let target = Object.assign({}, source)
for (var i in target) {
if (util.checkType(target[i]) || util.checkType(target[i], 'Array')) {
target[i] = JSON.stringify(target[i])
}
}
return target
},
// 删除指定id的数据
deleteById: (source, index) => {
if (!util.checkType(source, 'Array')) {
return source
}
return source.splice(index, 1)
}
}
export default util
import React from 'react'
import { Link } from 'react-router'
import { hot } from 'react-hot-loader'
import { Icon, Layout, Menu } from 'antd'
import ajax from 'libs/ajax'
const { Header, Content, Footer } = Layout
const SubMenu = Menu.SubMenu
const App = class App extends React.Component {
constructor (props) {
super(props)
this.state = {
user: {}
}
}
componentWillMount () {
ajax({
url: '/system/user'
}).then(data => {
this.setState(state => {
state.user = data
return state
})
}).catch(err => err)
}
goIndex () {
this.props.router.push('activity')
}
logOut () {
ajax({
url: 'system/logout',
method: 'post'
}).then(() => {
this.props.router.push('login')
}).catch(err => err)
}
render () {
let { user } = this.state
return (
<Layout className='layout'>
<Header>
<div className='logo' onClick={this.goIndex.bind(this)}><span></span></div>
<div className='user'>
<Link to=''><div className='headimg'>{user.username} | </div></Link>
<Link to='' onClick={this.logOut.bind(this)}><div className='logOut'><Icon type='logout' /></div></Link>
</div>
<Menu
theme='light'
mode='horizontal'
defaultSelectedKeys={[]}
style={{ lineHeight: '64px' }}
>
<Menu.Item key='0'><Link to='/activity'><Icon type='book' />活动市场</Link></Menu.Item>
{/* <Menu.Item key='1'><Link to='/plugin'><Icon type='cloud' />模版市场</Link></Menu.Item> */}
<SubMenu title={<span><Icon type='appstore' />插件市场</span>}>
<Menu.Item key='setting:1'><Link to='/plugin'>插件列表</Link></Menu.Item>
<Menu.Item key='setting:2'><Link to='/plugin/cate'>插件分类</Link></Menu.Item>
</SubMenu>
<Menu.Item key='2'><Link to='/layout'><Icon type='shop' />模板市场</Link> </Menu.Item>
</Menu>
</Header>
<Content style={{ padding: '64px 50px 75px 50px' }}>{this.props.children}</Content>
<Footer style={{ textAlign: 'center' }}>
Jimu ©2018 Created by Tuia Frontend
</Footer>
</Layout>
)
}
}
export default hot(module)(App)
import React from 'react'
import { Input, Switch, Upload, InputNumber, Button, Icon } from 'antd'
import Styles from './index.less'
import { ChromePicker } from 'react-color'
const pluginAttrItem = ({ item = {}, option = {}, onChange }) => {
let value = option[item.key]
switch (item.type.toLowerCase()) {
case 'number':
return (<InputNumber onChange={e => onChange(e, item)} value={value} />)
case 'boolean':
return (<Switch checked={value} onChange={e => onChange(e, item)} />)
case 'image':
return (<Upload name='file' listType='picture' showUploadList={false} action='/api/system/upload' onChange={e => onChange(e, item)}>
{value ? <img className={Styles.editImage} src={value} /> : <Button><Icon type='upload' /> 上传图片</Button>}
</Upload>)
case 'color':
return (<ChromePicker onChangeComplete={e => onChange(e, item)} color={value} />)
default:
return (<Input onChange={e => onChange(e, item)} value={value} />)
}
}
export default pluginAttrItem
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
.editImage {
max-width: 100%;
}
\ No newline at end of file
import React from 'react'
import styles from './index.less'
import { Button, Icon } from 'antd'
const pluginItem = props => {
const { item, remove } = props
const thumb = item.thumb ? item.thumb : '//yun.dui88.com/jimo-web/b47235c0-9c84-11e8-8beb-b77a9f575a78.jpeg'
return (
<div className={styles.plugin} onClick={props.onClick}>
<div className={styles.thumb} style={{ backgroundImage: 'url(' + thumb + ')' }} />
<div className={styles.title}>{item.title}</div>
<div className={styles.cate}>#{item.basics_cate.title}#</div>
<div className={styles.line} />
<div className={styles.revisor}>{item.revisor || '佚名'}</div>
{/* <div className={styles.avatar} style={{ backgroundImage: 'url(' + item.avatar + ')' }} /> */}
<div className={styles.action}>
<a href={`#/plugin/edit/${item.id}`}><Button type='primary' ghost><Icon type='edit' />编辑</Button></a>
<a href='javascript:;' onClick={remove.bind(this, item.id)}><Button type='danger' ghost><Icon type='delete' />删除</Button></a>
</div>
</div>
)
}
export default pluginItem
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
@h: 302px;
.plugin {
position: relative;
width: 200px;
max-width: 100%;
height: @h;
background-color: #fff;
display: inline-block;
margin-right: 40px;
margin-bottom: 40px;
border-radius: 4px;
border: 1px solid #e5e5e5;
cursor: pointer;
&:hover {
// box-shadow: 0 0 10px 4px #ddd;
.action {
display: block;
}
}
.thumb{
.size(120,160);
.pos(40,20);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.title{
.text(12,30);
.pos(13,198);
color: #333;
font-weight: bold;
}
.line{
.size(180,1);
background-color: #eaeaea;
.pos(10,257);
}
.cate{
.text(12,20);
.pos(10,227);
color: #333;
}
.revisor{
.text(12,20);
.pos(13,270);
color: #333;
}
.avatar{
.size(24,24);
background-color: @primary-color;
border-radius: 50%;
.pos(166,268);
}
.action{
background-color: #fff;
display: none;
z-index: 99;
width: 100%;
height: 250px;
position: absolute;
top: 0;
left: 0;
a{
display: block;
text-align: center;
}
button{
border-width: 2px;
.size(180,48);
margin: 50px 0 0 0;
}
}
}
import React from 'react'
import styles from './index.less'
const pluginItem = props => {
const item = props.item
const thumb = item.thumb ? item.thumb : '//yun.dui88.com/jimo-web/b47235c0-9c84-11e8-8beb-b77a9f575a78.jpeg'
return (
<div className={styles.plugin + ' plugin'} onClick={props.onClick}>
<div className={styles.thumb} style={{ backgroundImage: 'url(' + thumb + ')' }} />
<div className={styles.title}>{item.title}</div>
</div>
)
}
export default pluginItem
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
@h: 282px;
.plugin {
position: relative;
width: 80px;
height: 80px;
cursor: pointer;
border: 1px solid #e5e5e5;
border-left-width: 0;
// border-top-width: 0;
padding-top: 50px;
.thumb{
.size(28,28);
.pos(26,16);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.title{
.text(12,16);
width: 100%;
overflow: hidden;
text-align: center;
color: #333;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
}
}
import React from 'react'
import { hot } from 'react-hot-loader'
const container = class container extends React.Component {
render () {
return (
this.props.children
)
}
}
export default hot(module)(container)
This diff is collapsed.
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
@leftWidth: 240px;
@rightWidth: 280px;
@headerHeight: 72;
.main {
position: relative;
margin-left: -50px;
margin-right: -50px;
}
.left {
position: absolute;
padding: @headerHeight *1px 0 15px 1px;
left: 0;
top: 0;
width: @leftWidth;
height: 100%;
overflow-x: visible;
overflow-y: scroll;
background-color: #fff;
}
.content {
padding: 30px;
width: 100%;
padding-left: @leftWidth;
padding-right: @rightWidth;
}
.right {
padding: @headerHeight *1px 0 0 0;
position: absolute;
right: 0;
top: 0;
width: @rightWidth;
height: 100%;
overflow-y: scroll;
background-color: #fff;
}
.header{
border-bottom: 1px solid @normal-color;
padding:0 0 0 @leftWidth;
height: @headerHeight *1px;
span {
display: inline-block;
color: #666;
cursor: pointer;
}
}
.headerRight{
.posr(192,0);
}
.back{
height: @headerHeight *1px;
line-height: @headerHeight *1px;
.pos(12,0);
i{
margin-right: 10px;
}
}
.action{
height: @headerHeight;
line-height: 2;
padding: 19px 10px 14px 10px;
user-select: none;
i{
display: block;
font-size: 18;
svg {
margin: 0 auto;
}
}
@disabled-color: #c2c2c2;
&[disabled]{
pointer-events: none;
color: @disabled-color;
}
}
.listTitle {
display: block;
font-size: bold;
color: #666;
.text(12,40);
padding-left: 20px;
width: 100%;
// border-bottom: 1px solid @normal-color;
}
.preview{
margin-top: 40px;
width: 395px;
padding: 10px 10px 10px 10px;
min-height: 643px;
.center;
background-color: #e0e0e0;
border:1px dashed rgba(224,224,224,1);
.previewContent{
width: 375px;
min-height: 603px;
background-color:#fff;
>div{
width: 375px;
min-height: 603px;
}
}
}
.rightTitle{
width: 100%;
padding-left: 28px;
color: @primary-color;
.text(12,40);
border-bottom: 1px solid @normal-color;
}
.comItem {
width: 100%;
height: 150px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
&:hover {
.comItemAction {
display: block;
}
}
}
.comItemAction {
width: 100%;
height: 100%;
position: relative;
// background-color: rgba(255, 255, 255, 1);
button {
.posr(10, 10);
}
}
.editItem {
width: 100%;
padding: 10px;
}
.editItemTitle {
.text(12,32);
display: block;
}
.pluginItemContainer{
float: left;
}
.cateListContainer{
width: @leftWidth * 1px;
.clearfix;
}
::-webkit-scrollbar {
width: 0;
height: 0;
}
\ No newline at end of file
import React from 'react'
import { Link } from 'react-router'
import { Select, Button, Modal, message, Icon, Pagination } from 'antd'
import styles from './index.less'
import { hot } from 'react-hot-loader'
import ajax from 'libs/ajax'
import util from 'libs/util'
const Option = Select.Option
class ActivityList extends React.Component {
constructor (props) {
super(props)
let self = this
this.state = {
actList: [
],
pagination: {
current: 0,
total: 0,
pageSize: 10,
onChange: this.handleChange.bind(self)
}
}
}
actClone (id) {
Modal.confirm({
title: '确定克隆该活动?',
confirmLoading: true,
onOk: () => {
return ajax({
url: `/basics/activity/clone/${id}`,
method: 'post'
}).then(data => {
message.success('克隆成功')
this.getActList()
}).catch(err => err)
}
})
}
actDelete (id) {
Modal.confirm({
title: '确定删除该活动?',
confirmLoading: true,
okType: 'danger',
onOk: () => {
return ajax({
url: `/basics/activity`,
params: {
id
},
method: 'delete'
}).then(data => {
message.success('删除成功')
this.getActList()
}).catch(err => err)
}
})
}
actPublish (id) {
Modal.confirm({
title: '确定发布该活动?',
confirmLoading: true,
okType: 'success',
onOk: () => {
return ajax({
url: `/basics/activity/publish`,
data: {
id
},
method: 'post'
}).then(data => {
message.success('发布成功')
this.getActList()
}).catch(err => err)
}
})
}
getActList (opts = {
pageIndex: 1
}) {
let { pageSize } = this.state.pagination
let { pageIndex, status } = opts
ajax({
url: 'basics/activity',
params: {
pageIndex,
pageSize: pageSize,
status
}
}).then(res => {
this.setState({
actList: res.lists
})
this.setState(function (prevState) {
prevState.pagination.current = res.pageIndex
prevState.pagination.total = res.count
return {
pagination: prevState.pagination
}
})
}).catch(err => err)
}
handleChange (page, pageSize) {
this.getActList({ pageIndex: page })
}
handleStatusChange (value) {
this.getActList({ pageIndex: this.state.pagination.pageIndex, status: value })
}
componentWillMount () {
this.getActList()
}
copyLink (str) {
util.copyToClipboard(str)
message.success('复制成功')
}
render () {
const { pagination } = this.state
return (
<div>
<div className={'jimu-tools ' + styles.tools}>
<Select
showSearch
style={{ width: 120 }}
placeholder='全部状态'
onChange={this.handleStatusChange.bind(this)}
>
<Option value={0}>全部状态</Option>
<Option value='1'>未发布</Option>
<Option value='2'>已上线</Option>
</Select>
<div className={styles.bars}>
<Button type='primary'><Link to='/activity/edit'>新建活动</Link></Button>
</div>
</div>
<div className='jimu-container'>
<div className={styles.list}>
{
this.state.actList.map(item => {
return (<div className={styles.item} key={item.id}>
<span className={styles.thumb} />
<span className={styles.id}>ID:{item.id}</span>
<span className={styles.revisor}><Icon type='user' />{item.revisor}</span>
<span className={styles.title}>{item.title}</span>
<span className={styles.summary}>#{item.summary}#</span>
<span className={styles.status}>{item.status === 2 ? <span><i className={styles.success} />已发布</span> : <span><i className={styles.failed} />未发布</span>}</span>
<div className={styles.right}>
<Link title='编辑' to={'/activity/edit?id=' + item.id} className={styles.link}>编辑</Link>
<a href={`/api/basics/activity/preview/${item.id}`} target='blank' className={styles.link}>预览</a>
<a href='javascript:;' className={styles.link} onClick={this.actClone.bind(this, item.id)}>克隆</a>
<a href='javascript:;' disabled={item.pageLink ? '' : 'disabled'} className={styles.link} onClick={this.copyLink.bind(this, item.pageLink)}>复制链接</a>
<Button type='primary' className={styles.publish} onClick={this.actPublish.bind(this, item.id)}>发布</Button>
<Button type='danger' className={styles.delete} onClick={this.actDelete.bind(this, item.id)}>删除</Button>
</div>
</div>
)
})
}
</div>
<Pagination className={styles.pagination} showTotal={(total, range) => `当前查看为 ${range[0]} - ${range[1]} , 共 ${total} 个`} onChange={pagination.onChange} current={pagination.current} pageSize={pagination.pageSize} total={pagination.total} />,
</div>
</div>
)
}
}
export default hot(module)(ActivityList)
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
.tools {
padding-top: 20px;
}
.bars {
float: right;
button {
margin-left: 8px;
}
}
.list{
}
.item{
height: 80px;
position: relative;
border-bottom: 1px solid #eaeaea;
span {
display: inline-block;
}
button{
margin-right: 20px;
font-size: 12px;
height: 24px;
}
&:hover{
background:rgba(87,140,254,0.08);
}
}
.thmub{
.size(45,60);
.pos(10,10);
}
.id{
color: #999;
.text(12,20);
.pos(75,5);
}
.revisor{
color: #999;
.text(12,20);
.pos(211,5);
i{
vertical-align: middle;
margin-right: 10px;
}
}
.title{
color: #333;
font-weight: bold;
.text(12,26);
.pos(75,25);
}
.summary{
color: #333;
.text(12,20);
.pos(75,56);
}
.status{
color: #666;
.text(12,20);
.pos(351,30);
i{
.size(8,8);
vertical-align: middle;
margin-right: 8px;
display: inline-block;
border-radius: 50%;
}
.success{
background-color: @success-color;
}
.failed{
background-color: @error-color;
}
}
.right{
position: relative;
.posr(0,0);
}
.link{
display: inline-block;
margin-right: 20px;
font-size: 12px;
line-height: 80px;
text-decoration: none;
}
.pagination{
text-align: center;
padding-top: 50px;
}
\ No newline at end of file
import React, { Component } from 'react'
import { hot } from 'react-hot-loader'
import styles from './index.less'
class guide extends Component {
constructor(props) {
super(props)
this.state = {
origin: window.location.origin
}
}
render() {
return (
<div className={styles.body}>
<div className={styles.content}>
<p className={styles.title}>选择下列功能版本</p>
<a href={`${this.state.origin}/layers#/template`}>创建弹层</a>
<a href={`${this.state.origin}/pro#/home`}>创建落地页</a>
</div>
</div>
)
}
}
export default hot(module)(guide)
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
.body {
width: 100%;
height: 100%;
background-color: @primary-color;
}
.title {
text-align: center;
font-size: 26px;
margin: 70px 0;
color: #fff;
}
.content {
.size(460, 400);
.middle;
text-align: center;
a {
@height: 50;
.size(130, @height);
background-color: #fff;
margin-right: 20px;
color: @primary-color;
display: inline-block;
border-radius: 5px;
line-height: @height*1px;
text-align: center;
font-size: 20px;
}
}
import { hot } from 'react-hot-loader'
import React from 'react'
import { Button, Form, Input, Tabs, message } from 'antd'
import { Controlled as CodeMirror } from 'react-codemirror2'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material.css'
import 'codemirror/mode/htmlembedded/htmlembedded'
import 'codemirror/mode/css/css'
import 'codemirror/mode/javascript/javascript'
import ajax from 'libs/ajax'
const FormItem = Form.Item
const TextArea = Input.TextArea
const TabPane = Tabs.TabPane
// import Styles from './index.less'
class layoutEdit extends React.Component {
constructor (props) {
super(props)
this.state = {
formData: {
id: '',
title: '',
summary: '',
template: `<html>
<head>
<title>{{injecttitle}}</title>
{{injectcss}}
</head>
<body>
<div id="app">
{{injecthtml}}
</div>
{{injectjs}}
</body>
</html>`
}
}
}
componentWillMount () {
let { id } = this.props.router.location.query
id && ajax({
url: '/basics/layout',
params: {
id
}
}).then(res => {
let data = res.lists[0]
this.setState(state => {
state.formData = {
id: data.id,
title: data.title,
summary: data.summary,
template: data.template
}
return state
})
})
}
handleSubmit () {
this.props.form.validateFields(
(err) => {
if (!err) {
const formData = this.props.form.getFieldsValue()
const { id, template } = this.state.formData
formData.id = id
formData.template = template
this.saveData(formData)
}
}
)
}
saveData (data) {
ajax({
url: '/basics/layout',
method: data.id ? 'put' : 'post',
data
}).then(res => {
message.success('保存成功')
this.props.router.push('layout')
})
}
// 编辑器改变
handCodeChange (key, value) {
this.setFormData(key, value)
}
// 设置表单值
setFormData (key, value) {
this.setState(prevState => {
let formData = prevState.formData
formData[key] = value
return {
formData
}
})
}
render () {
const { formData } = this.state
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
}
const options = {
html: {
mode: 'xml',
htmlMode: true,
theme: 'material',
lineNumbers: true,
tabSize: 2,
autofocus: true
}
}
const { getFieldDecorator } = this.props.form
return (<div><div className='jimu-tools'>
<div className={'jimu-bars'}>
<Button type='primary' onClick={this.handleSubmit.bind(this)}>保存</Button>
</div>
</div><div className='jimu-container'>
<Form onSubmit={this.handleSubmit}>
<FormItem {...formItemLayout} label='标题'>
{getFieldDecorator('title', {
initialValue: formData.title,
rules: [{
required: true, message: '请输入标题'
}]
})(<Input />
)}
</FormItem>
<FormItem {...formItemLayout} label='简介'>
{getFieldDecorator('summary', {
initialValue: formData.summary,
rules: [{
required: true, message: '请输入简介'
}]
})(<TextArea rows='4' />
)}
</FormItem>
<FormItem {...formItemLayout} label='模板代码'>
<Tabs defaultActiveKey='0' tabBarStyle={{ height: '40px' }}>
<TabPane tab='html' key='1'>
<CodeMirror value={formData.template} onBeforeChange={(editor, data, value) => this.handCodeChange('template', value)} options={options.html} />
</TabPane>
</Tabs>
</FormItem>
</Form>
</div></div>
)
}
}
const WrappedApp = Form.create()(layoutEdit)
export default hot(module)(WrappedApp)
import React from 'react'
import { hot } from 'react-hot-loader'
import { Button, Icon, Modal, message } from 'antd'
import { Link } from 'react-router'
import ajax from 'libs/ajax'
import styles from './index.less'
class layoutList extends React.Component {
constructor (props) {
super(props)
this.state = {
list: []
}
}
componentWillMount () {
this.getList()
}
getList () {
ajax({
url: '/basics/layout',
params: {
pageSize: 100
}
}).then(res => {
this.setState(state => {
state.list = res.lists
return state
})
})
}
layoutDelete (id) {
Modal.confirm({
title: `确定删除该模板吗?`,
okType: 'danger',
onOk: () => {
return ajax({
url: '/basics/layout',
method: 'delete',
params: {
id
}
}).then(res => {
message.success('删除成功')
this.getList()
})
},
onCancel() {
}
})
}
render () {
const { list } = this.state
return (<div><div className='jimu-tools'>
<div className={'jimu-bars'}>
<Link to='/layout/edit'><Button type='primary'>新建模板</Button></Link>
</div>
</div><div className='jimu-container'>
{
list && list.map(item => {
return (<div className={styles.item} key={item.id}>
<span className={styles.thumb} />
<span className={styles.id}>ID:{item.id}</span>
<span className={styles.revisor}><Icon type='user' />{item.revisor}</span>
<span className={styles.title}>{item.title}</span>
<span className={styles.summary}>#{item.summary}#</span>
<div className={styles.right}>
<Link title='编辑' to={'/layout/edit?id=' + item.id} className={styles.link}>编辑</Link>
<Button type='danger' className={styles.delete} onClick={this.layoutDelete.bind(this, item.id)}>删除</Button>
</div>
</div>
)
})
}
</div></div>)
}
}
export default hot(module)(layoutList)
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
.item{
height: 80px;
position: relative;
border-bottom: 1px solid #eaeaea;
span {
display: inline-block;
}
button{
margin-right: 20px;
font-size: 12px;
height: 24px;
}
&:hover{
background:rgba(87,140,254,0.08);
}
}
.thmub{
.size(45,60);
.pos(10,10);
}
.id{
color: #999;
.text(12,20);
.pos(75,5);
}
.revisor{
color: #999;
.text(12,20);
.pos(211,5);
i{
vertical-align: middle;
margin-right: 10px;
}
}
.title{
color: #333;
font-weight: bold;
.text(12,26);
.pos(75,25);
}
.summary{
color: #333;
.text(12,20);
.pos(75,56);
}
.status{
color: #666;
.text(12,20);
.pos(351,30);
i{
.size(8,8);
vertical-align: middle;
margin-right: 8px;
display: inline-block;
border-radius: 50%;
}
.success{
background-color: @success-color;
}
.failed{
background-color: @error-color;
}
}
.right{
position: relative;
.posr(0,0);
}
.link{
display: inline-block;
margin-right: 20px;
font-size: 12px;
line-height: 80px;
text-decoration: none;
}
import React from 'react'
import { hot } from 'react-hot-loader'
import ajax from 'libs/ajax'
import { Form } from 'antd'
import styles from './index.less'
class NormalLoginForm extends React.Component {
constructor (props) {
super(props)
const href = window.location.href
this.state = {
origin: window.location.origin,
isTest: href.indexOf('test') > -1 || href.indexOf('dev') > -1 || href.indexOf('localhost') > -1 || href.indexOf('127.0.0.1') > -1
}
}
handleSubmit (e) {
e.preventDefault()
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
}
})
}
componentWillMount () {
ajax({
url: '/system/user'
}).then(user => {
this.props.router.push('/')
}).catch(err => err)
}
render () {
return (
<div className={styles.login}>
<div className={styles.loginBody}>
<p className={styles.title}>选择下列登录方式</p>
<a className={styles.item} href={`http://sso.duiba${this.state.isTest ? 'test' : ''}.com.cn/login?redirect=${this.state.origin}/basics&systemId=29`}>SSO</a>
</div>
</div>
)
}
}
const login = Form.create()(NormalLoginForm)
export default hot(module)(login)
@import '~assets/styles/mixins.less';
@import '~assets/styles/theme.less';
.login{
width: 100%;
height: 100%;
background-color: @primary-color;
.loginBody{
.size(460,400);
border-radius: 5px;
.middle;
text-align: center;
}
.title{
text-align: center;
font-size: 26px;
margin: 70px 0;
color: #fff;
}
.item{
display: inline-block;
.size(60,60);
line-height: 60px;
font-size: 20px;
display: inline-block;
text-align: center;
border-radius: 50%;
color: #999;
background-color: #fff;
}
}
\ No newline at end of file
import React from 'react'
import { Tag, Button, Input, Modal } from 'antd'
import { hot } from 'react-hot-loader'
import styles from './index.less'
import ajax from 'libs/ajax'
class PluginCate extends React.Component {
constructor (props) {
super(props)
this.state = {
showEdit: false,
data: {},
cateList: []
}
}
componentWillMount () {
this.getCate()
}
getCate () {
ajax({
url: 'basics/cate',
params: {
pageSize: 100
}
}).then(res => {
this.setState({
cateList: res.lists
})
}).catch(err => err)
}
getColor () {
const colors = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple']
return colors[parseInt(Math.random() * colors.length)]
}
toggleEdit (show) {
this.setState(prevState => ({
showEdit: show
}))
}
handleChange = (e) => {
let value = e.target.value
this.setState(preState => {
preState.data.title = value
return {
data: preState.data
}
})
}
handleCancel = (e) => {
this.setState({
showEdit: false
})
}
handleEdit = (data) => {
this.toggleEdit(true)
this.setState({
data: Object.assign({}, data)
})
}
handleSave = () => {
const { data } = this.state
ajax({
url: 'basics/cate',
method: data.id ? 'put' : 'post',
data: data
}).then(res => {
this.setState({
showEdit: false,
data: {}
})
this.getCate()
}).catch(err => err)
}
render () {
const { cateList, showEdit, data } = this.state
return (
<div>
<div className='jimu-tools'>
<div className={styles.bars}>
<Button type='primary' onClick={this.handleEdit.bind(this, {})}>添加分类</Button>
</div>
</div>
<div className='jimu-container'>
{
cateList.length === 0 ? null : cateList.map(item => <Tag color={this.getColor()} key={item.id} onClick={e => this.handleEdit(item, e)}>{item.title}</Tag>)
}
</div>
<Modal
title='添加分类'
visible={showEdit}
onCancel={this.handleCancel}
onOk={this.handleSave}>
<Input value={data.title} onChange={e => this.handleChange(e)} />
</Modal>
</div>
)
}
}
export default hot(module)(PluginCate)
@import '~assets/styles/mixins.less';
.bars {
float: right;
button {
margin-left: 8px;
}
}
\ No newline at end of file
import React from 'react'
import { Form, Select, Button, Input, Tabs, Upload, Icon, message } from 'antd'
import styles from './index.less'
import { hot } from 'react-hot-loader'
import ajax from 'libs/ajax'
import { Controlled as CodeMirror } from 'react-codemirror2'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material.css'
import 'codemirror/mode/htmlembedded/htmlembedded'
import 'codemirror/mode/css/css'
import 'codemirror/mode/javascript/javascript'
const Option = Select.Option
const FormItem = Form.Item
const TabPane = Tabs.TabPane
const TextArea = Input.TextArea
class PluginEdit extends React.Component {
constructor (props) {
super(props)
this.state = {
formData: {
id: 0,
cateId: 0,
title: '',
summary: '',
html: '<p>这是一个示例内容</p>',
css: 'body{background-color:#fff;}',
js: 'console.log(opts);',
config: `[{"title": "测试配置项","type": "string","key": "demo"}]`,
thumb: ''
},
cateList: [],
pluginId: props.params.id
}
}
// 初始化数据
componentWillMount () {
this.getCateList()
if (this.state.pluginId !== '0') {
this.getComponentInfo(this.state.pluginId)
}
}
beforeUpload (file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('文件不能超过2mb')
}
return isLt2M
}
// 获取插件信息
getComponentInfo (pluginId) {
ajax({
url: 'basics/component',
params: {
id: pluginId
}
}).then(res => {
this.props.form.setFieldsValue({
cateId: res.lists[0].cateId
})
this.setState({
formData: res.lists[0]
})
}).catch(err => err)
}
// 保存插件信息
saveComponentInfo (data) {
ajax({
url: 'basics/component',
method: data.id ? 'put' : 'post',
data: data
}).then(res => {
message.info('保存成功')
this.props.router.push('plugin')
}).catch(err => err)
}
// 获取分类
getCateList () {
ajax({
url: 'basics/cate',
params: {
pageSize: 100
}
}).then(res => {
this.setState({
cateList: res.lists
})
}).catch(err => err)
}
// 获取分类名称
getCateName (cateList, id) {
let name = '请选择类别'
cateList.forEach(item => {
if (item.id === id) {
name = item.title
}
})
return name
}
// 保存
handleSubmit () {
this.props.form.validateFields(
(err) => {
if (!err) {
const formData = this.props.form.getFieldsValue()
formData.html = this.state.formData.html
formData.css = this.state.formData.css
formData.js = this.state.formData.js
formData.config = this.state.formData.config
formData.thumb = this.state.formData.thumb
if (this.state.pluginId !== '0') {
formData.id = parseInt(this.state.pluginId)
}
this.saveComponentInfo(formData)
}
}
)
}
// 编辑器改变
handCodeChange (key, value) {
this.setFormData(key, value)
}
// 设置表单值
setFormData (key, value) {
this.setState(prevState => {
let formData = prevState.formData
formData[key] = value
return {
formData
}
})
}
// 上传
handleUpload (res) {
if (res.file.response && res.file.response.code === 0) {
this.setFormData('thumb', res.file.response.data)
} else if (res.file.response && res.file.response.msg) {
message.error(res.file.response.msg)
}
}
render () {
const { getFieldDecorator } = this.props.form
const options = {
html: {
mode: 'xml',
htmlMode: true,
theme: 'material',
lineNumbers: true,
tabSize: 2,
autofocus: true
},
css: {
mode: 'css',
theme: 'material',
lineNumbers: true,
tabSize: 2
},
js: {
mode: 'javascript',
theme: 'material',
lineNumbers: true,
tabSize: 2
},
config: {
mode: 'javascript',
theme: 'material',
lineNumbers: true,
tabSize: 2
}
}
const formData = this.state.formData
const cateList = this.state.cateList
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
}
return (
<div>
<div className='jimu-tools'>
<div className={styles.bars}>
<Button type='primary' onClick={this.handleSubmit.bind(this)}>保存</Button>
</div>
</div>
<div className='jimu-container'>
<div>
<Form onSubmit={this.handleSubmit}>
<FormItem label='所属类别' {...formItemLayout}>
{getFieldDecorator('cateId', {
rules: [{ required: true, message: '请选择插件类别' }]
})(
<Select
placeholder='所属类别'
>
{
cateList.map(item => {
return (
<Option key={item.id} value={item.id}>{item.title}</Option>
)
})
}
</Select>
)}
</FormItem>
<FormItem label='插件名称' {...formItemLayout}>
{getFieldDecorator('title', {
initialValue: formData.title,
rules: [{ required: true, message: '请输入插件名称' }]
})(
<Input placeholder='请输入插件名称' />
)}
</FormItem>
<FormItem label='插件描述' {...formItemLayout}>
{getFieldDecorator('summary', {
initialValue: formData.summary,
rules: [{ required: true, message: '请输入插件描述' }]
})(
<TextArea rows={4} placeholder='请输入插件描述' />
)}
</FormItem>
<FormItem label='封面图' {...formItemLayout}>
<Upload name='file'
beforeUpload={this.beforeUpload}
onChange={this.handleUpload.bind(this)}
action='/api/system/upload'
showUploadList={false}
listType='picture'>
<Button>
<Icon type='upload' /> 上传缩略图
</Button>
</Upload>
{formData.thumb ? <div style={{ width: '300px', marginTop: '20px' }}><img style={{ width: '100%' }} alt='thumb' src={formData.thumb} /></div> : ''}
</FormItem>
<FormItem label='相关代码' {...formItemLayout}>
<Tabs defaultActiveKey='0' tabBarStyle={{ height: '40px' }}>
<TabPane tab='html' key='1'>
<CodeMirror value={formData.html} onBeforeChange={(editor, data, value) => this.handCodeChange('html', value)} options={options.html} />
</TabPane>
<TabPane tab='css' key='2'>
<CodeMirror value={formData.css} onBeforeChange={(editor, data, value) => this.handCodeChange('css', value)} options={options.css} />
</TabPane>
<TabPane tab='js' key='3'>
<CodeMirror value={formData.js} onBeforeChange={(editor, data, value) => this.handCodeChange('js', value)} options={options.js} />
</TabPane>
<TabPane tab='config' key='4'>
<CodeMirror value={formData.config} onBeforeChange={(editor, data, value) => this.handCodeChange('config', value)} options={options.config} />
</TabPane>
</Tabs>
</FormItem>
</Form>
</div>
</div>
</div>
)
}
}
const WrappedApp = Form.create()(PluginEdit)
export default hot(module)(WrappedApp)
@import '~assets/styles/mixins.less';
.bars {
float: right;
button {
margin-left: 8px;
}
}
\ No newline at end of file
import React from 'react'
import { Link } from 'react-router'
import { Button, Select, Row, Modal, message } from 'antd'
import styles from './index.less'
import { hot } from 'react-hot-loader'
import PluginItem from 'basics/components/pluginItem/index'
import ajax from 'libs/ajax'
const Option = Select.Option
class PluginList extends React.Component {
constructor (props) {
super(props)
this.state = {
cateList: [],
componentList: []
}
}
// 初始化数据
componentWillMount () {
this.getCateList()
this.getComponentList()
}
// 获取分类
getCateList () {
ajax({
url: 'basics/cate',
params: {
pageSize: 100
}
}).then(res => {
this.setState({
cateList: res.lists
})
}).catch(err => err)
}
// 获取列表
getComponentList (opts = {}) {
let { cateId } = opts
ajax({
url: 'basics/component',
params: {
pageSize: 100,
cateId: cateId || ''
}
}).then(res => {
this.setState({
componentList: res.lists
})
}).catch(err => err)
}
// 筛选
handleCateChange (value) {
this.getComponentList({
cateId: value
})
}
// 删除一个组件
pluginRemove (id) {
Modal.confirm({
title: '确定删除该插件?',
confirmLoading: true,
okType: 'danger',
onOk: () => {
return ajax({
url: `/basics/component`,
params: {
id
},
method: 'delete'
}).then(data => {
message.success('删除成功')
this.getComponentList()
}).catch(err => err)
}
})
}
render () {
const { cateList } = this.state
return (
<div>
<div className='jimu-tools'>
<Select
style={{ width: 120 }}
placeholder='全部类别'
onChange={this.handleCateChange.bind(this)}
>
<Option value={0} key={0}>全部类别</Option>
{cateList.map(item => <Option value={item.id} key={item.id}>{item.title}</Option>)}
</Select>
<div className={styles.bars}>
<Button type='primary'><Link to='/plugin/edit/0'>新建</Link></Button>
</div>
</div>
<div style={{ padding: '10px 0' }}>
<Row gutter={16}>
{
this.state.componentList.map((item) => {
return (
<PluginItem item={item} remove={this.pluginRemove} key={item.id} />
)
})
}
</Row>
</div>
</div>
)
}
}
export default hot(module)(PluginList)
@import '~assets/styles/mixins.less';
.bars {
float: right;
button {
margin-left: 8px;
}
}
.gutter {
margin-bottom: 30px;
& > div {
position: relative;
left: 50%;
transform: translate(-50%);
}
}
\ No newline at end of file
<html>
<head>
<title>积木平台</title>
</head>
<body>
<div id="app">
</div>
</body>
</html>
\ No newline at end of file
import React from 'react'
import {
render
} from 'react-dom'
import {
Router,
hashHistory
} from 'react-router'
import 'assets/styles/admin.less'
import routes from './routes'
import { LocaleProvider } from 'antd'
import zhCN from 'antd/lib/locale-provider/zh_CN'
render(<LocaleProvider locale={zhCN}><Router history={hashHistory} routes={routes} /></LocaleProvider>, document.getElementById('app'))
import app from './app'
import container from './container'
const routes = [
{
path: '/',
component: container,
getIndexRoute (partialNextState, callback) {
require.ensure([], function (require) {
callback(null, {
component: require('./containers/guide/index').default
})
})
},
childRoutes: [{
path: '/',
component: app,
childRoutes: [
{
path: 'activity',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/activity/list/index').default)
})
}
}, {
path: 'plugin',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/plugin/list/index').default)
})
}
}, {
path: 'plugin/edit/:id',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/plugin/edit/index').default)
})
}
}, {
path: 'plugin/cate',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/plugin/cate/index').default)
})
}
}, {
path: 'layout',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/layout/list/index').default)
})
}
}, {
path: 'layout/edit',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/layout/edit/index').default)
})
}
}
]
}, {
path: 'login',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/login/index').default)
})
}
}, {
path: 'activity/edit',
getComponents (nextState, callback) {
require.ensure([], function (require) {
callback(null, require('./containers/activity/edit/index').default)
})
}
}]
}
]
export default routes
import React, { Component } from 'react'
import loadjs from 'loadjs'
import uuidv4 from 'uuid/v4'
const uuidGen = () => uuidv4().split('-')[0]
const mapUUIDs = []
const mapOptions = {}
const mapCallbacks = {}
export default class AMap extends Component {
myRef = React.createRef()
initAMap = uuid => () => {
// 地图加载
var map = new window.AMap.Map(uuid, {
resizeEnable: true
})
// 定位
map.plugin('AMap.Geolocation', () => {
var geolocation = new window.AMap.Geolocation({
enableHighAccuracy: true, // 是否使用高精度定位,默认:true
timeout: 10000, // 超过10秒后停止定位,默认:无穷大
buttonOffset: new window.AMap.Pixel(10, 20), // 定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
zoomToAccuracy: true, // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
buttonPosition: 'RB',
scrollWheel: false
})
// map.addControl(geolocation)
geolocation.getCurrentPosition()
})
const onInit = mapCallbacks[uuid] && mapCallbacks[uuid].onInit
onInit && onInit(map)
}
// 主要因为amap脚本市入口脚本,没有真正的sdk
initWhenNotHasSDK = uuid => {
mapUUIDs.push(uuid)
}
initWhenHasSDK = uuid => {
this.initAMap(uuid)()
}
componentWillMount() {
const uuid = uuidGen()
this.uuid = uuid
mapOptions[uuid] = {}
mapCallbacks[uuid] = {}
}
componentWillUnmount() {
const index = mapUUIDs.findIndex(uuid => uuid === this.uuid)
mapUUIDs.splice(index, 1)
delete mapOptions[this.uuid]
delete mapCallbacks[this.uuid]
}
componentDidMount() {
// 增加dom的id
this.myRef.current.id = this.uuid
// 在实例上同步uuid
this.uuid = this.uuid
// 如果当前存在AMap SDK则直接初始化即可,如果未存在AMap SDK则加到初始化队列中
window.AMap ? this.initWhenHasSDK(this.uuid) : this.initWhenNotHasSDK(this.uuid)
// once模式加载SDK并声明回调初始化队列
if (!loadjs.isDefined('AMap')) {
loadjs(
[
`https://webapi.amap.com/maps?v=1.4.6&key=13b7051cc83cc78dcfa7bb094256139c&plugin=AMap.Marker,AMap.Autocomplete,AMap.PlaceSearch&callback=initAMaps`
],
'AMap'
)
window.initAMaps = () => {
mapUUIDs.forEach(mapuuid => this.initAMap(mapuuid)())
}
}
}
render() {
const { style, options, onInit } = this.props
mapOptions[this.uuid] = options
mapCallbacks[this.uuid].onInit = onInit
return <div ref={this.myRef} style={style} />
}
}
import React from 'react'
import { DragSource, DropTarget } from 'react-dnd'
function dragDirection(
dragIndex,
hoverIndex,
initialClientOffset,
clientOffset,
sourceClientOffset
) {
const hoverMiddleY = (initialClientOffset.y - sourceClientOffset.y) / 2
const hoverClientY = clientOffset.y - sourceClientOffset.y
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
return 'downward'
}
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
return 'upward'
}
}
class BodyRow extends React.Component {
render() {
const {
isOver,
connectDragSource,
connectDropTarget,
moveRow,
dragRow,
clientOffset,
sourceClientOffset,
initialClientOffset,
...restProps
} = this.props
const style = { ...restProps.style, cursor: 'move' }
let className = restProps.className
if (isOver && initialClientOffset) {
const direction = dragDirection(
dragRow.index,
restProps.index,
initialClientOffset,
clientOffset,
sourceClientOffset
)
if (direction === 'downward') {
className += ' drop-over-downward'
}
if (direction === 'upward') {
className += ' drop-over-upward'
}
}
return connectDragSource(
connectDropTarget(<tr {...restProps} className={className} style={style} />)
)
}
}
const rowSource = {
beginDrag(props) {
return {
index: props.index
}
}
}
const rowTarget = {
drop(props, monitor) {
const dragIndex = monitor.getItem().index
const hoverIndex = props.index
if (dragIndex === hoverIndex) {
return
}
props.moveRow(dragIndex, hoverIndex)
monitor.getItem().index = hoverIndex
}
}
const DragableBodyRow = DropTarget('row', rowTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
sourceClientOffset: monitor.getSourceClientOffset()
}))(
DragSource('row', rowSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
dragRow: monitor.getItem(),
clientOffset: monitor.getClientOffset(),
initialClientOffset: monitor.getInitialClientOffset()
}))(BodyRow)
)
export const tableComponents = {
body: {
row: DragableBodyRow
}
}
import React, { Component } from 'react'
import { Link } from 'react-router'
import { Breadcrumb } from 'antd'
import style from './index.less'
export default class breadcrumb extends Component {
itemRender (route, params, routes, paths) {
const last = routes.indexOf(route) === routes.length - 1
return last ? (route.breadcrumbName ? <span>{route.breadcrumbName}</span> : null) : (route.breadcrumbName ? <Link to={paths.join('/') || '/'}>{route.breadcrumbName}</Link> : null)
}
render () {
let { routes } = this.props
routes = routes.filter(el => el.path)
return (
<Breadcrumb className={style.breadcrumb} itemRender={this.itemRender} routes={routes} />
)
}
};
.breadcrumb{
line-height: 64px !important;
float: left;
}
\ No newline at end of file
'use strict'
import React from 'react'
import { SketchPicker } from 'react-color'
import { Popover } from 'antd'
class ColorPicker extends React.Component {
state = {
selectColor: '#ccc',
visible: false
}
handleClick = () => {
this.setState({ visible: true })
}
hide = () => {
this.setState({
visible: false
})
}
handleVisibleChange = visible => {
this.setState({ visible })
}
colorChanged = color => {
const rgba = color.rgb
this.setState({
selectColor: `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})`
})
}
handleChange = (color, event) => {
const rgba = color.rgb
this.props.onChange(`rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})`)
}
componentDidMount() {
this.setState({
selectColor: this.props.defaultValue || this.props.value
})
}
render() {
const selectDiv = {
width: '100%',
height: '32px',
border: '1px solid rgba(217, 217, 217, 0.5)',
borderRadius: 0,
cursor: 'pointer'
}
const { placement } = this.props
// this.props.size === 'small' && (selectDiv.height = '24px')
return (
<Popover
content={
<SketchPicker
width={238}
color={this.state.selectColor}
onChangeComplete={this.colorChanged}
onChange={this.handleChange}
/>
}
title={false}
placement={placement || 'left'}
trigger="click"
visible={this.state.visible}
onVisibleChange={this.handleVisibleChange}
overlayClassName="color-picker-popover"
>
<div
onClick={this.handleClick}
style={Object.assign({}, selectDiv, {
backgroundColor: this.state.selectColor
})}
/>
</Popover>
)
}
}
export default ColorPicker
import React from 'react'
import { Icon, Input, Tooltip, InputNumber, Select } from 'antd'
import Style from './index.less'
import { observer } from 'mobx-react'
const { Option, OptGroup } = Select
@observer
export default class CommonEditor extends React.Component {
constructor(props) {
super(props)
this.state = {
isEditing: false
}
}
handleChange = e => {
if (this.props.onChange) {
this.props.onChange(e.target.value)
}
}
handleSelect = value => {
if (this.props.onChange) {
this.props.onChange(value)
}
}
isEditing = e => {
e.stopPropagation()
this.setState({
isEditing: true
})
}
handleComplate = () => {
this.setState({
isEditing: false
})
}
getValueText = value => {
let valueText
const { type, options } = this.props
if (type === 'select') {
let itemFined = options.find(item => item.value === value)
valueText = itemFined ? itemFined.text : this.props.initText
} else if (type === 'select-group') {
let keys = Object.keys(options)
for (let i = 0; i < keys.length; i++) {
const obj = options[keys[i]].find(item => item.value === value)
if (obj) {
valueText = obj.text
break
}
}
} else {
valueText = value
}
return valueText
}
render() {
const { value, style, className, step, min, max, disabledTooltip, precision } = this.props
const { isEditing } = this.state
return (
<div
style={Object.assign({}, style, { width: this.props.width })}
className={`${Style.item} ${className}`}
onClick={this.isEditing}
>
{isEditing || (
<Tooltip title={this.getValueText(value)} {...(disabledTooltip ? { visible: false } : {})}>
<span style={{ maxWidth: `calc(100% - 14px)` }} className={Style.editor_wrap}>
{this.getValueText(value)}
</span>
</Tooltip>
)}
{isEditing || <Icon type="edit" className={Style.icon} />}
{isEditing &&
(() => {
const { type, value, options } = this.props
switch (type) {
case 'input':
return (
<Input
size="small"
value={value}
onChange={this.handleChange}
onPressEnter={this.handleComplate}
onBlur={this.handleComplate}
autoFocus
/>
)
case 'inputNumber':
return (
<InputNumber
size="small"
value={value}
onChange={this.handleSelect}
onBlur={this.handleComplate}
autoFocus
precision={precision || 1}
step={step || 1}
min={min || 0}
max={max || 1}
/>
)
case 'select':
return (
<Select
size="small"
value={value}
onChange={this.handleSelect}
onBlur={this.handleComplate}
style={{ width: this.props.width }}
autoFocus
>
{options.map(item => {
return (
<Option value={item.value} key={item.value}>
{item.text}
</Option>
)
})}
</Select>
)
case 'select-group':
return (
<Select
size="small"
value={value}
onChange={this.handleSelect}
onBlur={this.handleComplate}
style={{ width: this.props.width }}
autoFocus
>
{Object.keys(options).map((label, index) => {
return (
<OptGroup label={label} key={index}>
{options[label].map(item => (
<Option value={item.value} key={item.value}>
{item.text}
</Option>
))}
</OptGroup>
)
})}
</Select>
)
default:
return (
<Input
size="small"
value={value}
onChange={this.handleChange}
onPressEnter={this.handleComplate}
onBlur={this.handleComplate}
autoFocus
/>
)
}
})()}
</div>
)
}
}
.editor_wrap
{
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
display: inline-block;
}
.item {
cursor: pointer;
display: flex;
align-items: center;
.icon {
position: relative;
visibility: hidden;
}
}
.item:hover {
.icon {
visibility: visible;
}
}
\ No newline at end of file
import React, { Component } from 'react'
import { Modal, Icon } from 'antd'
import styles from './index.less'
import cn from 'classnames'
import { hashHistory } from 'react-router'
export default class ExitNotify extends Component {
constructor() {
super(...arguments)
this.state = {
modalVisible: false
}
}
show = () => {
this.setState({
modalVisible: true
})
}
cancel = () => {
this.setState({
modalVisible: false
})
}
handleConfirm = () => {
this.props.confirm()
}
handleCancel = () => {
this.cancel()
hashHistory.push('/home')
}
render() {
const { onCancel, content, name } = this.props
const TriggerIns = content({
show: this.show
})
return (
<React.Fragment>
{TriggerIns}
<Modal
width={400}
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', height: '100%' }}>
退出
<Icon type="close" onClick={this.cancel} style={{ cursor: 'pointer' }} />
</div>
}
footer={null}
visible={this.state.modalVisible}
wrapClassName="newSiteModal"
onCancel={onCancel}
closable={false}
destroyOnClose
>
<div className={styles['wrapper']}>
<div className={styles['name']}>{name} 还有更新尚未保存,您确定要退出吗?</div>
<div className={styles['btns']}>
<div className={cn(styles['cancel'], styles['button'])} onClick={this.handleCancel}>
不保存
</div>
<div className={cn(styles['confirm'], styles['button'])} onClick={this.handleConfirm}>
保存并退出
</div>
</div>
</div>
</Modal>
</React.Fragment>
)
}
}
@import '../../config/common.less';
.wrapper {
position: relative;
height: 142px;
.name {
padding-top: 30px;
color: #666;
font-size: 14px;
font-weight: bold;
}
.btns {
position: absolute;
right: 20px;
bottom: 20px;
display: flex;
.button {
width: 100px;
font-size:14px;
line-height: 32px;
border-radius: 2px;
text-align: center;
cursor: pointer;
user-select: none;
}
.cancel {
color: #666666;
background: rgba(243, 243, 243, 1);
}
.confirm {
background-color: @primary-color;
color: #fff;
margin-left: 16px;
transition: all .1s ease;
}
}
}
\ No newline at end of file
/**
* 目前因为没有footer需求,用于今后拓展
*/
import React from 'react'
import { Layout } from 'antd'
import styles from './index.less'
const { Footer: AntdFooter } = Layout
class Footer extends React.Component {
render () {
return (
<AntdFooter className={styles.footer} />
)
};
};
export default Footer
.footer {
padding: 0!important;
}
\ No newline at end of file
import React from 'react'
import { inject, observer } from 'mobx-react'
import common from 'pro/lib/common'
import utils from 'pro/lib/utils'
import { Layout } from 'antd'
import Breadcrumb from 'pro/components/breadcrumb'
import styles from './index.less'
const { Header: AntdHeader } = Layout
@inject('userStore')
@observer
class Header extends React.Component {
handleQuit = () => {
common.fetch('/sso/outLogin').then(res => {
if (res.success) {
utils.logout()
}
})
};
render () {
const { routes } = this.props
const { name } = this.props.userStore.userInfo
return (
<AntdHeader className={styles.header}>
<div className={styles.logo} onClick={() => utils.jumpTo('/workplace/home')}>
推啊奥利奥
</div>
<Breadcrumb routes={routes} />
<div className={styles.user_info_wrap}>
<span>{name}</span>
<span onClick={this.handleQuit}>退出</span>
</div>
</AntdHeader>
)
}
}
export default Header
.header {
padding: 0 20px!important;
font-weight: 500;
}
.logo {
width: 205px;
font-size: 18px;
color: #fff;
float: left;
cursor: pointer;
}
.user_info_wrap {
float: right;
span {
padding: 5px 10px;
color: #fff;
&:first-child {
border-right: 1px solid rgba(255,255,255,.45);
}
&:last-child {
cursor: pointer;
}
}
}
\ No newline at end of file
import React, { Component } from 'react'
import TuiaIcon from 'wp/constructors/fc/icon'
import ReactCSS from 'reactcss'
import classes from './index.less'
export default class Input extends Component {
constructor() {
super(...arguments)
this.state = {
isFocus: false
}
}
handleClick = () => {
this.setState({
isFocus: true
})
}
handleBlur = () => {
this.setState({
isFocus: false
})
}
render() {
const { isFocus } = this.state
const { onChange, value, placeholder } = this.props
const commonStyle = {
transition: 'all .2s ease-in-out'
}
const styles = ReactCSS(
{
default: {
'input-wrapper': {
backgroundColor: 'transparent',
display: 'flex',
alignItems: 'flex-start',
height: 15
},
icon: {
color: '#6A6B79',
fontSize: 14,
marginRight: 7
},
input: {
backgroundColor: 'transparent',
width: 28,
borderBottomColor: 'transparent',
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
paddingBottom: 4,
...commonStyle
}
},
isFocus: {
input: {
width: 160,
borderBottomColor: '#9E9EAE'
}
}
},
{
isFocus
}
)
return (
<div style={styles['input-wrapper']} onClick={this.handleClick}>
<TuiaIcon type="icon-menu_search" style={styles['icon']} />
<input
placeholder={!isFocus ? '搜索' : placeholder || '搜索'}
value={value}
onChange={onChange}
style={styles['input']}
className={classes['input']}
onBlur={!value ? this.handleBlur : undefined}
/>
</div>
)
}
}
.input::-webkit-input-placeholder {
color: #999;
font-size: 14px;
}
\ No newline at end of file
import React, { Component } from 'react'
import styles from './index.less'
import { Icon } from 'antd'
export default class JMInput2 extends Component {
constructor() {
super(...arguments)
this.state = {
isFocus: false,
isHover: false
}
this.ref = React.createRef()
}
handleMouseEnter = () => {
this.setState({
isHover: true
})
}
handleMouseLeave = () => {
this.setState({
isHover: false
})
}
handleFocus = () => {
this.setState({
isFocus: true
})
}
handleBlur = () => {
this.setState({
isFocus: false
})
}
handleClear = () => {
this.props.onChange({ target: { value: '' } })
this.ref && this.ref.current.focus()
}
render() {
const { isFocus, isHover } = this.state
const { label, value, onChange, maxLength, placeholder, autoFocus } = this.props
return (
<div className={styles['jimu-input-2']}>
<div className={styles['label']}>{label}</div>
<div
className={styles['body']}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
>
<input
className={styles['input']}
type="text"
placeholder={placeholder}
maxLength={maxLength}
value={value}
onChange={onChange}
autoFocus={autoFocus}
ref={this.ref}
/>
<div className={styles['underline']} />
<div className={styles['suffix']}>
{value.length === 0 ? (
<div>{maxLength}</div>
) : isHover ? (
<Icon
type="close-circle"
theme="outlined"
onClick={this.handleClear}
/>
) : isFocus ? (
<div>
{value.length}/{maxLength}
</div>
) : null}
</div>
</div>
</div>
)
}
}
@import '../../config/common.less';
@bottom-dis: 12px;
@right-dis: 13px;
.jimu-input-2 {
position: relative;
.label {
color: #333333;
font-size: 12px;
line-height: 24px;
}
.body {
position: relative;
.suffix {
position: absolute;
right: @right-dis;
bottom: @bottom-dis - 1px;
line-height: 1;
color: #666;
font-size: 12px;
i {
font-size: 14px;
color: #999;
cursor: pointer;
}
}
.input {
padding: 9px @right-dis 9px 8px;
border-bottom: 1px solid #e5e5e5;
line-height: 1;
width: 100%;
color: #333;
font-size: 14px;
font-weight: bold;
&::-webkit-input-placeholder {
font-size: 14px;
color: #999;
}
}
.underline {
width: 100%;
overflow: hidden;
position: absolute;
bottom: 0;
visibility: hidden;
&:before {
content: '';
display: block;
transform: rotate(1deg);
transform-origin: center;
height: 1px;
background-color: @primary-color;
transition: all .2s linear;
}
}
.input:focus ~ .underline {
visibility: visible;
&:before {
transform: rotate(0deg);
}
}
}
}
/**
* 数据加载提示框
*/
import styles from './index.less'
let loadingNum = 0
const Loading = (function () {
return {
open (text = '正在处理,请稍等...') {
loadingNum++
if (loadingNum > 1) {
return
}
// 生成dom
const doc = window.document
this.node = doc.createElement('div')
this.node.className = styles.my_loading
let node2 = doc.createElement('div')
node2.className = styles.my_loading_wrap
this.node.appendChild(node2)
let node3 = doc.createElement('section')
node3.className = styles.my_loading_content
node2.appendChild(node3)
let node4 = doc.createElement('i')
node4.className = 'anticon anticon-spin anticon-loading ' + styles.my_loading_img
let node5 = doc.createElement('span')
node5.className = styles.my_loading_text
node5.innerHTML = text
node3.appendChild(node4)
node3.appendChild(node5)
doc.body.appendChild(this.node)
},
close () {
loadingNum--
if (loadingNum > 0) {
return
}
this.node && window.document.body.removeChild(this.node)
}
}
})()
export default Loading
.my_loading
{
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .8);
.my_loading_wrap
{
position: relative;
top: 50%;
left: 50%;
overflow: hidden;
width: 290px;
height: 108px;
margin-top: -54px;
margin-left: -145px;
border-radius: 10px;
background-color: #fff;
.my_loading_content
{
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.my_loading_img
{
font-size: 20px;
margin-right: 5px;
}
.my_loading_text
{
margin-left: 5px;
font-size: 16px;
font-weight: bold;
}
}
}
}
\ No newline at end of file
import React from 'react'
import { Link } from 'react-router'
import Permission from 'pro/lib/permission'
import {
Layout,
Menu as AntdMenu,
Icon
} from 'antd'
import MENU from './menu-data'
import styles from './index.less'
const { SubMenu } = AntdMenu
const { Sider } = Layout
class Menu extends React.Component {
constructor (props) {
super(props)
this.state = {
collapsed: false
}
this.defaultOpenKeys = []
}
componentWillMount () {
// 设置默认打开全部的一级菜单
MENU.map((item) => {
this.defaultOpenKeys.push(item.key)
})
}
// 菜单伸缩
_toggle = () => {
this.setState((preState, props) => ({
collapsed: !preState.collapsed
}))
}
// 生成一级菜单
_createSubMenu = () => {
return MENU.map((item) => {
const { icon, title, key, auth } = item
const subMenuTitle = (
<span>
<Icon type={icon} />
<span>{title}</span>
</span>
)
return Permission.ifRender(auth) ? (
<SubMenu
key={key}
title={subMenuTitle}
>
{ this._createItemMenu(item) }
</SubMenu>
) : null
})
}
// 生成二级菜单
_createItemMenu = (subMenuData) => {
const { children } = subMenuData
return children.map((subItem) => {
const { auth, key, route, title } = subItem
return Permission.ifRender(auth) ? (
<AntdMenu.Item key={key}>
<Link to={route}>{title}</Link>
</AntdMenu.Item>
) : null
})
}
// 获取当前路由
getCurrentPaths = () => {
const { currentLocation = {} } = this.props
const { pathname = '' } = currentLocation
const paths = pathname.split('/')
paths.shift()
return paths
}
render () {
const currentPaths = this.getCurrentPaths()
const defaultOpenKeys = MENU.map(el => el.key)
const selectedKeys = [currentPaths[currentPaths.length - 1]]
const { collapsed } = this.state
return (
<Sider
width={180}
trigger={null}
className={styles.slider}
collapsible
collapsed={collapsed}
>
<div className={styles.menu_top} onClick={this._toggle}>
<Icon
className='trigger'
type={collapsed ? 'menu-unfold' : 'menu-fold'}
/>
</div>
<AntdMenu
mode='inline'
selectedKeys={selectedKeys}
defaultOpenKeys={defaultOpenKeys}
className={styles.menu}
>
{this._createSubMenu()}
</AntdMenu>
</Sider>
)
}
}
export default Menu
.menu_top {
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
cursor: pointer;
}
.slider {
background: #fff!important;
overflow-y: auto;
}
.menu {
border-right: 0;
}
\ No newline at end of file
const MENU = [
{
title: 'example',
icon: 'setting',
key: 'example',
auth: 'example',
children: [
{
title: 'exampleChild',
key: 'exampleChild',
auth: 'exampleChild',
route: 'workplace/example/exampleChild'
}
]
}
]
export default MENU
import React, { Component } from 'react'
import { Modal, Icon } from 'antd'
import TuiaIcon from 'wp/constructors/fc/icon'
import cn from 'classnames'
import styles from './index.less'
import JMInput2 from 'pro/components/jm-input-2'
import { hashHistory } from 'react-router'
import { sceneGen } from 'wp/constructors/other-default'
import { removeLocalStorage, setLocalStorage } from 'wp/utils/helper'
// const { Option } = Select
export default class Preview extends Component {
constructor() {
super(...arguments)
this.state = {
scene: sceneGen(),
label: {
id: 1
},
hotLabels: [
{
name: '金融投资'
},
{
name: '旅游宣传'
},
{
name: '女性时尚'
},
{
name: '艺术培训教育'
}
]
}
}
show = () => {
this.setState({
modalVisible: true
})
}
cancel = () => {
this.setState({
modalVisible: false
})
}
changeWidth = (width, height = 603) => {
this.setState({
scene: Object.assign({}, this.state.scene, {
width: width + 'px',
height: height + 'px'
})
})
}
handleChangeProp = (key, value) => {
this.setState({
scene: Object.assign({}, this.state.scene, {
[key]: value
})
})
}
handleSelect = value => {
this.setState({
label: {
id: value
}
})
}
handleConfirm = () => {
removeLocalStorage('pageId')
// const timer = setTimeout(() => {
// store.WPHookStore.walkHooks('onSceneInnerWidthChangeEnd', {
// width: getNumber(this.state.scene.width)
// })
// store.WPSceneStore.init(this.state.scene)
// clearTimeout(timer)
// }, 100)
// 通过localStorage做组件通信
setLocalStorage('createScene', this.state.scene)
hashHistory.push('/workplace')
}
render() {
const { width, name, title } = this.state.scene
const { label } = this.state
const { onCancel, content } = this.props
const TriggerIns = content({
show: this.show
})
const isCompleted = Boolean(width && name && title && label.id)
return (
<React.Fragment>
{TriggerIns}
<Modal
width={633}
title={
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%'
}}
>
新建站点
<Icon
type="close"
onClick={this.cancel}
style={{ cursor: 'pointer' }}
/>
</div>
}
footer={null}
visible={this.state.modalVisible}
wrapClassName="newSiteModal"
onCancel={onCancel}
closable={false}
destroyOnClose
mask={false}
>
<div className={styles['wrapper']}>
<div className={styles['left']}>
<div className={styles['title']}>您的设计稿尺寸</div>
<div>
<div
className={cn(styles['mobile-model'], {
[styles['active']]: width === '320px'
})}
onClick={this.changeWidth.bind(this, 320, 603)}
>
<TuiaIcon type="icon-system_phone" />
<div>iPhone 5/5c/5s</div>
<div>640x1136 px</div>
</div>
<div
className={cn(styles['mobile-model'], {
[styles['active']]: width === '375px'
})}
onClick={this.changeWidth.bind(this, 375, 734)}
>
<TuiaIcon type="icon-system_phone1" />
<div>iPhone X</div>
<div>750x1624 px</div>
</div>
</div>
</div>
<div className={styles['gutter']} />
<div className={styles['right']}>
<JMInput2
value={name}
onChange={e => this.handleChangeProp('name', e.target.value)}
label="页面名称"
placeholder="必填,不能超过15个字符"
maxLength={15}
autoFocus
/>
<div style={{ height: 7 }} />
<JMInput2
value={title}
onChange={e => this.handleChangeProp('title', e.target.value)}
label="网页标题"
placeholder="必填,不能超过15个字符"
maxLength={15}
/>
{/* <div className={styles['all-labels']}>
<div className={styles['name']}>行业标签</div>
<Select
value={label.id}
placeholder="非必填,选择行业标签"
style={{ width: '100%' }}
onSelect={value => this.handleSelect(value)}
>
<Option value={1}>贷款</Option>
<Option value={2}>信用卡</Option>
<Option value={3}>保险</Option>
<Option value={4}>p2p</Option>
</Select>
</div>
<div className={styles['hot-labels']}>
<div className={styles['name']}>热门标签</div>
<div className={styles['labels-wrapper']}>
{this.state.hotLabels.map((item, idx) => {
return (
<div className={styles['item']} key={idx}>
#{item.name}#
</div>
)
})}
</div>
</div> */}
</div>
<div className={styles['btns']}>
<div
className={cn(styles['cancel'], styles['button'])}
onClick={this.cancel}
>
取消
</div>
<div
className={cn(styles['confirm'], styles['button'], {
[styles['btn-active']]: isCompleted
})}
onClick={isCompleted ? this.handleConfirm : undefined}
>
创建
</div>
</div>
</div>
</Modal>
</React.Fragment>
)
}
}
This diff is collapsed.
/*
* @Author: 周成
* @Date: 2018-07-11 17:53:20
* @Last Modified by: 周成
* @Last Modified time: 2018-07-11 18:07:03
*/
/**
* 文档参考:https://github.com/zpao/qrcode.react
* visible: 组件显示状态
* onCancel: 组件关闭事件
*/
import React, { Component } from 'react'
import QRCode from 'qrcode.react'
import { Modal } from 'antd'
import styles from './index.less'
class QrCode extends Component {
onCancel = () => {
this.props.onCancel && this.props.onCancel()
}
render () {
const { visible, value } = this.props
return (
<Modal
title='预览'
visible={visible}
onCancel={this.onCancel}
footer={false}
width={232}
>
<div className={styles.qrcode}>
<QRCode {...this.props} />
<div className='mt20'>
<a href={value} target='_blank' onClick={this.onCancel}>点击预览</a>
</div>
</div>
</Modal>
)
}
}
export default QrCode
@import '~src/styles/global.less';
.qrcode{
text-align: center;
a{
color: @primary-color;
}
}
\ No newline at end of file
This diff is collapsed.
.swiper-container {
width: 100%;
height: 100%;
.swiper-slide {
text-align: center;
font-size: 18px;
background: transparent;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
}
}
This diff is collapsed.
.relate-ad {
.status-wrapper {
display: flex;
align-items: center;
>div {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 6px;
}
.green {
background-color: #7ED321;
}
.red {
background-color: #EB1C1C;
}
.gray {
background-color: #c2c2c2;
}
}
.toolbar {
display: flex;
margin-bottom: 10px;
margin-top: 10px;
.search-input {
width: 240px;
}
.search-button {
width: 60px;
margin-left: 8px;
}
}
.footer {
display: flex;
justify-content: space-between;
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
/*
* @Author: chenhaojie
* @Date: 2018-12-17 11:46:57
* @LastEditors: chenhaojie
* @LastEditTime: 2018-12-21 17:34:04
* @Email: chenhaojie@tuia.cn
* @Description:
*/
export const SitePrefix = '/java'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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