完善 ChatView 页面功能与用户交互逻辑
- 增加请求状态管理,区分 RUNNING 和 DONE 状态 - 优化消息列表归一化处理逻辑,提高代码可读性 - 添加按键事件支持 (Ctrl+Enter) 以改善发送消息体验 - 更新输入框与发送按钮的禁用逻辑,明确状态反馈 - 提升 SSE 连接恢复的安全性,增加鉴权检查 - 优化界面显示,新增快捷键提示及样式改进 - 移除 package-lock.json 中多余的 peer 标记
This commit is contained in:
@@ -19,6 +19,7 @@ const inputText = ref('')
|
||||
const uploadFiles = ref([])
|
||||
const fileList = ref([])
|
||||
const sending = ref(false)
|
||||
const runStatus = ref('DONE')
|
||||
const loadingMessages = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const showJumpDown = ref(false)
|
||||
@@ -72,6 +73,48 @@ const normalizeMessage = (msg = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
const extractRunStatus = (msg = {}) => {
|
||||
const status = msg.payload?.status || msg.content
|
||||
return status === 'RUNNING' || status === 'DONE' ? status : null
|
||||
}
|
||||
|
||||
const applyRunStatus = (msg = {}) => {
|
||||
if (msg.type !== 'run.status') return false
|
||||
const status = extractRunStatus(msg)
|
||||
if (!status) return false
|
||||
runStatus.value = status
|
||||
if (typeof msg.seq === 'number') {
|
||||
lastSeq.value = Math.max(lastSeq.value || 0, msg.seq)
|
||||
summary.lastSeq = lastSeq.value
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const refreshLastSeq = (list = []) => {
|
||||
const maxSeq = list.reduce((max, item) => {
|
||||
const seq = typeof item.seq === 'number' ? item.seq : 0
|
||||
return seq > max ? seq : max
|
||||
}, lastSeq.value || 0)
|
||||
lastSeq.value = maxSeq
|
||||
summary.lastSeq = maxSeq
|
||||
}
|
||||
|
||||
const normalizeMessageList = (list = []) => {
|
||||
let latestStatus = runStatus.value
|
||||
const normalized = []
|
||||
list.forEach((msg) => {
|
||||
if (msg?.type === 'run.status') {
|
||||
const status = extractRunStatus(msg)
|
||||
if (status) latestStatus = status
|
||||
return
|
||||
}
|
||||
normalized.push(normalizeMessage(msg))
|
||||
})
|
||||
runStatus.value = latestStatus
|
||||
refreshLastSeq(list)
|
||||
return normalized
|
||||
}
|
||||
|
||||
const upsertMessage = (msg) => {
|
||||
const list = messagesState.value
|
||||
const idx = list.findIndex(
|
||||
@@ -106,6 +149,8 @@ const normalizedSummary = (data = {}) => ({
|
||||
tags: data.last_message_type ? [data.last_message_type] : [],
|
||||
})
|
||||
|
||||
const runActive = computed(() => runStatus.value === 'RUNNING')
|
||||
const inputLocked = computed(() => summary.status === 'CLOSED')
|
||||
const messages = computed(() => messagesState.value || [])
|
||||
|
||||
const handleFileChange = (uploadFile, uploadFilesList) => {
|
||||
@@ -142,8 +187,19 @@ const handleFileRemove = (file, list) => {
|
||||
uploadFiles.value = filtered.map(toAttachment)
|
||||
}
|
||||
|
||||
const handleComposerKeydown = (event) => {
|
||||
if (event.key === 'Enter' && event.ctrlKey) {
|
||||
event.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (sending.value) return
|
||||
if (runActive.value) {
|
||||
ElMessage.info('Agent 正在处理中,请稍候再试')
|
||||
return
|
||||
}
|
||||
if (summary.status === 'CLOSED') {
|
||||
ElMessage.warning('会话已归档,无法发送')
|
||||
return
|
||||
@@ -291,7 +347,7 @@ const fetchMessages = async () => {
|
||||
}
|
||||
)
|
||||
const list = data.data || []
|
||||
messagesState.value = list.map((msg) => normalizeMessage(msg))
|
||||
messagesState.value = normalizeMessageList(list)
|
||||
if (list.length) {
|
||||
const last = list[list.length - 1]
|
||||
lastSeq.value = last.seq || 0
|
||||
@@ -331,6 +387,7 @@ const fetchSingleMessage = async (messageId) => {
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
const detail = data.data || data
|
||||
if (applyRunStatus(detail)) return
|
||||
const normalized = normalizeMessage(detail)
|
||||
upsertMessage(normalized)
|
||||
} catch (err) {
|
||||
@@ -366,6 +423,26 @@ const stopSse = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const checkSseAuth = async () => {
|
||||
const token = localStorage.getItem('ars-token') || ''
|
||||
if (!token) {
|
||||
router.replace({ name: 'login' })
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await axios.get(`${apiBase}/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err.response?.status === 401) {
|
||||
router.replace({ name: 'login' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const startSse = () => {
|
||||
if (typeof window === 'undefined' || typeof EventSource === 'undefined')
|
||||
return
|
||||
@@ -391,6 +468,7 @@ const startSse = () => {
|
||||
fetchSingleMessage(data.message_id)
|
||||
return
|
||||
}
|
||||
if (applyRunStatus(data)) return
|
||||
const normalized = normalizeMessage(data)
|
||||
upsertMessage(normalized)
|
||||
nextTick(() => scrollToBottom())
|
||||
@@ -400,7 +478,10 @@ const startSse = () => {
|
||||
}
|
||||
es.onerror = () => {
|
||||
stopSse()
|
||||
sseRetryTimer.value = window.setTimeout(() => startSse(), 3000)
|
||||
checkSseAuth().then((authed) => {
|
||||
if (!authed) return
|
||||
sseRetryTimer.value = window.setTimeout(() => startSse(), 3000)
|
||||
})
|
||||
}
|
||||
eventSource.value = es
|
||||
}
|
||||
@@ -409,6 +490,7 @@ const applySession = () => {
|
||||
if (!sessionId.value) return
|
||||
stopSse()
|
||||
lastSeq.value = 0
|
||||
runStatus.value = 'DONE'
|
||||
localStorage.setItem('ars-current-session', sessionId.value)
|
||||
fetchSummary()
|
||||
fetchMessages().then(() => {
|
||||
@@ -566,6 +648,7 @@ onBeforeUnmount(() => {
|
||||
multiple
|
||||
accept="image/*,.pdf,.txt,.doc,.docx,.md"
|
||||
:auto-upload="false"
|
||||
:disabled="inputLocked"
|
||||
:show-file-list="false"
|
||||
:file-list="fileList"
|
||||
:on-change="handleFileChange"
|
||||
@@ -581,17 +664,24 @@ onBeforeUnmount(() => {
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
class="composer-input"
|
||||
:placeholder="summary.status === 'CLOSED' ? '会话已归档,无法继续发送' : '输入消息,支持粘贴大段文本与附件...'"
|
||||
:disabled="summary.status === 'CLOSED'"
|
||||
:disabled="inputLocked"
|
||||
@keydown="handleComposerKeydown"
|
||||
/>
|
||||
<el-button
|
||||
color="#1d4ed8"
|
||||
class="send-btn"
|
||||
:loading="sending"
|
||||
:disabled="summary.status === 'CLOSED'"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送
|
||||
</el-button>
|
||||
<div class="send-wrap">
|
||||
<el-button
|
||||
color="#1d4ed8"
|
||||
class="send-btn"
|
||||
:loading="sending || runActive"
|
||||
:disabled="inputLocked || runActive"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送
|
||||
</el-button>
|
||||
<span class="send-hint">
|
||||
<span class="keycap">Ctrl</span> +
|
||||
<span class="keycap">Enter</span> 发送
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -909,6 +999,32 @@ onBeforeUnmount(() => {
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.send-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.send-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.keycap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #ffffff;
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pending-attachments {
|
||||
padding: 10px 0 0;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
@@ -989,5 +1105,10 @@ onBeforeUnmount(() => {
|
||||
.send-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.send-wrap {
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user