重命名前端项目目录:web-flowith -> web
- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁 - 保持所有前端代码和配置文件不变 - 统一项目目录命名规范
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
<template>
|
||||
<div class="chat-page">
|
||||
<!-- 聊天头部 -->
|
||||
<header class="chat-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<a-button type="text" @click="$router.back()" class="back-btn">
|
||||
<ArrowLeftOutlined />
|
||||
</a-button>
|
||||
<div class="chat-info">
|
||||
<a-avatar
|
||||
:src="kaikaiAvatar"
|
||||
:size="40"
|
||||
class="kaikai-avatar"
|
||||
/>
|
||||
<div class="chat-details">
|
||||
<h1 class="chat-title">开开</h1>
|
||||
<p class="chat-status">
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="{
|
||||
'connected': chatStore.wsConnected,
|
||||
'connecting': chatStore.connectionStatus === 'CONNECTING',
|
||||
'disconnected': !chatStore.wsConnected
|
||||
}"
|
||||
></span>
|
||||
{{ getConnectionStatusText() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<a-button type="text" @click="showHistory = true" class="action-btn">
|
||||
<HistoryOutlined />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 连接状态提示 -->
|
||||
<div
|
||||
v-if="!chatStore.wsConnected"
|
||||
class="connection-alert"
|
||||
:class="{ 'connecting': chatStore.connectionStatus === 'CONNECTING' }"
|
||||
>
|
||||
<div class="alert-content">
|
||||
<span v-if="chatStore.connectionStatus === 'CONNECTING'">正在连接...</span>
|
||||
<span v-else-if="chatStore.connectionStatus === 'ERROR'">连接失败,正在重试...</span>
|
||||
<span v-else>连接已断开,正在重连...</span>
|
||||
<a-button
|
||||
v-if="chatStore.connectionStatus === 'DISCONNECTED'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="chatStore.connectWebSocket()"
|
||||
>
|
||||
手动重连
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<main class="chat-main" ref="chatMainRef">
|
||||
<div class="messages-container">
|
||||
<div
|
||||
v-for="message in chatStore.messages"
|
||||
:key="message.id"
|
||||
class="message-wrapper"
|
||||
:class="{ 'user-message': message.type === 'user' }"
|
||||
>
|
||||
<div class="message-bubble">
|
||||
<div v-if="message.type === 'ai'" class="message-avatar">
|
||||
<a-avatar :src="kaikaiAvatar" :size="32" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
<div class="message-meta">
|
||||
<span class="message-time">{{ formatTime.friendly(message.timestamp) }}</span>
|
||||
<span v-if="message.type === 'user' && message.status" class="message-status" :class="message.status">
|
||||
<template v-if="message.status === 'sending'">发送中</template>
|
||||
<template v-else-if="message.status === 'sent'">已发送</template>
|
||||
<template v-else-if="message.status === 'delivered'">已送达</template>
|
||||
<template v-else-if="message.status === 'read'">已读</template>
|
||||
<template v-else-if="message.status === 'failed'">发送失败</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 正在输入指示器 -->
|
||||
<div v-if="chatStore.isTyping" class="message-wrapper">
|
||||
<div class="message-bubble">
|
||||
<div class="message-avatar">
|
||||
<a-avatar :src="kaikaiAvatar" :size="32" />
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 消息输入区域 -->
|
||||
<footer class="chat-footer">
|
||||
<div class="input-container">
|
||||
<a-input
|
||||
v-model:value="messageInput"
|
||||
:placeholder="getInputPlaceholder()"
|
||||
class="message-input"
|
||||
@press-enter="sendMessage"
|
||||
:disabled="chatStore.isTyping || !chatStore.wsConnected"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="sendMessage"
|
||||
:loading="chatStore.isTyping"
|
||||
:disabled="!messageInput.trim() || !chatStore.wsConnected"
|
||||
class="send-btn"
|
||||
:title="chatStore.wsConnected ? '发送消息' : '连接已断开'"
|
||||
>
|
||||
<SendOutlined />
|
||||
</a-button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 聊天历史抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="showHistory"
|
||||
title="聊天记录"
|
||||
placement="right"
|
||||
:width="320"
|
||||
>
|
||||
<div class="history-content">
|
||||
<div class="search-section">
|
||||
<a-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索关键词..."
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
|
||||
<a-date-picker
|
||||
v-model:value="searchDate"
|
||||
placeholder="按日期查询"
|
||||
class="date-picker"
|
||||
style="width: 100%; margin-top: 12px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="history-messages">
|
||||
<div
|
||||
v-for="message in filteredMessages"
|
||||
:key="message.id"
|
||||
class="history-message"
|
||||
:class="{ 'user': message.type === 'user' }"
|
||||
>
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
<div class="message-time">{{ formatTime.standard(message.timestamp) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
HistoryOutlined,
|
||||
SendOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useChatStore } from '@/stores'
|
||||
import { formatTime } from '@/utils'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
|
||||
// 响应式数据
|
||||
const messageInput = ref('')
|
||||
const showHistory = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const searchDate = ref<Dayjs | null>(null)
|
||||
const chatMainRef = ref<HTMLElement>()
|
||||
|
||||
// 开开头像
|
||||
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png'
|
||||
|
||||
// 计算属性
|
||||
const filteredMessages = computed(() => {
|
||||
let messages = chatStore.messages
|
||||
|
||||
// 关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
messages = messages.filter(msg =>
|
||||
msg.content.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 日期筛选
|
||||
if (searchDate.value) {
|
||||
const targetDate = searchDate.value.format('YYYY-MM-DD')
|
||||
messages = messages.filter(msg => {
|
||||
const msgDate = new Date(msg.timestamp).toISOString().split('T')[0]
|
||||
return msgDate === targetDate
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
})
|
||||
|
||||
// 方法
|
||||
const sendMessage = async () => {
|
||||
if (!messageInput.value.trim() || chatStore.isTyping || !chatStore.wsConnected) return
|
||||
|
||||
const content = messageInput.value.trim()
|
||||
messageInput.value = ''
|
||||
|
||||
await chatStore.sendMessage(content)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 获取连接状态文本
|
||||
const getConnectionStatusText = () => {
|
||||
switch (chatStore.connectionStatus) {
|
||||
case 'CONNECTED':
|
||||
return '在线'
|
||||
case 'CONNECTING':
|
||||
return '连接中...'
|
||||
case 'DISCONNECTED':
|
||||
return '离线'
|
||||
case 'ERROR':
|
||||
return '连接错误'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取输入框占位符
|
||||
const getInputPlaceholder = () => {
|
||||
if (!chatStore.wsConnected) {
|
||||
return '连接已断开,请等待重连...'
|
||||
}
|
||||
if (chatStore.isTyping) {
|
||||
return '开开正在输入...'
|
||||
}
|
||||
return '和开开说点什么...'
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (chatMainRef.value) {
|
||||
chatMainRef.value.scrollTop = chatMainRef.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听消息变化,自动滚动到底部
|
||||
watch(
|
||||
() => chatStore.messages.length,
|
||||
() => {
|
||||
scrollToBottom()
|
||||
}
|
||||
)
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
chatStore.initChat()
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
// 断开WebSocket连接
|
||||
chatStore.disconnectWebSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: $light-gray;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: white;
|
||||
box-shadow: $shadow-sm;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: $text-medium;
|
||||
|
||||
&:hover {
|
||||
color: $tech-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.kaikai-avatar {
|
||||
border: 2px solid white;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.chat-details {
|
||||
.chat-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: $font-size-xs;
|
||||
color: $text-medium;
|
||||
margin: 0;
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&.connected {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
background: #faad14;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.action-btn {
|
||||
color: $text-medium;
|
||||
|
||||
&:hover {
|
||||
color: $tech-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connection-alert {
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
color: #ff4d4f;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
text-align: center;
|
||||
font-size: $font-size-sm;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.connecting {
|
||||
background: #fffbe6;
|
||||
border-color: #ffe58f;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-sm;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
|
||||
&.user-message {
|
||||
justify-content: flex-end;
|
||||
|
||||
.message-bubble {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
background: $tech-blue;
|
||||
color: white;
|
||||
border-radius: 18px 18px 4px 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-sm;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: white;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
.message-text {
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: $font-size-xs;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
.user-message & {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.message-wrapper:not(.user-message) & {
|
||||
color: $text-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
font-size: $font-size-xs;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
|
||||
&.sending {
|
||||
color: #faad14;
|
||||
background: rgba(250, 173, 20, 0.1);
|
||||
}
|
||||
|
||||
&.sent {
|
||||
color: #52c41a;
|
||||
background: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
&.delivered {
|
||||
color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&.read {
|
||||
color: #722ed1;
|
||||
background: rgba(114, 46, 209, 0.1);
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
|
||||
.user-message & {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: $text-medium;
|
||||
border-radius: 50%;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
background: white;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
border-radius: $border-radius-full;
|
||||
|
||||
:deep(.ant-input) {
|
||||
border-radius: $border-radius-full;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-md;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// 历史记录样式
|
||||
.history-content {
|
||||
.search-section {
|
||||
margin-bottom: $spacing-lg;
|
||||
padding-bottom: $spacing-lg;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.history-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.history-message {
|
||||
padding: $spacing-md;
|
||||
background: #f5f5f5;
|
||||
border-radius: $border-radius-md;
|
||||
|
||||
&.user {
|
||||
background: rgba(74, 144, 226, 0.1);
|
||||
margin-left: $spacing-lg;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: $font-size-sm;
|
||||
line-height: 1.4;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user