基础架构, 第一版发布
16
.editorconfig
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
7
.prettierignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
**/*.md
|
||||||
|
**/*.svg
|
||||||
|
**/*.ejs
|
||||||
|
**/*.html
|
||||||
|
package.json
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
11
.prettierrc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ".prettierrc",
|
||||||
|
"options": { "parser": "json" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
.umirc.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { defineConfig } from 'umi';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dynamicImport: {
|
||||||
|
loading: '@/components/LoadingCp',
|
||||||
|
},
|
||||||
|
dva: {
|
||||||
|
immer: true
|
||||||
|
},
|
||||||
|
antd: {},
|
||||||
|
sass: {
|
||||||
|
implementation: require('node-sass'),
|
||||||
|
},
|
||||||
|
title: '趣谈前端-h5-visible-tool',
|
||||||
|
exportStatic: {},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
// component: '@/layouts',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/editor',
|
||||||
|
component: '../pages/editor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/preview',
|
||||||
|
component: '../pages/editor/preview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/prevH5',
|
||||||
|
component: '../pages/editor/preH5',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
"primary-color": "#2F54EB",
|
||||||
|
// "btn-primary-bg": "#2F54EB"
|
||||||
|
},
|
||||||
|
extraBabelPlugins: [
|
||||||
|
['import', { libraryName: "zarm", style: true }],
|
||||||
|
],
|
||||||
|
// sass: {},
|
||||||
|
alias: {
|
||||||
|
components: path.resolve(__dirname, 'src/components/'),
|
||||||
|
utils: path.resolve(__dirname, 'src/utils/'),
|
||||||
|
assets: path.resolve(__dirname, 'src/assets/')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
29315
npm-shrinkwrap.json
generated
Normal file
49
package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "umi dev",
|
||||||
|
"build": "umi build",
|
||||||
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||||
|
"test": "umi-test",
|
||||||
|
"test:coverage": "umi-test --coverage"
|
||||||
|
},
|
||||||
|
"gitHooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,less,md,json}": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.ts?(x)": [
|
||||||
|
"prettier --parser=typescript --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/charts": "^0.9.9",
|
||||||
|
"@ant-design/icons": "^4.2.1",
|
||||||
|
"@umijs/plugin-sass": "^1.1.1",
|
||||||
|
"@umijs/preset-react": "1.x",
|
||||||
|
"@umijs/test": "^3.0.12",
|
||||||
|
"antd": "^4.2.3",
|
||||||
|
"antd-img-crop": "^3.10.0",
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"babel-plugin-import": "^1.13.0",
|
||||||
|
"file-saver": "^2.0.2",
|
||||||
|
"lint-staged": "^10.0.7",
|
||||||
|
"node-sass": "^4.14.1",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"qrcode.react": "^1.0.0",
|
||||||
|
"react": "^16.12.0",
|
||||||
|
"react-color": "^2.18.1",
|
||||||
|
"react-dnd": "^11.1.3",
|
||||||
|
"react-dnd-html5-backend": "^11.1.3",
|
||||||
|
"react-dom": "^16.12.0",
|
||||||
|
"react-draggable": "^4.4.3",
|
||||||
|
"react-grid-layout": "^1.0.0",
|
||||||
|
"sass-loader": "^9.0.3",
|
||||||
|
"umi": "^3.0.12",
|
||||||
|
"video-react": "^0.14.1",
|
||||||
|
"yorkie": "^2.0.0",
|
||||||
|
"zarm": "^2.5.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/.DS_Store
vendored
Normal file
51
src/.umi/core/devScripts.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
if (window.g_initWebpackHotDevClient) {
|
||||||
|
function tryApplyUpdates(onHotUpdateSuccess?: Function) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (!module.hot) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUpdateAvailable() {
|
||||||
|
// @ts-ignore
|
||||||
|
return window.g_getMostRecentCompilationHash() !== __webpack_hash__;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is update available?
|
||||||
|
// @ts-ignore
|
||||||
|
if (!isUpdateAvailable() || module.hot.status() !== 'idle') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApplyUpdates(err: Error | null, updatedModules: any) {
|
||||||
|
if (err || !updatedModules || window.g_getHadRuntimeError()) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onHotUpdateSuccess?.();
|
||||||
|
|
||||||
|
if (isUpdateAvailable()) {
|
||||||
|
// While we were updating, there was a new update! Do it again.
|
||||||
|
tryApplyUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
module.hot.check(true).then(
|
||||||
|
function (updatedModules: any) {
|
||||||
|
handleApplyUpdates(null, updatedModules);
|
||||||
|
},
|
||||||
|
function (err: Error) {
|
||||||
|
handleApplyUpdates(err, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.g_initWebpackHotDevClient({
|
||||||
|
tryApplyUpdates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
26
src/.umi/core/history.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { createBrowserHistory } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
"basename": "/"
|
||||||
|
};
|
||||||
|
if ((<any>window).routerBase) {
|
||||||
|
options.basename = (<any>window).routerBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove initial history because of ssr
|
||||||
|
let history: any = process.env.__IS_SERVER ? null : createBrowserHistory(options);
|
||||||
|
export const createHistory = (hotReload = false) => {
|
||||||
|
if (!hotReload) {
|
||||||
|
history = createBrowserHistory(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return history;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通常仅微前端场景需要调用这个 API
|
||||||
|
export const setCreateHistoryOptions = (newOpts: any = {}) => {
|
||||||
|
options = { ...options, ...newOpts };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { history };
|
||||||
8
src/.umi/core/plugin.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Plugin } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
|
||||||
|
|
||||||
|
const plugin = new Plugin({
|
||||||
|
validKeys: ['modifyClientRenderOpts','patchRoutes','rootContainer','render','onRouteChange','dva','getInitialState','request',],
|
||||||
|
});
|
||||||
|
|
||||||
|
export { plugin };
|
||||||
210
src/.umi/core/pluginConfig.d.ts
vendored
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/** Created by Umi Plugin **/
|
||||||
|
|
||||||
|
export interface IConfigFromPlugins {
|
||||||
|
routes?: {
|
||||||
|
/**
|
||||||
|
* Any valid URL path
|
||||||
|
*/
|
||||||
|
path?: string;
|
||||||
|
/**
|
||||||
|
* A React component to render only when the location matches.
|
||||||
|
*/
|
||||||
|
component?: string | (() => any);
|
||||||
|
wrappers?: string[];
|
||||||
|
/**
|
||||||
|
* navigate to a new location
|
||||||
|
*/
|
||||||
|
redirect?: string;
|
||||||
|
/**
|
||||||
|
* When true, the active class/style will only be applied if the location is matched exactly.
|
||||||
|
*/
|
||||||
|
exact?: boolean;
|
||||||
|
routes?: any[];
|
||||||
|
[k: string]: any;
|
||||||
|
}[];
|
||||||
|
history?: {
|
||||||
|
type?: "browser" | "hash" | "memory";
|
||||||
|
options?: {};
|
||||||
|
};
|
||||||
|
polyfill?: {
|
||||||
|
imports?: string[];
|
||||||
|
};
|
||||||
|
alias?: {};
|
||||||
|
analyze?: {
|
||||||
|
analyzerMode?: "server" | "static" | "disabled";
|
||||||
|
analyzerHost?: string;
|
||||||
|
analyzerPort?: any;
|
||||||
|
openAnalyzer?: boolean;
|
||||||
|
generateStatsFile?: boolean;
|
||||||
|
statsFilename?: string;
|
||||||
|
logLevel?: "info" | "warn" | "error" | "silent";
|
||||||
|
defaultSizes?: "stat" | "parsed" | "gzip";
|
||||||
|
[k: string]: any;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* postcss autoprefixer, default flexbox: no-2009
|
||||||
|
*/
|
||||||
|
autoprefixer?: {};
|
||||||
|
base?: string;
|
||||||
|
chainWebpack?: () => any;
|
||||||
|
chunks?: string[];
|
||||||
|
/**
|
||||||
|
* more css-loader options see https://webpack.js.org/loaders/css-loader/#options
|
||||||
|
*/
|
||||||
|
cssLoader?: {
|
||||||
|
url?: boolean | (() => any);
|
||||||
|
import?: boolean | (() => any);
|
||||||
|
modules?: boolean | string | {};
|
||||||
|
sourceMap?: boolean;
|
||||||
|
importLoaders?: number;
|
||||||
|
onlyLocals?: boolean;
|
||||||
|
esModule?: boolean;
|
||||||
|
localsConvention?: "asIs" | "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly";
|
||||||
|
};
|
||||||
|
cssModulesTypescriptLoader?: {
|
||||||
|
mode?: "emit" | "verify";
|
||||||
|
};
|
||||||
|
cssnano?: {};
|
||||||
|
copy?: string[];
|
||||||
|
define?: {};
|
||||||
|
devScripts?: {};
|
||||||
|
/**
|
||||||
|
* devServer configs
|
||||||
|
*/
|
||||||
|
devServer?: {
|
||||||
|
/**
|
||||||
|
* devServer port, default 8000
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
|
host?: string;
|
||||||
|
https?:
|
||||||
|
| {
|
||||||
|
key?: string;
|
||||||
|
cert?: string;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
| boolean;
|
||||||
|
headers?: {};
|
||||||
|
writeToDisk?: boolean | (() => any);
|
||||||
|
[k: string]: any;
|
||||||
|
};
|
||||||
|
devtool?: string;
|
||||||
|
/**
|
||||||
|
* Code splitting for performance optimization
|
||||||
|
*/
|
||||||
|
dynamicImport?: {
|
||||||
|
/**
|
||||||
|
* loading the component before loaded
|
||||||
|
*/
|
||||||
|
loading?: string;
|
||||||
|
};
|
||||||
|
exportStatic?: {
|
||||||
|
htmlSuffix?: boolean;
|
||||||
|
dynamicRoot?: boolean;
|
||||||
|
/**
|
||||||
|
* extra render paths only enable in ssr
|
||||||
|
*/
|
||||||
|
extraRoutePaths?: () => any;
|
||||||
|
};
|
||||||
|
externals?: {} | string | (() => any);
|
||||||
|
extraBabelPlugins?: any[];
|
||||||
|
extraBabelPresets?: any[];
|
||||||
|
extraPostCSSPlugins?: any[];
|
||||||
|
/**
|
||||||
|
* fork-ts-checker-webpack-plugin options see https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options
|
||||||
|
*/
|
||||||
|
forkTSChecker?: {
|
||||||
|
async?: boolean;
|
||||||
|
typescript?: boolean | {};
|
||||||
|
eslint?: {};
|
||||||
|
issue?: {};
|
||||||
|
formatter?: string | {};
|
||||||
|
logger?: {};
|
||||||
|
[k: string]: any;
|
||||||
|
};
|
||||||
|
hash?: boolean;
|
||||||
|
ignoreMomentLocale?: boolean;
|
||||||
|
inlineLimit?: number;
|
||||||
|
lessLoader?: {};
|
||||||
|
manifest?: {
|
||||||
|
fileName?: string;
|
||||||
|
publicPath?: "";
|
||||||
|
basePath?: string;
|
||||||
|
writeToFileEmit?: boolean;
|
||||||
|
};
|
||||||
|
mountElementId?: "";
|
||||||
|
mpa?: {};
|
||||||
|
nodeModulesTransform?: {
|
||||||
|
type?: "all" | "none";
|
||||||
|
exclude?: string[];
|
||||||
|
};
|
||||||
|
outputPath?: "";
|
||||||
|
plugins?: string[];
|
||||||
|
postcssLoader?: {};
|
||||||
|
presets?: string[];
|
||||||
|
proxy?: {};
|
||||||
|
publicPath?: string;
|
||||||
|
runtimePublicPath?: boolean;
|
||||||
|
ssr?: {
|
||||||
|
/**
|
||||||
|
* remove window.g_initialProps in html, to force execing Page getInitialProps functions
|
||||||
|
*/
|
||||||
|
forceInitial?: boolean;
|
||||||
|
/**
|
||||||
|
* disable serve-side render in umi dev mode.
|
||||||
|
*/
|
||||||
|
devServerRender?: boolean;
|
||||||
|
mode?: "stream" | "string";
|
||||||
|
/**
|
||||||
|
* static markup in static site
|
||||||
|
*/
|
||||||
|
staticMarkup?: boolean;
|
||||||
|
};
|
||||||
|
singular?: boolean;
|
||||||
|
styleLoader?: {};
|
||||||
|
targets?: {};
|
||||||
|
terserOptions?: {};
|
||||||
|
theme?: {};
|
||||||
|
runtimeHistory?: {};
|
||||||
|
favicon?: string;
|
||||||
|
headScripts?: any[];
|
||||||
|
links?: any[];
|
||||||
|
metas?: any[];
|
||||||
|
scripts?: any[];
|
||||||
|
styles?: any[];
|
||||||
|
title?: string;
|
||||||
|
mock?: {
|
||||||
|
exclude?: string[];
|
||||||
|
};
|
||||||
|
antd?: {
|
||||||
|
dark?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
|
config?: {};
|
||||||
|
};
|
||||||
|
dva?: {
|
||||||
|
immer?: boolean;
|
||||||
|
hmr?: boolean;
|
||||||
|
skipModelValidate?: boolean;
|
||||||
|
extraModels?: string[];
|
||||||
|
};
|
||||||
|
locale?: {
|
||||||
|
default?: string;
|
||||||
|
useLocalStorage?: boolean;
|
||||||
|
baseNavigator?: boolean;
|
||||||
|
title?: boolean;
|
||||||
|
antd?: boolean;
|
||||||
|
baseSeparator?: string;
|
||||||
|
};
|
||||||
|
layout?: {};
|
||||||
|
request?: {
|
||||||
|
dataField?: "";
|
||||||
|
};
|
||||||
|
sass?: {
|
||||||
|
implementation?: any;
|
||||||
|
sassOptions?: {};
|
||||||
|
prependData?: string | (() => any);
|
||||||
|
sourceMap?: boolean;
|
||||||
|
webpackImporter?: boolean;
|
||||||
|
};
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
18
src/.umi/core/pluginRegister.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { plugin } from './plugin';
|
||||||
|
import * as Plugin_0 from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-dva/runtime.tsx';
|
||||||
|
import * as Plugin_1 from '../plugin-initial-state/runtime';
|
||||||
|
import * as Plugin_2 from '../plugin-model/runtime';
|
||||||
|
|
||||||
|
plugin.register({
|
||||||
|
apply: Plugin_0,
|
||||||
|
path: '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-dva/runtime.tsx',
|
||||||
|
});
|
||||||
|
plugin.register({
|
||||||
|
apply: Plugin_1,
|
||||||
|
path: '../plugin-initial-state/runtime',
|
||||||
|
});
|
||||||
|
plugin.register({
|
||||||
|
apply: Plugin_2,
|
||||||
|
path: '../plugin-model/runtime',
|
||||||
|
});
|
||||||
3
src/.umi/core/polyfill.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import 'core-js';
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
38
src/.umi/core/routes.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { ApplyPluginsType, dynamic } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
|
||||||
|
import { plugin } from './plugin';
|
||||||
|
import LoadingComponent from '@/components/LoadingCp';
|
||||||
|
|
||||||
|
export function getRoutes() {
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"path": "/editor",
|
||||||
|
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor'), loading: LoadingComponent}),
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/preview",
|
||||||
|
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor__preview' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/preview'), loading: LoadingComponent}),
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/prevH5",
|
||||||
|
"component": dynamic({ loader: () => import(/* webpackChunkName: 'p__editor__preH5' */'/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/preH5'), loading: LoadingComponent}),
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// allow user to extend routes
|
||||||
|
plugin.applyPlugins({
|
||||||
|
key: 'patchRoutes',
|
||||||
|
type: ApplyPluginsType.event,
|
||||||
|
args: { routes },
|
||||||
|
});
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
9
src/.umi/core/umiExports.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export { history, setCreateHistoryOptions } from './history';
|
||||||
|
export { plugin } from './plugin';
|
||||||
|
export * from '../plugin-dva/exports';
|
||||||
|
export * from '../plugin-dva/connect';
|
||||||
|
export * from '../plugin-initial-state/exports';
|
||||||
|
export * from '../plugin-model/useModel';
|
||||||
|
export * from '../plugin-request/request';
|
||||||
|
export * from '../plugin-helmet/exports';
|
||||||
83
src/.umi/plugin-dva/connect.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { IRoute } from '@umijs/core';
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
|
import React from 'react';
|
||||||
|
import { EffectsCommandMap, SubscriptionAPI } from 'dva';
|
||||||
|
import { match } from 'react-router-dom';
|
||||||
|
import { Location, LocationState, History } from 'history';
|
||||||
|
|
||||||
|
export * from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/models/editorModal';
|
||||||
|
|
||||||
|
export interface Action<T = any> {
|
||||||
|
type: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Reducer<S = any, A extends Action = AnyAction> = (
|
||||||
|
state: S | undefined,
|
||||||
|
action: A
|
||||||
|
) => S;
|
||||||
|
|
||||||
|
export type ImmerReducer<S = any, A extends Action = AnyAction> = (
|
||||||
|
state: S,
|
||||||
|
action: A
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type Effect = (
|
||||||
|
action: AnyAction,
|
||||||
|
effects: EffectsCommandMap,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type P: Type of payload
|
||||||
|
* @type C: Type of callback
|
||||||
|
*/
|
||||||
|
export type Dispatch = <P = any, C = (payload: P) => void>(action: {
|
||||||
|
type: string;
|
||||||
|
payload?: P;
|
||||||
|
callback?: C;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => any;
|
||||||
|
|
||||||
|
export type Subscription = (api: SubscriptionAPI, done: Function) => void | Function;
|
||||||
|
|
||||||
|
export interface Loading {
|
||||||
|
global: boolean;
|
||||||
|
effects: { [key: string]: boolean | undefined };
|
||||||
|
models: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type P: Params matched in dynamic routing
|
||||||
|
*/
|
||||||
|
export interface ConnectProps<
|
||||||
|
P extends { [K in keyof P]?: string } = {},
|
||||||
|
S = LocationState,
|
||||||
|
T = {}
|
||||||
|
> {
|
||||||
|
dispatch?: Dispatch;
|
||||||
|
// https://github.com/umijs/umi/pull/2194
|
||||||
|
match?: match<P>;
|
||||||
|
location: Location<S> & { query: T };
|
||||||
|
history: History;
|
||||||
|
route: IRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequiredConnectProps<
|
||||||
|
P extends { [K in keyof P]?: string } = {},
|
||||||
|
S = LocationState,
|
||||||
|
T = {}
|
||||||
|
> = Required<ConnectProps<P, S, T>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type T: React props
|
||||||
|
* @type U: match props types
|
||||||
|
*/
|
||||||
|
export type ConnectRC<
|
||||||
|
T = {},
|
||||||
|
U = {},
|
||||||
|
S = {},
|
||||||
|
Q = {}
|
||||||
|
> = React.ForwardRefRenderFunction<any, T & RequiredConnectProps<U, S, Q>>;
|
||||||
|
|
||||||
68
src/.umi/plugin-dva/dva.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Component } from 'react';
|
||||||
|
import { ApplyPluginsType } from 'umi';
|
||||||
|
import dva from 'dva';
|
||||||
|
// @ts-ignore
|
||||||
|
import createLoading from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva-loading/dist/index.esm.js';
|
||||||
|
import { plugin, history } from '../core/umiExports';
|
||||||
|
|
||||||
|
let app:any = null;
|
||||||
|
|
||||||
|
export function _onCreate(options = {}) {
|
||||||
|
const runtimeDva = plugin.applyPlugins({
|
||||||
|
key: 'dva',
|
||||||
|
type: ApplyPluginsType.modify,
|
||||||
|
initialValue: {},
|
||||||
|
});
|
||||||
|
app = dva({
|
||||||
|
history,
|
||||||
|
|
||||||
|
...(runtimeDva.config || {}),
|
||||||
|
// @ts-ignore
|
||||||
|
...(typeof window !== 'undefined' && window.g_useSSR ? { initialState: window.g_initialProps } : {}),
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(createLoading());
|
||||||
|
app.use(require('/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva-immer/dist/index.js')());
|
||||||
|
(runtimeDva.plugins || []).forEach((plugin:any) => {
|
||||||
|
app.use(plugin);
|
||||||
|
});
|
||||||
|
app.model({ namespace: 'editorModal', ...(require('/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/pages/editor/models/editorModal.js').default) });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _DvaContainer extends Component {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
// run only in client, avoid override server _onCreate()
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
_onCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
let app = getApp();
|
||||||
|
app._models.forEach((model:any) => {
|
||||||
|
app.unmodel(model.namespace);
|
||||||
|
});
|
||||||
|
app._models = [];
|
||||||
|
try {
|
||||||
|
// 释放 app,for gc
|
||||||
|
// immer 场景 app 是 read-only 的,这里 try catch 一下
|
||||||
|
app = null;
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const app = getApp();
|
||||||
|
app.router(() => this.props.children);
|
||||||
|
return app.start()();
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/.umi/plugin-dva/exports.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
export { connect, useDispatch, useStore, useSelector } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/dva';
|
||||||
|
export { getApp as getDvaApp } from './dva';
|
||||||
8
src/.umi/plugin-dva/runtime.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { _DvaContainer, getApp, _onCreate } from './dva';
|
||||||
|
|
||||||
|
export function rootContainer(container) {
|
||||||
|
return React.createElement(_DvaContainer, null, container);
|
||||||
|
}
|
||||||
|
|
||||||
3
src/.umi/plugin-helmet/exports.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
// @ts-ignore
|
||||||
|
export { Helmet } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/react-helmet';
|
||||||
26
src/.umi/plugin-initial-state/Provider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { useModel } from '../plugin-model/useModel';
|
||||||
|
if (typeof useModel !== 'function') {
|
||||||
|
throw new Error('[plugin-initial-state]: useModel is not a function, @umijs/plugin-model is required.')
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
export default (props: Props) => {
|
||||||
|
const { children } = props;
|
||||||
|
const appLoaded = useRef(false);
|
||||||
|
const { loading = false } = useModel('@@initialState') || {};
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!loading){
|
||||||
|
appLoaded.current = true
|
||||||
|
}
|
||||||
|
}, [loading])
|
||||||
|
// initial state loading 时,阻塞渲染
|
||||||
|
if (loading && !appLoaded.current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
};
|
||||||
7
src/.umi/plugin-initial-state/exports.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { InitialState as InitialStateType } from '../plugin-initial-state/models/initialState';
|
||||||
|
|
||||||
|
export type InitialState = InitialStateType;
|
||||||
|
export const __PLUGIN_INITIAL_STATE = 1;
|
||||||
2
src/.umi/plugin-initial-state/models/initialState.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export default () => ({ loading: false, refresh: () => {} })
|
||||||
13
src/.umi/plugin-initial-state/runtime.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import Provider from './Provider';
|
||||||
|
|
||||||
|
export function rootContainer(container: React.ReactNode) {
|
||||||
|
return React.createElement(
|
||||||
|
// 这里的 plugin-initial-state 不能从 constant 里取,里面有 path 依赖
|
||||||
|
// 但 webpack-5 没有 node 补丁(包括 path)
|
||||||
|
Provider,
|
||||||
|
null,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/.umi/plugin-model/Provider.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import initialState from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/src/.umi/plugin-initial-state/models/initialState';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Dispatcher from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/dispatcher';
|
||||||
|
// @ts-ignore
|
||||||
|
import Executor from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/executor';
|
||||||
|
// @ts-ignore
|
||||||
|
import { UmiContext } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/constant';
|
||||||
|
|
||||||
|
export const models = { '@@initialState': initialState, };
|
||||||
|
|
||||||
|
export type Model<T extends keyof typeof models> = {
|
||||||
|
[key in keyof typeof models]: ReturnType<typeof models[T]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Models<T extends keyof typeof models> = Model<T>[T]
|
||||||
|
|
||||||
|
const dispatcher = new Dispatcher!();
|
||||||
|
const Exe = Executor!;
|
||||||
|
|
||||||
|
export default ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UmiContext.Provider value={dispatcher}>
|
||||||
|
{
|
||||||
|
Object.entries(models).map(pair => (
|
||||||
|
<Exe key={pair[0]} namespace={pair[0]} hook={pair[1] as any} onUpdate={(val: any) => {
|
||||||
|
const [ns] = pair as [keyof typeof models, any];
|
||||||
|
dispatcher.data[ns] = val;
|
||||||
|
dispatcher.update(ns);
|
||||||
|
}} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{children}
|
||||||
|
</UmiContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/.umi/plugin-model/runtime.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/* eslint-disable import/no-dynamic-require */
|
||||||
|
import React from 'react';
|
||||||
|
import Provider from './Provider';
|
||||||
|
|
||||||
|
export function rootContainer(container: React.ReactNode) {
|
||||||
|
return React.createElement(
|
||||||
|
Provider,
|
||||||
|
null,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/.umi/plugin-model/useModel.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useState, useEffect, useContext, useRef } from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import isEqual from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/fast-deep-equal/index.js';
|
||||||
|
// @ts-ignore
|
||||||
|
import { UmiContext } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/plugin-model/lib/helpers/constant';
|
||||||
|
import { Model, models } from './Provider';
|
||||||
|
|
||||||
|
export type Models<T extends keyof typeof models> = Model<T>[T]
|
||||||
|
|
||||||
|
export function useModel<T extends keyof Model<T>>(model: T): Model<T>[T]
|
||||||
|
export function useModel<T extends keyof Model<T>, U>(model: T, selector: (model: Model<T>[T]) => U): U
|
||||||
|
|
||||||
|
export function useModel<T extends keyof Model<T>, U>(
|
||||||
|
namespace: T,
|
||||||
|
updater?: (model: Model<T>[T]) => U
|
||||||
|
) : typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>{
|
||||||
|
|
||||||
|
type RetState = typeof updater extends undefined ? Model<T>[T] : ReturnType<NonNullable<typeof updater>>
|
||||||
|
const dispatcher = useContext<any>(UmiContext);
|
||||||
|
const updaterRef = useRef(updater);
|
||||||
|
updaterRef.current = updater;
|
||||||
|
const [state, setState] = useState<RetState>(
|
||||||
|
() => updaterRef.current ? updaterRef.current(dispatcher.data![namespace]) : dispatcher.data![namespace]
|
||||||
|
);
|
||||||
|
const stateRef = useRef<any>(state);
|
||||||
|
stateRef.current = state;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: any) => {
|
||||||
|
if(updater && updaterRef.current){
|
||||||
|
const currentState = updaterRef.current(e);
|
||||||
|
const previousState = stateRef.current
|
||||||
|
if(!isEqual(currentState, previousState)){
|
||||||
|
setState(currentState);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dispatcher.callbacks![namespace]!.add(handler);
|
||||||
|
} catch (e) {
|
||||||
|
dispatcher.callbacks![namespace] = new Set();
|
||||||
|
dispatcher.callbacks![namespace]!.add(handler);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
dispatcher.callbacks![namespace]!.delete(handler);
|
||||||
|
}
|
||||||
|
}, [namespace]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
274
src/.umi/plugin-request/request.ts
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* Base on https://github.com/umijs//Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/umi-request
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
extend,
|
||||||
|
Context,
|
||||||
|
RequestOptionsInit,
|
||||||
|
OnionMiddleware,
|
||||||
|
RequestOptionsWithoutResponse,
|
||||||
|
RequestMethod,
|
||||||
|
RequestOptionsWithResponse,
|
||||||
|
RequestResponse,
|
||||||
|
RequestInterceptor,
|
||||||
|
ResponseInterceptor,
|
||||||
|
} from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/umi-request';
|
||||||
|
// @ts-ignore
|
||||||
|
|
||||||
|
import { ApplyPluginsType } from 'umi';
|
||||||
|
import { history, plugin } from '../core/umiExports';
|
||||||
|
|
||||||
|
import { message, notification } from 'antd';
|
||||||
|
import useUmiRequest, { UseRequestProvider } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@ahooksjs/use-request';
|
||||||
|
import {
|
||||||
|
BaseOptions,
|
||||||
|
BasePaginatedOptions,
|
||||||
|
BaseResult,
|
||||||
|
CombineService,
|
||||||
|
LoadMoreFormatReturn,
|
||||||
|
LoadMoreOptions,
|
||||||
|
LoadMoreOptionsWithFormat,
|
||||||
|
LoadMoreParams,
|
||||||
|
LoadMoreResult,
|
||||||
|
OptionsWithFormat,
|
||||||
|
PaginatedFormatReturn,
|
||||||
|
PaginatedOptionsWithFormat,
|
||||||
|
PaginatedParams,
|
||||||
|
PaginatedResult,
|
||||||
|
} from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@ahooksjs/use-request/lib/types';
|
||||||
|
|
||||||
|
type ResultWithData<T = any> = { data?: T; [key: string]: any };
|
||||||
|
|
||||||
|
function useRequest<
|
||||||
|
R = any,
|
||||||
|
P extends any[] = any,
|
||||||
|
U = any,
|
||||||
|
UU extends U = any
|
||||||
|
>(
|
||||||
|
service: CombineService<R, P>,
|
||||||
|
options: OptionsWithFormat<R, P, U, UU>,
|
||||||
|
): BaseResult<U, P>;
|
||||||
|
function useRequest<R extends ResultWithData = any, P extends any[] = any>(
|
||||||
|
service: CombineService<R, P>,
|
||||||
|
options?: BaseOptions<R['data'], P>,
|
||||||
|
): BaseResult<R['data'], P>;
|
||||||
|
function useRequest<R extends LoadMoreFormatReturn = any, RR = any>(
|
||||||
|
service: CombineService<RR, LoadMoreParams<R>>,
|
||||||
|
options: LoadMoreOptionsWithFormat<R, RR>,
|
||||||
|
): LoadMoreResult<R>;
|
||||||
|
function useRequest<
|
||||||
|
R extends ResultWithData<LoadMoreFormatReturn | any> = any,
|
||||||
|
RR extends R = any
|
||||||
|
>(
|
||||||
|
service: CombineService<R, LoadMoreParams<R['data']>>,
|
||||||
|
options: LoadMoreOptions<RR['data']>,
|
||||||
|
): LoadMoreResult<R['data']>;
|
||||||
|
|
||||||
|
function useRequest<R = any, Item = any, U extends Item = any>(
|
||||||
|
service: CombineService<R, PaginatedParams>,
|
||||||
|
options: PaginatedOptionsWithFormat<R, Item, U>,
|
||||||
|
): PaginatedResult<Item>;
|
||||||
|
function useRequest<Item = any, U extends Item = any>(
|
||||||
|
service: CombineService<
|
||||||
|
ResultWithData<PaginatedFormatReturn<Item>>,
|
||||||
|
PaginatedParams
|
||||||
|
>,
|
||||||
|
options: BasePaginatedOptions<U>,
|
||||||
|
): PaginatedResult<Item>;
|
||||||
|
function useRequest(service: any, options: any = {}) {
|
||||||
|
return useUmiRequest(service, {
|
||||||
|
formatResult: result => result?.data,
|
||||||
|
requestMethod: (requestOptions: any) => {
|
||||||
|
if (typeof requestOptions === 'string') {
|
||||||
|
return request(requestOptions);
|
||||||
|
}
|
||||||
|
if (typeof requestOptions === 'object') {
|
||||||
|
const { url, ...rest } = requestOptions;
|
||||||
|
return request(url, rest);
|
||||||
|
}
|
||||||
|
throw new Error('request options error');
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestConfig extends RequestOptionsInit {
|
||||||
|
errorConfig?: {
|
||||||
|
errorPage?: string;
|
||||||
|
adaptor?: (resData: any, ctx: Context) => ErrorInfoStructure;
|
||||||
|
};
|
||||||
|
middlewares?: OnionMiddleware[];
|
||||||
|
requestInterceptors?: RequestInterceptor[];
|
||||||
|
responseInterceptors?: ResponseInterceptor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ErrorShowType {
|
||||||
|
SILENT = 0,
|
||||||
|
WARN_MESSAGE = 1,
|
||||||
|
ERROR_MESSAGE = 2,
|
||||||
|
NOTIFICATION = 4,
|
||||||
|
REDIRECT = 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorInfoStructure {
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
errorCode?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
showType?: ErrorShowType;
|
||||||
|
traceId?: string;
|
||||||
|
host?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestError extends Error {
|
||||||
|
data?: any;
|
||||||
|
info?: ErrorInfoStructure;
|
||||||
|
request?: Context['req'];
|
||||||
|
response?: Context['res'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ERROR_PAGE = '/exception';
|
||||||
|
|
||||||
|
let requestMethodInstance: RequestMethod;
|
||||||
|
const getRequestMethod = () => {
|
||||||
|
if (requestMethodInstance) {
|
||||||
|
// request method 已经示例化
|
||||||
|
return requestMethodInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime 配置可能应为依赖顺序的问题在模块初始化的时候无法获取,所以需要封装一层在异步调用后初始化相关方法
|
||||||
|
// 当用户的 app.ts 中依赖了该文件的情况下就该模块的初始化时间就会被提前,无法获取到运行时配置
|
||||||
|
const requestConfig: RequestConfig = plugin.applyPlugins({
|
||||||
|
key: 'request',
|
||||||
|
type: ApplyPluginsType.modify,
|
||||||
|
initialValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorAdaptor =
|
||||||
|
requestConfig.errorConfig?.adaptor || (resData => resData);
|
||||||
|
|
||||||
|
requestMethodInstance = extend({
|
||||||
|
errorHandler: (error: RequestError) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (error?.request?.options?.skipErrorHandler) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
let errorInfo: ErrorInfoStructure | undefined;
|
||||||
|
if (error.name === 'ResponseError' && error.data && error.request) {
|
||||||
|
const ctx: Context = {
|
||||||
|
req: error.request,
|
||||||
|
res: error.response,
|
||||||
|
};
|
||||||
|
errorInfo = errorAdaptor(error.data, ctx);
|
||||||
|
error.message = errorInfo?.errorMessage || error.message;
|
||||||
|
error.data = error.data;
|
||||||
|
error.info = errorInfo;
|
||||||
|
}
|
||||||
|
errorInfo = error.info;
|
||||||
|
|
||||||
|
if (errorInfo) {
|
||||||
|
const errorMessage = errorInfo?.errorMessage;
|
||||||
|
const errorCode = errorInfo?.errorCode;
|
||||||
|
const errorPage =
|
||||||
|
requestConfig.errorConfig?.errorPage || DEFAULT_ERROR_PAGE;
|
||||||
|
|
||||||
|
switch (errorInfo?.showType) {
|
||||||
|
case ErrorShowType.SILENT:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
|
case ErrorShowType.WARN_MESSAGE:
|
||||||
|
message.warn(errorMessage);
|
||||||
|
break;
|
||||||
|
case ErrorShowType.ERROR_MESSAGE:
|
||||||
|
message.error(errorMessage);
|
||||||
|
break;
|
||||||
|
case ErrorShowType.NOTIFICATION:
|
||||||
|
notification.open({
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ErrorShowType.REDIRECT:
|
||||||
|
// @ts-ignore
|
||||||
|
history.push({
|
||||||
|
pathname: errorPage,
|
||||||
|
query: { errorCode, errorMessage },
|
||||||
|
});
|
||||||
|
// redirect to error page
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message.error(errorMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(error.message || 'Request error, please retry.');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
...requestConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 中间件统一错误处理
|
||||||
|
// 后端返回格式 { success: boolean, data: any }
|
||||||
|
// 按照项目具体情况修改该部分逻辑
|
||||||
|
requestMethodInstance.use(async (ctx, next) => {
|
||||||
|
await next();
|
||||||
|
const { req, res } = ctx;
|
||||||
|
// @ts-ignore
|
||||||
|
if (req.options?.skipErrorHandler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { options } = req;
|
||||||
|
const { getResponse } = options;
|
||||||
|
const resData = getResponse ? res.data : res;
|
||||||
|
const errorInfo = errorAdaptor(resData, ctx);
|
||||||
|
if (errorInfo.success === false) {
|
||||||
|
// 抛出错误到 errorHandler 中处理
|
||||||
|
const error: RequestError = new Error(errorInfo.errorMessage);
|
||||||
|
error.name = 'BizError';
|
||||||
|
error.data = resData;
|
||||||
|
error.info = errorInfo;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add user custom middlewares
|
||||||
|
const customMiddlewares = requestConfig.middlewares || [];
|
||||||
|
customMiddlewares.forEach(mw => {
|
||||||
|
requestMethodInstance.use(mw);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add user custom interceptors
|
||||||
|
const requestInterceptors = requestConfig.requestInterceptors || [];
|
||||||
|
const responseInterceptors = requestConfig.responseInterceptors || [];
|
||||||
|
requestInterceptors.map(ri => {
|
||||||
|
requestMethodInstance.interceptors.request.use(ri);
|
||||||
|
});
|
||||||
|
responseInterceptors.map(ri => {
|
||||||
|
requestMethodInstance.interceptors.response.use(ri);
|
||||||
|
});
|
||||||
|
|
||||||
|
return requestMethodInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RequestMethodInUmi<R = false> {
|
||||||
|
<T = any>(
|
||||||
|
url: string,
|
||||||
|
options: RequestOptionsWithResponse & { skipErrorHandler?: boolean },
|
||||||
|
): Promise<RequestResponse<T>>;
|
||||||
|
<T = any>(
|
||||||
|
url: string,
|
||||||
|
options: RequestOptionsWithoutResponse & { skipErrorHandler?: boolean },
|
||||||
|
): Promise<T>;
|
||||||
|
<T = any>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestOptionsInit & { skipErrorHandler?: boolean },
|
||||||
|
): R extends true ? Promise<RequestResponse<T>> : Promise<T>;
|
||||||
|
}
|
||||||
|
const request: RequestMethodInUmi = (url: any, options: any) => {
|
||||||
|
const requestMethod = getRequestMethod();
|
||||||
|
return requestMethod(url, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { request, useRequest, UseRequestProvider };
|
||||||
59
src/.umi/umi.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import './core/polyfill';
|
||||||
|
import '@@/core/devScripts';
|
||||||
|
import { plugin } from './core/plugin';
|
||||||
|
import './core/pluginRegister';
|
||||||
|
import { createHistory } from './core/history';
|
||||||
|
import { ApplyPluginsType } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/runtime';
|
||||||
|
import { renderClient } from '/Users/apple/Desktop/github/zhiku.tec/h5-visible-tool/node_modules/@umijs/renderer-react/dist/index.js';
|
||||||
|
import { getRoutes } from './core/routes';
|
||||||
|
|
||||||
|
|
||||||
|
require('../global.css');
|
||||||
|
|
||||||
|
const getClientRender = (args: { hot?: boolean; routes?: any[] } = {}) => plugin.applyPlugins({
|
||||||
|
key: 'render',
|
||||||
|
type: ApplyPluginsType.compose,
|
||||||
|
initialValue: () => {
|
||||||
|
const opts = plugin.applyPlugins({
|
||||||
|
key: 'modifyClientRenderOpts',
|
||||||
|
type: ApplyPluginsType.modify,
|
||||||
|
initialValue: {
|
||||||
|
routes: args.routes || getRoutes(),
|
||||||
|
plugin,
|
||||||
|
history: createHistory(args.hot),
|
||||||
|
isServer: process.env.__IS_SERVER,
|
||||||
|
dynamicImport: true,
|
||||||
|
rootElement: 'root',
|
||||||
|
defaultTitle: `趣谈前端-h5-visible-tool`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderClient(opts);
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientRender = getClientRender();
|
||||||
|
export default clientRender();
|
||||||
|
|
||||||
|
|
||||||
|
window.g_umi = {
|
||||||
|
version: '3.2.16',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// hot module replacement
|
||||||
|
// @ts-ignore
|
||||||
|
if (module.hot) {
|
||||||
|
// @ts-ignore
|
||||||
|
module.hot.accept('./core/routes', () => {
|
||||||
|
const ret = require('./core/routes');
|
||||||
|
if (ret.then) {
|
||||||
|
ret.then(({ getRoutes }) => {
|
||||||
|
getClientRender({ hot: true, routes: getRoutes() })();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getClientRender({ hot: true, routes: ret.getRoutes() })();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
src/assets/01.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/02.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src/assets/03.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/04.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/05.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/ballred.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/assets/ballwhite.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
src/assets/first.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/five.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
src/assets/four.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/assets/three.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
src/assets/two.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/components/.DS_Store
vendored
Normal file
34
src/components/BackTop/index.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { BackToTop, Icon } from 'zarm'
|
||||||
|
|
||||||
|
const themeObj = {
|
||||||
|
simple: { bgColor: '#fff', color: '#999' },
|
||||||
|
black: { bgColor: '#000', color: '#fff' },
|
||||||
|
danger: { bgColor: '#ff5050', color: '#fff' },
|
||||||
|
primary: { bgColor: '#00bc71', color: '#fff' },
|
||||||
|
blue: { bgColor: '#06c', color: '#fff' }
|
||||||
|
}
|
||||||
|
const BackTop = memo((props) => {
|
||||||
|
const {
|
||||||
|
theme = 'simple'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return <BackToTop>
|
||||||
|
<div style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
lineHeight: '48px',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: themeObj[theme].bgColor,
|
||||||
|
color: themeObj[theme].color,
|
||||||
|
fontSize: 20,
|
||||||
|
borderRadius: 30,
|
||||||
|
boxShadow: '0 2px 10px 0 rgba(0, 0, 0, 0.2)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}>
|
||||||
|
<Icon type="arrow-top" />
|
||||||
|
</div>
|
||||||
|
</BackToTop>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default BackTop
|
||||||
88
src/components/Calibration/index.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
export default function Calibration(props) {
|
||||||
|
const { direction, multiple } = props
|
||||||
|
const [ calibrationLength, setCalibration ] = useState({})
|
||||||
|
const calibrationRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let calibration = calibrationRef.current.getBoundingClientRect()
|
||||||
|
setCalibration({width: calibration.width, height: calibration.height})
|
||||||
|
let length = direction === 'up' ? (calibration.width) : calibration.height
|
||||||
|
for(let i=0; i<(length / 5 ) ; i++){
|
||||||
|
if(i % 10 === 0){
|
||||||
|
generateElement(true, i)
|
||||||
|
}else {
|
||||||
|
generateElement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [direction])
|
||||||
|
|
||||||
|
const generateElement = (item, num) => {
|
||||||
|
let createSpan = document.createElement('div')
|
||||||
|
createSpan.className = 'calibrationLine'
|
||||||
|
createSpan.style.backgroundColor = '#ccc'
|
||||||
|
calibrationRef.current.style.display = 'flex'
|
||||||
|
calibrationRef.current.style.justifyContent = 'space-between'
|
||||||
|
if(direction === 'up'){
|
||||||
|
calibrationRef.current.style.marginLeft = '50px'
|
||||||
|
createSpan.style.width = '1px'
|
||||||
|
createSpan.style.height = '6px'
|
||||||
|
createSpan.style.display = 'inline-block'
|
||||||
|
}else {
|
||||||
|
calibrationRef.current.style.flexDirection = 'column'
|
||||||
|
createSpan.style.height = '1px'
|
||||||
|
createSpan.style.width = '6px'
|
||||||
|
}
|
||||||
|
if(item){
|
||||||
|
let createSpanContent = document.createElement('span')
|
||||||
|
if(direction === 'up') {
|
||||||
|
createSpan.style.height = '12px'
|
||||||
|
createSpanContent.style.transform = 'translate3d(-4px, 20px, 0px)'
|
||||||
|
createSpan.style.transform = 'translateY(0px)'
|
||||||
|
}else {
|
||||||
|
createSpan.style.width = '12px'
|
||||||
|
createSpanContent.style.paddingLeft = '20px'
|
||||||
|
}
|
||||||
|
createSpanContent.style.display = 'block'
|
||||||
|
createSpanContent.className = 'calibrationNumber'
|
||||||
|
createSpanContent.innerHTML = num * 5
|
||||||
|
createSpan.appendChild(createSpanContent)
|
||||||
|
}
|
||||||
|
calibrationRef.current.appendChild(createSpan)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let width = calibrationLength.width ? calibrationLength.width : (calibrationRef.current.getBoundingClientRect().width)
|
||||||
|
let height = calibrationLength.height ? calibrationLength.height : (calibrationRef.current.getBoundingClientRect().height)
|
||||||
|
let arr = [...calibrationRef.current.querySelectorAll('.calibrationLine')]
|
||||||
|
if(arr.length) {
|
||||||
|
if(direction === 'up'){
|
||||||
|
calibrationRef.current.style.width = (multiple.toFixed(1) * (width)) + 'px'
|
||||||
|
arr.forEach((el) => {
|
||||||
|
let dom = [...el.querySelectorAll('.calibrationNumber')][0]
|
||||||
|
if(dom){
|
||||||
|
dom.style.transform = `translate3d(-4px, 16px, 0px) scale(${(multiple + 0.1).toFixed(1)})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}else {
|
||||||
|
calibrationRef.current.style.height = (multiple.toFixed(1) * (height)) + 'px'
|
||||||
|
arr.forEach((el) => {
|
||||||
|
let dom = [...el.querySelectorAll('.calibrationNumber')][0]
|
||||||
|
if(dom){
|
||||||
|
dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(multiple + 0.1).toFixed(1)})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [calibrationLength.height, calibrationLength.width, direction, multiple])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.calibration} ref={calibrationRef}>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/components/Calibration/index.less
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.calibration{
|
||||||
|
width: calc(200% - 50px);
|
||||||
|
height: 200%;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
:global(.calibrationNumber){
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/components/Carousel/index.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Carousel } from 'zarm';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const XCarousel = memo(props => {
|
||||||
|
const {
|
||||||
|
direction,
|
||||||
|
swipeable,
|
||||||
|
imgList
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const contentRender = () => {
|
||||||
|
return imgList.map((item, i) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.carousel__item__pic} key={+i}>
|
||||||
|
<img src={item.imgUrl[0].url} alt="" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={{width: '100%', overflow: 'hidden'}}>
|
||||||
|
<Carousel
|
||||||
|
onChange={(index) => {
|
||||||
|
// console.log(`onChange: ${index}`);
|
||||||
|
}}
|
||||||
|
direction={direction}
|
||||||
|
swipeable={swipeable}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
>
|
||||||
|
{contentRender()}
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default XCarousel;
|
||||||
10
src/components/Carousel/index.less
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.carousel__item__pic {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: top;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/components/Color/index.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { SketchPicker } from 'react-color'
|
||||||
|
import { rgba2Obj } from '@/utils/tool'
|
||||||
|
// import styles from './index.less'
|
||||||
|
|
||||||
|
class colorPicker extends React.Component {
|
||||||
|
state = {
|
||||||
|
displayColorPicker: false,
|
||||||
|
color: rgba2Obj(this.props.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.setState({ displayColorPicker: !this.state.displayColorPicker })
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
this.setState({ displayColorPicker: false })
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = (color) => {
|
||||||
|
this.setState({ color: color.rgb })
|
||||||
|
this.props.onChange && this.props.onChange(`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`)
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '5px',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '1px',
|
||||||
|
boxShadow: '0 0 0 1px rgba(0,0,0,.1)',
|
||||||
|
display: 'inline-block',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={ this.handleClick }
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '36px',
|
||||||
|
height: '14px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
background: `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
{ this.state.displayColorPicker ?
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: '2'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0px',
|
||||||
|
right: '0px',
|
||||||
|
bottom: '0px',
|
||||||
|
left: '0px'
|
||||||
|
}}
|
||||||
|
onClick={ this.handleClose }
|
||||||
|
/>
|
||||||
|
<SketchPicker color={ this.state.color } onChange={ this.handleChange } />
|
||||||
|
</div> : null }
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default colorPicker
|
||||||
0
src/components/Color/index.less
Normal file
96
src/components/DataList/editorModal.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React, { memo, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Modal
|
||||||
|
} from 'antd';
|
||||||
|
import Upload from '@/components/Upload';
|
||||||
|
|
||||||
|
// import styles from './index.less';
|
||||||
|
const normFile = e => {
|
||||||
|
console.log('Upload event:', e);
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return e && e.fileList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 14 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditorModal = (props) => {
|
||||||
|
const { item, onSave, visible, onCancel } = props
|
||||||
|
const onFinish = values => {
|
||||||
|
onSave && onSave(values)
|
||||||
|
}
|
||||||
|
const handleOk = () => {
|
||||||
|
form.validateFields().then(values => {
|
||||||
|
console.log(values)
|
||||||
|
values.id = item.id
|
||||||
|
onSave && onSave(values)
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
}, [item])
|
||||||
|
|
||||||
|
return !!item && (
|
||||||
|
<Modal
|
||||||
|
title="编辑数据源"
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name={`form_editor_modal`}
|
||||||
|
{...formItemLayout}
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={item}
|
||||||
|
>
|
||||||
|
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题!' }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="描述" name="desc">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="链接地址" name="link">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
{
|
||||||
|
!!window['currentCates'] &&
|
||||||
|
<Form.Item label="分类" name="type" rules={[{ required: true, message: '请选择分类!' }]}>
|
||||||
|
<Select placeholder="请选择">
|
||||||
|
{
|
||||||
|
window['currentCates'].map((v, i) => {
|
||||||
|
return <Option value={i} key={i}>{ v }</Option>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Form.Item label="上传图片" name="imgUrl" valuePropName="fileList" getValueFromEvent={normFile}>
|
||||||
|
<Upload />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(EditorModal)
|
||||||
187
src/components/DataList/index.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React, { memo, useState, useEffect, useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
EditOutlined,
|
||||||
|
MinusCircleOutlined,
|
||||||
|
MenuOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { DragSource, DropTarget, DndProvider } from "react-dnd";
|
||||||
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
|
import EditorModal from './editorModal';
|
||||||
|
import { uuid } from '@/utils/tool';
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
function ListItem(props) {
|
||||||
|
const { title, desc, link, imgUrl, type, onDel, onEdit,
|
||||||
|
// 这些 props 由 React DnD注入,参考`collect`函数定义
|
||||||
|
isDragging, connectDragSource, connectDragPreview, connectDropTarget,
|
||||||
|
// 这些是组件收到的 props
|
||||||
|
item, style = {}, find, move, change, remove, ...restProps
|
||||||
|
} = props
|
||||||
|
const opacity = isDragging ? 0.5 : 1
|
||||||
|
return connectDropTarget( // 列表项本身作为 Drop 对象
|
||||||
|
connectDragPreview( // 整个列表项作为跟随拖动的影像
|
||||||
|
<div className={styles.listItem} {...restProps} style={Object.assign(style, { opacity })}>
|
||||||
|
<div className={styles.tit}>{ title }</div>
|
||||||
|
<div className={styles.desc}>{ desc }</div>
|
||||||
|
<div className={styles.actionBar}>
|
||||||
|
<span className={styles.action} onClick={onEdit}><EditOutlined /></span>
|
||||||
|
<span className={styles.action} onClick={onDel}><MinusCircleOutlined /></span>
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<span className={styles.action}><MenuOutlined /></span>
|
||||||
|
) // 拖动图标作为 Drag 对象
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = "item";
|
||||||
|
const dragSpec = {
|
||||||
|
// 拖动开始时,返回描述 source 数据。后续通过 monitor.getItem() 获得
|
||||||
|
beginDrag: props => ({
|
||||||
|
id: props.id,
|
||||||
|
originalIndex: props.find(props.id).index
|
||||||
|
}),
|
||||||
|
// 拖动停止时,处理 source 数据
|
||||||
|
endDrag(props, monitor) {
|
||||||
|
const { id: droppedId, originalIndex } = monitor.getItem();
|
||||||
|
const didDrop = monitor.didDrop();
|
||||||
|
// source 是否已经放置在 target
|
||||||
|
if (!didDrop) {
|
||||||
|
return props.move(droppedId, originalIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragCollect = (connect, monitor) => ({
|
||||||
|
connectDragSource: connect.dragSource(), // 用于包装需要拖动的组件
|
||||||
|
connectDragPreview: connect.dragPreview(), // 用于包装需要拖动跟随预览的组件
|
||||||
|
isDragging: monitor.isDragging() // 用于判断是否处于拖动状态
|
||||||
|
})
|
||||||
|
|
||||||
|
const dropSpec = {
|
||||||
|
canDrop: () => false, // item 不处理 drop
|
||||||
|
hover(props, monitor) {
|
||||||
|
const { id: draggedId } = monitor.getItem();
|
||||||
|
const { id: overId } = props;
|
||||||
|
// 如果 source item 与 target item 不同,则交换位置并重新排序
|
||||||
|
if (draggedId !== overId) {
|
||||||
|
const { index: overIndex } = props.find(overId);
|
||||||
|
props.move(draggedId, overIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropCollect = (connect, monitor) => ({
|
||||||
|
connectDropTarget: connect.dropTarget() // 用于包装需接收拖拽的组件
|
||||||
|
})
|
||||||
|
|
||||||
|
const DndItem = DropTarget(type, dropSpec, dropCollect)(
|
||||||
|
DragSource(type, dragSpec, dragCollect)(ListItem)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const List = function(props) {
|
||||||
|
const { onChange, value, connectDropTarget } = props
|
||||||
|
const [list, setList] = useState(value)
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [curItem, setCurItem] = useState()
|
||||||
|
|
||||||
|
const handleDel = (id) => {
|
||||||
|
let newVal = value.filter(item => id !== item.id)
|
||||||
|
onChange(newVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const find = id => {
|
||||||
|
const item = list.find(c => `${c.id}` === id);
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
index: list.indexOf(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const move = (id, toIndex) => {
|
||||||
|
const { item, index } = find(id);
|
||||||
|
const oldList = [...list];
|
||||||
|
oldList.splice(index, 1);
|
||||||
|
oldList.splice(toIndex, 0, item);
|
||||||
|
if(onChange) {
|
||||||
|
onChange(oldList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setList(oldList)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
setVisible(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleEdit = useCallback((item) => {
|
||||||
|
setVisible(true)
|
||||||
|
setCurItem(item)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSave = useCallback((item) => {
|
||||||
|
setVisible(false)
|
||||||
|
console.log(22, list, item)
|
||||||
|
if(onChange) {
|
||||||
|
onChange(list.map(p => p.id === item.id ? item : p))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setList(prev => prev.map(p => p.id === item.id ? item : p))
|
||||||
|
}, [curItem])
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const item = {
|
||||||
|
title: '新增项标题',
|
||||||
|
desc: '新增项描述',
|
||||||
|
id: uuid(8, 10),
|
||||||
|
imgUrl: [],
|
||||||
|
link: ''
|
||||||
|
}
|
||||||
|
if(onChange) {
|
||||||
|
onChange([...list, item])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setList([...list, item])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setList(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return connectDropTarget(
|
||||||
|
<div className={styles.dataList}>
|
||||||
|
{
|
||||||
|
!!(list && list.length) && list.map((item, i) =>
|
||||||
|
<DndItem
|
||||||
|
{...item}
|
||||||
|
onDel={handleDel.bind(this, item.id)}
|
||||||
|
onEdit={handleEdit.bind(this, item)}
|
||||||
|
key={i}
|
||||||
|
id={`${item.id}`}
|
||||||
|
find={find}
|
||||||
|
move={move}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div style={{marginTop: '10px'}}><Button type="primary" onClick={handleAdd} block>添加</Button></div>
|
||||||
|
<EditorModal visible={visible} onCancel={handleCancel} item={curItem} onSave={handleSave} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DndList = DropTarget(type, {}, connect => ({
|
||||||
|
connectDropTarget: connect.dropTarget()
|
||||||
|
}))(List)
|
||||||
|
|
||||||
|
// 将 HTMLBackend 作为参数传给 DragDropContext
|
||||||
|
export default memo((props) => {
|
||||||
|
return <DndProvider backend={HTML5Backend}>
|
||||||
|
<DndList {...props} />
|
||||||
|
</DndProvider>
|
||||||
|
})
|
||||||
42
src/components/DataList/index.less
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.dataList {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.listItem {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
&:hover {
|
||||||
|
.actionBar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.tit {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.actionBar {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: none;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: -20px 0 10px 10px #fff;
|
||||||
|
.action {
|
||||||
|
margin-right: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:last-child {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/components/DynamicEngine/.DS_Store
vendored
Normal file
117
src/components/DynamicEngine/components.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { NoticeBar } from 'zarm'
|
||||||
|
import styles from './components.less'
|
||||||
|
|
||||||
|
const Header = memo((props) => {
|
||||||
|
const {
|
||||||
|
bgColor,
|
||||||
|
logo,
|
||||||
|
logoText,
|
||||||
|
fontSize,
|
||||||
|
color
|
||||||
|
} = props
|
||||||
|
return <header className={styles.header} style={{backgroundColor: bgColor}}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<img src={logo && logo[0].url} alt={logoText} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.title} style={{fontSize, color}}>{ logoText }</div>
|
||||||
|
</header>
|
||||||
|
})
|
||||||
|
|
||||||
|
const Text = memo((props) => {
|
||||||
|
const {
|
||||||
|
align,
|
||||||
|
text,
|
||||||
|
fontSize,
|
||||||
|
color,
|
||||||
|
lineHeight
|
||||||
|
} = props
|
||||||
|
return <div className={styles.textWrap} style={{color, textAlign: align, fontSize, lineHeight}}>
|
||||||
|
{ text }
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
const Notice = memo((props) => {
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
speed,
|
||||||
|
theme,
|
||||||
|
link,
|
||||||
|
isClose = false
|
||||||
|
} = props
|
||||||
|
return <NoticeBar theme={theme} closable={isClose} speed={speed}><a style={{color: 'inherit'}}>{ text }</a></NoticeBar>
|
||||||
|
})
|
||||||
|
|
||||||
|
const Qrcode = memo((props) => {
|
||||||
|
const {
|
||||||
|
qrcode,
|
||||||
|
text,
|
||||||
|
color,
|
||||||
|
fontSize = 14
|
||||||
|
} = props
|
||||||
|
return <div style={{width: '240px', margin: '16px auto'}}>
|
||||||
|
<img src={qrcode && qrcode[0].url} alt={text} style={{width: '100%'}} />
|
||||||
|
<div style={{textAlign: 'center', color, fontSize, paddingTop: '8px'}}>{ text }</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
const Footer = memo((props) => {
|
||||||
|
const {
|
||||||
|
bgColor,
|
||||||
|
text,
|
||||||
|
color,
|
||||||
|
align,
|
||||||
|
fontSize,
|
||||||
|
height
|
||||||
|
} = props
|
||||||
|
return <footer style={{backgroundColor: bgColor, color, fontSize, textAlign: align, height, lineHeight: height + 'px'}}>{ text }</footer>
|
||||||
|
})
|
||||||
|
|
||||||
|
const Image = memo((props) => {
|
||||||
|
const {
|
||||||
|
imgUrl,
|
||||||
|
round = 0
|
||||||
|
} = props
|
||||||
|
return <div style={{borderRadius: round, width: '100%', textAlign: 'center', overflow: 'hidden'}}>
|
||||||
|
<img src={imgUrl && imgUrl[0].url} alt="" style={{width: '100%'}} />
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
const List = memo((props) => {
|
||||||
|
const {
|
||||||
|
round,
|
||||||
|
sourceData,
|
||||||
|
imgSize,
|
||||||
|
fontSize,
|
||||||
|
color
|
||||||
|
} = props
|
||||||
|
return <div className={styles.list}>
|
||||||
|
<div className={styles.sourceList}>
|
||||||
|
{
|
||||||
|
sourceData.map((item, i) => {
|
||||||
|
return <div className={styles.sourceItem} key={i}>
|
||||||
|
<div className={styles.imgWrap}>
|
||||||
|
<img src={item.imgUrl[0] ? item.imgUrl[0].url : 'http://io.nainor.com/uploads/01_173e15d3493.png'} alt={item.desc} style={{width: imgSize, height: imgSize, objectFit: 'cover', borderRadius: round}} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<a className={styles.tit} style={{fontSize, color}} href={item.link ? item.link : '#'}>
|
||||||
|
{ item.title }
|
||||||
|
<div className={styles.desc} style={{fontSize: fontSize*0.8, color: 'rgba(0,0,0, .3)'}}>{ item.desc }</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
Header,
|
||||||
|
Text,
|
||||||
|
Notice,
|
||||||
|
Qrcode,
|
||||||
|
Footer,
|
||||||
|
Image,
|
||||||
|
List
|
||||||
|
}
|
||||||
43
src/components/DynamicEngine/components.less
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.header {
|
||||||
|
box-sizing: content-box;
|
||||||
|
padding: 3px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #000;
|
||||||
|
.logo {
|
||||||
|
margin-right: 10px;
|
||||||
|
max-width: 160px;
|
||||||
|
max-height: 46px;
|
||||||
|
height: 46px;
|
||||||
|
overflow: hidden;
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list {
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 94%;
|
||||||
|
.sourceList {
|
||||||
|
.sourceItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
.imgWrap {
|
||||||
|
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-left: 12px;
|
||||||
|
.tit {
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/components/DynamicEngine/index.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { dynamic } from 'umi';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { useMemo, memo } from 'react';
|
||||||
|
|
||||||
|
const needList = ['Tab', 'Carousel', 'Upload', 'Video']
|
||||||
|
|
||||||
|
const DynamicFunc = (type) => dynamic({
|
||||||
|
loader: async function() {
|
||||||
|
let Component;
|
||||||
|
if(needList.includes(type)) {
|
||||||
|
const { default: Graph } = await import(`@/components/${type}`)
|
||||||
|
Component = Graph
|
||||||
|
}else {
|
||||||
|
const Components = await import(`@/components/DynamicEngine/components`)
|
||||||
|
Component = Components[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (props) => {
|
||||||
|
const { config } = props
|
||||||
|
return <Component {...config} />
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: () => <div style={{ paddingTop: 10, textAlign: 'center' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
const DynamicEngine = memo((props) => {
|
||||||
|
const { type, config } = props
|
||||||
|
const Dynamic = useMemo(() => {
|
||||||
|
return DynamicFunc(type)
|
||||||
|
}, [type])
|
||||||
|
return <Dynamic type={type} config={config} />
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DynamicEngine
|
||||||
8
src/components/DynamicEngine/mediaTpl.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const mediaTpl = [
|
||||||
|
{
|
||||||
|
type:'Video',
|
||||||
|
h: 107
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default mediaTpl
|
||||||
532
src/components/DynamicEngine/schema.js
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
export default {
|
||||||
|
"Carousel": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "direction",
|
||||||
|
"name": "方向",
|
||||||
|
"type": "Radio",
|
||||||
|
"range": [
|
||||||
|
{
|
||||||
|
key: "top",
|
||||||
|
text: "从上到下"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "left",
|
||||||
|
text: "从左到右"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "swipeable",
|
||||||
|
"name": "是否可拖拽",
|
||||||
|
"type": "Switch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "imgList",
|
||||||
|
"name": "图片列表",
|
||||||
|
"type": "DataList",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"direction": "left",
|
||||||
|
"swipeable": false,
|
||||||
|
"imgList": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "趣谈小课1",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740bd7c3dc.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "趣谈小课1",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/2_1740bd8d525.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Text": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "text",
|
||||||
|
"name": "文字",
|
||||||
|
"type": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "标题颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "字体大小",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "align",
|
||||||
|
"name": "对齐方式",
|
||||||
|
"type": "Select",
|
||||||
|
"range": [
|
||||||
|
{
|
||||||
|
"key": "left",
|
||||||
|
"text": "左对齐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "center",
|
||||||
|
"text": "居中对齐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "right",
|
||||||
|
"text": "右对齐"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "lineHeight",
|
||||||
|
"name": "行高",
|
||||||
|
"type": "Number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"text": "我是文本",
|
||||||
|
"color": "rgba(60,60,60,1)",
|
||||||
|
"fontSize": 18,
|
||||||
|
"align": "center",
|
||||||
|
"lineHeight": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tab": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "tabs",
|
||||||
|
"name": "项目类别",
|
||||||
|
"type": "MutiText"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "activeColor",
|
||||||
|
"name": "激活颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "文字颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "文字大小",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sourceData",
|
||||||
|
"name": "数据源",
|
||||||
|
"type": "DataList"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"tabs": ["类别一", "类别二"],
|
||||||
|
"color": "rgba(153,153,153,1)",
|
||||||
|
"activeColor": "rgba(0,102,204,1)",
|
||||||
|
"fontSize": 16,
|
||||||
|
"sourceData": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "趣谈小课1",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"type": 0,
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740c6fbcd9.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "趣谈小课2",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"type": 0,
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/2_1740c7033a9.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "趣谈小课3",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"type": 1,
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740c6fbcd9.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Notice": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "text",
|
||||||
|
"name": "文本",
|
||||||
|
"type": "Text"
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// "key": "link",
|
||||||
|
// "name": "链接",
|
||||||
|
// "type": "Text"
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
"key": "speed",
|
||||||
|
"name": "滚动速度",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "theme",
|
||||||
|
"name": "主题",
|
||||||
|
"type": "Select",
|
||||||
|
"range": [
|
||||||
|
{
|
||||||
|
"key": "default",
|
||||||
|
"text": "默认"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "warning",
|
||||||
|
"text": "警告"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "primary",
|
||||||
|
"text": "主要"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "success",
|
||||||
|
"text": "成功"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "danger",
|
||||||
|
"text": "危险"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "isClose",
|
||||||
|
"name": "是否可关闭",
|
||||||
|
"type": "Switch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"text": "通知栏: 趣谈前端上线啦",
|
||||||
|
"link": "",
|
||||||
|
"speed": 50,
|
||||||
|
"theme": "warning",
|
||||||
|
"isClose": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Qrcode": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "qrcode",
|
||||||
|
"name": "二维码",
|
||||||
|
"type": "Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "text",
|
||||||
|
"name": "文字",
|
||||||
|
"type": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "文字颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "文字大小",
|
||||||
|
"type": "Number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"qrcode": [
|
||||||
|
{
|
||||||
|
uid: "001",
|
||||||
|
name: "image.png",
|
||||||
|
status: "done",
|
||||||
|
url: "http://io.nainor.com/uploads/code_173e1705e0c.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "二维码",
|
||||||
|
"color": "rgba(153,153,153,1)",
|
||||||
|
"fontSize": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Footer": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "bgColor",
|
||||||
|
"name": "背景色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "height",
|
||||||
|
"name": "高度",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "text",
|
||||||
|
"name": "文字",
|
||||||
|
"type": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "字体大小",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "文字颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "align",
|
||||||
|
"name": "对齐方式",
|
||||||
|
"type": "Select",
|
||||||
|
"range": [
|
||||||
|
{
|
||||||
|
"key": "left",
|
||||||
|
"text": "左对齐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "center",
|
||||||
|
"text": "居中对齐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "right",
|
||||||
|
"text": "右对齐"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"bgColor": "rgba(0,0,0,1)",
|
||||||
|
"text": "页脚Footer",
|
||||||
|
"color": "rgba(255,255,255,1)",
|
||||||
|
"align": "center",
|
||||||
|
"fontSize": 16,
|
||||||
|
"height": 48
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Image": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "imgUrl",
|
||||||
|
"name": "上传",
|
||||||
|
"type": "Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "round",
|
||||||
|
"name": "圆角",
|
||||||
|
"type": "Number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
uid: "001",
|
||||||
|
name: "image.png",
|
||||||
|
status: "done",
|
||||||
|
url: "http://io.nainor.com/uploads/4_1740bf4535c.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"round": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Header": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "bgColor",
|
||||||
|
"name": "背景色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "height",
|
||||||
|
"name": "高度",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "logo",
|
||||||
|
"name": "logo",
|
||||||
|
"type": "Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "logoText",
|
||||||
|
"name": "logo文字",
|
||||||
|
"type": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "文字颜色",
|
||||||
|
"type": "Color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "文字大小",
|
||||||
|
"type": "Number"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"bgColor": "rgba(0,0,0,1)",
|
||||||
|
"logo": [
|
||||||
|
{
|
||||||
|
uid: "001",
|
||||||
|
name: "image.png",
|
||||||
|
status: "done",
|
||||||
|
url: "http://io.nainor.com/uploads/3_1740be8a482.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"logoText": "页头Header",
|
||||||
|
"fontSize": 20,
|
||||||
|
"color": "rgba(255,255,255,1)",
|
||||||
|
"height": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"List": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "sourceData",
|
||||||
|
"name": "数据源",
|
||||||
|
"type": "DataList"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "round",
|
||||||
|
"name": "圆角",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "imgSize",
|
||||||
|
"name": "图片大小",
|
||||||
|
"type": "Select",
|
||||||
|
"range": [
|
||||||
|
{
|
||||||
|
"key": 60,
|
||||||
|
"text": "60 x 60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 80,
|
||||||
|
"text": "80 x 80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 100,
|
||||||
|
"text": "100 x 100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 120,
|
||||||
|
"text": "120 x 120"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": 150,
|
||||||
|
"text": "150 x 150"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontSize",
|
||||||
|
"name": "文字大小",
|
||||||
|
"type": "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "color",
|
||||||
|
"name": "文字颜色",
|
||||||
|
"type": "Color"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"sourceData": [
|
||||||
|
{
|
||||||
|
"title": "趣谈小课",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740c6fbcd9.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "趣谈小课",
|
||||||
|
"desc": "致力于打造优质小课程",
|
||||||
|
"link": "xxxxx",
|
||||||
|
"imgUrl": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740c6fbcd9.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"round": 0,
|
||||||
|
"imgSize": 80,
|
||||||
|
"fontSize": 16,
|
||||||
|
"color": "rgba(153,153,153,1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Video": {
|
||||||
|
"editData": [
|
||||||
|
{
|
||||||
|
"key": "poster",
|
||||||
|
"name": "视频封面",
|
||||||
|
"type": "Upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"name": "视频链接",
|
||||||
|
"type": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"poster": [
|
||||||
|
{
|
||||||
|
"uid": "001",
|
||||||
|
"name": "image.png",
|
||||||
|
"status": "done",
|
||||||
|
"url": "http://io.nainor.com/uploads/1_1740c6fbcd9.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
40
src/components/DynamicEngine/template.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const template = [
|
||||||
|
{
|
||||||
|
type:'Text',
|
||||||
|
h: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Carousel',
|
||||||
|
h: 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Tab',
|
||||||
|
h: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Notice',
|
||||||
|
h: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Qrcode',
|
||||||
|
h: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Footer',
|
||||||
|
h: 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Image',
|
||||||
|
h: 188
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'Header',
|
||||||
|
h: 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type:'List',
|
||||||
|
h: 110
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default template
|
||||||
346
src/components/Form/data.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
"Date": "2010-01",
|
||||||
|
"scales": 1998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-02",
|
||||||
|
"scales": 1850
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-03",
|
||||||
|
"scales": 1720
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-04",
|
||||||
|
"scales": 1818
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-05",
|
||||||
|
"scales": 1920
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-06",
|
||||||
|
"scales": 1802
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-07",
|
||||||
|
"scales": 1945
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-08",
|
||||||
|
"scales": 1856
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-09",
|
||||||
|
"scales": 2107
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-10",
|
||||||
|
"scales": 2140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-11",
|
||||||
|
"scales": 2311
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2010-12",
|
||||||
|
"scales": 1972
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-01",
|
||||||
|
"scales": 1760
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-02",
|
||||||
|
"scales": 1824
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-03",
|
||||||
|
"scales": 1801
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-04",
|
||||||
|
"scales": 2001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-05",
|
||||||
|
"scales": 1640
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-06",
|
||||||
|
"scales": 1502
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-07",
|
||||||
|
"scales": 1621
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-08",
|
||||||
|
"scales": 1480
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-09",
|
||||||
|
"scales": 1549
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-10",
|
||||||
|
"scales": 1390
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-11",
|
||||||
|
"scales": 1325
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2011-12",
|
||||||
|
"scales": 1250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-01",
|
||||||
|
"scales": 1394
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-02",
|
||||||
|
"scales": 1406
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-03",
|
||||||
|
"scales": 1578
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-04",
|
||||||
|
"scales": 1465
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-05",
|
||||||
|
"scales": 1689
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-06",
|
||||||
|
"scales": 1755
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-07",
|
||||||
|
"scales": 1495
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-08",
|
||||||
|
"scales": 1508
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-09",
|
||||||
|
"scales": 1433
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-10",
|
||||||
|
"scales": 1344
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-11",
|
||||||
|
"scales": 1201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2012-12",
|
||||||
|
"scales": 1065
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-01",
|
||||||
|
"scales": 1255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-02",
|
||||||
|
"scales": 1429
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-03",
|
||||||
|
"scales": 1398
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-04",
|
||||||
|
"scales": 1678
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-05",
|
||||||
|
"scales": 1524
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-06",
|
||||||
|
"scales": 1688
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-07",
|
||||||
|
"scales": 1500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-08",
|
||||||
|
"scales": 1670
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-09",
|
||||||
|
"scales": 1734
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-10",
|
||||||
|
"scales": 1699
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-11",
|
||||||
|
"scales": 1508
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2013-12",
|
||||||
|
"scales": 1680
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-01",
|
||||||
|
"scales": 1750
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-02",
|
||||||
|
"scales": 1602
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-03",
|
||||||
|
"scales": 1834
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-04",
|
||||||
|
"scales": 1722
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-05",
|
||||||
|
"scales": 1430
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-06",
|
||||||
|
"scales": 1280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-07",
|
||||||
|
"scales": 1367
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-08",
|
||||||
|
"scales": 1155
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-09",
|
||||||
|
"scales": 1289
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-10",
|
||||||
|
"scales": 1104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-11",
|
||||||
|
"scales": 1246
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2014-12",
|
||||||
|
"scales": 1098
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-01",
|
||||||
|
"scales": 1189
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-02",
|
||||||
|
"scales": 1276
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-03",
|
||||||
|
"scales": 1033
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-04",
|
||||||
|
"scales": 956
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-05",
|
||||||
|
"scales": 845
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-06",
|
||||||
|
"scales": 1089
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-07",
|
||||||
|
"scales": 944
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-08",
|
||||||
|
"scales": 1043
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-09",
|
||||||
|
"scales": 893
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-10",
|
||||||
|
"scales": 840
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-11",
|
||||||
|
"scales": 934
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2015-12",
|
||||||
|
"scales": 810
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-01",
|
||||||
|
"scales": 782
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-02",
|
||||||
|
"scales": 1089
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-03",
|
||||||
|
"scales": 745
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-04",
|
||||||
|
"scales": 680
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-05",
|
||||||
|
"scales": 802
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-06",
|
||||||
|
"scales": 697
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-07",
|
||||||
|
"scales": 583
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-08",
|
||||||
|
"scales": 456
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-09",
|
||||||
|
"scales": 524
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-10",
|
||||||
|
"scales": 398
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-11",
|
||||||
|
"scales": 278
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2016-12",
|
||||||
|
"scales": 195
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2017-01",
|
||||||
|
"scales": 145
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Date": "2017-02",
|
||||||
|
"scales": 207
|
||||||
|
}
|
||||||
|
]
|
||||||
27
src/components/Form/index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Area } from '@ant-design/charts';
|
||||||
|
import data from './data';
|
||||||
|
|
||||||
|
const XArea = (props) => {
|
||||||
|
const { configData: { title, description } } = props
|
||||||
|
const config = {
|
||||||
|
title: {
|
||||||
|
visible: true,
|
||||||
|
text: title || '',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
visible: true,
|
||||||
|
text: description || '',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
xField: 'Date',
|
||||||
|
yField: 'scales',
|
||||||
|
xAxis: {
|
||||||
|
type: 'dateTime',
|
||||||
|
tickCount: 5,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <Area {...config} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default XArea;
|
||||||
0
src/components/Form/index.less
Normal file
151
src/components/FormEditor/index.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
InputNumber,
|
||||||
|
Input,
|
||||||
|
Switch,
|
||||||
|
Radio,
|
||||||
|
Button
|
||||||
|
} from 'antd';
|
||||||
|
import Upload from '@/components/Upload';
|
||||||
|
import DataList from '@/components/DataList';
|
||||||
|
import MutiText from '@/components/MutiText';
|
||||||
|
import Color from '@/components/Color';
|
||||||
|
|
||||||
|
// import styles from './index.less';
|
||||||
|
const normFile = e => {
|
||||||
|
console.log('Upload event:', e);
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return e && e.fileList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const formItemLayout = {
|
||||||
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultConfig = [
|
||||||
|
{
|
||||||
|
"key": "tabs",
|
||||||
|
"name": "项目类别",
|
||||||
|
"type": "mutiText",
|
||||||
|
"defaultValue": ["类别一", "类别二"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const FormEditor = (props) => {
|
||||||
|
const { config = defaultConfig, defaultValue, onSave, onDel, uid } = props
|
||||||
|
const onFinish = values => {
|
||||||
|
onSave && onSave(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDel = () => {
|
||||||
|
onDel && onDel(uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
}, [defaultValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name={`form_editor`}
|
||||||
|
{...formItemLayout}
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={defaultValue}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
config.map((item, i) => {
|
||||||
|
return <React.Fragment key={i}>
|
||||||
|
{
|
||||||
|
item.type === 'Number' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Text' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'DataList' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<DataList />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Color' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<Color />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'MutiText' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<MutiText />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Select' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<Select placeholder="请选择">
|
||||||
|
{
|
||||||
|
item.range.map((v, i) => {
|
||||||
|
return <Option value={v.key} key={i}>{ v.text }</Option>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Radio' &&
|
||||||
|
<Form.Item label={item.name} name={item.key}>
|
||||||
|
<Radio.Group>
|
||||||
|
{
|
||||||
|
item.range.map((v, i) => {
|
||||||
|
return <Radio value={v.key} key={i}>{ v.text }</Radio>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Switch' &&
|
||||||
|
<Form.Item label={item.name} name={item.key} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
item.type === 'Upload' &&
|
||||||
|
<Form.Item label={item.name} name={item.key} valuePropName="fileList" getValueFromEvent={normFile}>
|
||||||
|
<Upload />
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<Form.Item wrapperCol={{ span: 12, offset: 6 }}>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button type="danger" style={{marginLeft: '20px'}} onClick={handleDel}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FormEditor)
|
||||||
0
src/components/FormEditor/index.less
Normal file
8
src/components/LoadingCp/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<div style={{ paddingTop: 100, textAlign: 'center' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
70
src/components/MutiText/index.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react'
|
||||||
|
import { Input, Button, Popconfirm } from 'antd'
|
||||||
|
import { MinusCircleOutlined } from '@ant-design/icons'
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
export default memo(function MutiText(props) {
|
||||||
|
const { value, onChange } = props
|
||||||
|
const [valueList, setValueList] = useState(value || [])
|
||||||
|
const handleAdd = () => {
|
||||||
|
setValueList((prev) => {
|
||||||
|
return [...prev, '新增项']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDel = (index) => {
|
||||||
|
setValueList((prev) => {
|
||||||
|
let newList = prev.filter((item, i) => i !== index)
|
||||||
|
onChange && onChange(newList)
|
||||||
|
return newList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (index, e) => {
|
||||||
|
const { value } = e.target
|
||||||
|
setValueList((prev) => {
|
||||||
|
let newList = prev.map((item, i) => (i === index ? value : item))
|
||||||
|
onChange && onChange(newList)
|
||||||
|
return newList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
window['currentCates'] = valueList
|
||||||
|
return () => {
|
||||||
|
window['currentCates'] = null
|
||||||
|
}
|
||||||
|
}, [valueList])
|
||||||
|
|
||||||
|
return <div className={styles.mutiText}>
|
||||||
|
{
|
||||||
|
valueList.length ?
|
||||||
|
valueList.map((item, i) => {
|
||||||
|
return <div className={styles.iptWrap} key={i}>
|
||||||
|
<Input defaultValue={item} onChange={handleChange.bind(this, i)} />
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除吗?"
|
||||||
|
onConfirm={handleDel.bind(this, i)}
|
||||||
|
placement="bottom"
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<span className={styles.delBtn}><MinusCircleOutlined /></span>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}) :
|
||||||
|
<div className={styles.iptWrap}>
|
||||||
|
<Input />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
valueList.length < 3 &&
|
||||||
|
<div className={styles.iptWrap}>
|
||||||
|
<Button type="primary" onClick={handleAdd}>添加项目</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
})
|
||||||
11
src/components/MutiText/index.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.mutiText {
|
||||||
|
.iptWrap {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
.delBtn {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/components/Tab/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Tabs } from 'zarm';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { Panel } = Tabs;
|
||||||
|
|
||||||
|
const XTab = (props) => {
|
||||||
|
const {
|
||||||
|
tabs,
|
||||||
|
activeColor,
|
||||||
|
color,
|
||||||
|
fontSize,
|
||||||
|
sourceData
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const tabWrapRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tabWrapRef.current.querySelector('.za-tabs__line').style.backgroundColor = activeColor
|
||||||
|
}, [activeColor])
|
||||||
|
|
||||||
|
return <div className={styles.tabWrap} ref={tabWrapRef}>
|
||||||
|
<Tabs canSwipe onChange={(i) => { console.log(i); }}>
|
||||||
|
{
|
||||||
|
tabs.map((item, i) => {
|
||||||
|
return <Panel title={item} key={i}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{
|
||||||
|
sourceData.filter(item => item.type === i).map((item, i) => {
|
||||||
|
return <div className={styles.item} key={i}>
|
||||||
|
<a className={styles.imgWrap} href={item.link} title={item.desc}>
|
||||||
|
<img src={item.imgUrl[0] ? item.imgUrl[0].url : 'http://io.nainor.com/uploads/01_173e15d3493.png'} alt={ item.title } />
|
||||||
|
<div className={styles.title} style={{fontSize, color}}>{ item.title }</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default XTab;
|
||||||
27
src/components/Tab/index.less
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.tabWrap {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.item {
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
.imgWrap {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80%;
|
||||||
|
img {
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
line-height: 2.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/components/Upload/index.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Upload, Modal, message } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import ImgCrop from 'antd-img-crop';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
function getBase64(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PicturesWall extends React.Component {
|
||||||
|
state = {
|
||||||
|
previewVisible: false,
|
||||||
|
previewImage: '',
|
||||||
|
previewTitle: '',
|
||||||
|
fileList: this.props.fileList || []
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCancel = () => this.setState({ previewVisible: false });
|
||||||
|
|
||||||
|
handlePreview = async file => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = await getBase64(file.originFileObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
previewImage: file.url || file.preview,
|
||||||
|
previewVisible: true,
|
||||||
|
previewTitle: file.name || file.url.substring(file.url.lastIndexOf('/') + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = ({ file, fileList }) => {
|
||||||
|
this.setState({ fileList })
|
||||||
|
if(file.status === 'done') {
|
||||||
|
const files = fileList.map(item => {
|
||||||
|
const { uid, name, status } = item
|
||||||
|
const url = item.url || item.response.result.url
|
||||||
|
return { uid, name, status, url }
|
||||||
|
})
|
||||||
|
this.props.onChange && this.props.onChange(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBeforeUpload = (file) => {
|
||||||
|
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg' || file.type === 'image/gif';
|
||||||
|
if (!isJpgOrPng) {
|
||||||
|
message.error('只能上传格式为jpeg/png/gif的图片');
|
||||||
|
}
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
if (!isLt2M) {
|
||||||
|
message.error('图片必须小于2MB!');
|
||||||
|
}
|
||||||
|
return isJpgOrPng && isLt2M;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { previewVisible, previewImage, fileList, previewTitle } = this.state;
|
||||||
|
const {
|
||||||
|
// action换上你的服务器接口地址
|
||||||
|
action = '',
|
||||||
|
headers,
|
||||||
|
withCredentials = true,
|
||||||
|
maxLen = 1
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const uploadButton = (
|
||||||
|
<div>
|
||||||
|
<PlusOutlined />
|
||||||
|
<div className="ant-upload-text">上传</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImgCrop modalTitle="裁剪图片" modalOk="确定" modalCancel="取消" rotate={true} aspect={375/158}>
|
||||||
|
<Upload
|
||||||
|
fileList={fileList}
|
||||||
|
onPreview={this.handlePreview}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
name="file"
|
||||||
|
listType="picture-card"
|
||||||
|
className={styles.avatarUploader}
|
||||||
|
action={action}
|
||||||
|
withCredentials={withCredentials}
|
||||||
|
headers={{
|
||||||
|
'x-requested-with': localStorage.getItem('user') || '',
|
||||||
|
'authorization': localStorage.getItem('token') || '',
|
||||||
|
...headers
|
||||||
|
}}
|
||||||
|
beforeUpload={this.handleBeforeUpload}
|
||||||
|
>
|
||||||
|
{fileList.length >= maxLen ? null : uploadButton}
|
||||||
|
</Upload>
|
||||||
|
<Modal
|
||||||
|
visible={previewVisible}
|
||||||
|
title={previewTitle}
|
||||||
|
footer={null}
|
||||||
|
onCancel={this.handleCancel}
|
||||||
|
>
|
||||||
|
<img alt="example" style={{ width: '100%' }} src={previewImage} />
|
||||||
|
</Modal>
|
||||||
|
</ImgCrop>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PicturesWall
|
||||||
9
src/components/Upload/index.less
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
:global(.ant-upload-select-picture-card i) {
|
||||||
|
color: #999;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-upload-select-picture-card .ant-upload-text) {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
1049
src/components/Video/index.css
Normal file
17
src/components/Video/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Player, BigPlayButton } from 'video-react'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
const VideoPlayer = memo((props) => {
|
||||||
|
const {
|
||||||
|
poster,
|
||||||
|
url
|
||||||
|
} = props
|
||||||
|
return <div>
|
||||||
|
<Player playsInline poster={poster[0].url} src={url || 'https://gossv.vcg.com/cmsUploadVideo/creative/1移轴/7月移轴.mp4'}>
|
||||||
|
<BigPlayButton position="center" />
|
||||||
|
</Player>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default VideoPlayer
|
||||||
10
src/global.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import '~react-grid-layout/css/styles.css';
|
||||||
|
@import '~react-resizable/css/styles.css';
|
||||||
14
src/layouts/__tests__/index.test.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import BasicLayout from '..';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
describe('Layout: BasicLayout', () => {
|
||||||
|
it('Render correctly', () => {
|
||||||
|
const wrapper = renderer.create(<BasicLayout />);
|
||||||
|
expect(wrapper.root.children.length).toBe(1);
|
||||||
|
const outerLayer = wrapper.root.children[0];
|
||||||
|
expect(outerLayer.type).toBe('div');
|
||||||
|
const title = outerLayer.children[0];
|
||||||
|
expect(title.type).toBe('h1');
|
||||||
|
expect(title.children[0]).toBe('Yay! Welcome to umi!');
|
||||||
|
});
|
||||||
|
});
|
||||||
150
src/layouts/test.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { DragSource, DropTarget, DragDropContext } from "react-dnd";
|
||||||
|
import HTML5Backend from "react-dnd-html5-backend";
|
||||||
|
import { faTrashAlt, faArrowsAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
function Item(props) {
|
||||||
|
const {
|
||||||
|
// 这些 props 由 React DnD注入,参考`collect`函数定义
|
||||||
|
isDragging, connectDragSource, connectDragPreview, connectDropTarget,
|
||||||
|
// 这些是组件收到的 props
|
||||||
|
item, style = {}, find, move, change, remove, ...restProps
|
||||||
|
} = props;
|
||||||
|
const opacity = isDragging ? 0.5 : 1;
|
||||||
|
const onRemove = event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
remove(item);
|
||||||
|
}
|
||||||
|
return connectDropTarget( // 列表项本身作为 Drop 对象
|
||||||
|
connectDragPreview( // 整个列表项作为跟随拖动的影像
|
||||||
|
<div {...restProps} style={Object.assign(style, { opacity })}>
|
||||||
|
<p className="title">{item.title || "任务标题"}</p>
|
||||||
|
<ul className="oper-list">
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<li className="oper-item icon-move">
|
||||||
|
<FontAwesomeIcon icon={faArrowsAlt} />
|
||||||
|
</li>
|
||||||
|
) // 拖动图标作为 Drag 对象
|
||||||
|
}
|
||||||
|
<li className="oper-item" onClick={onRemove}>
|
||||||
|
<FontAwesomeIcon icon={faTrashAlt} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = "item";
|
||||||
|
const dragSpec = {
|
||||||
|
// 拖动开始时,返回描述 source 数据。后续通过 monitor.getItem() 获得
|
||||||
|
beginDrag: props => ({
|
||||||
|
id: props.id,
|
||||||
|
originalIndex: props.find(props.id).index
|
||||||
|
}),
|
||||||
|
// 拖动停止时,处理 source 数据
|
||||||
|
endDrag(props, monitor) {
|
||||||
|
const { id: droppedId, originalIndex } = monitor.getItem();
|
||||||
|
const didDrop = monitor.didDrop();
|
||||||
|
// source 是否已经放置在 target
|
||||||
|
if (!didDrop) {
|
||||||
|
return props.move(droppedId, originalIndex);
|
||||||
|
}
|
||||||
|
return props.change(droppedId, originalIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dragCollect = (connect, monitor) => ({
|
||||||
|
connectDragSource: connect.dragSource(), // 用于包装需要拖动的组件
|
||||||
|
connectDragPreview: connect.dragPreview(), // 用于包装需要拖动跟随预览的组件
|
||||||
|
isDragging: monitor.isDragging() // 用于判断是否处于拖动状态
|
||||||
|
});
|
||||||
|
const dropSpec = {
|
||||||
|
canDrop: () => false, // item 不处理 drop
|
||||||
|
hover(props, monitor) {
|
||||||
|
const { id: draggedId } = monitor.getItem();
|
||||||
|
const { id: overId } = props;
|
||||||
|
// 如果 source item 与 target item 不同,则交换位置并重新排序
|
||||||
|
if (draggedId !== overId) {
|
||||||
|
const { index: overIndex } = props.find(overId);
|
||||||
|
props.move(draggedId, overIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dropCollect = (connect, monitor) => ({
|
||||||
|
connectDropTarget: connect.dropTarget() // 用于包装需接收拖拽的组件
|
||||||
|
});
|
||||||
|
|
||||||
|
const DndItem = DropTarget(type, dropSpec, dropCollect)(
|
||||||
|
DragSource(type, dragSpec, dragCollect)(Item)
|
||||||
|
);
|
||||||
|
|
||||||
|
function List(props) {
|
||||||
|
let { list: propsList, activeItem, connectDropTarget } = props;
|
||||||
|
propsList = propsList.map(item => {
|
||||||
|
const isActive = activeItem.id === item.id;
|
||||||
|
item = isActive ? activeItem : item;
|
||||||
|
item.active = isActive;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
const [list, setList] = useState(propsList);
|
||||||
|
const find = id => {
|
||||||
|
const item = list.find(c => `${c.id}` === id);
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
index: list.indexOf(item)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const move = (id, toIndex) => {
|
||||||
|
const { item, index } = find(id);
|
||||||
|
list.splice(index, 1);
|
||||||
|
list.splice(toIndex, 0, item);
|
||||||
|
setList([...list]);
|
||||||
|
};
|
||||||
|
const change = (id, fromIndex) => {
|
||||||
|
const { index: toIndex } = find(id);
|
||||||
|
props.onDropEnd(list, fromIndex, toIndex);
|
||||||
|
};
|
||||||
|
const remove = item => {
|
||||||
|
const newList = list.filter(it => it.id !== item.id);
|
||||||
|
setList(newList);
|
||||||
|
props.onDelete(newList);
|
||||||
|
};
|
||||||
|
const onClick = event => {
|
||||||
|
const { id } = event.currentTarget;
|
||||||
|
const { item } = find(id);
|
||||||
|
props.onClick(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
return connectDropTarget(
|
||||||
|
<ul className="list">
|
||||||
|
{list.map((item, index) => (
|
||||||
|
<li
|
||||||
|
className={classnames("item", { active: item.active })}
|
||||||
|
key={item.id}
|
||||||
|
>
|
||||||
|
<div className="index">{index + 1}</div>
|
||||||
|
<DndItem
|
||||||
|
className="info"
|
||||||
|
id={`${item.id}`}
|
||||||
|
item={item}
|
||||||
|
find={find}
|
||||||
|
move={move}
|
||||||
|
change={change}
|
||||||
|
remove={remove}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DndList = DropTarget(type, {}, connect => ({
|
||||||
|
connectDropTarget: connect.dropTarget()
|
||||||
|
}))(List);
|
||||||
|
|
||||||
|
// 将 HTMLBackend 作为参数传给 DragDropContext
|
||||||
|
export default DragDropContext(HTML5Backend)(DndList);
|
||||||
25
src/pages/.umi/TitleWrapper.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class UmiReactTitle extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
document.title = this.props.route._title;
|
||||||
|
}
|
||||||
|
getTitle() {
|
||||||
|
const separator = '' || ' - ';
|
||||||
|
const title = this.props.route._title.split(separator).map(item => {
|
||||||
|
return formatMessage({
|
||||||
|
id: item.trim(),
|
||||||
|
defaultMessage: item.trim(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
return title.join(separator);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (document.title === this.props.route._title) {
|
||||||
|
document.title = this.props.route._title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/pages/.umi/dva.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import dva from 'dva';
|
||||||
|
import { Component } from 'react';
|
||||||
|
import createLoading from 'dva-loading';
|
||||||
|
import history from '@tmp/history';
|
||||||
|
|
||||||
|
let app = null;
|
||||||
|
|
||||||
|
export function _onCreate() {
|
||||||
|
const plugins = require('umi/_runtimePlugin');
|
||||||
|
const runtimeDva = plugins.mergeConfig('dva');
|
||||||
|
app = dva({
|
||||||
|
history,
|
||||||
|
|
||||||
|
...(runtimeDva.config || {}),
|
||||||
|
...(window.g_useSSR ? { initialState: window.g_initialData } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(createLoading());
|
||||||
|
(runtimeDva.plugins || []).forEach(plugin => {
|
||||||
|
app.use(plugin);
|
||||||
|
});
|
||||||
|
app.use(require('/Users/xujiang/Documents/qk/h5-visible-plat/node_modules/dva-immer/dist/index.js')());
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class _DvaContainer extends Component {
|
||||||
|
render() {
|
||||||
|
const app = getApp();
|
||||||
|
app.router(() => this.props.children);
|
||||||
|
return app.start()();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/pages/.umi/history.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// create history
|
||||||
|
const history = require('umi/lib/createHistory').default({
|
||||||
|
basename: window.routerBase,
|
||||||
|
});
|
||||||
|
window.g_history = history;
|
||||||
|
export default history;
|
||||||
3
src/pages/.umi/polyfills.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'core-js';
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
135
src/pages/.umi/router.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Router as DefaultRouter,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
StaticRouter,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import dynamic from 'umi/dynamic';
|
||||||
|
import renderRoutes from 'umi/lib/renderRoutes';
|
||||||
|
import history from '@@/history';
|
||||||
|
import { routerRedux, dynamic as _dvaDynamic } from 'dva';
|
||||||
|
|
||||||
|
const Router = routerRedux.ConnectedRouter;
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/editor',
|
||||||
|
component: __IS_BROWSER
|
||||||
|
? _dvaDynamic({
|
||||||
|
app: require('@tmp/dva').getApp(),
|
||||||
|
models: () => [
|
||||||
|
import(/* webpackChunkName: 'p__editor__models__editorModal.js' */ '/Users/xujiang/Documents/qk/h5-visible-plat/src/pages/editor/models/editorModal.js').then(
|
||||||
|
m => {
|
||||||
|
return { namespace: 'editorModal', ...m.default };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "p__editor" */ '../editor'),
|
||||||
|
LoadingComponent: require('/Users/xujiang/Documents/qk/h5-visible-plat/src/components/LoadingCp/index')
|
||||||
|
.default,
|
||||||
|
})
|
||||||
|
: require('../editor').default,
|
||||||
|
exact: true,
|
||||||
|
_title: 'test',
|
||||||
|
_title_default: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/preview',
|
||||||
|
component: __IS_BROWSER
|
||||||
|
? _dvaDynamic({
|
||||||
|
app: require('@tmp/dva').getApp(),
|
||||||
|
models: () => [
|
||||||
|
import(/* webpackChunkName: 'p__editor__models__editorModal.js' */ '/Users/xujiang/Documents/qk/h5-visible-plat/src/pages/editor/models/editorModal.js').then(
|
||||||
|
m => {
|
||||||
|
return { namespace: 'editorModal', ...m.default };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "p__editor__preview" */ '../editor/preview'),
|
||||||
|
LoadingComponent: require('/Users/xujiang/Documents/qk/h5-visible-plat/src/components/LoadingCp/index')
|
||||||
|
.default,
|
||||||
|
})
|
||||||
|
: require('../editor/preview').default,
|
||||||
|
exact: true,
|
||||||
|
_title: 'test',
|
||||||
|
_title_default: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/prevH5',
|
||||||
|
component: __IS_BROWSER
|
||||||
|
? _dvaDynamic({
|
||||||
|
app: require('@tmp/dva').getApp(),
|
||||||
|
models: () => [
|
||||||
|
import(/* webpackChunkName: 'p__editor__models__editorModal.js' */ '/Users/xujiang/Documents/qk/h5-visible-plat/src/pages/editor/models/editorModal.js').then(
|
||||||
|
m => {
|
||||||
|
return { namespace: 'editorModal', ...m.default };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "p__editor__preH5" */ '../editor/preH5'),
|
||||||
|
LoadingComponent: require('/Users/xujiang/Documents/qk/h5-visible-plat/src/components/LoadingCp/index')
|
||||||
|
.default,
|
||||||
|
})
|
||||||
|
: require('../editor/preH5').default,
|
||||||
|
exact: true,
|
||||||
|
_title: 'test',
|
||||||
|
_title_default: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: () =>
|
||||||
|
React.createElement(
|
||||||
|
require('/Users/xujiang/Documents/qk/h5-visible-plat/node_modules/umi-build-dev/lib/plugins/404/NotFound.js')
|
||||||
|
.default,
|
||||||
|
{ pagesPath: 'src/pages', hasRoutesInConfig: true },
|
||||||
|
),
|
||||||
|
_title: 'test',
|
||||||
|
_title_default: 'test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
window.g_routes = routes;
|
||||||
|
const plugins = require('umi/_runtimePlugin');
|
||||||
|
plugins.applyForEach('patchRoutes', { initialValue: routes });
|
||||||
|
|
||||||
|
export { routes };
|
||||||
|
|
||||||
|
export default class RouterWrapper extends React.Component {
|
||||||
|
unListen() {}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// route change handler
|
||||||
|
function routeChangeHandler(location, action) {
|
||||||
|
plugins.applyForEach('onRouteChange', {
|
||||||
|
initialValue: {
|
||||||
|
routes,
|
||||||
|
location,
|
||||||
|
action,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.unListen = history.listen(routeChangeHandler);
|
||||||
|
// dva 中 history.listen 会初始执行一次
|
||||||
|
// 这里排除掉 dva 的场景,可以避免 onRouteChange 在启用 dva 后的初始加载时被多执行一次
|
||||||
|
const isDva =
|
||||||
|
history.listen
|
||||||
|
.toString()
|
||||||
|
.indexOf('callback(history.location, history.action)') > -1;
|
||||||
|
if (!isDva) {
|
||||||
|
routeChangeHandler(history.location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unListen();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props || {};
|
||||||
|
return <Router history={history}>{renderRoutes(routes, props)}</Router>;
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/pages/.umi/umi.js
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import './polyfills';
|
||||||
|
import history from './history';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import findRoute, {
|
||||||
|
getUrlQuery,
|
||||||
|
} from '/Users/xujiang/Documents/qk/h5-visible-plat/node_modules/umi-build-dev/lib/findRoute.js';
|
||||||
|
|
||||||
|
// runtime plugins
|
||||||
|
const plugins = require('umi/_runtimePlugin');
|
||||||
|
window.g_plugins = plugins;
|
||||||
|
plugins.init({
|
||||||
|
validKeys: [
|
||||||
|
'patchRoutes',
|
||||||
|
'render',
|
||||||
|
'rootContainer',
|
||||||
|
'modifyRouteProps',
|
||||||
|
'onRouteChange',
|
||||||
|
'modifyInitialProps',
|
||||||
|
'initialProps',
|
||||||
|
'dva',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
plugins.use(require('../../../node_modules/umi-plugin-dva/lib/runtime'));
|
||||||
|
|
||||||
|
const app = require('@tmp/dva')._onCreate();
|
||||||
|
window.g_app = app;
|
||||||
|
|
||||||
|
// render
|
||||||
|
let clientRender = async () => {
|
||||||
|
window.g_isBrowser = true;
|
||||||
|
let props = {};
|
||||||
|
// Both support SSR and CSR
|
||||||
|
if (window.g_useSSR) {
|
||||||
|
// 如果开启服务端渲染则客户端组件初始化 props 使用服务端注入的数据
|
||||||
|
props = window.g_initialData;
|
||||||
|
} else {
|
||||||
|
const pathname = location.pathname;
|
||||||
|
const activeRoute = findRoute(require('@@/router').routes, pathname);
|
||||||
|
// 在客户端渲染前,执行 getInitialProps 方法
|
||||||
|
// 拿到初始数据
|
||||||
|
if (
|
||||||
|
activeRoute &&
|
||||||
|
activeRoute.component &&
|
||||||
|
activeRoute.component.getInitialProps
|
||||||
|
) {
|
||||||
|
const initialProps = plugins.apply('modifyInitialProps', {
|
||||||
|
initialValue: {},
|
||||||
|
});
|
||||||
|
props = activeRoute.component.getInitialProps
|
||||||
|
? await activeRoute.component.getInitialProps({
|
||||||
|
route: activeRoute,
|
||||||
|
isServer: false,
|
||||||
|
location,
|
||||||
|
...initialProps,
|
||||||
|
})
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rootContainer = plugins.apply('rootContainer', {
|
||||||
|
initialValue: React.createElement(require('./router').default, props),
|
||||||
|
});
|
||||||
|
ReactDOM[window.g_useSSR ? 'hydrate' : 'render'](
|
||||||
|
rootContainer,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const render = plugins.compose(
|
||||||
|
'render',
|
||||||
|
{ initialValue: clientRender },
|
||||||
|
);
|
||||||
|
|
||||||
|
const moduleBeforeRendererPromises = [];
|
||||||
|
// client render
|
||||||
|
if (__IS_BROWSER) {
|
||||||
|
Promise.all(moduleBeforeRendererPromises)
|
||||||
|
.then(() => {
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
window.console && window.console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export server render
|
||||||
|
let serverRender, ReactDOMServer;
|
||||||
|
if (!__IS_BROWSER) {
|
||||||
|
const { matchRoutes } = require('react-router-config');
|
||||||
|
const { StaticRouter } = require('react-router');
|
||||||
|
// difference: umi-history has query object
|
||||||
|
const { createLocation } = require('umi-history');
|
||||||
|
// don't remove, use stringify html map
|
||||||
|
const stringify = require('serialize-javascript');
|
||||||
|
const router = require('./router');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Load dynamicImport Component
|
||||||
|
* 2. Get Component initialProps function data
|
||||||
|
* return Component props
|
||||||
|
* @param pathname
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
const getInitialProps = async (pathname, props) => {
|
||||||
|
const { routes } = router;
|
||||||
|
const matchedComponents = matchRoutes(routes, pathname)
|
||||||
|
.map(({ route }) => {
|
||||||
|
if (route.component) {
|
||||||
|
return !route.component.preload
|
||||||
|
? // 同步
|
||||||
|
route.component
|
||||||
|
: // 异步,支持 dynamicImport
|
||||||
|
route.component.preload().then(component => component.default);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(c => c);
|
||||||
|
const loadedComponents = await Promise.all(matchedComponents);
|
||||||
|
|
||||||
|
// get Store
|
||||||
|
const initialProps = plugins.apply('modifyInitialProps', {
|
||||||
|
initialValue: {},
|
||||||
|
});
|
||||||
|
// support getInitialProps
|
||||||
|
const promises = loadedComponents.map(component => {
|
||||||
|
if (component && component.getInitialProps) {
|
||||||
|
return component.getInitialProps({
|
||||||
|
isServer: true,
|
||||||
|
...props,
|
||||||
|
...initialProps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
serverRender = async (ctx = {}) => {
|
||||||
|
// ctx.req.url may be `/bar?locale=en-US`
|
||||||
|
const [pathname] = (ctx.req.url || '').split('?');
|
||||||
|
// global
|
||||||
|
global.req = {
|
||||||
|
url: ctx.req.url,
|
||||||
|
};
|
||||||
|
const location = createLocation(ctx.req.url);
|
||||||
|
const activeRoute = findRoute(router.routes, pathname);
|
||||||
|
// omit component
|
||||||
|
const { component, ...restRoute } = activeRoute || {};
|
||||||
|
// router context hook
|
||||||
|
// get current router status 40x / 30x, share with server
|
||||||
|
const context = {};
|
||||||
|
// TODO: getInitialProps timeout handle
|
||||||
|
const initialData = await getInitialProps(pathname, {
|
||||||
|
route: restRoute,
|
||||||
|
// only exist in server
|
||||||
|
req: ctx.req || {},
|
||||||
|
res: ctx.res || {},
|
||||||
|
context,
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前路由(不包含 Layout)的 getInitialProps 有返回值
|
||||||
|
// Page 值为 undefined 时,有 getInitialProps 无返回,此时 return dva model
|
||||||
|
const pageData = initialData[initialData.length - 1];
|
||||||
|
if (pageData === undefined) {
|
||||||
|
initialData[initialData.length - 1] = plugins.apply('initialProps', {
|
||||||
|
initialValue: pageData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// reduce all match component getInitialProps
|
||||||
|
// in the same object key
|
||||||
|
// page data key will override layout key
|
||||||
|
const props = Array.isArray(initialData)
|
||||||
|
? initialData.reduce(
|
||||||
|
(acc, curr) => ({
|
||||||
|
...acc,
|
||||||
|
...curr,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const App = React.createElement(
|
||||||
|
StaticRouter,
|
||||||
|
{
|
||||||
|
location: ctx.req.url,
|
||||||
|
context,
|
||||||
|
},
|
||||||
|
React.createElement(router.default, props),
|
||||||
|
);
|
||||||
|
|
||||||
|
// render rootContainer for htmlTemplateMap
|
||||||
|
const rootContainer = plugins.apply('rootContainer', {
|
||||||
|
initialValue: App,
|
||||||
|
});
|
||||||
|
const htmlTemplateMap = {};
|
||||||
|
const matchPath = activeRoute ? activeRoute.path : undefined;
|
||||||
|
return {
|
||||||
|
htmlElement: matchPath ? htmlTemplateMap[matchPath] : '',
|
||||||
|
rootContainer,
|
||||||
|
matchPath,
|
||||||
|
g_initialData: props,
|
||||||
|
context,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// using project react-dom version
|
||||||
|
// https://github.com/facebook/react/issues/13991
|
||||||
|
ReactDOMServer = require('react-dom/server');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ReactDOMServer };
|
||||||
|
export default (__IS_BROWSER ? null : serverRender);
|
||||||
|
|
||||||
|
require('../../global.css');
|
||||||
|
|
||||||
|
// hot module replacement
|
||||||
|
if (__IS_BROWSER && module.hot) {
|
||||||
|
module.hot.accept('./router', () => {
|
||||||
|
clientRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
0
src/pages/.umi/umiExports.ts
Normal file
14
src/pages/__tests__/index.test.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Index from '..';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
|
||||||
|
describe('Page: index', () => {
|
||||||
|
it('Render correctly', () => {
|
||||||
|
const wrapper = renderer.create(<Index />);
|
||||||
|
expect(wrapper.root.children.length).toBe(1);
|
||||||
|
const outerLayer = wrapper.root.children[0];
|
||||||
|
expect(outerLayer.type).toBe('div');
|
||||||
|
expect(outerLayer.children.length).toBe(2);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
220
src/pages/editor/Container.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import React, { useState, useEffect, useRef, memo } from 'react'
|
||||||
|
import { Button, Input, Collapse, Slider, Empty, Popover, Modal, message } from 'antd'
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
PieChartOutlined,
|
||||||
|
ExpandOutlined,
|
||||||
|
MobileOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
CopyOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { connect } from 'dva'
|
||||||
|
import QRCode from 'qrcode.react'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import SourceBox from './SourceBox'
|
||||||
|
import TargetBox from './TargetBox'
|
||||||
|
import Calibration from 'components/Calibration'
|
||||||
|
import DynamicEngine from 'components/DynamicEngine'
|
||||||
|
import FormEditor from 'components/FormEditor'
|
||||||
|
import template from 'components/DynamicEngine/template'
|
||||||
|
import mediaTpl from 'components/DynamicEngine/mediaTpl'
|
||||||
|
import schema from 'components/DynamicEngine/schema'
|
||||||
|
import req from '@/utils/req'
|
||||||
|
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
const { Panel } = Collapse;
|
||||||
|
const { confirm } = Modal;
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
const Container = memo((props) => {
|
||||||
|
const [ scaleNum , setScale ] = useState(1)
|
||||||
|
|
||||||
|
const { pointData, curPoint, dispatch } = props
|
||||||
|
|
||||||
|
// 指定画布的id
|
||||||
|
let canvasId = 'js_canvas'
|
||||||
|
|
||||||
|
const iptRef = useRef(null)
|
||||||
|
|
||||||
|
const backSize = () => {
|
||||||
|
setScale(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateHeader = (text) => {
|
||||||
|
return <div><PieChartOutlined /> { text }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const toPreview = () => {
|
||||||
|
localStorage.setItem('pointData', JSON.stringify(pointData))
|
||||||
|
savePreview()
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(`/preview?tid=${props.location.query.tid}`)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSliderChange = (v) => {
|
||||||
|
setScale(prev => v >= 150 ? 1.5 : (v / 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSlider = (type) => {
|
||||||
|
if(type) {
|
||||||
|
setScale(prev => prev >= 1.5 ? 1.5 : (prev + 0.1))
|
||||||
|
}else {
|
||||||
|
setScale(prev => prev <= 0.5 ? 0.5 : (prev - 0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSave = (data) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/modPointData',
|
||||||
|
payload: { ...curPoint, item: {...curPoint.item, config: data} }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDel = (id) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/delPointData',
|
||||||
|
payload: { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
const { tid } = props.location.query || ''
|
||||||
|
return <QRCode value={`${location.protocol}//${location.host}/preview?tid=${tid}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveTpl = () => {
|
||||||
|
confirm({
|
||||||
|
title: '确定要保存吗?',
|
||||||
|
content: <div className={styles.saveForm}>
|
||||||
|
<div className={styles.formIpt}>
|
||||||
|
<span>模版名称:</span><Input ref={iptRef} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.formIpt}>
|
||||||
|
<span>访问链接:</span><Input disabled value="暂未开放,保存之后可以在模版库中访问" />
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
okText: '保存',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk() {
|
||||||
|
let name = iptRef.current.state.value
|
||||||
|
req.post('/visible/tpl/save', { name, tpl: pointData }).then(res => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const downLoadJson = () => {
|
||||||
|
const jsonStr = JSON.stringify(pointData)
|
||||||
|
const blob = new Blob([jsonStr], { type: "text/plain;charset=utf-8" })
|
||||||
|
saveAs(blob, "template.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePreview = () => {
|
||||||
|
const { tid } = props.location.query || ''
|
||||||
|
req.post('/visible/preview', { tid, tpl: pointData })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.editorWrap}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.logoArea}>
|
||||||
|
<div className={styles.backBtn}><ArrowLeftOutlined /></div>
|
||||||
|
<div className={styles.logo}>Dooring</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controlArea}>
|
||||||
|
<div className={styles.tit}>H5可视化编辑器</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.btnArea}>
|
||||||
|
<Button type="primary" style={{marginRight: '9px'}}>使用模版库</Button>
|
||||||
|
<Button type="primary" style={{marginRight: '9px'}} onClick={handleSaveTpl} disabled={!pointData.length}><DownloadOutlined />保存</Button>
|
||||||
|
<Button style={{marginRight: '9px'}} title="下载json文件" onClick={downLoadJson} disabled={!pointData.length}><CopyOutlined /></Button>
|
||||||
|
<Popover placement="bottom" title={null} content={content} trigger="click">
|
||||||
|
<Button style={{marginRight: '9px'}} onClick={savePreview} disabled={!pointData.length}><MobileOutlined /></Button>
|
||||||
|
</Popover>
|
||||||
|
<Button onClick={toPreview} disabled={!pointData.length}>预览</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.list} >
|
||||||
|
<div className={styles.searchBar}>
|
||||||
|
<Search placeholder="搜索组件" onSearch={value => console.log(value)} enterButton />
|
||||||
|
</div>
|
||||||
|
<div className={styles.componentList}>
|
||||||
|
<Collapse accordion ghost expandIconPosition="right">
|
||||||
|
<Panel header={generateHeader("基础组件")} key="1">
|
||||||
|
{
|
||||||
|
template.map((value,i) =>
|
||||||
|
<TargetBox item={value} key={i} canvasId={canvasId}>
|
||||||
|
<DynamicEngine {...value} config={schema[value.type].config} />
|
||||||
|
</TargetBox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Panel>
|
||||||
|
<Panel header={generateHeader("媒体组件")} key="2">
|
||||||
|
{
|
||||||
|
mediaTpl.map((value,i) =>
|
||||||
|
<TargetBox item={value} key={i} canvasId={canvasId}>
|
||||||
|
<DynamicEngine {...value} config={schema[value.type].config} />
|
||||||
|
</TargetBox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Panel>
|
||||||
|
<Panel header={generateHeader("可视化组件")} key="3">
|
||||||
|
正在开发中...
|
||||||
|
</Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.tickMark} id='calibration'>
|
||||||
|
<div className={styles.tickMarkTop}>
|
||||||
|
<Calibration direction='up' id='calibrationUp' multiple={scaleNum} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.tickMarkLeft}>
|
||||||
|
<Calibration direction='right' id='calibrationRight' multiple={scaleNum} />
|
||||||
|
</div>
|
||||||
|
<SourceBox scaleNum={scaleNum} canvasId={canvasId} />
|
||||||
|
<div className={styles.sliderWrap}>
|
||||||
|
<span className={styles.sliderBtn} onClick={handleSlider.bind(this, 0)}>-</span>
|
||||||
|
<Slider defaultValue={100} className={styles.slider} onChange={handleSliderChange} min={50} max={150} value={scaleNum * 100} />
|
||||||
|
<span className={styles.sliderBtn} onClick={handleSlider.bind(this, 1)}>+</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.backSize}><ExpandOutlined onClick={backSize} /></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.attrSetting}>
|
||||||
|
{
|
||||||
|
pointData.length && curPoint ?
|
||||||
|
<>
|
||||||
|
<div className={styles.tit}>属性设置</div>
|
||||||
|
<FormEditor
|
||||||
|
config={curPoint.item.editableEl}
|
||||||
|
uid={curPoint.id}
|
||||||
|
defaultValue={curPoint.item.config}
|
||||||
|
onSave={handleFormSave}
|
||||||
|
onDel={handleDel}
|
||||||
|
/>
|
||||||
|
</> :
|
||||||
|
<div style={{paddingTop: '100px'}}><Empty /></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(({ editorModal: { pointData, curPoint } }) => ({
|
||||||
|
pointData, curPoint
|
||||||
|
}))(Container)
|
||||||
111
src/pages/editor/SourceBox.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { memo, useEffect, useState } from 'react'
|
||||||
|
import { useDrop } from 'react-dnd'
|
||||||
|
import Draggable from 'react-draggable'
|
||||||
|
import GridLayout from 'react-grid-layout'
|
||||||
|
import { connect } from 'dva'
|
||||||
|
import DynamicEngine from 'components/DynamicEngine'
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
const SourceBox = memo((props) => {
|
||||||
|
const { pointData, scaleNum, canvasId, dispatch } = props
|
||||||
|
const [canvasRect, setCanvasRect] = useState([])
|
||||||
|
const [{ isOver }, drop] = useDrop({
|
||||||
|
accept: [],
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
canDrop: monitor.canDrop(),
|
||||||
|
item: monitor.getItem()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const dragStop = (layout, oldItem, newItem, placeholder, e, element) => {
|
||||||
|
const curPointData = pointData.filter(item => item.id === newItem.i)[0]
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/modPointData',
|
||||||
|
payload: { ...curPointData, point: newItem }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragStart = (layout, oldItem, newItem, placeholder, e, element) => {
|
||||||
|
const curPointData = pointData.filter(item => item.id === newItem.i)[0]
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/modPointData',
|
||||||
|
payload: { ...curPointData }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResizeStop = (layout, oldItem, newItem, placeholder, e, element) => {
|
||||||
|
const curPointData = pointData.filter(item => item.id === newItem.i)[0]
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/modPointData',
|
||||||
|
payload: { ...curPointData, point: newItem }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let { width, height } = document.getElementById(canvasId).getBoundingClientRect()
|
||||||
|
setCanvasRect([width, height])
|
||||||
|
}, [canvasId])
|
||||||
|
|
||||||
|
const opacity = isOver ? 0.7 : 1
|
||||||
|
const backgroundColor = isOver ? 1 : 0.7
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable handle=".js_box">
|
||||||
|
<div className={styles.canvasBox}>
|
||||||
|
<div style={{transform: `scale(${scaleNum})`, position: 'relative', width: '100%', height: '100%'}}>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={canvasId}
|
||||||
|
className={styles.canvas}
|
||||||
|
style={{
|
||||||
|
opacity, backgroundColor
|
||||||
|
}}
|
||||||
|
ref={drop}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="js_box"
|
||||||
|
style={{
|
||||||
|
width: '12px',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: '0 6px 6px 0',
|
||||||
|
backgroundColor: '#2f54eb',
|
||||||
|
right: '-12px',
|
||||||
|
top: '0',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'move'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
pointData.length > 0 ?
|
||||||
|
<GridLayout
|
||||||
|
className={styles.layout}
|
||||||
|
cols={24}
|
||||||
|
rowHeight={2}
|
||||||
|
width={canvasRect[0]}
|
||||||
|
margin={[0,0]}
|
||||||
|
onDragStop={dragStop}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onResizeStop={onResizeStop}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
pointData.map(value =>
|
||||||
|
<div className={styles.dragItem} key={value.id} data-grid={value.point}>
|
||||||
|
<DynamicEngine {...value.item} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</GridLayout> : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(({ editorModal: { pointData } }) => ({
|
||||||
|
pointData
|
||||||
|
}))(SourceBox)
|
||||||
61
src/pages/editor/TargetBox.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React, { useMemo, memo } from 'react'
|
||||||
|
import { useDrag } from 'react-dnd'
|
||||||
|
import { connect } from 'dva'
|
||||||
|
import schema from 'components/DynamicEngine/schema'
|
||||||
|
import { uuid } from '@/utils/tool';
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
const TargetBox = memo((props) => {
|
||||||
|
const { item, dispatch, canvasId, pointData } = props
|
||||||
|
const [{ isDragging }, drag] = useDrag({
|
||||||
|
item: { type: item.type, config: schema[item.type].config, h: item.h, editableEl: schema[item.type].editData },
|
||||||
|
collect: (monitor) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
// begin(monitor) {
|
||||||
|
// getStartPoint(monitor.getSourceClientOffset())
|
||||||
|
// },
|
||||||
|
end(item, monitor) {
|
||||||
|
let parentDiv = document.getElementById(canvasId),
|
||||||
|
pointRect = parentDiv.getBoundingClientRect(),
|
||||||
|
top = pointRect.top,
|
||||||
|
pointEnd = monitor.getSourceClientOffset(),
|
||||||
|
y = pointEnd.y - top,
|
||||||
|
col = 24, // 网格列数
|
||||||
|
cellHeight = 2
|
||||||
|
// 转换成网格规则的坐标和大小
|
||||||
|
let gridY = Math.ceil(y / cellHeight)
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'editorModal/addPointData',
|
||||||
|
payload: {
|
||||||
|
id: uuid(6, 10),
|
||||||
|
item,
|
||||||
|
point: { i: `x-${pointData.length}`, x: 0, y: gridY, w: col, h: item.h, isBounded: true }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
opacity: isDragging ? 0.4 : 1,
|
||||||
|
cursor: 'move'
|
||||||
|
}),
|
||||||
|
[isDragging],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.module}
|
||||||
|
style={{ ...containerStyle}}
|
||||||
|
ref={drag}
|
||||||
|
>
|
||||||
|
{ props.children }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(({ editorModal: { pointData } }) => ({
|
||||||
|
pointData
|
||||||
|
}))(TargetBox)
|
||||||
20
src/pages/editor/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
|
||||||
|
import Container from './Container'
|
||||||
|
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
function BasicLayout(props) {
|
||||||
|
return (
|
||||||
|
<div className={styles.layout}>
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<Container {...props} />
|
||||||
|
</DndProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BasicLayout;
|
||||||
235
src/pages/editor/index.less
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
.layout{
|
||||||
|
.editorWrap {
|
||||||
|
background-color: rgba(245,245,245,1);
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
.logoArea {
|
||||||
|
width: 300px;
|
||||||
|
.backBtn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 10px;
|
||||||
|
margin-right: 26px;
|
||||||
|
background-color: rgba(222, 224, 230, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
display: inline-block;
|
||||||
|
width: 105px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.controlArea {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
.tit {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btnArea {
|
||||||
|
width: 400px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
// height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
.list{
|
||||||
|
width: 380px;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 2px 0px 10px rgba(0,0,0,0.2);
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #fff;
|
||||||
|
overflow: scroll;
|
||||||
|
.searchBar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.module{
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: border .3s;
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid #06c;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tickMark{
|
||||||
|
width: calc(100% - 780px);
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
.tickMarkTop {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.tickMarkLeft {
|
||||||
|
width: 50px;
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.canvasBox{
|
||||||
|
width: 375px;
|
||||||
|
min-height: 664px;
|
||||||
|
height: 664px;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -200px;
|
||||||
|
top: 100px;
|
||||||
|
.canvas{
|
||||||
|
position: relative;
|
||||||
|
width: 375px;
|
||||||
|
min-height: 664px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 2px 0px 10px rgba(0,0,0,0.2);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
transition: transform 300ms ease-out;
|
||||||
|
.dragItem {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: move;
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid #06c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sliderWrap {
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
right: 60px;
|
||||||
|
bottom: 90px;
|
||||||
|
.slider {
|
||||||
|
width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.sliderBtn {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin: 0 12px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.backSize {
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
bottom: 95px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.attrSetting {
|
||||||
|
width: 400px;
|
||||||
|
padding: 20px 0 20px 20px;
|
||||||
|
background:#fff;
|
||||||
|
height: 100vh;
|
||||||
|
box-shadow:-2px 0px 4px 0px rgba(0,0,0,0.1);
|
||||||
|
.tit {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.posWrap {
|
||||||
|
padding: 20px;
|
||||||
|
.posItem {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0,0,0, .5);
|
||||||
|
.posTit {
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
.pos {
|
||||||
|
margin-right: 40px;
|
||||||
|
color: #000;
|
||||||
|
&::before {
|
||||||
|
content: attr(data-flag);
|
||||||
|
color: rgba(0,0,0,.4);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphWrap {
|
||||||
|
.graphTable {
|
||||||
|
.iptControl {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
.iptLabel {
|
||||||
|
width: 108px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: rgba(0,0,0,.5);
|
||||||
|
}
|
||||||
|
.cpSelector {
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.colorSelector {
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 42px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveForm {
|
||||||
|
padding-top: 10px;
|
||||||
|
.formIpt {
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
line-height: 2.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
57
src/pages/editor/models/editorModal.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// import * as mesService from '../services/editorService'
|
||||||
|
// import { router } from 'umi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespace: 'editorModal',
|
||||||
|
state: {
|
||||||
|
pointData: [],
|
||||||
|
curPoint: null,
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
addPointData(state, { payload }) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pointData: [...state.pointData, payload],
|
||||||
|
curPoint: payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modPointData(state, { payload }) {
|
||||||
|
const { id } = payload
|
||||||
|
const pointData = state.pointData.map(item => {
|
||||||
|
if(item.id === id) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
return { ...item }
|
||||||
|
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pointData,
|
||||||
|
curPoint: payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delPointData(state, { payload }) {
|
||||||
|
const { id } = payload
|
||||||
|
const pointData = state.pointData.filter(item => item.id !== id)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pointData,
|
||||||
|
curPoint: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
setup({ dispatch, history }) {
|
||||||
|
return history.listen(({ pathname, query }) => {
|
||||||
|
// Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、
|
||||||
|
// 服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
|
||||||
|
if (pathname !== '/dataModel/view') {
|
||||||
|
dispatch({ type: 'initDetail', data: {} })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/pages/editor/preH5.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Header, Text, Notice, Qrcode, Footer, Image, List
|
||||||
|
} from '@/components/DynamicEngine/components'
|
||||||
|
import Carousel from '@/components/Carousel'
|
||||||
|
import Tab from '@/components/Tab'
|
||||||
|
|
||||||
|
export default function PrevH5(props) {
|
||||||
|
return <div style={{width: '375px', margin: '20px auto', border: '10px solid #000', borderRadius: '20px', minHeight: '664px'}}>
|
||||||
|
<Notice />
|
||||||
|
<Header />
|
||||||
|
<Carousel />
|
||||||
|
<Text color="#000" />
|
||||||
|
<Text text="专注于前端技术一站式配置平台" lineHeight={1.2} color="#ccc" fontSize={12} />
|
||||||
|
<Qrcode />
|
||||||
|
<Image />
|
||||||
|
<Tab />
|
||||||
|
<List />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
66
src/pages/editor/preview.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React, { memo, useEffect, useState } from 'react'
|
||||||
|
import GridLayout from 'react-grid-layout'
|
||||||
|
import DynamicEngine from 'components/DynamicEngine'
|
||||||
|
import req from '@/utils/req'
|
||||||
|
import styles from './index.less'
|
||||||
|
|
||||||
|
// 可视化组件类型
|
||||||
|
// const componentTypes = ['Column', 'Pie']
|
||||||
|
|
||||||
|
const pcStyle = {
|
||||||
|
width: 395,
|
||||||
|
margin: '20px auto',
|
||||||
|
border: '10px solid #000',
|
||||||
|
borderRadius: '20px',
|
||||||
|
height: '684px',
|
||||||
|
overflow: 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
const PreviewPage = memo((props) => {
|
||||||
|
const [pointData, setPointData] = useState(() => {
|
||||||
|
let pointDataStr = localStorage.getItem('pointData')
|
||||||
|
let points
|
||||||
|
|
||||||
|
try{
|
||||||
|
points = JSON.parse(pointDataStr) || []
|
||||||
|
}catch(err) {
|
||||||
|
points = []
|
||||||
|
}
|
||||||
|
return points.map(item => ({...item, point: {...item.point, isDraggable: false, isResizable: false } }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const vw = window.innerWidth
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { tid } = props.location.query
|
||||||
|
req.get('/visible/preview/get', { params: { tid } }).then(res => {
|
||||||
|
setPointData(res)
|
||||||
|
}).catch(err => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close()
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={vw > 800 ? pcStyle : {}}>
|
||||||
|
<GridLayout
|
||||||
|
className={styles.layout}
|
||||||
|
cols={24}
|
||||||
|
rowHeight={2}
|
||||||
|
width={vw > 800 ? 375 :vw }
|
||||||
|
margin={[0,0]}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
pointData.map(value =>
|
||||||
|
<div className={styles.dragItem} key={value.id} data-grid={value.point}>
|
||||||
|
<DynamicEngine {...value.item} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</GridLayout>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PreviewPage
|
||||||
5
src/pages/editor/services/editorService.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import req from '@/utils/req'
|
||||||
|
|
||||||
|
export function getTemplate(data) {
|
||||||
|
return req('/test', { method: 'GET', params: data })
|
||||||
|
}
|
||||||
49
src/utils/req.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { message } from 'antd'
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: 'xxxxxxx',
|
||||||
|
timeout: 10000,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加请求拦截器
|
||||||
|
instance.interceptors.request.use(function (config) {
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
config.headers = {
|
||||||
|
'x-requested-with': localStorage.getItem('user') || '',
|
||||||
|
'authorization': localStorage.getItem('token') || ''
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, function (error) {
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
instance.interceptors.response.use(function (response) {
|
||||||
|
// 对响应数据做点什么, 这里是自定义的信息头,用来给前端说明展示的信息
|
||||||
|
if(response.headers['x-x-x'] === 'xxx') {
|
||||||
|
message.success(response.data.msg);
|
||||||
|
}
|
||||||
|
return response.data.result;
|
||||||
|
}, function (error) {
|
||||||
|
// 对响应错误做点什么
|
||||||
|
const { response } = error;
|
||||||
|
if(response.status === 404) {
|
||||||
|
message.error('请求资源未发现');
|
||||||
|
}else if(response.status === 403) {
|
||||||
|
message.error(response.data.msg, () => {
|
||||||
|
window.location.href = '/admin/login'
|
||||||
|
});
|
||||||
|
}else {
|
||||||
|
message.error(response.data.msg);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default instance
|
||||||
38
src/utils/tool.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 生成uuid
|
||||||
|
function uuid(len, radix) {
|
||||||
|
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
||||||
|
let uuid = [], i;
|
||||||
|
radix = radix || chars.length;
|
||||||
|
|
||||||
|
if (len) {
|
||||||
|
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
|
||||||
|
} else {
|
||||||
|
let r;
|
||||||
|
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
|
||||||
|
uuid[14] = '4';
|
||||||
|
|
||||||
|
for (i = 0; i < 36; i++) {
|
||||||
|
if (!uuid[i]) {
|
||||||
|
r = 0 | Math.random()*16;
|
||||||
|
uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将rgba字符串对象转化为rgba对象
|
||||||
|
function rgba2Obj(rgba = '') {
|
||||||
|
let reg = /rgba\((\d+),(\d+),(\d+),(\d+)\)/g
|
||||||
|
let rgbaObj = {}
|
||||||
|
rgba.replace(reg, (m, r, g, b, a) => {
|
||||||
|
rgbaObj = {r, g, b, a}
|
||||||
|
})
|
||||||
|
return rgbaObj
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
uuid,
|
||||||
|
rgba2Obj
|
||||||
|
}
|
||||||
12
webpack.config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 不是真实的 webpack 配置,仅为兼容 webstorm 和 intellij idea 代码跳转
|
||||||
|
* ref: https://github.com/umijs/umi/issues/1109#issuecomment-423380125
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': require('path').resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||