forked from wangziqi/gongxue-base
@@ -4,7 +4,15 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
min-width: 320px;
|
||||
background: #f5f5f7;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
background: #f5f5f7;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -15,10 +23,55 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
img,
|
||||
svg,
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-shell,
|
||||
.app-main,
|
||||
.app-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Shared responsive toolbar: add these classes to page filter/action rows. */
|
||||
.responsive-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.responsive-toolbar__group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-drawer-content-wrapper {
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
/* === 通用:表格容器横向滚动(防双重滚动条) === */
|
||||
.ant-table-wrapper {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-inline: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* ── 表格单元格省略号截断(按需启用) ──
|
||||
@@ -54,6 +107,65 @@ body {
|
||||
|
||||
/* === 手机 (< 576px) === */
|
||||
@media (max-width: 575px) {
|
||||
.app-header {
|
||||
height: 56px;
|
||||
padding-inline: 8px !important;
|
||||
line-height: 56px;
|
||||
}
|
||||
|
||||
.app-header .ant-btn {
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
min-height: calc(100dvh - 72px);
|
||||
margin: 8px !important;
|
||||
padding: 12px !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.app-navigation-drawer .ant-drawer-header {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.responsive-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.responsive-toolbar__group,
|
||||
.responsive-toolbar > .ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-toolbar__group > .ant-space-item,
|
||||
.responsive-toolbar > .ant-space > .ant-space-item {
|
||||
flex: 1 1 140px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.responsive-toolbar .ant-input,
|
||||
.responsive-toolbar .ant-input-affix-wrapper,
|
||||
.responsive-toolbar .ant-input-search,
|
||||
.responsive-toolbar .ant-picker,
|
||||
.responsive-toolbar .ant-select,
|
||||
.responsive-toolbar .ant-upload,
|
||||
.responsive-toolbar .ant-upload-wrapper,
|
||||
.responsive-toolbar .ant-btn {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-btn:not(.ant-btn-sm),
|
||||
.ant-input-affix-wrapper,
|
||||
.ant-input-search-button,
|
||||
.ant-picker,
|
||||
.ant-select-single .ant-select-selector {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -65,12 +177,48 @@ body {
|
||||
font-size: 13px;
|
||||
}
|
||||
.ant-modal {
|
||||
max-width: calc(100vw - 24px) !important;
|
||||
margin: 12px auto !important;
|
||||
top: 12px;
|
||||
max-width: calc(100vw - 16px) !important;
|
||||
margin: 0 auto !important;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.ant-modal .ant-modal-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.ant-modal .ant-modal-body {
|
||||
max-height: 60vh;
|
||||
max-height: calc(100dvh - 180px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.ant-modal .ant-modal-footer {
|
||||
display: flex;
|
||||
}
|
||||
.ant-modal .ant-modal-footer .ant-btn {
|
||||
flex: 1;
|
||||
min-height: 40px;
|
||||
}
|
||||
.ant-drawer .ant-drawer-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.ant-drawer .ant-drawer-body {
|
||||
padding: 16px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.ant-popover,
|
||||
.ant-picker-dropdown {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
.ant-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
.ant-pagination .ant-pagination-options {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
.ant-alert {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
@@ -78,9 +226,8 @@ body {
|
||||
.ant-card {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ant-space-item .ant-btn {
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
.ant-card .ant-card-body {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,3 +237,82 @@ body {
|
||||
max-width: calc(100vw - 48px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notifications-filter {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.notifications-layout,
|
||||
.notifications-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notifications-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.notifications-content .ant-list-item {
|
||||
align-items: flex-start;
|
||||
padding: 14px 6px !important;
|
||||
}
|
||||
|
||||
.notifications-content .ant-list-item-meta-avatar {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.notifications-content .ant-list-item-meta-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.ant-table-wrapper::before {
|
||||
content: '表格可左右滑动查看';
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #8c8c8c;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-content {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-cell-fix-left,
|
||||
.ant-table-wrapper .ant-table-cell-fix-right {
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-toolbar--single {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
/* Input.Search is an internal compact group. Keep the text field flexible and
|
||||
the search icon button fixed instead of applying toolbar full-width rules
|
||||
to both children. */
|
||||
.responsive-toolbar .ant-input-search > .ant-input-affix-wrapper {
|
||||
flex: 1 1 auto;
|
||||
width: auto !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.responsive-toolbar .ant-input-search > .ant-input-search-btn {
|
||||
flex: 0 0 40px;
|
||||
width: 40px !important;
|
||||
min-width: 40px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,13 +177,14 @@ const MainLayout: React.FC = () => {
|
||||
), [selectedKeys, openKeys, menuItems, handleMenuClick]);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Layout className="app-shell" style={{ minHeight: '100vh' }}>
|
||||
{!isMobile && (
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={isTablet ? true : collapsed}
|
||||
theme="light"
|
||||
className="app-sidebar"
|
||||
style={{ background: '#fff', borderRight: '1px solid #e5e5e7' }}
|
||||
>
|
||||
<div
|
||||
@@ -210,13 +211,15 @@ const MainLayout: React.FC = () => {
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
size={240}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
className="app-navigation-drawer"
|
||||
title="恭学教育基地"
|
||||
>
|
||||
{menuContent}
|
||||
</Drawer>
|
||||
)}
|
||||
<Layout style={{ background: '#f5f5f7' }}>
|
||||
<Layout className="app-main" style={{ background: '#f5f5f7' }}>
|
||||
<Header
|
||||
className="app-header"
|
||||
style={{
|
||||
padding: '0 16px',
|
||||
background: '#fff',
|
||||
@@ -268,8 +271,9 @@ const MainLayout: React.FC = () => {
|
||||
</div>
|
||||
</Header>
|
||||
<Content
|
||||
className="app-content"
|
||||
style={{
|
||||
margin: isMobile ? 12 : isTablet ? 16 : 24,
|
||||
margin: isMobile ? 8 : isTablet ? 16 : 24,
|
||||
padding: isMobile ? 12 : isTablet ? 16 : 24,
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
|
||||
@@ -683,6 +683,21 @@ const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) =>
|
||||
width: 110,
|
||||
render: (value: string) => (value === 'dingtalk' ? '钉钉同步' : value === 'schedule' ? '课程生成' : value === 'lesson' ? '课堂点名' : '人工记录'),
|
||||
},
|
||||
{
|
||||
title: '打卡设备',
|
||||
width: 220,
|
||||
render: (_: unknown, record: AttendanceRecordItem) => {
|
||||
const info = getPunchDisplayInfo(record);
|
||||
if (!info) return <span className="muted-text">—</span>;
|
||||
return (
|
||||
<div className="punch-device-cell">
|
||||
<Tag color={info.machine ? 'green' : 'blue'}>{info.label}</Tag>
|
||||
{info.detail && <strong>{info.detail}</strong>}
|
||||
{info.time && <span>{dayjs(info.time).format('HH:mm:ss')}</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
|
||||
@@ -257,16 +257,8 @@ const BillsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<div className="responsive-toolbar">
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Input.Search
|
||||
placeholder="搜索学生姓名或账单周期"
|
||||
allowClear
|
||||
@@ -308,7 +300,7 @@ const BillsPage: React.FC = () => {
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
<Space>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<PermissionButton
|
||||
permission="bill:generate"
|
||||
type="primary"
|
||||
|
||||
@@ -10,6 +10,7 @@ import dayjs from 'dayjs';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate';
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
@@ -84,10 +85,7 @@ interface StudentItem {
|
||||
studentNo?: string;
|
||||
}
|
||||
|
||||
interface UserItem {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
type UserItem = TeacherCandidateUser;
|
||||
|
||||
// ---- Constants ----
|
||||
|
||||
@@ -595,16 +593,13 @@ const ClassDetailPage: React.FC = () => {
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择教师"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="搜索姓名、用户名、角色或学科"
|
||||
value={teacherUserId}
|
||||
onChange={setTeacherUserId}
|
||||
options={allUsers.map((u) => ({
|
||||
value: u.id,
|
||||
label: u.username,
|
||||
}))}
|
||||
filterOption={(input, option) =>
|
||||
(option?.label as string)?.toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={buildTeacherCandidateOptions(allUsers)}
|
||||
notFoundContent="没有可分配的工作人员账号"
|
||||
/>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -220,7 +220,7 @@ const ClassesPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Space style={{ marginBottom: 16 }} wrap className="responsive-toolbar responsive-toolbar--single">
|
||||
<Input
|
||||
placeholder="搜索名称/编码"
|
||||
prefix={<SearchOutlined />}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildTeacherCandidateLabel,
|
||||
buildTeacherCandidateOptions,
|
||||
isTeacherCandidate,
|
||||
type TeacherCandidateUser,
|
||||
} from './teacher-candidate';
|
||||
|
||||
const baseUser = (overrides: Partial<TeacherCandidateUser> = {}): TeacherCandidateUser => ({
|
||||
id: 1,
|
||||
username: 'teacher',
|
||||
name: '测试老师',
|
||||
isActive: true,
|
||||
isArchived: false,
|
||||
studentStatus: null,
|
||||
roles: [{ code: 'teacher', name: '任课老师' }],
|
||||
profile: { subjects: ['数学', '物理'] },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('class teacher candidates', () => {
|
||||
it('keeps non-teacher staff roles because class duty is selected separately', () => {
|
||||
expect(
|
||||
isTeacherCandidate(baseUser({ roles: [{ code: 'academic', name: '教务管理员' }] })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps staff-linked accounts so they can serve as head or life teachers', () => {
|
||||
expect(isTeacherCandidate(baseUser({ studentStatus: 'staff' }))).toBe(true);
|
||||
});
|
||||
|
||||
it('excludes active students, disabled, archived, and super-admin accounts', () => {
|
||||
const users = [
|
||||
baseUser({ id: 1, studentStatus: 'active' }),
|
||||
baseUser({ id: 2, isActive: false }),
|
||||
baseUser({ id: 3, isArchived: true }),
|
||||
baseUser({
|
||||
id: 4,
|
||||
roles: [{ code: 'super_admin', name: '超级管理员' }],
|
||||
}),
|
||||
];
|
||||
|
||||
expect(buildTeacherCandidateOptions(users)).toEqual([]);
|
||||
});
|
||||
|
||||
it('shows real name, username, system role, and teaching subjects', () => {
|
||||
expect(buildTeacherCandidateLabel(baseUser())).toBe(
|
||||
'测试老师(teacher) · 任课老师 · 数学/物理',
|
||||
);
|
||||
});
|
||||
});
|
||||
44
apps/admin/src/pages/Classes/teacher-candidate.ts
Normal file
44
apps/admin/src/pages/Classes/teacher-candidate.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface TeacherCandidateRole {
|
||||
code?: string | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TeacherCandidateUser {
|
||||
id: number;
|
||||
username: string;
|
||||
name?: string | null;
|
||||
isActive: boolean;
|
||||
isArchived: boolean;
|
||||
studentStatus?: string | null;
|
||||
roles?: TeacherCandidateRole[];
|
||||
profile?: { subjects?: string[] } | null;
|
||||
}
|
||||
|
||||
const isSuperAdminRole = (role: TeacherCandidateRole) =>
|
||||
role.code === 'super_admin' || role.name === '超级管理员' || role.name === '超管';
|
||||
|
||||
export const isTeacherCandidate = (user: TeacherCandidateUser) => {
|
||||
if (!user.isActive || user.isArchived) return false;
|
||||
if (user.studentStatus && user.studentStatus !== 'staff') return false;
|
||||
return !(user.roles || []).some(isSuperAdminRole);
|
||||
};
|
||||
|
||||
export const buildTeacherCandidateLabel = (user: TeacherCandidateUser) => {
|
||||
const displayName = user.name?.trim();
|
||||
const identity =
|
||||
displayName && displayName !== user.username
|
||||
? `${displayName}(${user.username})`
|
||||
: user.username;
|
||||
const roleNames = [...new Set((user.roles || []).map((role) => role.name).filter(Boolean))];
|
||||
const subjects = [
|
||||
...new Set((user.profile?.subjects || []).map((subject) => subject.trim()).filter(Boolean)),
|
||||
];
|
||||
|
||||
return [identity, roleNames.join('/'), subjects.join('/')].filter(Boolean).join(' · ');
|
||||
};
|
||||
|
||||
export const buildTeacherCandidateOptions = (users: TeacherCandidateUser[]) =>
|
||||
users.filter(isTeacherCandidate).map((user) => ({
|
||||
value: user.id,
|
||||
label: buildTeacherCandidateLabel(user),
|
||||
}));
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { List, Typography, Menu, Layout, Button, Empty, Spin, Space } from 'antd';
|
||||
import { List, Typography, Menu, Layout, Button, Empty, Spin, Space, Grid, Select } from 'antd';
|
||||
import {
|
||||
BellOutlined,
|
||||
DollarOutlined,
|
||||
@@ -13,6 +13,7 @@ import { message } from '../../ui/app-message';
|
||||
import { formatNotificationText } from '../../utils/notification-display';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
interface NotificationItem {
|
||||
id: number;
|
||||
@@ -49,6 +50,8 @@ function timeAgo(dateStr: string): string {
|
||||
}
|
||||
|
||||
const NotificationsPage: React.FC = () => {
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.sm;
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -101,27 +104,39 @@ const NotificationsPage: React.FC = () => {
|
||||
? notifications
|
||||
: notifications.filter((n) => n.type === filter);
|
||||
|
||||
const filterItems = [
|
||||
{ key: 'all', icon: <BellOutlined />, label: '全部' },
|
||||
{ key: 'bill_generated', icon: <DollarOutlined />, label: '账单' },
|
||||
{ key: 'check_in', icon: <HomeOutlined />, label: '入住' },
|
||||
{ key: 'class_change', icon: <TeamOutlined />, label: '班级' },
|
||||
{ key: 'announcement', icon: <SettingOutlined />, label: '公告' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100%', background: '#fff' }}>
|
||||
<Sider width={180} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[filter]}
|
||||
onClick={({ key }) => setFilter(key)}
|
||||
items={[
|
||||
{ key: 'all', icon: <BellOutlined />, label: '全部' },
|
||||
{ key: 'bill_generated', icon: <DollarOutlined />, label: '账单' },
|
||||
{ key: 'check_in', icon: <HomeOutlined />, label: '入住' },
|
||||
{ key: 'class_change', icon: <TeamOutlined />, label: '班级' },
|
||||
{ key: 'announcement', icon: <SettingOutlined />, label: '公告' },
|
||||
]}
|
||||
/>
|
||||
</Sider>
|
||||
<Content style={{ padding: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Layout className="notifications-layout" style={{ minHeight: '100%', background: '#fff' }}>
|
||||
{!isMobile && (
|
||||
<Sider width={180} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[filter]}
|
||||
onClick={({ key }) => setFilter(key)}
|
||||
items={filterItems}
|
||||
/>
|
||||
</Sider>
|
||||
)}
|
||||
<Content className="notifications-content" style={{ padding: isMobile ? 0 : 24 }}>
|
||||
<div className="notifications-header">
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>通知中心</Typography.Title>
|
||||
<Button onClick={handleMarkAll}>全部已读</Button>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<Select
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
options={filterItems.map((item) => ({ value: item.key, label: item.label }))}
|
||||
className="notifications-filter"
|
||||
/>
|
||||
)}
|
||||
<Spin spinning={loading}>
|
||||
{filtered.length === 0 ? (
|
||||
<Empty description="暂无通知" />
|
||||
@@ -165,7 +180,7 @@ const NotificationsPage: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<Space wrap size={[8, 2]}>
|
||||
<Typography.Text
|
||||
strong={!item.isRead}
|
||||
style={{ fontSize: 15 }}
|
||||
|
||||
@@ -334,16 +334,8 @@ const OccupanciesPage: React.FC = () => {
|
||||
closable
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<div className="responsive-toolbar">
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Button type={showActive ? 'primary' : 'default'} onClick={() => setShowActive(true)}>
|
||||
在住记录
|
||||
</Button>
|
||||
@@ -358,7 +350,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
/>
|
||||
<RangePicker value={dateRange} onChange={(dates) => { setDateRange(dates ? [dates[0], dates[1]] : null); }} placeholder={['入住开始', '入住结束']} style={{ width: 240 }} />
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<PermissionButton
|
||||
permission="occupancy:checkin"
|
||||
type="primary"
|
||||
|
||||
@@ -411,16 +411,8 @@ const RoomsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<div className="responsive-toolbar">
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<h3 style={{ margin: 0 }}>宿舍管理</h3>
|
||||
<Input.Search
|
||||
placeholder="搜索房间号"
|
||||
@@ -458,7 +450,7 @@ const RoomsPage: React.FC = () => {
|
||||
: `显示已归档${archivedCount > 0 ? ` (${archivedCount})` : ''}`}
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
|
||||
@@ -529,16 +529,8 @@ const StudentsPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<div className="responsive-toolbar">
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Input.Search
|
||||
placeholder="搜索学生姓名"
|
||||
onSearch={setSearchName}
|
||||
@@ -586,7 +578,7 @@ const StudentsPage: React.FC = () => {
|
||||
: `显示已归档${archivedCount > 0 ? ` (${archivedCount})` : ''}`}
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Space wrap className="responsive-toolbar__group">
|
||||
<Popconfirm
|
||||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||||
onConfirm={handleBatchDelete}
|
||||
|
||||
@@ -181,7 +181,7 @@ const TeachersPage: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 16 }}>教师管理</h2>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }} wrap className="responsive-toolbar responsive-toolbar--single">
|
||||
<Input.Search
|
||||
placeholder="搜索姓名/用户名"
|
||||
allowClear
|
||||
|
||||
@@ -484,7 +484,8 @@ export class RbacService {
|
||||
lastLoginAt: u.lastLoginAt,
|
||||
createdAt: u.createdAt,
|
||||
updatedAt: u.updatedAt,
|
||||
roles: u.roles?.map((r) => ({ id: r.id, name: r.name })) || [],
|
||||
roles: u.roles?.map((r) => ({ id: r.id, code: r.code, name: r.name })) || [],
|
||||
profile: u.profile || {},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user