clear commit

This commit is contained in:
bac-joker 2020-08-27 17:18:57 +08:00
commit 7b0269a428
836 changed files with 100821 additions and 0 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

20
.eslintrc.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
extends: [
'@webank/eslint-config-webank/vue',
],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
//
// Vue: false
},
rules: {
'no-plusplus': 'off',
'no-bitwise': 'off',
'vue/comment-directive': 'off',
'no-param-reassign': 'off',
'func-names': 'off',
'class-methods-use-this': 'off'
}
};

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
.idea
.git
.vscode
.history
.cache
node_modules
npm-debug.log
/packages/fes-template/dist
/packages/fes-doc/docs/.vuepress/dist

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present harrywan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7
lerna.json Normal file
View File

@ -0,0 +1,7 @@
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "npm"
}

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "fes",
"description": "一个好用的前端管理台快速开发框架",
"preferGlobal": true,
"scripts": {
"bootstrap": "lerna bootstrap"
},
"author": "harrywan qlin",
"license": "MIT",
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"repository": {
"type": "git",
"url": "https://github.com/WeBankFinTech/fes.git"
},
"dependencies": {
"lerna": "^3.18.4"
},
"devDependencies": {
"@webank/eslint-config-webank": "^0.1.6"
}
}

21
packages/fes-cli/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present harrywan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
packages/fes-cli/bin/index.js Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env node
const commander = require('commander');
const pkg = require('../package.json');
const generateConfig = require('../build/helpers/config');
const log = require('../build/helpers/log');
commander.usage('<command> [options]')
.version(pkg.version)
.option('-e, --env <env>', '配置环境 local(本地) | sit(测试) | prod(生产)')
.description(pkg.description);
commander.command('init [name]')
.description('创建项目')
.action(async (name) => {
const projectInit = require('../build/tasks/init');
const config = generateConfig('init');
await projectInit(config, name);
});
commander.command('update')
.description('将 fes2 项目升级到 fes3')
.action(() => {
const update = require('../build/tasks/update');
const config = generateConfig('update');
update(config);
});
commander.command('route')
.description('根据当前项目结构自动生成 route')
.action(() => {
const route = require('../build/tasks/route');
const config = generateConfig('route');
route(config);
});
commander.command('components')
.description('预编译 src/components 里面的组件')
.action(() => {
const components = require('../build/tasks/components');
const config = generateConfig('components');
components(config);
});
commander.command('dev')
.description('开发调试, 默认 local')
.action(() => {
const dev = require('../build/tasks/dev');
const config = generateConfig('dev', commander.env || 'local');
dev(config);
});
commander.command('build')
.description('打包压缩,默认 prod')
.action(() => {
const build = require('../build/tasks/build');
const config = generateConfig('build', commander.env || 'prod');
build(config);
});
commander.parse(process.argv);
if (!process.argv.slice(2).length) {
commander.outputHelp((text) => {
log.message(text);
return '';
});
}

View File

@ -0,0 +1,8 @@
const autoprefixer = require('autoprefixer');
const browsers = require('../helpers/browser');
module.exports = {
plugins: [
autoprefixer({ browsers })
]
};

View File

@ -0,0 +1,504 @@
const path = require('path');
const fs = require('fs');
const merge = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlPlugin = require('html-webpack-plugin');
const browsers = require('../helpers/browser');
module.exports = function webpackConfig(configs, webpack, mode) {
let template = path.resolve(
configs.folders.PROJECT_DIR,
'./src/index.html'
);
if (!fs.existsSync(template)) {
template = path.resolve(configs.folders.FES_DIR, './src/index.html');
}
const isDev = mode === 'dev';
const isBuild = mode === 'build';
const projectNodeModulesDir = path.resolve(
configs.folders.PROJECT_DIR,
'./node_modules'
);
const cliNodeModulesDir = path.resolve(
configs.folders.CLI_DIR,
'./node_modules'
);
const nodeModulesDir = ['node_modules', projectNodeModulesDir, cliNodeModulesDir];
const presets = [
[
path.resolve(cliNodeModulesDir, '@babel/preset-env'),
{
modules: false,
useBuiltIns: 'entry',
corejs: 3,
targets: {
browsers
}
}
]
];
const plugins = [
path.resolve(cliNodeModulesDir, '@babel/plugin-proposal-object-rest-spread'),
path.resolve(cliNodeModulesDir, '@babel/plugin-syntax-dynamic-import')
];
const cssloaders = [
isDev
? {
loader: 'vue-style-loader',
options: {
sourceMap: false,
shadowMode: false
}
}
: {
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../'
}
},
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
config: {
path: path.resolve(configs.folders.CLI_DIR, 'build/configs/postcss.config.js')
},
sourceMap: false
}
}
];
const baseConfig = {
mode: isDev ? 'development' : 'production',
context: path.resolve(configs.folders.PROJECT_DIR),
entry: {
app: [
path.resolve(configs.folders.CLI_DIR, './node_modules/babel-polyfill'),
// path.resolve(configs.folders.CLI_DIR, './build/utils/create-nonce'),
path.resolve(configs.folders.FES_DIR, './src/app.js')
]
},
resolve: {
extensions: ['.js', '.fes', '.vue', '.json'],
modules: nodeModulesDir,
alias: {
projectRoot: configs.folders.PROJECT_DIR,
'@': path.resolve(configs.folders.PROJECT_DIR, 'src'),
'@@': path.resolve(configs.folders.FES_DIR, 'src'),
assets: path.resolve(
configs.folders.PROJECT_DIR,
'./src/assets/'
),
vue$: 'vue/dist/vue.esm.js'
}
},
resolveLoader: {
modules: nodeModulesDir
},
output: {
globalObject: 'this',
filename: isDev ? 'js/[name].js' : 'js/[name].[contenthash:8].js',
chunkFilename: isDev ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].js',
path: configs.folders.PROJECT_DIST_DIR,
publicPath: isDev ? '/' : './'
},
module: {
noParse: /^(vue|vue-router|vuex|vuex-router-sync|axios|@webank\/fes-ui)$/,
rules: [
/* config.module.rule('vue') */
{
test: /\.vue|fes$/,
use: [
{
loader: 'cache-loader',
options: {
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/vue-loader')
}
},
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
},
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/vue-loader')
}
}
]
},
/* config.module.rule('images') */
{
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: isDev ? 'img/[name].[ext]' : 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('svg') */
{
test: /\.(svg)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
name: isDev ? 'img/[name].[ext]' : 'img/[name].[hash:8].[ext]'
}
}
]
},
/* config.module.rule('media') */
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: isDev ? 'media/[name].[ext]' : 'media/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('fonts') */
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: isDev ? 'fonts/[name].[ext]' : 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
/* config.module.rule('css') */
{
test: /\.css$/,
use: cssloaders
},
/* config.module.rule('postcss') */
{
test: /\.p(ost)?css$/,
use: cssloaders
},
/* config.module.rule('scss') */
{
test: /\.scss$/,
use: cssloaders.concat([
{
loader: 'sass-loader',
options: {
sourceMap: false
}
}
])
},
/* config.module.rule('sass') */
{
test: /\.sass$/,
use: cssloaders.concat([
{
loader: 'sass-loader',
options: {
sourceMap: false,
indentedSyntax: true
}
}
])
},
/* config.module.rule('less') */
{
test: /\.less$/,
use: cssloaders.concat([
{
loader: 'less-loader',
options: {
sourceMap: false,
javascriptEnabled: true
}
}
])
},
/* config.module.rule('stylus') */
{
test: /\.styl(us)?$/,
use: cssloaders.concat([
{
loader: 'stylus-loader',
options: {
sourceMap: false,
preferPathResolver: 'webpack'
}
}
])
},
/* config.module.rule('js') */
{
test: /\.m?jsx?$/,
use: [
{
loader: 'cache-loader',
options: {
cacheDirectory: path.resolve(configs.folders.PROJECT_DIR, 'node_modules/.cache/babel-loader')
}
},
{
loader: 'thread-loader'
},
{
loader: 'babel-loader',
options: {
presets,
plugins
}
}
]
}
]
},
devtool: isDev && 'cheap-module-eval-source-map',
plugins: [
/* config.plugin('progress') */
new webpack.ProgressPlugin(),
/* config.plugin('vue-loader') */
new VueLoaderPlugin(),
/* config.plugin('define') */
new webpack.DefinePlugin({
'process.privateFesEnv': {
env: `"${configs.env}"`
},
'process.env': {
env: JSON.stringify(configs.env),
command: JSON.stringify(configs.command)
}
}),
/* config.plugin('clean dist') */
isBuild && new CleanWebpackPlugin(),
/* config.plugin('extract-css') */
isBuild
&& new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css'
}),
/* config.plugin('Copy static') */
isBuild
&& new CopyPlugin([
{
from: configs.folders.PROJECT_STATIC_DIR,
to: path.resolve(
configs.folders.PROJECT_DIST_DIR,
'static'
)
}
]),
/* config.plugin('optimize-css') */
isBuild
&& new OptimizeCssnanoPlugin({
sourceMap: false,
cssnanoOptions: {
preset: [
'default',
{
mergeLonghand: false,
cssDeclarationSorter: false
}
]
}
}),
/* config.plugin('hash-module-ids') */
isBuild
&& new webpack.HashedModuleIdsPlugin({
hashDigest: 'hex'
}),
/* config.plugin('固定一下 chunk id') */
isBuild
&& new webpack.NamedChunksPlugin((chunk) => {
if (chunk.name) {
return chunk.name;
}
// eslint-disable-next-line
const hash = require('hash-sum');
const joinedHash = hash(
Array.from(chunk.modulesIterable, m => m.id).join('_')
);
return `chunk-${joinedHash}`;
}),
/* config.plugin('Copyright') */
isBuild
&& new webpack.BannerPlugin(
'Created By MumbleFe. Copyright © 2015 - 2018 WeBank.'
),
/* config.plugin('case-sensitive-paths') */
new CaseSensitivePathsPlugin(),
/* config.plugin('friendly-errors') */
new FriendlyErrorsPlugin(),
/* config.plugin('index.html') */
new HtmlPlugin({
template,
minify: isBuild && {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
collapseBooleanAttributes: true,
removeScriptTypeAttributes: true
}
})
]
};
if (isBuild) {
baseConfig.optimization = {
minimizer: [
new TerserPlugin({
test: /\.m?js(\?.*)?$/i,
chunkFilter: () => true,
warningsFilter: () => true,
extractComments: false,
sourceMap: true,
cache: true,
cacheKeys: defaultCacheKeys => defaultCacheKeys,
parallel: true,
include: undefined,
exclude: undefined,
minify: undefined,
terserOptions: {
output: {
comments: /^\**!|@preserve|@license|@cc_on/i
},
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true
},
mangle: {
safari10: true
}
}
})
],
splitChunks: {
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
},
runtimeChunk: true
};
}
baseConfig.plugins = baseConfig.plugins.filter(plu => plu !== false);
let advancedConfig = {};
const projectWebpackConfigFile = path.resolve(configs.folders.PROJECT_DIR, 'webpack.config.js');
if (fs.existsSync(projectWebpackConfigFile)) {
console.log('[init] 加载项目个性webpack配置文件');
// eslint-disable-next-line
advancedConfig = require(projectWebpackConfigFile)(mode, configs, webpack);
}
return merge(baseConfig, advancedConfig);
};

View File

@ -0,0 +1 @@
module.exports = ['>1%', 'last 2 versions', 'safari >= 7', 'ie >= 9'];

View File

@ -0,0 +1,62 @@
const path = require('path');
const fs = require('fs');
const log = require('./log');
function generateConfig(command, env) {
// cli目录
const CLI_DIR = path.dirname(path.dirname(fs.realpathSync(process.argv[1])));
// 解决git-bash目录问题
const PROJECT_DIR = process.env.PWD || process.cwd();
const FES_DIR = path.resolve(PROJECT_DIR, './node_modules/@webank/fes-core');
const PROJECT_DIST_DIR = path.resolve(PROJECT_DIR, 'dist');
const PROJECT_CACHE_DIR = path.resolve(PROJECT_DIR, './.cache');
const PROJECT_PAGE_DIR = path.resolve(PROJECT_DIR, './src/pages');
const PROJECT_CPN_DIR = path.resolve(PROJECT_DIR, './src/components');
const PROJECT_STATIC_DIR = path.join(PROJECT_DIR, './src/static');
const projectName = path.basename(PROJECT_DIR);
const fesConfigFile = path.join(PROJECT_DIR, 'fes.config.js');
const config = {
command,
env,
ports: {
server: 5000,
liveReload: 35729
},
projectName,
folders: {
CLI_DIR,
FES_DIR,
PROJECT_DIR,
PROJECT_STATIC_DIR,
PROJECT_DIST_DIR,
PROJECT_CACHE_DIR,
PROJECT_PAGE_DIR,
PROJECT_CPN_DIR
}
};
if (fs.existsSync(fesConfigFile)) {
try {
// eslint-disable-next-line
const fesCofig = require(path.join(config.folders.PROJECT_DIR, 'fes.config.js'));
config.CDN = fesCofig.env[config.env].cdn;
config.needCDN = !!config.CDN;
} catch (e) {
config.needCDN = false;
}
}
if (!config.needCDN) {
if (config.command === 'dev' || config.command === 'build') {
log.warn('项目没有配置cdn打包之后将不会请求cdn的地址请开发者注意');
}
}
return config;
}
module.exports = generateConfig;

View File

@ -0,0 +1,44 @@
const
http = require('http');
const webpack = require('webpack');
const express = require('express');
const opn = require('opn');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackDevMiddleware = require('webpack-dev-middleware');
const initMock = require('../mock/init.js');
module.exports = function createDevServer(port, defaultConfig) {
defaultConfig.entry.app.unshift('webpack-hot-middleware/client?reload=true');
defaultConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
defaultConfig.plugins.push(new webpack.NamedModulesPlugin());
const app = express();
const compiler = webpack(defaultConfig);
// devServer 自带支持,添加自定义插件。
app.use(webpackDevMiddleware(compiler, {
lazy: false,
watchOptions: {
aggregateTimeout: 300,
poll: 1000
},
stats: {
colors: true,
chunks: false,
timings: true
},
publicPath: defaultConfig.output.publicPath
}));
app.use(webpackHotMiddleware(compiler));
app.use('/static', express.static('src/static'));
// 初始化Mock数据
initMock(app);
opn(`http://localhost:${port}`);
http.createServer(app).listen(port);
};

View File

@ -0,0 +1,25 @@
const net = require('net');
function checkout(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
resolve(checkout(port + 1));
}
});
server.once('listening', () => {
server.close(() => {
resolve(port);
});
});
server.listen(port);
});
}
module.exports = function getPort(basePort) {
basePort = basePort || 5000;
return checkout(basePort);
};

View File

@ -0,0 +1,13 @@
const chalk = require('chalk');
module.exports = {
error(msg) {
return console.log(chalk.red(msg));
},
warn(msg) {
return console.log(chalk.yellow(msg));
},
message(msg) {
return console.log(chalk.cyan(msg));
}
};

View File

