diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 588535f..596fcd4 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -379,29 +379,21 @@ border-radius: 10px; font-size: 14px; color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + } svg { color: var(--text-tertiary); + flex-shrink: 0; } span { flex: 1; } - - .change-btn { - background: none; - border: none; - padding: 4px; - cursor: pointer; - color: var(--text-tertiary); - display: flex; - align-items: center; - justify-content: center; - - &:hover { - color: var(--text-primary); - } - } } .media-options { @@ -649,9 +641,245 @@ } } } + + .date-picker-modal { + background: var(--card-bg); + padding: 28px 32px; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); + min-width: 420px; + max-width: 500px; + + h3 { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 20px; + } + + .quick-select { + display: flex; + gap: 8px; + margin-bottom: 20px; + + .quick-btn { + flex: 1; + padding: 10px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + border-color: var(--primary); + color: var(--primary); + } + + &:active { + transform: scale(0.98); + } + } + } + + .date-display { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: var(--bg-secondary); + border-radius: 12px; + margin-bottom: 24px; + + .date-display-item { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: rgba(var(--primary-rgb), 0.05); + } + + &.active { + background: rgba(var(--primary-rgb), 0.1); + border: 1px solid var(--primary); + } + + .date-label { + font-size: 12px; + color: var(--text-tertiary); + font-weight: 500; + } + + .date-value { + font-size: 15px; + color: var(--text-primary); + font-weight: 600; + } + } + + .date-separator { + font-size: 14px; + color: var(--text-tertiary); + padding: 0 4px; + } + } + + .calendar-container { + margin-bottom: 20px; + } + + .calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding: 0 4px; + + .calendar-nav-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + border-color: var(--primary); + color: var(--primary); + } + + &:active { + transform: scale(0.95); + } + } + + .calendar-month { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + } + } + + .calendar-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; + margin-bottom: 8px; + + .calendar-weekday { + text-align: center; + font-size: 12px; + font-weight: 500; + color: var(--text-tertiary); + padding: 8px 0; + } + } + + .calendar-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; + + .calendar-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: var(--text-primary); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + position: relative; + + &.empty { + cursor: default; + } + + &:not(.empty):hover { + background: var(--bg-hover); + } + + &.in-range { + background: rgba(var(--primary-rgb), 0.08); + } + + &.start, + &.end { + background: var(--primary); + color: #fff; + font-weight: 600; + + &:hover { + background: var(--primary-hover); + } + } + } + } + + .date-picker-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + + button { + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:active { + transform: scale(0.98); + } + } + + .cancel-btn { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + + &:hover { + background: var(--bg-hover); + } + } + + .confirm-btn { + background: var(--primary); + color: #fff; + border: none; + + &:hover { + background: var(--primary-hover); + } + } + } + } } @keyframes exportSpin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 105d29c..8fb4fd4 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { Search, Download, FolderOpen, RefreshCw, Check, Calendar, FileJson, FileText, Table, Loader2, X, ChevronDown, FileSpreadsheet, Database, FileCode, CheckCircle, XCircle, ExternalLink } from 'lucide-react' +import { Search, Download, FolderOpen, RefreshCw, Check, Calendar, FileJson, FileText, Table, Loader2, X, ChevronDown, ChevronLeft, ChevronRight, FileSpreadsheet, Database, FileCode, CheckCircle, XCircle, ExternalLink } from 'lucide-react' import * as configService from '../services/config' import './ExportPage.scss' @@ -35,7 +35,10 @@ function ExportPage() { const [isExporting, setIsExporting] = useState(false) const [exportProgress, setExportProgress] = useState({ current: 0, total: 0, currentName: '' }) const [exportResult, setExportResult] = useState(null) - + const [showDatePicker, setShowDatePicker] = useState(false) + const [calendarDate, setCalendarDate] = useState(new Date()) + const [selectingStart, setSelectingStart] = useState(true) + const [options, setOptions] = useState({ format: 'chatlab', dateRange: { @@ -145,7 +148,8 @@ function ExportPage() { exportAvatars: options.exportAvatars, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), - end: Math.floor(options.dateRange.end.getTime() / 1000) + // 将结束日期设置为当天的 23:59:59,以包含当天的所有消息 + end: Math.floor(new Date(options.dateRange.end.getFullYear(), options.dateRange.end.getMonth(), options.dateRange.end.getDate(), 23, 59, 59).getTime() / 1000) } : null } @@ -167,6 +171,54 @@ function ExportPage() { } } + const getDaysInMonth = (date: Date) => { + const year = date.getFullYear() + const month = date.getMonth() + return new Date(year, month + 1, 0).getDate() + } + + const getFirstDayOfMonth = (date: Date) => { + const year = date.getFullYear() + const month = date.getMonth() + return new Date(year, month, 1).getDay() + } + + const generateCalendar = () => { + const daysInMonth = getDaysInMonth(calendarDate) + const firstDay = getFirstDayOfMonth(calendarDate) + const days: (number | null)[] = [] + + for (let i = 0; i < firstDay; i++) { + days.push(null) + } + + for (let i = 1; i <= daysInMonth; i++) { + days.push(i) + } + + return days + } + + const handleDateSelect = (day: number) => { + const year = calendarDate.getFullYear() + const month = calendarDate.getMonth() + const selectedDate = new Date(year, month, day) + + if (selectingStart) { + setOptions({ + ...options, + dateRange: options.dateRange ? { ...options.dateRange, start: selectedDate } : { start: selectedDate, end: new Date() } + }) + setSelectingStart(false) + } else { + setOptions({ + ...options, + dateRange: options.dateRange ? { ...options.dateRange, end: selectedDate } : { start: new Date(), end: selectedDate } + }) + setSelectingStart(true) + } + } + const formatOptions = [ { value: 'chatlab', label: 'ChatLab', icon: FileCode, desc: '标准格式,支持其他软件导入' }, { value: 'chatlab-jsonl', label: 'ChatLab JSONL', icon: FileCode, desc: '流式格式,适合大量消息' }, @@ -281,12 +333,10 @@ function ExportPage() { 导出全部时间 {!options.useAllTime && options.dateRange && ( -
+
setShowDatePicker(true)}> {formatDate(options.dateRange.start)} - {formatDate(options.dateRange.end)} - +
)}
@@ -387,6 +437,130 @@ function ExportPage() { )} + + {/* 日期选择弹窗 */} + {showDatePicker && ( +
setShowDatePicker(false)}> +
e.stopPropagation()}> +

选择时间范围

+
+ + + +
+
+
setSelectingStart(true)} + > + 开始日期 + + {options.dateRange?.start.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + })} + +
+ +
setSelectingStart(false)} + > + 结束日期 + + {options.dateRange?.end.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + })} + +
+
+
+
+ + + {calendarDate.getFullYear()}年{calendarDate.getMonth() + 1}月 + + +
+
+ {['日', '一', '二', '三', '四', '五', '六'].map(day => ( +
{day}
+ ))} +
+
+ {generateCalendar().map((day, index) => { + if (day === null) { + return
+ } + + const currentDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth(), day) + const isStart = options.dateRange?.start.toDateString() === currentDate.toDateString() + const isEnd = options.dateRange?.end.toDateString() === currentDate.toDateString() + const isInRange = options.dateRange && currentDate >= options.dateRange.start && currentDate <= options.dateRange.end + + return ( +
handleDateSelect(day)} + > + {day} +
+ ) + })} +
+
+
+ + +
+
+
+ )}
) }