feat(web): 健康模块 13 页面按钮级权限控制 — AuthButton 包装
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

使用 AuthButton 声明式组件包装健康模块全部操作按钮:
- health.patient.manage: PatientList/PatientDetail/PatientTagManage
- health.appointment.manage: AppointmentList
- health.doctor.manage: DoctorList/DoctorSchedule
- health.follow-up.manage: FollowUpTaskList
- health.consultation.manage: ConsultationList/ConsultationDetail
- health.points.manage: OfflineEventList/PointsProductList/PointsOrderList/PointsRuleList
This commit is contained in:
iven
2026-04-25 23:33:32 +08:00
parent 69dcb8fee7
commit 69313a177e
13 changed files with 303 additions and 246 deletions

View File

@@ -26,6 +26,7 @@ import { doctorApi } from '../../api/health/doctors';
import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
import { AuthButton } from '../../components/AuthButton';
/** 预约类型选项 */
const APPOINTMENT_TYPE_OPTIONS = [
@@ -305,19 +306,21 @@ export default function AppointmentList() {
return <span style={{ color: '#999' }}></span>;
}
return (
<Dropdown
menu={{
items: transitions.map((t) => ({
key: t.value,
label: t.label,
onClick: () => handleStatusChange(record, t.value),
})),
}}
>
<Button type="link" size="small">
<DownOutlined />
</Button>
</Dropdown>
<AuthButton code="health.appointment.manage">
<Dropdown
menu={{
items: transitions.map((t) => ({
key: t.value,
label: t.label,
onClick: () => handleStatusChange(record, t.value),
})),
}}
>
<Button type="link" size="small">
<DownOutlined />
</Button>
</Dropdown>
</AuthButton>
);
},
},
@@ -352,9 +355,11 @@ export default function AppointmentList() {
</Space>
</Col>
<Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.appointment.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
</Col>
</Row>

View File

@@ -6,6 +6,7 @@ import { consultationApi, type Session, type Message } from '../../api/health/co
import { StatusTag } from './components/StatusTag';
import { ImagePreview } from './components/ImagePreview';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
const PAGE_SIZE = 30;
@@ -276,21 +277,23 @@ export default function ConsultationDetail() {
</Typography.Text>
)}
{session && !isClosed && (
<Popconfirm
title="确认关闭该咨询会话?"
onConfirm={handleClose}
okText="确认"
cancelText="取消"
>
<Button
size="small"
danger
icon={<CloseCircleOutlined />}
style={{ marginLeft: 'auto' }}
<AuthButton code="health.consultation.manage">
<Popconfirm
title="确认关闭该咨询会话?"
onConfirm={handleClose}
okText="确认"
cancelText="取消"
>
</Button>
</Popconfirm>
<Button
size="small"
danger
icon={<CloseCircleOutlined />}
style={{ marginLeft: 'auto' }}
>
</Button>
</Popconfirm>
</AuthButton>
)}
</div>

View File

@@ -20,6 +20,7 @@ import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
import { ExportButton } from './components/ExportButton';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
const STATUS_OPTIONS = [
{ value: 'waiting', label: '等待中' },
@@ -258,26 +259,28 @@ export default function ConsultationList() {
key: 'actions',
width: 120,
render: (_: unknown, record: Session) => (
<Space size={4}>
{record.status !== 'closed' && (
<Popconfirm
title="确认关闭该咨询会话?"
onConfirm={() => handleClose(record)}
okText="确认"
cancelText="取消"
>
<Button
type="link"
size="small"
danger
icon={<CloseCircleOutlined />}
loading={closingId === record.id}
<AuthButton code="health.consultation.manage">
<Space size={4}>
{record.status !== 'closed' && (
<Popconfirm
title="确认关闭该咨询会话?"
onConfirm={() => handleClose(record)}
okText="确认"
cancelText="取消"
>
</Button>
</Popconfirm>
)}
</Space>
<Button
type="link"
size="small"
danger
icon={<CloseCircleOutlined />}
loading={closingId === record.id}
>
</Button>
</Popconfirm>
)}
</Space>
</AuthButton>
),
},
];
@@ -305,16 +308,18 @@ export default function ConsultationList() {
value={query.status}
onChange={handleFilterChange}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
createForm.resetFields();
setCreateOpen(true);
}}
>
</Button>
<AuthButton code="health.consultation.manage">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
createForm.resetFields();
setCreateOpen(true);
}}
>
</Button>
</AuthButton>
<ExportButton
fetchUrl="/health/consultation-sessions/export"
params={exportParams}

View File