@ -0,0 +1,142 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const httpProxy = require('http-proxy');
const url = require('url');
const util = require('./util');
const proxy = httpProxy.createProxyServer();
global.router = express.Router();
/**
* 数据模拟函数
*/
function cgiMock() {
// eslint-disable-next-line
const option = getOption(arguments);
if (!option.url || !option.result) {
return;
}
// option.method is one of ['get','post','delete','put'...]
const method = option.method || 'use';
global.router[method.toLowerCase()](option.url, (req, res) => {
setTimeout(() => {
// set header
res.set(option.headers);
// set Content-Type
option.type && res.type(option.type);
// set status code
res.status(option.statusCode);
// set cookie
util.each(option.cookies, (item) => {
const name = item.name;
const value = item.value;
delete item.name;
delete item.value;
res.cookie(name, value, item);
});
// do result
if (util.isFunction(option.result)) {
option.result(req, res);
} else if (util.isArray(option.result) || util.isObject(option.result)) {
!option.type && res.type('json');
res.json(option.result);
} else {
!option.type && res.type('text');
res.send(option.result.toString());
}
}, option.timeout);
});
}
// 根据参数个数获取配置
function getOption(arg) {
const len = arg.length;
// 默认配置
const option = {
headers: {
'Cache-Control': 'no-cache'
},
statusCode: 200,
cookies: [],
timeout: 0
};
if (len === 0) {
return cgiMock;
} if (len === 1) {
const newOption = arg[0];
if (util.isObject(newOption)) {
util.each(newOption, (value, key) => {
if (key === 'headers') {
util.each(newOption.headers, (headervalue, headerkey) => {
option.headers[headerkey] = newOption.headers[headerkey];
});
} else {
option[key] = newOption[key];
}
});
}
} else {
option.url = arg[0];
option.result = arg[1];
}
return option;
}
// 把基于 cgiMockfile 的相对绝对转成绝对路径
function parsePath(value) {
return path.join(global.cgiMockFilePath, value);
}
// log proxy data
proxy.on('open', (proxySocket) => {
proxySocket.on('data', (chunk) => {
console.log(chunk.toString());
});
});
proxy.on('proxyRes', (proxyRes) => {
console.log('RAW Response from the target', JSON.stringify(proxyRes.headers, true, 2));
const cookie = proxyRes.headers['set-cookie'];
if (cookie && cookie.length > 0) {
for (let i = 0; i < cookie.length; i++) {
cookie[i] = cookie[i].replace('Secure', '');
}
}
});
proxy.on('error', (e) => {
console.log(e);
});
// 规则之外的请求转发
cgiMock.proxy = function (host) {
process.nextTick(() => {
global.router.use((req, res) => {
proxy.web(req, res, {
target: host,
secure: false
});
});
});
proxy.on('proxyReq', (proxyReq) => {
proxyReq.setHeader('Host', url.parse(host).host);
});
};
// 读取文件内容
cgiMock.file = function (file) {
return fs.readFileSync(parsePath(file));
};
cgiMock.prefix = '/';
module.exports = cgiMock;

View File

@ -0,0 +1,112 @@
const Mock = require('mockjs');
const faker = require('faker');
const path = require('path');
const fs = require('fs');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const onFinished = require('on-finished');
const util = require('./util');
const cgiMock = require('./cgiMock');
const log = require('../helpers/log');
const main = {
init(app, argv, cwd) {
this.app = app;
this.argv = argv;
this.cwd = cwd;
app.use(logger('dev'));
app.use(
bodyParser.urlencoded({
extended: false
})
);
app.use(cookieParser());
this.customRoute();
this.defaultRoute();
},
customRoute() {
const argv = this.argv;
const defaultCgiMockFile = path.join(process.cwd(), 'mock.js');
const home = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME'];
let cgiMockFile;
if (argv) {
if (argv.f) {
if (process.platform === 'win32') {
cgiMockFile = path.resolve(this.cwd, this.argv.f);
} else if (argv.f[0] === '~') {
cgiMockFile = path.resolve(
home,
argv.f.replace(/^~\//, '')
);
} else {
cgiMockFile = path.resolve(this.cwd, this.argv.f);
}
} else {
cgiMockFile = defaultCgiMockFile;
}
} else {
cgiMockFile = defaultCgiMockFile;
}
global.cgiMockFilePath = path.resolve(cgiMockFile, '..');
const loadRouteConfig = function () {
util.cleanCache(cgiMockFile);
try {
if (!fs.existsSync(cgiMockFile)) {
log.error('[WARN] 不存在mock.js文件');
} else {
// eslint-disable-next-line
const projectMock = require(cgiMockFile);
if (util.isFunction(projectMock)) {
global.router.stack = [];
projectMock(cgiMock, Mock, faker);
log.message('[SUCCESS] mock.js 加载成功');
} else {
log.error(
`[ERROR] mock.js cannot be ${typeof projectMock}`
);
}
}
} catch (e) {
log.error('[ERROR] mock.js 有误,请检查');
log.error(JSON.stringify(e));
}
};
loadRouteConfig();
this.app.use(cgiMock.prefix, (req, res, next) => {
onFinished(res, () => {
loadRouteConfig();
});
global.router(req, res, next);
});
util.watchFile(cgiMockFile, () => {
log.message('[INFO] mock.js 发生变化');
loadRouteConfig();
});
},
defaultRoute() {
const app = this.app;
setTimeout(() => {
app.use((err, req, res) => {
res.json({
status: err.status || 500,
message: err.message,
err
});
});
});
}
};
module.exports = main.init.bind(main);

View File

@ -0,0 +1,17 @@
const express = require('express');
const argv = require('yargs').argv;
const port = argv.p || 8888;
const cwd = process.cwd();
const app = express();
const init = require('../init');
init(app, argv, cwd);
app.set('port', port);
app.listen(port, () => {
console.log(`cgiMock server listening on ${port}`);
});

View File

@ -0,0 +1,64 @@
module.exports = function mock(cgiMock, Mock) {
const Random = Mock.Random;
// 前缀,全局(可选)
cgiMock.prefix = '/prefix';
// 返回一个数字
cgiMock('/number', 123);
// 返回一个json
cgiMock({
url: '/json',
result: {
code: '400101', msg: "不合法的请求:Missing cookie 'wb_app_id' for method parameter of type String", transactionTime: '20170309171146', success: false
}
});
// 利用mock.js 产生随机文本
cgiMock('/text', Random.cparagraph());
// 返回一个字符串 利用mock.js 产生随机字符
cgiMock('/string', Mock.mock({
'string|1-10': '★'
}));
// 正则匹配url, 返回一个字符串
cgiMock(/\/abc|\/xyz/, 'regexp test!');
// option.result 参数如果是一个函数, 可以实现自定义返回内容, 接收的参数是是经过 express 封装的 req 和 res 对象.
cgiMock(/\/function$/, (req, res) => {
res.send('function test');
});
// 返回文本 fs.readFileSync
cgiMock('/file', cgiMock.file('./test.json'));
// 更复杂的规则配置
cgiMock({
url: /\/who/,
method: 'GET',
result(req, res) {
if (req.query.name === 'kwan') {
res.json({ kwan: '孤独患者' });
} else {
res.send('Nooooooooooo');
}
},
headers: {
'Content-Type': 'text/plain',
'Content-Length': '123',
ETag: '12345'
},
cookies: [
{
name: 'myname', value: 'kwan', maxAge: 900000, httpOnly: true
}
],
// 接口随机延迟
timeout: Mock.mock({
'number|1000-5000': 1000
}).number
});
};

View File

@ -0,0 +1 @@
file test

View File

@ -0,0 +1,62 @@
const fs = require('fs');
const toString = Object.prototype.toString;
module.exports = {
isArray(value) {
return toString.call(value) === '[object Array]';
},
isObject(value) {
return toString.call(value) === '[object Object]';
},
isFunction(value) {
return toString.call(value) === '[object Function]';
},
each(val, callback) {
if (this.isArray(val)) {
val.forEach(callback);
}
if (this.isObject(val)) {
Object.keys(val).forEach((key) => {
callback(val[key], key);
});
}
},
watchFile(filename, callback) {
const isWin = (process.platform === 'win32');
if (isWin) {
return fs.watch(filename, (event) => {
if (event === 'change') {
return callback(filename);
}
return null;
});
}
return fs.watchFile(filename, {
interval: 200
}, (curr, prev) => {
if (curr.mtime > prev.mtime) {
return callback(filename);
}
return null;
});
},
unwatchFile(watcher, filename) {
if (watcher) {
watcher.close && watcher.close();
} else {
fs.unwatchFile(filename);
}
},
cleanCache(modulePath) {
if (require.cache[modulePath]) {
delete require.cache[modulePath];
}
}
};

View File

@ -0,0 +1,26 @@
// 全局注册common目录下的组件
const fs = require('fs');
const Path = require('path');
const stringUtil = require('node-plus-string');
function addComp(path, outputCommonDir, components) {
const dirList = fs.readdirSync(path);
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isFile()
&& item[0] !== '.'
&& ['.fes', '.vue'].indexOf(Path.extname(item)) !== -1) {
const fileName = Path.basename(item, Path.extname(item));
const tagName = stringUtil.capitalize(fileName);
components.push({
tagName,
path: Path.resolve(outputCommonDir, item).replace(/\\/g, '\\\\')
});
}
});
}
module.exports = function genComponents(commonDir, outputCommonDir) {
const components = [];
addComp(commonDir, outputCommonDir, components);
return components;
};

View File

@ -0,0 +1,121 @@
// pages
// ├── index.fes # 根路由页面 路径 index.html#/
// ├── a.fes # 路径 /a
// ├── b
// │ ├── index.fes # 路径 /b
// │ ├── @id.fes # 动态路由 /b/:id
// │ └── c.fes # 路径 /b/c
// └── layout.fes # 根路由下所有page共用的外层
const fs = require('fs');
const Path = require('path');
let pagesDir;
let outputPageDir;
let components = [];
function checkHasLayout(path) {
const dirList = fs.readdirSync(path);
let hasLayout = false;
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isFile()
&& item[0] !== '.' && ['.fes', '.vue'].indexOf(Path.extname(item)) !== -1
&& Path.basename(item, Path.extname(item)) === 'layout') {
hasLayout = true;
}
});
return hasLayout;
}
function routeUrlFormmter(str) {
return str.replace(/@/g, ':');
}
function genRoute(path, prePathUrl, preRoutes) {
const hasLayout = checkHasLayout(path);
const dirList = fs.readdirSync(path);
let childRoutes = {};
const parentRoutes = {};
const preRouteUrl = routeUrlFormmter(prePathUrl);
if (hasLayout) {
parentRoutes[preRouteUrl] = {
subRoutes: childRoutes
};
} else {
childRoutes = parentRoutes;
}
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isFile()
&& item[0] !== '.' && ['.fes', '.vue'].indexOf(Path.extname(item)) !== -1) {
const fileName = Path.basename(item, Path.extname(item));
const preRouteName = path.slice(pagesDir.length + 1);
let routePath = Path.posix.join(preRouteUrl, (fileName === 'index' ? '' : fileName.replace(/@/g, ':')));
const filePath = Path.resolve(path.replace(pagesDir, outputPageDir), item);
let routeName = preRouteName ? `${preRouteName}_${fileName}` : fileName;
routeName = routeName.replace(/\//g, '_').replace(/@/g, '');
routePath = routePath.replace(/@/g, ':');
if (hasLayout && fileName === 'index') {
routePath = '/';
} else if (hasLayout && fileName !== 'index') {
routePath = routePath.split(preRouteUrl)[1];
}
components.push({
name: routeName,
path: filePath.replace(/\\/g, '\\\\')
});
if (fileName === 'layout') {
parentRoutes[preRouteUrl].component = routeName;
return;
}
childRoutes[routePath] = {
name: routeName || 'index',
component: routeName
};
}
});
preRoutes = Object.assign(preRoutes, parentRoutes);
const toNextRoutes = hasLayout ? childRoutes : preRoutes;
dirList.forEach((item) => {
if (fs.statSync(`${path}/${item}`).isDirectory()) {
let toNextPreRouteUrl = Path.posix.join(prePathUrl, item);
if (hasLayout) {
toNextPreRouteUrl = Path.posix.join('/', item);
}
genRoute(`${path}/${item}`, toNextPreRouteUrl, toNextRoutes, outputPageDir, components);
}
});
}
function fixRouter2(routes, newRoutes, f) {
Object.keys(routes).forEach((p) => {
const item = {
path: f ? p.slice(1) : p,
component: routes[p].component
};
if (routes[p].name) {
item.name = routes[p].name;
}
if (routes[p].subRoutes) {
item.children = [];
fixRouter2(routes[p].subRoutes, item.children, true);
}
newRoutes.push(item);
});
}
module.exports = function (_pagesDir, _outputPageDir) {
const routes = {};
const newRoutes = [];
components = [];
pagesDir = _pagesDir;
outputPageDir = _outputPageDir;
genRoute(pagesDir, '/', routes);
fixRouter2(routes, newRoutes);
return {
routes,
newRoutes,
components
};
};

View File

@ -0,0 +1,25 @@
const webpack = require('webpack');
const log = require('../helpers/log');
const createProdConfig = require('../configs/webpack.config');
const generateRoute = require('./route');
const generateComponent = require('./components');
function startBuild(config) {
try {
generateRoute(config);
generateComponent(config);
const webpackConfig = createProdConfig(config, webpack, 'build');
webpack(webpackConfig, (err) => {
if (err) {
log.error(err);
return;
}
console.log('[build] success');
});
} catch (e) {
log.error(e);
}
}
module.exports = startBuild;

View File

@ -0,0 +1,45 @@
const render = require('json-templater/string');
const path = require('path');
const endOfLine = require('os').EOL;
const fs = require('fs-extra');
const getCommonComponent = require('../preComplie/components');
function generateComponent(config) {
const OUTPUT_PATH = path.resolve(config.folders.PROJECT_CACHE_DIR, 'commonComp.js');
const IMPORT_TEMPLATE = 'import {{name}} from \'{{path}}\';';
const LIST_TEMPLATE = ' {{name}}';
const MAIN_TEMPLATE = `
/**
* 全局组件配置输出
*/
{{include}}
export default {
{{list}}
};
`;
const components = getCommonComponent(config.folders.PROJECT_CPN_DIR, config.folders.PROJECT_CPN_DIR);
const componentsTemplate = [];
const listTemplate = [];
components.forEach((item) => {
componentsTemplate.push(render(IMPORT_TEMPLATE, {
name: item.tagName,
path: item.path
}));
listTemplate.push(render(LIST_TEMPLATE, {
name: item.tagName
}));
});
const template = render(MAIN_TEMPLATE, {
include: componentsTemplate.join(endOfLine),
list: listTemplate.join(`,${endOfLine}`)
});
fs.outputFileSync(OUTPUT_PATH, template);
}
module.exports = generateComponent;

View File

@ -0,0 +1,63 @@
const path = require('path');
const chokidar = require('chokidar');
const webpack = require('webpack');
const createDevServer = require('../helpers/createDevServer');
const getPort = require('../helpers/getPort');
const log = require('../helpers/log');
const createDevConfig = require('../configs/webpack.config');
const generateRoute = require('./route');
const generateComponent = require('./components');
function routeHandle(config) {
generateRoute(config);
// 监听pages变化重新生成路由
const pagesWatcher = chokidar.watch(path.resolve(config.folders.PROJECT_DIR, './src/pages'));
pagesWatcher.on('ready', () => {
pagesWatcher.on('add', (filePath) => {
if (path.extname(filePath) === '.fes' || path.extname(filePath) === '.vue') {
generateRoute(config);
}
}).on('unlink', (filePath) => {
if (path.extname(filePath) === '.fes' || path.extname(filePath) === '.vue') {
generateRoute(config);
}
});
});
}
function globalComponentHandle(config) {
generateComponent(config);
// 监听components变化重新生成组件注入文件
const compWatcher = chokidar.watch(path.resolve(config.folders.PROJECT_DIR, './src/components'));
compWatcher.on('ready', () => {
compWatcher.on('add', (filePath) => {
if (path.extname(filePath) === '.fes' || path.extname(filePath) === '.vue') {
generateComponent(config);
}
}).on('unlink', (filePath) => {
if (path.extname(filePath) === '.fes' || path.extname(filePath) === '.vue') {
generateComponent(config);
}
});
});
}
function startDev(config) {
routeHandle(config);
globalComponentHandle(config);
const webpackConfig = createDevConfig(config, webpack, 'dev');
if (!webpackConfig) return;
getPort(config.ports.server)
.then((port) => {
log.message(`------------ find port success. port: ${port}`);
createDevServer(port, webpackConfig);
}).catch((err) => {
log.message('------------ build error.');
log.error(err);
});
}
module.exports = startDev;

