+ )}
+
{hasMoreMessages && (
{isLoadingMore ? (
@@ -772,7 +1245,6 @@ function ChatPage(_props: ChatPageProps) {
回到底部
- )}
{/* 会话详情面板 */}
{showDetailPanel && (
diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss
index 588535f..12fe9d2 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 {
@@ -471,6 +463,43 @@
margin: 8px 0 0;
}
+ .select-folder-btn {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 10px 16px;
+ margin-top: 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);
+
+ svg {
+ color: var(--primary);
+ }
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+
+ svg {
+ color: var(--text-secondary);
+ transition: color 0.2s;
+ }
+ }
+
.export-action {
padding: 20px 24px;
border-top: 1px solid var(--border-color);
@@ -649,9 +678,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 e39dd07..a8fc14f 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'
@@ -15,6 +15,7 @@ interface ExportOptions {
format: 'chatlab' | 'chatlab-jsonl' | 'json' | 'html' | 'txt' | 'excel' | 'sql'
dateRange: { start: Date; end: Date } | null
useAllTime: boolean
+ exportAvatars: boolean
}
interface ExportResult {
@@ -34,14 +35,18 @@ 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: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
end: new Date()
},
- useAllTime: true
+ useAllTime: true,
+ exportAvatars: true
})
const loadSessions = useCallback(async () => {
@@ -140,9 +145,11 @@ function ExportPage() {
const sessionList = Array.from(selectedSessions)
const exportOptions = {
format: options.format,
+ 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
}
@@ -164,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: '流式格式,适合大量消息' },
@@ -278,24 +333,55 @@ function ExportPage() {
导出全部时间
{!options.useAllTime && options.dateRange && (
-
+
setShowDatePicker(true)}>
{formatDate(options.dateRange.start)} - {formatDate(options.dateRange.end)}
-
+
)}