analysis-chat.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <template>
  2. <view class="page-chat" :class="themeClass">
  3. <!-- ========== Member Bar ========== -->
  4. <view class="member-bar">
  5. <text class="member-bar-title">分析成员</text>
  6. <view class="member-avatars">
  7. <view v-for="(m, i) in memberList" :key="i" class="mini-avatar">
  8. <text class="mini-avatar-text">{{ m.name ? m.name[0] : '?' }}</text>
  9. </view>
  10. </view>
  11. </view>
  12. <!-- ========== Chat Messages ========== -->
  13. <scroll-view
  14. class="chat-scroll"
  15. scroll-y
  16. :scroll-into-view="scrollToId"
  17. scroll-with-animation
  18. @scrolltolower="onScrollToLower"
  19. >
  20. <view class="chat-list">
  21. <view
  22. v-for="(msg, idx) in messages"
  23. :key="msg.id || idx"
  24. :id="'msg-' + idx"
  25. class="msg-item"
  26. :class="msg.role === 'user' ? 'msg-user' : 'msg-ai'"
  27. >
  28. <view v-if="msg.role === 'ai'" class="msg-avatar ai-avatar">AI</view>
  29. <view class="msg-bubble" :class="msg.role === 'user' ? 'bubble-user' : 'bubble-ai'">
  30. <text class="msg-content">{{ msg.content }}</text>
  31. </view>
  32. <view v-if="msg.role === 'user'" class="msg-avatar user-avatar">我</view>
  33. </view>
  34. <!-- Loading indicator -->
  35. <view v-if="loadingResponse" class="msg-item msg-ai">
  36. <view class="msg-avatar ai-avatar">AI</view>
  37. <view class="msg-bubble bubble-ai">
  38. <text class="thinking-text">正在思考...</text>
  39. </view>
  40. </view>
  41. </view>
  42. </scroll-view>
  43. <!-- ========== Input Bar ========== -->
  44. <view class="input-bar">
  45. <view class="input-wrap">
  46. <input
  47. v-model="inputText"
  48. class="chat-input"
  49. placeholder="输入您的问题..."
  50. placeholder-class="input-placeholder"
  51. :disabled="loadingResponse"
  52. @confirm="sendMessage"
  53. confirm-type="send"
  54. />
  55. </view>
  56. <view
  57. class="btn-send"
  58. :class="{ disabled: !inputText.trim() || loadingResponse }"
  59. @click="sendMessage"
  60. >
  61. <text class="btn-send-text">发送</text>
  62. </view>
  63. </view>
  64. </view>
  65. </template>
  66. <script setup>
  67. import { ref, computed, nextTick, watch } from 'vue'
  68. import { onLoad } from '@dcloudio/uni-app'
  69. import request from '@/utils/api'
  70. import { useTheme } from '@/composables/useThemeStyle'
  71. const { themeClass } = useTheme()
  72. const recordId = ref(null)
  73. const messages = ref([])
  74. const inputText = ref('')
  75. const loadingResponse = ref(false)
  76. const memberList = ref([])
  77. const scrollToId = ref('')
  78. onLoad(async (query) => {
  79. if (query && query.recordId) {
  80. recordId.value = query.recordId
  81. await Promise.all([loadMembers(), loadHistory()])
  82. }
  83. })
  84. async function loadMembers() {
  85. try {
  86. const detail = await request.post('/relation/analysis/detail', { id: recordId.value })
  87. if (detail && detail.memberSnapshot) {
  88. try {
  89. const parsed = JSON.parse(detail.memberSnapshot)
  90. memberList.value = Array.isArray(parsed) ? parsed : []
  91. } catch (e) {
  92. memberList.value = []
  93. }
  94. }
  95. if (detail && detail.title) {
  96. uni.setNavigationBarTitle({ title: detail.title })
  97. }
  98. } catch (e) {
  99. // ignore
  100. }
  101. }
  102. async function loadHistory() {
  103. try {
  104. const msgs = await request.post('/relation/analysis/chat/history', { recordId: recordId.value })
  105. messages.value = msgs || []
  106. scrollToBottom()
  107. } catch (e) {
  108. messages.value = []
  109. }
  110. }
  111. function scrollToBottom() {
  112. nextTick(() => {
  113. scrollToId.value = 'msg-' + (messages.value.length - 1)
  114. })
  115. }
  116. async function sendMessage() {
  117. const text = inputText.value.trim()
  118. if (!text || loadingResponse.value) return
  119. // Optimistically add user message
  120. const tempMsg = { id: Date.now(), role: 'user', content: text }
  121. messages.value.push(tempMsg)
  122. inputText.value = ''
  123. scrollToBottom()
  124. loadingResponse.value = true
  125. try {
  126. const res = await request.post('/relation/analysis/chat/send', {
  127. recordId: recordId.value,
  128. query: text
  129. })
  130. if (res && res.answer) {
  131. messages.value.push({ id: Date.now() + 1, role: 'ai', content: res.answer })
  132. } else {
  133. messages.value.push({ id: Date.now() + 1, role: 'ai', content: 'AI 解读服务暂时不可用,请稍后再试。' })
  134. }
  135. } catch (e) {
  136. messages.value.push({ id: Date.now() + 1, role: 'ai', content: '发送失败,请重试。' })
  137. } finally {
  138. loadingResponse.value = false
  139. scrollToBottom()
  140. }
  141. }
  142. function onScrollToLower() {
  143. // reached top — could load more history if paginated
  144. }
  145. </script>
  146. <style scoped lang="scss">
  147. .page-chat {
  148. display: flex;
  149. flex-direction: column;
  150. height: 100vh;
  151. background: var(--color-bg);
  152. }
  153. /* Member bar */
  154. .member-bar {
  155. display: flex;
  156. align-items: center;
  157. padding: 10px 16px;
  158. background: var(--color-bg-card);
  159. border-bottom: 1px solid var(--color-border);
  160. gap: 10px;
  161. }
  162. .member-bar-title {
  163. font-size: 13px;
  164. font-weight: 600;
  165. color: var(--color-text-secondary);
  166. }
  167. .member-avatars {
  168. display: flex;
  169. gap: 4px;
  170. }
  171. .mini-avatar {
  172. width: 28px;
  173. height: 28px;
  174. border-radius: 50%;
  175. background: var(--color-brand);
  176. display: flex;
  177. align-items: center;
  178. justify-content: center;
  179. }
  180. .mini-avatar-text {
  181. font-size: 11px;
  182. font-weight: 700;
  183. color: #FFFFFF;
  184. }
  185. /* Chat scroll area */
  186. .chat-scroll {
  187. flex: 1;
  188. overflow-y: auto;
  189. padding: 16px;
  190. }
  191. .chat-list {
  192. display: flex;
  193. flex-direction: column;
  194. }
  195. /* Message item */
  196. .msg-item {
  197. display: flex;
  198. align-items: flex-start;
  199. margin-bottom: 16px;
  200. gap: 10px;
  201. }
  202. .msg-user {
  203. justify-content: flex-end;
  204. }
  205. .msg-ai {
  206. justify-content: flex-start;
  207. }
  208. .msg-avatar {
  209. width: 36px;
  210. height: 36px;
  211. border-radius: 50%;
  212. display: flex;
  213. align-items: center;
  214. justify-content: center;
  215. flex-shrink: 0;
  216. font-size: 12px;
  217. font-weight: 700;
  218. }
  219. .user-avatar {
  220. background: var(--color-gold);
  221. color: #FFFFFF;
  222. }
  223. .ai-avatar {
  224. background: var(--color-brand);
  225. color: #FFFFFF;
  226. }
  227. .msg-bubble {
  228. max-width: 72%;
  229. padding: 12px 14px;
  230. border-radius: 16px;
  231. word-break: break-word;
  232. }
  233. .bubble-user {
  234. background: var(--color-brand);
  235. color: #FFFFFF;
  236. border-bottom-right-radius: 4px;
  237. }
  238. .bubble-ai {
  239. background: var(--color-bg-card);
  240. color: var(--color-text-primary);
  241. border-bottom-left-radius: 4px;
  242. border: 1px solid var(--color-border);
  243. }
  244. .msg-content {
  245. font-size: 14px;
  246. line-height: 1.6;
  247. white-space: pre-wrap;
  248. }
  249. .thinking-text {
  250. font-size: 14px;
  251. color: var(--color-text-secondary);
  252. }
  253. /* Input bar */
  254. .input-bar {
  255. display: flex;
  256. align-items: center;
  257. padding: 12px 16px;
  258. padding-bottom: 24px;
  259. background: var(--color-bg-card);
  260. border-top: 1px solid var(--color-border);
  261. gap: 10px;
  262. }
  263. .input-wrap {
  264. flex: 1;
  265. background: var(--color-bg);
  266. border-radius: 22px;
  267. padding: 0 16px;
  268. height: 44px;
  269. display: flex;
  270. align-items: center;
  271. }
  272. .chat-input {
  273. width: 100%;
  274. font-size: 15px;
  275. color: var(--color-text-primary);
  276. background: transparent;
  277. }
  278. .input-placeholder {
  279. color: var(--color-text-secondary);
  280. font-size: 14px;
  281. }
  282. .btn-send {
  283. width: 60px;
  284. height: 40px;
  285. background: var(--color-gold);
  286. border-radius: 20px;
  287. display: flex;
  288. align-items: center;
  289. justify-content: center;
  290. flex-shrink: 0;
  291. }
  292. .btn-send:active {
  293. transform: scale(0.95);
  294. }
  295. .btn-send.disabled {
  296. opacity: 0.5;
  297. pointer-events: none;
  298. }
  299. .btn-send-text {
  300. color: #FFFFFF;
  301. font-size: 14px;
  302. font-weight: 700;
  303. }
  304. </style>