View File

@ -0,0 +1,15 @@
const init = require('./init.js');
const route = require('./route.js');
const components = require('./components.js');
const build = require('./build.js');
const dev = require('./dev.js');
const update = require('./update.js');
module.exports = {
init,
route,
components,
build,
dev,
update
};

View File

@ -0,0 +1,54 @@
const path = require('path');
const fs = require('fs-extra');
const prompts = require('prompts');
const { exec } = require('child_process');
const log = require('../helpers/log');
function createProject(config, projectName) {
log.message('正在初始化项目...');
const projectDir = path.resolve(config.folders.PROJECT_DIR, projectName);
if (fs.pathExistsSync(projectDir)) {
log.error('该项目已存在,请重新输入!');
return Promise.reject();
}
return new Promise((resolve, reject) => {
fs.copy(`${config.folders.CLI_DIR}/template`, `${config.folders.PROJECT_DIR}/${projectName}`).then(() => {
exec(`cd ${config.folders.PROJECT_DIR}/${projectName} && git init && npm i @webank/fes-core @webank/fes-ui && npm i`, (err) => {
if (err) {
log.error(err);
reject(err);
return;
}
log.message(`项目 ${projectName} 创建完成,请执行下面的命令进行使用:`);
log.message(`$ cd ${projectName}`);
log.message('$ npm run dev');
resolve();
});
}).catch((err) => {
log.error(err);
reject(err);
});
});
}
async function initProject(config, projectName) {
if (projectName) {
await createProject(config, projectName);
} else {
const response = await prompts([
{
type: 'text',
name: 'name',
message: '请输入项目名称: '
}
]);
if (!response.name) {
await initProject(config, projectName);
} else {
await createProject(config, response.name);
}
}
}
module.exports = initProject;

View File

@ -0,0 +1,35 @@
const render = require('json-templater/string');
const path = require('path');
const endOfLine = require('os').EOL;
const fs = require('fs-extra');
const getRoute = require('../preComplie/route');
function generateRoute(config) {
const OUTPUT_PATH = path.resolve(config.folders.PROJECT_CACHE_DIR, 'routeConfig.js');
const IMPORT_TEMPLATE = 'import {{name}} from \'{{path}}\';';
const MAIN_TEMPLATE = `
{{include}}
export default {{routes}};
`;
const routes = getRoute(config.folders.PROJECT_PAGE_DIR, config.folders.PROJECT_PAGE_DIR);
const componentsTemplate = [];
routes.components.forEach((item) => {
componentsTemplate.push(render(IMPORT_TEMPLATE, {
name: item.name,
path: item.path
}));
});
const template = render(MAIN_TEMPLATE, {
include: componentsTemplate.join(endOfLine),
routes: JSON.stringify(routes.newRoutes).replace(/"component":"(.+?)"/g, '"component": $1')
});
fs.outputFileSync(OUTPUT_PATH, template);
}
module.exports = generateRoute;

View File

@ -0,0 +1,15 @@
const { exec } = require('child_process');
const log = require('../helpers/log');
function update(config) {
log.message('安装@webank/fes-core @webank/fes-ui...');
exec(`cd ${config.folders.PROJECT_DIR} && npm i @webank/fes-core @webank/fes-ui --save && npm i`, (err) => {
if (err) {
console.error(err);
return;
}
log.message('升级完毕');
});
}
module.exports = update;

View File

@ -0,0 +1,7 @@
# 变更
* 改用 commander 描述命令行参数
* 删除命令选项 https(实际上代码中没有用到)
* 更改 configs >> config
* init 模版使用 cli 自带模版fes-core/fes-ui 创建项目时安装,新项目都采用最新的版本
* 该更 route | components 生成方式(使其不依赖 gulp 的编译)

View File

@ -0,0 +1,98 @@
{
"name": "@webank/fes-cli",
"version": "0.1.1",
"description": "一个好用的前端管理台快速开发框架",
"preferGlobal": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"fes": "./bin/index.js"
},
"author": "harrywan,qlin",
"repository": {
"type": "git",
"url": "https://github.com/WeBankFinTech/fes.js.git"
},
"license": "MIT",
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"dependencies": {
"@babel/core": "^7.6.4",
"@babel/helper-module-imports": "^7.7.0",
"@babel/plugin-proposal-class-properties": "^7.7.0",
"@babel/plugin-proposal-decorators": "^7.7.0",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.6.3",
"@babel/runtime": "^7.7.2",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"autoprefixer": "^8.1.0",
"babel-loader": "^8.0.6",
"babel-polyfill": "^6.26.0",
"body-parser": "^1.5.2",
"cache-loader": "^4.1.0",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"chalk": "^1.1.1",
"chokidar": "^1.7.0",
"clean-webpack-plugin": "^3.0.0",
"commander": "^4.1.0",
"cookie-parser": "^1.4.3",
"copy-webpack-plugin": "^5.0.4",
"cross-spawn": "^2.1.0",
"css-loader": "^3.1.0",
"execa": "^0.8.0",
"express": "^4.14.0",
"express-http-proxy": "^0.10.0",
"express-session": "^1.7.2",
"faker": "^4.1.0",
"file-loader": "^4.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"fs": "0.0.2",
"fs-extra": "^8.1.0",
"hash-sum": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"http-proxy": "^1.12.0",
"is-ci": "^1.0.10",
"json-templater": "^1.2.0",
"lodash": "^4.17.4",
"mini-css-extract-plugin": "^0.8.0",
"mockjs": "^1.1.0",
"morgan": "^1.2.2",
"node-plus-string": "^1.0.1",
"node-sass": "^4.13.0",
"normalize-path": "^1.0.0",
"on-finished": "^2.3.0",
"opn": "^4.0.2",
"path": "^0.12.7",
"postcss-loader": "^3.0.0",
"prompts": "^2.3.0",
"request": "^2.81.0",
"require-dir": "^0.3.0",
"sass-loader": "^8.0.0",
"shelljs": "^0.5.3",
"string-replace-loader": "^2.2.0",
"strip-indent": "^2.0.0",
"style-loader": "^1.0.0",
"tar-fs": "^1.16.0",
"terser-webpack-plugin": "^2.2.1",
"thread-loader": "^2.1.3",
"url-loader": "^2.2.0",
"vue-loader": "^15.7.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
"webpack-dev-server": "^3.9.0",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2",
"yargs": "^3.31.0"
}
}

View File

@ -0,0 +1,17 @@
module.exports = {
extends: [
'@webank/eslint-config-webank/vue',
],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
//
// Vue: false
},
rules: {
'no-plusplus': 'off',
'no-bitwise': 'off',
'vue/comment-directive': 'off'
}
};

8
packages/fes-cli/template/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
.idea
.git
.vscode
.cache
/dist
.history
/node_modules

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present harrywan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,140 @@
module.exports = {
mode: 'vertical', // 可选有vertical、horizontal默认vertical
theme: 'blue', // 可选有blue、dark默认blue
fesName: 'xx 运营平台', // 项目名称
favicon: 'static/favicon.ico', // 图标
// 环境变量配置, 默认使用local环境
env: {
// 本地开发环境
local: {
api: ''
},
// 测试环境 --env=sit 触发使用
develop: {
api: ''
},
// 生产环境 --env=prod 触发使用
prod: {
api: ''
}
},
// 配置角色-路由访问权限使用FesApp.setRole('unLogin')来修改当前用户的角色,控制路由访问权限
roles: {
unLogin: ['/home'],
service: ['/list', '/home'],
admin: ['/list', '/api/fes']
},
// map
map: {
status: [['1', '成功'], ['2', '失败']]
},
// 左侧菜单配置
menu: [
{
title: '$i18n.menu.interface',
path: '/api',
subMenu: [
{
title: 'Fes',
path: '/api/fes'
},
{
title: 'FesApp',
path: '/api/fesApp'
},
{
title: 'FesApi',
path: '/api/fesApi'
},
{
title: 'FesMenu',
path: '/api/fesMenu'
},
{
title: 'FesMap',
path: '/api/fesMap'
},
{
title: 'FesFesx',
path: '/api/fesFesx'
},
{
title: 'FesStorage',
path: '/api/fesStorage'
},
{
title: 'FesUtil',
path: '/api/fesUtil'
}
]
},
{
title: '路由',
path: '/route'
},
{
icon: 'static/bell.png',
title: '列表页',
path: '/list'
},
{
title: '内容很多的编辑',
path: '/list/edit'
},
{
title: '显示头部',
path: '/header'
},
{
title: '静态资源',
path: '/static'
},
{
title: '子路由',
path: '/layout',
subMenu: [
{
title: '子路由A',
path: '/layout/a'
},
{
title: '子路由B',
path: '/layout/b'
}
]
},
{
title: '国际化',
path: '/i18n'
}
],
i18n: {
locale: 'en', // default zh-cn
messages: {
'zh-cn': {
menu: {
interface: '接口'
},
overview: '概述',
i18n: {
internationalization: '国际化,基于',
achieve: '实现。',
ui: 'UI组件'
},
title: '标题'
},
en: {
menu: {
interface: 'interface'
},
overview: 'Overview',
i18n: {
internationalization: 'internationalizationbase on',
achieve: 'to achieve.',
ui: 'UI components'
},
title: 'title'
}
}
}
};

View File

@ -0,0 +1,113 @@
module.exports = (cgiMock, Mock) => {
const { Random } = Mock;
// 前缀,全局(可选)
// cgiMock.prefix = '';
// 返回一个数字
cgiMock('/number', 123);
// 返回一个json
cgiMock({
url: '/json',
result: {
code: '400101', msg: "不合法的请求:Missing cookie 'wb_app_id' for method parameter of type String", transactionTime: '20170309171146', success: false
}
});
// 利用mock.js 产生随机文本
cgiMock('/text', Random.cparagraph());
// 返回一个字符串 利用mock.js 产生随机字符
cgiMock('/string', Mock.mock({
'string|1-10': '★'
}));
// 正则匹配url, 返回一个字符串
// cgiMock(/\/abc|\/xyz/, 'regexp test!');
// option.result 参数如果是一个函数, 可以实现自定义返回内容, 接收的参数是是经过 express 封装的 req 和 res 对象.
// cgiMock(/\/function$/, function (req, res) {
// res.send('function test');
// });
// 返回文本 fs.readFileSync
// cgiMock('/file', cgiMock.file('./test.json'));
// 更复杂的规则配置
cgiMock({
url: /\/who/,
method: 'GET',
result(req, res) {
if (req.query.name === 'kwan') {
res.json({ kwan: '孤独患者' });
} else {
res.send('Nooooooooooo');
}
},
headers: {
'Content-Type': 'text/plain',
'Content-Length': '123',
ETag: '12345'
},
cookies: [
{
name: 'myname', value: 'kwan', maxAge: 900000, httpOnly: true
}
],
// 接口随机延迟
timeout: Mock.mock({
'number|1000-5000': 1000
}).number
});
// 登录
cgiMock('/login', (req, res) => {
res.send(JSON.stringify({
code: '0',
msg: '',
result: {
username: '万纯harrywan',
roleName: '管理员'
}
}));
});
cgiMock('/getTestList', (req, res) => {
const list = [];
for (let i = 0; i < req.body.pageSize; i++) {
list.push({
a: i
});
}
res.send(JSON.stringify({
code: '0',
msg: 'this is message',
result: {
list,
page: {
pageSize: req.body.pageSize,
currentPage: req.body.currentPage,
totalPage: 1000
}
}
}));
});
cgiMock('/getNumber', (req, res) => {
res.send(JSON.stringify({
code: '0',
msg: 'this is message',
result: 4
}));
});
cgiMock('/getRoleName', (req, res) => {
res.send(JSON.stringify({
code: '0',
msg: 'this is message',
result: 'admin'
}));
});
};

View File

@ -0,0 +1,57 @@
{
"name": "@webank/fes-template",
"version": "0.1.0",
"description": "fes项目模版",
"main": "index.js",
"scripts": {
"build": "fes build",
"dev": "fes dev"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint",
"git add"
],
"*.fes": [
"eslint",
"git add"
],
"*.vue": [
"eslint",
"git add"
]
},
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"files": [
".eslintrc.js",
".gitignore",
"fes.config.js",
"mock.js",
"package.json",
"README.md",
"/src"
],
"repository": {
"type": "git"
},
"author": "harrywan qlin",
"license": "MIT",
"devDependencies": {
"@webank/eslint-config-webank": "^0.1.4",
"husky": "^3.0.9",
"lint-staged": "^9.4.2"
},
"dependencies": {},
"peerDependencies": {}
}

View File

