| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <template>
- <view class="page-chat" :class="themeClass">
- <!-- ========== Member Bar ========== -->
- <view class="member-bar">
- <text class="member-bar-title">分析成员</text>
- <view class="member-avatars">
- <view v-for="(m, i) in memberList" :key="i" class="mini-avatar">
- <text class="mini-avatar-text">{{ m.name ? m.name[0] : '?' }}</text>
- </view>
- </view>
- </view>
- <!-- ========== Chat Messages ========== -->
- <scroll-view
- class="chat-scroll"
- scroll-y
- :scroll-into-view="scrollToId"
- scroll-with-animation
- @scrolltolower="onScrollToLower"
- >
- <view class="chat-list">
- <view
- v-for="(msg, idx) in messages"
- :key="msg.id || idx"
- :id="'msg-' + idx"
- class="msg-item"
- :class="msg.role === 'user' ? 'msg-user' : 'msg-ai'"
- >
- <view v-if="msg.role === 'ai'" class="msg-avatar ai-avatar">AI</view>
- <view class="msg-bubble" :class="msg.role === 'user' ? 'bubble-user' : 'bubble-ai'">
- <text class="msg-content">{{ msg.content }}</text>
- </view>
- <view v-if="msg.role === 'user'" class="msg-avatar user-avatar">我</view>
- </view>
- <!-- Loading indicator -->
- <view v-if="loadingResponse" class="msg-item msg-ai">
- <view class="msg-avatar ai-avatar">AI</view>
- <view class="msg-bubble bubble-ai">
- <text class="thinking-text">正在思考...</text>
- </view>
- </view>
- </view>
- </scroll-view>
- <!-- ========== Input Bar ========== -->
- <view class="input-bar">
- <view class="input-wrap">
- <input
- v-model="inputText"
- class="chat-input"
- placeholder="输入您的问题..."
- placeholder-class="input-placeholder"
- :disabled="loadingResponse"
- @confirm="sendMessage"
- confirm-type="send"
- />
- </view>
- <view
- class="btn-send"
- :class="{ disabled: !inputText.trim() || loadingResponse }"
- @click="sendMessage"
- >
- <text class="btn-send-text">发送</text>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, computed, nextTick, watch } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import request from '@/utils/api'
- import { useTheme } from '@/composables/useThemeStyle'
- const { themeClass } = useTheme()
- const recordId = ref(null)
- const messages = ref([])
- const inputText = ref('')
- const loadingResponse = ref(false)
- const memberList = ref([])
- const scrollToId = ref('')
- onLoad(async (query) => {
- if (query && query.recordId) {
- recordId.value = query.recordId
- await Promise.all([loadMembers(), loadHistory()])
- }
- })
- async function loadMembers() {
- try {
- const detail = await request.post('/relation/analysis/detail', { id: recordId.value })
- if (detail && detail.memberSnapshot) {
- try {
- const parsed = JSON.parse(detail.memberSnapshot)
- memberList.value = Array.isArray(parsed) ? parsed : []
- } catch (e) {
- memberList.value = []
- }
- }
- if (detail && detail.title) {
- uni.setNavigationBarTitle({ title: detail.title })
- }
- } catch (e) {
- // ignore
- }
- }
- async function loadHistory() {
- try {
- const msgs = await request.post('/relation/analysis/chat/history', { recordId: recordId.value })
- messages.value = msgs || []
- scrollToBottom()
- } catch (e) {
- messages.value = []
- }
- }
- function scrollToBottom() {
- nextTick(() => {
- scrollToId.value = 'msg-' + (messages.value.length - 1)
- })
- }
- async function sendMessage() {
- const text = inputText.value.trim()
- if (!text || loadingResponse.value) return
- // Optimistically add user message
- const tempMsg = { id: Date.now(), role: 'user', content: text }
- messages.value.push(tempMsg)
- inputText.value = ''
- scrollToBottom()
- loadingResponse.value = true
- try {
- const res = await request.post('/relation/analysis/chat/send', {
- recordId: recordId.value,
- query: text
- })
- if (res && res.answer) {
- messages.value.push({ id: Date.now() + 1, role: 'ai', content: res.answer })
- } else {
- messages.value.push({ id: Date.now() + 1, role: 'ai', content: 'AI 解读服务暂时不可用,请稍后再试。' })
- }
- } catch (e) {
- messages.value.push({ id: Date.now() + 1, role: 'ai', content: '发送失败,请重试。' })
- } finally {
- loadingResponse.value = false
- scrollToBottom()
- }
- }
- function onScrollToLower() {
- // reached top — could load more history if paginated
- }
- </script>
- <style scoped lang="scss">
- .page-chat {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background: var(--color-bg);
- }
- /* Member bar */
- .member-bar {
- display: flex;
- align-items: center;
- padding: 10px 16px;
- background: var(--color-bg-card);
- border-bottom: 1px solid var(--color-border);
- gap: 10px;
- }
- .member-bar-title {
- font-size: 13px;
- font-weight: 600;
- color: var(--color-text-secondary);
- }
- .member-avatars {
- display: flex;
- gap: 4px;
- }
- .mini-avatar {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: var(--color-brand);
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .mini-avatar-text {
- font-size: 11px;
- font-weight: 700;
- color: #FFFFFF;
- }
- /* Chat scroll area */
- .chat-scroll {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
- }
- .chat-list {
- display: flex;
- flex-direction: column;
- }
- /* Message item */
- .msg-item {
- display: flex;
- align-items: flex-start;
- margin-bottom: 16px;
- gap: 10px;
- }
- .msg-user {
- justify-content: flex-end;
- }
- .msg-ai {
- justify-content: flex-start;
- }
- .msg-avatar {
- width: 36px;
- height: 36px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- font-size: 12px;
- font-weight: 700;
- }
- .user-avatar {
- background: var(--color-gold);
- color: #FFFFFF;
- }
- .ai-avatar {
- background: var(--color-brand);
- color: #FFFFFF;
- }
- .msg-bubble {
- max-width: 72%;
- padding: 12px 14px;
- border-radius: 16px;
- word-break: break-word;
- }
- .bubble-user {
- background: var(--color-brand);
- color: #FFFFFF;
- border-bottom-right-radius: 4px;
- }
- .bubble-ai {
- background: var(--color-bg-card);
- color: var(--color-text-primary);
- border-bottom-left-radius: 4px;
- border: 1px solid var(--color-border);
- }
- .msg-content {
- font-size: 14px;
- line-height: 1.6;
- white-space: pre-wrap;
- }
- .thinking-text {
- font-size: 14px;
- color: var(--color-text-secondary);
- }
- /* Input bar */
- .input-bar {
- display: flex;
- align-items: center;
- padding: 12px 16px;
- padding-bottom: 24px;
- background: var(--color-bg-card);
- border-top: 1px solid var(--color-border);
- gap: 10px;
- }
- .input-wrap {
- flex: 1;
- background: var(--color-bg);
- border-radius: 22px;
- padding: 0 16px;
- height: 44px;
- display: flex;
- align-items: center;
- }
- .chat-input {
- width: 100%;
- font-size: 15px;
- color: var(--color-text-primary);
- background: transparent;
- }
- .input-placeholder {
- color: var(--color-text-secondary);
- font-size: 14px;
- }
- .btn-send {
- width: 60px;
- height: 40px;
- background: var(--color-gold);
- border-radius: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .btn-send:active {
- transform: scale(0.95);
- }
- .btn-send.disabled {
- opacity: 0.5;
- pointer-events: none;
- }
- .btn-send-text {
- color: #FFFFFF;
- font-size: 14px;
- font-weight: 700;
- }
- </style>
|