Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager

feature/price_manager
黄文强@HWQ-PC 1 year ago
commit acea97d3e5

@ -74,6 +74,16 @@ VALUES ('审核价格', '/products/offer/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('录入价格', '/products/offer/put', 'products') 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]) INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1) VALUES (1, 1)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])

Binary file not shown.

@ -12,7 +12,10 @@
"createdOn": "创建时间", "createdOn": "创建时间",
"action": "操作", "action": "操作",
"action.edit": "编辑", "action.edit": "编辑",
"action.enable": "启用",
"action.disable": "禁用", "action.disable": "禁用",
"action.enable.title": "确定启用该账号吗?",
"action.disable.title": "确定禁用该账号吗?",
"action.resetPassword": "重置密码", "action.resetPassword": "重置密码",
"accountList": "管理账号", "accountList": "管理账号",

@ -32,8 +32,7 @@ import AirticketPlan from "@/views/airticket/Plan";
import { ThemeContext } from '@/stores/ThemeContext' import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage' import { usingStorage } from '@/hooks/usingStorage'
import { isNotEmpty } from '@/utils/commons' import { isNotEmpty } from '@/utils/commons'
import { appendRequestParams } from '@/utils/request' import { notifyAuth } from "./utils/lifecycle"
import { fireAuth } from "./utils/lifecycle"
import ProductsManage from '@/views/products/Manage'; import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail'; import ProductsDetail from '@/views/products/Detail';
@ -42,19 +41,13 @@ import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET,
import './i18n'; import './i18n';
const { loginToken, userId } = usingStorage()
const initAppliction = async () => { const initAppliction = async () => {
if (isNotEmpty(loginToken)) { const { loginToken, userId } = usingStorage()
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userId)
} if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await notifyAuth()
if (isNotEmpty(userId)) {
appendRequestParams('lmi_sn', userId)
await fireAuth()
} }
} }

