forked from wangziqi/gongxue-base
fix: align permission-gated UI actions
This commit is contained in:
@@ -38,6 +38,7 @@ import EditableCell from '../../components/EditableCell';
|
||||
import JinshujuMatchModal from '../../components/JinshujuMatchModal';
|
||||
import { maskIdNumber, maskPhone } from '../../utils/sensitive';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '在读', color: 'green' },
|
||||
@@ -84,6 +85,10 @@ interface StudentFilterLookups {
|
||||
|
||||
const StudentsPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission, hasAnyPermission } = usePermission();
|
||||
const canViewOrganizations = hasPermission('organization:view');
|
||||
const canChooseOrganization =
|
||||
canViewOrganizations && hasAnyPermission('student:create', 'student:edit');
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
@@ -189,12 +194,17 @@ const StudentsPage: React.FC = () => {
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get('/organizations', { params: { includeArchived: 'false' } })
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
if (canViewOrganizations) {
|
||||
api
|
||||
.get('/organizations', { params: { includeArchived: 'false' } })
|
||||
.then((res: unknown) => {
|
||||
setOrganizations(res as Array<{ id: number; name: string }>);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
setOrganizations([]);
|
||||
setFilterOrganizationId(undefined);
|
||||
}
|
||||
api
|
||||
.get<StudentFilterLookups>('/students/filter-lookups')
|
||||
.then((res) => {
|
||||
@@ -202,7 +212,7 @@ const StudentsPage: React.FC = () => {
|
||||
setTeacherOptions(res.teachers || []);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
}, [canViewOrganizations]);
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
@@ -417,15 +427,17 @@ const StudentsPage: React.FC = () => {
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ marginRight: 4 }}>{maskPhone(v)}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '电话', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
{hasPermission('log:create') ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '电话', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -454,15 +466,17 @@ const StudentsPage: React.FC = () => {
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ marginRight: 4 }}>{maskIdNumber(v)}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '身份证号', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
{hasPermission('log:create') ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '身份证号', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -506,15 +520,17 @@ const StudentsPage: React.FC = () => {
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ marginRight: 4 }}>{maskPhone(v)}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '紧急联系人电话', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
{hasPermission('log:create') ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: '8px 4px', flex: 'none' }}
|
||||
onClick={() => handleViewSensitive(record.id, '紧急联系人电话', v)}
|
||||
title="点击查看完整号码"
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -523,28 +539,33 @@ const StudentsPage: React.FC = () => {
|
||||
title: '所属机构',
|
||||
dataIndex: 'organization',
|
||||
width: 100,
|
||||
render: (organization: { name?: string } | null, record: any) => (
|
||||
<EditableCell
|
||||
value={record.organizationId}
|
||||
editor="select"
|
||||
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
|
||||
permission="student:edit"
|
||||
disabled={record.status === 'archived'}
|
||||
required
|
||||
onSave={(next) => saveCell(record, 'organizationId', next)}
|
||||
>
|
||||
{organization?.name ? (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{organization.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
),
|
||||
render: (organization: { name?: string } | null, record: any) =>
|
||||
canChooseOrganization ? (
|
||||
<EditableCell
|
||||
value={record.organizationId}
|
||||
editor="select"
|
||||
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
|
||||
permission="student:edit"
|
||||
disabled={record.status === 'archived'}
|
||||
required
|
||||
onSave={(next) => saveCell(record, 'organizationId', next)}
|
||||
>
|
||||
{organization?.name ? (
|
||||
<Tag
|
||||
color="purple"
|
||||
style={{ maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{organization.name}
|
||||
</Tag>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</EditableCell>
|
||||
) : organization?.name ? (
|
||||
<Tag color="purple">{organization.name}</Tag>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
@@ -649,7 +670,15 @@ const StudentsPage: React.FC = () => {
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleViewSensitive, openDrawer, showArchived, organizations, saveCell],
|
||||
[
|
||||
handleViewSensitive,
|
||||
openDrawer,
|
||||
showArchived,
|
||||
organizations,
|
||||
saveCell,
|
||||
hasPermission,
|
||||
canChooseOrganization,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -679,21 +708,23 @@ const StudentsPage: React.FC = () => {
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="所属机构"
|
||||
allowClear
|
||||
style={{ width: 140 }}
|
||||
value={filterOrganizationId}
|
||||
onChange={(v) => {
|
||||
setFilterOrganizationId(v);
|
||||
}}
|
||||
>
|
||||
{organizations.map((t: { id: number; name: string }) => (
|
||||
<Select.Option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{canViewOrganizations ? (
|
||||
<Select
|
||||
placeholder="所属机构"
|
||||
allowClear
|
||||
style={{ width: 140 }}
|
||||
value={filterOrganizationId}
|
||||
onChange={(v) => {
|
||||
setFilterOrganizationId(v);
|
||||
}}
|
||||
>
|
||||
{organizations.map((t: { id: number; name: string }) => (
|
||||
<Select.Option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
) : null}
|
||||
<Select
|
||||
placeholder="所属班级"
|
||||
allowClear
|
||||
@@ -765,22 +796,26 @@ const StudentsPage: React.FC = () => {
|
||||
>
|
||||
添加学生
|
||||
</PermissionButton>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={handleCreateStudentsImport}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={handleUpdateExistingStudentsImport}
|
||||
>
|
||||
<Button icon={<SwapOutlined />}>更新已有学生资料</Button>
|
||||
</Upload>
|
||||
{hasPermission('student:import') ? (
|
||||
<>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={handleCreateStudentsImport}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={handleUpdateExistingStudentsImport}
|
||||
>
|
||||
<Button icon={<SwapOutlined />}>更新已有学生资料</Button>
|
||||
</Upload>
|
||||
</>
|
||||
) : null}
|
||||
<PermissionButton
|
||||
permission="student:edit"
|
||||
permission="sync:read"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={() => setJinshujuOpen(true)}
|
||||
>
|
||||
@@ -934,23 +969,27 @@ const StudentsPage: React.FC = () => {
|
||||
<Form.Item name="emergencyPhone" label="紧急联系人电话">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="organizationId"
|
||||
label="所属机构"
|
||||
rules={[{ required: true, message: '请选择所属机构' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择所属机构"
|
||||
options={organizations.map(
|
||||
(organization: { id: number; name: string; isHost?: boolean }) => ({
|
||||
value: organization.id,
|
||||
label: organization.isHost ? `${organization.name}(本机构)` : organization.name,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
{canChooseOrganization ? (
|
||||
<Form.Item
|
||||
name="organizationId"
|
||||
label="所属机构"
|
||||
rules={[{ required: true, message: '请选择所属机构' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择所属机构"
|
||||
options={organizations.map(
|
||||
(organization: { id: number; name: string; isHost?: boolean }) => ({
|
||||
value: organization.id,
|
||||
label: organization.isHost
|
||||
? `${organization.name}(本机构)`
|
||||
: organization.name,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item name="supervisor" label="负责人/班主任">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
@@ -968,11 +1007,16 @@ const StudentsPage: React.FC = () => {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<JinshujuMatchModal
|
||||
open={jinshujuOpen}
|
||||
onClose={() => setJinshujuOpen(false)}
|
||||
onApplied={() => { setJinshujuOpen(false); fetchData(); }}
|
||||
/>
|
||||
{hasPermission('sync:read') ? (
|
||||
<JinshujuMatchModal
|
||||
open={jinshujuOpen}
|
||||
onClose={() => setJinshujuOpen(false)}
|
||||
onApplied={() => {
|
||||
setJinshujuOpen(false);
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Drawer
|
||||
title={null}
|
||||
open={drawerOpen}
|
||||
|
||||
Reference in New Issue
Block a user