@ -0,0 +1,45 @@
import './assets/styles/main.scss';
export default function () {
this.FesApp.set('FesName', '$i18n.title');
setTimeout(() => {
this.FesApp.setRole('admin', false);
}, 1000);
// 设置退出逻辑
this.on('fes_logout', () => {
this.FesApp.setRole('unLogin');
this.FesStorage.set('userLogin', false);
});
// 设置logo点击事件
this.on('fes_logo_click', () => {
window.Toast('你点击了LOGO');
});
// 设置路由钩子
this.FesApp.setBeforeRouter((from, to, next) => {
next();
});
this.FesApp.setAfterRouter((route) => {
console.log(`您浏览到了${route.path}`);
});
// // 设置当前角色
// if (!this.FesStorage.get('userLogin') === true) {
// this.setRole('unLogin')
// }
// 设置AJAX配置
this.FesApi.option({
});
// 设置响应结构
this.FesApi.setResponse({
successCode: '0',
codePath: 'code',
messagePath: 'msg',
resultPath: 'result'
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,131 @@
.login-panel {
position: absolute;
left: 0;
top: 0;
margin: 0;
right: 0;
width: 100%;
height: 100%;
background-color: $body-background;
background-image: url("../images/bg.png");
background-position: left bottom;
background-repeat: no-repeat;
background-size: 100% auto;
font-size: 18px;
color: $black-text-color;
}
.login-panel .login-panel-swap {
position: relative;
width: 945px;
height: 324px;
margin: 10% auto;
border-radius: $border-radius-small;
}
.login-panel .logo {
display: inline-block;
vertical-align: middle;
width: 400px;
height: 100%;
text-align: center;
.logo-text {
display: inline-block;
vertical-align: middle;
margin-top: 130px;
font-size: 28px;
font-weight: bold;
}
}
.login-panel .split {
display: inline-block;
background: $border-color-base;
width: 1px;
height: 180px;
vertical-align: middle;
}
.login-panel .error {
margin-top: 10px;
width: 350px;
color: $error-color;
font-size: 14px;
.ui-icon-exclamation-circle{
margin-right: 6px;
}
}
.login-panel .login-form {
position: relative;
display: inline-block;
vertical-align: middle;
padding: 0 75px 0;
height: 270px;
input::-webkit-input-placeholder{
color: $black-text-color;
}
input::-moz-placeholder {
color: $black-text-color;
}
/* ie */
input:-ms-input-placeholder {
color: $black-text-color;
}
/* firefox 19+ */
input:-moz-placeholder {
color: $black-text-color;
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset !important;
-webkit-text-fill-color: $black-text-color !important;
}
.line{
padding-top: 30px;
border-bottom: 1px solid $border-color-split;
}
input[type="text"],
input[type="password"]{
margin: 0;
width: 350px;
height: 40px;
line-height: 40px;
padding: 0 15px;
vertical-align: middle;
background: 0 0;
outline: 0;
border: none;
font-size: 16px;
color: $black-text-color;
}
button {
margin-top: 10px;
width: 350px;
height: 48px;
border-radius: 4px;
line-height: 46px;
border: none;
outline: 0;
background-color: $primary-color;
color: #ffffff;
cursor: pointer;
text-align: center;
font-size: 18px;
&:hover {
background-color: $selected-color;
}
}
}
.ie-palceholder::after{
content: '用户名';
position: absolute;
left: 88px;
top: 38px;
font-size: 16px;
}
.ie-palceholder-password::after{
content: '密码';
top: 110px;
}

View File

@ -0,0 +1,50 @@
@import "variables";
@import "login";
.article {
padding: 20px;
h1 {
font-size: 26px;
font-weight: 400;
margin: 12px 0;
}
h2 {
margin: 25px 0 12px;
font-size: 20px;
font-weight: 400;
}
h3 {
font-size: 16px;
font-weight: 400;
}
p {
font-size: 14px;
margin: 5px;
}
ul {
padding-left: 40px;
}
li {
list-style-type: disc;
margin-bottom: 5px;
font-size: 14px;
}
table{
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 500px;
margin-bottom: 24px;
}
th,td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
}

View File

@ -0,0 +1,90 @@
// Color
$primary-color : #3399ff;
$info-color : #2db7f5;
$success-color : #00cc66;
$warning-color : #ff9900;
$error-color : #ff5500;
$link-color : #3399ff;
$link-hover-color : #5cadff;
$link-focus-color : rgba(51,153,255, .2);
$link-active-color : #3091f2;
$selected-color : rgba($primary-color, .9);
$tooltip-color : #fff;
//辅助/图标
$subsidiary-color : #9ea7b4;
$disabled-color : #f3f3f3;
// Base
$body-background : #fff;
$component-background : #fff;
$font-family : "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
$code-family : Consolas,Menlo,Courier,monospace;
$title-color : #464c5b;
$text-color : #657180;
$black-text-color : #333333;
$sub-text-color : #999;
$dark-color : #333;
//失效 Disabled
$tip-color : #c3cbd6;
$font-size-lg : 16px;
$font-size-base : 14px;
$font-size-small : 12px;
$line-height-base : 1.5;
$line-height-computed : floor(($font-size-base * $line-height-base));
$border-radius-base : 6px;
$border-radius-small : 4px;
$cursor-disabled : not-allowed;
// Border color
$border-color-base : #d7dde4; // outside
$border-color-split : #e3e8ee; // inside
// Background color
$background-color-base : #f7f7f7; // base
$background-color-select-hover: #f3f3f3;
$tooltip-bg : rgba(70, 76, 91, .9);
$head-bg : #f9fafc;
$table-thead-bg : #f5f7f9;
$table-td-stripe-bg : #f5f7f9;
$table-td-hover-bg : #ebf7ff;
$table-td-highlight-bg : #ebf7ff;
// Z-index
$zindex-spin : 8;
$zindex-affix : 10;
$zindex-back-top : 10;
$zindex-select : 900;
$zindex-modal : 1000;
$zindex-message : 1010;
$zindex-notification : 1010;
$zindex-tooltip : 1060;
$zindex-loading-bar : 2000;
// Animation
$animation-time : .3s;
$transition-time : .2s;
$ease-out : cubic-bezier(0.215, 0.61, 0.355, 1);
$ease-in : cubic-bezier(0.55, 0.055, 0.675, 0.19);
$ease-in-out : cubic-bezier(0.645, 0.045, 0.355, 1);
$ease-out-back : cubic-bezier(0.12, 0.4, 0.29, 1.46);
$ease-in-back : cubic-bezier(0.71, -0.46, 0.88, 0.6);
$ease-in-out-back : cubic-bezier(0.71, -0.46, 0.29, 1.46);
$ease-out-circ : cubic-bezier(0.08, 0.82, 0.17, 1);
$ease-in-circ : cubic-bezier(0.6, 0.04, 0.98, 0.34);
$ease-in-out-circ : cubic-bezier(0.78, 0.14, 0.15, 0.86);
$ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1);
$ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06);
$ease-in-out-quint : cubic-bezier(0.86, 0, 0.07, 1);
// Shadow
$shadow-color : rgba(0, 0, 0, .2);
$shadow-1-up : 0 -1px 6px $shadow-color;
$shadow-1-down : 0 1px 6px $shadow-color;
$shadow-1-left : -1px 0 6px $shadow-color;
$shadow-1-right : 1px 0 6px $shadow-color;
$shadow-2 : 0 2px 8px $shadow-color;
$box-shadow-base : $shadow-1-down;
$mask-color: rgba(55, 55, 55, .6);

View File

@ -0,0 +1,14 @@
<template>
<div style="text-align: center">
我是头部
</div>
</template>
<script>
// components*.fes使
// fesHeader.fes
export default {
FesReady() {
// do something
}
};
</script>

View File

@ -0,0 +1,9 @@
<template>
<div />
</template>
<script>
//
// fesLeft.fes
export default {
};
</script>

View File

@ -0,0 +1,74 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>对Vue做了一层封装给Vue添加了一些属性方便使用</p>
<h2>属性</h2>
<ul>
<li><router-link to="/api/fesApp">this.FesApp App实例</router-link></li>
<li><router-link to="/api/fesUtil">this.FesUtil 工具函数</router-link></li>
<li><router-link to="/api/fesStorage">this.FesStorage 操作存储: cookiesessionStoragelocalStorage</router-link></li>
<li><router-link to="/api/fesApi">this.FesApi 操作Ajax</router-link></li>
</ul>
<h2>响应式数据FesData</h2>
<ul>
<li>
<p>双向绑定的值类似绑定到ng里面scope的值</p>
<p>
<input v-model="data1"> {{data1}}
</p>
</li>
<li>
<p><router-link to="/api/fesMap"><strong>数据字典FesMap</strong></router-link></p>
<p>FesMap会自动挂载到FesData</p>
<p>FesMap: {{FesMap}}</p>
</li>
<li>
<p><router-link to="/api/fesFesx"><strong>全局状态FesFesx</strong></router-link></p>
<p>FesFesx会自动挂载到FesData </p>
<p>FesFesx: {{FesFesx}}</p>
</li>
</ul>
<h2>异步响应数据FesSyncData</h2>
<p>进入路由时根据接口查询到的值并且设置成响应数据</p>
<p>
<input v-model="data2">
</p>
<p>
data2: {{data2}}
</p>
<h2>过滤器</h2>
<p>日期: {{date}} => {{date | date}}</p>
<p>日期带参数: {{date}} => {{date | date('yyyy-MM-dd')}}</p>
<p>金钱: {{money}} => {{money | money}}</p>
<p>银行卡: {{card}} => {{card | card}}</p>
<p>加密: {{safety}} => {{safety | safety(4,3)}}</p>
<p>数据字典: {{map}} => {{map | map(FesMap.status)}}</p>
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesData: {
data1: '1',
money: 100000,
card: '11122233242323',
safety: '42222991018719191',
map: '1',
date: new Date().getTime()
},
FesSyncData: {
data2: ['/getNumber', {
max: 100,
min: 1
}]
},
FesReady() {
// do something
}
};
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>对vue-resource的封装公共的异常处理响应处理等</p>
<h2>函数</h2>
<ul>
<li>
<p>get(url, data, option)</p>
<p>
发起一个get请求返回值是promise对象
</p>
</li>
<li>
<p>post(url, data, option)</p>
<p>
发起一个post请求返回值是promise对象
</p>
</li>
<li>
<p>fetch(url, data, option)</p>
<p>
默认调用post请求返回值是promise对象
</p>
</li>
<li>
<p>option(option)</p>
<p>
设置ajax的公共配置比如root根路径timeout超时时间xhrxhr对象
</p>
</li>
<li>
<p>setError(errors)</p>
<p>
设置当响应状态是非200时触发的事件钩子比如401啊等
</p>
</li>
<li>
<p>setResponse(constructionOfResponse)</p>
<p>
设置响应结构响应一般会由状态码错误消息数据组成通过此函数设置一个路径当响应回来是来解析响应
</p>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,94 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>一个App表示一个应用管理一些应用级别的状态</p>
<h2>属性</h2>
<ul>
<li>
<router-link to="/api/fesApi">
<p>this.FesApi</p>
<p>操作ajax的对象</p>
</router-link>
</li>
<li>
<router-link to="/api/fesMap">
<p>this.FesMap</p>
<p>数据字典保存数据字典的容器提供查找函数</p>
</router-link>
</li>
<li>
<router-link to="/api/fesUtil">
<p>this.FesUtil</p>
<p>工具函数操作dom对象等</p>
</router-link>
</li>
<li>
<router-link to="/api/fesFesx">
<p>this.FesFesx</p>
<p>存储全局状态的容器</p>
</router-link>
</li>
<li>
<router-link to="/api/fesStorage">
<p>this.FesStorage</p>
<p>操作存储: cookiesessionStoragelocalStorage</p>
</router-link>
</li>
<li>
<p>this.router</p>
<p>当前路由对象具体api查询vue-router v0.7</p>
</li>
</ul>
<h2>函数</h2>
<ul>
<li>
<p>init</p>
<p>
初始化整个应用我们只需要在app.js写入自定义的初始化内容比如设置菜单项目名等
</p>
</li>
<li>
<p>get(prop)</p>
<p>
根据prop获取在App保存的应用层面的状态值
</p>
</li>
<li>
<p>set(prop, value)</p>
<p>
设置应用层面的状态包括FesNameFesMenuFesUserNameFesRoleNameFesLogout
</p>
</li>
<li>
<p>setRole(role)</p>
<p>
我们需要在common/rolesConfig.js中配置角色所属的菜单权限可以把未登录状态也当作一种角色根据当前登录状态或者用户设置不同的角色菜单也会根据角色展示
</p>
</li>
<li>
<p>getAllowPage()</p>
<p>
返回当前能访问的页面
</p>
</li>
<li>
<p>setBeforeRouter(beforeRouter)</p>
<p>
设置路由切换之前的事件钩子可以根据条件判断是否阻止切换
</p>
</li>
<li>
<p>setAfterRouter(afterRouter)</p>
<p>
路由切换之后的事件钩子
</p>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,26 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>全局的状态管理容器</p>
<h2>函数</h2>
<ul>
<li>
<p>get(prop)</p>
<p>
根据prop获取值
</p>
</li>
<li>
<p>set(prop, value)</p>
<p>
设置一个全局的状态
</p>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,20 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>数据字典管理对象当程序加载时从common/map.js中读取原始数据转换成{value: value, text: text}存入FesMap</p>
<h2>函数</h2>
<ul>
<li>
<p>getNameByValue(name, value)</p>
<p>
从FesMap[name]中找出值是value的那一条数据
</p>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,129 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>
配置公共左侧菜单
<br> 在调用FesApp.setRole(...)之后FesMenu只显示当前用户可以访问的菜单
</p>
<h2>API</h2>
<p>
<table>
<thead>
<tr>
<th style="text-align:left">
属性
</th>
<th style="text-align:left">
说明
</th>
<th style="text-align:left">
类型
</th>
<th style="text-align:left">
默认值
</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">
title
</td>
<td style="text-align:left">
菜单标题
</td>
<td style="text-align:left">
String
</td>
<td style="text-align:left">
</td>
</tr>
<tr>
<td style="text-align:left">
path
</td>
<td style="text-align:left">
菜单项点击后跳转路径
</td>
<td style="text-align:left">
String
</td>
<td style="text-align:left">
</td>
</tr>
<tr>
<td style="text-align:left">
subMenu
</td>
<td style="text-align:left">
子菜单每项也有path和title
</td>
<td style="text-align:left">
Array
</td>
<td style="text-align:left">
</td>
</tr>
</tbody>
</table>
</p>
<h2>示例</h2>
<pre>
//app.js
this.set("FesMenu", [{
title: "接口",
path: '/api',
subMenu: [{
title: "Fes",
path: '/api/fes',
},{
title: "FesApp",
path: '/api/fesApp',
},{
title: "FesApi",
path: '/api/fesApi',
},{
title: "FesMenu",
path: '/api/fesMenu',
},{
title: "FesMap",
path: '/api/fesMap',
},{
title: "FesFesx",
path: '/api/fesFesx',
},{
title: "FesStorage",
path: '/api/fesStorage',
},{
title: "FesUtil",
path: '/api/fesUtil',
}]
},{
title: "简单的列表页",
path: '/list'
},{
title: "自定义内容列表页",
path: '/list1'
},{
title: "内容很多的编辑",
path: '/list/edit'
}]
);
</pre>
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesData: {
data1: '1',
money: 100000,
card: '11122233242323',
safety: '42222991018719191',
map: '1',
date: new Date().getTime()
}
};
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>操作Storage包含cookiesessionStoragelocalStorage category值SESSION对应sessionStorageLOCAL对应localStorageCOOKIE对应cookie</p>
<h2>函数</h2>
<ul>
<li>
<p>set(key, value, category = SESSION, expired)</p>
<p>
往Storage中存入一个值
</p>
</li>
<li>
<p>get(key, category = SESSION)</p>
<p>
从Storage中取得值
</p>
</li>
<li>
<p>clear(category = SESSION)</p>
<p>
清除所有值
</p>
</li>
<li>
<p>remove(key, category = SESSION)</p>
<p>
删除key对应的值
</p>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,33 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>工具集合</p>
<h2>函数</h2>
<ul>
<li>getClass</li>
<li>hasClass</li>
<li>setClass</li>
<li>addClass</li>
<li>removeClass</li>
<li>contains</li>
<li>addEvent</li>
<li>triggerEvent</li>
<li>merge</li>
<li>extend</li>
<li>isPlainObject</li>
<li>isNumber</li>
<li>isDate</li>
<li>isFunction</li>
<li>isObject</li>
<li>isArray</li>
<li>isObjectLike</li>
<li>isString</li>
<li>isNull</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
};
</script>

View File

