diff --git a/doc/RBAC 权限.sql b/doc/RBAC 权限.sql index b463af5..3118103 100644 --- a/doc/RBAC 权限.sql +++ b/doc/RBAC 权限.sql @@ -74,6 +74,16 @@ VALUES ('审核价格', '/products/offer/audit', 'products') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) VALUES ('录入价格', '/products/offer/put', 'products') +-- 默认页面 +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('最新计划', 'route=/reservation/newest', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('机票订票', 'route=/airticket', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('产品管理(客服)', 'route=/products', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('产品管理(供应商)', 'route=/products?from', 'page') + INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) VALUES (1, 1) INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) diff --git a/doc/价格管理平台.bmpr b/doc/价格管理平台.bmpr index eb1a8d6..de2695d 100644 Binary files a/doc/价格管理平台.bmpr and b/doc/价格管理平台.bmpr differ diff --git a/public/locales/zh/account.json b/public/locales/zh/account.json index 35c0dc0..3849f07 100644 --- a/public/locales/zh/account.json +++ b/public/locales/zh/account.json @@ -12,7 +12,10 @@ "createdOn": "创建时间", "action": "操作", "action.edit": "编辑", + "action.enable": "启用", "action.disable": "禁用", + "action.enable.title": "确定启用该账号吗?", + "action.disable.title": "确定禁用该账号吗?", "action.resetPassword": "重置密码", "accountList": "管理账号", diff --git a/src/main.jsx b/src/main.jsx index 02f2244..5d1c8cd 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -32,8 +32,7 @@ import AirticketPlan from "@/views/airticket/Plan"; import { ThemeContext } from '@/stores/ThemeContext' import { usingStorage } from '@/hooks/usingStorage' import { isNotEmpty } from '@/utils/commons' -import { appendRequestParams } from '@/utils/request' -import { fireAuth } from "./utils/lifecycle" +import { notifyAuth } from "./utils/lifecycle" import ProductsManage from '@/views/products/Manage'; import ProductsDetail from '@/views/products/Detail'; @@ -42,19 +41,13 @@ import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, import './i18n'; -const { loginToken, userId } = usingStorage() const initAppliction = async () => { - if (isNotEmpty(loginToken)) { - appendRequestParams('token', loginToken) - appendRequestParams('lmi_sn', userId) + const { loginToken, userId } = usingStorage() - } - - if (isNotEmpty(userId)) { - appendRequestParams('lmi_sn', userId) - await fireAuth() + if (isNotEmpty(userId) && isNotEmpty(loginToken)) { + await notifyAuth() } } diff --git a/src/stores/Account.js b/src/stores/Account.js index 01010d8..a57b6db 100644 --- a/src/stores/Account.js +++ b/src/stores/Account.js @@ -70,16 +70,15 @@ const useAccountStore = create((set, get) => ({ accountList: [], - disableAccount: async (userId) => { + toggleAccountStatus: async (userId, status) => { + + const statusValue = status ? 'enable' : 'disable' const formData = new FormData() formData.append('lmi_sn', userId) - // enable | disable - formData.append('account_status', 'disable') - - const result = await postAccountStatus(formData) + formData.append('account_status', statusValue) - console.info(result) + return postAccountStatus(formData) }, resetAccountPassword: async (userId, password) => { @@ -91,7 +90,7 @@ const useAccountStore = create((set, get) => ({ return postAccountPassword(formData) }, - newRole: () => { + newEmptyRole: () => { return { role_id: null, role_name: '', @@ -99,6 +98,19 @@ const useAccountStore = create((set, get) => ({ } }, + newEmptyAccount: () => { + return { + accountId: null, + userId: null, + lmi2_sn: null, + username: '', + realname: '', + email: '', + travelAgencyId: null, + roleId: '' + } + }, + saveOrUpdateRole: async (formValues) => { const formData = new FormData() formData.append('role_id', formValues.role_id) @@ -117,7 +129,6 @@ const useAccountStore = create((set, get) => ({ formData.append('user_name', formValues.username) formData.append('real_name', formValues.realname) formData.append('email', formValues.email) - formData.append('travel_agency_id', formValues.travelAgencyId) formData.append('roles', formValues.roleId) @@ -147,6 +158,7 @@ const useAccountStore = create((set, get) => ({ lastLogin: r.wu_lastlogindate, travelAgencyName: r.travel_agency_name, travelAgencyId: r.travel_agency_id, + disabled: r.wu_limitsign, // 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。 roleId: parseInt(r.roles), role: r.roles_name, diff --git a/src/stores/Auth.js b/src/stores/Auth.js index 344e147..4160fe3 100644 --- a/src/stores/Auth.js +++ b/src/stores/Auth.js @@ -3,14 +3,12 @@ import { appendRequestParams, fetchJSON, postForm } from '@/utils/request' import { HT_HOST } from "@/config" import { loadPageSpy } from '@/pageSpy' import { usingStorage } from '@/hooks/usingStorage' -import { devtools } from 'zustand/middleware' -import { obervseLifecycle } from '@/utils/lifecycle' +import { lifecycleware } from '@/utils/lifecycle' const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN' const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID' const KEY_USER_ID = 'G-INT:USER_ID' -const KEY_USER_DETAIL = 'G-JSON:USER_DETAIL' const WILDCARD_TOKEN = '*' @@ -45,67 +43,52 @@ async function fetchLastRequet() { return errcode !== 0 ? {} : result } -const useAuthStore = create(obervseLifecycle((set, get) => ({ +const useAuthStore = create(lifecycleware((set, get) => ({ - onAuth: () => { + onAuth: async () => { const { startTokenInterval, loadUserPermission } = get() - const { userId } = usingStorage() - loadUserPermission(userId) - startTokenInterval() - }, - - tokenInterval: null, + const { userId, loginToken } = usingStorage() - tokenTimeout: false, - - loginStatus: 0, - - permissionList: [], - - isPermitted: (perm) => { - const { permissionList } = get() - // 测试权限使用: - // if (perm === '/account/management') return false - // if (perm === '/account/role/new') return false - // return true - // 以上是 Hardcode 判断 - // 以下是权限列表从数据库读取后使用的方法 - return permissionList.some((value) => { - if (value.indexOf(WILDCARD_TOKEN) == 0) { - return true - } - if (value === perm) { - return true - } - return false - }) + appendRequestParams('token', loginToken) + appendRequestParams('lmi_sn', userId) + await loadUserPermission(userId) + startTokenInterval() }, - validateUserPassword: async (usr, pwd) => { - const { startTokenInterval, loadUserPermission } = get() + authenticate: async (usr, pwd) => { + const { onAuth } = get() const { setStorage } = usingStorage() const { token: loginToken } = await fetchLoginToken(usr, pwd) const userDetail = await fetchUserDetail(loginToken) - await loadUserPermission(userDetail.LMI_SN) + + setStorage(KEY_LOGIN_TOKEN, loginToken) + setStorage(KEY_USER_ID, userDetail.LMI_SN) + setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN) + + await onAuth() set(() => ({ tokenTimeout: false, loginStatus: 302 })) - - setStorage(KEY_LOGIN_TOKEN, loginToken) - setStorage(KEY_USER_ID, userDetail.LMI_SN) - setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN) - appendRequestParams('token', loginToken) - appendRequestParams('lmi_sn', userDetail.LMI_SN) - // loadPageSpy(`${json.Result.VName}-${json.Result.LoginName}`) - startTokenInterval() }, loadUserPermission: async(userId) => { + let deaultPage = '/' const permissionResult = await fetchPermissionListByUserId(userId) + const pageList = permissionResult.filter(p => { + return p.res_category === 'page' + }) + if (pageList.length > 0) { + const resPattern = pageList[0].res_pattern + const splitResult = resPattern.split('=') + if (splitResult.length > 1) + deaultPage = splitResult[1] + } + set(() => ({ + defaultRoute: deaultPage, permissionList: permissionResult.map(p => p.res_pattern) })) }, @@ -116,6 +99,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({ clearStorage() clearInterval(tokenInterval) set(() => ({ + defaultRoute: '/', loginStatus: 0, tokenInterval: null, tokenTimeout: true @@ -152,6 +136,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({ })) }, + // 迁移到 Account.js changeUserPassword: (password, newPassword) => { const { userId } = usingStorage() const formData = new FormData(); @@ -170,6 +155,35 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({ }); }, + isPermitted: (perm) => { + const { permissionList } = get() + // 测试权限使用: + // if (perm === '/account/management') return false + // if (perm === '/account/role/new') return false + // return true + // 以上是 Hardcode 判断 + // 以下是权限列表从数据库读取后使用的方法 + return permissionList.some((value) => { + if (value.indexOf(WILDCARD_TOKEN) == 0) { + return true + } + if (value === perm) { + return true + } + return false + }) + }, + + tokenInterval: null, + + tokenTimeout: false, + + loginStatus: 0, + + defaltRoute: '', + + permissionList: [], + }))) export default useAuthStore diff --git a/src/utils/lifecycle.js b/src/utils/lifecycle.js index a2de514..0d3deb1 100644 --- a/src/utils/lifecycle.js +++ b/src/utils/lifecycle.js @@ -1,28 +1,28 @@ const initListener = [] const authListener = [] -export const onInit = (fn) => { +export const addInitLinstener = (fn) => { initListener.push(fn) } -export const onAuth = (fn) => { +export const addAuthLinstener = (fn) => { authListener.push(fn) } -export const fireInit = async () => { +export const notifyInit = async () => { initListener.forEach(async (fn) => { await fn() }) } -export const fireAuth = (obj) => { - authListener.forEach(fn => fn(obj)) +export const notifyAuth = async (obj) => { + authListener.forEach(async (fn) => await fn(obj)) } // Zustand 中间件,用于订阅前端应用的生命周期,实验阶段 -export const obervseLifecycle = (fn) => (set, get, store) => { +export const lifecycleware = (fn) => (set, get, store) => { - onInit(() => { + addInitLinstener(() => { if (store.getState().hasOwnProperty('onInit')) { store.getState().onInit() } else { @@ -30,7 +30,7 @@ export const obervseLifecycle = (fn) => (set, get, store) => { } }) - onAuth(() => { + addAuthLinstener(() => { if (store.getState().hasOwnProperty('onAuth')) { store.getState().onAuth() } else { diff --git a/src/views/App.jsx b/src/views/App.jsx index 3541d9b..c3b227d 100644 --- a/src/views/App.jsx +++ b/src/views/App.jsx @@ -29,8 +29,8 @@ function App() { const [password, setPassword] = useState('') const [userDetail, setUserDetail] = useState({}) - const [validateUserPassword, tokenTimeout, isPermitted] = useAuthStore( - (state) => [state.validateUserPassword, state.tokenTimeout, state.isPermitted]) + const [authenticate, tokenTimeout, isPermitted] = useAuthStore( + (state) => [state.authenticate, state.tokenTimeout, state.isPermitted]) const { loginToken } = usingStorage() @@ -61,7 +61,7 @@ function App() { }, [href]) const onSubmit = () => { - validateUserPassword(userDetail?.username, password) + authenticate(userDetail?.username, password) .catch(ex => { console.error(ex) alert(t('Validation.LoginFailed')) diff --git a/src/views/Login.jsx b/src/views/Login.jsx index f9c3008..1ce2cc3 100644 --- a/src/views/Login.jsx +++ b/src/views/Login.jsx @@ -6,9 +6,8 @@ import useAuthStore from '@/stores/Auth' import useNoticeStore from '@/stores/Notice' function Login() { - const [validateUserPassword, loginStatus] = - useAuthStore((state) => [state.validateUserPassword, state.loginStatus]) - const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount) + const [authenticate, loginStatus, defaultRoute] = + useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute]) const { t, i18n } = useTranslation() const { notification } = App.useApp() @@ -17,12 +16,12 @@ function Login() { useEffect (() => { if (loginStatus === 302) { - navigate('/') + navigate(defaultRoute) } }, [loginStatus]) const onFinish = (values) => { - validateUserPassword(values.username, values.password) + authenticate(values.username, values.password) .catch(ex => { console.error(ex) notification.error({ diff --git a/src/views/account/Management.jsx b/src/views/account/Management.jsx index d3e3d2d..d541fe5 100644 --- a/src/views/account/Management.jsx +++ b/src/views/account/Management.jsx @@ -3,7 +3,7 @@ import useAccountStore, { fetchRoleList, fetchTravelAgencyByName } from '@/store import useFormStore from '@/stores/Form' import { isEmpty } from '@/utils/commons' import { ExclamationCircleFilled } from '@ant-design/icons' -import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography } from 'antd' +import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography, Switch } from 'antd' import dayjs from 'dayjs' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -41,7 +41,7 @@ function Management() { render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss')) }, { - title: t('account:action.edit'), + title: t('account:action'), dataIndex: 'account:action', render: actionRender }, @@ -53,10 +53,12 @@ function Management() { ) } - function actionRender(text, account) { + function actionRender(_, account) { return ( - + { + showDisableConfirm(account, checked) + }} /> ) @@ -69,9 +71,9 @@ function Management() { const [currentTravelAgency, setCurrentTravelAgency] = useState(null) const [accountForm] = Form.useForm() - const [searchAccountByCriteria, accountList, disableAccount, saveOrUpdateAccount, resetAccountPassword] = + const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] = useAccountStore((state) => - [state.searchAccountByCriteria, state.accountList, state.disableAccount, state.saveOrUpdateAccount, state.resetAccountPassword]) + [state.searchAccountByCriteria, state.accountList, state.toggleAccountStatus, state.saveOrUpdateAccount, state.resetAccountPassword, state.newEmptyAccount]) const formValues = useFormStore(state => state.formValues) const { notification, modal } = App.useApp() @@ -115,8 +117,13 @@ function Management() { setAccountModalOpen(true) } + const onNewAccount = () => { + const emptyAccount = newEmptyAccount() + accountForm.setFieldsValue(emptyAccount) + setAccountModalOpen(true) + } + const onAccountFinish = (values) => { - console.log(values) saveOrUpdateAccount(values) .then(() => { handelAccountSearch() @@ -156,13 +163,27 @@ function Management() { setCurrentTravelAgency(newValue) } - const showDisableConfirm = (account) => { + const showDisableConfirm = (account, status) => { + + const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title') + modal.confirm({ - title: 'Do you want to disable this account?', + title: confirmTitle, icon: , - content: `Username: ${account.username}, Realname: ${account.realname}`, + content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname, onOk() { - disableAccount(account.userId) + toggleAccountStatus(account.userId, status) + .then(() => { + handelAccountSearch() + }) + .catch(ex => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }) + }) }, onCancel() { }, @@ -170,7 +191,7 @@ function Management() { } const showResetPasswordConfirm = (account) => { - const randomPassword = account.username + (Math.floor(Math.random() * 900) + 100) + const randomPassword = account.username + '@' + (Math.floor(Math.random() * 900) + 100) modal.confirm({ title: 'Do you want to reset password?', icon: , @@ -179,8 +200,8 @@ function Management() { resetAccountPassword(account.userId, randomPassword) .then(() => { notification.info({ - message: '新密码:' + randomPassword, - description: `请复制密码给 [${account.realname}]`, + message: `请复制新密码给 [${account.realname}]`, + description: '新密码:' + randomPassword, placement: 'top', duration: 60, }) @@ -317,7 +338,7 @@ function Management() { - + diff --git a/src/views/account/RoleList.jsx b/src/views/account/RoleList.jsx index 71909f1..b4f9f92 100644 --- a/src/views/account/RoleList.jsx +++ b/src/views/account/RoleList.jsx @@ -69,6 +69,7 @@ function RoleList() { ['domestic', '国内供应商'], ['air-ticket', '机票供应商'], ['products', '产品价格'], + ['page', '默认页面'], ]); const permissionTree = [] @@ -107,11 +108,11 @@ function RoleList() { const [roleAllList, setRoleAllList] = useState([]) const [roleForm] = Form.useForm() - const [saveOrUpdateRole, newRole] = + const [saveOrUpdateRole, newEmptyRole] = useAccountStore((state) => - [state.saveOrUpdateRole, state.newRole]) + [state.saveOrUpdateRole, state.newEmptyRole]) - const { notification, modal } = App.useApp() + const { notification } = App.useApp() const onRoleSeleted = (role) => { fetchPermissionListByRoleId({ role_id: role.role_id }) @@ -123,7 +124,7 @@ function RoleList() { } const onNewRole = () => { - const role = newRole() + const role = newEmptyRole() roleForm.setFieldsValue(role) setRoleModalOpen(true) }