权限管理

This commit is contained in:
LittleBoy 2024-08-28 17:25:11 +08:00
parent 7c84803b36
commit 2939228cfa
14 changed files with 231 additions and 121 deletions

View File

@ -0,0 +1,22 @@
import React from "react";
import useAuth from "@/hooks/useAuth.ts";
type PermissionKeys =
'apply_button'
| 'apply_page'
| 'bill_page'
| 'complete_button'
| 'confirm_button'
| 'manual_payment'
| 'permission_edit';
type PermissionCheckProps = {
permission: PermissionKeys ;
children: React.ReactNode;
}
export const PermissionCheck: React.FC<PermissionCheckProps> = ({permission, children}) => {
const {user} = useAuth()
return <>
{user && user.permissions && user.permissions[permission] ? children : null}
</>
}

View File

@ -1,4 +1,4 @@
import {useEffect, useState} from "react";
import {useRequest} from "ahooks";
function getRemoteUserNameList() {
return new Promise<string[][]>((resolve, reject) => {
@ -23,13 +23,18 @@ function getRemoteUserNameList() {
}
export function useRemoteUserList() {
const loadUserList = async () => {
const list = await getRemoteUserNameList();
return list.flat();
}
const {data:usernameList} = useRequest(loadUserList)
// const [userList, setUserList] = useState<string[]>([])
//const [usernameList, setUserList] = useState<string[]>([])
const [usernameList, setUserList] = useState<string[]>([])
useEffect(()=>{
getRemoteUserNameList().then(data=>{
setUserList(data.flat())
})
},[])
// useEffect(()=>{
// getRemoteUserNameList().then(data=>{
// setUserList(data.flat())
// })
// },[])
return usernameList
}

View File

@ -21,6 +21,8 @@
"query_bill": "Failed to query bill:",
"remove": "Remove",
"save": "Save",
"save_failed": "Save failed",
"save_success": "Saved successfully",
"select_excel_file": "Select File",
"select_upload_file": "Select File",
"student_number": "Student Number",
@ -162,7 +164,8 @@
"permission": {
"message": {
"empty_tips": "No data, please create new",
"error_require": "Please set your username or role"
"error_require": "Please set your username or role",
"username_exist": "Username already exists"
},
"title": {
"add": "New",

View File

@ -21,6 +21,8 @@
"query_bill": "查询账单失败:",
"remove": "删除",
"save": "保存",
"save_failed": "保存失败",
"save_success": "保存成功",
"select_excel_file": "选择文件",
"select_upload_file": "选择文件",
"student_number": "学号",
@ -162,7 +164,8 @@
"permission": {
"message": {
"empty_tips": "暂无数据,请新增",
"error_require": "请设置用户名或角色"
"error_require": "请设置用户名或角色",
"username_exist": "用户名已经存在了"
},
"title": {
"add": "新增",

View File

@ -21,6 +21,8 @@
"query_bill": "查詢帳單失敗:",
"remove": "刪除",
"save": "儲存",
"save_failed": "保存失敗",
"save_success": "保存成功",
"select_excel_file": "選擇文件",
"select_upload_file": "選擇文件",
"student_number": "學號",
@ -162,7 +164,8 @@
"permission": {
"message": {
"empty_tips": "暫無數據,請新增",
"error_require": "請設定使用者名稱或角色"
"error_require": "請設定使用者名稱或角色",
"username_exist": "使用者名稱已經存在了"
},
"title": {
"add": "新增",

View File

@ -3,11 +3,16 @@ import {Button, Checkbox, Empty, Popconfirm, Select, Space, Toast} from "@douyin
import {useTranslation} from "react-i18next";
import {useEffect, useMemo} from "react";
import './permission.less';
import {Card} from "@/components/card";
import './permission.less';
import {useRemoteUserList} from "@/hooks/useRemoteUserList.ts";
import {getUserPermissionList, removeUserPermission} from "@/service/api/user.ts";
import {
createUserPermission,
getUserPermissionList,
removeUserPermission,
updateUserPermission
} from "@/service/api/user.ts";
import useAuth from "@/hooks/useAuth.ts";
// const PermissionList = [
@ -20,28 +25,49 @@ import {getUserPermissionList, removeUserPermission} from "@/service/api/user.ts
// 'permission'
// ]
const UserPermissionItem = ({it, onChange, usernameOptionList}: {
type UserPermissionItemProps = {
it: UserPermission;
onChange: (action:'remove'|'saved'|'modify',value:UserPermission) => void;
onChange: (action: 'remove' | 'saved' | 'modify', value: UserPermission) => Promise<void>;
usernameOptionList: OptionValue[]
}) => {
roleOptionList: OptionValue[]
}
const UserPermissionItem = ({it, onChange, usernameOptionList,roleOptionList}: UserPermissionItemProps) => {
const {t} = useTranslation()
const {user} = useAuth()
const [state,setState] = useSetState({
loading: false
})
const onValueChange = (value: {
[key:string]:string|boolean
})=>{
onChange('modify',{
[key: string]: string | boolean
}) => {
onChange('modify', {
...it,
...value
})
}).then();
}
const onSave = ()=>{
const onRemove = () => {
if(it.id > 0){
setState({loading: true})
onChange('remove', it).finally(()=>{
setState({loading: false})
})
return;
}
onChange('remove', it).then();
}
const onSave = () => {
console.log(it)
if(!it.role || !it.username){
if (!it.role || !it.username) {
Toast.warning(t('permission.message.error_require'))
return;
}
setState({loading: true})
onChange('saved', it).finally(()=>{
setState({loading: false})
})
}
// const [values,setValues] = use
return (<div className="table-row">
<div className="item item-username item-type">
@ -60,11 +86,7 @@ const UserPermissionItem = ({it, onChange, usernameOptionList}: {
<div className="form-item">
<Select
style={{width: '100%'}}
optionList={[
{label: 'ROOT', value: 'root'},
{label: 'RO', value: 'ro'},
{label: 'FO', value: 'fo'},
]}
optionList={roleOptionList}
defaultValue={it.role}
onChange={(value) => onValueChange({role: String(value)})}
placeholder={t('base.please_select')}
@ -104,18 +126,20 @@ const UserPermissionItem = ({it, onChange, usernameOptionList}: {
<div className={`item item-type item-type-bill-permission`}>
<Checkbox
defaultChecked={it.permission_edit}
disabled={user?.username == it.username}
onChange={e => onValueChange({permission_edit: !!e.target.checked})}/>
</div>
<div className="item item-operation text-center">
<Space>
<Popconfirm
title={t('base.warning')} onConfirm={() => onChange('remove',it)}
position={'topRight'}
content={`${t('base.confirm_delete')}?`}
>
<Button theme={'solid'} type="danger" size={'small'}>{t('base.delete')}</Button>
</Popconfirm>
<Button size={'small'} theme={'solid'} onClick={onSave}>{t('base.save')}</Button>
{user?.username != it.username && <Popconfirm
disabled={state.loading}
title={t('base.warning')} onConfirm={onRemove}
position={'topRight'}
content={`${t('base.confirm_delete')}?`}
>
<Button theme={'solid'} type="danger" size={'small'}>{t('base.delete')}</Button>
</Popconfirm>}
<Button loading={state.loading} size={'small'} theme={'solid'} onClick={onSave}>{t('base.save')}</Button>
</Space>
</div>
</div>)
@ -124,19 +148,47 @@ const UserPermissionItem = ({it, onChange, usernameOptionList}: {
const Permission = () => {
const {t} = useTranslation()
const usernameList = useRemoteUserList();
const {user} = useAuth();
const [state, setState] = useSetState<{
list: UserPermission[];
allList: UserPermission[];
loading?: boolean;
}>({
list: []
list: [],
allList:[]
})
const usernameOptionList = useMemo(() => (usernameList.map(name => ({label: name, value: name}))), [usernameList])
const usernameOptionList = useMemo(() => (usernameList?usernameList.map(name => ({label: name, value: name})):[]), [usernameList])
const roleOptionList = useMemo(() => {
const list = [
{label: 'ROOT', value: 'root'},
{label: 'RO', value: 'ro'},
{label: 'FO', value: 'fo'},
];
const userRole = user?.permissions?.role ?? 'staff';
if(userRole == 'root') return list;
return list//.filter(it => (it.value == userRole));
}, [])
// load user permission list
const loadUserPermissionList = () => {
setState({loading: true})
getUserPermissionList().then(list => {
setState({allList:[...list]})
const userRole = user?.permissions?.role ?? 'staff';
list.forEach(it=>{
it.permission_edit = !!it.permission_edit
it.bill_page = !!it.bill_page
it.apply_page = !!it.apply_page
it.complete_button = !!it.complete_button
it.confirm_button = !!it.confirm_button
it.apply_button = !!it.apply_button
it.manual_payment = !!it.manual_payment
})
if(userRole != 'root') {
list = list.filter(it => (it.role == userRole));
}
setState({loading: false, list})
})
}
@ -144,7 +196,7 @@ const Permission = () => {
// remove a user permission
const removeItem = (index: number, id: number) => {
const removeFromList = ()=>{
const removeFromList = () => {
const newList = [...state.list];
newList.splice(index, 1)
setState({list: newList})
@ -155,28 +207,6 @@ const Permission = () => {
}
removeFromList();
}
// const onUsernameChange = (index: number, name: string, role: string) => {
// const newList = [...state.list];
// newList[index].username = name;
// newList[index].role = role;
// setState({list: newList})
// }
// const onPermissionChange = (_index: number, name: string, checked?: boolean) => {
// const newList = [...state.list];
// const index = newList[_index].permissions.indexOf(name)
// if (index == -1) { // 没有找到
// if (checked) { // 已经选中
// newList[index].permissions.push(name)
// }
// } else {
// if (!checked) { // 已经选中
// newList[index].permissions.splice(index, 1)
// }
// }
// setState({list: newList})
// }
const addNewRecord = () => {
setState({
list: [...state.list, {
@ -194,14 +224,34 @@ const Permission = () => {
})
}
const handleChange = (action:'remove'|'saved'|'modify',value:UserPermission,index:number) =>{
if(action == 'remove') removeItem(index,value.id)
else if(action == 'modify'){
const handleChange = async (action: 'remove' | 'saved' | 'modify', value: UserPermission, index: number) => {
if (action == 'remove') removeItem(index, value.id)
else if (action == 'modify') {
const newList = [...state.list];
newList[index] = {
...value
}
setState({list: newList})
} else if (action == 'saved') {
// 判断是否存在同名用户
const exist = state.list.filter(it=>it.id > 0).find(it => it.username == value.username && it.id != value.id)
if (exist) {
Toast.error(t('permission.message.username_exist'))
return;
}
const process = value.id > 0 ? updateUserPermission : createUserPermission;
try {
await process(value).then((newValue) => {
Toast.success(t('base.save_success'))
const newList = [...state.list];
newList[index] = {
...newValue
}
setState({list: newList})
})
} catch (e) {
Toast.error(t('base.save_failed') + `(${(e as Error).message})`)
}
}
}
@ -223,20 +273,24 @@ const Permission = () => {
<div className="item item-operation">{t('bill.title_operate')}</div>
</div>
{state.list.map((it, index) => (<UserPermissionItem
key={index} it={it} onChange={(action,value) => handleChange(action,value,index)}
key={index} it={it}
onChange={async (action, value) => {
await handleChange(action, value, index)
}}
usernameOptionList={usernameOptionList}
roleOptionList={roleOptionList}
/>))}
{state.list.length == 0 && <div style={{backgroundColor:'#fafafa'}}>
{state.list.length == 0 && <div style={{backgroundColor: '#fafafa'}}>
<Empty
description={t('permission.message.empty_tips')}
style={{paddingBottom:20}}
style={{paddingBottom: 20}}
/>
</div>}
</div>}
</div>
</div>
<Space style={{marginTop: 20}}>
<Button style={{width: 100}} onClick={addNewRecord} theme={'solid'}>{t('permission.title.add')}</Button>
</Space>
{!state.loading && <Space style={{marginTop: 20}}>
<Button style={{width: 100}} onClick={addNewRecord} theme={'solid'}>{t('permission.title.add')}</Button>
</Space>}
</Card>)
}
export default Permission

View File

@ -118,15 +118,17 @@ export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
<div className="import-record-wrapper">
<div className="table-list">
<table>
{records.map((record, index) => {
return <tr key={index}>
{record.map((item, index) => {
return <td key={index}>
<div className={'import-record-item'}>{item}</div>
</td>
})}
</tr>
})}
<tbody>
{records.map((record, index) => {
return <tr key={index}>
{record.map((item, index) => {
return <td key={index}>
<div className={'import-record-item'}>{item}</div>
</td>
})}
</tr>
})}
</tbody>
</table>
</div>
</div>

View File

@ -16,6 +16,7 @@ import {BillTypeConfirmModal} from "@/pages/bill/components/bill_type_confirm.ts
import {BillTypeConfirmBatch} from "@/pages/bill/components/bill_type_confirm_batch.tsx";
import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx";
import {ImportBillModal} from "@/pages/bill/components/import_bill_modal.tsx";
import {PermissionCheck} from "@/components/permission";
const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
@ -29,6 +30,7 @@ const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
// }
const BillQuery = () => {
// const {createPDF,downloadPDF} = useDownloadReceiptPDF()
const [state, setState] = useSetState<{
updateBill?: BillModel;
@ -88,10 +90,13 @@ const BillQuery = () => {
// }
const operation = (bill: BillModel) => {
return (<div className={'table-operation-render'}>
{bill.status != BillStatus.PAID &&
<Button onClick={() => setState({updateBill: bill})} size={'small'} theme={'solid'}
type={'danger'}>{t('bill.paid')}</Button>
}
<PermissionCheck permission={'complete_button'}>
{bill.status != BillStatus.PAID &&
<Button onClick={() => setState({updateBill: bill})} size={'small'} theme={'solid'}
type={'danger'}>{t('bill.paid')}</Button>
}
</PermissionCheck>
{bill.status == BillStatus.PENDING && <>
<Popconfirm
title={'Notice'} onConfirm={() => onCancelBill(bill)} position={'topRight'}
@ -109,16 +114,22 @@ const BillQuery = () => {
<DownloadButton bill={bill} text={t('bill.download_receipt')}/>
{/*<Button*/}
{/* onClick={() => showBillPDF(bill)} size={'small'}>download pdf</Button>*/}
{bill.confirm_status == 'UNCONFIRMED' ? <Button
onClick={() => setState({confirmBill: bill})}
size={'small'} theme={'solid'}
type={'primary'}>{t('bill.confirm_bill_type')}</Button>
: <Popconfirm
title={'Warning'} onConfirm={() => onCancelBillConfirm(bill.id)} position={'topRight'}
content={`${t('base.confirm_next_operation')}?`}
>
<Button size={'small'} theme={'solid'} type={'danger'}>{t('bill.btn_cancel_confirm')}</Button>
</Popconfirm>}
<PermissionCheck permission={'confirm_button'}>
{
bill.confirm_status == 'UNCONFIRMED'
? <Button
onClick={() => setState({confirmBill: bill})}
size={'small'} theme={'solid'}
type={'primary'}>{t('bill.confirm_bill_type')}</Button>
: <Popconfirm
title={'Warning'} onConfirm={() => onCancelBillConfirm(bill.id)} position={'topRight'}
content={`${t('base.confirm_next_operation')}?`}
>
<Button size={'small'} theme={'solid'} type={'danger'}>{t('bill.btn_cancel_confirm')}</Button>
</Popconfirm>
}
</PermissionCheck>
</>
}
</div>)

View File

@ -7,8 +7,10 @@ import SearchForm from "@/components/bill/search-form.tsx";
import {BillList} from "@/components/bill/list.tsx";
import {billList, BillQueryParams, confirmBills} from "@/service/api/bill.ts";
import {BizError} from "@/service/types.ts";
import useAuth from "@/hooks/useAuth.ts";
const BillReconciliation = () => {
const {user} = useAuth()
const {t} = useTranslation()
const [queryParams, setBillQueryParams] = useState<BillQueryParams>({
apply_status: 'UNCHECKED'
@ -108,7 +110,9 @@ const BillReconciliation = () => {
/>
<BillList
source={data} type={'reconciliation'}
operationRender={queryParams.apply_status == 'CHECKED' ? undefined : operation}
operationRender={queryParams.apply_status == 'CHECKED' ? undefined : (
user && user.permissions?.apply_button ? operation : undefined
)}
beforeTotalAmount={<div>{queryParams.apply_status != 'CHECKED' && (
(selectKeys.length == 0) ? <Button theme={'solid'} disabled style={{marginRight: 10}}>
{t('bill.confirm_batch')}

View File

@ -10,24 +10,25 @@ export const AllDashboardMenu = [
key: 'manual',
icon: <IconQRCode/>,
path: '/dashboard/manual',
permission: 'manual_payment',
},
{
key: 'bill',
icon: <IconQuery/>,
path: '/dashboard/bill',
role: ['root', 'ro', 'fo']
permission: 'bill_page',
},
{
key: 'check',
icon: <IconReconciliation/>,
path: '/dashboard/reconciliation',
role: ['root', 'fo']
permission: 'apply_page',
},
{
key: 'permission',
icon: <IconPermission/>,
path: '/dashboard/permission',
role: ['root']
permission: 'permission_edit',
}
]
@ -36,10 +37,11 @@ export function DashboardNavigation() {
const {user} = useAuth();
const navItems = useMemo(() => {
if (!user) return [];
if (!user || !user.permissions) return [];
return AllDashboardMenu.filter(it => {
return !it.role || it.role.includes(user.role)
if(user.permissions && user.permissions[it.permission]) return true;
return false
});
}, [user])
return (<div className={'dashboard-menu-container'}>

View File

@ -63,7 +63,7 @@ export function addBillRecord(bill: CreateBillRecordModel) {
}
export function uploadBillingRecordFile(file: File, payment_channel: string, check_student = 'true') {
return uploadFile('/bill/import', file, {payment_channel, check_student})
return uploadFile('/bills/import', file, {payment_channel, check_student})
}
export function getAsiaPayData(id: number) {

View File

@ -1,4 +1,4 @@
import {get, post, remove} from "@/service/request.ts";
import {get, post, put, remove} from "@/service/request.ts";
export function getUserInfo() {
return get<UserProfile>('/userinfo')
@ -24,11 +24,11 @@ export function getUserPermissionList(){
return get<UserPermission[]>('/permissions')
}
export function createUserPermission(){
return get<UserPermission[]>('/permissions')
export function createUserPermission(data:UserPermission){
return post<UserPermission>('/permission',data)
}
export function updateUserPermission(){
return get<UserPermission[]>('/permissions')
export function updateUserPermission(data:UserPermission){
return put<UserPermission>(`/permissions/${data.id}`,data)
}
export function removeUserPermission(id:number){
return remove(`/permissions/${id}`)

View File

@ -95,7 +95,7 @@ export function uploadFile<T>(url: string, file: UploadFileModel | File, data: A
})
}
return request<T>(url, 'post', data, returnOrigin, {
return request<T>(url, 'post', formData, returnOrigin, {
'Content-Type': 'multipart/form-data'
})
// return new Promise<T>((resolve, reject) => {

27
src/types/auth.d.ts vendored
View File

@ -1,5 +1,18 @@
declare type UserRole = 'root' | 'ro' | 'fo' | 'staff'
declare type UserPermission = {
id: number;
username: string;
role: string;
manual_payment: boolean;
bill_page: boolean;
apply_page: boolean;
complete_button: boolean;
confirm_button: boolean;
apply_button: boolean;
permission_edit: boolean;
}
declare type UserProfile = {
id: string | number;
token: string;
@ -14,6 +27,7 @@ declare type UserProfile = {
type: string;
roles: UserRole[];
role: UserRole;
permissions:UserPermission | null;
origin_role?: UserRole;
}
@ -37,17 +51,4 @@ declare type AuthContextType = {
declare type PermissionUserList = {
role_name: string;
username_list: string[];
}
declare type UserPermission = {
id: number;
username: string;
role: string;
manual_payment: boolean;
bill_page: boolean;
apply_page: boolean;
complete_button: boolean;
confirm_button: boolean;
apply_button: boolean;
permission_edit: boolean;
}