@ -70,16 +70,15 @@ const useAccountStore = create((set, get) => ({
accountList: [], accountList: [],
disableAccount: async (userId) => { toggleAccountStatus: async (userId, status) => {
const statusValue = status ? 'enable' : 'disable'
const formData = new FormData() const formData = new FormData()
formData.append('lmi_sn', userId) formData.append('lmi_sn', userId)
// enable | disable formData.append('account_status', statusValue)
formData.append('account_status', 'disable')
const result = await postAccountStatus(formData)
console.info(result) return postAccountStatus(formData)
}, },
resetAccountPassword: async (userId, password) => { resetAccountPassword: async (userId, password) => {
@ -91,7 +90,7 @@ const useAccountStore = create((set, get) => ({
return postAccountPassword(formData) return postAccountPassword(formData)
}, },
newRole: () => { newEmptyRole: () => {
return { return {
role_id: null, role_id: null,
role_name: '', 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) => { saveOrUpdateRole: async (formValues) => {
const formData = new FormData() const formData = new FormData()
formData.append('role_id', formValues.role_id) formData.append('role_id', formValues.role_id)
@ -117,7 +129,6 @@ const useAccountStore = create((set, get) => ({
formData.append('user_name', formValues.username) formData.append('user_name', formValues.username)
formData.append('real_name', formValues.realname) formData.append('real_name', formValues.realname)
formData.append('email', formValues.email) formData.append('email', formValues.email)
formData.append('travel_agency_id', formValues.travelAgencyId) formData.append('travel_agency_id', formValues.travelAgencyId)
formData.append('roles', formValues.roleId) formData.append('roles', formValues.roleId)
@ -147,6 +158,7 @@ const useAccountStore = create((set, get) => ({
lastLogin: r.wu_lastlogindate, lastLogin: r.wu_lastlogindate,
travelAgencyName: r.travel_agency_name, travelAgencyName: r.travel_agency_name,
travelAgencyId: r.travel_agency_id, travelAgencyId: r.travel_agency_id,
disabled: r.wu_limitsign,
// 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。 // 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。
roleId: parseInt(r.roles), roleId: parseInt(r.roles),
role: r.roles_name, role: r.roles_name,

@ -3,14 +3,12 @@ import { appendRequestParams, fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config" import { HT_HOST } from "@/config"
import { loadPageSpy } from '@/pageSpy' import { loadPageSpy } from '@/pageSpy'
import { usingStorage } from '@/hooks/usingStorage' 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_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID' const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID' const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_USER_DETAIL = 'G-JSON:USER_DETAIL'
const WILDCARD_TOKEN = '*' const WILDCARD_TOKEN = '*'
@ -45,67 +43,52 @@ async function fetchLastRequet() {
return errcode !== 0 ? {} : result return errcode !== 0 ? {} : result
} }
const useAuthStore = create(obervseLifecycle((set, get) => ({ const useAuthStore = create(lifecycleware((set, get) => ({
onAuth: () => { onAuth: async () => {
const { startTokenInterval, loadUserPermission } = get() const { startTokenInterval, loadUserPermission } = get()
const { userId } = usingStorage() const { userId, loginToken } = usingStorage()
loadUserPermission(userId)
startTokenInterval()
},
tokenInterval: null,
tokenTimeout: false, appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userId)
loginStatus: 0, await loadUserPermission(userId)
startTokenInterval()
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
})
}, },
validateUserPassword: async (usr, pwd) => { authenticate: async (usr, pwd) => {
const { startTokenInterval, loadUserPermission } = get() const { onAuth } = get()
const { setStorage } = usingStorage() const { setStorage } = usingStorage()
const { token: loginToken } = await fetchLoginToken(usr, pwd) const { token: loginToken } = await fetchLoginToken(usr, pwd)
const userDetail = await fetchUserDetail(loginToken) 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(() => ({ set(() => ({
tokenTimeout: false, tokenTimeout: false,
loginStatus: 302 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) => { loadUserPermission: async(userId) => {
let deaultPage = '/'
const permissionResult = await fetchPermissionListByUserId(userId) 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(() => ({ set(() => ({
defaultRoute: deaultPage,
permissionList: permissionResult.map(p => p.res_pattern) permissionList: permissionResult.map(p => p.res_pattern)
})) }))
}, },
@ -116,6 +99,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({
clearStorage() clearStorage()
clearInterval(tokenInterval) clearInterval(tokenInterval)
set(() => ({ set(() => ({
defaultRoute: '/',
loginStatus: 0, loginStatus: 0,
tokenInterval: null, tokenInterval: null,
tokenTimeout: true tokenTimeout: true
@ -152,6 +136,7 @@ const useAuthStore = create(obervseLifecycle((set, get) => ({
})) }))
}, },
// 迁移到 Account.js
changeUserPassword: (password, newPassword) => { changeUserPassword: (password, newPassword) => {
const { userId } = usingStorage() const { userId } = usingStorage()
const formData = new FormData(); 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 export default useAuthStore

@ -1,28 +1,28 @@
const initListener = [] const initListener = []
const authListener = [] const authListener = []
export const onInit = (fn) => { export const addInitLinstener = (fn) => {
initListener.push(fn) initListener.push(fn)
} }
export const onAuth = (fn) => { export const addAuthLinstener = (fn) => {
authListener.push(fn) authListener.push(fn)
} }
export const fireInit = async () => { export const notifyInit = async () => {
initListener.forEach(async (fn) => { initListener.forEach(async (fn) => {
await fn() await fn()
}) })
} }
export const fireAuth = (obj) => { export const notifyAuth = async (obj) => {
authListener.forEach(fn => fn(obj)) authListener.forEach(async (fn) => await fn(obj))
} }
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段 // Zustand 中间件,用于订阅前端应用的生命周期,实验阶段
export const obervseLifecycle = (fn) => (set, get, store) => { export const lifecycleware = (fn) => (set, get, store) => {
onInit(() => { addInitLinstener(() => {
if (store.getState().hasOwnProperty('onInit')) { if (store.getState().hasOwnProperty('onInit')) {
store.getState().onInit() store.getState().onInit()
} else { } else {
@ -30,7 +30,7 @@ export const obervseLifecycle = (fn) => (set, get, store) => {
} }
}) })
onAuth(() => { addAuthLinstener(() => {
if (store.getState().hasOwnProperty('onAuth')) { if (store.getState().hasOwnProperty('onAuth')) {
store.getState().onAuth() store.getState().onAuth()
} else { } else {

@ -29,8 +29,8 @@ function App() {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [userDetail, setUserDetail] = useState({}) const [userDetail, setUserDetail] = useState({})
const [validateUserPassword, tokenTimeout, isPermitted] = useAuthStore( const [authenticate, tokenTimeout, isPermitted] = useAuthStore(
(state) => [state.validateUserPassword, state.tokenTimeout, state.isPermitted]) (state) => [state.authenticate, state.tokenTimeout, state.isPermitted])
const { loginToken } = usingStorage() const { loginToken } = usingStorage()
@ -61,7 +61,7 @@ function App() {
}, [href]) }, [href])
const onSubmit = () => { const onSubmit = () => {
validateUserPassword(userDetail?.username, password) authenticate(userDetail?.username, password)
.catch(ex => { .catch(ex => {
console.error(ex) console.error(ex)
alert(t('Validation.LoginFailed')) alert(t('Validation.LoginFailed'))

@ -6,9 +6,8 @@ import useAuthStore from '@/stores/Auth'
import useNoticeStore from '@/stores/Notice' import useNoticeStore from '@/stores/Notice'
function Login() { function Login() {
const [validateUserPassword, loginStatus] = const [authenticate, loginStatus, defaultRoute] =
useAuthStore((state) => [state.validateUserPassword, state.loginStatus]) useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount)
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { notification } = App.useApp() const { notification } = App.useApp()
@ -17,12 +16,12 @@ function Login() {
useEffect (() => { useEffect (() => {
if (loginStatus === 302) { if (loginStatus === 302) {
navigate('/') navigate(defaultRoute)
} }
}, [loginStatus]) }, [loginStatus])
const onFinish = (values) => { const onFinish = (values) => {
validateUserPassword(values.username, values.password) authenticate(values.username, values.password)
.catch(ex => { .catch(ex => {
console.error(ex) console.error(ex)
notification.error({ notification.error({

@ -3,7 +3,7 @@ import useAccountStore, { fetchRoleList, fetchTravelAgencyByName } from '@/store
import useFormStore from '@/stores/Form' import useFormStore from '@/stores/Form'
import { isEmpty } from '@/utils/commons' import { isEmpty } from '@/utils/commons'
import { ExclamationCircleFilled } from '@ant-design/icons' 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 dayjs from 'dayjs'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -41,7 +41,7 @@ function Management() {
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss')) 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', dataIndex: 'account:action',
render: actionRender render: actionRender
}, },
@ -53,10 +53,12 @@ function Management() {
) )
} }
function actionRender(text, account) { function actionRender(_, account) {
return ( return (
<Space key='actionRenderSpace' size='middle'> <Space key='actionRenderSpace' size='middle'>
<Button type='link' key='disable' onClick={() => showDisableConfirm(account)}>{t('account:action.disable')}</Button> <Switch checkedChildren={t('account:action.enable')} unCheckedChildren={t('account:action.disable')} checked={account.disabled==0} onChange={(checked) => {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button> <Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
</Space> </Space>
) )
@ -69,9 +71,9 @@ function Management() {
const [currentTravelAgency, setCurrentTravelAgency] = useState(null) const [currentTravelAgency, setCurrentTravelAgency] = useState(null)
const [accountForm] = Form.useForm() const [accountForm] = Form.useForm()
const [searchAccountByCriteria, accountList, disableAccount, saveOrUpdateAccount, resetAccountPassword] = const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] =
useAccountStore((state) => 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 formValues = useFormStore(state => state.formValues)
const { notification, modal } = App.useApp() const { notification, modal } = App.useApp()
@ -115,8 +117,13 @@ function Management() {
setAccountModalOpen(true) setAccountModalOpen(true)
} }
const onNewAccount = () => {
const emptyAccount = newEmptyAccount()
accountForm.setFieldsValue(emptyAccount)
setAccountModalOpen(true)
}
const onAccountFinish = (values) => { const onAccountFinish = (values) => {
console.log(values)
saveOrUpdateAccount(values) saveOrUpdateAccount(values)
.then(() => { .then(() => {
handelAccountSearch() handelAccountSearch()
@ -156,13 +163,27 @@ function Management() {
setCurrentTravelAgency(newValue) setCurrentTravelAgency(newValue)
} }
const showDisableConfirm = (account) => { const showDisableConfirm = (account, status) => {
const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title')
modal.confirm({ modal.confirm({
title: 'Do you want to disable this account?', title: confirmTitle,
icon: <ExclamationCircleFilled />, icon: <ExclamationCircleFilled />,
content: `Username: ${account.username}, Realname: ${account.realname}`, content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() { onOk() {
disableAccount(account.userId) toggleAccountStatus(account.userId, status)
.then(() => {
handelAccountSearch()
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
}, },
onCancel() { onCancel() {
}, },
@ -170,7 +191,7 @@ function Management() {
} }
const showResetPasswordConfirm = (account) => { 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({ modal.confirm({
title: 'Do you want to reset password?', title: 'Do you want to reset password?',
icon: <ExclamationCircleFilled />, icon: <ExclamationCircleFilled />,
@ -179,8 +200,8 @@ function Management() {
resetAccountPassword(account.userId, randomPassword) resetAccountPassword(account.userId, randomPassword)
.then(() => { .then(() => {
notification.info({ notification.info({
message: '新密码:' + randomPassword, message: `请复制新密码给 [${account.realname}]`,
description: `请复制密码给 [${account.realname}]`, description: '新密码:' + randomPassword,
placement: 'top', placement: 'top',
duration: 60, duration: 60,
}) })
@ -317,7 +338,7 @@ function Management() {
<Row> <Row>
<Col span={24}> <Col span={24}>
<Space> <Space>
<Button onClick={() => setAccountModalOpen(true)}>{t('account:newAccount')}</Button> <Button onClick={() => onNewAccount()}>{t('account:newAccount')}</Button>
</Space> </Space>
</Col> </Col>
</Row> </Row>

@ -69,6 +69,7 @@ function RoleList() {
['domestic', '国内供应商'], ['domestic', '国内供应商'],
['air-ticket', '机票供应商'], ['air-ticket', '机票供应商'],
['products', '产品价格'], ['products', '产品价格'],
['page', '默认页面'],
]); ]);
const permissionTree = [] const permissionTree = []
@ -107,11 +108,11 @@ function RoleList() {
const [roleAllList, setRoleAllList] = useState([]) const [roleAllList, setRoleAllList] = useState([])
const [roleForm] = Form.useForm() const [roleForm] = Form.useForm()
const [saveOrUpdateRole, newRole] = const [saveOrUpdateRole, newEmptyRole] =
useAccountStore((state) => useAccountStore((state) =>
[state.saveOrUpdateRole, state.newRole]) [state.saveOrUpdateRole, state.newEmptyRole])
const { notification, modal } = App.useApp() const { notification } = App.useApp()
const onRoleSeleted = (role) => { const onRoleSeleted = (role) => {
fetchPermissionListByRoleId({ role_id: role.role_id }) fetchPermissionListByRoleId({ role_id: role.role_id })
@ -123,7 +124,7 @@ function RoleList() {
} }
const onNewRole = () => { const onNewRole = () => {
const role = newRole() const role = newEmptyRole()
roleForm.setFieldsValue(role) roleForm.setFieldsValue(role)
setRoleModalOpen(true) setRoleModalOpen(true)
} }

Loading…
Cancel
Save