统计分析页面

main
wangyunfei888 1 day ago
parent b928cb3316
commit 8095d8c99f

@ -1,10 +1,200 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import { Button, Segmented, Select } from 'antd';
import { PlusOutlined, GlobalOutlined, BranchesOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import styles from './ActivityCalendar.less';
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const calendarEvents = [
{
id: 'earth-day',
title: '世界环保日',
start: dayjs('2020-12-14'),
end: dayjs('2020-12-15'),
tone: 'sand',
icon: <GlobalOutlined />,
},
{
id: 'tree-day',
title: '全国植树日',
start: dayjs('2020-12-24'),
end: dayjs('2020-12-31'),
tone: 'mint',
icon: <BranchesOutlined />,
},
];
const buildGrid = (referenceDate, viewMode) => {
if (viewMode === 'week') {
const startOfWeek = referenceDate.startOf('week');
const days = Array.from({ length: 7 }, (_, index) => startOfWeek.add(index, 'day'));
return { days, gridStart: startOfWeek, weeks: 1 };
}
const startOfMonth = referenceDate.startOf('month');
const gridStart = startOfMonth.startOf('week');
const days = Array.from({ length: 42 }, (_, index) => gridStart.add(index, 'day'));
return { days, gridStart, weeks: 6 };
};
const buildEventSegments = (events, gridStart, weeks) => {
const gridEnd = gridStart.add(weeks * 7 - 1, 'day').endOf('day');
const segments = [];
events.forEach((event) => {
const start = event.start;
const end = event.end;
if (end.isBefore(gridStart) || start.isAfter(gridEnd)) {
return;
}
for (let weekIndex = 0; weekIndex < weeks; weekIndex += 1) {
const weekStart = gridStart.add(weekIndex * 7, 'day');
const weekEnd = weekStart.add(6, 'day').endOf('day');
const segmentStart = start.isAfter(weekStart) ? start : weekStart;
const segmentEnd = end.isBefore(weekEnd) ? end : weekEnd;
if (segmentEnd.isBefore(weekStart) || segmentStart.isAfter(weekEnd)) {
continue;
}
const columnStart = segmentStart.diff(weekStart, 'day') + 1;
const columnEnd = segmentEnd.diff(weekStart, 'day') + 2;
segments.push({
id: `${event.id}-${weekIndex}`,
title: event.title,
tone: event.tone,
icon: event.icon,
rowStart: weekIndex + 1,
rowEnd: weekIndex + 2,
columnStart,
columnEnd,
});
}
});
return segments;
};
const ActivityCalendar = () => {
const [currentDate, setCurrentDate] = useState(dayjs('2020-12-01'));
const [viewMode, setViewMode] = useState('month');
const { days, gridStart, weeks } = useMemo(() => buildGrid(currentDate, viewMode), [currentDate, viewMode]);
const segments = useMemo(() => buildEventSegments(calendarEvents, gridStart, weeks), [gridStart, weeks]);
const yearOptions = useMemo(() => {
const baseYear = dayjs().year();
const startYear = Math.min(2020, baseYear - 4);
return Array.from({ length: 10 }, (_, index) => startYear + index);
}, []);
const handleYearChange = (year) => {
setCurrentDate((prev) => prev.year(year));
};
const handleMonthChange = (month) => {
setCurrentDate((prev) => prev.month(month - 1));
};
const handleViewChange = (mode) => {
setViewMode(mode);
};
const handleBackToToday = () => {
const today = dayjs();
setCurrentDate(today);
setViewMode('month');
};
return (
<div className={styles.container}>
<div className={styles.toolbar}>
<Button type="primary" icon={<PlusOutlined />} className={styles.createButton}>
新建活动
</Button>
<div className={styles.toolbarRight}>
<Select
size="middle"
value={currentDate.year()}
className={styles.select}
popupClassName={styles.dropdown}
onChange={handleYearChange}
options={yearOptions.map((year) => ({ value: year, label: `${year}` }))}
/>
<Select
size="middle"
value={currentDate.month() + 1}
className={styles.select}
popupClassName={styles.dropdown}
onChange={handleMonthChange}
options={Array.from({ length: 12 }, (_, index) => ({
value: index + 1,
label: `${index + 1}`,
}))}
/>
<Segmented
value={viewMode}
onChange={handleViewChange}
options={[
{ label: '月', value: 'month' },
{ label: '周', value: 'week' },
]}
className={styles.segmented}
/>
<Button className={styles.dayButton} onClick={handleBackToToday}>
</Button>
</div>
</div>
<div className={styles.calendarWrapper}>
<div className={styles.weekHeader}>
{weekDays.map((day) => (
<div key={day} className={styles.weekCell}>
{day}
</div>
))}
</div>
<div className={styles.calendarGrid}>
{days.map((day) => {
const isCurrentMonth = day.month() === currentDate.month();
return (
<div className={styles.placeholder}>
活动日历 待开发
<div
key={day.format('YYYY-MM-DD')}
className={`${styles.dayCell} ${isCurrentMonth ? '' : styles.dimmed}`}
>
<span className={styles.dayNumber}>{day.date()}</span>
</div>
);
})}
{segments.map((segment) => (
<div
key={segment.id}
className={`${styles.eventBlock} ${styles[segment.tone]}`}
style={{
gridColumnStart: segment.columnStart,
gridColumnEnd: segment.columnEnd,
gridRowStart: segment.rowStart,
gridRowEnd: segment.rowEnd,
}}
>
<div className={styles.eventInfo}>
<span className={styles.eventDot} />
<span className={styles.eventTitle}>{segment.title}</span>
</div>
<div className={styles.eventIcon}>{segment.icon}</div>
</div>
))}
</div>
</div>
</div>
);
};

@ -1,8 +1,215 @@
.placeholder {
.container {
background: #fff;
border-radius: 4px;
padding: 24px;
min-height: 72vh;
font-size: 16px;
color: #666;
border-radius: 10px;
padding: 16px 20px 20px;
display: flex;
flex-direction: column;
height: 100%;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.03);
box-sizing: border-box;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.createButton {
background: linear-gradient(98deg, #12d49c 0%, #0ec9c9 100%);
border: none;
border-radius: 8px;
padding: 6px 16px;
height: 34px;
box-shadow: 0 6px 14px rgba(18, 212, 156, 0.25);
&:hover,
&:focus {
background: linear-gradient(98deg, #0fcf94 0%, #0cbcbc 100%);
}
}
.toolbarRight {
display: flex;
align-items: center;
gap: 10px;
}
.select {
width: 88px;
:global(.ant-select-selector) {
border-radius: 6px;
border-color: #e8e8e8;
box-shadow: none !important;
height: 34px;
}
}
.dropdown {
:global(.ant-select-item-option-selected) {
background: #e8fff2 !important;
}
}
.segmented {
:global(.ant-segmented-group) {
gap: 6px;
}
:global(.ant-segmented-item) {
padding: 4px 12px;
min-width: 38px;
height: 34px;
align-items: center;
justify-content: center;
}
:global(.ant-segmented-item-selected) {
background: #0fcfaf;
color: #fff;
}
}
.dayButton {
width: 38px;
height: 34px;
border-radius: 6px;
border: 1px solid #e8e8e8;
color: #999;
background: #fff;
&:hover,
&:focus {
border-color: #0fcfaf;
color: #0fcfaf;
}
}
.calendarWrapper {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
border: 1px solid #f2f2f2;
}
.weekHeader {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: #f9fbfc;
color: #9fa6ad;
font-size: 12px;
border-bottom: 1px solid #f2f2f2;
}
.weekCell {
text-align: center;
padding: 8px 0;
border-right: 1px solid #f2f2f2;
&:last-child {
border-right: none;
}
}
.calendarGrid {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: minmax(110px, 1fr);
position: relative;
background: #fff;
border-left: 1px solid #f2f2f2;
border-bottom: 1px solid #f2f2f2;
overflow: hidden;
}
.dayCell {
border-right: 1px solid #f2f2f2;
border-top: 1px solid #f2f2f2;
padding: 10px;
position: relative;
box-sizing: border-box;
}
.dayNumber {
position: absolute;
top: 8px;
right: 10px;
color: #9fa6ad;
font-size: 12px;
}
.dimmed {
opacity: 0.45;
}
.eventBlock {
position: relative;
margin: 34px 6px 10px;
border-radius: 8px;
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
pointer-events: none;
border: 1px solid transparent;
box-sizing: border-box;
z-index: 1;
}
.eventInfo {
display: flex;
align-items: center;
gap: 10px;
}
.eventDot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #2bd1ab;
flex-shrink: 0;
}
.eventTitle {
color: #6f6f6f;
}
.eventIcon {
font-size: 22px;
}
.sand {
background: #fff7e6;
border-color: #ffe7c2;
color: #d48806;
.eventDot {
background: #f5a623;
}
.eventIcon {
color: #f5a623;
}
}
.mint {
background: #e8fff2;
border-color: #c5f4da;
color: #12a77c;
.eventDot {
background: #12d49c;
}
.eventIcon {
color: #12a77c;
}
}

@ -1,10 +1,162 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import { Button, Checkbox, Select } from 'antd';
import {
PlusOutlined,
FilePdfOutlined,
FileWordOutlined,
PictureOutlined,
} from '@ant-design/icons';
import styles from './ActivityManagement.less';
const statusMeta = {
ongoing: { label: '进行中', ribbon: '#20c997' },
published: { label: '已发布', ribbon: '#20c997' },
finished: { label: '已结束', ribbon: '#fa8c16' },
archived: { label: '已归档', ribbon: '#999999' },
};
const sampleFiles = [
{ type: 'pdf', name: '总结.pdf', icon: <FilePdfOutlined /> },
{ type: 'word', name: '方案.docx', icon: <FileWordOutlined /> },
{ type: 'img', name: '活动图片', icon: <PictureOutlined /> },
{ type: 'img', name: '活动图片', icon: <PictureOutlined /> },
{ type: 'img', name: '活动图片', icon: <PictureOutlined /> },
];
const activities = [
{
id: 'a1',
title: '2024-01环境应急演练',
participants: 98,
duration: 'xxxxxxxx',
owner: 'xxxxxxxx',
attachments: sampleFiles,
attachmentCount: 15,
status: 'published',
creator: '张三',
},
{
id: 'a2',
title: '2024-01环境应急演练',
participants: 98,
duration: 'xxxxxxxx',
owner: 'xxxxxxxx',
attachments: sampleFiles,
attachmentCount: 15,
status: 'finished',
creator: '李四',
},
{
id: 'a3',
title: '2024-01环境应急演练',
participants: 98,
duration: 'xxxxxxxx',
owner: 'xxxxxxxx',
attachments: sampleFiles,
attachmentCount: 15,
status: 'archived',
creator: '张三',
},
{
id: 'a4',
title: '2024-01环境应急演练',
participants: 98,
duration: 'xxxxxxxx',
owner: 'xxxxxxxx',
attachments: sampleFiles,
attachmentCount: 15,
status: 'published',
creator: '李四',
},
];
const ActivityManagement = () => {
const [selectedStatuses, setSelectedStatuses] = useState(Object.keys(statusMeta));
const [creator, setCreator] = useState('all');
const creators = useMemo(() => ['全部', '张三', '李四'], []);
const filteredActivities = useMemo(
() => activities.filter((item) => selectedStatuses.includes(item.status)
&& (creator === 'all' || item.creator === creator)),
[selectedStatuses, creator]
);
const handleStatusChange = (checkedValues) => {
setSelectedStatuses(checkedValues);
};
const statusOptions = useMemo(
() => Object.keys(statusMeta).map((key) => ({ label: statusMeta[key].label, value: key })),
[]
);
return (
<div className={styles.placeholder}>
活动管理 待开发
<div className={styles.container}>
<div className={styles.toolbar}>
<Button type="primary" icon={<PlusOutlined />} className={styles.createButton}>
新建活动
</Button>
<div className={styles.filters}>
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>活动状态</span>
<Checkbox.Group
options={statusOptions}
value={selectedStatuses}
onChange={handleStatusChange}
className={styles.statusGroup}
/>
</div>
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>创建人</span>
<Select
value={creator}
onChange={setCreator}
className={styles.creatorSelect}
options={creators.map((item, index) => ({ value: index === 0 ? 'all' : item, label: item }))}
/>
</div>
</div>
</div>
<div className={styles.grid}>
{filteredActivities.map((item) => {
const status = statusMeta[item.status];
return (
<div key={item.id} className={styles.card}>
<div className={styles.ribbon} style={{ background: status.ribbon }}>
{status.label}
</div>
<div className={styles.cardBody}>
<div className={styles.titleRow}>
<div className={styles.title}>{item.title}</div>
</div>
<div className={styles.metaRow}>
<div>参与人数 <span className={styles.emphasis}>{item.participants}</span></div>
<div>活动时长 <span className={styles.muted}>{item.duration}</span></div>
<div>负责人 <span className={styles.muted}>{item.owner}</span></div>
</div>
<div className={styles.metaRow}>
<div>附件数量 <span className={styles.emphasis}>{item.attachmentCount}</span></div>
</div>
<div className={styles.files}>
{item.attachments.map((file, idx) => (
<div key={`${item.id}-${idx}`} className={styles.fileChip}>
{file.icon}
<span>{file.name}</span>
</div>
))}
</div>
</div>
<div className={styles.actions}>
<Button type="link" className={styles.view}>查看</Button>
<Button type="link" className={styles.copy}>复用</Button>
<Button type="link" className={styles.archive}>归档</Button>
</div>
</div>
);
})}
</div>
</div>
);
};

@ -1,8 +1,193 @@
.placeholder {
background: #fff;
.container {
background: #f6f8fb;
border-radius: 10px;
padding: 14px 16px 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.createButton {
background: linear-gradient(98deg, #12d49c 0%, #0ec9c9 100%);
border: none;
border-radius: 8px;
padding: 6px 16px;
height: 34px;
box-shadow: 0 6px 14px rgba(18, 212, 156, 0.25);
&:hover,
&:focus {
background: linear-gradient(98deg, #0fcf94 0%, #0cbcbc 100%);
}
}
.filters {
display: flex;
align-items: center;
gap: 24px;
}
.filterGroup {
display: flex;
align-items: center;
gap: 10px;
color: #666;
font-size: 13px;
}
.filterLabel {
color: #888;
min-width: 64px;
text-align: right;
}
.statusGroup {
:global(.ant-checkbox-wrapper) {
margin-right: 10px;
}
:global(.ant-checkbox-inner) {
border-radius: 4px;
padding: 24px;
min-height: 72vh;
}
}
.creatorSelect {
width: 120px;
:global(.ant-select-selector) {
border-radius: 6px !important;
height: 34px;
box-shadow: none !important;
}
}
.grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
gap: 12px;
overflow-y: auto;
padding-right: 4px;
}
.card {
position: relative;
background: #fff;
border: 1px solid #eef2f7;
border-radius: 10px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
height: 214px;
overflow: hidden;
}
.ribbon {
position: absolute;
top: 0;
right: 0;
color: #fff;
font-size: 12px;
padding: 8px 14px;
transform: translate(26px, -6px) rotate(45deg);
width: 92px;
text-align: center;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
.cardBody {
padding: 16px 16px 8px;
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.titleRow {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.metaRow {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
color: #666;
font-size: 13px;
&:last-of-type {
grid-template-columns: 1fr;
}
}
.emphasis {
color: #333;
font-weight: 600;
margin-left: 6px;
}
.muted {
color: #999;
margin-left: 6px;
}
.files {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(86px, 1fr));
gap: 8px;
}
.fileChip {
background: #f6f8fb;
border-radius: 8px;
padding: 8px 10px;
display: flex;
align-items: center;
gap: 6px;
color: #4a5560;
font-size: 12px;
}
.actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
border-top: 1px solid #f0f2f5;
background: #fbfbfb;
}
.actions :global(.ant-btn) {
border: none;
height: 44px;
border-radius: 0;
box-shadow: none;
font-size: 14px;
}
.view {
color: #14b981;
}
.copy {
color: #3e7aff;
}
.archive {
color: #999;
}

@ -1,10 +1,157 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import { Button, Checkbox, Select } from 'antd';
import {
PlusOutlined,
} from '@ant-design/icons';
import styles from './EventReport.less';
const statusMeta = {
pending: { label: '待处理', ribbon: '#12c48b', action: '立案', actionColor: '#12c48b' },
awaiting: { label: '待接收', ribbon: '#fa8c16', action: '催办', actionColor: '#fa8c16' },
processing: { label: '处理中', ribbon: '#8aa4beff', action: '不予立案', actionColor: '#3b9cff' },
finished: { label: '已结束', ribbon: '#9a7bff', action: '查看报告', actionColor: '#3b82f6' },
};
const stats = [
{ title: '事件总数', value: 163, time: '2025-09-11 07:35' },
{ title: '待处理', value: 13, time: '2025-09-11 07:35' },
{ title: '处理中', value: 13, time: '2025-09-11 07:35' },
{ title: '待接收', value: 63, time: '2025-09-11 07:35' },
{ title: '已结束', value: 16, time: '2025-09-11 07:35' },
];
const events = [
{
id: 'e1',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'pending',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
{
id: 'e2',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'awaiting',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
{
id: 'e3',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'finished',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
{
id: 'e4',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'pending',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
{
id: 'e5',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'awaiting',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
{
id: 'e6',
code: 'EVT-006 废水排放异常 (06-05)',
status: 'finished',
reporter: '98',
assignTo: 'xxxxxxxxxx',
response: 'xxxxxxxxxx',
description: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
];
const EventReport = () => {
const [selectedStatuses, setSelectedStatuses] = useState(Object.keys(statusMeta));
const [reporterFilter, setReporterFilter] = useState('全部');
const statusOptions = useMemo(
() => Object.keys(statusMeta).map((key) => ({ label: statusMeta[key].label, value: key })),
[]
);
const filteredEvents = useMemo(
() => events.filter((item) => selectedStatuses.includes(item.status)),
[selectedStatuses]
);
return (
<div className={styles.container}>
<div className={styles.statsRow}>
{stats.map((item) => (
<div key={item.title} className={styles.statCard}>
<div className={styles.statTitle}>{item.title}</div>
<div className={styles.statValue}>{item.value}</div>
<div className={styles.statTime}>{item.time}</div>
</div>
))}
</div>
<div className={styles.filterRow}>
<Button type="primary" icon={<PlusOutlined />} className={styles.reportButton}>
上报事件
</Button>
<div className={styles.filters}>
<span className={styles.filterTitle}>事件状态</span>
<Checkbox.Group
options={statusOptions}
value={selectedStatuses}
onChange={setSelectedStatuses}
className={styles.statusGroup}
/>
<div className={styles.rightFilters}>
<span className={styles.filterLabel}>举报类型</span>
<Select
value={reporterFilter}
onChange={setReporterFilter}
className={styles.select}
options={['全部', '匿名', '公众'].map((item) => ({ value: item, label: item }))}
/>
</div>
</div>
</div>
<div className={styles.grid}>
{filteredEvents.map((item) => {
const meta = statusMeta[item.status];
return (
<div className={styles.placeholder}>
事件上报 待开发
<div key={item.id} className={styles.card}>
<div className={styles.ribbon} style={{ background: meta.ribbon }}>
{meta.label}
</div>
<div className={styles.cardHeader}>
<span className={styles.cardTitle}>{item.code}</span>
</div>
<div className={styles.cardBody}>
<div className={styles.row}>事件地点 <span className={styles.emphasis}>{item.reporter}</span> <span className={styles.muted}></span> <span className={styles.muted}>2025-02-05</span></div>
<div className={styles.row}>报警人 <span className={styles.muted}>{item.assignTo}</span></div>
<div className={styles.row}>报警方式 <span className={styles.muted}>{item.response}</span></div>
<div className={styles.row}>事件描述 <span className={styles.description}>{item.description}</span></div>
</div>
<div className={styles.cardFooter}>
<Button type="link" style={{ color: meta.actionColor }}>{meta.action}</Button>
<Button type="link" className={styles.secondaryAction}>不予立案</Button>
</div>
</div>
);
})}
</div>
</div>
);
};

@ -1,8 +1,192 @@
.placeholder {
.container {
background: #f6f8fb;
border-radius: 10px;
padding: 14px 14px 18px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 100%;
}
.statsRow {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.statCard {
background: #fff;
border-radius: 4px;
padding: 24px;
min-height: 72vh;
font-size: 16px;
border-radius: 10px;
padding: 12px 16px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 6px;
}
.statTitle {
color: #6c7681;
font-size: 12px;
}
.statValue {
font-size: 28px;
font-weight: 700;
color: #222;
}
.statTime {
color: #9fa6ad;
font-size: 12px;
}
.filterRow {
background: #fff;
border-radius: 10px;
padding: 10px 12px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.04);
}
.reportButton {
background: linear-gradient(98deg, #12d49c 0%, #0ec9c9 100%);
border: none;
border-radius: 8px;
padding: 0 16px;
height: 34px;
box-shadow: 0 6px 14px rgba(18, 212, 156, 0.25);
}
.filters {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 14px;
}
.filterTitle {
color: #666;
font-size: 13px;
margin-right: 6px;
}
.statusGroup {
:global(.ant-checkbox-wrapper) {
margin-right: 12px;
font-size: 13px;
color: #555;
}
:global(.ant-checkbox-inner) {
border-radius: 4px;
}
}
.rightFilters {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
.filterLabel {
color: #888;
font-size: 13px;
}
.select {
width: 120px;
:global(.ant-select-selector) {
border-radius: 6px !important;
height: 34px;
box-shadow: none !important;
}
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.card {
position: relative;
background: #fff;
border-radius: 10px;
border: 1px solid #eef1f6;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.04);
overflow: hidden;
min-height: 170px;
display: flex;
flex-direction: column;
}
.ribbon {
position: absolute;
top: 0;
right: 0;
color: #fff;
font-size: 12px;
padding: 8px 14px;
transform: translate(26px, -6px) rotate(45deg);
width: 92px;
text-align: center;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12);
}
.cardHeader {
padding: 14px 16px 6px;
}
.cardTitle {
font-size: 14px;
font-weight: 600;
color: #333;
}
.cardBody {
padding: 0 16px 10px;
display: flex;
flex-direction: column;
gap: 6px;
color: #555;
font-size: 12px;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.emphasis {
color: #12c48b;
font-weight: 600;
}
.muted {
color: #888;
}
.description {
color: #555;
}
.cardFooter {
margin-top: auto;
border-top: 1px solid #f0f2f5;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.secondaryAction {
color: #3e7aff;
}

@ -1,10 +1,175 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Button, Checkbox, Select } from 'antd';
import ReactECharts from 'echarts-for-react';
import styles from './StatisticsAnalysis.less';
const statusOptions = [
{ label: '环保活动', value: 'env' },
{ label: '环保事件', value: 'event' },
{ label: '环保事件', value: 'incident' },
];
const StatisticsAnalysis = () => {
const activityTypeOption = useMemo(
() => ({
color: ['#6abdfc', '#a585ff', '#fa8c16', '#ff6b6b'],
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: '70%' },
series: [
{
type: 'pie',
radius: ['30%', '60%'],
center: ['35%', '50%'],
label: { formatter: '{b}: {d}%' },
data: [
{ value: 30, name: '培训' },
{ value: 15, name: '演练' },
{ value: 10, name: '其他' },
{ value: 45, name: '宣传' },
],
},
],
}),
[]
);
const participationOption = useMemo(
() => ({
color: ['#6ee7d8', '#8fd0ff'],
tooltip: { trigger: 'axis' },
grid: { left: 80, right: 30, top: 30, bottom: 30 },
xAxis: {
type: 'value',
boundaryGap: [0, 0.1],
axisLine: { lineStyle: { color: '#d8dfe6' } },
},
yAxis: {
type: 'category',
data: ['xxx部门', 'xxx部门', 'xxx部门', 'xxx部门', 'xxx部门', 'xxx部门'],
axisLine: { lineStyle: { color: '#d8dfe6' } },
},
series: [
{
name: '参与率',
type: 'bar',
barWidth: 16,
data: [92, 88, 74, 56, 48, 30],
label: { show: false },
},
],
}),
[]
);
const incidentTypeOption = useMemo(
() => ({
color: ['#4b7bff', '#5fd6f1', '#2fd192', '#ff9f7f', '#e85d8b', '#5e6ce6'],
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', right: 10, top: 'middle' },
series: [
{
type: 'pie',
radius: ['40%', '65%'],
center: ['40%', '50%'],
label: { formatter: '{b} {d}%' },
data: [
{ value: 45, name: 'xxx2' },
{ value: 10, name: 'xxx3' },
{ value: 9, name: 'xxx5' },
{ value: 13, name: 'xxx4' },
{ value: 15, name: 'xxx3' },
{ value: 8, name: 'xxx6' },
],
},
],
}),
[]
);
const trendOption = useMemo(
() => ({
color: ['#6abdfc', '#f7a35c'],
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 50, top: 30, bottom: 40 },
xAxis: [{ type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] }],
yAxis: [
{ type: 'value', name: '活动数', axisLine: { lineStyle: { color: '#d8dfe6' } } },
{ type: 'value', name: '参与率', axisLine: { lineStyle: { color: '#d8dfe6' } } },
],
series: [
{
name: '活动数',
type: 'bar',
data: [8, 18, 12, 10, 13, 9, 11, 10, 12, 16, 9, 14],
barWidth: 14,
},
{
name: '参与率',
type: 'line',
yAxisIndex: 1,
data: [55, 60, 58, 63, 60, 62, 65, 63, 64, 68, 66, 70],
smooth: true,
},
],
}),
[]
);
const departmentOption = useMemo(
() => ({
color: ['#6abdfc', '#63e2b7'],
tooltip: { trigger: 'axis' },
legend: { top: 0 },
grid: { left: 50, right: 20, top: 40, bottom: 30 },
xAxis: [{ type: 'category', data: ['生产部', '运营部', '安全部', '后勤部', '质检部', '供应部'] }],
yAxis: [{ type: 'value' }],
series: [
{ name: '事件总数', type: 'bar', barWidth: 12, data: [35, 22, 28, 18, 20, 25] },
{ name: '处置完成', type: 'bar', barWidth: 12, data: [28, 18, 20, 14, 16, 19] },
],
}),
[]
);
return (
<div className={styles.placeholder}>
统计分析 待开发
<div className={styles.container}>
<div className={styles.toolbar}>
<Button type="primary">导出PDF</Button>
<div className={styles.toolbarFilters}>
<span className={styles.filterLabel}>事件状态</span>
<Checkbox.Group options={statusOptions} value={statusOptions.map((item) => item.value)} />
<span className={styles.filterLabel}>统计周期</span>
<Select defaultValue="2024年上半年" className={styles.select} options={[{ value: '2024H1', label: '2024年上半年' }, { value: '2024H2', label: '2024年下半年' }]} />
<span className={styles.filterLabel}>对比周期</span>
<Select defaultValue="去年同期" className={styles.select} options={[{ value: 'lastYear', label: '去年同期' }, { value: '去年全年', label: '去年全年' }]} />
</div>
</div>
<div className={styles.rowThree}>
<div className={styles.card}>
<div className={styles.cardTitle}>活动类型分布</div>
<ReactECharts option={activityTypeOption} style={{ height: 240 }} />
</div>
<div className={styles.card}>
<div className={styles.cardTitle}>活动参与热度</div>
<ReactECharts option={participationOption} style={{ height: 240 }} />
</div>
<div className={styles.card}>
<div className={styles.cardTitle}>事件类型分布</div>
<ReactECharts option={incidentTypeOption} style={{ height: 240 }} />
</div>
</div>
<div className={styles.rowTwo}>
<div className={styles.cardWide}>
<div className={styles.cardTitle}>月度活动趋势</div>
<ReactECharts option={trendOption} style={{ height: 240 }} />
</div>
<div className={styles.card}>
<div className={styles.cardTitle}>事件部门分布</div>
<ReactECharts option={departmentOption} style={{ height: 240 }} />
</div>
</div>
</div>
);
};

@ -1,8 +1,72 @@
.placeholder {
.container {
background: #f6f8fb;
border-radius: 10px;
padding: 14px 14px 18px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 100%;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border-radius: 4px;
padding: 24px;
min-height: 72vh;
font-size: 16px;
border-radius: 10px;
padding: 10px 12px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.04);
}
.toolbarFilters {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.filterLabel {
color: #666;
font-size: 13px;
}
.select {
width: 120px;
:global(.ant-select-selector) {
border-radius: 6px !important;
height: 34px;
box-shadow: none !important;
}
}
.rowThree {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.rowTwo {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 12px;
}
.card,
.cardWide {
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.04);
padding: 10px 12px 6px;
display: flex;
flex-direction: column;
}
.cardTitle {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}

Loading…
Cancel
Save