|
|
@@ -0,0 +1,322 @@
|
|
|
+import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|
|
+import { AgGridReact } from 'ag-grid-react';
|
|
|
+import {
|
|
|
+ ClientSideRowModelModule,
|
|
|
+ ColDef,
|
|
|
+ ModuleRegistry,
|
|
|
+ PaginationModule,
|
|
|
+ ValueFormatterParams,
|
|
|
+ GridReadyEvent,
|
|
|
+} from 'ag-grid-community';
|
|
|
+import styles from './EstateExport.module.css'; // 改为 CSS Modules
|
|
|
+
|
|
|
+// 1. 类型定义(补全接口,确保类型安全)
|
|
|
+export interface IEstateExportHistory {
|
|
|
+ id: string;
|
|
|
+ exportDate: string; // yyyymmdd 格式
|
|
|
+ exporter: string; // 担当者名
|
|
|
+ exportContent: string; // 1:物件マスタCSV, 2:分析用CSV
|
|
|
+}
|
|
|
+
|
|
|
+// 2. AG Grid 模块注册(保持原有逻辑,补充注释)
|
|
|
+ModuleRegistry.registerModules([
|
|
|
+ PaginationModule, // 分页模块
|
|
|
+ ClientSideRowModelModule // 客户端行模型(本地数据分页)
|
|
|
+]);
|
|
|
+
|
|
|
+// 3. 枚举与映射配置(保持原有,补充类型约束)
|
|
|
+export enum ExportContentType {
|
|
|
+ PROPERTY_MASTER = '1',
|
|
|
+ ANALYSIS = '2'
|
|
|
+}
|
|
|
+
|
|
|
+const EXPORT_CONTENT_MAP: Record<ExportContentType, string> = {
|
|
|
+ [ExportContentType.PROPERTY_MASTER]: '物件マスタCSV',
|
|
|
+ [ExportContentType.ANALYSIS]: '分析用CSV'
|
|
|
+};
|
|
|
+
|
|
|
+// 4. 工具函数(抽离通用逻辑,提高复用性)
|
|
|
+/**
|
|
|
+ * 日期格式验证(yyyymmdd)
|
|
|
+ * @param date - 待验证日期字符串
|
|
|
+ * @returns 验证结果
|
|
|
+ */
|
|
|
+const validateDate = (date: string): boolean => {
|
|
|
+ const regex = /^\d{8}$/;
|
|
|
+ if (!regex.test(date)) return false;
|
|
|
+
|
|
|
+ const year = parseInt(date.substring(0, 4), 10);
|
|
|
+ const month = parseInt(date.substring(4, 6), 10) - 1; // 月份从 0 开始
|
|
|
+ const day = parseInt(date.substring(6, 8), 10);
|
|
|
+
|
|
|
+ const validDate = new Date(year, month, day);
|
|
|
+ return (
|
|
|
+ validDate.getFullYear() === year &&
|
|
|
+ validDate.getMonth() === month &&
|
|
|
+ validDate.getDate() === day
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化日期显示(yyyymmdd → yyyy/mm/dd)
|
|
|
+ * @param date - 原始日期字符串
|
|
|
+ * @returns 格式化后的日期
|
|
|
+ */
|
|
|
+const formatDisplayDate = (date: string): string => {
|
|
|
+ if (!date || date.length !== 8) return '';
|
|
|
+ return `${date.slice(0, 4)}/${date.slice(4, 6)}/${date.slice(6, 8)}`;
|
|
|
+};
|
|
|
+
|
|
|
+const EstateExportHistory: React.FC = () => {
|
|
|
+ // 5. 状态管理(补充类型约束,初始化更合理)
|
|
|
+ const [baseDate, setBaseDate] = useState<string>('');
|
|
|
+ const [selectedContent, setSelectedContent] = useState<ExportContentType>(
|
|
|
+ ExportContentType.ANALYSIS
|
|
|
+ );
|
|
|
+ const [historyList, setHistoryList] = useState<IEstateExportHistory[]>([]);
|
|
|
+ const [isExporting, setIsExporting] = useState<boolean>(false);
|
|
|
+ const [gridApi, setGridApi] = useState<any>(null); // AG Grid API 实例
|
|
|
+ const [isLoading, setIsLoading] = useState<boolean>(true); // 列表加载状态
|
|
|
+
|
|
|
+ // 6. 样式配置(使用 CSS Modules,避免全局污染)
|
|
|
+ const containerStyle = useMemo(() => ({
|
|
|
+ width: '100%',
|
|
|
+ height: '300px',
|
|
|
+ borderRadius: '4px',
|
|
|
+ overflow: 'hidden'
|
|
|
+ }), []);
|
|
|
+
|
|
|
+ // 7. AG Grid 配置(优化默认列配置,补充缺失功能)
|
|
|
+ const defaultColDef = useMemo<ColDef>(() => ({
|
|
|
+ flex: 1,
|
|
|
+ minWidth: 120,
|
|
|
+ sortable: true, // 开启排序(提升体验)
|
|
|
+ filter: true, // 开启筛选(提升体验)
|
|
|
+ resizable: true,
|
|
|
+ menuTabs: ['filterMenuTab'], // 只显示筛选菜单(简化操作)
|
|
|
+ cellStyle: { display: 'flex', alignItems: 'center' } // 单元格垂直居中
|
|
|
+ }), []);
|
|
|
+
|
|
|
+ const paginationPageSize = useMemo(() => 5, []); // 调整为每页 5 条(更合理)
|
|
|
+
|
|
|
+ // 8. 列定义(优化格式化逻辑,抽离为独立函数)
|
|
|
+ const columnDefs = useMemo<ColDef<IEstateExportHistory>[]>(() => [
|
|
|
+ {
|
|
|
+ headerName: '出力日付',
|
|
|
+ field: 'exportDate',
|
|
|
+ flex: 1.2,
|
|
|
+ valueFormatter: (params: ValueFormatterParams<IEstateExportHistory>) =>
|
|
|
+ formatDisplayDate(params.value),
|
|
|
+ sort: 'desc' // 默认按日期降序排序
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headerName: '担当者',
|
|
|
+ field: 'exporter',
|
|
|
+ flex: 1,
|
|
|
+ filter: 'agTextColumnFilter',
|
|
|
+ filterParams: { matchContains: true } // 包含匹配(更灵活)
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headerName: '出力内容',
|
|
|
+ field: 'exportContent',
|
|
|
+ flex: 1.5,
|
|
|
+ valueFormatter: (params: ValueFormatterParams<IEstateExportHistory>) =>
|
|
|
+ EXPORT_CONTENT_MAP[params.value as ExportContentType] || '不明',
|
|
|
+ filter: 'agSetColumnFilter', // 下拉筛选(更直观)
|
|
|
+ filterParams: {
|
|
|
+ values: Object.values(ExportContentType).map(key =>
|
|
|
+ EXPORT_CONTENT_MAP[key as ExportContentType]
|
|
|
+ ),
|
|
|
+ cellRenderer: (params: any) =>
|
|
|
+ EXPORT_CONTENT_MAP[params.value as ExportContentType] || '不明'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ], []);
|
|
|
+
|
|
|
+ // 9. 初始化数据(优化加载状态,模拟真实接口延迟)
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchHistory = async () => {
|
|
|
+ try {
|
|
|
+ // 模拟接口请求延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 600));
|
|
|
+
|
|
|
+ const mockBackendData: IEstateExportHistory[] = [
|
|
|
+ { id: '1', exportDate: '20270812', exporter: 'BBBB', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ { id: '2', exportDate: '20250814', exporter: 'AAAA', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ { id: '3', exportDate: '20250813', exporter: 'AAAA', exportContent: ExportContentType.PROPERTY_MASTER },
|
|
|
+ { id: '4', exportDate: '20250812', exporter: 'CCCC', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ { id: '5', exportDate: '20250811', exporter: 'DDDD', exportContent: ExportContentType.PROPERTY_MASTER },
|
|
|
+ { id: '6', exportDate: '20250810', exporter: 'AAAA', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ { id: '7', exportDate: '20250809', exporter: 'BBBB', exportContent: ExportContentType.PROPERTY_MASTER },
|
|
|
+ { id: '8', exportDate: '20250808', exporter: 'CCCC', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ { id: '9', exportDate: '20250807', exporter: 'DDDD', exportContent: ExportContentType.PROPERTY_MASTER },
|
|
|
+ { id: '10', exportDate: '20250806', exporter: 'AAAA', exportContent: ExportContentType.ANALYSIS },
|
|
|
+ ];
|
|
|
+
|
|
|
+ setHistoryList(mockBackendData);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('履歴データの取得に失敗しました', error);
|
|
|
+ alert('履歴データの取得に失敗しました。再試行してください。');
|
|
|
+ } finally {
|
|
|
+ setIsLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchHistory();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 10. Grid 初始化回调(保存 API 实例,用于后续操作)
|
|
|
+ const onGridReady = useCallback((params: GridReadyEvent<IEstateExportHistory>) => {
|
|
|
+ setGridApi(params.api);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 11. 导出处理逻辑(优化异步流程,增强用户反馈)
|
|
|
+ const handleExport = useCallback(async () => {
|
|
|
+ if (isExporting) return;
|
|
|
+
|
|
|
+ // 验证基準日
|
|
|
+ if (!baseDate) {
|
|
|
+ alert('基準日を入力してください');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!validateDate(baseDate)) {
|
|
|
+ alert('基準日はyyyymmddの形式で正しい日付を入力してください');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsExporting(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 模拟接口请求(实际项目中替换为 axios/fetch)
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
+
|
|
|
+ const newHistory: IEstateExportHistory = {
|
|
|
+ id: Date.now().toString(),
|
|
|
+ exportDate: baseDate,
|
|
|
+ exporter: '現在ユーザー',
|
|
|
+ exportContent: selectedContent
|
|
|
+ };
|
|
|
+
|
|
|
+ // 新增数据添加到列表顶部,并保持排序
|
|
|
+ setHistoryList(prev => [newHistory, ...prev].sort((a, b) =>
|
|
|
+ b.exportDate.localeCompare(a.exportDate)
|
|
|
+ ));
|
|
|
+
|
|
|
+ // 刷新 Grid 视图
|
|
|
+ gridApi?.refreshCells();
|
|
|
+ alert('エクスポートが完了しました');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('エクスポートに失敗しました', error);
|
|
|
+ alert('エクスポートに失敗しました。再試行してください。');
|
|
|
+ } finally {
|
|
|
+ setBaseDate('');
|
|
|
+ setIsExporting(false);
|
|
|
+ }
|
|
|
+ }, [baseDate, selectedContent, isExporting, gridApi]);
|
|
|
+
|
|
|
+ // 12. 日期输入处理(限制输入格式,增强用户体验)
|
|
|
+ const handleDateChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
+ // 只允许输入数字,且长度不超过 8
|
|
|
+ const value = e.target.value.replace(/\D/g, '').slice(0, 8);
|
|
|
+ setBaseDate(value);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className={styles.container}>
|
|
|
+ <h2 className={styles.title}>エクスポート</h2>
|
|
|
+
|
|
|
+ {/* 导出操作区 */}
|
|
|
+ <div className={styles.exportSection}>
|
|
|
+ <h3 className={styles.subtitle}>エクスポート操作</h3>
|
|
|
+ <div className={styles.formGroup}>
|
|
|
+ <div className={styles.formItem}>
|
|
|
+ <label className={styles.label}>基準日</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ className={styles.input}
|
|
|
+ placeholder="yyyymmdd"
|
|
|
+ value={baseDate}
|
|
|
+ onChange={handleDateChange}
|
|
|
+ maxLength={8}
|
|
|
+ disabled={isExporting}
|
|
|
+ aria-label="基準日入力"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className={styles.formItem}>
|
|
|
+ <label className={styles.label}>出力内容</label>
|
|
|
+ <div className={styles.radioGroup}>
|
|
|
+ <label className={styles.radioItem}>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="exportContent"
|
|
|
+ value={ExportContentType.PROPERTY_MASTER}
|
|
|
+ checked={selectedContent === ExportContentType.PROPERTY_MASTER}
|
|
|
+ onChange={() => setSelectedContent(ExportContentType.PROPERTY_MASTER)}
|
|
|
+ disabled={isExporting}
|
|
|
+ />
|
|
|
+ <span className={styles.radioLabel}>物件マスタCSV</span>
|
|
|
+ </label>
|
|
|
+ <label className={styles.radioItem}>
|
|
|
+ <input
|
|
|
+ type="radio"
|
|
|
+ name="exportContent"
|
|
|
+ value={ExportContentType.ANALYSIS}
|
|
|
+ checked={selectedContent === ExportContentType.ANALYSIS}
|
|
|
+ onChange={() => setSelectedContent(ExportContentType.ANALYSIS)}
|
|
|
+ disabled={isExporting}
|
|
|
+ />
|
|
|
+ <span className={styles.radioLabel}>分析用CSV</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ className={`${styles.exportBtn} ${isExporting ? styles.disabledBtn : ''}`}
|
|
|
+ onClick={handleExport}
|
|
|
+ disabled={isExporting}
|
|
|
+ >
|
|
|
+ {isExporting ? '処理中...' : '出力'}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 导出历史区 */}
|
|
|
+ <div className={styles.historySection}>
|
|
|
+ <h3 className={styles.subtitle}>出力履歴</h3>
|
|
|
+ <div className={styles.underline}></div>
|
|
|
+
|
|
|
+ {isLoading ? (
|
|
|
+ // 加载状态
|
|
|
+ <div className={styles.loading}>
|
|
|
+ <span>履歴データを読み込んでいます...</span>
|
|
|
+ </div>
|
|
|
+ ) : historyList.length === 0 ? (
|
|
|
+ // 空状态
|
|
|
+ <div className={styles.emptyState}>
|
|
|
+ <span>出力履歴がありません</span>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ // AG Grid 表格
|
|
|
+ <div className={`ag-theme-alpine ${styles.gridContainer}`} style={containerStyle}>
|
|
|
+ <AgGridReact<IEstateExportHistory>
|
|
|
+ rowData={historyList}
|
|
|
+ columnDefs={columnDefs}
|
|
|
+ defaultColDef={defaultColDef}
|
|
|
+ pagination={true}
|
|
|
+ paginationPageSize={paginationPageSize}
|
|
|
+ paginationPageSizeSelector={[5, 10, 20]}
|
|
|
+ onGridReady={onGridReady}
|
|
|
+ animateRows={true}
|
|
|
+ suppressDragLeaveHidesColumns={true}
|
|
|
+ enableCellTextSelection={true}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default EstateExportHistory;
|