sg-paisou 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/GitCommitMessageStorage.xml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/sg-paisou-web.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/.vscode/settings.json +2 -0
- package/README.md +2 -0
- package/auto-imports.d.ts +10 -0
- package/components.d.ts +18 -0
- package/index.html +21 -0
- package/package.json +39 -0
- package/public/mathjax/all-packages.js +34 -0
- package/public/mathjax/talEditorConfig.js +5 -0
- package/public/mathjax/tex-svg.js +3 -0
- package/src/App.vue +38 -0
- package/src/assets/images/Camera-icon.png +0 -0
- package/src/assets/images/anger.png +0 -0
- package/src/assets/images/answer-icon.png +0 -0
- package/src/assets/images/back.png +0 -0
- package/src/assets/images/bg-img.png +0 -0
- package/src/assets/images/collect.png +0 -0
- package/src/assets/images/collected.png +0 -0
- package/src/assets/images/empty-bookmark.png +0 -0
- package/src/assets/images/empty-history.png +0 -0
- package/src/assets/images/feedback-thinkie-img.png +0 -0
- package/src/assets/images/history.png +0 -0
- package/src/assets/images/image 5.png +0 -0
- package/src/assets/images/insolubility.png +0 -0
- package/src/assets/images/key-points.png +0 -0
- package/src/assets/images/photograph-icon.png +0 -0
- package/src/assets/images/praise-icon.png +0 -0
- package/src/assets/images/praiseing-icon.png +0 -0
- package/src/assets/images/question.png +0 -0
- package/src/assets/images/smiling.png +0 -0
- package/src/assets/images/solution-icon.png +0 -0
- package/src/assets/images/star-icon.png +0 -0
- package/src/assets/images/trample-icon.png +0 -0
- package/src/assets/images/trampleing-icon.png +0 -0
- package/src/assets/images/volume-icon.png +0 -0
- package/src/assets/images/volumeing-icon.png +0 -0
- package/src/components/AIChatSDK/AIChatComponent.vue +963 -0
- package/src/components/AIChatSDK/component/Dialogs.vue +340 -0
- package/src/components/AIChatSDK/component/SpecialQuestions.vue +208 -0
- package/src/components/AIChatSDK/index.ts +146 -0
- package/src/components/AIChatSDK/style.scss +432 -0
- package/src/components/AIChatSDK/types.ts +275 -0
- package/src/components/AIChatSDK/utils/imageUtils.ts +61 -0
- package/src/components/AIChatSDK/utils/latex.ts +34 -0
- package/src/components/AIChatSDK/utils/mergeConfig.ts +125 -0
- package/src/components/ImagePreview.vue +62 -0
- package/src/components/PageHeader/index.vue +121 -0
- package/src/config.ts +11 -0
- package/src/env.d.ts +11 -0
- package/src/main.ts +12 -0
- package/src/router.ts +20 -0
- package/src/style.css +33 -0
- package/src/type.ts +106 -0
- package/src/utils/TTS_README.md +232 -0
- package/src/utils/bridge.ts +42 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/listenOsEvent.ts +3 -0
- package/src/utils/messageToast.ts +43 -0
- package/src/utils/render.ts +81 -0
- package/src/utils/request.ts +87 -0
- package/src/utils/tts.ts +319 -0
- package/src/utils/typewriter.ts +61 -0
- package/src/utils/useSSE.ts +113 -0
- package/src/views/History/index.vue +419 -0
- package/src/views/QuestionChatPage/index.vue +480 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.ts +41 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="history-page">
|
|
3
|
+
<PageHeader title="History" :back-type="fromSource" />
|
|
4
|
+
|
|
5
|
+
<div class="tabs">
|
|
6
|
+
<div
|
|
7
|
+
v-for="tab in tabs"
|
|
8
|
+
:key="tab.value"
|
|
9
|
+
:class="['tab-item', { active: activeTab === tab.value }]"
|
|
10
|
+
@click="activeTab = tab.value"
|
|
11
|
+
>
|
|
12
|
+
{{ tab.label }}
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="content" ref="contentRef" @scroll="handleScroll">
|
|
17
|
+
<div class="content-list">
|
|
18
|
+
<!--首次加载状态-->
|
|
19
|
+
<div v-if="isInitialLoading" class="loading-indicator">
|
|
20
|
+
<div class="loading-spinner"></div>
|
|
21
|
+
<span>Loading...</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- 数据列表 -->
|
|
25
|
+
<template v-else-if="currentGroups.length > 0">
|
|
26
|
+
<div v-for="(group, index) in currentGroups" :key="index" class="date-group">
|
|
27
|
+
<div class="date-title">{{ group.date }}</div>
|
|
28
|
+
<div class="items-grid">
|
|
29
|
+
<div
|
|
30
|
+
v-for="item in group.items"
|
|
31
|
+
:key="item.id"
|
|
32
|
+
class="history-item"
|
|
33
|
+
@click="handleItemClick(item)"
|
|
34
|
+
>
|
|
35
|
+
<div class="item-image-wrapper">
|
|
36
|
+
<img :src="item.imageUrl" :alt="item.title" class="item-image" />
|
|
37
|
+
<div v-if="item.isBookmarked" class="bookmark-badge">Bookmarked</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- 加载更多提示 -->
|
|
44
|
+
<div v-if="loading" class="loading-indicator">
|
|
45
|
+
<div class="loading-spinner"></div>
|
|
46
|
+
<span>Loading...</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- 没有更多数据 -->
|
|
50
|
+
<div v-if="!hasMore" class="no-more-data">
|
|
51
|
+
No more data
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<!-- 空状态(非首次加载且无数据) -->
|
|
56
|
+
<div v-else class="empty-state">
|
|
57
|
+
<img :src="emptyConfig.image" :alt="emptyConfig.alt" />
|
|
58
|
+
<span>{{ emptyConfig.text }}</span>
|
|
59
|
+
<span v-if="emptyConfig.description" class="empty-state-description">
|
|
60
|
+
{{ emptyConfig.description }}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script lang="ts" setup>
|
|
69
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
70
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
71
|
+
import PageHeader from '../../components/PageHeader/index.vue'
|
|
72
|
+
import type { IHistoryItem, IHistoryGroup, ISessionItem } from '../../type'
|
|
73
|
+
import { getSessions, getFavorites } from '../../utils/request'
|
|
74
|
+
import emptyHistoryImg from '@/assets/images/empty-history.png'
|
|
75
|
+
import emptyBookmarkImg from '@/assets/images/empty-bookmark.png'
|
|
76
|
+
|
|
77
|
+
const route = useRoute()
|
|
78
|
+
const router = useRouter()
|
|
79
|
+
|
|
80
|
+
// Tab 配置
|
|
81
|
+
const tabs = [
|
|
82
|
+
{ label: 'All', value: 'all' as const },
|
|
83
|
+
{ label: 'Bookmarks', value: 'bookmarks' as const }
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
const activeTab = ref<'all' | 'bookmarks'>('all')
|
|
87
|
+
const fromSource = ref<'client' | 'page'>(route.query.from === 'page' ? 'page' : 'client')
|
|
88
|
+
const contentRef = ref<HTMLElement>()
|
|
89
|
+
const loading = ref(false)
|
|
90
|
+
const pageSize = 10
|
|
91
|
+
|
|
92
|
+
// 为每个 Tab 独立维护分页状态
|
|
93
|
+
const pagingState = ref({
|
|
94
|
+
all: { currentPage: 1, hasMore: true, total: 0 },
|
|
95
|
+
bookmarks: { currentPage: 1, hasMore: true, total: 0 }
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// 为每个 Tab 独立维护初始加载状态
|
|
99
|
+
const initialLoadingState = ref({
|
|
100
|
+
all: true,
|
|
101
|
+
bookmarks: true
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// 数据存储
|
|
105
|
+
const historyData = ref<IHistoryItem[]>([])
|
|
106
|
+
const bookmarksData = ref<IHistoryItem[]>([])
|
|
107
|
+
|
|
108
|
+
// 当前 Tab 的分页状态
|
|
109
|
+
const currentPaging = computed(() => pagingState.value[activeTab.value])
|
|
110
|
+
const hasMore = computed(() => currentPaging.value.hasMore)
|
|
111
|
+
const isInitialLoading = computed(() => initialLoadingState.value[activeTab.value])
|
|
112
|
+
|
|
113
|
+
// 公共数据分组函数
|
|
114
|
+
const groupByDate = (items: IHistoryItem[]): IHistoryGroup[] => {
|
|
115
|
+
const groups: Record<string, IHistoryItem[]> = {}
|
|
116
|
+
items.forEach(item => {
|
|
117
|
+
(groups[item.date] ||= []).push(item)
|
|
118
|
+
})
|
|
119
|
+
return Object.entries(groups).map(([date, items]) => ({ date, items }))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 当前 Tab 的分组数据
|
|
123
|
+
const currentGroups = computed(() =>
|
|
124
|
+
groupByDate(activeTab.value === 'all' ? historyData.value : bookmarksData.value)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// 空状态配置
|
|
128
|
+
const emptyConfig = computed(() =>
|
|
129
|
+
activeTab.value === 'all'
|
|
130
|
+
? {
|
|
131
|
+
image: emptyHistoryImg,
|
|
132
|
+
alt: 'empty-history',
|
|
133
|
+
text: 'No search history.'
|
|
134
|
+
}
|
|
135
|
+
: {
|
|
136
|
+
image: emptyBookmarkImg,
|
|
137
|
+
alt: 'empty-bookmark',
|
|
138
|
+
text: 'No bookmarks yet',
|
|
139
|
+
description: 'Find a result and tap the bookmark icon to save it here'
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
// 格式化日期
|
|
145
|
+
const formatDate = (dateString: string): string => {
|
|
146
|
+
const date = new Date(dateString)
|
|
147
|
+
const today = new Date()
|
|
148
|
+
const dateStr = date.toDateString()
|
|
149
|
+
|
|
150
|
+
if (dateStr === today.toDateString()) return 'Today'
|
|
151
|
+
|
|
152
|
+
const yesterday = new Date(today)
|
|
153
|
+
yesterday.setDate(yesterday.getDate() - 1)
|
|
154
|
+
if (dateStr === yesterday.toDateString()) return 'Yesterday'
|
|
155
|
+
|
|
156
|
+
return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${date.getFullYear()}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 判断是否超过7天
|
|
160
|
+
const isOverSevenDays = (dateString: string): 0 | 1 => {
|
|
161
|
+
const diffDays = Math.floor((Date.now() - new Date(dateString).getTime()) / (86400000))
|
|
162
|
+
return diffDays > 7 ? 1 : 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 转换 API 数据
|
|
166
|
+
const convertSessionToHistoryItem = (session: ISessionItem): IHistoryItem => ({
|
|
167
|
+
id: session.id,
|
|
168
|
+
title: session.content || session.summary || session.orcText || 'Untitled',
|
|
169
|
+
imageUrl: session.imagePath || '',
|
|
170
|
+
isBookmarked: session.favorite === 1,
|
|
171
|
+
date: formatDate(session.createdAt),
|
|
172
|
+
isOverSevenDays: isOverSevenDays(session.createdAt),
|
|
173
|
+
questionData: session
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// 点击历史记录项
|
|
177
|
+
const handleItemClick = (item: IHistoryItem) => {
|
|
178
|
+
try {
|
|
179
|
+
sessionStorage.setItem('selectedHistoryItem', JSON.stringify(item))
|
|
180
|
+
router.push({
|
|
181
|
+
path: '/',
|
|
182
|
+
query: { from: 'history', itemId: item.id, isOverSevenDays: String(item.isOverSevenDays) }
|
|
183
|
+
})
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('存储历史记录项数据失败:', error)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 加载数据(统一处理历史记录和书签)
|
|
190
|
+
const loadData = async (type: 'all' | 'bookmarks', page: number = 1) => {
|
|
191
|
+
loading.value = true
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const apiCall = type === 'all' ? getSessions : getFavorites
|
|
195
|
+
const response = await apiCall(
|
|
196
|
+
{ page, pageSize },
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if (response?.sessions) {
|
|
200
|
+
const newItems = response.sessions.map(convertSessionToHistoryItem)
|
|
201
|
+
const targetData = type === 'all' ? historyData : bookmarksData
|
|
202
|
+
|
|
203
|
+
targetData.value = page === 1 ? newItems : [...targetData.value, ...newItems]
|
|
204
|
+
|
|
205
|
+
// 更新对应 Tab 的分页状态
|
|
206
|
+
pagingState.value[type].total = response.total
|
|
207
|
+
pagingState.value[type].currentPage = page
|
|
208
|
+
pagingState.value[type].hasMore = targetData.value.length < response.total
|
|
209
|
+
|
|
210
|
+
// 首次加载完成,关闭初始加载状态
|
|
211
|
+
if (page === 1) {
|
|
212
|
+
initialLoadingState.value[type] = false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`加载${type === 'all' ? '历史记录' : '书签'}第 ${page} 页,当前共 ${targetData.value.length}/${response.total} 条`)
|
|
216
|
+
} else {
|
|
217
|
+
// 如果没有数据,也关闭初始加载状态
|
|
218
|
+
if (page === 1) {
|
|
219
|
+
initialLoadingState.value[type] = false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(`加载${type === 'all' ? '历史记录' : '书签'}失败:`, error)
|
|
224
|
+
pagingState.value[type].hasMore = false
|
|
225
|
+
// 加载失败时也关闭初始加载状态,以便显示空状态
|
|
226
|
+
if (page === 1) {
|
|
227
|
+
initialLoadingState.value[type] = false
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
loading.value = false
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 加载更多数据
|
|
235
|
+
const loadMore = async () => {
|
|
236
|
+
if (loading.value || !currentPaging.value.hasMore) return
|
|
237
|
+
|
|
238
|
+
const nextPage = currentPaging.value.currentPage + 1
|
|
239
|
+
await loadData(activeTab.value, nextPage)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 处理滚动事件
|
|
243
|
+
const handleScroll = (event: Event) => {
|
|
244
|
+
const target = event.target as HTMLElement
|
|
245
|
+
if (!target) return
|
|
246
|
+
|
|
247
|
+
const { scrollTop, scrollHeight, clientHeight } = target
|
|
248
|
+
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
249
|
+
loadMore()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 监听 Tab 切换
|
|
254
|
+
watch(activeTab, async (newTab) => {
|
|
255
|
+
// 重置滚动位置
|
|
256
|
+
contentRef.value && (contentRef.value.scrollTop = 0)
|
|
257
|
+
|
|
258
|
+
// 如果该 Tab 还没有数据,则加载第一页
|
|
259
|
+
const targetData = newTab === 'all' ? historyData : bookmarksData
|
|
260
|
+
if (targetData.value.length === 0) {
|
|
261
|
+
// 如果还没有数据,确保初始加载状态为 true
|
|
262
|
+
initialLoadingState.value[newTab] = true
|
|
263
|
+
await loadData(newTab, 1)
|
|
264
|
+
} else {
|
|
265
|
+
// 如果已有数据,确保初始加载状态为 false
|
|
266
|
+
initialLoadingState.value[newTab] = false
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
onMounted(() => {
|
|
271
|
+
loadData('all', 1)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
<style scoped lang="scss">
|
|
277
|
+
.history-page {
|
|
278
|
+
width: 100%;
|
|
279
|
+
height: 100vh;
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
background: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7));
|
|
283
|
+
|
|
284
|
+
.tabs {
|
|
285
|
+
display: flex;
|
|
286
|
+
gap: 12px;
|
|
287
|
+
padding: 0 48px 12px;
|
|
288
|
+
|
|
289
|
+
.tab-item {
|
|
290
|
+
padding: 8px 16px;
|
|
291
|
+
font-size: 16px;
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
color: #666;
|
|
294
|
+
background: #FFF;
|
|
295
|
+
border-radius: 70px;
|
|
296
|
+
line-height: 24px;
|
|
297
|
+
|
|
298
|
+
&.active {
|
|
299
|
+
color: #FF9D00;
|
|
300
|
+
background: rgba(255, 157, 0, 0.1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.content {
|
|
306
|
+
flex: 1;
|
|
307
|
+
padding: 12px 48px 24px;
|
|
308
|
+
overflow-y: auto;
|
|
309
|
+
|
|
310
|
+
.content-list {
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-direction: column;
|
|
313
|
+
gap: 24px;
|
|
314
|
+
min-height: 100%;
|
|
315
|
+
|
|
316
|
+
.date-group {
|
|
317
|
+
.date-title {
|
|
318
|
+
font-size: 20px;
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
color: #222;
|
|
321
|
+
line-height: 32px;
|
|
322
|
+
margin-bottom: 12px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.items-grid {
|
|
326
|
+
display: grid;
|
|
327
|
+
grid-template-columns: repeat(2, 1fr);
|
|
328
|
+
gap: 24px;
|
|
329
|
+
|
|
330
|
+
.history-item {
|
|
331
|
+
|
|
332
|
+
.item-image-wrapper {
|
|
333
|
+
position: relative;
|
|
334
|
+
height: 180px;
|
|
335
|
+
border-radius: 12px;
|
|
336
|
+
overflow: hidden;
|
|
337
|
+
|
|
338
|
+
.item-image {
|
|
339
|
+
width: 100%;
|
|
340
|
+
height: 100%;
|
|
341
|
+
object-fit: cover;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.bookmark-badge {
|
|
345
|
+
position: absolute;
|
|
346
|
+
top: 16px;
|
|
347
|
+
left: 16px;
|
|
348
|
+
font-size: 18px;
|
|
349
|
+
line-height: 18px;
|
|
350
|
+
color: #FFF;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.empty-state {
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-direction: column;
|
|
360
|
+
align-items: center;
|
|
361
|
+
justify-content: center;
|
|
362
|
+
gap: 20px;
|
|
363
|
+
flex: 1;
|
|
364
|
+
|
|
365
|
+
img {
|
|
366
|
+
width: 300px;
|
|
367
|
+
height: 200px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
span {
|
|
371
|
+
font-size: 20px;
|
|
372
|
+
font-weight: 500;
|
|
373
|
+
line-height: 24px;
|
|
374
|
+
color: #222;
|
|
375
|
+
|
|
376
|
+
&.empty-state-description {
|
|
377
|
+
font-size: 16px;
|
|
378
|
+
color: #999;
|
|
379
|
+
margin-top: -8px;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.loading-indicator {
|
|
385
|
+
display: flex;
|
|
386
|
+
flex-direction: column;
|
|
387
|
+
align-items: center;
|
|
388
|
+
justify-content: center;
|
|
389
|
+
gap: 12px;
|
|
390
|
+
text-align: center;
|
|
391
|
+
font-size: 16px;
|
|
392
|
+
color: #666;
|
|
393
|
+
min-height: 200px;
|
|
394
|
+
|
|
395
|
+
.loading-spinner {
|
|
396
|
+
width: 20px;
|
|
397
|
+
height: 20px;
|
|
398
|
+
border: 4px solid rgba(255, 157, 0, 0.2);
|
|
399
|
+
border-top-color: #FF9D00;
|
|
400
|
+
border-radius: 50%;
|
|
401
|
+
animation: spin 1s linear infinite;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.no-more-data {
|
|
406
|
+
text-align: center;
|
|
407
|
+
padding-bottom: 24px;
|
|
408
|
+
font-size: 16px;
|
|
409
|
+
color: #999;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@keyframes spin {
|
|
416
|
+
to { transform: rotate(360deg); }
|
|
417
|
+
}
|
|
418
|
+
</style>
|
|
419
|
+
|