@ -0,0 +1,77 @@
<template>
<div>
<Fes-search-panel>
<Wb-form :label-width="150" type="query">
<Form-item label="姓名:">
<wb-input placeholder="请输入" />
</Form-item>
<Form-item label="身份证:">
<wb-input placeholder="请输入" />
</Form-item>
</Wb-form>
<div slot="button">
<Wb-button @click="search" type="primary" icon="search">
查询
</Wb-button>
</div>
</Fes-search-panel>
<Fes-list-panel>
<Wb-table :data="data">
<Column prop="date" name="日期" />
<Column prop="name" name="姓名" />
<Column prop="age" name="年龄" />
<Column prop="adr" name="地址" />
<Column prop="status" name="等级" />
</Wb-table>
<Pagination :size="paginationOption.pageSize" :current="paginationOption.currentPage" :total="paginationOption.totalPage" @on-change="changePage" />
</Fes-list-panel>
</div>
</template>
<script>
export default {
FesHeader: true,
FesData() {
return {
data: [{
name: '张晓刚',
age: 24,
date: new Date(2016, 9, 10),
adr: '北京市海淀区西二旗',
status: 1
}, {
name: '李晓红',
age: 26,
date: new Date(2016, 9, 11),
adr: '北京市海淀区西二旗',
status: 2
}, {
name: '隔壁老王',
age: 22,
date: new Date(2016, 9, 12),
adr: '北京市海淀区西二旗',
status: 3
}, {
name: '我爸是李刚',
age: 19,
date: new Date(2016, 9, 13),
adr: '北京市海淀区西二旗',
status: 4
}],
paginationOption: {
pageSize: 10,
currentPage: 1,
totalPage: 1
}
};
},
methods: {
search() {
// do something
},
changePage() {
// do something
}
}
};
</script>

View File

@ -0,0 +1,142 @@
<template>
<div class="login-panel">
<div class="login-panel-swap">
<div class="logo">
<span class="logo-text">
xx运营平台
</span>
</div>
<div class="split" />
<div class="login-form">
<div :class="getStyle('userFocus')" class="line">
<input
ref="username"
v-model="username"
@input="input"
@keydown.enter="login"
@focus="focusHandler('userFocus')"
@blur="blurHandler('userFocus')"
type="text"
name="username"
autocomplete="off"
autofocus
placeholder="用户名"
>
</div>
<div :class="getStyle('passwordFocus')" class="line">
<input
ref="password"
v-model="password"
@input="input"
@keydown.enter="login"
@focus="focusHandler('passwordFocus')"
@blur="blurHandler('passwordFocus')"
type="password"
name="password"
autocomplete="off"
placeholder="密码"
>
</div>
<div class="line">
<button @click="login">
登录
</button>
</div>
<div v-show="error" class="error">
<Icon type="exclamation-circle" />{{error}}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
FesLeft: false,
FesData() {
return {
userFocus: false,
passwordFocus: false,
username: '',
password: '',
error: '' // 8-16
};
},
FesReady() {
this.$nextTick(() => {
this.$refs.username.focus();
});
this.initStyle();
if (this.FesStorage.get('userLogin') === true) {
this.getRole();
}
},
methods: {
login() {
if (this.validate()) {
this.FesApi.fetch('login').then(() => {
//
this.FesApp.set('FesUserName', '万纯harrywan');
this.FesApp.set('FesRoleName', '管理员');
this.FesStorage.set('userLogin', true);
this.getRole();
});
}
},
getRole() {
this.FesApi.fetch('getRoleName').then((res) => {
// rolesConfig
this.FesApp.setRole(res);
});
},
input() {
this.error = '';
},
validate() {
const { username } = this;
const { password } = this;
if (username === '' || username == null) {
this.error = '请输入用户名';
return false;
}
if (!/^[a-zA-Z0-9]+([._\\-]*[a-zA-Z0-9])*@([a-zA-Z0-9]+[-a-zA-Z0-9]*[a-zA-Z0-9]+.){1,63}[a-zA-Z0-9]+$/.test(username)) {
this.error = '请输入正确邮箱账号';
return false;
}
if (password === '' || password == null) {
this.error = '请输入密码';
return false;
}
return true;
},
getStyle(type) {
let style = '';
if (this[type] && this.isIE(9)) {
if (type === 'passwordFocus' && !this.password) {
style += ' ie-palceholder ie-palceholder-password';
} else if (type === 'userFocus' && !this.username) {
style += ' ie-palceholder';
}
}
return style;
},
initStyle() {
if (this.isIE(9)) {
!this.password && (this.passwordFocus = true);
!this.username && (this.userFocus = true);
}
},
focusHandler(type) {
this.isIE(9) && (this[type] = false);
},
blurHandler(type) {
this[type] = true;
},
isIE(ver) {
const b = document.createElement('b');
b.innerHTML = `<!--[if IE ${ver}]><i></i><![endif]-->`;
return b.getElementsByTagName('i').length === 1;
}
}
};
</script>

View File

@ -0,0 +1,48 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>
{{$t('i18n.internationalization')}}<a target="_blank" href="https://kazupon.github.io/vue-i18n">
vue-i18n
</a>{{$t('i18n.achieve')}}
<Wb-select v-model="locale" @on-change="change" class="select">
<wb-option value="zh-cn">
zh-cn
</wb-option>
<wb-option value="en">
en
</wb-option>
</Wb-select>
</p>
<h2>{{$t('i18n.ui')}}</h2>
<Date-picker :value="value" />
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesData() {
const local = this.FesApp.i18n.locale;
return {
locale: local,
value: +new Date()
};
},
FesReady() {
// do something
},
methods: {
change() {
this.FesApp.setLocale(this.locale);
}
}
};
</script>
<style>
.select{
width: 200px;
}
.article ul{
padding: 0;
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<div class="article">
A子页面
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesReady() {
// do something
}
};
</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="article">
B子页面
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesReady() {
// do something
}
};
</script>

View File

@ -0,0 +1,16 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>
根路由下所有page共用的外层
</p>
<router-view />
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesReady() {
// do something
}
};
</script>

View File

@ -0,0 +1,89 @@
<template>
<Panel title="新增XXXX">
<Icon slot="action" type="check" title="保存" size="20" />
<Icon slot="action" type="rollback" title="返回到列表页面" size="20" />
<Wb-form :label-width="150">
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
<Row>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
<Cell span="12">
<Form-item label="应用名称:">
<wb-input placeholder="我是文本哦" />
</Form-item>
</Cell>
</Row>
</Wb-form>
</Panel>
</template>
<script type="text/ecmascript-6">
export default {
FesData() {
return {
};
},
methods: {}
};
</script>

View File

@ -0,0 +1,84 @@
<template>
<div>
<fes-search-panel>
<Wb-form :label-width="150" type="query">
<Form-item label="姓名:">
<wb-input v-model="query.name" placeholder="请输入" />
</Form-item>
<Form-item label="身份证:">
<wb-input v-model="query.id" placeholder="请输入" />
</Form-item>
</Wb-form>
<div slot="button">
<Wb-button @click="search" type="primary" icon="search">
查询
</Wb-button>
</div>
</fes-search-panel>
<fes-list-panel>
<Wb-table :data="data">
<Column prop="date" name="日期" />
<Column prop="name" name="姓名" />
<Column prop="age" name="年龄" />
<Column prop="adr" name="地址" />
<Column prop="status" name="等级" />
</Wb-table>
<Pagination :size="paginationOption.pageSize" :current="paginationOption.currentPage" :total="paginationOption.totalPage" @on-change="changePage" />
</fes-list-panel>
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesDataCache: 'test',
FesData() {
return {
query: {
name: '',
id: ''
},
data: [{
name: '张晓刚',
age: 24,
date: new Date(2016, 9, 10),
adr: '北京市海淀区西二旗',
status: 1
}, {
name: '李晓红',
age: 26,
date: new Date(2016, 9, 11),
adr: '北京市海淀区西二旗',
status: 2
}, {
name: '隔壁老王',
age: 22,
date: new Date(2016, 9, 12),
adr: '北京市海淀区西二旗',
status: 3
}, {
name: '我爸是李刚',
age: 19,
date: new Date(2016, 9, 13),
adr: '北京市海淀区西二旗',
status: 4
}],
paginationOption: {
pageSize: 10,
currentPage: 1,
totalPage: 2
}
};
},
FesReady() {
console.log(this);
},
methods: {
changePage({ current, size }) {
this.paginationOption.currentPage = current;
this.paginationOption.pageSize = size;
},
search() {
this.FesApp.router.push('/test');
}
}
};
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>
Fes的路由是根据src/pages目录自动生成的<br>
</p>
<h2>生成规则</h2>
<pre>
pages
index.fes # 根路由页面 路径 http://localhost:5000/index.html#!/
a.fes # 路径 /a
b
index.fes # 路径 /b
@id.fes # 动态路由 /b/:id
c.fes # 路径 /b/c(优先于/b/:id进行匹配)
layout.fes # 根路由下所有page共用的外层
</pre>
</div>
</template>
<script type="text/ecmascript-6">
export default {
FesData() {
return {
value: +new Date()
};
}
};
</script>

View File

@ -0,0 +1,18 @@
<template>
<div class="article">
<h2>{{$t('overview')}}</h2>
<p>存放静态资源文件不需要经过打包处理的跟vue体系无关的前端资源可以放在这里</p>
<a target="_blank" href="static/1.txt">
点击下载 1.txt
</a>
</div>
</template>
<script>
export default {
FesData() {
return {
};
}
};
</script>

View File

@ -0,0 +1 @@
111

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

21
packages/fes-core/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present harrywan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,33 @@
{
"name": "@webank/fes-core",
"version": "0.1.0",
"description": "一个好用的前端管理台快速开发框架",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "harrywan,qlin",
"repository": {
"type": "git",
"url": "https://github.com/WeBankFinTech/fes.js.git"
},
"license": "MIT",
"keywords": [
"管理端",
"fes",
"fast",
"easy",
"strong"
],
"dependencies": {
"axios": "^0.16.2",
"lodash": "^4.17.15",
"vue": "^2.6.10",
"vue-i18n": "^8.4.0",
"vue-router": "^2.6.0",
"vue-template-compiler": "^2.6.10"
},
"peerDependencies": {
"@webank/fes-core": "^0.1.0",
"@webank/fes-ui": "^0.1.0"
}
}

View File

@ -0,0 +1,395 @@
/**
* 操作Api
*/
import axios from 'axios';
import util from '../util';
import env from '../env';
import storage from '../storage';
const trim = function (obj) {
Object.keys(obj).forEach((p) => {
if (util.isString(obj[p])) {
obj[p] = obj[p].trim();
} else if (util.isPlainObject(obj[p])) {
trim(obj[p]);
} else if (util.isArray(obj[p])) {
trim(obj[p]);
}
});
};
const requsetLog = {
data: storage.get('FES_AJAX_LOG') || [],
importantApi: {},
creatLog(url, data, status) {
let _data;
if (data) {
_data = JSON.stringify(data);
}
if (_data && _data.length > 1000) {
data = _data.slice(0, 1000); // 大约1K
}
const now = new Date().getTime();
const obj = {
url,
data,
timestamp: now,
status: status || 'send'
};
if (this.data.length >= 500) {
this.data.shift();
}
this.data.push(obj);
try {
storage.set('FES_AJAX_LOG', this.data);
} catch (e) {
storage.remove('FES_AJAX_LOG');
this.data = [obj];
storage.set('FES_AJAX_LOG', this.data);
}
return obj;
},
changeLogStatus(log, newStatus) {
const logs = this.data.filter(obj => obj.timestamp === log.timestamp);
if (logs.length > 0) {
logs[0].status = newStatus;
storage.set('FES_AJAX_LOG', this.data);
}
},
getLogByURL(url, data) {
return this.data.filter(obj => obj.url === url && JSON.stringify(data) === JSON.stringify(obj.data));
}
};
const instance = axios.create({
method: 'post',
baseURL: env.api,
timeout: 10000,
withCredentials: true
});
const api = {
instance,
error: {},
constructionOfResponse: {
codePath: 'code',
successCode: '0',
messagePath: 'msg',
resultPath: 'result'
}
};
const getData = function (data, resultFormat) {
const _arr = ['codePath', 'messagePath', 'resultPath'];
const arr = []; const
rst = {};
for (let i = 0; i < _arr.length; i++) {
const pathArray = resultFormat[_arr[i]].split('.');
const pathLength = pathArray.length;
let result;
if (pathLength === 1 && pathArray[0] === '*') {
result = data;
} else {
result = data[pathArray[0]];
}
for (let j = 1; j < pathLength; j++) {
result = result[pathArray[j]];
if (!result) {
if (j < pathLength - 1) {
console.error(`【FEX】ConstructionOfResponse配置错误${_arr[i]}拿到的值是undefined请检查配置`);
}
break;
}
}
arr.push(result);
}
rst.code = arr[0];
rst.message = arr[1];
rst.result = arr[2];
return rst;
};
const success = function (response) {
// 响应结构
const resultFormat = (response.config && response.config.resultFormat) || api.constructionOfResponse;
// 哪些code不处理错误
const ignoreCode = (response.config && response.config.ignoreCode) || [];
if (util.isNull(resultFormat.codePath) || util.isNull(resultFormat.successCode)
|| util.isNull(resultFormat.messagePath) || util.isNull(resultFormat.resultPath)) {
console.error('【FEX】Api配置错误: 请调用setConstructionOfResponse来设置API的响应结构');
return null;
}
let data;
if (util.isString(response.data)) {
data = JSON.parse(response.data);
} else if (util.isObject(response.data)) {
data = response.data;
} else {
throw new Error(util.format('fesMessages.defaultError'));
}
const { code, message, result } = getData(data, resultFormat);
if (code !== resultFormat.successCode) {
let _message = '';
if (api.error[code]) {
api.error[code].forEach(fn => fn(response));
} else if (!ignoreCode.includes(code) && ignoreCode !== '*') {
_message = message || util.format('fesMessages.defaultError');
}
const error = new Error(_message);
error.response = response;
throw error;
}
return result || {};
};
const fail = function (error) {
let _message = '';
const response = error.response;
if (response && api.error[response.status]) {
api.error[response.status].forEach(fn => fn(response));
} else {
_message = util.format('fesMessages.defaultError');
try {
if (response && response.data) {
let data;
if (util.isString(response.data)) {
data = JSON.parse(response.data);
} else if (util.isObject(response.data)) {
data = response.data;
}
if (data) {
const { message } = getData(data, (response.config && response.config.resultFormat) || api.constructionOfResponse);
_message = message;
}
}
} catch (e) {
// 可以啥都不做
}
}
error.message = _message;
throw error;
};
const param = function (url, data, option) {
const method = instance.defaults.method || 'post';
if (util.isNull(url)) {
return console.error('请传入URL');
} if (!util.isNull(url) && util.isNull(data) && util.isNull(option)) {
option = {
method
};
} else if (!util.isNull(url) && !util.isNull(data) && util.isNull(option)) {
option = {
method
};
if (util.isString(data)) {
option.method = data;
} else if (util.isObject(data)) {
option.data = data;
}
} else if (!util.isNull(url) && !util.isNull(data) && !util.isNull(option)) {
if (!util.isObject(data)) {
data = {};
}
if (util.isString(option)) {
option = {
method: option
};
} else if (util.isObject(option)) {
option.method = option.method || method;
} else {
option = {
method
};
}
if (option.method === 'get' || option.method === 'delete' || option.method === 'head' || option.method === 'options') {
option.params = data;
}
if (option.method === 'post' || option.method === 'put' || option.method === 'patch') {
option.data = data;
}
}
// 过滤参数中的空格
const _data = option.params || option.data;
if (_data && util.isObject(_data) && option.trim !== false) {
trim(_data);
}
option.url = url;
// 如果传了button
if (option.button) {
option.button.currentDisabled = true;
}
return instance.request(option);
};
const action = function (url, data, option) {
// 记录日志
const log = requsetLog.creatLog(url, data);
return param(url, data, option)
.then(success, fail)
.then((response) => {
requsetLog.changeLogStatus(log, 'success');
if (option && option.button) {
option.button.currentDisabled = false;
}
return response;
})
.catch((error) => {
requsetLog.changeLogStatus(log, 'fail');
if (option && option.button) {
option.button.currentDisabled = false;
}
error.message && window.Toast.error(error.message);
throw error;
});
};
api.fetch = function (url, data, option) {
if (requsetLog.importantApi[url]) {
const logs = requsetLog.getLogByURL(url, data);
if (logs.length > 0) {
const compareLog = logs[logs.length - 1];
if (compareLog.status === 'compare') {
requsetLog.creatLog(url, data, 'notAllowed');
return {
then: () => {}
};
}
const importantApiOption = requsetLog.importantApi[url];
const control = importantApiOption.control || 10000;
const message = importantApiOption.message || util.format('fesMessages.importInterfaceTip', { s: control / 1000 });
if (new Date().getTime() - compareLog.timestamp < control) {
const oldStatus = compareLog.status;
requsetLog.changeLogStatus(compareLog, 'compare');
return new Promise(((resolve, reject) => {
window.Message.confirm(util.format('fesMessages.tip'), message).then((index) => {
if (compareLog.status === 'compare') {
requsetLog.changeLogStatus(compareLog, oldStatus);
}
if (index === 0) {
resolve(action(url, data, option));
} else {
reject(new Error('不允许相同操作间隔过小'));
}
});
}));
}
return action(url, data, option);
}
return action(url, data, option);
}
return action(url, data, option);
};
/**
* 设置 request Header
* @param headers Object
*/
api.setHeader = function (headers = {}) {
Object.keys(headers).forEach((p) => {
if (['delete', 'get', 'head', 'post', 'put', 'patch', 'common'].includes(p)) {
instance.defaults.headers[p] = Object.assign({}, instance.defaults.headers[p], headers[p]);
} else {
instance.defaults.headers.common[p] = headers[p];
}
});
};
/**
* 配置ajax请求参数
* @param option
*/
api.option = function (option = {}) {
const {
root,
baseURL,
timeout,
headers,
config,
...others
} = option;
if (root || baseURL) {
instance.defaults.baseURL = root || baseURL;
}
if (timeout && util.isNumber(timeout)) {
instance.defaults.timeout = timeout;
}
if (headers) {
api.setHeader(headers);
}
const otherPropertys = Object.assign({}, others, config);
Object.keys(otherPropertys).forEach((p) => {
instance.defaults[p] = otherPropertys[p];
});
};
/**
* 请求拦截器
* @param before function 请求之前的拦截器
*/
api.setReqInterceptor = function (before, error) {
if (Array.isArray(before)) {
return instance.interceptors.request.use(...before);
}
return instance.interceptors.request.use(before, error);
};
api.ejectReqInterceptor = function (interceptor) {
return instance.interceptors.request.eject(interceptor);
};
/**
* 响应拦截器
* @param after function 响应之后的拦截器
*/
api.setResInterceptor = function (after, error) {
if (Array.isArray(after)) {
return instance.interceptors.response.use(...after);
}
return instance.interceptors.response.use(after, error);
};
api.ejectResInterceptor = function (interceptor) {
return instance.interceptors.response.eject(interceptor);
};
/**
* 配置错误响应
* @param option
*/
api.setError = function (option) {
if (option && util.isObject(option)) {
Object.keys(option).forEach((key) => {
if (!util.isArray(api.error[key])) {
api.error[key] = [];
}
api.error[key].push(option[key]);
});
}
};
/**
* 设置响应结构
* @param constructionOfResponse
*/
api.setResponse = function (constructionOfResponse) {
this.constructionOfResponse = constructionOfResponse;
};
/**
* 配置重要请求
*/
api.setImportant = function (option) {
if (option && util.isObject(option)) {
requsetLog.importantApi = option;
} else {
console.error('【FEX】ImportantApi配置错误: 参数必须是对象{"/get": { message:"xxx", control: 10000 } }');
}
};
export default api;

