1
0
mirror of https://github.com/chillzhuang/Sword synced 2024-11-22 02:09:26 +08:00

🎉 升级为多租户系统

This commit is contained in:
smallchill 2019-03-18 14:32:06 +08:00
parent 39baec4b69
commit 0c3b807ea4
41 changed files with 1001 additions and 176 deletions

View File

@ -1,12 +1,15 @@
## 简介
Sword 是 [SpringBlade](https://gitee.com/smallc/SpringBlade)前端UI项目基于react 、ant design、dva用于快速构建系统中后台业务。
## 文档
* 文档地址:[Sword开发手册](https://www.kancloud.cn/smallchill/sword)
## 官网
* 官网地址:[https://bladex.vip](https://bladex.vip)
## 在线演示
* Sword演示地址[https://sword.bladex.vip](https://sword.bladex.vip)
* Saber演示地址[https://saber.avue.top](https://saber.avue.top)
* Saber演示地址[https://saber.bladex.vip](https://saber.bladex.vip)
## 后端项目地址
* [Gitee地址](https://gitee.com/smallc/SpringBlade)

View File

@ -15,7 +15,7 @@ export default [
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
authority: ['administrator', 'admin', 'user', 'test'],
routes: [
// dashboard
{ path: '/', redirect: '/dashboard/workplace' },
@ -156,6 +156,16 @@ export default [
{ path: '/system/param/view/:id', component: './System/Param/ParamView' },
],
},
{
path: '/system/tenant',
routes: [
{ path: '/system/tenant', redirect: '/system/tenant/list' },
{ path: '/system/tenant/list', component: './System/Tenant/Tenant' },
{ path: '/system/tenant/add', component: './System/Tenant/TenantAdd' },
{ path: '/system/tenant/edit/:id', component: './System/Tenant/TenantEdit' },
{ path: '/system/tenant/view/:id', component: './System/Tenant/TenantView' },
],
},
],
},
{

View File

@ -6,18 +6,21 @@ function getFakeList(req, res) {
data.push({
id: '1',
deptName: '刀锋科技',
tenantCode: '000000',
fullName: '江苏刀锋科技有限公司',
sort: '1',
children: [
{
id: '2',
deptName: '常州刀锋',
tenantCode: '000000',
fullName: '常州刀锋科技有限公司',
sort: '1',
},
{
id: '3',
deptName: '南京刀锋',
tenantCode: '000000',
fullName: '南京刀锋科技有限公司',
sort: '2',
},
@ -33,6 +36,7 @@ function getFakeDetail(req, res) {
id: 2,
parentId: 1,
parentName: '江苏刀锋',
tenantCode: '000000',
deptName: '常州刀锋',
fullName: '常州刀锋科技有限公司',
sort: 1,

View File

@ -54,6 +54,11 @@ function getFakeRoutes(req, res) {
code: 'param',
name: '参数管理',
},
{
path: '/system/tenant',
code: 'tenant',
name: '租户管理',
},
],
},
{
@ -397,6 +402,43 @@ function getFakeButtons(req, res) {
},
],
},
{
code: 'tenant',
children: [
{
code: 'tenant_add',
name: '新增',
path: '/system/tenant/add',
source: 'plus',
action: 1,
alias: 'add',
},
{
code: 'tenant_edit',
name: '修改',
path: '/system/tenant/edit',
source: 'form',
action: 2,
alias: 'edit',
},
{
code: 'tenant_delete',
name: '删除',
path: '/api/blade-system/tenant/remove',
source: 'delete',
action: 3,
alias: 'delete',
},
{
code: 'tenant_view',
name: '查看',
path: '/system/tenant/view',
source: 'file-text',
action: 2,
alias: 'view',
},
],
},
{
code: 'log_usual',
children: [

View File

@ -15,105 +15,105 @@ const proxy = {
title: '博客标题1',
categoryName: '批转通知',
content: '博客内容1',
date: '2018-05-08 12:00:00',
releaseTime: '2018-05-08 12:00:00',
},
{
id: '2',
title: '博客标题2',
categoryName: '发布通知',
content: '博客内容2',
date: '2018-06-08 12:00:00',
releaseTime: '2018-06-08 12:00:00',
},
{
id: '3',
title: '博客标题3',
categoryName: '任免通知',
content: '博客内容3',
date: '2018-07-08 12:00:00',
releaseTime: '2018-07-08 12:00:00',
},
{
id: '4',
title: '博客标题4',
categoryName: '指示通知',
content: '博客内容4',
date: '2018-08-08 12:00:00',
releaseTime: '2018-08-08 12:00:00',
},
{
id: '5',
title: '博客标题5',
categoryName: '转发通知',
content: '博客内容5',
date: '2018-09-08 12:00:00',
releaseTime: '2018-09-08 12:00:00',
},
{
id: '6',
title: '博客标题1',
categoryName: '批转通知',
content: '博客内容1',
date: '2018-05-08 12:00:00',
releaseTime: '2018-05-08 12:00:00',
},
{
id: '7',
title: '博客标题2',
categoryName: '发布通知',
content: '博客内容2',
date: '2018-06-08 12:00:00',
releaseTime: '2018-06-08 12:00:00',
},
{
id: '8',
title: '博客标题3',
categoryName: '任免通知',
content: '博客内容3',
date: '2018-07-08 12:00:00',
releaseTime: '2018-07-08 12:00:00',
},
{
id: '9',
title: '博客标题4',
categoryName: '指示通知',
content: '博客内容4',
date: '2018-08-08 12:00:00',
releaseTime: '2018-08-08 12:00:00',
},
{
id: '10',
title: '博客标题5',
categoryName: '转发通知',
content: '博客内容5',
date: '2018-09-08 12:00:00',
releaseTime: '2018-09-08 12:00:00',
},
{
id: '11',
title: '博客标题1',
categoryName: '批转通知',
content: '博客内容1',
date: '2018-05-08 12:00:00',
releaseTime: '2018-05-08 12:00:00',
},
{
id: '12',
title: '博客标题2',
categoryName: '发布通知',
content: '博客内容2',
date: '2018-06-08 12:00:00',
releaseTime: '2018-06-08 12:00:00',
},
{
id: '13',
title: '博客标题3',
categoryName: '任免通知',
content: '博客内容3',
date: '2018-07-08 12:00:00',
releaseTime: '2018-07-08 12:00:00',
},
{
id: '14',
title: '博客标题4',
categoryName: '指示通知',
content: '博客内容4',
date: '2018-08-08 12:00:00',
releaseTime: '2018-08-08 12:00:00',
},
{
id: '15',
title: '博客标题5',
categoryName: '转发通知',
content: '博客内容5',
date: '2018-09-08 12:00:00',
releaseTime: '2018-09-08 12:00:00',
},
],
},
@ -138,7 +138,7 @@ const proxy = {
title: '通知标题详情',
category: '3',
categoryName: '转发通知',
date: '2018-12-31 23:33:33',
releaseTime: '2018-12-31 23:33:33',
content: '通知公告内容详情',
},
message: 'success',

View File

@ -7,12 +7,14 @@ function getFakeList(req, res) {
{
id: '1',
roleName: '超级管理员',
tenantCode: '000000',
roleAlias: 'administrator',
sort: '1',
children: [
{
id: '2',
roleName: '管理员',
tenantCode: '000001',
roleAlias: 'admin',
sort: '1',
},
@ -21,18 +23,21 @@ function getFakeList(req, res) {
{
id: '3',
roleName: '用户',
tenantCode: '000002',
roleAlias: 'user',
sort: '2',
children: [
{
id: '4',
roleName: '普通用户',
tenantCode: '000003',
roleAlias: 'user',
sort: '1',
},
{
id: '5',
roleName: '访客',
tenantCode: '000004',
roleAlias: 'guest',
sort: '2',
},
@ -49,6 +54,7 @@ function getFakeDetail(req, res) {
id: 2,
parentId: 1,
parentName: '超级管理员',
tenantCode: '000000',
roleName: '用户',
roleAlias: 'user',
sort: 1,

75
mock/tenant.js Normal file
View File

@ -0,0 +1,75 @@
import { delay } from 'roadhog-api-doc';
function getFakeList(req, res) {
const json = { code: 200, success: true, msg: '操作成功' };
const list = [];
list.push(
{
id: '1',
tenantCode: '000000',
tenantName: '管理组',
linkman: 'Chill',
contactNumber: '66666666666',
address: '管理组地址',
},
{
id: '2',
tenantCode: '000001',
tenantName: '用户组',
linkman: 'Bill',
contactNumber: '23333333333',
address: '用户组地址',
}
);
json.data = {
total: 10,
size: 10,
current: 2,
searchCount: true,
pages: 1,
records: list,
};
return res.json(json);
}
function getFakeDetail(req, res) {
const json = { code: 200, success: true, msg: '操作成功' };
json.data = {
id: '1',
tenantCode: '000000',
tenantName: '管理组',
linkman: 'Chill',
contactNumber: '66666666666',
address: '管理组地址',
};
return res.json(json);
}
function fakeSuccess(req, res) {
const json = { code: 200, success: true, msg: '操作成功' };
return res.json(json);
}
function getFakeTenantSelect(req, res) {
const json = { code: 200, success: true, msg: '操作成功' };
json.data = [
{
tenantCode: '000000',
tenantName: '管理组',
},
{
tenantCode: '000001',
tenantName: '用户组',
},
];
return res.json(json);
}
const proxy = {
'GET /api/blade-system/tenant/list': getFakeList,
'GET /api/blade-system/tenant/select': getFakeTenantSelect,
'GET /api/blade-system/tenant/detail': getFakeDetail,
'POST /api/blade-system/tenant/submit': fakeSuccess,
'POST /api/blade-system/tenant/remove': fakeSuccess,
};
export default delay(proxy, 500);

View File

@ -6,6 +6,7 @@ function getFakeList(req, res) {
list.push(
{
id: '1',
tenantCode: '000000',
account: 'admin',
name: '超级管理员',
realName: '管理员',
@ -17,6 +18,7 @@ function getFakeList(req, res) {
},
{
id: '2',
tenantCode: '000001',
account: 'user',
name: '系统用户',
realName: '用户',
@ -40,8 +42,9 @@ function getFakeList(req, res) {
function getFakeDetail(req, res) {
const json = { code: 200, success: true, msg: '操作成功' };
const detail = {
json.data = {
id: '1',
tenantCode: '000000',
account: 'admin',
name: '超级管理员',
realName: '管理员',
@ -56,7 +59,6 @@ function getFakeDetail(req, res) {
birthday: '2018-12-31 23:33:33',
statusName: '启用',
};
json.data = detail;
return res.json(json);
}

36
src/actions/tenant.js Normal file
View File

@ -0,0 +1,36 @@
export const TENANT_NAMESPACE = 'tenant';
export function TENANT_LIST(payload) {
return {
type: `${TENANT_NAMESPACE}/fetchList`,
payload,
};
}
export function TENANT_DETAIL(id) {
return {
type: `${TENANT_NAMESPACE}/fetchDetail`,
payload: { id },
};
}
export function TENANT_CLEAR_DETAIL() {
return {
type: `${TENANT_NAMESPACE}/clearDetail`,
payload: {},
};
}
export function TENANT_SUBMIT(payload) {
return {
type: `${TENANT_NAMESPACE}/submit`,
payload,
};
}
export function TENANT_REMOVE(payload) {
return {
type: `${TENANT_NAMESPACE}/remove`,
payload,
};
}

View File

@ -14,6 +14,13 @@ export function USER_INIT() {
};
}
export function USER_CHANGE_INIT(payload) {
return {
type: `${USER_NAMESPACE}/fetchChangeInit`,
payload,
};
}
export function USER_DETAIL(id) {
return {
type: `${USER_NAMESPACE}/fetchDetail`,

View File

@ -12,6 +12,7 @@ export interface ILoginProps {
export default class Login extends React.Component<ILoginProps, any> {
public static Tab: typeof LoginTab;
public static TenantCode: typeof LoginItem;
public static UserName: typeof LoginItem;
public static Password: typeof LoginItem;
public static Mobile: typeof LoginItem;

View File

@ -3,6 +3,20 @@ import { Icon } from 'antd';
import styles from './index.less';
export default {
TenantCode: {
props: {
size: 'large',
id: 'tenantCode',
prefix: <Icon type="home" className={styles.prefixIcon} />,
placeholder: 'admin',
},
rules: [
{
required: true,
message: 'Please enter tenantcode!',
},
],
},
UserName: {
props: {
size: 'large',

View File

@ -30,10 +30,10 @@ export default class ToolBar extends PureComponent {
</Button>
))}
{renderLeftButton ? renderLeftButton() : null}
{renderRightButton ? (
<div style={{ float: 'right', marginRight: '20px' }}>{renderRightButton()}</div>
) : null}
</div>
{renderRightButton ? (
<div style={{ float: 'right', marginRight: '20px' }}>renderRightButton()</div>
) : null}
</div>
);
}

View File

@ -51,10 +51,12 @@ const UserLayout = ({ children }) => (
<div className={styles.header}>
<Link to="/">
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
<span className={styles.title}>Sword 企业级开发平台</span>
</Link>
</div>
<div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
<div className={styles.desc}>
Sword是SpringBlade前端UI项目基于react ant designumidva等流行技术栈
</div>
</div>
{children}
</div>

View File

@ -1,4 +1,5 @@
export default {
'app.login.tenantCode': 'tenantCode',
'app.login.userName': 'userName',
'app.login.password': 'password',
'app.login.message-invalid-credentials': 'Invalid username or passwordadmin/ant.design',
@ -20,6 +21,7 @@ export default {
'app.register-result.view-mailbox': 'View mailbox',
'validation.email.required': 'Please enter your email!',
'validation.email.wrong-format': 'The email address is in the wrong format!',
'validation.userName.tenantCode': 'Please enter your tenantCode!',
'validation.userName.required': 'Please enter your userName!',
'validation.password.required': 'Please enter your password!',
'validation.password.twice': 'The passwords entered twice do not match!',

View File

@ -13,6 +13,7 @@ export default {
'menu.system.menu': 'menu',
'menu.system.role': 'role',
'menu.system.param': 'parameter',
'menu.system.tenant': 'tenant',
'menu.monitor': 'monitor',
'menu.monitor.log': 'log',
'menu.monitor.log.log_usual': 'usual log',

View File

@ -1,4 +1,5 @@
export default {
'app.login.tenantCode': '租户编号',
'app.login.userName': '用户名',
'app.login.password': '密码',
'app.login.message-invalid-credentials': '账户或密码错误admin/ant.design',
@ -20,6 +21,7 @@ export default {
'app.register-result.view-mailbox': '查看邮箱',
'validation.email.required': '请输入邮箱地址!',
'validation.email.wrong-format': '邮箱地址格式错误!',
'validation.tenantCode.required': '请输入租户编号!',
'validation.userName.required': '请输入用户名!',
'validation.password.required': '请输入密码!',
'validation.password.twice': '两次输入的密码不匹配!',

View File

@ -13,6 +13,7 @@ export default {
'menu.system.menu': '菜单管理',
'menu.system.role': '角色管理',
'menu.system.param': '参数管理',
'menu.system.tenant': '租户管理',
'menu.monitor': '系统监控',
'menu.monitor.log': '日志管理',
'menu.monitor.log.log_usual': '通用日志',

View File

@ -1,4 +1,5 @@
export default {
'app.login.tenantCode': '租戶編號',
'app.login.userName': '賬戶',
'app.login.password': '密碼',
'app.login.message-invalid-credentials': '賬戶或密碼錯誤admin/ant.design',
@ -20,6 +21,7 @@ export default {
'app.register-result.view-mailbox': '查看郵箱',
'validation.email.required': '請輸入郵箱地址!',
'validation.email.wrong-format': '郵箱地址格式錯誤!',
'validation.tenantCode.required': '請輸入租戶編號!',
'validation.userName.required': '請輸入賬戶!',
'validation.password.required': '請輸入密碼!',
'validation.password.twice': '兩次輸入的密碼不匹配!',

View File

@ -13,6 +13,7 @@ export default {
'menu.system.menu': '菜單管理',
'menu.system.role': '角色管理',
'menu.system.param': '參數管理',
'menu.system.tenant': '租戶管理',
'menu.monitor': '系統監控',
'menu.monitor.log': '日志管理',
'menu.monitor.log.log_usual': '通用日志',

View File

@ -18,6 +18,94 @@ import { getRoutes, setRoutes, getButtons, setButtons } from '../utils/authority
import { MENU_NAMESPACE } from '../actions/menu';
import { formatRoutes, formatButtons } from '../utils/utils';
const { check } = Authorized;
// Conversion router to menu.
function formatter(data, parentAuthority, parentName) {
return data
.map(item => {
if (!item.name || !item.path) {
return null;
}
let locale = 'menu';
if (parentName) {
locale = `${parentName}.${item.name}`;
} else {
locale = `menu.${item.name}`;
}
// if enableMenuLocale use item.name,
// close menu international
const name = menu.disableLocal
? item.name
: formatMessage({ id: locale, defaultMessage: item.name });
const result = {
...item,
name,
locale,
authority: item.authority || parentAuthority,
};
if (item.routes) {
const children = formatter(item.routes, item.authority, locale);
// Reduce memory usage
result.children = children;
}
delete result.routes;
return result;
})
.filter(item => item);
}
const memoizeOneFormatter = memoizeOne(formatter, isEqual);
/**
* get SubMenu or Item
*/
const getSubMenu = item => {
// doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
return {
...item,
children: filterMenuData(item.children), // eslint-disable-line
};
}
return item;
};
/**
* filter menuData
*/
const filterMenuData = menuData => {
if (!menuData) {
return [];
}
return menuData
.filter(item => item.name && !item.hideInMenu)
.map(item => check(item.authority, getSubMenu(item)))
.filter(item => item);
};
/**
* 获取面包屑映射
* @param {Object} menuData 菜单配置
*/
const getBreadcrumbNameMap = menuData => {
const routerMap = {};
const flattenMenuData = data => {
data.forEach(menuItem => {
if (menuItem.children) {
flattenMenuData(menuItem.children);
}
// Reduce memory usage
routerMap[menuItem.path] = menuItem;
});
};
flattenMenuData(menuData);
return routerMap;
};
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
export default {
namespace: MENU_NAMESPACE,
@ -165,91 +253,3 @@ export default {
},
},
};
const { check } = Authorized;
// Conversion router to menu.
function formatter(data, parentAuthority, parentName) {
return data
.map(item => {
if (!item.name || !item.path) {
return null;
}
let locale = 'menu';
if (parentName) {
locale = `${parentName}.${item.name}`;
} else {
locale = `menu.${item.name}`;
}
// if enableMenuLocale use item.name,
// close menu international
const name = menu.disableLocal
? item.name
: formatMessage({ id: locale, defaultMessage: item.name });
const result = {
...item,
name,
locale,
authority: item.authority || parentAuthority,
};
if (item.routes) {
const children = formatter(item.routes, item.authority, locale);
// Reduce memory usage
result.children = children;
}
delete result.routes;
return result;
})
.filter(item => item);
}
const memoizeOneFormatter = memoizeOne(formatter, isEqual);
/**
* get SubMenu or Item
*/
const getSubMenu = item => {
// doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
return {
...item,
children: filterMenuData(item.children), // eslint-disable-line
};
}
return item;
};
/**
* filter menuData
*/
const filterMenuData = menuData => {
if (!menuData) {
return [];
}
return menuData
.filter(item => item.name && !item.hideInMenu)
.map(item => check(item.authority, getSubMenu(item)))
.filter(item => item);
};
/**
* 获取面包屑映射
* @param {Object} menuData 菜单配置
*/
const getBreadcrumbNameMap = menuData => {
const routerMap = {};
const flattenMenuData = data => {
data.forEach(menuItem => {
if (menuItem.children) {
flattenMenuData(menuItem.children);
}
// Reduce memory usage
routerMap[menuItem.path] = menuItem;
});
};
flattenMenuData(menuData);
return routerMap;
};
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);

87
src/models/tenant.js Normal file
View File

@ -0,0 +1,87 @@
import { message } from 'antd';
import router from 'umi/router';
import { TENANT_NAMESPACE } from '../actions/tenant';
import { list, submit, detail, remove } from '../services/tenant';
export default {
namespace: TENANT_NAMESPACE,
state: {
data: {
list: [],
pagination: false,
},
detail: {},
},
effects: {
*fetchList({ payload }, { call, put }) {
const response = yield call(list, payload);
if (response.success) {
yield put({
type: 'saveList',
payload: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
}
},
*fetchDetail({ payload }, { call, put }) {
const response = yield call(detail, payload);
if (response.success) {
yield put({
type: 'saveDetail',
payload: {
detail: response.data,
},
});
}
},
*clearDetail({ payload }, { put }) {
yield put({
type: 'removeDetail',
payload: { payload },
});
},
*submit({ payload }, { call }) {
const response = yield call(submit, payload);
if (response.success) {
message.success('提交成功');
router.push('/system/tenant');
}
},
*remove({ payload }, { call }) {
const {
data: { keys },
success,
} = payload;
const response = yield call(remove, { ids: keys });
if (response.success) {
success();
}
},
},
reducers: {
saveList(state, action) {
return {
...state,
data: action.payload,
};
},
saveDetail(state, action) {
return {
...state,
detail: action.payload.detail,
};
},
removeDetail(state) {
return {
...state,
detail: {},
};
},
},
};

View File

@ -2,6 +2,7 @@ import { message } from 'antd';
import router from 'umi/router';
import { USER_NAMESPACE } from '../actions/user';
import { query as queryUsers, list, submit, detail, remove, grant } from '../services/user';
import { select as tenants } from '../services/tenant';
import { tree as roles } from '../services/role';
import { tree as depts } from '../services/dept';
import { getCurrentUser } from '../utils/authority';
@ -19,6 +20,7 @@ export default {
init: {
roleTree: [],
deptTree: [],
tenantList: [],
},
detail: {},
},
@ -57,9 +59,24 @@ export default {
*fetchInit({ payload }, { call, put }) {
const responseRole = yield call(roles, payload);
const responseDept = yield call(depts, payload);
if (responseRole.success && responseDept.success) {
const responseTenant = yield call(tenants, payload);
if (responseRole.success && responseDept.success && responseTenant.success) {
yield put({
type: 'saveInit',
payload: {
roleTree: responseRole.data,
deptTree: responseDept.data,
tenantList: responseTenant.data,
},
});
}
},
*fetchChangeInit({ payload }, { call, put }) {
const responseRole = yield call(roles, payload);
const responseDept = yield call(depts, payload);
if (responseRole.success && responseDept.success) {
yield put({
type: 'saveChangeInit',
payload: {
roleTree: responseRole.data,
deptTree: responseDept.data,
@ -146,6 +163,14 @@ export default {
init: action.payload,
};
},
saveChangeInit(state, action) {
const newState = state;
newState.init.roleTree = action.payload.roleTree;
newState.init.deptTree = action.payload.deptTree;
return {
...newState,
};
},
saveDetail(state, action) {
return {
...state,

View File

@ -122,7 +122,7 @@ class Notice extends PureComponent {
},
{
title: formatMessage({ id: 'desk.notice.date' }),
dataIndex: 'date',
dataIndex: 'releaseTime',
},
];

View File

@ -29,7 +29,7 @@ class NoticeAdd extends PureComponent {
const params = {
...values,
date: func.format(values.date),
releaseTime: func.format(values.releaseTime),
};
dispatch(NOTICE_SUBMIT(params));
@ -100,7 +100,7 @@ class NoticeAdd extends PureComponent {
)}
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="desk.notice.date" />}>
{getFieldDecorator('date', {
{getFieldDecorator('releaseTime', {
rules: [
{
required: true,

View File

@ -41,7 +41,7 @@ class NoticeAdd extends PureComponent {
const params = {
id,
...values,
date: func.format(values.date),
releaseTime: func.format(values.releaseTime),
};
dispatch(NOTICE_SUBMIT(params));
});
@ -113,14 +113,14 @@ class NoticeAdd extends PureComponent {
)}
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="desk.notice.date" />}>
{getFieldDecorator('date', {
{getFieldDecorator('releaseTime', {
rules: [
{
required: true,
message: formatMessage({ id: 'desk.notice.date.validation' }),
},
],
initialValue: moment(detail.date, 'YYYY-MM-DD HH:mm:ss'),
initialValue: moment(detail.releaseTime, 'YYYY-MM-DD HH:mm:ss'),
})(
<DatePicker
style={{ width: '100%' }}

View File

@ -66,7 +66,7 @@ class NoticeAdd extends PureComponent {
<span>{detail.categoryName}</span>
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="desk.notice.date" />}>
<span>{detail.date}</span>
<span>{detail.releaseTime}</span>
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="desk.notice.content" />}>
<span>{detail.content}</span>

View File

@ -2,10 +2,10 @@ import React, { Component } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Checkbox, Alert } from 'antd';
import Login from '@/components/Login';
import Login from '../../components/Login';
import styles from './Login.less';
const { Tab, UserName, Password, Submit } = Login;
const { Tab, TenantCode, UserName, Password, Submit } = Login;
@connect(({ login, loading }) => ({
login,
@ -80,6 +80,16 @@ class LoginPage extends Component {
login.type === 'account' &&
!submitting &&
this.renderMessage(formatMessage({ id: 'app.login.message-invalid-credentials' }))}
<TenantCode
name="tenantCode"
placeholder={`${formatMessage({ id: 'app.login.tenantCode' })}: 000000`}
rules={[
{
required: true,
message: formatMessage({ id: 'validation.tenantCode.required' }),
},
]}
/>
<UserName
name="account"
placeholder={`${formatMessage({ id: 'app.login.userName' })}: admin`}

View File

@ -26,12 +26,17 @@ class Dept extends PureComponent {
return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<Col md={6} sm={24}>
<FormItem label="部门名称">
{getFieldDecorator('deptName')(<Input placeholder="请输入部门名称" />)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<Col md={6} sm={24}>
<FormItem label="租户编号">
{getFieldDecorator('tenantCode')(<Input placeholder="请输入角色名称" />)}
</FormItem>
</Col>
<Col md={6} sm={24}>
<FormItem label="部门全称">
{getFieldDecorator('fullName')(<Input placeholder="请输入部门全称" />)}
</FormItem>
@ -64,6 +69,10 @@ class Dept extends PureComponent {
title: '部门名称',
dataIndex: 'deptName',
},
{
title: '租户编号',
dataIndex: 'tenantCode',
},
{
title: '部门全称',
dataIndex: 'fullName',

View File

@ -109,12 +109,6 @@ class DeptAdd extends PureComponent {
<Col span={10}>
<FormItem {...formItemLayout} label="上级部门">
{getFieldDecorator('parentId', {
rules: [
{
required: true,
message: '请选择上级部门',
},
],
initialValue: detail.id,
})(
<TreeSelect

View File

@ -121,12 +121,6 @@ class DeptEdit extends PureComponent {
<Col span={10}>
<FormItem {...formItemLayout} label="上级部门">
{getFieldDecorator('parentId', {
rules: [
{
required: true,
message: '请选择上级部门',
},
],
initialValue: detail.parentId,
})(
<TreeSelect

View File

@ -117,12 +117,17 @@ class Role extends PureComponent {
return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<Col md={6} sm={24}>
<FormItem label="角色名称">
{getFieldDecorator('roleName')(<Input placeholder="请输入角色名称" />)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<Col md={6} sm={24}>
<FormItem label="租户编号">
{getFieldDecorator('tenantCode')(<Input placeholder="请输入角色名称" />)}
</FormItem>
</Col>
<Col md={6} sm={24}>
<FormItem label="角色别名">
{getFieldDecorator('roleAlias')(<Input placeholder="请输入角色别名" />)}
</FormItem>
@ -175,6 +180,10 @@ class Role extends PureComponent {
title: '角色名称',
dataIndex: 'roleName',
},
{
title: '租户编号',
dataIndex: 'tenantCode',
},
{
title: '角色别名',
dataIndex: 'roleAlias',

View File

@ -0,0 +1,105 @@
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Button, Col, Form, Input, Row } from 'antd';
import Panel from '../../../components/Panel';
import { TENANT_LIST } from '../../../actions/tenant';
import Grid from '../../../components/Sword/Grid';
const FormItem = Form.Item;
@connect(({ tenant, loading }) => ({
tenant,
loading: loading.models.tenant,
}))
@Form.create()
class Tenant extends PureComponent {
// ============ 查询 ===============
handleSearch = params => {
const { dispatch } = this.props;
dispatch(TENANT_LIST(params));
};
// ============ 查询表单 ===============
renderSearchForm = onReset => {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={6} sm={24}>
<FormItem label="租户编号">
{getFieldDecorator('tenantCode')(<Input placeholder="请输入租户编号" />)}
</FormItem>
</Col>
<Col md={6} sm={24}>
<FormItem label="租户名称">
{getFieldDecorator('tenantName')(<Input placeholder="请输入租户名称" />)}
</FormItem>
</Col>
<Col md={6} sm={24}>
<FormItem label="联系电话">
{getFieldDecorator('contactNumber')(<Input placeholder="请输入联系电话" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};
render() {
const code = 'tenant';
const {
form,
loading,
tenant: { data },
} = this.props;
const columns = [
{
title: '租户编号',
dataIndex: 'tenantCode',
},
{
title: '租户名称',
dataIndex: 'tenantName',
},
{
title: '联系人',
dataIndex: 'linkman',
},
{
title: '联系电话',
dataIndex: 'contactNumber',
},
{
title: '联系地址',
dataIndex: 'address',
},
];
return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
renderSearchForm={this.renderSearchForm}
loading={loading}
data={data}
columns={columns}
/>
</Panel>
);
}
}
export default Tenant;

View File

@ -0,0 +1,96 @@
import React, { PureComponent } from 'react';
import { Form, Input, Card, Button } from 'antd';
import { connect } from 'dva';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { TENANT_SUBMIT } from '../../../actions/tenant';
const FormItem = Form.Item;
const { TextArea } = Input;
@connect(({ loading }) => ({
submitting: loading.effects['tenant/submit'],
}))
@Form.create()
class TenantAdd extends PureComponent {
handleSubmit = e => {
e.preventDefault();
const { dispatch, form } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
dispatch(TENANT_SUBMIT(values));
}
});
};
render() {
const {
form: { getFieldDecorator },
submitting,
} = this.props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const action = (
<Button type="primary" onClick={this.handleSubmit} loading={submitting}>
提交
</Button>
);
return (
<Panel title="新增" back="/system/tenant" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="租户名称">
{getFieldDecorator('tenantName', {
rules: [
{
required: true,
message: '请输入租户名称',
},
],
})(<Input placeholder="请输入租户名称" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系人">
{getFieldDecorator('linkman', {
rules: [
{
required: true,
message: '请输入联系人',
},
],
})(<Input placeholder="请输入联系人" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系电话">
{getFieldDecorator('contactNumber', {
rules: [
{
required: true,
message: '请输入联系电话',
},
],
})(<Input placeholder="请输入联系电话" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系地址">
{getFieldDecorator('address')(
<TextArea style={{ minHeight: 32 }} rows={3} placeholder="请输入联系地址" />
)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}
export default TenantAdd;

View File

@ -0,0 +1,122 @@
import React, { PureComponent } from 'react';
import { Form, Input, Card, Button } from 'antd';
import { connect } from 'dva';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { TENANT_DETAIL, TENANT_SUBMIT } from '../../../actions/tenant';
const FormItem = Form.Item;
const { TextArea } = Input;
@connect(({ tenant, loading }) => ({
tenant,
submitting: loading.effects['tenant/submit'],
}))
@Form.create()
class TenantEdit extends PureComponent {
componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;
dispatch(TENANT_DETAIL(id));
}
handleSubmit = e => {
e.preventDefault();
const {
dispatch,
match: {
params: { id },
},
form,
} = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
id,
...values,
};
console.log(params);
dispatch(TENANT_SUBMIT(params));
}
});
};
render() {
const {
form: { getFieldDecorator },
tenant: { detail },
submitting,
} = this.props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const action = (
<Button type="primary" onClick={this.handleSubmit} loading={submitting}>
提交
</Button>
);
return (
<Panel title="修改" back="/system/tenant" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="租户名称">
{getFieldDecorator('tenantName', {
rules: [
{
required: true,
message: '请输入租户名称',
},
],
initialValue: detail.tenantName,
})(<Input placeholder="请输入租户名称" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系人">
{getFieldDecorator('linkman', {
rules: [
{
required: true,
message: '请输入联系人',
},
],
initialValue: detail.linkman,
})(<Input placeholder="请输入联系人" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系电话">
{getFieldDecorator('contactNumber', {
rules: [
{
required: true,
message: '请输入联系电话',
},
],
initialValue: detail.contactNumber,
})(<Input placeholder="请输入联系电话" />)}
</FormItem>
<FormItem {...formItemLayout} label="联系地址">
{getFieldDecorator('address', {
initialValue: detail.address,
})(<TextArea style={{ minHeight: 32 }} rows={3} placeholder="请输入联系地址" />)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}
export default TenantEdit;

View File

@ -0,0 +1,83 @@
import React, { PureComponent } from 'react';
import router from 'umi/router';
import { Form, Card, Button } from 'antd';
import { connect } from 'dva';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { TENANT_DETAIL } from '../../../actions/tenant';
const FormItem = Form.Item;
@connect(({ tenant }) => ({
tenant,
}))
@Form.create()
class TenantView extends PureComponent {
componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;
dispatch(TENANT_DETAIL(id));
}
handleEdit = () => {
const {
match: {
params: { id },
},
} = this.props;
router.push(`/system/tenant/edit/${id}`);
};
render() {
const {
tenant: { detail },
} = this.props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const action = (
<Button type="primary" onClick={this.handleEdit}>
修改
</Button>
);
return (
<Panel title="查看" back="/system/tenant" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="租户编号">
<span>{detail.tenantCode}</span>
</FormItem>
<FormItem {...formItemLayout} label="租户名称">
<span>{detail.tenantName}</span>
</FormItem>
<FormItem {...formItemLayout} label="联系人">
<span>{detail.linkman}</span>
</FormItem>
<FormItem {...formItemLayout} label="联系电话">
<span>{detail.contactNumber}</span>
</FormItem>
<FormItem {...formItemLayout} label="联系地址">
<span>{detail.address}</span>
</FormItem>
</Card>
</Form>
</Panel>
);
}
}
export default TenantView;

View File

@ -173,6 +173,10 @@ class User extends PureComponent {
} = this.props;
const columns = [
{
title: '租户编号',
dataIndex: 'tenantCode',
},
{
title: '登录账号',
dataIndex: 'account',

View File

@ -4,7 +4,7 @@ import { connect } from 'dva';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { USER_INIT, USER_SUBMIT } from '../../../actions/user';
import { USER_INIT, USER_CHANGE_INIT, USER_SUBMIT } from '../../../actions/user';
import func from '../../../utils/Func';
const FormItem = Form.Item;
@ -42,11 +42,17 @@ class UserAdd extends PureComponent {
});
};
handleChange = value => {
const { dispatch, form } = this.props;
form.resetFields(['roleId', 'deptId']);
dispatch(USER_CHANGE_INIT({ tenantCode: value }));
};
render() {
const {
form: { getFieldDecorator },
user: {
init: { roleTree, deptTree },
init: { roleTree, deptTree, tenantList },
},
submitting,
} = this.props;
@ -60,15 +66,6 @@ class UserAdd extends PureComponent {
},
};
const formAllItemLayout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 20,
},
};
const action = (
<Button type="primary" onClick={this.handleSubmit} loading={submitting}>
提交
@ -80,8 +77,8 @@ class UserAdd extends PureComponent {
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<Row gutter={24}>
<Col span={20}>
<FormItem {...formAllItemLayout} label="登录账号">
<Col span={10}>
<FormItem {...formItemLayout} label="登录账号">
{getFieldDecorator('account', {
rules: [
{
@ -92,6 +89,34 @@ class UserAdd extends PureComponent {
})(<Input placeholder="请输入登录账号" />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem {...formItemLayout} label="所属租户">
{getFieldDecorator('tenantCode', {
rules: [
{
required: true,
message: '请选择所属租户',
},
],
})(
<Select
showSearch
onChange={this.handleChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
allowClear
placeholder="请选择所属租户"
>
{tenantList.map(d => (
<Select.Option key={d.tenantCode} value={d.tenantCode}>
{d.tenantName}
</Select.Option>
))}
</Select>
)}
</FormItem>
</Col>
</Row>
<Row gutter={24}>
<Col span={10}>

View File

@ -5,7 +5,7 @@ import { connect } from 'dva';
import Panel from '../../../components/Panel';
import func from '../../../utils/Func';
import styles from '../../../layouts/Sword.less';
import { USER_DETAIL, USER_INIT, USER_SUBMIT } from '../../../actions/user';
import { USER_CHANGE_INIT, USER_DETAIL, USER_INIT, USER_SUBMIT } from '../../../actions/user';
const FormItem = Form.Item;
@ -49,12 +49,18 @@ class UserEdit extends PureComponent {
});
};
handleChange = value => {
const { dispatch, form } = this.props;
form.resetFields(['roleId', 'deptId']);
dispatch(USER_CHANGE_INIT({ tenantCode: value }));
};
render() {
const {
form: { getFieldDecorator },
user: {
detail,
init: { roleTree, deptTree },
init: { roleTree, deptTree, tenantList },
},
submitting,
} = this.props;
@ -70,15 +76,6 @@ class UserEdit extends PureComponent {
},
};
const formAllItemLayout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 20,
},
};
const action = (
<Button type="primary" onClick={this.handleSubmit} loading={submitting}>
提交
@ -90,8 +87,8 @@ class UserEdit extends PureComponent {
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<Row gutter={24}>
<Col span={20}>
<FormItem {...formAllItemLayout} label="登录账号">
<Col span={10}>
<FormItem {...formItemLayout} label="登录账号">
{getFieldDecorator('account', {
rules: [
{
@ -103,6 +100,35 @@ class UserEdit extends PureComponent {
})(<Input placeholder="请输入登录账号" />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem {...formItemLayout} label="所属租户">
{getFieldDecorator('tenantCode', {
rules: [
{
required: true,
message: '请选择所属租户',
},
],
initialValue: detail.tenantCode,
})(
<Select
showSearch
onChange={this.handleChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
allowClear
placeholder="请选择所属租户"
>
{tenantList.map(d => (
<Select.Option key={d.tenantCode} value={d.tenantCode}>
{d.tenantName}
</Select.Option>
))}
</Select>
)}
</FormItem>
</Col>
</Row>
<Row gutter={24}>
<Col span={10}>

View File

@ -46,15 +46,6 @@ class UserView extends PureComponent {
},
};
const formAllItemLayout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 20,
},
};
const action = (
<Button type="primary" onClick={this.handleEdit}>
修改
@ -66,11 +57,16 @@ class UserView extends PureComponent {
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<Row gutter={24}>
<Col span={20}>
<FormItem {...formAllItemLayout} label="登录账号">
<Col span={10}>
<FormItem {...formItemLayout} label="登录账号">
<span>{detail.account}</span>
</FormItem>
</Col>
<Col span={10}>
<FormItem {...formItemLayout} label="所属租户">
<span>{detail.tenantCode}</span>
</FormItem>
</Col>
</Row>
<Row gutter={24}>
<Col span={10}>

29
src/services/tenant.js Normal file
View File

@ -0,0 +1,29 @@
import { stringify } from 'qs';
import func from '../utils/Func';
import request from '../utils/request';
export async function list(params) {
return request(`/api/blade-system/tenant/list?${stringify(params)}`);
}
export async function select(params) {
return request(`/api/blade-system/tenant/select?${stringify(params)}`);
}
export async function submit(params) {
return request('/api/blade-system/tenant/submit', {
method: 'POST',
body: params,
});
}
export async function detail(params) {
return request(`/api/blade-system/tenant/detail?${stringify(params)}`);
}
export async function remove(params) {
return request('/api/blade-system/tenant/remove', {
method: 'POST',
body: func.toFormData(params),
});
}