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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect'; import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect'; import { DoctorSelect } from './components/DoctorSelect';
import { useThemeMode } from '../../hooks/useThemeMode'; import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'pending', label: '待处理' }, { value: 'pending', label: '待处理' },
@@ -289,34 +290,36 @@ export default function FollowUpTaskList() {
key: 'actions', key: 'actions',
width: 220, width: 220,
render: (_: unknown, record: FollowUpTask) => ( render: (_: unknown, record: FollowUpTask) => (
<Space size={4}> <AuthButton code="health.follow-up.manage">
<Button <Space size={4}>
type="link" <Button
size="small" type="link"
icon={<EditOutlined />} size="small"
onClick={() => openRecordModal(record)} 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 />}>
</Button> </Button>
</Popconfirm> <Button
</Space> 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} value={query.status}
onChange={(value) => handleFilterChange('status', value)} onChange={(value) => handleFilterChange('status', value)}
/> />
<Button <AuthButton code="health.follow-up.manage">
type="primary" <Button
icon={<PlusOutlined />} type="primary"
onClick={() => { icon={<PlusOutlined />}
createForm.resetFields(); onClick={() => {
setCreateOpen(true); createForm.resetFields();
}} setCreateOpen(true);
> }}
>
</Button>
</Button>
</AuthButton>
<span <span
style={{ style={{
fontSize: 13, fontSize: 13,

View File

@@ -30,6 +30,7 @@ import {
type OfflineEvent, type OfflineEvent,
type CreateOfflineEventReq, type CreateOfflineEventReq,
} from '../../api/health/points'; } from '../../api/health/points';
import { AuthButton } from '../../components/AuthButton';
/** 活动状态映射 */ /** 活动状态映射 */
const STATUS_MAP: Record<string, { text: string; color: string }> = { const STATUS_MAP: Record<string, { text: string; color: string }> = {
@@ -245,35 +246,37 @@ export default function OfflineEventList() {
key: 'action', key: 'action',
width: 200, width: 200,
render: (_: unknown, record: OfflineEvent) => ( render: (_: unknown, record: OfflineEvent) => (
<Space size="small"> <AuthButton code="health.points.manage">
<Button <Space size="small">
type="link" <Button
size="small" type="link"
icon={<EditOutlined />} size="small"
onClick={() => openEdit(record)} 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 />}>
</Button> </Button>
</Popconfirm> <Button
</Space> 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> </Space>
</Col> </Col>
<Col> <Col>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}> <AuthButton code="health.points.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</Button>
</AuthButton>
</Col> </Col>
</Row> </Row>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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