Skip to content

Commit fa4c277

Browse files
committed
refactor(chat-ui): 拆分聊天页组件并抽离组合式逻辑
- 将聊天页从超大单文件拆分为会话列表、会话面板、消息列表、消息项和覆盖层组件\n- 抽离会话、消息、搜索、导出、编辑、历史窗口等组合式逻辑\n- 下沉消息格式化、消息归一化、聊天记录解析与文件图标等工具模块\n- 新增 chat.css 并在 Nuxt 中注册,统一聊天页样式
1 parent 512d5ef commit fa4c277

21 files changed

+9638
-9382
lines changed

frontend/assets/css/chat.css

Lines changed: 1177 additions & 0 deletions
Large diffs are not rendered by default.

frontend/components/chat/ChatOverlays.vue

Lines changed: 1625 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<template>
2+
<div class="flex-1 flex flex-col min-h-0 min-w-0">
3+
<div v-if="selectedContact" class="flex-1 flex flex-col min-h-0 relative">
4+
<div class="chat-header">
5+
<div class="flex items-center gap-3">
6+
<h2 class="text-base font-medium text-gray-900" :class="{ 'privacy-blur': privacyMode }">
7+
{{ selectedContact ? selectedContact.name : '' }}
8+
</h2>
9+
</div>
10+
<div class="ml-auto flex items-center gap-2">
11+
<button class="header-btn-icon" @click="refreshSelectedMessages" :disabled="isLoadingMessages" title="刷新消息">
12+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
14+
</svg>
15+
</button>
16+
<button class="header-btn-icon" @click="openExportModal" :disabled="isExportCreating" title="导出聊天记录">
17+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
19+
</svg>
20+
</button>
21+
<button class="header-btn-icon" :class="{ 'header-btn-icon-active': reverseMessageSides }" @click="toggleReverseMessageSides" :disabled="!selectedContact" :title="reverseMessageSides ? '取消反转消息位置' : '反转消息位置'">
22+
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
23+
<path d="M4 7h14" />
24+
<path d="M14 3l4 4-4 4" />
25+
<path d="M20 17H6" />
26+
<path d="M10 13l-4 4 4 4" />
27+
</svg>
28+
</button>
29+
<button class="header-btn-icon" :class="{ 'header-btn-icon-active': messageSearchOpen }" @click="toggleMessageSearch" :title="messageSearchOpen ? '关闭搜索 (Esc)' : '搜索聊天记录 (Ctrl+F)'">
30+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 16 16">
31+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z" />
32+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 14L11.1 11.1" />
33+
</svg>
34+
</button>
35+
<button class="header-btn-icon" :class="{ 'header-btn-icon-active': timeSidebarOpen }" @click="toggleTimeSidebar" :disabled="!selectedContact || isLoadingMessages" title="按日期定位">
36+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M8 7V3m8 4V3M3 11h18" />
38+
<rect x="4" y="5" width="16" height="16" rx="2" ry="2" stroke-width="1.8" />
39+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 14h2m3 0h2m3 0h2M7 18h2m3 0h2" />
40+
</svg>
41+
</button>
42+
<select
43+
v-model="messageTypeFilter"
44+
class="message-filter-select"
45+
:disabled="isLoadingMessages || searchContext.active"
46+
:title="searchContext.active ? '上下文模式下暂不可筛选' : '筛选消息类型'"
47+
>
48+
<option v-for="opt in messageTypeFilterOptions" :key="opt.value" :value="opt.value">
49+
{{ opt.label }}
50+
</option>
51+
</select>
52+
</div>
53+
</div>
54+
55+
<div v-if="searchContext.active" class="px-6 py-2 border-b border-emerald-200 bg-emerald-50 flex items-center gap-3">
56+
<div class="text-sm text-emerald-900">
57+
{{ searchContextBannerText }}
58+
</div>
59+
<div class="ml-auto flex items-center gap-2">
60+
<button type="button" class="text-xs px-3 py-1 rounded-md bg-white border border-emerald-200 hover:bg-emerald-100" @click="exitSearchContext">
61+
退出定位
62+
</button>
63+
<button type="button" class="text-xs px-3 py-1 rounded-md bg-white border border-gray-200 hover:bg-gray-50" @click="refreshSelectedMessages">
64+
返回最新
65+
</button>
66+
</div>
67+
</div>
68+
69+
<MessageList :state="state" />
70+
71+
<button
72+
v-if="showJumpToBottom"
73+
type="button"
74+
class="absolute bottom-6 right-6 z-20 w-10 h-10 rounded-full bg-white/90 border border-gray-200 shadow hover:bg-white flex items-center justify-center"
75+
title="回到最新"
76+
@click="scrollToBottom"
77+
>
78+
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
79+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
80+
</svg>
81+
</button>
82+
</div>
83+
84+
<div v-else class="flex-1 flex items-center justify-center">
85+
<div class="text-center">
86+
<div class="w-20 h-20 mx-auto mb-5 rounded-2xl bg-gradient-to-br from-[#03C160]/10 to-[#03C160]/5 flex items-center justify-center">
87+
<svg class="w-10 h-10 text-[#03C160]/60" viewBox="0 0 24 24" fill="currentColor">
88+
<path d="M12 19.8C17.52 19.8 22 15.99 22 11.3C22 6.6 17.52 2.8 12 2.8C6.48 2.8 2 6.6 2 11.3C2 13.29 2.8 15.12 4.15 16.57C4.6 17.05 4.82 17.29 4.92 17.44C5.14 17.79 5.21 17.99 5.23 18.4C5.24 18.59 5.22 18.81 5.16 19.26C5.1 19.75 5.07 19.99 5.13 20.16C5.23 20.49 5.53 20.71 5.87 20.72C6.04 20.72 6.27 20.63 6.72 20.43L8.07 19.86C8.43 19.71 8.61 19.63 8.77 19.59C8.95 19.55 9.04 19.54 9.22 19.54C9.39 19.53 9.64 19.57 10.14 19.65C10.74 19.75 11.37 19.8 12 19.8Z"/>
89+
</svg>
90+
</div>
91+
<h3 class="text-base font-medium text-gray-700 mb-1.5">选择一个会话</h3>
92+
<p class="text-sm text-gray-400">
93+
从左侧列表选择联系人查看聊天记录
94+
</p>
95+
</div>
96+
</div>
97+
</div>
98+
</template>
99+
100+
<script>
101+
import { defineComponent } from 'vue'
102+
import MessageList from '~/components/chat/MessageList.vue'
103+
104+
export default defineComponent({
105+
name: 'ConversationPane',
106+
components: { MessageList },
107+
props: {
108+
state: { type: Object, required: true }
109+
},
110+
setup(props) {
111+
return {
112+
...props.state
113+
}
114+
}
115+
})
116+
</script>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<img
3+
v-if="iconUrl"
4+
:src="iconUrl"
5+
alt=""
6+
class="wechat-file-icon"
7+
/>
8+
<svg v-else-if="kind === 'ppt'" viewBox="0 0 24 24" fill="none" class="wechat-file-icon text-orange-500">
9+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" fill="none" />
10+
<path d="M14 2v6h6" stroke="currentColor" stroke-width="1.5" />
11+
<text x="6" y="17" font-size="5" fill="currentColor" font-weight="bold">PPT</text>
12+
</svg>
13+
<svg v-else-if="kind === 'txt'" viewBox="0 0 24 24" fill="none" class="wechat-file-icon text-gray-500">
14+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" fill="none" />
15+
<path d="M14 2v6h6" stroke="currentColor" stroke-width="1.5" />
16+
<text x="6" y="17" font-size="5" fill="currentColor" font-weight="bold">TXT</text>
17+
</svg>
18+
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="wechat-file-icon text-gray-400">
19+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z" />
20+
</svg>
21+
</template>
22+
23+
<script setup>
24+
import { computed } from 'vue'
25+
import { getFileIconKind, getFileIconUrl } from '~/lib/chat/file-icons'
26+
27+
const props = defineProps({
28+
fileName: { type: String, default: '' }
29+
})
30+
31+
const kind = computed(() => getFileIconKind(props.fileName))
32+
const iconUrl = computed(() => getFileIconUrl(props.fileName))
33+
</script>

0 commit comments

Comments
 (0)