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

@@ -37,6 +37,8 @@ import { maskPhone, maskIdNumber } from '../../utils/sensitive';
import { useViewSensitive } from '../../hooks/useViewSensitive';
import { message } from '../../ui/app-message';
import EditableCell from '../EditableCell';
import { usePermission } from '../../hooks/usePermission';
import PermissionButton from '../PermissionButton';
// ---- Types ----
@@ -318,7 +320,19 @@ const InlineArchiveSummary: React.FC<{
organizations: Array<{ id: number; name: string }>;
onRefresh: () => void;
onViewSensitive: (fieldLabel: string, value: string) => void;
}> = ({ studentId, student, profile, result, organizations, onRefresh, onViewSensitive }) => {
canViewSensitive: boolean;
canChooseOrganization: boolean;
}> = ({
studentId,
student,
profile,
result,
organizations,
onRefresh,
onViewSensitive,
canViewSensitive,
canChooseOrganization,
}) => {
const saveStudent = async (field: keyof StudentInfo, value: unknown) => {
await api.put(`/students/${studentId}`, { [field]: value });
message.success('学生资料已保存');
@@ -356,9 +370,11 @@ const InlineArchiveSummary: React.FC<{
{student.phone ? (
<span>
<span style={{ marginRight: 8 }}>{maskPhone(student.phone)}</span>
<a onClick={() => onViewSensitive('电话', student.phone)}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
{canViewSensitive ? (
<a onClick={() => onViewSensitive('电话', student.phone)}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
) : null}
</span>
) : (
'-'
@@ -402,9 +418,11 @@ const InlineArchiveSummary: React.FC<{
{student.idNumber ? (
<span>
<span style={{ marginRight: 8 }}>{maskIdNumber(student.idNumber)}</span>
<a onClick={() => onViewSensitive('身份证号', student.idNumber)}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
{canViewSensitive ? (
<a onClick={() => onViewSensitive('身份证号', student.idNumber)}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
) : null}
</span>
) : (
'-'
@@ -438,9 +456,11 @@ const InlineArchiveSummary: React.FC<{
{student.emergencyPhone ? (
<span>
<span style={{ marginRight: 8 }}>{maskPhone(student.emergencyPhone)}</span>
<a onClick={() => onViewSensitive('紧急联系人电话', student.emergencyPhone || '')}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
{canViewSensitive ? (
<a onClick={() => onViewSensitive('紧急联系人电话', student.emergencyPhone || '')}>
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
</a>
) : null}
</span>
) : (
'-'
@@ -448,15 +468,25 @@ const InlineArchiveSummary: React.FC<{
</EditableCell>
</Descriptions.Item>
<Descriptions.Item label="所属机构">
<EditableCell
value={student.organizationId}
editor="select"
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
permission="student:edit"
onSave={(next) => saveStudent('organizationId', next)}
>
{student.organization?.name ? <Tag color="purple">{student.organization.name}</Tag> : '-'}
</EditableCell>
{canChooseOrganization ? (
<EditableCell
value={student.organizationId}
editor="select"
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
permission="student:edit"
onSave={(next) => saveStudent('organizationId', next)}
>
{student.organization?.name ? (
<Tag color="purple">{student.organization.name}</Tag>
) : (
'-'
)}
</EditableCell>
) : student.organization?.name ? (
<Tag color="purple">{student.organization.name}</Tag>
) : (
'-'
)}
</Descriptions.Item>
<Descriptions.Item label="负责人">
<EditableCell
@@ -604,6 +634,7 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
studentId,
onRefresh,
}) => {
const { hasPermission } = usePermission();
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
@@ -760,7 +791,8 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
return (
<div>
<Button
<PermissionButton
permission="student:edit"
icon={<PlusOutlined />}
type="primary"
onClick={() => {
@@ -770,7 +802,7 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
style={{ marginBottom: 16 }}
>
</Button>
</PermissionButton>
<Table<EnrollmentRecord>
columns={columns}
dataSource={data}
@@ -783,8 +815,8 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
/>
<Modal
title="添加报读记录"
open={modalOpen}
onOk={handleAdd}
open={modalOpen && hasPermission('student:edit')}
onOk={hasPermission('student:edit') ? handleAdd : undefined}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
@@ -827,6 +859,7 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
const ExamScoresTab: React.FC<
TabProps & { data: ExamScoreRecord[]; enrollments: EnrollmentRecord[] }
> = ({ data, studentId, enrollments, onRefresh }) => {
const { hasPermission } = usePermission();
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
@@ -995,7 +1028,8 @@ const ExamScoresTab: React.FC<
return (
<div>
<Button
<PermissionButton
permission="student:edit"
icon={<PlusOutlined />}
type="primary"
onClick={() => {
@@ -1005,7 +1039,7 @@ const ExamScoresTab: React.FC<
style={{ marginBottom: 16 }}
>
</Button>
</PermissionButton>
<Table<ExamScoreRecord>
columns={columns}
dataSource={data}
@@ -1018,8 +1052,8 @@ const ExamScoresTab: React.FC<
/>
<Modal
title="添加考试成绩"
open={modalOpen}
onOk={handleAdd}
open={modalOpen && hasPermission('student:edit')}
onOk={hasPermission('student:edit') ? handleAdd : undefined}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
@@ -1074,6 +1108,7 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
studentId,
onRefresh,
}) => {
const { hasPermission } = usePermission();
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
@@ -1183,7 +1218,8 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
return (
<div>
<Button
<PermissionButton
permission="student:edit"
icon={<PlusOutlined />}
type="primary"
onClick={() => {
@@ -1193,7 +1229,7 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
style={{ marginBottom: 16 }}
>
</Button>
</PermissionButton>
<Table<LearningRecord>
columns={columns}
dataSource={data}
@@ -1206,8 +1242,8 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
/>
<Modal
title="添加学情记录"
open={modalOpen}
onOk={handleAdd}
open={modalOpen && hasPermission('student:edit')}
onOk={hasPermission('student:edit') ? handleAdd : undefined}
onCancel={() => setModalOpen(false)}
confirmLoading={saving}
>
@@ -1250,6 +1286,7 @@ const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({
studentId,
onRefresh,
}) => {
const { hasPermission } = usePermission();
const [uploading, setUploading] = useState(false);
const handleDelete = async (attachmentId: number) => {
@@ -1294,11 +1331,13 @@ const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({
>
</Button>
<Popconfirm title="确定归档该附件?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" danger icon={<InboxOutlined />}>
</Button>
</Popconfirm>
{hasPermission('student:edit') ? (
<Popconfirm title="确定归档该附件?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" danger icon={<InboxOutlined />}>
</Button>
</Popconfirm>
) : null}
</Space>
),
},
@@ -1306,37 +1345,39 @@ const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({
return (
<div>
<Upload
showUploadList={false}
customRequest={async (options) => {
const formData = new FormData();
formData.append(
'file',
options.file instanceof File
? options.file
: new File([options.file as Blob], 'attachment'),
);
setUploading(true);
try {
await api.post(`/archive/${studentId}/attachments`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
message.success('上传成功');
options.onSuccess?.({});
onRefresh();
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : '上传失败';
message.error(msg);
options.onError?.(e instanceof Error ? e : new Error(msg));
} finally {
setUploading(false);
}
}}
>
<Button icon={<UploadOutlined />} loading={uploading}>
</Button>
</Upload>
{hasPermission('student:edit') ? (
<Upload
showUploadList={false}
customRequest={async (options) => {
const formData = new FormData();
formData.append(
'file',
options.file instanceof File
? options.file
: new File([options.file as Blob], 'attachment'),
);
setUploading(true);
try {
await api.post(`/archive/${studentId}/attachments`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
message.success('上传成功');
options.onSuccess?.({});
onRefresh();
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : '上传失败';
message.error(msg);
options.onError?.(e instanceof Error ? e : new Error(msg));
} finally {
setUploading(false);
}
}}
>
<Button icon={<UploadOutlined />} loading={uploading}>
</Button>
</Upload>
) : null}
<Table<AttachmentRecord>
columns={columns}
dataSource={data}
@@ -1359,6 +1400,10 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
inDrawer,
onClose,
}) => {
const { hasPermission, hasAnyPermission } = usePermission();
const canViewOrganizations = hasPermission('organization:view');
const canChooseOrganization =
canViewOrganizations && hasAnyPermission('student:create', 'student:edit');
const [aggregateData, setAggregateData] = useState<StudentProfileAggregate | null>(null);
const [organizations, setOrganizations] = useState<Array<{ id: number; name: string }>>([]);
const [loading, setLoading] = useState(false);
@@ -1381,13 +1426,17 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
}, [fetchData]);
useEffect(() => {
if (!canViewOrganizations) {
setOrganizations([]);
return;
}
api
.get('/organizations', { params: { includeArchived: 'false' } })
.then((res: unknown) => {
setOrganizations(res as Array<{ id: number; name: string }>);
})
.catch(() => {});
}, []);
}, [canViewOrganizations]);
const handlePreviewReport = useCallback(async () => {
try {
@@ -1470,7 +1519,8 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
<Space>
<Button type="text" icon={<CloseOutlined />} onClick={onClose} aria-label="关闭档案" />
<span style={{ fontSize: 16, fontWeight: 500 }}>
- {student.name}{student.studentNo ? ` (${student.studentNo})` : ''}
- {student.name}
{student.studentNo ? ` (${student.studentNo})` : ''}
</span>
</Space>
<Space>
@@ -1507,6 +1557,8 @@ const StudentProfileContent: React.FC<StudentProfileContentProps> = ({
organizations={organizations}
onRefresh={fetchData}
onViewSensitive={handleViewSensitive}
canViewSensitive={hasPermission('log:create')}
canChooseOrganization={canChooseOrganization}
/>
<Tabs defaultActiveKey="enrollments" items={tabItems} />