重命名前端项目目录:web-flowith -> web

- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁
- 保持所有前端代码和配置文件不变
- 统一项目目录命名规范
This commit is contained in:
2025-07-24 22:20:19 +08:00
parent ca42a7d9a4
commit bbe8fcd776
57 changed files with 0 additions and 0 deletions
+636
View File
@@ -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>