View File

@ -0,0 +1,9 @@
import './polyfill';
// eslint-disable-next-line
import '@webank/fes-ui/dist/styles/fes-ui.css';
import './views/styles/index.scss';
// eslint-disable-next-line
import init from 'projectRoot/src/app.js';
import App from './instance/app';
App.init(init);

View File

@ -0,0 +1,61 @@
// eslint-disable-next-line
import fesConfig from 'projectRoot/fes.config.js';
// 设置默认
if (!fesConfig.mode) {
fesConfig.mode = 'vertical';
}
if (!fesConfig.theme) {
fesConfig.theme = 'blue';
}
if (!fesConfig.env) {
fesConfig.env = {};
}
if (!fesConfig.roles) {
fesConfig.roles = {};
}
if (!fesConfig.menu) {
fesConfig.menu = [];
}
if (!fesConfig.i18n) {
fesConfig.i18n = {
locale: 'zh-cn',
messages: {
}
};
}
if (!fesConfig.i18n.locale) {
fesConfig.i18n.locale = 'zh-cn';
}
if (!fesConfig.i18n.messages) {
fesConfig.i18n.messages = {};
}
if (!fesConfig.i18n.messages['zh-cn']) {
fesConfig.i18n.messages['zh-cn'] = {};
}
if (!fesConfig.i18n.messages.en) {
fesConfig.i18n.messages.en = {};
}
Object.assign(fesConfig.i18n.messages['zh-cn'], {
fesMessages: {
defaultError: '后台接口异常,请联系开发处理!',
importInterfaceTip: '两个相同请求间隔小于 {s} 秒,是否继续?',
tip: '提示',
noPermission: '您没有访问当前路径的权限'
}
});
Object.assign(fesConfig.i18n.messages.en, {
fesMessages: {
defaultError: 'Server-end API error, please contact the admin.',
importInterfaceTip: 'Repetitive request in {s} seconds, continue anyway?',
tip: 'Tips',
noPermission: 'You dont have the authority to access.'
}
});
export default fesConfig;

View File

@ -0,0 +1,22 @@
// TODO runtime 实例和具体功能解耦
/*eslint-disable */
import app from '../instance/app';
/**
* 常用的指令
*/
export const permission = {
bind(el, binding) {
const dispaly = el.style.display;
const setDispaly = () => {
const urls = app.getAllowPage() || [];
if (urls.indexOf(binding.value) === -1) {
el.style.display = 'none';
} else {
el.style.display = dispaly;
}
};
setDispaly();
app.FesUtil.event.on('fes_allowPage_change', setDispaly);
}
};

3
packages/fes-core/src/env/index.js vendored Normal file
View File

@ -0,0 +1,3 @@
import fesConfig from '../config';
export default fesConfig.env[process.privateFesEnv.env] || {};

View File

@ -0,0 +1,4 @@
import FesxClass from './fesx';
const insideName = `inside_${window.location.pathname.replace(/\//g, '_')}`;
export default new FesxClass(insideName);

View File

@ -0,0 +1,56 @@
/**
* 全局状态管理
*/
import Vue from 'vue';
import storage from '../storage';
import util from '../util';
class Fesx {
constructor(name) {
Object.defineProperty(this, 'name', {
value: name,
enumerable: false
});
Object.defineProperty(this, 'pre', {
value: `FesFesx_${this.name}_`,
enumerable: false
});
const keys = Object.keys(sessionStorage);
const len = keys.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
if (key.indexOf(this.pre) === 0) {
Vue.set(this, key.slice(this.pre.length), storage.get(key));
}
}
}
get(prop) {
if (!this[prop]) {
this.set(prop, storage.get(this.pre + prop));
}
return this[prop];
}
set(prop, value) {
Vue.set(this, prop, value);
if (!util.isFunction(value)) {
storage.set(this.pre + prop, value);
}
return this;
}
clear() {
const keys = Object.keys(sessionStorage);
const len = keys.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
if (key.indexOf(this.pre) === 0) {
storage.remove(key);
Vue.set(this, key.slice(this.pre.length), undefined);
}
}
}
}
export default Fesx;

View File

@ -0,0 +1,9 @@
/**
* 全局状态管理
*/
import Fesx from './fesx';
const collection = new Fesx('outside');
export default collection;

View File

@ -0,0 +1,154 @@
/**
* 常用的过滤器
*/
import util from '../util';
/**
* 日期格式化
* @param _date
* @param format
* @returns {*}
*/
export function date(timestap, format) {
if (!timestap) return '';
format = format || 'yyyy-MM-dd hh:mm:ss';
timestap = Number(timestap);
const time = new Date(timestap);
const obj = {
'y+': time.getFullYear(),
'M+': time.getMonth() + 1,
'd+': time.getDate(),
'h+': time.getHours(),
'm+': time.getMinutes(),
's+': time.getSeconds()
};
if (new RegExp('(y+)').test(format)) {
format = format.replace(RegExp.$1, obj['y+']);
}
Object.keys(obj).forEach((j) => {
if (new RegExp(`(${j})`).test(format)) {
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? (obj[j]) : ((`00${obj[j]}`).substr((`${obj[j]}`).length)));
}
});
return format;
}
/**
* 资金格式化插件
* @param value
* @returns {string|*}
*/
export function money(value) {
const m = [];
value = Number(value).toFixed(2);
// 获取小数部分
const decimals = value.match(/\.[0-9]*/g);
// 获取整数部分
const integer = parseInt(value, 10).toString();
const temp = integer.split('');
const length = temp.length;
// 添加","分隔符
function formart() {
let count = 0;
for (let n = length; n > 0; n--, count++) {
if (count && count % 3 === 0) {
m.unshift(',');
count = 0;
}
m.unshift(temp.pop());
}
const result = m.join('');
return decimals ? result.concat(decimals) : result;
}
return length > 3 ? formart() : value;
}
/**
* 银行卡四位加一空格
* @param value
* @returns {*}
*/
export function card(value) {
value = `${value}`;
const reg = /([0-9]{4})/g;
if (value) {
value = value.replace(reg, '$1 ');
}
return value;
}
/**
* 给字符串中间加***
* @param value
* @param frontLen
* @param backLen
* @returns {*}
*/
export function safety(value, frontLen, backLen) {
if (value) {
const len = value.length;
let front = '';
let back = '';
if (frontLen && len > frontLen) {
front = value.slice(0, frontLen);
}
if (backLen && len > (frontLen + backLen)) {
back = value.slice(len - backLen);
}
return `${front}***${back}`;
}
return '';
}
/**
* 把数据字典中的值转换成text
* @param value
* @param arr
* @returns {string}
*/
export function map(value, arr) {
let name = '';
if (arr && util.isArray(arr)) {
arr.forEach((item) => {
if (item.value === value) {
name = item.text;
}
});
}
return name;
}
/**
* 过滤掉数据中的值
* @param value
* @param arr
* @returns {string}
*/
export function allow(value, arr) {
const _arr = [];
if (util.isArray(value)) {
value.forEach((obj) => {
if (util.isArray(arr)) {
if (arr.indexOf(obj.value) !== -1) {
_arr.push(obj);
}
}
});
}
return _arr;
}
export function capitalize(text) {
return text[0].toUpperCase() + text.slice(1);
}
export function uppercase(text) {
return text.toUpperCase();
}
export function lowercase(text) {
return text.toLowerCase();
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Fes</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit|ie-stand">
<meta name="applicable-device" content="pc">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,328 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueI18n from 'vue-i18n';
// eslint-disable-next-line
import UiWebank from '@webank/fes-ui';
// eslint-disable-next-line
import routerConfig from 'projectRoot/.cache/routeConfig.js';
// eslint-disable-next-line
import commonCompConfig from 'projectRoot/.cache/commonComp.js';
import Page from './page';
import fesComponents from '../views/components';
import root from '../views/layout/root.vue';
import * as filters from '../filter';
// eslint-disable-next-line
import * as directives from '../directive';
import util from '../util';
import _fesx from '../fesx/_fesx';
import storage from '../storage';
import api from '../api';
import map from '../map';
import fesx from '../fesx';
import fesConfig from '../config';
import env from '../env';
import permission from './permission';
if (process.privateFesEnv.env !== 'prod') {
Vue.config.debug = true;
Vue.config.devtools = true;
}
const rolesConfig = fesConfig.roles;
class App {
constructor() {
this.FesApp = this;
this.FesApi = api;
this.FesStorage = storage;
this.FesMap = map;
this.FesFesx = fesx;
this.FesUtil = util;
this.FesEnv = env;
// 允许的路由
// 默认可以访问所有路由第一次addAllowPage的时候需要删除'*'
this._roleId = _fesx.get('FesRoleId');
if (this._roleId) {
permission.set(rolesConfig[this._roleId] || ['*']);
} else {
permission.set(_fesx.get('FesAllowPageList') || ['*']);
}
this.router = null;
this.beforeRouter = null;
this.afterRouter = null;
this.i18n = null;
}
init(func) {
window.Vue = Vue;
// ======================安装插件====================
Vue.use(VueRouter);
Vue.use(UiWebank);
Vue.use(VueI18n);
Vue.use(fesComponents);
Vue.use(Page, this);
// =====================注册全局过滤器================
Object.keys(filters).forEach((p) => {
Vue.filter(p, filters[p]);
});
// =====================注册全局组件==================
Object.keys(commonCompConfig).forEach((p) => {
Vue.component(p, commonCompConfig[p]);
});
// =====================注册全局指令==================
Object.keys(directives).forEach((p) => {
Vue.directive(p, directives[p]);
});
// 设置系统名称
if (fesConfig.fesName) {
this.set('FesName', fesConfig.fesName);
}
// 设置系统名称
if (fesConfig.favicon) {
this.setFavicon(fesConfig.favicon);
}
if (util.isFunction(func)) {
func.call(this);
}
this.run();
}
run() {
this.creatRouter();
this.creatI18n();
// eslint-disable-next-line
new Vue({
el: '#app',
extends: root,
router: this.router,
i18n: this.i18n
});
}
creatI18n() {
this.i18n = new VueI18n(fesConfig.i18n);
this.setLocale(fesConfig.i18n.locale);
}
creatRouter() {
this.router = new VueRouter({
routes: routerConfig,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
}
return {
x: 0,
y: 0
};
}
});
this.handleRouter();
}
handleRouter() {
this.router.beforeEach(async (to, from, next) => {
util.history.record(to.path);
let path;
if (to.matched.length === 1) {
path = to.matched[0].path;
} else {
path = to.path;
}
// 只有允许的路由才能进
const canRoute = await permission.match(path);
if (canRoute) {
if (this.beforeRouter && util.isFunction(this.beforeRouter)) {
this.beforeRouter(to, from, next);
} else {
next();
}
} else {
window.Toast.error(util.format('fesMessages.noPermission'));
if (from.path) {
next(false);
} else {
next('/');
}
}
});
this.router.afterEach((route) => {
// 更新页面的title
let title;
fesConfig.menu.forEach((parent) => {
if (parent.path === route.path) {
title = parent.title;
} else if (parent.subMenu && parent.subMenu.length > 0) {
parent.subMenu.forEach((son) => {
if (son.path === route.path) {
title = son.title;
}
});
}
});
// 设置切换路由时页面的标题
let fesName = this.get('FesName');
if (fesName.slice(0, 6) === '$i18n.') {
fesName = util.format(fesName.slice(6));
}
document.title = title ? `${fesName} | ${title}` : fesName;
if (this.afterRouter && util.isFunction(this.afterRouter)) {
this.afterRouter(route);
}
});
this.setDefaultPage();
}
async getDefaultPage(update) {
// 如果router已初始化通过当前链接来找路由返回当前链接对应的路由
if (this.router && !update) {
const currentPath = this.router.history.getCurrentLocation();
const isMatchCurrentPath = await permission.match(currentPath);
if (isMatchCurrentPath) {
return currentPath;
}
}
// 返回权限列表第一个 > 路由表第一个
const allAllowedRoute = await permission.get();
return allAllowedRoute.length > 0 ? allAllowedRoute[0] : routerConfig[0];
}
async setDefaultPage() {
const defaultPage = await this.getDefaultPage(true);
this.router.addRoutes([{
path: '/',
redirect: () => defaultPage
}]);
}
async setRole(roleId, redirect = true, update = true) {
if (_fesx.get('FesRoleId') !== roleId) {
if (rolesConfig[roleId] instanceof Array) {
permission.set(rolesConfig[roleId]);
this.set('FesRoleId', roleId);
if (this.router && redirect) {
const defaultPage = await this.getDefaultPage(update);
this.router.push(defaultPage);
}
util.event.trigger('fes_allowPage_change');
} else {
console.error(`rolesConfig配置错误不存在角色${roleId}`);
}
}
return this;
}
async setAllowPage(pageList, redirect = true, update = true) {
if (pageList instanceof Array) {
permission.set(pageList);
this.set('FesRoleId', ''); // 通过角色控制权限和通过路由控制权限互斥,只能使用一种
this.set('FesAllowPageList', pageList);
if (this.router && redirect) {
const defaultPage = await this.getDefaultPage(update);
this.router.push(defaultPage);
}
util.event.trigger('fes_allowPage_change');
}
return this;
}
// 废弃 API
getAllowPage() {
// 异步的这里会造成 break;
return permission.getSync();
}
getAllowPageAsync() {
return permission.get();
}
get(prop) {
return _fesx.get(prop);
}
set(prop, value) {
_fesx.set(prop, value);
return this;
}
// 添加过滤器
addFilter(name, func) {
Vue.filter(name, func);
return this;
}
// 添加指令
addDirective(name, option) {
Vue.directive(name, option);
return this;
}
// 添加组件
addComponent(name, c) {
Vue.component(name, c);
return this;
}
// 第三方插件
addThrid(name, option) {
Vue.use(name, option);
return this;
}
setBeforeRouter(beforeRouter) {
this.beforeRouter = beforeRouter;
return this;
}
setAfterRouter(afterRouter) {
this.afterRouter = afterRouter;
return this;
}
// 添加favicon
setFavicon(url) {
let favicon = document.querySelector('#favicon');
if (!favicon) {
favicon = document.createElement('link');
favicon.id = 'favicon';
favicon.rel = 'shortcut icon';
favicon.type = 'image/png';
favicon.href = url;
document.head.appendChild(favicon);
} else {
favicon.href = url;
}
return this;
}
setLocale(lang) {
// 修改vue-i18n的语言
this.i18n.locale = lang;
// 修改组件库的语言
UiWebank.i18n.setLocale(lang);
return this;
}
}
util.merge(App.prototype, util.event);
// 暂时去掉package.json引入安全检测不通过
// App.prototype.version = packageConfig.version;
App.prototype.engine = 'Vue';
export default new App();

