fix: align permission-gated UI actions

This commit is contained in:
2026-07-23 11:32:13 +08:00
parent adf738288f
commit c98d37307e
28 changed files with 1340 additions and 718 deletions

View File

@@ -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}