fix: align permission-gated UI actions
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user