@ -0,0 +1,41 @@ |
|||||
|
{ |
||||
|
// 使用 IntelliSense 了解相关属性。 |
||||
|
// 悬停以查看现有属性的描述。 |
||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [ |
||||
|
{ |
||||
|
"type": "node", |
||||
|
"request": "launch", |
||||
|
"name": "启动API", |
||||
|
"program": "${workspaceRoot}/server.js", |
||||
|
"env": { |
||||
|
"NODE_ENV": "development" |
||||
|
}, |
||||
|
"args": [ |
||||
|
"-p 14000", |
||||
|
"-f http://localhost:14000", |
||||
|
// "-g postgres://postgres:123@10.8.30.32:5432/yinjiguanli", |
||||
|
// "-g postgres://postgres:123456@221.230.55.27:5432/yinjiguanli", |
||||
|
// "-g postgres://FashionAdmin:123456@10.8.30.156:5432/SmartEmergency", |
||||
|
"-g postgres://postgres:Mantis1921@116.63.50.139:54327/smartYingji" |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
"type": "node", |
||||
|
"request": "launch", |
||||
|
"name": "run mocha", |
||||
|
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", |
||||
|
"stopOnEntry": false, |
||||
|
"args": [ |
||||
|
"app/test/*.test.js", |
||||
|
"--no-timeouts" |
||||
|
], |
||||
|
"cwd": "${workspaceRoot}", |
||||
|
"runtimeExecutable": null, |
||||
|
"env": { |
||||
|
"NODE_ENV": "development" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
|
||||
|
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2 |
||||
|
|
||||
|
MAINTAINER liuxinyi "liu.xinyi@free-sun.com.cn" |
||||
|
|
||||
|
COPY . /var/app |
||||
|
|
||||
|
WORKDIR /var/app |
||||
|
|
||||
|
EXPOSE 8080 |
||||
|
|
||||
|
CMD ["-g", "postgres://FashionAdmin:123456@iota-m1:5433/SmartRiver", "--qnak", "5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu", "--qnsk", "w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5", "--qnbkt", "anxinyun-test", "--qndmn", "http://test.resources.anxinyun.cn"] |
||||
|
|
||||
|
ENTRYPOINT [ "node", "server.js" ] |
@ -0,0 +1,3 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
module.exports = require('./lib'); |
@ -0,0 +1,189 @@ |
|||||
|
'use strict'; |
||||
|
const Hex = require('crypto-js/enc-hex'); |
||||
|
const MD5 = require('crypto-js/md5'); |
||||
|
const moment = require('moment'); |
||||
|
const uuid = require('uuid'); |
||||
|
|
||||
|
async function login(ctx, next) { |
||||
|
const transaction = await ctx.fs.dc.orm.transaction(); |
||||
|
try { |
||||
|
const models = ctx.fs.dc.models; |
||||
|
const params = ctx.request.body; |
||||
|
let password = Hex.stringify(MD5(params.password)); |
||||
|
|
||||
|
const userRes = await models.User.findOne({ |
||||
|
where: { |
||||
|
username: params.username, |
||||
|
password: password, |
||||
|
delete: false, |
||||
|
}, |
||||
|
attributes: { exclude: ['password'] }, |
||||
|
include: [{ |
||||
|
attributes: ["resourceId"], |
||||
|
model: models.UserResource |
||||
|
}] |
||||
|
}); |
||||
|
|
||||
|
if (!userRes) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { |
||||
|
"message": "账号或密码错误" |
||||
|
} |
||||
|
} else if (!userRes.enable) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { message: "该用户已被禁用" } |
||||
|
} else { |
||||
|
const token = uuid.v4(); |
||||
|
|
||||
|
let userRslt = Object.assign(userRes.dataValues, { |
||||
|
authorized: true, |
||||
|
token: token, |
||||
|
userResources: userRes.userResources.map(r => r.resourceId), |
||||
|
}); |
||||
|
|
||||
|
await models.UserToken.create({ |
||||
|
token: token, |
||||
|
userInfo: userRslt, |
||||
|
expired: moment().add(30, 'days').format() |
||||
|
}); |
||||
|
|
||||
|
ctx.status = 200; |
||||
|
ctx.body = userRslt; |
||||
|
} |
||||
|
await transaction.commit(); |
||||
|
} catch (error) { |
||||
|
await transaction.rollback(); |
||||
|
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { |
||||
|
"message": "登录失败" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 微信小程序登录 |
||||
|
* @@requires.body {phone-手机号, password-密码} ctx |
||||
|
*/ |
||||
|
async function wxLogin(ctx, next) { |
||||
|
const transaction = await ctx.fs.dc.orm.transaction(); |
||||
|
try { |
||||
|
const models = ctx.fs.dc.models; |
||||
|
const params = ctx.request.body; |
||||
|
let password = Hex.stringify(MD5(params.password)); |
||||
|
const userRes = await models.User.findOne({ |
||||
|
where: { |
||||
|
phone: params.phone, |
||||
|
password: password, |
||||
|
delete: false, |
||||
|
}, |
||||
|
attributes: { exclude: ['password'] } |
||||
|
}); |
||||
|
if (!userRes) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { message: "手机号或密码错误" } |
||||
|
} else if (!userRes.enable) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { message: "该用户已被禁用" } |
||||
|
} else { |
||||
|
const token = uuid.v4(); |
||||
|
//获取用户关注区域信息
|
||||
|
const departmentRes = await models.Department.findOne({ where: { id: userRes.departmentId } }); |
||||
|
let attentionRegion = departmentRes; |
||||
|
while (attentionRegion.dependence && attentionRegion.type != 1) { |
||||
|
const departmentParent = await models.Department.findOne({ where: { id: attentionRegion.dependence } }); |
||||
|
attentionRegion = { |
||||
|
...departmentParent.dataValues, |
||||
|
nextRegin: attentionRegion |
||||
|
} |
||||
|
} |
||||
|
//获取用户权限信息
|
||||
|
const resourceRes = await models.UserResource.findAll({ |
||||
|
where: { |
||||
|
userId: userRes.id |
||||
|
}, |
||||
|
include: [{ |
||||
|
model: models.Resource, |
||||
|
attributes: ['code', 'name'], |
||||
|
}], |
||||
|
attributes: [] |
||||
|
}); |
||||
|
let userRslt = Object.assign({ |
||||
|
authorized: true, |
||||
|
token: token, |
||||
|
...userRes.dataValues |
||||
|
}); |
||||
|
await models.UserToken.create({ |
||||
|
token: token, |
||||
|
userInfo: userRslt, |
||||
|
expired: moment().add(30, 'day').format('YYYY-MM-DD HH:mm:ss') |
||||
|
}, { transaction: transaction }); |
||||
|
ctx.status = 200; |
||||
|
ctx.body = Object.assign({ |
||||
|
...userRslt, |
||||
|
userRegionType: departmentRes.type,//1-市级,2-区县级,3-乡镇级,4-村级
|
||||
|
attentionRegion: attentionRegion, |
||||
|
resources: resourceRes.map(r => r.resource) |
||||
|
}); |
||||
|
} |
||||
|
await transaction.commit(); |
||||
|
} catch (error) { |
||||
|
await transaction.rollback(); |
||||
|
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { |
||||
|
"message": "登录失败" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function logout(ctx) { |
||||
|
try { |
||||
|
const { token, code } = ctx.request.body; |
||||
|
const models = ctx.fs.dc.models; |
||||
|
|
||||
|
await models.UserToken.destroy({ |
||||
|
where: { |
||||
|
token: token, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
ctx.status = 204; |
||||
|
} catch (error) { |
||||
|
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { |
||||
|
"message": "登出失败" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 微信小程序登出 |
||||
|
* @request.body {token-用户登录Token} ctx |
||||
|
*/ |
||||
|
async function wxLogout(ctx) { |
||||
|
try { |
||||
|
const { token } = ctx.request.body; |
||||
|
const models = ctx.fs.dc.models; |
||||
|
await models.UserToken.destroy({ |
||||
|
where: { |
||||
|
token: token, |
||||
|
}, |
||||
|
}); |
||||
|
ctx.status = 204; |
||||
|
} catch (error) { |
||||
|
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); |
||||
|
ctx.status = 400; |
||||
|
ctx.body = { |
||||
|
"message": "登出失败" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
login, |
||||
|
wxLogin, |
||||
|
logout, |
||||
|
wxLogout |
||||
|
}; |
@ -0,0 +1,36 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const routes = require('./routes'); |
||||
|
const authenticator = require('./middlewares/authenticator'); |
||||
|
// const apiLog = require('./middlewares/api-log');
|
||||
|
const businessRest = require('./middlewares/business-rest'); |
||||
|
|
||||
|
module.exports.entry = function (app, router, opts) { |
||||
|
app.fs.logger.log('info', '[FS-AUTH]', 'Inject auth and api mv into router.'); |
||||
|
|
||||
|
app.fs.api = app.fs.api || {}; |
||||
|
app.fs.api.authAttr = app.fs.api.authAttr || {}; |
||||
|
app.fs.api.logAttr = app.fs.api.logAttr || {}; |
||||
|
|
||||
|
router.use(authenticator(app, opts)); |
||||
|
router.use(businessRest(app, router, opts)); |
||||
|
// router.use(apiLog(app, opts));
|
||||
|
|
||||
|
router = routes(app, router, opts); |
||||
|
}; |
||||
|
|
||||
|
module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Sequelize, models: {} }
|
||||
|
require('./models/user')(dc); |
||||
|
require('./models/user_token')(dc); |
||||
|
require('./models/department')(dc); |
||||
|
require('./models/resource')(dc); |
||||
|
require('./models/user_resource')(dc); |
||||
|
require('./models/places')(dc); |
||||
|
require('./models/user_placeSecurityRecord')(dc); |
||||
|
require('./models/report_type')(dc); |
||||
|
require('./models/report_downManage')(dc); |
||||
|
require('./models/department')(dc); |
||||
|
require('./models/report_configition')(dc); |
||||
|
require('./models/report_collection')(dc); |
||||
|
require('./models/report_rectify')(dc); |
||||
|
}; |
@ -0,0 +1,83 @@ |
|||||
|
/** |
||||
|
* Created by PengPeng on 2017/4/26. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const moment = require('moment'); |
||||
|
const pathToRegexp = require('path-to-regexp'); |
||||
|
|
||||
|
function factory(app, opts) { |
||||
|
async function sendToEsAsync(producer, payloads) { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
producer.send(payloads, function (err) { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(); |
||||
|
} |
||||
|
}); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function logger(ctx, next) { |
||||
|
const { path, method } = ctx; |
||||
|
const start = Date.now(); |
||||
|
|
||||
|
// 等待路由处理
|
||||
|
await next(); |
||||
|
|
||||
|
try { |
||||
|
let logAttr = null; |
||||
|
for (let prop in app.fs.api.logAttr) { |
||||
|
let keys = []; |
||||
|
let re = pathToRegexp(prop.replace(/\:[A-Za-z_\-]+\b/g, '(\\d+)'), keys); |
||||
|
if (re.test(`${method}${path}`)) { |
||||
|
logAttr = app.fs.api.logAttr[prop]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
let parameter = null, parameterShow = null, user_id, _token, app_key; |
||||
|
if (ctx.fs.api) { |
||||
|
const { actionParameter, actionParameterShow, userId, token, appKey } = ctx.fs.api; |
||||
|
parameter = actionParameter; |
||||
|
parameterShow = actionParameterShow; |
||||
|
user_id = userId; |
||||
|
_token = token; |
||||
|
app_key = appKey; |
||||
|
} |
||||
|
const producer = ctx.fs.kafka.producer; |
||||
|
|
||||
|
const message = { |
||||
|
log_time: moment().toISOString(), |
||||
|
method: method, |
||||
|
content: logAttr ? logAttr.content : '', |
||||
|
parameter: JSON.stringify(parameter) || JSON.stringify(ctx.request.body), |
||||
|
parameter_show: parameterShow, |
||||
|
visible: logAttr ? logAttr.visible : true, |
||||
|
cost: Date.now() - start, |
||||
|
status_code: ctx.status, |
||||
|
url: ctx.request.url, |
||||
|
user_agent: ctx.request.headers["user-agent"], |
||||
|
user_id: user_id, |
||||
|
session: _token, |
||||
|
app_key: app_key, |
||||
|
header: JSON.stringify(ctx.request.headers), |
||||
|
ip: ctx.request.headers["x-real-ip"] || ctx.ip |
||||
|
}; |
||||
|
|
||||
|
const payloads = [{ |
||||
|
topic: `${opts.kafka.topicPrefix}`, |
||||
|
messages: [JSON.stringify(message)], |
||||
|
partition: 0 |
||||
|
}]; |
||||
|
|
||||
|
await sendToEsAsync(producer, payloads); |
||||
|
|
||||
|
} catch (e) { |
||||
|
ctx.fs.logger.error(`日志记录失败: ${e}`); |
||||
|
} |
||||
|
} |
||||
|
return logger; |
||||
|
} |
||||
|
|
||||
|
module.exports = factory; |
@ -0,0 +1,150 @@ |
|||||
|
/** |
||||
|
* Created by PengLing on 2017/3/27. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const pathToRegexp = require('path-to-regexp'); |
||||
|
const util = require('util'); |
||||
|
const moment = require('moment'); |
||||
|
|
||||
|
class ExcludesUrls { |
||||
|
constructor(opts) { |
||||
|
this.allUrls = undefined; |
||||
|
this.reload(opts); |
||||
|
} |
||||
|
|
||||
|
sanitizePath(path) { |
||||
|
if (!path) return '/'; |
||||
|
const p = '/' + path.replace(/^\/+/i, '').replace(/\/+$/, '').replace(/\/{2,}/, '/'); |
||||
|
return p; |
||||
|
} |
||||
|
|
||||
|
reload(opts) { |
||||
|
// load all url
|
||||
|
if (!this.allUrls) { |
||||
|
this.allUrls = opts; |
||||
|
let that = this; |
||||
|
this.allUrls.forEach(function (url, i, arr) { |
||||
|
if (typeof url === "string") { |
||||
|
url = { p: url, o: '*' }; |
||||
|
arr[i] = url; |
||||
|
} |
||||
|
const keys = []; |
||||
|
let eachPath = url.p; |
||||
|
url.p = (!eachPath || eachPath === '(.*)' || util.isRegExp(eachPath)) ? eachPath : that.sanitizePath(eachPath); |
||||
|
url.pregexp = pathToRegexp(eachPath, keys); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
isExcluded(path, method) { |
||||
|
return this.allUrls.some(function (url) { |
||||
|
return !url.auth |
||||
|
&& url.pregexp.test(path) |
||||
|
&& (url.o === '*' || url.o.indexOf(method) !== -1); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断Url是否不鉴权 |
||||
|
* @param {*} opts {exclude: [*] or []},'*'或['*']:跳过所有路由; []:所有路由都要验证 |
||||
|
* @param {*} path 当前request的path |
||||
|
* @param {*} method 当前request的method |
||||
|
*/ |
||||
|
let isPathExcluded = function (opts, path, method) { |
||||
|
let excludeAll = Boolean(opts.exclude && opts.exclude.length && opts.exclude[0] == '*'); |
||||
|
let excludes = null; |
||||
|
if (!excludeAll) { |
||||
|
let excludeOpts = opts.exclude || []; |
||||
|
excludeOpts.push({ p: '/login', o: 'POST' }); |
||||
|
excludeOpts.push({ p: '/wxLogin', o: 'POST' }); |
||||
|
excludeOpts.push({ p: '/logout', o: 'PUT' }); |
||||
|
excludeOpts.push({ p: '/wxLogout', o: 'PUT' }); |
||||
|
excludes = new ExcludesUrls(excludeOpts); |
||||
|
} |
||||
|
let excluded = excludeAll || excludes.isExcluded(path, method); |
||||
|
return excluded; |
||||
|
}; |
||||
|
|
||||
|
let authorizeToken = async function (ctx, token) { |
||||
|
let rslt = null; |
||||
|
const tokenFormatRegexp = /^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/g; |
||||
|
if (token && tokenFormatRegexp.test(token)) { |
||||
|
try { |
||||
|
const axyRes = await ctx.fs.dc.models.UserToken.findOne({ |
||||
|
where: { |
||||
|
token: token, |
||||
|
expired: { $gte: moment().format('YYYY-MM-DD HH:mm:ss') } |
||||
|
} |
||||
|
}); |
||||
|
const { userInfo, expired } = axyRes; |
||||
|
if (!expired || moment().valueOf() <= moment(expired).valueOf()) { |
||||
|
rslt = { |
||||
|
'authorized': userInfo.authorized, |
||||
|
'resources': (userInfo || {}).resources || [], |
||||
|
}; |
||||
|
ctx.fs.api.userId = userInfo.id; |
||||
|
ctx.fs.api.userInfo = userInfo; |
||||
|
ctx.fs.api.token = token; |
||||
|
} |
||||
|
} catch (err) { |
||||
|
const { error } = err.response || {}; |
||||
|
ctx.fs.logger.log('[anxinyun]', '[AUTH] failed', (error || {}).message || `cannot GET /users/${token}`); |
||||
|
} |
||||
|
} |
||||
|
return rslt; |
||||
|
}; |
||||
|
|
||||
|
let isResourceAvailable = function (resources, options) { |
||||
|
let authCode = null; |
||||
|
// authorize user by authorization attribute
|
||||
|
const { authAttr, method, path } = options; |
||||
|
console.log(resources, options) |
||||
|
for (let prop in authAttr) { |
||||
|
let keys = []; |
||||
|
let re = pathToRegexp(prop.replace(/\:[A-Za-z_\-]+\b/g, '(\\d+)'), keys); |
||||
|
if (re.test(`${method}${path}`)) { |
||||
|
authCode = authAttr[prop]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return !authCode || (resources || []).some(code => code === authCode); |
||||
|
}; |
||||
|
|
||||
|
function factory(app, opts) { |
||||
|
return async function auth(ctx, next) { |
||||
|
const { path, method, header, query } = ctx; |
||||
|
ctx.fs.logger.log('[AUTH] start', path, method); |
||||
|
ctx.fs.api = ctx.fs.api || {}; |
||||
|
ctx.fs.port = opts.port; |
||||
|
ctx.redis = app.redis; |
||||
|
let error = null; |
||||
|
if (path) { |
||||
|
if (!isPathExcluded(opts, path, method)) { |
||||
|
const user = await authorizeToken(ctx, header.token || query.token); |
||||
|
if (user && user.authorized) { |
||||
|
// if (!isResourceAvailable(user.resources, { authAttr: app.fs.auth.authAttr, path, method })) {
|
||||
|
// error = { status: 403, name: 'Forbidden' }
|
||||
|
// } else {
|
||||
|
// error = { status: 401, name: 'Unauthorized' }
|
||||
|
// }
|
||||
|
} else { |
||||
|
error = { status: 401, name: 'Unauthorized' } |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
error = { status: 401, name: 'Unauthorized' }; |
||||
|
} |
||||
|
if (error) { |
||||
|
ctx.fs.logger.log('[AUTH] failed', path, method); |
||||
|
ctx.status = error.status; |
||||
|
ctx.body = error.name; |
||||
|
} else { |
||||
|
ctx.fs.logger.log('[AUTH] passed', path, method); |
||||
|
await next(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = factory; |
@ -0,0 +1,50 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const request = require('superagent'); |
||||
|
const buildUrl = (url,token) => { |
||||
|
let connector = url.indexOf('?') === -1 ? '?' : '&'; |
||||
|
return `${url}${connector}token=${token}`; |
||||
|
}; |
||||
|
|
||||
|
function factory(app, router, opts) { |
||||
|
return async function (ctx, next) { |
||||
|
|
||||
|
const token = ctx.fs.api.token; |
||||
|
|
||||
|
//console.log(username,password)
|
||||
|
const req = { |
||||
|
get: (url, query) => { |
||||
|
return request |
||||
|
.get(buildUrl(url,token)) |
||||
|
.query(query) |
||||
|
}, |
||||
|
post: (url, data, query) => { |
||||
|
return request |
||||
|
.post(buildUrl(url,token)) |
||||
|
.query(query) |
||||
|
//.set('Content-Type', 'application/json')
|
||||
|
.send(data); |
||||
|
}, |
||||
|
|
||||
|
put: (url, data) => { |
||||
|
return request |
||||
|
.put(buildUrl(url,token)) |
||||
|
//.set('Content-Type', 'application/json')
|
||||
|
.send(data); |
||||
|
}, |
||||
|
|
||||
|
delete: (url) => { |
||||
|
return request |
||||
|
.del(buildUrl(url,token)) |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
app.business = app.business || {}; |
||||
|
app.business.request = req; |
||||
|
|
||||
|
await next(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
module.exports = factory; |
||||
|
|
@ -0,0 +1,108 @@ |
|||||
|
/* eslint-disable*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
module.exports = dc => { |
||||
|
const DataTypes = dc.ORM; |
||||
|
const sequelize = dc.orm; |
||||
|
const User = sequelize.define("user", { |
||||
|
id: { |
||||
|
type: DataTypes.INTEGER, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: null, |
||||
|
primaryKey: true, |
||||
|
field: "id", |
||||
|
autoIncrement: true, |
||||
|
unique: "user_id_uindex" |
||||
|
}, |
||||
|
name: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: null, |
||||
|
primaryKey: false, |
||||
|
field: "name", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
username: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: "用户名 账号", |
||||
|
primaryKey: false, |
||||
|
field: "username", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
password: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: null, |
||||
|
primaryKey: false, |
||||
|
field: "password", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
departmentId: { |
||||
|
type: DataTypes.INTEGER, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: "部门id", |
||||
|
primaryKey: false, |
||||
|
field: "department_id", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
email: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: true, |
||||
|
defaultValue: null, |
||||
|
comment: null, |
||||
|
primaryKey: false, |
||||
|
field: "email", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
enable: { |
||||
|
type: DataTypes.BOOLEAN, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: "启用状态", |
||||
|
primaryKey: false, |
||||
|
field: "enable", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
delete: { |
||||
|
type: DataTypes.BOOLEAN, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: null, |
||||
|
primaryKey: false, |
||||
|
field: "delete", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
phone: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: false, |
||||
|
defaultValue: null, |
||||
|
comment: "手机号(小程序使用手机号登录)", |
||||
|
primaryKey: false, |
||||
|
field: "phone", |
||||
|
autoIncrement: false |
||||
|
}, |
||||
|
post: { |
||||
|
type: DataTypes.STRING, |
||||
|
allowNull: true, |
||||
|
defaultValue: null, |
||||
|
comment: "职位", |
||||
|
primaryKey: false, |
||||
|
field: "post", |
||||
|
autoIncrement: false |
||||
|
} |
||||
|
}, { |
||||
|
tableName: "user", |
||||
|
comment: "", |
||||
|
indexes: [] |
||||
|
}); |
||||
|
dc.models.User = User; |
||||
|
|
||||
|
|
||||
|
return User; |
||||
|
}; |
@ -0,0 +1,32 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const auth = require('../../controllers/auth'); |
||||
|
|
||||
|
module.exports = function (app, router, opts) { |
||||
|
/** |
||||
|
* @api {Post} login 登录. |
||||
|
* @apiVersion 1.0.0 |
||||
|
* @apiGroup Auth |
||||
|
*/ |
||||
|
app.fs.api.logAttr['POST/login'] = { content: '登录', visible: true }; |
||||
|
router.post('/login', auth.login); |
||||
|
|
||||
|
/** |
||||
|
* @api {POST} wxLogin 微信小程序登录.(使用手机号、密码登录) |
||||
|
* @apiVersion 1.0.0 |
||||
|
* @apiGroup Auth |
||||
|
*/ |
||||
|
app.fs.api.logAttr['POST/wxLogin'] = { content: '微信小程序登录', visible: true }; |
||||
|
router.post('/wxLogin', auth.wxLogin); |
||||
|
|
||||
|
app.fs.api.logAttr['PUT/logout'] = { content: '登出', visible: false }; |
||||
|
router.put('/logout', auth.logout); |
||||
|
|
||||
|
/** |
||||
|
* @api {PUT} wxLogout 微信小程序登出 |
||||
|
* @apiVersion 1.0.0 |
||||
|
* @apiGroup Auth |
||||
|
*/ |
||||
|
app.fs.api.logAttr['PUT/wxLogout'] = { content: '登出', visible: false }; |
||||
|
router.put('/wxLogout', auth.wxLogout); |
||||
|
}; |
@ -0,0 +1,17 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const path = require('path'); |
||||
|
const fs = require('fs'); |
||||
|
|
||||
|
module.exports = function (app, router, opts) { |
||||
|
fs.readdirSync(__dirname).forEach((filename) => { |
||||
|
if (filename.indexOf('.') !== 0 &&fs.lstatSync(path.join(__dirname, filename)).isDirectory()) { |
||||
|
fs.readdirSync(path.join(__dirname, filename)).forEach((api) => { |
||||
|
if (api.indexOf('.') == 0 || api.indexOf('.js') == -1) return; |
||||
|
require(`./${filename}/${api}`)(app, router, opts); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return router; |
||||
|
}; |
@ -0,0 +1,103 @@ |
|||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
const path = require('path'); |
||||
|
const os = require('os'); |
||||
|
const moment = require('moment'); |
||||
|
const args = require('args'); |
||||
|
|
||||
|
const dev = process.env.NODE_ENV == 'development'; |
||||
|
|
||||
|
// 启动参数
|
||||
|
args.option(['p', 'port'], '启动端口'); |
||||
|
args.option(['g', 'pg'], 'postgre服务URL'); |
||||
|
args.option(['f', 'fileHost'], '文件中心本地化存储: WebApi 服务器地址(必填), 该服务器提供文件上传Web服务'); |
||||
|
|
||||
|
const flags = args.parse(process.argv); |
||||
|
|
||||
|
const IOT_VIDEO_ACCESS_DB = process.env.IOT_VIDEO_ACCESS_DB || flags.pg; |
||||
|
const IOT_VIDEO_ACCESS_LOCAL_SVR_ORIGIN = process.env.IOT_VIDEO_ACCESS_LOCAL_SVR_ORIGIN || flags.fileHost; |
||||
|
|
||||
|
if (!IOT_VIDEO_ACCESS_DB) { |
||||
|
console.log('缺少启动参数,异常退出'); |
||||
|
args.showHelp(); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
const product = { |
||||
|
port: flags.port || 8080, |
||||
|
staticDirs: ['static'], |
||||
|
mws: [ |
||||
|
{ |
||||
|
entry: require('@fs/attachment').entry, |
||||
|
opts: { |
||||
|
local: { |
||||
|
origin: IOT_VIDEO_ACCESS_LOCAL_SVR_ORIGIN || `http://localhost:${flags.port || 8080}`, |
||||
|
rootPath: 'static', |
||||
|
childPath: 'upload', |
||||
|
}, |
||||
|
maxSize: 104857600, // 100M
|
||||
|
} |
||||
|
}, { |
||||
|
entry: require('./app').entry, |
||||
|
opts: { |
||||
|
exclude: [], // 不做认证的路由,也可以使用 exclude: ["*"] 跳过所有路由
|
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
dc: { |
||||
|
url: IOT_VIDEO_ACCESS_DB, |
||||
|
opts: { |
||||
|
pool: { |
||||
|
max: 80, |
||||
|
min: 10, |
||||
|
idle: 10000 |
||||
|
}, |
||||
|
define: { |
||||
|
freezeTableName: true, // 固定表名
|
||||
|
timestamps: false // 不含列 "createAt"/"updateAt"/"DeleteAt"
|
||||
|
}, |
||||
|
timezone: '+08:00', |
||||
|
logging: false |
||||
|
}, |
||||
|
models: [require('./app').models] |
||||
|
}, |
||||
|
logger: { |
||||
|
level: 'info', |
||||
|
json: false, |
||||
|
filename: path.join(__dirname, 'log', 'runtime.log'), |
||||
|
colorize: false, |
||||
|
maxsize: 1024 * 1024 * 5, |
||||
|
rotationFormat: false, |
||||
|
zippedArchive: true, |
||||
|
maxFiles: 10, |
||||
|
prettyPrint: true, |
||||
|
label: '', |
||||
|
timestamp: () => moment().format('YYYY-MM-DD HH:mm:ss.SSS'), |
||||
|
eol: os.EOL, |
||||
|
tailable: true, |
||||
|
depth: null, |
||||
|
showLevel: true, |
||||
|
maxRetries: 1 |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const development = { |
||||
|
port: product.port, |
||||
|
staticDirs: product.staticDirs, |
||||
|
mws: product.mws, |
||||
|
dc: product.dc, |
||||
|
logger: product.logger |
||||
|
}; |
||||
|
|
||||
|
if (dev) { |
||||
|
// mws
|
||||
|
for (let mw of development.mws) { |
||||
|
// if (mw.opts.exclude) mw.opts.exclude = ['*']; // 使用 ['*'] 跳过所有路由
|
||||
|
} |
||||
|
// logger
|
||||
|
development.logger.filename = path.join(__dirname, 'log', 'development.log'); |
||||
|
development.logger.level = 'debug'; |
||||
|
development.dc.opts.logging = console.log; |
||||
|
} |
||||
|
|
||||
|
module.exports = dev ? development : product; |
@ -0,0 +1,36 @@ |
|||||
|
{ |
||||
|
"name": "smart-emergency", |
||||
|
"version": "1.0.0", |
||||
|
"description": "fs smart emergency api", |
||||
|
"main": "server.js", |
||||
|
"scripts": { |
||||
|
"test": "set DEBUG=true&&\"node_modules/.bin/mocha\" --harmony --reporter spec app/test/*.test.js", |
||||
|
"start": "set NODE_ENV=development&&node server -p 14000 -g postgres://postgres:123@10.8.30.32:5432/yinjiguanli -f http://localhost:14000", |
||||
|
"start:linux": "export NODE_ENV=development&&node server -p 4000 -g postgres://FashionAdmin:123456@10.8.30.39:5432/pm1", |
||||
|
"automate": "sequelize-automate -c sequelize-automate.config.js" |
||||
|
}, |
||||
|
"author": "", |
||||
|
"license": "MIT", |
||||
|
"repository": {}, |
||||
|
"dependencies": { |
||||
|
"@fs/attachment": "^1.0.0", |
||||
|
"args": "^3.0.7", |
||||
|
"crypto-js": "^4.0.0", |
||||
|
"file-saver": "^2.0.2", |
||||
|
"fs-web-server-scaffold": "^2.0.2", |
||||
|
"ioredis": "^4.19.4", |
||||
|
"koa-convert": "^1.2.0", |
||||
|
"koa-proxy": "^0.9.0", |
||||
|
"moment": "^2.24.0", |
||||
|
"path": "^0.12.7", |
||||
|
"path-to-regexp": "^3.0.0", |
||||
|
"pg": "^7.9.0", |
||||
|
"redis": "^3.1.2", |
||||
|
"request": "^2.88.2", |
||||
|
"superagent": "^3.5.2", |
||||
|
"uuid": "^3.3.2" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"mocha": "^6.0.2" |
||||
|
} |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
module.exports = { |
||||
|
// 数据库配置 与 sequelize 相同
|
||||
|
dbOptions: { |
||||
|
database: 'yinjiguanli', |
||||
|
username: 'postgres', |
||||
|
password: '123', |
||||
|
dialect: 'postgres', |
||||
|
host: '10.8.30.32', |
||||
|
port: 5432, |
||||
|
define: { |
||||
|
underscored: false, |
||||
|
freezeTableName: false, |
||||
|
charset: 'utf8mb4', |
||||
|
timezone: '+00: 00', |
||||
|
dialectOptions: { |
||||
|
collate: 'utf8_general_ci', |
||||
|
}, |
||||
|
timestamps: false, |
||||
|
}, |
||||
|
}, |
||||
|
options: { |
||||
|
type: 'freesun', // 指定 models 代码风格
|
||||
|
camelCase: true, // Models 文件中代码是否使用驼峰命名
|
||||
|
modalNameSuffix: false, // 模型名称是否带 ‘Model’ 后缀
|
||||
|
fileNameCamelCase: false, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js`
|
||||
|
dir: './app/lib/models', // 指定输出 models 文件的目录
|
||||
|
typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义
|
||||
|
emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir`
|
||||
|
tables: ['user_placeSecurityRecord', 'places'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性
|
||||
|
skipTables: ['user'], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性
|
||||
|
tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中
|
||||
|
ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面
|
||||
|
attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false
|
||||
|
}, |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
/** |
||||
|
* Created by rain on 2016/1/25. |
||||
|
*/ |
||||
|
|
||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
//from koa
|
||||
|
|
||||
|
const scaffold = require('fs-web-server-scaffold'); |
||||
|
const config = require('./config'); |
||||
|
|
||||
|
module.exports = scaffold(config); |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
const proxy = require('koa-proxy'); |
||||
|
const convert = require('koa-convert'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
app.use(convert(proxy({ |
||||
|
host: opts.host, |
||||
|
match: opts.match, |
||||
|
map: function (path) { |
||||
|
return path.replace(opts.match, ''); |
||||
|
} |
||||
|
}))); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,17 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"@babel/preset-react", |
||||
|
"@babel/preset-env" |
||||
|
], |
||||
|
"plugins": [ |
||||
|
"@babel/plugin-proposal-class-properties", |
||||
|
"@babel/plugin-proposal-object-rest-spread", |
||||
|
// ["import", { |
||||
|
// // "libraryName": "antd", |
||||
|
// "libraryDirectory": "es" |
||||
|
// }] |
||||
|
], |
||||
|
"env": { |
||||
|
"development": {} |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
{ |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [ |
||||
|
{ |
||||
|
"name": "Server", |
||||
|
"type": "node", |
||||
|
"request": "launch", |
||||
|
"program": "${workspaceRoot}/server.js", |
||||
|
"args": [ |
||||
|
"-u http://127.0.0.1:4000" |
||||
|
], |
||||
|
"outputCapture": "std", |
||||
|
"env": { |
||||
|
"NODE_ENV": "development" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
// 将设置放入此文件中以覆盖默认值和用户设置。 |
||||
|
{ |
||||
|
"editor.fontSize": 16, |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
|
||||
|
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2 |
||||
|
|
||||
|
COPY . /var/app |
||||
|
|
||||
|
WORKDIR /var/app |
||||
|
|
||||
|
EXPOSE 8080 |
||||
|
|
||||
|
CMD ["-u", "http://localhost:8088"] |
||||
|
|
||||
|
ENTRYPOINT [ "node", "server.js" ] |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 202 KiB |
@ -0,0 +1,14 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> |
||||
|
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div id='App'></div> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -0,0 +1,44 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> |
||||
|
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div id='App'></div> |
||||
|
|
||||
|
<!-- Webpack --> |
||||
|
<script type="text/javascript" src="http://localhost:5001/client/build/app.js"></script> |
||||
|
<!-- Vite --> |
||||
|
<script type="module"> |
||||
|
import RefreshRuntime from "http://localhost:5002/@react-refresh" |
||||
|
RefreshRuntime.injectIntoGlobalHook(window) |
||||
|
window.$RefreshReg$ = () => { } |
||||
|
window.$RefreshSig$ = () => (type) => type |
||||
|
window.__vite_plugin_react_preamble_installed__ = true |
||||
|
const global = window |
||||
|
</script> |
||||
|
<script type="module" src="http://localhost:5002/src/index.jsx"></script> |
||||
|
<!-- Vite End --> |
||||
|
<script> |
||||
|
//过滤掉一些无用的警告、没有价值的报错 |
||||
|
//代理console.warn方法 |
||||
|
const _consoleWarn = (...rest) => { |
||||
|
console.error(...rest) |
||||
|
}; |
||||
|
console.error = (...rest) => { |
||||
|
if ( |
||||
|
![ |
||||
|
'Each child in a list should have a unique "key" prop', |
||||
|
].some((item) => rest[0].indexOf(item) !== -1) |
||||
|
) { |
||||
|
_consoleWarn(...rest); |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
||||
|
* Date: 2016/2/22 |
||||
|
* Time: 15:29 |
||||
|
* |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
const views = require('koa-view'); |
||||
|
const path = require('path'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opt) { |
||||
|
app.use(views(__dirname)); |
||||
|
|
||||
|
router.get('(.*)', async function (ctx) { |
||||
|
await ctx.render(path.join(__dirname, './index')); |
||||
|
}); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,23 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { useEffect } from 'react'; |
||||
|
import Layout from './layout'; |
||||
|
import Auth from './sections/auth'; |
||||
|
import Example from './sections/example'; |
||||
|
|
||||
|
const App = props => { |
||||
|
const { projectName } = props |
||||
|
|
||||
|
useEffect(() => { |
||||
|
document.title = projectName; |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Layout |
||||
|
title={projectName} |
||||
|
sections={[Auth, Example]} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default App; |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export { |
||||
|
|
||||
|
}; |
@ -0,0 +1,8 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React from 'react'; |
||||
|
import { render } from 'react-dom'; |
||||
|
import App from './app'; |
||||
|
import './index.less'; |
||||
|
|
||||
|
render((<App projectName="飞尚物联" />), document.getElementById('App')); |
@ -0,0 +1,38 @@ |
|||||
|
// webpack (vite 用 alias 兼容了) |
||||
|
@import '~@douyinfe/semi-ui/dist/css/semi.min.css'; |
||||
|
@import '~perfect-scrollbar/css/perfect-scrollbar.css'; |
||||
|
@import '~nprogress/nprogress.css'; |
||||
|
|
||||
|
|
||||
|
*, |
||||
|
*::before, |
||||
|
*::after { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
html, |
||||
|
body { |
||||
|
margin: 0; |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
|
||||
|
a:link { |
||||
|
text-decoration: none; |
||||
|
color: unset |
||||
|
} |
||||
|
|
||||
|
a:visited { |
||||
|
text-decoration: none; |
||||
|
color: unset |
||||
|
} |
||||
|
|
||||
|
a:hover { |
||||
|
text-decoration: none; |
||||
|
color: unset |
||||
|
} |
||||
|
|
||||
|
a:active { |
||||
|
text-decoration: none; |
||||
|
color: unset |
||||
|
} |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
'use strict'; |
||||
|
import { RouteRequest } from '@peace/utils'; |
||||
|
import { RouteTable } from '$utils' |
||||
|
|
||||
|
export const INIT_LAYOUT = 'INIT_LAYOUT'; |
||||
|
export function initLayout (title, copyright, sections, actions) { |
||||
|
return { |
||||
|
type: INIT_LAYOUT, |
||||
|
payload: { |
||||
|
title, |
||||
|
copyright, |
||||
|
sections, |
||||
|
actions |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export const RESIZE = 'RESIZE'; |
||||
|
export function resize (clientHeight, clientWidth) { |
||||
|
const headerHeight = 60 |
||||
|
const footerHeight = 0 |
||||
|
return { |
||||
|
type: RESIZE, |
||||
|
payload: { |
||||
|
clientHeight: clientHeight - headerHeight - footerHeight, |
||||
|
clientWidth: clientWidth |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const INIT_API_ROOT = 'INIT_API_ROOT'; |
||||
|
export function initApiRoot () { |
||||
|
return dispatch => { |
||||
|
RouteRequest.get(RouteTable.apiRoot).then(res => { |
||||
|
localStorage.setItem('apiRoot', res.root); |
||||
|
dispatch({ |
||||
|
type: INIT_API_ROOT, |
||||
|
payload: { |
||||
|
apiRoot: res.root |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
import React from 'react'; |
||||
|
import moment from 'moment' |
||||
|
|
||||
|
export default class Footer extends React.Component { |
||||
|
render () { |
||||
|
// const { } = this.props; |
||||
|
|
||||
|
return ( |
||||
|
<div style={{ textAlign: 'center', lineHeight: '32px' }}> |
||||
|
Copyright © {moment().year()} All Rights Reserved 版权所有· 江西飞尚科技有限公司 |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,43 @@ |
|||||
|
'use strict'; |
||||
|
import React from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Nav } from '@douyinfe/semi-ui'; |
||||
|
|
||||
|
const Header = props => { |
||||
|
const { dispatch, history, user, actions } = props |
||||
|
|
||||
|
return ( |
||||
|
<div style={{ position: 'relative', height: 60, minWidth: 520 }}> |
||||
|
<div style={{ float: 'left', paddingLeft: 32, fontSize: 16 }}> |
||||
|
<div style={{ |
||||
|
lineHeight: '60px', display: 'inline-block', fontSize: 20, textShadow: '0 4px 3px rgba(0, 0, 0, 0.2)', |
||||
|
userSelect: 'none' |
||||
|
}}> |
||||
|
飞尚物联 |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="nav" style={{ float: 'right' }}> |
||||
|
<Nav mode={'horizontal'} onClick={({ itemKey }) => { |
||||
|
if (itemKey == 'logout') { |
||||
|
dispatch(actions.auth.logout(user)); |
||||
|
history.push(`/signin`); |
||||
|
} |
||||
|
}}> |
||||
|
<Nav.Sub itemKey={'user'} text={<div style={{ display: 'inline-block' }}>{user.displayName}</div>}> |
||||
|
<Nav.Item itemKey={'logout'} text={'退出'} /> |
||||
|
</Nav.Sub> |
||||
|
</Nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
}; |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { global, auth } = state; |
||||
|
return { |
||||
|
actions: global.actions, |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Header); |
@ -0,0 +1,59 @@ |
|||||
|
import React, { useEffect, useState } from 'react'; |
||||
|
import PerfectScrollbar from 'perfect-scrollbar'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Nav } from '@douyinfe/semi-ui'; |
||||
|
import { push } from 'react-router-redux'; |
||||
|
|
||||
|
let scrollbar = null |
||||
|
const Sider = props => { |
||||
|
const { collapsed, clientHeight, dispatch } = props |
||||
|
const [items, setItems] = useState([]) |
||||
|
const [selectedKeys, setSelectedKeys] = useState([]) |
||||
|
const [openKeys, setOpenKeys] = useState([]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const { sections, dispatch, user } = props; |
||||
|
let nextItems = [] |
||||
|
for (let c of sections) { |
||||
|
if (typeof c.getNavItem == 'function') { |
||||
|
let item = c.getNavItem(user, dispatch); |
||||
|
if (item) { |
||||
|
nextItems.push.apply(nextItems, item) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
setItems(nextItems) |
||||
|
|
||||
|
scrollbar = new PerfectScrollbar('#page-slider', { suppressScrollX: true }); |
||||
|
}, []) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (scrollbar) { |
||||
|
scrollbar.update(); |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return ( |
||||
|
<div id={'page-slider'} style={{ height: clientHeight, position: 'relative' }}> |
||||
|
<Nav |
||||
|
style={{}} |
||||
|
onSelect={({ selectedItems }) => { |
||||
|
const selectItem = selectedItems[0] |
||||
|
if (selectItem.to) { |
||||
|
dispatch(push(selectItem.to)) |
||||
|
} |
||||
|
}} |
||||
|
items={items} |
||||
|
/> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { global } = state; |
||||
|
return { |
||||
|
clientHeight: global.clientHeight, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Sider); |
@ -0,0 +1,6 @@ |
|||||
|
'use strict'; |
||||
|
import Layout from './layout'; |
||||
|
import NoMatch from './no-match'; |
||||
|
|
||||
|
export { Layout }; |
||||
|
export { NoMatch }; |
@ -0,0 +1,135 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Layout, Toast } from '@douyinfe/semi-ui'; |
||||
|
import Sider from '../../components/sider'; |
||||
|
import Header from '../../components/header'; |
||||
|
import Footer from '../../components/footer'; |
||||
|
import { resize } from '../../actions/global'; |
||||
|
import * as NProgress from 'nprogress'; |
||||
|
import PerfectScrollbar from 'perfect-scrollbar'; |
||||
|
|
||||
|
NProgress.configure({ |
||||
|
template: ` |
||||
|
<div class="bar" style="height:2px" role="bar"> |
||||
|
<div class="peg"></div> |
||||
|
</div> |
||||
|
<div class="spinner" role="spinner"> |
||||
|
<div class="spinner-icon"></div> |
||||
|
</div> |
||||
|
` |
||||
|
}); |
||||
|
|
||||
|
let scrollbar |
||||
|
|
||||
|
const LayoutContainer = props => { |
||||
|
const { |
||||
|
dispatch, msg, user, copyright, children, sections, clientWidth, clientHeight, |
||||
|
location, match, routes, history |
||||
|
} = props |
||||
|
const [collapsed, setCollapsed] = useState(false) |
||||
|
|
||||
|
NProgress.start(); |
||||
|
|
||||
|
const resize_ = () => { |
||||
|
dispatch(resize( |
||||
|
document.body.clientHeight, |
||||
|
document.body.clientWidth - (collapsed ? 120 : 240) |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true }); |
||||
|
|
||||
|
window.addEventListener('resize', resize_); |
||||
|
return () => { |
||||
|
window.removeEventListener('resize', resize_); |
||||
|
} |
||||
|
}, []) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
NProgress.done(); |
||||
|
if (!user || !user.authorized) { |
||||
|
history.push('/signin'); |
||||
|
} |
||||
|
if (msg) { |
||||
|
if (msg.done) { |
||||
|
Toast.success(msg.done); |
||||
|
} |
||||
|
if (msg.error) { |
||||
|
Toast.error(msg.error); |
||||
|
} |
||||
|
} |
||||
|
const dom = document.getElementById('page-content'); |
||||
|
if (dom) { |
||||
|
scrollbar.update(); |
||||
|
dom.scrollTop = 0; |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return ( |
||||
|
<Layout id="layout"> |
||||
|
<Layout.Header> |
||||
|
<Header |
||||
|
user={user} |
||||
|
pathname={location.pathname} |
||||
|
toggleCollapsed={() => { |
||||
|
setCollapsed(!collapsed); |
||||
|
}} |
||||
|
collapsed={collapsed} |
||||
|
history={history} |
||||
|
/> |
||||
|
</Layout.Header> |
||||
|
<Layout> |
||||
|
<Layout.Sider> |
||||
|
<Sider |
||||
|
sections={sections} |
||||
|
dispatch={dispatch} |
||||
|
user={user} |
||||
|
pathname={location.pathname} |
||||
|
collapsed={collapsed} |
||||
|
/> |
||||
|
</Layout.Sider> |
||||
|
<Layout.Content> |
||||
|
<div style={{ |
||||
|
margin: '12px 12px 0px', |
||||
|
}}> |
||||
|
<div id="page-content" style={{ |
||||
|
height: clientHeight - 12, |
||||
|
minWidth: 520, |
||||
|
position: 'relative', |
||||
|
}}> |
||||
|
<div style={{ |
||||
|
minHeight: clientHeight - 32 - 12, |
||||
|
position: 'relative', |
||||
|
padding: '12px 8px', |
||||
|
}}> |
||||
|
{children} |
||||
|
</div> |
||||
|
<Layout.Footer> |
||||
|
<Footer /> |
||||
|
</Layout.Footer> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Layout.Content> |
||||
|
</Layout> |
||||
|
</Layout > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { global, auth, ajaxResponse } = state; |
||||
|
return { |
||||
|
title: global.title, |
||||
|
copyright: global.copyright, |
||||
|
sections: global.sections, |
||||
|
actions: global.actions, |
||||
|
clientWidth: global.clientWidth, |
||||
|
clientHeight: global.clientHeight, |
||||
|
msg: ajaxResponse.msg, |
||||
|
user: auth.user |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(LayoutContainer); |
@ -0,0 +1,18 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import React from 'react'; |
||||
|
import moment from 'moment' |
||||
|
|
||||
|
const NoMatch = props => { |
||||
|
return ( |
||||
|
<div style={{ textAlign: 'center', padding: 120 }}> |
||||
|
<p style={{ fontSize: 80, lineHeight: 1.5 }}>404</p> |
||||
|
<p style={{ fontSize: 32, lineHeight: 2 }}>PAGE NOT FOUND</p> |
||||
|
<p>很遗憾,您暂时无法访问该页面。</p> |
||||
|
<p>请检查您访问的链接地址是否正确。</p> |
||||
|
<p style={{ marginTop: 80 }}>Copyright © {moment().year()} 飞尚</p> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default NoMatch; |
@ -0,0 +1,177 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useEffect, useState } from 'react'; |
||||
|
import moment from 'moment'; |
||||
|
import configStore from './store'; |
||||
|
import { Provider } from 'react-redux'; |
||||
|
import { createBrowserHistory } from 'history'; |
||||
|
import { ConnectedRouter } from 'connected-react-router' |
||||
|
import { Layout, NoMatch } from './containers'; |
||||
|
import { Switch, Route } from "react-router-dom"; |
||||
|
import { ConfigProvider } from '@douyinfe/semi-ui'; |
||||
|
import * as layoutActions from './actions/global'; |
||||
|
import zhCN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN'; |
||||
|
import { basicReducer } from '@peace/utils'; |
||||
|
import 'moment/locale/zh-cn'; |
||||
|
|
||||
|
moment.locale('zh-cn'); |
||||
|
|
||||
|
const { initLayout, initApiRoot, resize } = layoutActions; |
||||
|
|
||||
|
const Root = props => { |
||||
|
const { sections, title, copyright } = props; |
||||
|
const [history, setHistory] = useState(null) |
||||
|
const [store, setStore] = useState(null) |
||||
|
const [outerRoutes, setOuterRoutes] = useState([]) |
||||
|
const [combineRoutes, setCombineRoutes] = useState([]) |
||||
|
const [innnerRoutes, setInnerRoutes] = useState([]) |
||||
|
|
||||
|
const flatRoutes = (routes) => { |
||||
|
const combineRoutes = []; |
||||
|
|
||||
|
function flat (routes, parentRoute) { |
||||
|
routes.forEach((route, i) => { |
||||
|
let obj = { |
||||
|
path: route.path, |
||||
|
breadcrumb: route.breadcrumb, |
||||
|
component: route.component || null, |
||||
|
authCode: route.authCode || '', |
||||
|
key: route.key |
||||
|
} |
||||
|
if (!route.path.startsWith("/")) { |
||||
|
console.error('路由配置需以 "/" 开始:' + route.path); |
||||
|
} |
||||
|
if (route.path.length > 1 && route.path[route.path.length] == '/') { |
||||
|
console.error('除根路由路由配置不可以以 "/" 结束:' + route.path); |
||||
|
} |
||||
|
if (parentRoute && parentRoute != '/') { |
||||
|
obj.path = parentRoute + route.path; |
||||
|
} |
||||
|
if (route.exact) { |
||||
|
obj.exact = true |
||||
|
} |
||||
|
if (route.hasOwnProperty('childRoutes')) { |
||||
|
combineRoutes.push(obj); |
||||
|
flat(route.childRoutes, obj.path) |
||||
|
} else { |
||||
|
combineRoutes.push(obj) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
flat(routes); |
||||
|
return combineRoutes; |
||||
|
} |
||||
|
|
||||
|
const initReducer = (reducers, reducerName, action) => { |
||||
|
let reducerParams = {} |
||||
|
const { actionType, initReducer, reducer } = action()() |
||||
|
if (initReducer || reducer) { |
||||
|
if (reducer) { |
||||
|
if (reducer.name) { |
||||
|
reducerName = reducer.name |
||||
|
} |
||||
|
if (reducer.params) { |
||||
|
reducerParams = reducer.params |
||||
|
} |
||||
|
} else { |
||||
|
reducerName = `${reducerName}Rslt` |
||||
|
} |
||||
|
reducers[reducerName] = function (state, action) { |
||||
|
return basicReducer(state, action, Object.assign({ actionType: actionType }, reducerParams)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
let innerRoutes = [] |
||||
|
let outerRoutes = [] |
||||
|
let reducers = {} |
||||
|
let actions = { |
||||
|
layout: layoutActions |
||||
|
} |
||||
|
|
||||
|
for (let s of sections) { |
||||
|
if (!s.key) console.warn('请给你的section添加一个key值,section name:' + s.name); |
||||
|
for (let r of s.routes) { |
||||
|
if (r.type == 'inner' || r.type == 'home') { |
||||
|
innerRoutes.push(r.route) |
||||
|
} else if (r.type == 'outer') { |
||||
|
outerRoutes.push(r.route) |
||||
|
} |
||||
|
} |
||||
|
if (s.reducers) { |
||||
|
reducers = { ...reducers, ...s.reducers } |
||||
|
} |
||||
|
if (s.actions) { |
||||
|
actions = { ...actions, [s.key]: s.actions } |
||||
|
if (s.key != 'auth') { |
||||
|
for (let ak in s.actions) { |
||||
|
let actions = s.actions[ak] |
||||
|
if (actions && typeof actions == 'object') { |
||||
|
for (let actionName in actions) { |
||||
|
initReducer(reducers, actionName, actions[actionName]) |
||||
|
} |
||||
|
} else if (typeof actions == 'function') { |
||||
|
initReducer(reducers, ak, actions) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let history = createBrowserHistory(); |
||||
|
let store = configStore(reducers, history); |
||||
|
store.dispatch(initLayout(title, copyright, sections, actions)); |
||||
|
store.dispatch(resize(document.body.clientHeight, document.body.clientWidth)); |
||||
|
store.dispatch(actions.auth.initAuth()); |
||||
|
store.dispatch(initApiRoot()) |
||||
|
|
||||
|
const combineRoutes = flatRoutes(innerRoutes); |
||||
|
|
||||
|
setInnerRoutes(combineRoutes) |
||||
|
setHistory(history) |
||||
|
setStore(store) |
||||
|
setOuterRoutes(outerRoutes.map(route => ( |
||||
|
<Route |
||||
|
key={route.key} |
||||
|
exact |
||||
|
path={route.path} |
||||
|
component={route.component} |
||||
|
/> |
||||
|
))) |
||||
|
setCombineRoutes(combineRoutes.map(route => ( |
||||
|
<Route |
||||
|
key={route.key} |
||||
|
exact={route.hasOwnProperty('exact') ? route.exact : true} |
||||
|
path={route.path} |
||||
|
component={route.component} |
||||
|
/> |
||||
|
))) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
store ? |
||||
|
<ConfigProvider locale={zhCN}> |
||||
|
<Provider store={store}> |
||||
|
<ConnectedRouter history={history}> |
||||
|
<Switch> |
||||
|
{outerRoutes} |
||||
|
<Layout |
||||
|
history={history} |
||||
|
routes={innnerRoutes} |
||||
|
> |
||||
|
{combineRoutes} |
||||
|
</Layout> |
||||
|
<Route |
||||
|
path={'*'} |
||||
|
component={NoMatch} |
||||
|
/> |
||||
|
</Switch> |
||||
|
</ConnectedRouter> |
||||
|
</Provider> |
||||
|
</ConfigProvider> |
||||
|
: '' |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Root; |
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/1. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
const initState = { |
||||
|
msg: null |
||||
|
}; |
||||
|
|
||||
|
import Immutable from 'immutable'; |
||||
|
|
||||
|
/** |
||||
|
* 全局ajax响应处理: |
||||
|
* 判断action中是否有done字段,如果有,则修改store中的msg.done |
||||
|
* 判断action中是否有error字段,如果有,则修改store中msg.error |
||||
|
* 在layout中根据msg的值,呈现提示信息。 |
||||
|
*/ |
||||
|
export default function ajaxResponse(state = initState, action) { |
||||
|
if (action.done) { |
||||
|
return Immutable.fromJS(state).set('msg', {done: action.done}).toJS(); |
||||
|
} |
||||
|
|
||||
|
if (action.error) { |
||||
|
return Immutable.fromJS(state).set('msg', {error: action.error}).toJS(); |
||||
|
} |
||||
|
|
||||
|
return {msg: null}; |
||||
|
}; |
@ -0,0 +1,36 @@ |
|||||
|
'use strict'; |
||||
|
import Immutable from 'immutable'; |
||||
|
import { INIT_LAYOUT, RESIZE } from '../actions/global'; |
||||
|
|
||||
|
function global (state = { |
||||
|
title: '', |
||||
|
copyright: '', |
||||
|
sections: [], |
||||
|
actions: {}, |
||||
|
plugins: {}, |
||||
|
clientHeight: 768, |
||||
|
clientWidth: 1024 |
||||
|
}, action) { |
||||
|
const payload = action.payload; |
||||
|
switch (action.type) { |
||||
|
case RESIZE: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
clientHeight: payload.clientHeight, |
||||
|
clientWidth: payload.clientWidth |
||||
|
}).toJS(); |
||||
|
case INIT_LAYOUT: |
||||
|
return { |
||||
|
title: payload.title, |
||||
|
copyright: payload.copyright, |
||||
|
sections: payload.sections, |
||||
|
actions: payload.actions, |
||||
|
plugins: payload.plugins, |
||||
|
clientHeight: state.clientHeight, |
||||
|
detailsComponent: null |
||||
|
}; |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default global; |
@ -0,0 +1,15 @@ |
|||||
|
/** |
||||
|
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
||||
|
* Date: 2016/1/13 |
||||
|
* Time: 17:52 |
||||
|
* |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
import global from './global'; |
||||
|
import ajaxResponse from './ajaxResponse'; |
||||
|
|
||||
|
export default { |
||||
|
global, |
||||
|
ajaxResponse |
||||
|
}; |
@ -0,0 +1,13 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import storeProd from './store.prod' |
||||
|
import storeDev from './store.dev' |
||||
|
|
||||
|
let store = null; |
||||
|
if (process.env.NODE_ENV == 'production') { |
||||
|
store = storeProd; |
||||
|
} else { |
||||
|
store = storeDev; |
||||
|
} |
||||
|
|
||||
|
export default store; |
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/8. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; |
||||
|
import reduxThunk from 'redux-thunk'; |
||||
|
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
||||
|
import innerReducers from '../reducers'; |
||||
|
|
||||
|
function configStore(reducers, history) { |
||||
|
const reducer = Object.assign({}, innerReducers, reducers, { |
||||
|
router: connectRouter(history) |
||||
|
}); |
||||
|
|
||||
|
const composeEnhancers = |
||||
|
typeof window === 'object' && |
||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? |
||||
|
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ |
||||
|
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||
|
}) : compose; |
||||
|
|
||||
|
const enhancers = composeEnhancers( |
||||
|
applyMiddleware(routerMiddleware(history), reduxThunk) |
||||
|
); |
||||
|
|
||||
|
return createStore(combineReducers(reducer), {}, enhancers); |
||||
|
} |
||||
|
|
||||
|
export default configStore; |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/8. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
|
||||
|
import { createStore, combineReducers, applyMiddleware } from 'redux'; |
||||
|
import reduxThunk from 'redux-thunk'; |
||||
|
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
||||
|
import innerReducers from '../reducers'; |
||||
|
|
||||
|
function configStore(reducers, history){ |
||||
|
const reducer = Object.assign({}, innerReducers, reducers, { |
||||
|
router: connectRouter(history) |
||||
|
}); |
||||
|
|
||||
|
return createStore(combineReducers(reducer), {}, applyMiddleware(routerMiddleware(history), reduxThunk)); |
||||
|
} |
||||
|
|
||||
|
export default configStore; |
@ -0,0 +1,79 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { ApiTable } from '$utils' |
||||
|
import { Request } from '@peace/utils' |
||||
|
|
||||
|
export const INIT_AUTH = 'INIT_AUTH'; |
||||
|
export function initAuth () { |
||||
|
const user = JSON.parse(sessionStorage.getItem('user')) || {}; |
||||
|
return { |
||||
|
type: INIT_AUTH, |
||||
|
payload: { |
||||
|
user: user |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export const REQUEST_LOGIN = 'REQUEST_LOGIN'; |
||||
|
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; |
||||
|
export const LOGIN_ERROR = 'LOGIN_ERROR'; |
||||
|
export function login (username, password) { |
||||
|
return dispatch => { |
||||
|
dispatch({ type: REQUEST_LOGIN }); |
||||
|
|
||||
|
if (!username || !password) { |
||||
|
dispatch({ |
||||
|
type: LOGIN_ERROR, |
||||
|
payload: { error: '请输入账号名和密码' } |
||||
|
}); |
||||
|
return Promise.resolve(); |
||||
|
} |
||||
|
|
||||
|
return dispatch({ |
||||
|
type: LOGIN_SUCCESS, |
||||
|
payload: { |
||||
|
user: { |
||||
|
authorized: true, |
||||
|
displayName: 'TEST' |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const url = ApiTable.login; |
||||
|
return Request.post(url, { username, password, p: '456' }) |
||||
|
.then(user => { |
||||
|
sessionStorage.setItem('user', JSON.stringify(user)); |
||||
|
dispatch({ |
||||
|
type: LOGIN_SUCCESS, |
||||
|
payload: { user: user }, |
||||
|
}); |
||||
|
}, error => { |
||||
|
let { body } = error.response; |
||||
|
dispatch({ |
||||
|
type: LOGIN_ERROR, |
||||
|
payload: { |
||||
|
error: body && body.message ? body.message : '登录失败' |
||||
|
} |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const LOGOUT = 'LOGOUT'; |
||||
|
export function logout (user) { |
||||
|
const token = user.token; |
||||
|
const url = ApiTable.logout; |
||||
|
sessionStorage.removeItem('user'); |
||||
|
Request.put(url, { |
||||
|
token: token |
||||
|
}); |
||||
|
return { |
||||
|
type: LOGOUT |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
initAuth, |
||||
|
login, |
||||
|
logout |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/4/1. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
import auth from './auth'; |
||||
|
|
||||
|
export default { |
||||
|
...auth |
||||
|
}; |
@ -0,0 +1,4 @@ |
|||||
|
'use strict'; |
||||
|
import Login from './login'; |
||||
|
|
||||
|
export { Login }; |
@ -0,0 +1,62 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useEffect, useRef } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { push } from 'react-router-redux'; |
||||
|
import { Form, Button, Toast } from '@douyinfe/semi-ui'; |
||||
|
import { login } from '../actions/auth'; |
||||
|
|
||||
|
const Login = props => { |
||||
|
const { dispatch, user, error, isRequesting } = props |
||||
|
const form = useRef(); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (error) { |
||||
|
Toast.error(error); |
||||
|
form.current.setValue('password', '') |
||||
|
} |
||||
|
}, [error]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (user && user.authorized) { |
||||
|
dispatch(push('/example/e1')); |
||||
|
} |
||||
|
}, [user]) |
||||
|
|
||||
|
return ( |
||||
|
<div style={{ |
||||
|
height: '100vh', |
||||
|
display: 'flex', |
||||
|
alignItems: 'center', |
||||
|
justifyContent: 'center', |
||||
|
}}> |
||||
|
<div style={{ |
||||
|
width: 400, |
||||
|
height: 410, |
||||
|
padding: 30, |
||||
|
}}> |
||||
|
<p style={{ fontSize: 21, fontWeight: 'bold', textAlign: 'center' }}>飞尚物联</p> |
||||
|
<Form |
||||
|
onSubmit={values => { |
||||
|
dispatch(login(values.username, values.password)) |
||||
|
}} |
||||
|
getFormApi={formApi => form.current = formApi} |
||||
|
> |
||||
|
<Form.Input field='username' label='用户名' /> |
||||
|
<Form.Input field='password' mode="password" autoComplete="" label='密码' /> |
||||
|
<Button htmlType='submit' block theme="solid" >登录</Button> |
||||
|
</Form> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { auth } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
error: auth.error, |
||||
|
isRequesting: auth.isRequesting |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Login); |
@ -0,0 +1,12 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import routes from './routes'; |
||||
|
import reducers from './reducers'; |
||||
|
import actions from './actions'; |
||||
|
|
||||
|
export default { |
||||
|
key: 'auth', |
||||
|
reducers: reducers, |
||||
|
routes: routes, |
||||
|
actions: actions |
||||
|
}; |
@ -0,0 +1,40 @@ |
|||||
|
'use strict'; |
||||
|
import * as actionTypes from '../actions/auth'; |
||||
|
import Immutable from 'immutable'; |
||||
|
|
||||
|
const initState = { |
||||
|
user: {}, |
||||
|
isRequesting: false, |
||||
|
error: null |
||||
|
}; |
||||
|
|
||||
|
function auth(state = initState, action) { |
||||
|
const payload = action.payload; |
||||
|
switch (action.type){ |
||||
|
case actionTypes.INIT_AUTH: |
||||
|
return Immutable.fromJS(state).set('user', payload.user).toJS(); |
||||
|
case actionTypes.REQUEST_LOGIN: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: true, |
||||
|
error: null |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGIN_SUCCESS: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: false, |
||||
|
user: payload.user |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGIN_ERROR: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
isRequesting: false, |
||||
|
error: payload.error |
||||
|
}).toJS(); |
||||
|
case actionTypes.LOGOUT: |
||||
|
return Immutable.fromJS(state).merge({ |
||||
|
user: null |
||||
|
}).toJS(); |
||||
|
default: |
||||
|
return state; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default auth; |
@ -0,0 +1,6 @@ |
|||||
|
'use strict'; |
||||
|
import auth from './auth' |
||||
|
|
||||
|
export default { |
||||
|
auth |
||||
|
}; |
@ -0,0 +1,12 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { Login } from './containers'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'outer', |
||||
|
route: { |
||||
|
key:'signin', |
||||
|
path: "/signin", |
||||
|
component: Login |
||||
|
} |
||||
|
}]; |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { basicAction } from '@peace/utils' |
||||
|
import { ApiTable } from '$utils' |
||||
|
|
||||
|
export function getMembers (orgId) { |
||||
|
return dispatch => basicAction({ |
||||
|
type: 'get', |
||||
|
dispatch: dispatch, |
||||
|
actionType: 'GET_MEMBERS', |
||||
|
url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, |
||||
|
msg: { error: '获取用户列表失败' }, |
||||
|
reducer: { name: 'members' } |
||||
|
}); |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import * as example from './example' |
||||
|
|
||||
|
export default { |
||||
|
...example |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
import React, { useEffect } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Spin, Card } from '@douyinfe/semi-ui'; |
||||
|
import '../style.less' |
||||
|
const { Meta } = Card; |
||||
|
|
||||
|
const Example = (props) => { |
||||
|
const { dispatch, actions, user, loading } = props |
||||
|
|
||||
|
useEffect(() => { |
||||
|
// ACTION 示例 |
||||
|
dispatch(actions.example.getMembers(user.orgId)) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Spin tip="biubiubiu~" spinning={loading}> |
||||
|
<div id='example'> |
||||
|
<p>STYLE EXAMPLE</p> |
||||
|
</div> |
||||
|
<Card |
||||
|
style={{ maxWidth: 300 }} |
||||
|
cover={ |
||||
|
<img |
||||
|
alt="example" |
||||
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/card-cover-docs-demo2.jpeg" |
||||
|
/> |
||||
|
} |
||||
|
> |
||||
|
<Meta title="组件示例" /> |
||||
|
</Card> |
||||
|
</Spin> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps (state) { |
||||
|
const { auth, global, members } = state; |
||||
|
return { |
||||
|
loading: members.isRequesting, |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
members: members.data |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Example); |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import Example from './example'; |
||||
|
|
||||
|
export { Example }; |
@ -0,0 +1,15 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import reducers from './reducers'; |
||||
|
import routes from './routes'; |
||||
|
import actions from './actions'; |
||||
|
import { getNavItem } from './nav-item'; |
||||
|
|
||||
|
export default { |
||||
|
key: 'example', |
||||
|
name: '书写示例', |
||||
|
reducers: reducers, |
||||
|
routes: routes, |
||||
|
actions: actions, |
||||
|
getNavItem: getNavItem |
||||
|
}; |
@ -0,0 +1,15 @@ |
|||||
|
import React from 'react'; |
||||
|
import { IconCode } from '@douyinfe/semi-icons'; |
||||
|
|
||||
|
export function getNavItem (user, dispatch) { |
||||
|
return ( |
||||
|
[ |
||||
|
{ |
||||
|
itemKey: 'example', text: '举个栗子', icon: <IconCode />, |
||||
|
items: [ |
||||
|
{ itemKey: 'e1', to: '/example/e1', text: '举个棒子' }, |
||||
|
] |
||||
|
}, |
||||
|
] |
||||
|
); |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
'use strict'; |
||||
|
import { Example, } from './containers'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'inner', |
||||
|
route: { |
||||
|
path: '/example', |
||||
|
key: 'example', |
||||
|
breadcrumb: '栗子', |
||||
|
// 不设置 component 则面包屑禁止跳转
|
||||
|
childRoutes: [{ |
||||
|
path: '/e1', |
||||
|
key: 'e1', |
||||
|
component: Example, |
||||
|
breadcrumb: '棒子', |
||||
|
}] |
||||
|
} |
||||
|
}]; |
@ -0,0 +1,7 @@ |
|||||
|
#example { |
||||
|
box-shadow: 3px 3px 2px black; |
||||
|
} |
||||
|
|
||||
|
#example:hover { |
||||
|
color: yellowgreen; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export const AuthorizationCode = { |
||||
|
|
||||
|
}; |
@ -0,0 +1,14 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const isAuthorized = (authcode) => { |
||||
|
if (JSON.parse(sessionStorage.getItem('user'))) { |
||||
|
const { resources } = JSON.parse(sessionStorage.getItem('user')); |
||||
|
return resources.includes(authcode); |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
isAuthorized |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
'use strict'; |
||||
|
import { isAuthorized } from './func'; |
||||
|
import { AuthorizationCode } from './authCode'; |
||||
|
import { ApiTable, RouteTable } from './webapi' |
||||
|
|
||||
|
export { |
||||
|
isAuthorized, |
||||
|
|
||||
|
AuthorizationCode, |
||||
|
|
||||
|
ApiTable, |
||||
|
RouteTable, |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
export const ApiTable = { |
||||
|
login: 'login', |
||||
|
logout: 'logout', |
||||
|
|
||||
|
getEnterprisesMembers: 'enterprises/{enterpriseId}/members', |
||||
|
}; |
||||
|
|
||||
|
export const RouteTable = { |
||||
|
apiRoot: '/api/root', |
||||
|
fileUpload: '/_upload/new', |
||||
|
cleanUpUploadTrash: '/_upload/cleanup', |
||||
|
}; |
@ -0,0 +1,91 @@ |
|||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
const path = require('path'); |
||||
|
/*这种以CommonJS的同步形式去引入其它模块的方式代码更加简洁:获取组件*/ |
||||
|
const os = require('os'); |
||||
|
const moment = require('moment'); |
||||
|
const args = require('args'); |
||||
|
const dev = process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'developmentVite'; |
||||
|
const vite = process.env.NODE_ENV == 'developmentVite'; |
||||
|
|
||||
|
dev && console.log('\x1B[33m%s\x1b[0m', '请遵循并及时更新 readme.md,维护良好的开发环境,媛猿有责'); |
||||
|
// // 启动参数
|
||||
|
args.option(['p', 'port'], '启动端口'); |
||||
|
args.option(['u', 'api-url'], 'webapi的URL'); |
||||
|
|
||||
|
const flags = args.parse(process.argv); |
||||
|
|
||||
|
const API_URL = process.env.API_URL || flags.apiUrl; |
||||
|
|
||||
|
if (!API_URL) { |
||||
|
console.log('缺少启动参数,异常退出'); |
||||
|
args.showHelp(); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
const product = { |
||||
|
port: flags.port || 8080, |
||||
|
staticDirs: [path.join(__dirname, './client')], |
||||
|
mws: [{ |
||||
|
entry: require('./middlewares/proxy').entry, |
||||
|
opts: { |
||||
|
host: API_URL, |
||||
|
match: /^\/_api\//, |
||||
|
} |
||||
|
}, { |
||||
|
entry: require('./routes').entry, |
||||
|
opts: { |
||||
|
apiUrl: API_URL, |
||||
|
staticRoot: './client', |
||||
|
} |
||||
|
}, { |
||||
|
entry: require('./client').entry,// 静态信息
|
||||
|
opts: {} |
||||
|
}], |
||||
|
logger: { |
||||
|
level: 'debug', |
||||
|
json: false, |
||||
|
filename: path.join(__dirname, 'log', 'runtime.txt'), |
||||
|
colorize: true, |
||||
|
maxsize: 1024 * 1024 * 5, |
||||
|
rotationFormat: false, |
||||
|
zippedArchive: true, |
||||
|
maxFiles: 10, |
||||
|
prettyPrint: true, |
||||
|
label: '', |
||||
|
timestamp: () => moment().format('YYYY-MM-DD HH:mm:ss.SSS'), |
||||
|
eol: os.EOL, |
||||
|
tailable: true, |
||||
|
depth: null, |
||||
|
showLevel: true, |
||||
|
maxRetries: 1 |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
let config; |
||||
|
if (dev) { |
||||
|
config = { |
||||
|
port: product.port, |
||||
|
staticDirs: product.staticDirs, |
||||
|
mws: product.mws |
||||
|
.concat([ |
||||
|
vite ? |
||||
|
{ |
||||
|
entry: require('./middlewares/vite-dev').entry, |
||||
|
opts: {} |
||||
|
} |
||||
|
: |
||||
|
{ |
||||
|
entry: require('./middlewares/webpack-dev').entry, |
||||
|
opts: {} |
||||
|
} |
||||
|
]) |
||||
|
, |
||||
|
logger: product.logger |
||||
|
} |
||||
|
config.logger.filename = path.join(__dirname, 'log', 'development.txt'); |
||||
|
} else { |
||||
|
config = product; |
||||
|
} |
||||
|
|
||||
|
module.exports = config;//区分开发和发布
|
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
|
||||
|
"compilerOptions": { |
||||
|
"target": "es6", |
||||
|
"module": "commonjs", |
||||
|
"allowSyntheticDefaultImports": true |
||||
|
}, |
||||
|
"exclude": [ |
||||
|
"node_modules", |
||||
|
"bower_components", |
||||
|
"jspm_packages", |
||||
|
"tmp", |
||||
|
"temp" |
||||
|
] |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const proxy = require('koa-proxy'); |
||||
|
const convert = require('koa-convert'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
app.use(convert(proxy({ |
||||
|
host: opts.host, |
||||
|
match: opts.match, |
||||
|
map: function (path) { |
||||
|
return path.replace(opts.match, ''); |
||||
|
} |
||||
|
}))); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,25 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const express = require('express') |
||||
|
const { createServer: createViteServer } = require('vite') |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: async function (app, router, opts) { |
||||
|
const server = express() |
||||
|
|
||||
|
// 以中间件模式创建 Vite 服务器
|
||||
|
// 竟然会自动读 /vite.config.js 的配置
|
||||
|
const vite = await createViteServer({}) |
||||
|
// 将 vite 的 connect 实例作中间件使用
|
||||
|
server.use(vite.middlewares) |
||||
|
|
||||
|
server.use('*', async (req, res) => { |
||||
|
// 如果 `middlewareMode` 是 `'ssr'`,应在此为 `index.html` 提供服务.
|
||||
|
// 如果 `middlewareMode` 是 `'html'`,则此处无需手动服务 `index.html`
|
||||
|
// 因为 Vite 自会接管
|
||||
|
}) |
||||
|
|
||||
|
server.listen(5002) |
||||
|
console.info('vite server.listen 5002'); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,42 @@ |
|||||
|
'use strict'; |
||||
|
const express = require('express') |
||||
|
const webpack = require('webpack'); |
||||
|
const devConfig = require('../webpack.config'); |
||||
|
const middleware = require('webpack-dev-middleware'); |
||||
|
const proxy = require('koa-better-http-proxy'); |
||||
|
const url = require('url'); |
||||
|
|
||||
|
const compiler = webpack(devConfig); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
app.use(proxy('http://localhost:5001', { |
||||
|
filter: function (ctx) { |
||||
|
return /\/build/.test(url.parse(ctx.url).path); |
||||
|
}, |
||||
|
proxyReqPathResolver: function (ctx) { |
||||
|
return 'client' + url.parse(ctx.url).path; |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
app.use(proxy('http://localhost:5001', { |
||||
|
filter: function (ctx) { |
||||
|
return /\/$/.test(url.parse(ctx.url).path); |
||||
|
}, |
||||
|
proxyReqPathResolver: function (ctx) { |
||||
|
return 'client/build/index.html'; |
||||
|
} |
||||
|
})); |
||||
|
|
||||
|
const server = express(); |
||||
|
server.use(middleware(compiler)); |
||||
|
// server.use(require("webpack-hot-middleware")(compiler));
|
||||
|
server.listen('5001', function (err) { |
||||
|
if (err) { |
||||
|
console.error(err); |
||||
|
} else { |
||||
|
console.info(`webpack-dev listen 5001`); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}; |
@ -0,0 +1,73 @@ |
|||||
|
{ |
||||
|
"name": "fs-anxincloud-4.0", |
||||
|
"version": "1.0.0", |
||||
|
"description": "anxincloud-4.0", |
||||
|
"main": "server.js", |
||||
|
"scripts": { |
||||
|
"test": "mocha", |
||||
|
"start-vite": "cross-env NODE_ENV=developmentVite npm run start-params", |
||||
|
"start": "cross-env NODE_ENV=development npm run start-params", |
||||
|
"start-params": "node server -p 5000 -u http://127.0.0.1:4000", |
||||
|
"deploy": "export NODE_ENV=production&& npm run build && node server", |
||||
|
"build-dev": "export NODE_ENV=development&&webpack --config webpack.config.js", |
||||
|
"build": "export NODE_ENV=production&&webpack --config webpack.config.prod.js" |
||||
|
}, |
||||
|
"keywords": [ |
||||
|
"app" |
||||
|
], |
||||
|
"author": "", |
||||
|
"license": "ISC", |
||||
|
"devDependencies": { |
||||
|
"@babel/core": "^7.14.6", |
||||
|
"@babel/plugin-proposal-class-properties": "^7.14.5", |
||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.14.7", |
||||
|
"@babel/plugin-transform-runtime": "^7.14.5", |
||||
|
"@babel/polyfill": "^7.12.1", |
||||
|
"@babel/preset-env": "^7.14.7", |
||||
|
"@babel/preset-react": "^7.14.5", |
||||
|
"babel-loader": "^8.2.2", |
||||
|
"babel-plugin-import": "^1.13.3", |
||||
|
"connected-react-router": "^6.8.0", |
||||
|
"css-loader": "^3.5.0", |
||||
|
"express": "^4.17.1", |
||||
|
"file-loader": "^6.0.0", |
||||
|
"html-webpack-plugin": "^4.5.0", |
||||
|
"immutable": "^4.0.0-rc.12", |
||||
|
"less": "^3.12.2", |
||||
|
"less-loader": "^7.0.2", |
||||
|
"nprogress": "^0.2.0", |
||||
|
"react": "^17.0.0", |
||||
|
"react-dom": "^17.0.0", |
||||
|
"react-redux": "^7.2.1", |
||||
|
"react-router-dom": "^5.2.0", |
||||
|
"react-router-redux": "^4.0.8", |
||||
|
"redux": "^4.0.5", |
||||
|
"redux-thunk": "^2.3.0", |
||||
|
"style-loader": "^2.0.0", |
||||
|
"webpack": "^5.3.2", |
||||
|
"webpack-bundle-analyzer": "^4.1.0", |
||||
|
"webpack-cli": "^4.2.0", |
||||
|
"webpack-dev-middleware": "^4.0.2", |
||||
|
"webpack-hot-middleware": "^2.25.0" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@douyinfe/semi-ui": "^2.8.0", |
||||
|
"@fs/attachment": "^1.0.0", |
||||
|
"@peace/components": "0.0.35", |
||||
|
"@peace/utils": "^0.0.44", |
||||
|
"@vitejs/plugin-react": "^1.3.1", |
||||
|
"@vitejs/plugin-react-refresh": "^1.3.6", |
||||
|
"args": "^5.0.1", |
||||
|
"cross-env": "^7.0.3", |
||||
|
"fs-web-server-scaffold": "^1.0.6", |
||||
|
"koa-better-http-proxy": "^0.2.5", |
||||
|
"koa-proxy": "^1.0.0-alpha.3", |
||||
|
"koa-view": "^2.1.4", |
||||
|
"moment": "^2.22.0", |
||||
|
"npm": "^7.20.6", |
||||
|
"perfect-scrollbar": "^1.5.5", |
||||
|
"superagent": "^6.1.0", |
||||
|
"vite": "^2.9.5", |
||||
|
"webpack-dev-server": "^3.11.2" |
||||
|
} |
||||
|
} |
@ -0,0 +1,214 @@ |
|||||
|
创建时间:2021/08/19 |
||||
|
|
||||
|
## 1. 文档维护: |
||||
|
|
||||
|
- 文档相关内容若有更改,请及时更新文档,以备后来者查询; |
||||
|
|
||||
|
## 2. 项目开发: |
||||
|
|
||||
|
- 请遵循此文档约定的目录结构与约定 |
||||
|
|
||||
|
```js |
||||
|
|-- .babelrc |
||||
|
|-- config.js |
||||
|
|-- Dockerfile |
||||
|
|-- jsconfig.json |
||||
|
|-- package.json |
||||
|
|-- readme.md |
||||
|
|-- server.js |
||||
|
|-- webpack.config.js |
||||
|
|-- webpack.config.prod.js |
||||
|
|-- .vscode |
||||
|
| |-- launch.json |
||||
|
| |-- settings.json |
||||
|
|-- client |
||||
|
| |-- index.ejs |
||||
|
| |-- index.html // 当前 html 文件 |
||||
|
| |-- index.js |
||||
|
| |-- assets // 资源文件 |
||||
|
| | |-- images |
||||
|
| | |-- avatar |
||||
|
| |-- src // 项目代码 |
||||
|
| |-- app.js // 由此开始并加载模块 |
||||
|
| |-- index.js |
||||
|
| |-- components // 公用组件 |
||||
|
| | |-- index.js // 由此导出组件 |
||||
|
| | |-- Upload |
||||
|
| | |-- index.js |
||||
|
| |-- layout // 项目布局以及初始化等操作 |
||||
|
| | |-- index.js |
||||
|
| | |-- actions |
||||
|
| | | |-- global.js |
||||
|
| | |-- components |
||||
|
| | | |-- footer |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- header |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- sider |
||||
|
| | | |-- index.js |
||||
|
| | |-- containers |
||||
|
| | | |-- index.js |
||||
|
| | | |-- layout |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- index.less |
||||
|
| | | |-- no-match |
||||
|
| | | |-- index.js |
||||
|
| | |-- reducers |
||||
|
| | | |-- ajaxResponse.js |
||||
|
| | | |-- global.js // 全局数据,主要包含屏幕可视宽高、所有的 action 等 |
||||
|
| | | |-- index.js |
||||
|
| | |-- store |
||||
|
| | |-- index.js |
||||
|
| | |-- store.dev.js |
||||
|
| | |-- store.prod.js |
||||
|
| |-- sections // 各功能模块 |
||||
|
| | |-- auth // 比较特别的 Auth 模块,目前 action、reducer 依然采用原始写法;包含登录、忘记密码等项目基本功能页面 |
||||
|
| | | |-- index.js |
||||
|
| | | |-- routes.js |
||||
|
| | | |-- actions |
||||
|
| | | | |-- auth.js |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- components |
||||
|
| | | |-- containers |
||||
|
| | | | |-- index.js |
||||
|
| | | | |-- login.js |
||||
|
| | | |-- reducers |
||||
|
| | | | |-- auth.js |
||||
|
| | | | |-- index.js |
||||
|
| | | |-- __tests__ |
||||
|
| | |-- example // 示例模块,一般的功能模块应遵循此结构 |
||||
|
| | |-- index.js // 由此导出该模块信息,应包括一个 key 值,actions 等 |
||||
|
| | |-- nav-item.js // 用于生成菜单项,此文件内可以进行权限判断 |
||||
|
| | |-- routes.js // 路由文件 |
||||
|
| | |-- style.less // 样式文件,若样式并不是非常多,每个模块一个样式文件即可 |
||||
|
| | |-- actions |
||||
|
| | | |-- example.js // 具体的 action 操作 |
||||
|
| | | |-- index.js // 由此导出该项目的 action |
||||
|
| | |-- components // 组件 |
||||
|
| | |-- containers // 容器,此文件夹内应只包括该模块第一层级的页面 |
||||
|
| | | |-- example.js |
||||
|
| | | |-- index.js |
||||
|
| | |-- reducers // 若采用封装后的 action 写法,则 reducer 可不写 |
||||
|
| | |-- index.js |
||||
|
| |-- utils // |
||||
|
| |-- authCode.js |
||||
|
| |-- func.js // 常用函数 |
||||
|
| |-- index.js |
||||
|
| |-- webapi.js // api 路由 |
||||
|
|-- log |
||||
|
|-- middlewares |
||||
|
| |-- proxy.js |
||||
|
| |-- webpack-dev.js |
||||
|
|-- routes |
||||
|
| |-- index.js |
||||
|
| |-- attachment |
||||
|
|-- typings |
||||
|
|-- node |
||||
|
| |-- node.d.ts |
||||
|
|-- react |
||||
|
|-- react.d.ts |
||||
|
``` |
||||
|
|
||||
|
- 封装后一般 action 写法: |
||||
|
|
||||
|
`@peace/utils 的 actionHelp 中有详细注释` |
||||
|
|
||||
|
``` js |
||||
|
'use strict'; |
||||
|
|
||||
|
import { basicAction } from '@peace/utils' |
||||
|
import { ApiTable } from '$utils' |
||||
|
|
||||
|
export function getMembers(orgId) { |
||||
|
return dispatch => basicAction({ |
||||
|
type: 'get', |
||||
|
dispatch: dispatch, |
||||
|
actionType: 'GET_MEMBERS', |
||||
|
url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, |
||||
|
msg: { error: '获取用户列表失败' }, |
||||
|
reducer: { name: 'members' } |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
1. 若 type=post,则可以使用 data 属性发送对象格式数据; |
||||
|
|
||||
|
2. reducer.name 会作为该 action 对应的 reducer 的名字,从 state 里可以解构此变量,获得该 action 异步或其他操作获得的数据; |
||||
|
|
||||
|
3. msg 可以发送 `{ option:'获取用户列表' }` ,则 actionHelp 会自动将其处理为失败和成功两种情况; |
||||
|
|
||||
|
若单独写 success 或 error 的 key,则只在成功或失败的时候进行提示; |
||||
|
|
||||
|
4. 后续可以优化:type=get 时候, |
||||
|
|
||||
|
使用 query 属性将数据传递,在 @peace/utils 的 actionHelp 中将其添加到路由后面;eg. `{ enterpriseId: orgId }` |
||||
|
|
||||
|
使用 replace 属性传递对象数据,对象数据中将被替换的值为key,替换的值为 value,然后再 actionHelp 中更改路由;eg. `{ "{enterpriseId}": orgId}` |
||||
|
|
||||
|
5. 最终取得的 reducer 中的数据格式一般为: |
||||
|
``` js |
||||
|
{ |
||||
|
data: xxx, // 接口返回的数据格式 |
||||
|
isRequesting: false, // 请求状态 |
||||
|
success: true, // 以此判断请求是否成功,不用再以 payload.type 判断 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- actions 的引用 |
||||
|
|
||||
|
从 reducer 的 state.global.actions 里引用具体 action |
||||
|
|
||||
|
```js |
||||
|
const Example = (props) => { |
||||
|
const { dispatch, actions, user, loading } = props |
||||
|
|
||||
|
useEffect(() => { |
||||
|
dispatch(actions.example.getMembers(user.orgId)) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Spin tip="biubiubiu~" spinning={loading}> |
||||
|
example |
||||
|
</Spin> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global, members } = state; |
||||
|
return { |
||||
|
loading: members.isRequesting, |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
members: members.data |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(Example); |
||||
|
``` |
||||
|
|
||||
|
- 一般路由配置 |
||||
|
```js |
||||
|
'use strict'; |
||||
|
import { Example, } from './containers'; |
||||
|
|
||||
|
export default [{ |
||||
|
type: 'inner', // 是否在layout 内,如果为outer,则看不到 header、footer、sider等布局,比如登陆页面 |
||||
|
route: { |
||||
|
path: '/example', |
||||
|
key: 'example', |
||||
|
breadcrumb: '栗子', |
||||
|
// 不设置 component 则面包屑禁止跳转 |
||||
|
childRoutes: [{ |
||||
|
path: '/e1', // 自路由不必复写父路由内容,会自动拼接; 则此处组件的实际路由为 /example/e1 |
||||
|
key: 'e1', |
||||
|
component: Example, |
||||
|
breadcrumb: '棒子', |
||||
|
}] |
||||
|
} |
||||
|
}]; |
||||
|
``` |
||||
|
- cross-env 的使用限制 |
||||
|
|
||||
|
cross-env 可以统一不同操作系统下环境变量的导出方式,不用再在 windows 下写 set;linux 下写 export; 可以统一以 cross-env NODE_ENV=DEV 代替; |
||||
|
|
||||
|
但是这样的话就不能在同一条运行的命令中使用 && 切割,因为会把命令切割为两个环境,则最终拿不到我们设置的变量; |
@ -0,0 +1,92 @@ |
|||||
|
'use strict'; |
||||
|
const request = require('superagent'); |
||||
|
const parse = require('async-busboy'); |
||||
|
const path = require('path') |
||||
|
const fs = require('fs'); |
||||
|
|
||||
|
const ext = { |
||||
|
project: [".txt", ".dwg", ".doc", ".docx", ".xls", ".xlsx", ".pdf", ".png", ".jpg", ".svg"], |
||||
|
report: [".doc", ".docx", ".xls", ".xlsx", ".pdf"], |
||||
|
data: [".txt", ".xls", ".xlsx"], |
||||
|
image: [".png", ".jpg", ".svg"], |
||||
|
three: [".js"], |
||||
|
video: [".mp4"], |
||||
|
bpmn: [".bpmn", ".bpmn20.xml", ".zip", ".bar"], |
||||
|
app: [".apk"] |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
|
||||
|
const getApiRoot = async function (ctx) { |
||||
|
const { apiUrl } = opts; |
||||
|
|
||||
|
ctx.status = 200; |
||||
|
ctx.body = { root: apiUrl }; |
||||
|
}; |
||||
|
|
||||
|
let upload = async function (ctx, next) { |
||||
|
try { |
||||
|
const { files } = await parse(ctx.req); |
||||
|
const file = files[0]; |
||||
|
const extname = path.extname(file.filename).toLowerCase(); |
||||
|
const fileType = ctx.query.type || "image"; |
||||
|
const fileFolder = ctx.query.fileFolder || 'common'; |
||||
|
if (ext[fileType].indexOf(extname) < 0) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ name: 'UploadFailed', message: '文件格式无效' }); |
||||
|
return; |
||||
|
} |
||||
|
const date = new Date().toLocaleDateString(); |
||||
|
const time = new Date().getTime(); |
||||
|
let fileName = time + '_' + file.filename; |
||||
|
let saveFile = path.join(__dirname, '../../', `/client/assets/files/${fileFolder}`, fileName); |
||||
|
const pathUrl = `./client/assets/files/${fileFolder}`; |
||||
|
|
||||
|
const res1 = fs.existsSync(`./client/assets/files/${fileFolder}`); |
||||
|
!res1 && fs.mkdirSync(`./client/assets/files/${fileFolder}`); |
||||
|
const res = fs.existsSync(pathUrl); |
||||
|
!res && fs.mkdirSync(pathUrl); |
||||
|
let stream = fs.createWriteStream(saveFile); |
||||
|
fs.createReadStream(file.path).pipe(stream); |
||||
|
stream.on('error', function (err) { |
||||
|
app.fs.logger.log('error', '[Upload Heatmap]', err); |
||||
|
}); |
||||
|
ctx.status = 200; |
||||
|
ctx.body = { filename: path.join(`/assets/files/${fileFolder}`, fileName), name: 'UploadSuccess', message: '上传成功' }; |
||||
|
} catch (err) { |
||||
|
ctx.status = 500; |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.body = { err: 'upload error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let remove = async function (ctx, next) { |
||||
|
try { |
||||
|
const fkeys = ctx.request.body; |
||||
|
let removeUrl = path.join(__dirname, '../../', './client', fkeys.url); |
||||
|
const res = fs.existsSync(removeUrl); |
||||
|
if (!res) { |
||||
|
ctx.status = 400; |
||||
|
ctx.body = JSON.stringify({ name: 'DeleteFailed', message: '文件地址不存在' }); |
||||
|
return; |
||||
|
} |
||||
|
fs.unlink(removeUrl, function (error) { |
||||
|
if (error) { |
||||
|
console.log(error); |
||||
|
} |
||||
|
}) |
||||
|
ctx.status = 200; |
||||
|
ctx.body = { name: 'DeleteSuccess.', message: '删除成功' }; |
||||
|
} catch (err) { |
||||
|
ctx.status = 500; |
||||
|
ctx.fs.logger.error(err); |
||||
|
ctx.body = { err: 'upload cleanup error.' }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
router.get('/api/root', getApiRoot); |
||||
|
router.post('/_upload/new', upload); |
||||
|
router.delete('/_upload/cleanup', remove); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Created by liu.xinyi |
||||
|
* on 2016/7/7. |
||||
|
*/ |
||||
|
'use strict'; |
||||
|
const path = require('path'); |
||||
|
const fs = require('fs'); |
||||
|
|
||||
|
module.exports = { |
||||
|
entry: function (app, router, opts) { |
||||
|
fs.readdirSync(__dirname).forEach(function (dir) { |
||||
|
if(fs.lstatSync(path.join(__dirname, dir)).isDirectory()){ |
||||
|
fs.readdirSync(path.join(__dirname, dir)).forEach(function (api) { |
||||
|
require(`./${dir}/${api}`).entry(app, router, opts); |
||||
|
app.fs.logger.log('info', '[Router]', 'Inject api:', dir + '/' + path.basename(api, '.js')); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,8 @@ |
|||||
|
'use strict'; |
||||
|
/*jslint node:true*/ |
||||
|
//from koa
|
||||
|
|
||||
|
const scaffold = require('fs-web-server-scaffold'); |
||||
|
const config = require('./config.js'); |
||||
|
|
||||
|
module.exports = scaffold(config); |
@ -0,0 +1,30 @@ |
|||||
|
import path from 'path'; |
||||
|
import { defineConfig } from 'vite' |
||||
|
import react from '@vitejs/plugin-react' |
||||
|
import reactRefresh from '@vitejs/plugin-react-refresh' |
||||
|
|
||||
|
// https://vitejs.dev/config/
|
||||
|
export default defineConfig({ |
||||
|
root: './client/', |
||||
|
plugins: [react({})], |
||||
|
// plugins: [reactRefresh({})],
|
||||
|
resolve: { |
||||
|
alias: [ |
||||
|
{ |
||||
|
find: '$utils', replacement: path.join('/src/utils'), |
||||
|
}, |
||||
|
// 针对以 ~/[包名称]开头的,替换为 node_modules/@[包名称]
|
||||
|
{ |
||||
|
find: /^(~)(?!\/)(.+)/, replacement: path.join('node_modules/$2'), |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
cors: true, |
||||
|
server: { |
||||
|
hmr: { |
||||
|
protocol: 'ws', |
||||
|
host: 'localhost' |
||||
|
}, |
||||
|
middlewareMode: 'html', |
||||
|
} |
||||
|
}) |
@ -0,0 +1,67 @@ |
|||||
|
const path = require('path'); |
||||
|
const webpack = require('webpack'); |
||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; |
||||
|
|
||||
|
const PATHS = { |
||||
|
app: path.join(__dirname, 'client/src'), |
||||
|
build: path.join(__dirname, 'client/build') |
||||
|
}; |
||||
|
|
||||
|
module.exports = { |
||||
|
mode: "development", |
||||
|
devtool: 'source-map', |
||||
|
devServer: { |
||||
|
historyApiFallback: true, |
||||
|
}, |
||||
|
entry: { |
||||
|
app: ["@babel/polyfill", PATHS.app] |
||||
|
}, |
||||
|
output: { |
||||
|
publicPath: '/client/build/', |
||||
|
path: PATHS.build, |
||||
|
filename: '[name].js' |
||||
|
}, |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
||||
|
extensions: ['.js', '.jsx'], |
||||
|
alias: { |
||||
|
crypto: false, |
||||
|
$utils: path.resolve(__dirname, 'client/src/utils/'), |
||||
|
$components: path.resolve(__dirname, 'client/src/components/'), |
||||
|
} |
||||
|
}, |
||||
|
plugins: [ |
||||
|
new webpack.HotModuleReplacementPlugin(), |
||||
|
new BundleAnalyzerPlugin(), |
||||
|
], |
||||
|
module: { |
||||
|
rules: [{ |
||||
|
test: /\.css$/, |
||||
|
use: ['style-loader', { |
||||
|
loader: 'css-loader', |
||||
|
options: { |
||||
|
modules: true |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.less$/, |
||||
|
use: ['style-loader', 'css-loader', { |
||||
|
loader: 'less-loader', |
||||
|
options: { |
||||
|
lessOptions: { |
||||
|
javascriptEnabled: true |
||||
|
} |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.(js|jsx)$/, |
||||
|
use: 'babel-loader', |
||||
|
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
||||
|
}, { |
||||
|
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
||||
|
loader: "file-loader" |
||||
|
}] |
||||
|
} |
||||
|
}; |
@ -0,0 +1,76 @@ |
|||||
|
var path = require('path'); |
||||
|
var webpack = require('webpack'); |
||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin'); |
||||
|
|
||||
|
const PATHS = { |
||||
|
app: path.join(__dirname, 'client/src'), |
||||
|
build: path.join(__dirname, 'client/build') |
||||
|
}; |
||||
|
|
||||
|
module.exports = { |
||||
|
mode: "production", |
||||
|
entry: { |
||||
|
app: ["babel-polyfill", PATHS.app] |
||||
|
}, |
||||
|
output: { |
||||
|
path: PATHS.build, |
||||
|
publicPath: '/build', |
||||
|
filename: '[name].[hash:5].js' |
||||
|
}, |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
||||
|
extensions: ['.js', '.jsx'], |
||||
|
alias: { |
||||
|
crypto: false, |
||||
|
$utils: path.resolve(__dirname, 'client/src/utils/'), |
||||
|
$components: path.resolve(__dirname, 'client/src/components/'), |
||||
|
} |
||||
|
}, |
||||
|
plugins: [ |
||||
|
new HtmlWebpackPlugin({ |
||||
|
filename: '../index.html', |
||||
|
template: './client/index.ejs' |
||||
|
}) |
||||
|
], |
||||
|
optimization: { |
||||
|
splitChunks: { |
||||
|
cacheGroups: { |
||||
|
commons: { |
||||
|
test: /[\\/]node_modules[\\/]/, |
||||
|
name: "vendors", |
||||
|
chunks: "all" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
module: { |
||||
|
rules: [{ |
||||
|
test: /\.css$/, |
||||
|
use: ['style-loader', { |
||||
|
loader: 'css-loader', |
||||
|
options: { |
||||
|
modules: true |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.less$/, |
||||
|
use: ['style-loader', 'css-loader', |
||||
|
{ |
||||
|
loader: 'less-loader', options: { |
||||
|
lessOptions: { |
||||
|
javascriptEnabled: true |
||||
|
} |
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.jsx?$/, |
||||
|
use: 'babel-loader', |
||||
|
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
||||
|
},{ |
||||
|
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
||||
|
loader: "file-loader" |
||||
|
}] |
||||
|
} |
||||
|
}; |