941 lines
25 KiB
Vue
941 lines
25 KiB
Vue
<template>
|
||
<view class="assistant-page">
|
||
<view class="assistant-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="assistant-nav__btn" @tap="goBack">
|
||
<u-icon name="arrow-left" size="34" color="#1f2937" />
|
||
</view>
|
||
<text class="assistant-nav__title">世博头条 AI 助手</text>
|
||
<view class="assistant-nav__actions">
|
||
<view class="assistant-nav__btn" @tap="toggleHistory">
|
||
<u-icon name="clock" size="32" color="#1f2937" />
|
||
</view>
|
||
<view class="assistant-nav__btn" @tap="newSession">
|
||
<u-icon name="plus" size="32" color="#1f2937" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showHistory" class="history-panel">
|
||
<view class="history-panel__header">
|
||
<text>历史会话</text>
|
||
<text class="history-panel__clear" @tap="clearAllSessions">清空</text>
|
||
</view>
|
||
<scroll-view scroll-y class="history-panel__list">
|
||
<view v-if="!sessions.length" class="history-panel__empty">暂无历史会话</view>
|
||
<view v-for="item in sessions" :key="item.id" class="history-item" @tap="openSession(item)">
|
||
<text class="history-item__title">{{ item.title || '新的对话' }}</text>
|
||
<text class="history-item__desc">{{ item.last_message || item.last_answer }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<scroll-view
|
||
scroll-y
|
||
class="message-list"
|
||
:scroll-into-view="scrollIntoView"
|
||
scroll-with-animation
|
||
>
|
||
<view class="welcome">
|
||
<view class="welcome__badge">AI</view>
|
||
<view class="welcome__body">
|
||
<text class="welcome__title">你好,我可以帮你查站内内容</text>
|
||
<text class="welcome__desc">可以问资讯、赛事、社区帖子、彩票分析和加密行情。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quick-list">
|
||
<view v-for="item in quickQuestions" :key="item" class="quick-list__item" @tap="sendQuick(item)">
|
||
<text class="quick-list__text">{{ item }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-for="(item, index) in messages"
|
||
:id="'msg-' + index"
|
||
:key="item.id || index"
|
||
class="message-row"
|
||
:class="item.role === 'user' ? 'message-row--user' : 'message-row--assistant'"
|
||
>
|
||
<view v-if="item.role !== 'user'" class="message-row__avatar">AI</view>
|
||
<view class="message-row__body">
|
||
<view class="message-bubble">
|
||
<view v-if="item.pending" class="message-bubble__pending">
|
||
<view class="message-bubble__pulse" />
|
||
<text class="message-bubble__loading">正在整理站内资料...</text>
|
||
<text class="message-bubble__timer">分析用时 {{ analysisDurationText }}</text>
|
||
</view>
|
||
<text v-else-if="item.role === 'user'" class="message-bubble__text">{{ item.content }}</text>
|
||
<view v-else class="message-bubble__markdown">
|
||
<u-parse :html="renderAssistantMarkdown(item.content)" />
|
||
</view>
|
||
<view v-if="item.sources && item.sources.length" class="source-list">
|
||
<view v-for="source in item.sources" :key="source.type + source.source_id + source.title"
|
||
class="source-item" @tap="openSource(source)">
|
||
<view class="source-item__meta">{{ sourceTypeName(source.type) }}</view>
|
||
<text class="source-item__title">{{ source.title }}</text>
|
||
<text class="source-item__summary">{{ source.summary }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="input-panel">
|
||
<view class="input-bar">
|
||
<input
|
||
v-model="inputText"
|
||
class="input-bar__input"
|
||
type="text"
|
||
maxlength="500"
|
||
:cursor-spacing="18"
|
||
:disabled="sending"
|
||
placeholder="问问世博头条里的内容"
|
||
confirm-type="send"
|
||
@confirm="sendMessage"
|
||
/>
|
||
<view class="input-bar__send" :class="{ 'input-bar__send--disabled': !canSend }" @tap="sendMessage">
|
||
<u-icon name="arrow-upward" size="32" color="#fff" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onUnmounted, ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import {
|
||
assistantChat,
|
||
assistantClear,
|
||
assistantMessages,
|
||
assistantSessions,
|
||
type AssistantMessage,
|
||
type AssistantSession,
|
||
type AssistantSource
|
||
} from '@/api/assistant'
|
||
import { renderMarkdownToHtml } from '@/utils/assistant-markdown'
|
||
|
||
const STORAGE_CLIENT_ID = 'sbnews_assistant_client_id'
|
||
|
||
const statusBarHeight = ref(0)
|
||
const clientId = ref('')
|
||
const sessionId = ref(0)
|
||
const inputText = ref('')
|
||
const sending = ref(false)
|
||
const showHistory = ref(false)
|
||
const scrollIntoView = ref('')
|
||
const messages = ref<AssistantMessage[]>([])
|
||
const sessions = ref<AssistantSession[]>([])
|
||
const analysisElapsedSeconds = ref(0)
|
||
let analysisTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
const quickQuestions = [
|
||
'今天有什么值得关注的赛事?',
|
||
'帮我总结最新世界杯资讯',
|
||
'旧澳六合最近有什么分析线索?',
|
||
'BTC 今天行情怎么样?'
|
||
]
|
||
|
||
const canSend = computed(() => inputText.value.trim().length > 0 && !sending.value)
|
||
const analysisDurationText = computed(() => {
|
||
const total = Math.max(0, Number(analysisElapsedSeconds.value || 0))
|
||
const minutes = Math.floor(total / 60)
|
||
const seconds = total % 60
|
||
return `${minutes}m${seconds}s`
|
||
})
|
||
|
||
onLoad(async () => {
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
// #ifdef APP-PLUS || MP
|
||
statusBarHeight.value = sysInfo.statusBarHeight || 44
|
||
// #endif
|
||
// #ifdef H5
|
||
statusBarHeight.value = 0
|
||
// #endif
|
||
clientId.value = getClientId()
|
||
await loadSessions()
|
||
})
|
||
|
||
const getClientId = () => {
|
||
const cached = uni.getStorageSync(STORAGE_CLIENT_ID)
|
||
if (cached) return String(cached)
|
||
const id = `c_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||
uni.setStorageSync(STORAGE_CLIENT_ID, id)
|
||
return id
|
||
}
|
||
|
||
const loadSessions = async () => {
|
||
try {
|
||
sessions.value = await assistantSessions({ client_id: clientId.value })
|
||
} catch (e) {
|
||
sessions.value = []
|
||
}
|
||
}
|
||
|
||
const openSession = async (session: AssistantSession) => {
|
||
try {
|
||
const res = await assistantMessages({ client_id: clientId.value, session_id: session.id })
|
||
sessionId.value = res.session?.id || session.id
|
||
messages.value = res.messages || []
|
||
showHistory.value = false
|
||
scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({ title: '会话加载失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const newSession = () => {
|
||
sessionId.value = 0
|
||
messages.value = []
|
||
showHistory.value = false
|
||
inputText.value = ''
|
||
}
|
||
|
||
const toggleHistory = async () => {
|
||
showHistory.value = !showHistory.value
|
||
if (showHistory.value) {
|
||
await loadSessions()
|
||
}
|
||
}
|
||
|
||
const sendQuick = (text: string) => {
|
||
inputText.value = text
|
||
sendMessage()
|
||
}
|
||
|
||
const renderAssistantMarkdown = (content: string) => renderMarkdownToHtml(content)
|
||
|
||
const sendMessage = async () => {
|
||
const text = inputText.value.trim()
|
||
if (!text || sending.value) return
|
||
|
||
const userMessage: AssistantMessage = {
|
||
id: Date.now(),
|
||
session_id: sessionId.value,
|
||
role: 'user',
|
||
content: text
|
||
}
|
||
const pendingMessage: AssistantMessage = {
|
||
id: Date.now() + 1,
|
||
session_id: sessionId.value,
|
||
role: 'assistant',
|
||
content: '',
|
||
pending: true
|
||
}
|
||
messages.value.push(userMessage, pendingMessage)
|
||
inputText.value = ''
|
||
sending.value = true
|
||
startAnalysisTimer()
|
||
scrollToBottom()
|
||
|
||
try {
|
||
const res = await assistantChat({
|
||
message: text,
|
||
client_id: clientId.value,
|
||
session_id: sessionId.value || undefined
|
||
})
|
||
sessionId.value = res.session?.id || sessionId.value
|
||
const answer = res.message || {
|
||
role: 'assistant',
|
||
content: '我暂时没有生成成功,请稍后再试。',
|
||
sources: res.sources || []
|
||
}
|
||
messages.value.splice(messages.value.length - 1, 1, answer)
|
||
await loadSessions()
|
||
} catch (e: any) {
|
||
console.error('[ai_assistant] chat failed:', e)
|
||
const recovered = await recoverLatestAssistantReply(text)
|
||
if (recovered) return
|
||
messages.value.splice(messages.value.length - 1, 1, {
|
||
id: Date.now() + 2,
|
||
session_id: sessionId.value,
|
||
role: 'assistant',
|
||
content: getAssistantErrorMessage(e),
|
||
sources: []
|
||
})
|
||
} finally {
|
||
stopAnalysisTimer()
|
||
sending.value = false
|
||
scrollToBottom()
|
||
}
|
||
}
|
||
|
||
const recoverLatestAssistantReply = async (question: string) => {
|
||
try {
|
||
await loadSessions()
|
||
const matchedSession = sessions.value.find((item) => item.last_message === question)
|
||
if (!matchedSession?.id) return false
|
||
|
||
const res = await assistantMessages({ client_id: clientId.value, session_id: matchedSession.id })
|
||
const serverMessages = res.messages || []
|
||
const hasMatchedQuestion = serverMessages.some((item) => item.role === 'user' && item.content === question)
|
||
const hasAssistantReply = serverMessages.some((item) => item.role === 'assistant' && !item.pending)
|
||
if (!hasMatchedQuestion || !hasAssistantReply) return false
|
||
|
||
sessionId.value = res.session?.id || matchedSession.id
|
||
messages.value = serverMessages
|
||
await loadSessions()
|
||
scrollToBottom()
|
||
return true
|
||
} catch (e) {
|
||
console.error('[ai_assistant] recover latest reply failed:', e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
const getRawAssistantErrorMessage = (error: any) => {
|
||
return typeof error === 'string'
|
||
? error
|
||
: String(error?.msg || error?.errMsg || error?.message || error?.data?.msg || '')
|
||
}
|
||
|
||
const getAssistantErrorMessage = (error: any) => {
|
||
const rawMessage = getRawAssistantErrorMessage(error)
|
||
|
||
if (/timeout|超时/i.test(rawMessage)) {
|
||
return 'AI助手响应时间较长,请稍后再试;当前请求已超时。'
|
||
}
|
||
if (/abort|cancel/i.test(rawMessage)) {
|
||
return 'AI助手请求已取消,请重新发送问题。'
|
||
}
|
||
if (/request:fail|network|网络/i.test(rawMessage)) {
|
||
return rawMessage
|
||
? `AI助手请求失败,请检查网络后重试。错误详情:${rawMessage}`
|
||
: 'AI助手请求失败,请检查网络后重试。'
|
||
}
|
||
|
||
return rawMessage || 'AI助手请求失败,请稍后重试。'
|
||
}
|
||
|
||
const startAnalysisTimer = () => {
|
||
stopAnalysisTimer()
|
||
analysisElapsedSeconds.value = 0
|
||
const startAt = Date.now()
|
||
analysisTimer = setInterval(() => {
|
||
analysisElapsedSeconds.value = Math.floor((Date.now() - startAt) / 1000)
|
||
}, 1000)
|
||
}
|
||
|
||
const stopAnalysisTimer = () => {
|
||
if (analysisTimer) {
|
||
clearInterval(analysisTimer)
|
||
analysisTimer = null
|
||
}
|
||
}
|
||
|
||
const clearAllSessions = () => {
|
||
uni.showModal({
|
||
title: '清空历史',
|
||
content: '确定清空所有助手会话?',
|
||
success: async (res) => {
|
||
if (!res.confirm) return
|
||
try {
|
||
await assistantClear({ client_id: clientId.value })
|
||
sessions.value = []
|
||
newSession()
|
||
} catch (e) {
|
||
uni.showToast({ title: '清空失败', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
scrollIntoView.value = 'msg-' + Math.max(messages.value.length - 1, 0)
|
||
})
|
||
}
|
||
|
||
const openSource = (source: AssistantSource) => {
|
||
if (!source.path) return
|
||
if (source.path === '/pages/crypto/crypto' || source.path === '/pages/lottery_analysis/lottery_analysis') {
|
||
uni.switchTab({ url: source.path })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: source.path })
|
||
}
|
||
|
||
const sourceTypeName = (type: string) => {
|
||
const map: Record<string, string> = {
|
||
article: '资讯',
|
||
post: '社区',
|
||
match: '赛事',
|
||
lottery: '彩票',
|
||
crypto: '行情',
|
||
web_news: '外网资讯'
|
||
}
|
||
return map[type] || '来源'
|
||
}
|
||
|
||
const goBack = () => {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 1) {
|
||
uni.navigateBack()
|
||
return
|
||
}
|
||
uni.switchTab({ url: '/pages/index/index' })
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
stopAnalysisTimer()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.assistant-page {
|
||
height: 100vh;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f3f6fa;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.assistant-nav {
|
||
flex-shrink: 0;
|
||
min-height: 96rpx;
|
||
padding-left: 20rpx;
|
||
padding-right: 20rpx;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
display: flex;
|
||
align-items: center;
|
||
border-bottom: 1rpx solid rgba(211, 220, 232, 0.8);
|
||
box-shadow: 0 8rpx 28rpx rgba(31, 41, 55, 0.05);
|
||
|
||
&__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
&__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
&__btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__btn:active {
|
||
background: #eef3fb;
|
||
}
|
||
}
|
||
|
||
.history-panel {
|
||
flex-shrink: 0;
|
||
max-height: 46vh;
|
||
background: #fff;
|
||
border-bottom: 1rpx solid #dbe6f5;
|
||
box-shadow: 0 18rpx 36rpx rgba(38, 55, 77, 0.08);
|
||
|
||
&__header {
|
||
height: 80rpx;
|
||
padding: 0 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 26rpx;
|
||
color: #111827;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__clear {
|
||
color: #185dff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__list {
|
||
max-height: calc(46vh - 80rpx);
|
||
}
|
||
|
||
&__empty {
|
||
padding: 48rpx 0;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
|
||
.history-item {
|
||
padding: 22rpx 28rpx 24rpx;
|
||
border-top: 1rpx solid #f1f5f9;
|
||
background: #fff;
|
||
|
||
&__title {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
color: #111827;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__desc {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 24rpx;
|
||
color: #6b7280;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.message-list {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 24rpx 24rpx 36rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.welcome {
|
||
display: flex;
|
||
gap: 18rpx;
|
||
padding: 24rpx 24rpx 26rpx;
|
||
background: #fff;
|
||
border: 1rpx solid rgba(214, 224, 237, 0.92);
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 12rpx 28rpx rgba(41, 60, 87, 0.06);
|
||
|
||
&__badge {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 14rpx;
|
||
background: #185dff;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__title {
|
||
display: block;
|
||
font-size: 31rpx;
|
||
color: #111827;
|
||
font-weight: 700;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
&__desc {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
color: #6b7280;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.quick-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 14rpx;
|
||
margin: 22rpx 0 32rpx;
|
||
|
||
&__item {
|
||
min-height: 62rpx;
|
||
padding: 12rpx 18rpx;
|
||
border-radius: 14rpx;
|
||
background: #fff;
|
||
border: 1rpx solid rgba(207, 220, 238, 0.95);
|
||
color: #185dff;
|
||
font-size: 24rpx;
|
||
line-height: 1.35;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
box-shadow: 0 8rpx 20rpx rgba(24, 93, 255, 0.04);
|
||
}
|
||
|
||
&__text {
|
||
display: block;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
&__item:active {
|
||
background: #edf4ff;
|
||
border-color: #b9cff7;
|
||
}
|
||
}
|
||
|
||
.message-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 26rpx;
|
||
|
||
&--user {
|
||
justify-content: flex-end;
|
||
|
||
.message-row__body {
|
||
max-width: 82%;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.message-bubble {
|
||
background: #185dff;
|
||
border-color: #185dff;
|
||
color: #fff;
|
||
box-shadow: 0 12rpx 28rpx rgba(24, 93, 255, 0.22);
|
||
}
|
||
}
|
||
|
||
&--assistant {
|
||
justify-content: flex-start;
|
||
|
||
.message-row__body {
|
||
max-width: calc(100% - 78rpx);
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.message-bubble {
|
||
background: #fff;
|
||
color: #111827;
|
||
border: 1rpx solid rgba(214, 224, 237, 0.95);
|
||
box-shadow: 0 12rpx 30rpx rgba(39, 55, 77, 0.07);
|
||
}
|
||
}
|
||
|
||
&__avatar {
|
||
width: 58rpx;
|
||
height: 58rpx;
|
||
margin-right: 16rpx;
|
||
border-radius: 14rpx;
|
||
background: #111827;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 10rpx 20rpx rgba(17, 24, 39, 0.14);
|
||
}
|
||
|
||
&__body {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
.message-bubble {
|
||
max-width: 100%;
|
||
padding: 20rpx 24rpx;
|
||
border-radius: 16rpx;
|
||
box-sizing: border-box;
|
||
|
||
&__pending {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
&__pulse {
|
||
width: 36rpx;
|
||
height: 8rpx;
|
||
border-radius: 8rpx;
|
||
background: #185dff;
|
||
opacity: 0.78;
|
||
}
|
||
|
||
&__text,
|
||
&__loading,
|
||
&__markdown {
|
||
font-size: 28rpx;
|
||
line-height: 1.72;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
&__markdown {
|
||
white-space: normal;
|
||
}
|
||
|
||
&__loading {
|
||
color: #6b7280;
|
||
}
|
||
|
||
&__timer {
|
||
font-size: 22rpx;
|
||
line-height: 1.4;
|
||
color: #0f7a5f;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.message-bubble__markdown :deep(*) {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(p),
|
||
.message-bubble__markdown :deep(ul),
|
||
.message-bubble__markdown :deep(ol),
|
||
.message-bubble__markdown :deep(blockquote),
|
||
.message-bubble__markdown :deep(pre),
|
||
.message-bubble__markdown :deep(hr) {
|
||
margin: 0 0 18rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(h1),
|
||
.message-bubble__markdown :deep(h2),
|
||
.message-bubble__markdown :deep(h3),
|
||
.message-bubble__markdown :deep(h4),
|
||
.message-bubble__markdown :deep(h5),
|
||
.message-bubble__markdown :deep(h6) {
|
||
margin: 0 0 16rpx;
|
||
color: inherit;
|
||
font-weight: 700;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(h1) {
|
||
font-size: 34rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(h2) {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(h3) {
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(ul),
|
||
.message-bubble__markdown :deep(ol) {
|
||
padding-left: 34rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(li) {
|
||
margin-bottom: 10rpx;
|
||
line-height: 1.72;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(blockquote) {
|
||
padding: 14rpx 18rpx;
|
||
border-left: 6rpx solid #bfd5ff;
|
||
border-radius: 0 12rpx 12rpx 0;
|
||
background: #f6f9ff;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(code) {
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 8rpx;
|
||
background: #eff3f8;
|
||
color: #0f172a;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(pre) {
|
||
overflow-x: auto;
|
||
padding: 18rpx;
|
||
border-radius: 14rpx;
|
||
background: #0f172a;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(pre code) {
|
||
display: block;
|
||
padding: 0;
|
||
background: transparent;
|
||
color: #f8fafc;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(a) {
|
||
color: #185dff;
|
||
text-decoration: underline;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(strong) {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(hr) {
|
||
border: 0;
|
||
border-top: 1rpx solid #dbe4f0;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(img) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(table) {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 0 0 18rpx;
|
||
overflow: hidden;
|
||
border-radius: 12rpx;
|
||
background: #fff;
|
||
border: 1rpx solid #dbe4f0;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(th),
|
||
.message-bubble__markdown :deep(td) {
|
||
padding: 12rpx 14rpx;
|
||
border: 1rpx solid #dbe4f0;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
line-height: 1.6;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(th) {
|
||
background: #f6f9ff;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.message-bubble__markdown :deep(p:last-child),
|
||
.message-bubble__markdown :deep(ul:last-child),
|
||
.message-bubble__markdown :deep(ol:last-child),
|
||
.message-bubble__markdown :deep(blockquote:last-child),
|
||
.message-bubble__markdown :deep(pre:last-child),
|
||
.message-bubble__markdown :deep(table:last-child),
|
||
.message-bubble__markdown :deep(hr:last-child),
|
||
.message-bubble__markdown :deep(h1:last-child),
|
||
.message-bubble__markdown :deep(h2:last-child),
|
||
.message-bubble__markdown :deep(h3:last-child),
|
||
.message-bubble__markdown :deep(h4:last-child),
|
||
.message-bubble__markdown :deep(h5:last-child),
|
||
.message-bubble__markdown :deep(h6:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.source-list {
|
||
margin-top: 18rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.source-item {
|
||
position: relative;
|
||
padding: 18rpx 18rpx 18rpx 22rpx;
|
||
border-radius: 14rpx;
|
||
background: #f8fafc;
|
||
border: 1rpx solid #e2e8f0;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 18rpx;
|
||
bottom: 18rpx;
|
||
left: 0;
|
||
width: 6rpx;
|
||
border-radius: 0 6rpx 6rpx 0;
|
||
background: #0f7a5f;
|
||
}
|
||
|
||
&__meta {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 34rpx;
|
||
padding: 0 12rpx;
|
||
border-radius: 6rpx;
|
||
background: #e8f6f1;
|
||
color: #0f7a5f;
|
||
font-size: 20rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&__title {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 25rpx;
|
||
color: #111827;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
&__summary {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: #6b7280;
|
||
line-height: 1.45;
|
||
}
|
||
}
|
||
|
||
.input-panel {
|
||
flex-shrink: 0;
|
||
padding-top: 16rpx;
|
||
padding-left: 22rpx;
|
||
padding-right: 22rpx;
|
||
padding-bottom: calc(18rpx + constant(safe-area-inset-bottom));
|
||
padding-bottom: calc(18rpx + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.98);
|
||
border-top: 1rpx solid rgba(211, 220, 232, 0.86);
|
||
box-shadow: 0 -10rpx 28rpx rgba(31, 41, 55, 0.05);
|
||
}
|
||
|
||
.input-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
height: 64rpx;
|
||
overflow: hidden;
|
||
|
||
&__input {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
min-height: 64rpx;
|
||
max-height: 64rpx;
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
border-radius: 14rpx;
|
||
background: #eef3f8;
|
||
font-size: 28rpx;
|
||
line-height: 64rpx;
|
||
color: #111827;
|
||
}
|
||
|
||
&__send {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 14rpx;
|
||
background: #185dff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 10rpx 20rpx rgba(24, 93, 255, 0.22);
|
||
|
||
&--disabled {
|
||
background: #b8c5d8;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
}
|
||
</style>
|