View File

@ -0,0 +1,122 @@
import util from '../util';
import storage from '../storage';
import api from '../api';
import map from '../map';
import fesx from '../fesx';
import env from '../env';
import fesConfig from '../config';
const fesDataCache = {};
const Page = {
install(Vue, App) {
Vue.mixin({
data() {
const data = {
FesMap: map,
FesFesx: fesx
};
// 如果存在页面缓存
const cacheName = this.$options.FesDataCache;
if (cacheName && fesDataCache[cacheName] && util.history.current.type !== 'forward') {
return fesDataCache[cacheName];
}
if (this.$options.FesSyncData) {
Object.keys(this.$options.FesSyncData).forEach((p) => {
data[p] = null;
});
}
let fesData;
if (util.isFunction(this.$options.FesData)) {
this.FesFesx = fesx;
this.FesMap = map;
fesData = this.$options.FesData.call(this);
} else {
// 直接等于是对象的引用会导致下次进入页面FesData的值没变
fesData = this.$options.FesData;
}
if (fesData) {
Object.keys(fesData).forEach((p) => {
data[p] = fesData[p];
});
}
return data;
},
created() {
// route切换时重新设置为初始值
const comp = (this.$route && this.$route.matched) || [];
if (comp.length > 0) {
const matchPage = comp[comp.length - 1].components.default;
if (this.$options.__file === matchPage.__file) {
const defaultHeader = fesConfig.FesHeader === undefined ? false : fesConfig.FesHeader;
const defaultLeft = fesConfig.FesLeft === undefined ? true : fesConfig.FesLeft;
if (typeof matchPage.FesHeader === 'boolean') {
this.$root.header = matchPage.FesHeader;
} else {
this.$root.header = defaultHeader;
}
if (typeof matchPage.FesLeft === 'boolean') {
this.$root.left = matchPage.FesLeft;
} else {
this.$root.left = defaultLeft;
}
}
}
const syncData = this.$options.FesSyncData;
if (syncData) {
const arr = [];
Object.keys(syncData).forEach((p) => {
if (util.isArray(syncData[p])) {
arr.push([p, syncData[p][0], syncData[p][1]]);
} else {
console.error(`【FEX】异步参数【${p}】配置错误:值不是数组`, syncData[p]);
}
});
const requests = [];
for (let i = 0; i < arr.length; i++) {
requests.push(api.fetch(arr[i][1], util.merge({}, this.$route.params, this.$route.query, arr[i][2])));
}
Promise.all(requests).then((values) => {
values.forEach((value, index) => {
this[arr[index][0]] = value;
});
});
}
if (this.$options.FesCreated && util.isFunction(this.$options.FesCreated)) {
this.$options.FesCreated.call(this);
}
},
mounted() {
if (this.$options.FesReady && util.isFunction(this.$options.FesReady)) {
this.$options.FesReady.call(this);
}
},
beforeDestroy() {
const cacheName = this.$options.FesDataCache;
if (cacheName) {
fesDataCache[cacheName] = this.$data;
}
if (this.$options.FesBeforeDestroy && util.isFunction(this.$options.FesBeforeDestroy)) {
this.$options.FesBeforeDestroy.call(this);
}
},
destroyed() {
if (this.$options.FesDestroy && util.isFunction(this.$options.FesDestroy)) {
this.$options.FesDestroy.call(this);
}
}
});
// 注入自己的对象
Vue.prototype.FesApp = App;
Vue.prototype.FesUtil = util;
Vue.prototype.FesStorage = storage;
Vue.prototype.FesApi = api;
Vue.prototype.FesEnv = env;
}
};
export default Page;

View File

@ -0,0 +1,34 @@
/**
* 用户路由控制
*/
import util from '../util';
const permission = {
allowRoutesSync: [], // 兼容老的 API 请勿使用
allowRoutes: [],
format(allowRoutes) {
if (Array.isArray(allowRoutes)) {
return allowRoutes.map(allow => Promise.resolve(allow));
}
return [Promise.resolve(allowRoutes)];
},
set(data) {
this.allowRoutesSync = data;
this.allowRoutes = this.format(data);
},
getSync() {
return this.allowRoutesSync;
},
get() {
return Promise.all(this.allowRoutes).then(data => data.reduce((merge, cur) => merge.concat(cur), []));
},
merge(data) {
this.allowRoutes = this.allowRoutes.concat(this.format(data));
},
async match(path) {
const mergedAllowRoutes = await this.get();
return util.canRoute(path, mergedAllowRoutes);
}
};
export default permission;

View File

@ -0,0 +1,42 @@
/**
* 数据字典管理
*/
import util from '../util';
import fesConfig from '../config';
const data = fesConfig.map;
const $Map = Object.create({
getValueByName(name, text) {
// TODO 不确定这里是否需要 ===
// eslint-disable-next-line
const arr = this[name].filter(item => item.text == text);
return arr[0] ? arr[0].value : '';
},
getNameByValue(name, value) {
// TODO 不确定这里是否需要 ===
// eslint-disable-next-line
const arr = this[name].filter(item => item.value == value);
return arr[0] ? arr[0].text : '';
}
});
Object.keys(data).forEach((name) => {
$Map[name] = [];
if (util.isArray(data[name])) {
data[name].forEach((item) => {
if (item.length >= 2) {
$Map[name].push({
value: item[0],
text: item[1]
});
} else {
console.error(`【FEX】Map配置错误Name${name}的值必输是数组,类似['1', '成功']`, item);
}
});
} else {
console.error('【FEX】Map配置错误后面的值必须是数组', data[name]);
}
});
export default $Map;

View File

@ -0,0 +1,48 @@
/* eslint-disable */
class File {
toString() {
console.log('compatible File');
return 'function File() { [native code] }';
}
}
if (window.File === undefined) {
window.File = File;
}
// el remove
(function (arr) {
arr.forEach((item) => {
if (item.hasOwnProperty('remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove() {
if (this.parentNode !== null) this.parentNode.removeChild(this);
}
});
});
}([Element.prototype, CharacterData.prototype, DocumentType.prototype]));
// Function.bind
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
const aArgs = Array.prototype.slice.call(arguments, 1);
const fToBind = this;
const fNOP = function () {};
const fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

View File

@ -0,0 +1,183 @@
/* \
|*|
|*| :: cookies.js ::
|*|
|*| A complete cookies reader/writer framework with full unicode support.
|*|
|*| https://developer.mozilla.org/en-US/docs/DOM/document.cookie
|*|
|*| This framework is released under the GNU Public License, version 3 or later.
|*| http://www.gnu.org/licenses/gpl-3.0-standalone.html
|*|
|*| Syntaxes:
|*|
|*| * docCookies.setItem(name, value[, end[, path[, domain[, secure]]]])
|*| * docCookies.getItem(name)
|*| * docCookies.removeItem(name[, path], domain)
|*| * docCookies.hasItem(name)
|*| * docCookies.keys()
|*|
\ */
const docCookies = {
getItem(sKey) {
return decodeURIComponent(document.cookie.replace(new RegExp(`(?:(?:^|.*;)\\s*${encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&')}\\s*\\=\\s*([^;]*).*$)|^.*$`), '$1')) || null;
},
setItem(sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
let sExpires = '';
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : `; max-age=${vEnd}`;
break;
case String:
sExpires = `; expires=${vEnd}`;
break;
case Date:
sExpires = `; expires=${vEnd.toUTCString()}`;
break;
default: break;
}
}
document.cookie = `${encodeURIComponent(sKey)}=${encodeURIComponent(sValue)}${sExpires}${sDomain ? `; domain=${sDomain}` : ''}${sPath ? `; path=${sPath}` : ''}${bSecure ? '; secure' : ''}`;
return true;
},
removeItem(sKey, sPath, sDomain) {
if (!sKey || !this.hasItem(sKey)) {
return false;
}
document.cookie = `${encodeURIComponent(sKey)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT${sDomain ? `; domain=${sDomain}` : ''}${sPath ? `; path=${sPath}` : ''}`;
return true;
},
hasItem(sKey) {
return (new RegExp(`(?:^|;\\s*)${encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&')}\\s*\\=`)).test(document.cookie);
},
// eslint-disable-next-line
keys: /* optional method: you can safely remove it! */ function () {
const aKeys = document.cookie.replace(/((?:^|\s*;)[^=]+)(?=;|$)|^\s*|\s*(?:=[^;]*)?(?:\1|$)/g, '').split(/\s*(?:=[^;]*)?;\s*/);
for (let nIdx = 0; nIdx < aKeys.length; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
}
};
const isProd = process.env.NODE_ENV === 'production';
export const storageManager = {
set(key, value, storage) {
try {
window[storage].setItem(key, JSON.stringify(value));
} catch (e) {
!isProd && console.error(e);
}
},
get(key, storage) {
try {
if (window[storage].getItem(key)) {
return JSON.parse(window[storage].getItem(key));
}
return window[storage].getItem(key);
} catch (e) {
!isProd && console.error(e, key);
return null;
}
},
clear(storage) {
window[storage].clear();
},
remove(key, storage) {
window[storage].removeItem(key);
}
};
export const cookieManager = {
set(key, value, expired) {
if (expired) docCookies.setItem(key, value, expired);
else docCookies.setItem(key, value);
},
get(key) {
return docCookies.getItem(key);
},
clear() {
docCookies.keys().forEach((key) => {
docCookies.removeItem(key);
});
},
remove(key) {
docCookies.removeItem(key);
}
};
/**
* 操作cookiesessionStoragelocalStorage缓存
*/
const
SESSION = 'session';
const LOCAL = 'local';
const COOKIE = 'cookie';
export default {
set(key, value, category = SESSION, expired) {
const { storage, isWebStorage = true } = this._map(category);
if (isWebStorage) {
storageManager.set(key, value, storage);
} else {
cookieManager.set(key, value, expired);
}
},
get(key, category = SESSION) {
const { storage, isWebStorage = true } = this._map(category);
if (isWebStorage) {
return storageManager.get(key, storage);
}
return cookieManager.get(key);
},
clear(category = SESSION) {
const { storage, isWebStorage = true } = this._map(category);
if (isWebStorage) {
storageManager.clear(storage);
} else {
cookieManager.clear();
}
},
remove(key, category = SESSION) {
const { storage, isWebStorage = true } = this._map(category);
if (isWebStorage) {
storageManager.remove(key, storage);
} else {
cookieManager.remove(key);
}
},
_map(category) {
let isWebStorage = true; let
storage;
switch (true) {
case category === SESSION:
storage = 'sessionStorage';
break;
case category === LOCAL:
storage = 'localStorage';
break;
case category === COOKIE:
storage = 'cookie';
isWebStorage = false;
break;
default:
storage = 'sessionStorage';
}
return { isWebStorage, storage };
}
};

View File

@ -0,0 +1,106 @@
const inBrowser = typeof window !== 'undefined'
&& Object.prototype.toString.call(window) !== '[object Object]';
export const UA = inBrowser && window.navigator.userAgent.toLowerCase();
export const isIE = UA && UA.indexOf('trident') > 0;
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0;
/**
* For IE9 compat: when both class and :class are present
* getAttribute('class') returns wrong value...
*
* @param {Element} el
* @return {String}
*/
export function getClass(el) {
let classname = el.className;
if (typeof classname === 'object') {
classname = classname.baseVal || '';
}
return classname;
}
/**
* 判断dom节点是否有某样式
*
* @param {Element} el
* @return {String}
* @returns {boolean}
*/
export function hasClass(el, name) {
if (!el) return null;
const className = getClass(el);
const classes = className.split(' ');
return classes.indexOf(name) !== -1;
}
/**
* In IE9, setAttribute('class') will result in empty class
* if the element also has the :class attribute; However in
* PhantomJS, setting `className` does not work on SVG elements...
* So we have to do a conditional check here.
*
* @param {Element} el
* @param {String} cls
*/
export function setClass(el, cls) {
/* istanbul ignore if */
if (isIE9 && !/svg$/.test(el.namespaceURI)) {
el.className = cls;
} else {
el.setAttribute('class', cls);
}
}
/**
* Add class with compatibility for IE & SVG
*
* @param {Element} el
* @param {String} cls
*/
export function addClass(el, cls) {
if (el.classList) {
el.classList.add(cls);
} else {
const cur = ` ${getClass(el)} `;
if (cur.indexOf(` ${cls} `) < 0) {
setClass(el, (cur + cls).trim());
}
}
}
/**
* Remove class with compatibility for IE & SVG
*
* @param {Element} el
* @param {String} cls
*/
export function removeClass(el, cls) {
if (el.classList) {
el.classList.remove(cls);
} else {
let cur = ` ${getClass(el)} `;
const tar = ` ${cls} `;
while (cur.indexOf(tar) >= 0) {
cur = cur.replace(tar, ' ');
}
setClass(el, cur.trim());
}
if (!el.className) {
el.removeAttribute('class');
}
}
/**
* 从jquery扣过来的递归去算
*
* @param {Element} a
* @param {Element} b
* @returns {boolean}
*/
export function contains(a, b) {
const adown = a.nodeType === 9 ? a.documentElement : a;
const bup = b && b.parentNode;
return a === bup || !!(bup && bup.nodeType === 1 && adown.contains(bup));
}