@@ -22,6 +22,7 @@ import {
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { doctorApi, type Doctor, type CreateDoctorReq, type UpdateDoctorReq } from '../../api/health/doctors';
import { AuthButton } from '../../components/AuthButton';
/** 科室选项 — 可后续改为从字典接口获取 */
const DEPARTMENT_OPTIONS = [
@@ -232,26 +233,28 @@ export default function DoctorList() {
width: 140,
fixed: 'right' as const,
render: (_: unknown, record: Doctor) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Popconfirm
title="确定删除该医护?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
<AuthButton code="health.doctor.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
</Popconfirm>
</Space>
<Popconfirm
title="确定删除该医护?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
</AuthButton>
),
},
];
@@ -284,9 +287,11 @@ export default function DoctorList() {
</Space>
</Col>
<Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.doctor.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
</Col>
</Row>

View File

@@ -30,6 +30,7 @@ import {
import { DoctorSelect } from './components/DoctorSelect';
import { CalendarView, type ScheduleItem } from './components/CalendarView';
import { StatusTag } from './components/StatusTag';
import { AuthButton } from '../../components/AuthButton';
/** 时段选项 */
const PERIOD_OPTIONS = [
@@ -258,16 +259,18 @@ export default function DoctorSchedule() {
key: 'action',
width: 120,
render: (_: unknown, record: Schedule) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
</Space>
<AuthButton code="health.doctor.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
</Space>
</AuthButton>
),
},
];
@@ -309,9 +312,11 @@ export default function DoctorSchedule() {
<Col flex="auto" />
<Col>
{selectedDoctorId && (
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.doctor.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
)}
</Col>
</Row>

View File

@@ -19,6 +19,7 @@ import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
const STATUS_OPTIONS = [
{ value: 'pending', label: '待处理' },
@@ -289,34 +290,36 @@ export default function FollowUpTaskList() {
key: 'actions',
width: 220,
render: (_: unknown, record: FollowUpTask) => (
<Space size={4}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openRecordModal(record)}
>
</Button>
<Button
type="link"
size="small"
icon={<SwapOutlined />}
onClick={() => openAssignModal(record)}
>
</Button>
<Popconfirm
title="确认删除该随访任务?"
onConfirm={() => handleDelete(record)}
okText="确认"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
<AuthButton code="health.follow-up.manage">
<Space size={4}>
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openRecordModal(record)}
>
</Button>
</Popconfirm>
</Space>
<Button
type="link"
size="small"
icon={<SwapOutlined />}
onClick={() => openAssignModal(record)}
>
</Button>
<Popconfirm
title="确认删除该随访任务?"
onConfirm={() => handleDelete(record)}
okText="确认"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
</AuthButton>
),
},
];
@@ -344,16 +347,18 @@ export default function FollowUpTaskList() {
value={query.status}
onChange={(value) => handleFilterChange('status', value)}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
createForm.resetFields();
setCreateOpen(true);
}}
>
</Button>
<AuthButton code="health.follow-up.manage">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
createForm.resetFields();
setCreateOpen(true);
}}
>
</Button>
</AuthButton>
<span
style={{
fontSize: 13,

View File

@@ -30,6 +30,7 @@ import {
type OfflineEvent,
type CreateOfflineEventReq,
} from '../../api/health/points';
import { AuthButton } from '../../components/AuthButton';
/** 活动状态映射 */
const STATUS_MAP: Record<string, { text: string; color: string }> = {
@@ -245,35 +246,37 @@ export default function OfflineEventList() {
key: 'action',
width: 200,
render: (_: unknown, record: OfflineEvent) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Button
type="link"
size="small"
icon={<CheckCircleOutlined />}
onClick={() => handleCheckin(record)}
disabled={record.status === 'draft' || record.status === 'cancelled'}
>
</Button>
<Popconfirm
title="确定删除该活动?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
<AuthButton code="health.points.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
</Popconfirm>
</Space>
<Button
type="link"
size="small"
icon={<CheckCircleOutlined />}
onClick={() => handleCheckin(record)}
disabled={record.status === 'draft' || record.status === 'cancelled'}
>
</Button>
<Popconfirm
title="确定删除该活动?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
</AuthButton>
),
},
];
@@ -298,9 +301,11 @@ export default function OfflineEventList() {
</Space>
</Col>
<Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.points.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
</Col>
</Row>

View File

@@ -16,6 +16,7 @@ import {
} from 'antd';
import { ArrowLeftOutlined, EditOutlined } from '@ant-design/icons';
import { patientApi } from '../../api/health/patients';
import { AuthButton } from '../../components/AuthButton';
import type {
PatientDetail as PatientDetailType,
UpdatePatientReq,
@@ -187,9 +188,11 @@ export default function PatientDetail() {
</Space>
</div>
</div>
<Button icon={<EditOutlined />} onClick={openEditModal}>
</Button>
<AuthButton code="health.patient.manage">
<Button icon={<EditOutlined />} onClick={openEditModal}>
</Button>
</AuthButton>
</div>
<Descriptions column={3} size="small">
<Descriptions.Item label="性别">

View File

@@ -27,6 +27,7 @@ import type {
import { StatusTag } from './components/StatusTag';
import { GENDER_OPTIONS, BLOOD_TYPE_OPTIONS, STATUS_OPTIONS } from '../../constants/health';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
export default function PatientList() {
const [patients, setPatients] = useState<PatientListItem[]>([]);
@@ -239,33 +240,35 @@ export default function PatientList() {
key: 'actions',
width: 140,
render: (_: unknown, record: PatientListItem) => (
<Space size={4}>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
openEditModal(record);
}}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Popconfirm
title="确定删除此患者?"
onConfirm={(e) => {
e?.stopPropagation();
handleDelete(record.id);
}}
>
<AuthButton code="health.patient.manage">
<Space size={4}>
<Button
size="small"
type="text"
icon={<DeleteOutlined />}
danger
onClick={(e) => e.stopPropagation()}
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
openEditModal(record);
}}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
</Popconfirm>
</Space>
<Popconfirm
title="确定删除此患者?"
onConfirm={(e) => {
e?.stopPropagation();
handleDelete(record.id);
}}
>
<Button
size="small"
type="text"
icon={<DeleteOutlined />}
danger
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</Space>
</AuthButton>
),
},
];
@@ -301,9 +304,11 @@ export default function PatientList() {
options={STATUS_OPTIONS}
style={{ width: 130, borderRadius: 8 }}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
<AuthButton code="health.patient.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
</AuthButton>
</Space>
</div>

View File

@@ -14,6 +14,7 @@ import { TagsOutlined, AppstoreOutlined } from '@ant-design/icons';
import { patientApi, type TagItem } from '../../api/health/patients';
import type { PatientListItem } from '../../api/health/patients';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
export default function PatientTagManage() {
const [patients, setPatients] = useState<PatientListItem[]>([]);
@@ -178,14 +179,16 @@ export default function PatientTagManage() {
key: 'actions',
width: 120,
render: (_: unknown, record: PatientListItem) => (
<Button
size="small"
type="link"
icon={<TagsOutlined />}
onClick={() => openTagModal(record)}
>
</Button>
<AuthButton code="health.patient.manage">
<Button
size="small"
type="link"
icon={<TagsOutlined />}
onClick={() => openTagModal(record)}
>
</Button>
</AuthButton>
),
},
];

View File

@@ -23,6 +23,7 @@ import {
type PointsOrder,
} from '../../api/health/points';
import { patientApi } from '../../api/health/patients';
import { AuthButton } from '../../components/AuthButton';
/** 订单状态映射 */
const STATUS_MAP: Record<string, { text: string; color: string }> = {
@@ -228,13 +229,15 @@ export default function PointsOrderList() {
</Space>
</Col>
<Col>
<Button
type="primary"
icon={<CheckCircleOutlined />}
onClick={openVerifyModal}
>
</Button>
<AuthButton code="health.points.manage">
<Button
type="primary"
icon={<CheckCircleOutlined />}
onClick={openVerifyModal}
>
</Button>
</AuthButton>
</Col>
</Row>

View File

@@ -26,6 +26,7 @@ import {
type PointsProduct,
type CreatePointsProductReq,
} from '../../api/health/points';
import { AuthButton } from '../../components/AuthButton';
/** 商品类型映射 */
const PRODUCT_TYPES: Record<string, string> = {
@@ -212,23 +213,25 @@ export default function PointsProductList() {
key: 'action',
width: 140,
render: (_: unknown, record: PointsProduct) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Switch
size="small"
checked={record.is_active}
checkedChildren="上架"
unCheckedChildren="架"
onChange={() => handleToggleActive(record)}
/>
</Space>
<AuthButton code="health.points.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Switch
size="small"
checked={record.is_active}
checkedChildren="架"
unCheckedChildren="下架"
onChange={() => handleToggleActive(record)}
/>
</Space>
</AuthButton>
),
},
];
@@ -253,9 +256,11 @@ export default function PointsProductList() {
</Space>
</Col>
<Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.points.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
</Col>
</Row>

View File

@@ -26,6 +26,7 @@ import {
type PointsRule,
type CreatePointsRuleReq,
} from '../../api/health/points';
import { AuthButton } from '../../components/AuthButton';
/** 事件类型映射 */
const EVENT_TYPES: Record<string, string> = {
@@ -216,23 +217,25 @@ export default function PointsRuleList() {
key: 'action',
width: 200,
render: (_: unknown, record: PointsRule) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Switch
size="small"
checked={record.is_active}
checkedChildren="启用"
unCheckedChildren="用"
onChange={() => handleToggleActive(record)}
/>
</Space>
<AuthButton code="health.points.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Switch
size="small"
checked={record.is_active}
checkedChildren="用"
unCheckedChildren="停用"
onChange={() => handleToggleActive(record)}
/>
</Space>
</AuthButton>
),
},
];
@@ -247,9 +250,11 @@ export default function PointsRuleList() {
</span>
</Col>
<Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<AuthButton code="health.points.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
</Col>
</Row>