Files
Jog/jog-frontend/src/components/AiChatPanel.vue
T

202 lines
4.9 KiB
Vue
Raw Normal View History

2026-05-23 23:33:27 +08:00
<template>
<section class="chat-panel">
<div class="chat-header">
<span class="chat-title">{{ currentTitle }}</span>
<span v-if="isLoggedIn && currentSessionId" class="round-badge">{{ roundCount }}/{{ maxRounds }}</span>
</div>
<div ref="messagesRef" class="messages">
<div v-if="messages.length === 0 && !sending" class="welcome">
<p>我是 Jog AI 助手</p>
<p class="welcome-sub">可以回答博客写作技术问题等输入问题后点击发送开始对话</p>
</div>
<div
v-for="msg in messages"
:key="msg.id ?? msg._localId"
class="message-row"
:class="msg.role"
>
<div class="bubble">{{ msg.content }}</div>
</div>
<div v-if="sending" class="message-row assistant">
<div class="bubble thinking">正在思考</div>
</div>
</div>
<div v-if="readOnly" class="readonly-banner">
当前会话已满 {{ maxRounds }} 请点击左侧新会话开始新的对话
</div>
<div class="input-area">
<el-input
v-model="inputText"
type="textarea"
:rows="2"
:autosize="{ minRows: 2, maxRows: 6 }"
placeholder="请输入您的问题…"
:disabled="inputDisabled"
@keydown="onKeydown"
/>
<el-button
type="primary"
:loading="sending"
:disabled="sendDisabled"
@click="onSend"
>
发送
</el-button>
</div>
</section>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
const props = defineProps({
currentTitle: { type: String, default: 'AI 助手' },
messages: { type: Array, default: () => [] },
roundCount: { type: Number, default: 0 },
maxRounds: { type: Number, default: 5 },
readOnly: { type: Boolean, default: false },
sending: { type: Boolean, default: false },
currentSessionId: { type: [Number, String], default: null },
isLoggedIn: { type: Boolean, default: false },
})
const emit = defineEmits(['send'])
const inputText = ref('')
const messagesRef = ref(null)
const inputDisabled = computed(() => {
if (!props.isLoggedIn) return true
if (props.readOnly) return true
if (props.sending) return true
return false
})
const sendDisabled = computed(() => {
if (inputDisabled.value) return true
if (props.roundCount >= props.maxRounds) return true
return !inputText.value.trim()
})
function onSend() {
const text = inputText.value.trim()
if (!text || sendDisabled.value) return
emit('send', text)
inputText.value = ''
}
function onKeydown(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault()
onSend()
}
}
async function scrollToBottom() {
await nextTick()
const el = messagesRef.value
if (el) el.scrollTop = el.scrollHeight
}
watch(() => props.messages, () => scrollToBottom(), { deep: true })
watch(() => props.sending, () => { if (props.sending) scrollToBottom() })
</script>
<style scoped>
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
max-width: 720px;
margin: 0 auto;
width: 100%;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color-light);
}
.chat-title {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
}
.round-badge {
font-size: var(--font-size-sm);
color: var(--text-secondary);
background: var(--color-primary-light);
padding: 2px 10px;
border-radius: var(--radius-full);
}
.readonly-banner {
background: #fff7e6;
border: 1px solid #ffd591;
color: #ad6800;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
}
.messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md) 0;
min-height: 280px;
}
.welcome {
text-align: center;
color: var(--text-secondary);
padding: var(--spacing-2xl) var(--spacing-md);
}
.welcome-sub {
font-size: var(--font-size-sm);
margin-top: var(--spacing-sm);
}
.message-row {
display: flex;
margin-bottom: var(--spacing-md);
}
.message-row.user {
justify-content: flex-end;
}
.message-row.assistant {
justify-content: flex-start;
}
.bubble {
max-width: 85%;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-lg);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.message-row.user .bubble {
background: var(--color-primary);
color: #fff;
}
.message-row.assistant .bubble {
background: var(--border-color-light);
color: var(--text-regular);
}
.bubble.thinking {
color: var(--text-secondary);
font-style: italic;
}
.input-area {
display: flex;
gap: var(--spacing-sm);
align-items: flex-end;
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-color-light);
}
.input-area .el-textarea {
flex: 1;
}
</style>