View File

@ -0,0 +1,51 @@
const eventProxy = {
onObj: {},
oneObj: {},
on(key, fn) {
if (fn && typeof (fn) === 'function') {
if (this.onObj[key] === undefined) {
this.onObj[key] = [];
}
this.onObj[key].push(fn);
} else {
throw new Error('请传入正确的回调函数');
}
},
one(key, fn) {
if (this.oneObj[key] === undefined) {
this.oneObj[key] = [];
}
this.oneObj[key].push(fn);
},
off(key) {
this.onObj[key] = [];
this.oneObj[key] = [];
},
trigger(...args) {
if (args.length === 0) {
return false;
}
const key = args[0];
args = [].concat(Array.prototype.slice.call(args, 1));
if (this.onObj[key] !== undefined
&& this.onObj[key].length > 0) {
Object.keys(this.onObj[key]).forEach((i) => {
this.onObj[key][i].apply(null, args);
});
}
if (this.oneObj[key] !== undefined
&& this.oneObj[key].length > 0) {
Object.keys(this.oneObj[key]).forEach((i) => {
this.oneObj[key][i].apply(null, args);
this.oneObj[key][i] = undefined;
});
this.oneObj[key] = [];
}
return null;
}
};
export default eventProxy;

View File

@ -0,0 +1,46 @@
import fesConfig from '../config';
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = function (obj, key) {
return hasOwnProperty.call(obj, key);
};
const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
const template = function (string, ...args) {
if (args.length === 1 && typeof args[0] === 'object') {
args = args[0];
}
if (!args || !args.hasOwnProperty) {
args = {};
}
return string.replace(RE_NARGS, (match, prefix, i, index) => {
if (string[index - 1] === '{' && string[index + match.length] === '}') {
return i;
}
const result = hasOwn(args, i) ? args[i] : null;
if (result === null || result === undefined) {
return '';
}
return result;
});
};
export default function (path, options) {
const array = path.split('.');
let current = fesConfig.i18n.messages[fesConfig.i18n.locale];
if (!current) {
current = fesConfig.i18n.messages['zh-cn'];
}
let value;
for (let i = 0, j = array.length; i < j; i++) {
const property = array[i];
value = current[property];
if (i === j - 1) return template(value, options);
if (!value) return '';
current = value;
}
return '';
}

View File

@ -0,0 +1,38 @@
import storage from '../storage';
const history = {
data: storage.get('Fes_History') || [],
current: null
};
history.record = function (href) {
const length = history.data.length;
const obj = {
href,
type: ''
};
if (length === 0) {
obj.type = 'forward';
} else if (length > 0 && length <= 1) {
if (history.data[length - 1].href === href) {
obj.type = 'refresh';
} else {
obj.type = 'forward';
}
} else if (length > 1) {
const first = history.data[length - 1];
const second = history.data[length - 2];
if (first.href === href) {
obj.type = 'refresh';
} else if (second.href === href) {
obj.type = 'back';
} else {
obj.type = 'forward';
}
}
history.data.push(obj);
history.current = obj;
storage.set('Fes_History', history.data);
};
export default history;

View File

@ -0,0 +1,88 @@
import _ from 'lodash';
import * as domUtil from './dom';
import * as objectUtil from './object';
import * as typeUtil from './type';
import format from './format';
import event from './event';
import history from './history';
const util = {
// 验证一个path是否可以访问, 空的allowPage可以访问任何路由
canRoute(path, allowPage) {
path = path.split('?')[0];
if (Array.isArray(allowPage) && allowPage.length > 0) {
if (path === '' && allowPage.includes('/')) return true;
if (path) {
for (let i = 0; i < allowPage.length; i++) {
if (path === allowPage[i]) {
return true;
}
// 支持*匹配
const reg = new RegExp(`^${allowPage[i].replace('*', '.+')}$`);
if (reg.test(path)) {
return true;
}
}
}
}
return false;
},
getUrlParam(name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`);
let r = window.location.search.substr(1).match(reg);
const hashQuery = window.location.hash.split('?')[1];
if (r != null) {
return decodeURIComponent(r[2]);
} if (hashQuery) {
r = hashQuery.match(reg);
return r && decodeURIComponent(r[2]);
}
return null;
},
removeParam(name, content) {
if (typeof name !== 'string') return false;
const prefix = encodeURIComponent(`${name}=`);
const pars = content.split(/[&;]/g);
let i = 0; const
len = pars.length;
let value = '';
for (; i < len; i++) {
if (encodeURIComponent(pars[i]).lastIndexOf(prefix, 0) !== -1) {
pars.splice(i, 1);
}
}
value = (pars.length > 0 ? `?${pars.join('&')}` : '');
return value;
},
proxyFn(proxy, prop, apiArr) {
proxy[prop] = {};
const cache = {};
if (window.Proxy) {
proxy[prop] = new Proxy(proxy[prop], {
get(target, name) {
cache[name] = cache[name] ? cache[name] : [];
if (!target[name]) {
target[name] = function (...args) {
cache[name].push(args);
};
}
return target[name];
}
});
} else {
apiArr.forEach((api) => {
if (!proxy[prop][api]) {
proxy[prop][api] = function (...args) {
cache[api] = cache[api] ? cache[api] : [];
cache[api].push(args);
};
}
});
}
return cache;
},
_
};
objectUtil.merge(util, domUtil, objectUtil, typeUtil, { format }, { event }, { history });
export default util;

View File

@ -0,0 +1,27 @@
export function merge(...args) {
const base = args[0];
if (!base) return null;
[].forEach.call(args, (item, index) => {
if (index > 0) {
Object.keys(item).forEach((attrname) => {
base[attrname] = item[attrname];
});
}
});
return base;
}
export function extend(...args) {
const base = args[0];
if (!base) return null;
[].forEach.call(args, (item, index) => {
if (index > 0) {
Object.keys(item).forEach((attrname) => {
if (base[attrname] !== undefined) {
base[attrname] = item[attrname];
}
});
}
});
return base;
}

View File

@ -0,0 +1,36 @@
const objectToString = Object.prototype.toString;
const OBJECT_STRING = '[object Object]';
export function isPlainObject(obj) {
return objectToString.call(obj) === OBJECT_STRING;
}
export function isNumber(value) { return typeof value === 'number'; }
export function isDate(value) {
return objectToString.call(value) === '[object Date]';
}
export function isFunction(value) { return typeof value === 'function'; }
export function isObject(value) {
const type = typeof value;
return !!value && (type === 'object' || type === 'function');
}
export function isArray(value) {
return Array.isArray(value);
}
export function isObjectLike(value) {
return !!value && typeof value === 'object';
}
export function isString(value) {
return typeof value === 'string'
|| (!isArray(value) && isObjectLike(value) && objectToString.call(value) === '[object String]');
}
export function isNull(value) {
return value === undefined || value === null || value === '';
}

View File

@ -0,0 +1,29 @@
/* !
* fes-components v1.0.0
* (c) 2017 fanniehuang
* Released under the MIT License.
*/
import FesRouteMenu from './routeMenu.vue';
import FesSearchPanel from './searchPanel.vue';
import FesListPanel from './listPanel.vue';
const fesComp = {
FesRouteMenu,
FesSearchPanel,
FesListPanel
};
const install = function (Vue) {
Object.keys(fesComp).forEach((key) => {
Vue.component(key, fesComp[key]);
});
};
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
install,
version: '2.0.0'
}; // eslint-disable-line no-undef

View File

@ -0,0 +1,11 @@
<template>
<div class="query-page">
<div class="query-page-table">
<slot />
</div>
</div>
</template>
<script>
export default {
};
</script>

View File

@ -0,0 +1,91 @@
<template>
<route-menu :menu="authMenu" :width="width" :type="type" :mode="mode" :auto-close="autoClose" />
</template>
<script>
import util from '../../util/index';
export default {
props: {
mode: {
type: String,
default: 'vertical' //
},
width: {
type: [String, Number],
default: undefined
},
menu: {
type: Array,
default() {
return [];
}
},
type: {
type: String,
default: 'light'
},
autoClose: {
type: Boolean,
default: false
}
},
data() {
return {
allowPage: ['*']
};
},
computed: {
authMenu() {
return this.filterMenu(this.menu, this.allowPage);
}
},
created() {
this.allowPage = this.FesApp.getAllowPage();
util.event.on('fes_allowPage_change', () => {
this.allowPage = this.FesApp.getAllowPage();
});
},
methods: {
// menu
filterMenu(menu, allowPage) {
if (allowPage && menu) {
const menuData = [];
// menu访
for (let i = 0; i < menu.length; i++) {
const item = menu[i];
if (
item.path
&& (!item.subMenu || item.subMenu.length === 0)
) {
if (util.canRoute(item.path, allowPage)) {
menuData.push(item);
}
} else if (item.subMenu && item.subMenu.length > 0) {
const subMenu = [];
for (let j = 0; j < item.subMenu.length; j++) {
const subItem = item.subMenu[j];
if (
(subItem.path
&& util.canRoute(subItem.path, allowPage))
|| !subItem.path
) {
subMenu.push(subItem);
}
}
if (subMenu.length > 0) {
menuData.push({
...item,
subMenu
});
}
} else {
menuData.push(item);
}
}
return menuData;
}
return menu;
}
}
};
</script>

View File

@ -0,0 +1,14 @@
<template>
<div class="query-page">
<div class="query-page-search">
<slot />
<div class="query-page-search-buttons">
<slot name="button" />
</div>
</div>
</div>
</template>
<script>
export default {
};
</script>

View File

@ -0,0 +1,108 @@
<template>
<div class="layout-left-body">
<div v-if="!fesFesx.FesHideLeftLogo" :class="{ 'hasLogoEvent': fesFesx.FesLogoEvent }" @click="LogoClick" class="layout-left-logo">
<img src="~assets/images/logo.png">
<p>{{fesName}}</p>
</div>
<div class="layout-left-menu">
<fes-route-menu :menu="fesMenu" :type="menuTheme" :mode="menuMode" :auto-close="true" />
</div>
<fes-left ref="commonleft" />
<div v-if="!showCommonLeft" class="layout-left-user">
<div class="layout-left-user-name">
<p>{{fesFesx.FesUserName}}</p>
<p>{{fesFesx.FesRoleName}}</p>
</div>
<div class="layout-left-user-logout">
<Icon @click="logout" type="logout" size="24" />
</div>
</div>
</div>
</template>
<script>
import fesConfig from '../../config';
import _fes from '../../fesx/_fesx';
export default {
data() {
//
const obj = {
light: 'light',
blue: 'dark',
dark: 'dark'
};
return {
fesFesx: _fes,
menuData: fesConfig.menu || [],
menuMode: this.$parent.mode,
menuTheme: obj[this.$parent.theme],
showCommonLeft: false
};
},
computed: {
fesName() {
const fesName = _fes.get('FesName');
if (fesName.slice(0, 6) === '$i18n.') {
return this.$t(fesName.slice(6));
}
return fesName;
},
fesMenu() {
const menu = this.menuData;
// title
menu.forEach((element) => {
if (!element.__title) {
element.__title = element.title;
}
if (element.__title.slice(0, 6) === '$i18n.') {
element.title = this.$t(element.__title.slice(6));
}
//
if (element.subMenu) {
element.subMenu.forEach((son) => {
if (!son.__title) {
son.__title = son.title;
}
if (son.__title.slice(0, 6) === '$i18n.') {
son.title = this.$t(son.__title.slice(6));
}
});
}
});
return menu;
}
},
mounted() {
this.showCommonLeft = this.$refs.commonleft
&& this.$refs.commonleft.$el
&& this.$refs.commonleft.$el.innerHTML
&& this.$refs.commonleft.$el.innerHTML.trim() !== '';
this.FesApp.on('fes_logout', () => {
// FesName
const fesName = _fes.get('FesName');
_fes.clear();
_fes.set('FesName', fesName);
});
},
methods: {
logout() {
this.FesApp.set('FesRoleId', null);
const FesLogoutFn = this.FesApp.get('FesLogout');
if (this.FesUtil.isFunction(FesLogoutFn)) {
FesLogoutFn.call(this.FesApp);
}
this.FesApp.trigger('fes_logout', this.FesApp);
},
LogoClick() {
const logoClick = this.fesFesx.get('FesLogoEvent');
if (this.FesUtil.isFunction(logoClick)) {
logoClick.call(this);
}
this.FesApp.trigger('fes_logo_click', this.FesApp);
}
}
};
</script>

View File

@ -0,0 +1,67 @@
<template>
<div :class="getRootClass()" class="layout">
<div v-if="left" class="layout-left">
<left />
<span v-if="mode === 'vertical'" @click="toggleMenu" class="layout-left-fold-menu">
<Icon v-show="!leftHidden" type="double-left" />
<Icon v-show="leftHidden" type="double-right" />
</span>
</div>
<div class="layout-right">
<div v-if="header" class="layout-right-header">
<fes-header />
</div>
<div class="layout-right-body">
<router-view ref="pageview" />
</div>
</div>
</div>
</template>
<script>
import fesConfig from '../../config';
import left from './left.vue';
export default {
components: {
left
},
data() {
return {
mode: fesConfig.mode,
theme: fesConfig.theme,
leftHidden: false,
header: false,
left: true,
animate: false
};
},
methods: {
getRootClass() {
const arr = [
`layout-mode-${this.mode}`,
`layout-theme-${this.theme}`
];
if (!this.left) {
arr.push('layout-left-hide');
}
if (this.leftHidden) {
arr.push('layout-left-hidden');
}
if (!this.header) {
arr.push('layout-header-hide');
}
if (this.animate) {
arr.push('layout-animate');
}
return arr;
},
toggleMenu() {
this.animate = true;
setTimeout(() => {
this.animate = false;
}, 300);
this.leftHidden = !this.leftHidden;
}
}
};
</script>

View File

@ -0,0 +1,48 @@
.query-page {
.query-page-search {
position: relative;
background-color: #f7f7f7;
.query-page-search-buttons {
position: absolute;
margin-bottom: 16px;
margin-left: 50px;
height: 32px;
line-height: 32px;
bottom: 0;
right: 40px;
.ui-button+.ui-button {
margin-left: 8px;
}
}
.ui-form {
width: 75%;
}
.ui-form-item {
display: inline-block;
width: 33.33%;
}
}
.query-page-table {
.ui-page {
margin: 20px;
text-align: center;
}
}
.ui-modal-dialog {
overflow: auto;
}
.ui-modal-body {
width: 500px;
padding-right: 30px;
}
.link {
color: #3399ff;
cursor: pointer;
}
.link:hover {
color: #5cadff;
}
.link:active {
color: #3091f2;
}
}

View File

@ -0,0 +1,3 @@
@import "layout.scss";
@import "components.scss";
@import "polyfill.scss";

Some files were not shown because too many files have changed in this diff Show More