DataInterpretationModal.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <template>
  2. <a-modal
  3. :visible="visible"
  4. :title="null"
  5. :footer="null"
  6. :width="620"
  7. :closable="false"
  8. :destroy-on-close="true"
  9. wrap-class-name="data-interpretation-modal"
  10. @cancel="$emit('close')"
  11. >
  12. <!-- 标题栏 -->
  13. <div class="di-header">
  14. <span class="di-header__title">数据解读</span>
  15. <span class="di-header__close" @click="$emit('close')">✕</span>
  16. </div>
  17. <!-- 内容区域 -->
  18. <div class="di-body">
  19. <!-- 过程状态展示区 -->
  20. <div v-if="showProcess" class="di-process">
  21. <!-- 思考中 -->
  22. <div v-if="thinking" class="di-process__row di-process__thinking"><a-spin size="small" /><span>AI 正在思考...</span></div>
  23. <!-- Todo 列表 -->
  24. <div v-if="todos.length > 0" class="di-process__todos">
  25. <div v-for="(todo, i) in todos" :key="i" class="di-todo-item" :class="`di-todo-item--${todo.status}`">
  26. <span class="di-todo-item__icon">
  27. <span v-if="todo.status === 'completed'" class="di-todo-check">✓</span>
  28. <a-spin v-else-if="todo.status === 'in_progress'" size="small" />
  29. <span v-else class="di-todo-dot"></span>
  30. </span>
  31. <span class="di-todo-item__text">{{ todo.content }}</span>
  32. </div>
  33. </div>
  34. <!-- 执行工具中 -->
  35. <!-- <div v-if="executingTools.length > 0" class="di-process__row di-process__executing">
  36. <a-spin size="small" />
  37. <span>正在执行:{{ executingTools.join(', ') }}</span>
  38. </div> -->
  39. <!-- 工具调用记录 -->
  40. <!-- <div v-if="toolCalls.length > 0" class="di-process__tools">
  41. <div v-for="(tc, i) in toolCalls" :key="i" class="di-tool-item">
  42. <span class="di-tool-item__dot" :class="{ 'di-tool-item__dot--done': tc.status === 'result' }"></span>
  43. <span class="di-tool-item__name">{{ tc.tool }}</span>
  44. <span v-if="tc.status === 'call'" class="di-tool-item__tag">执行中</span>
  45. <span v-else class="di-tool-item__tag di-tool-item__tag--done">完成</span>
  46. </div>
  47. </div> -->
  48. </div>
  49. <!-- 无过程且加载中 -->
  50. <div v-if="loading && !hasContent && !hasProcess" class="di-loading">
  51. <a-spin tip="正在分析数据..." />
  52. </div>
  53. <!-- Markdown 正文 -->
  54. <div v-if="hasContent" class="di-markdown" v-html="renderedText"></div>
  55. <!-- 错误 -->
  56. <div v-else-if="error && !loading" class="di-error">数据解读失败:{{ error }}</div>
  57. </div>
  58. <!-- 流式加载指示条 -->
  59. <div v-if="loading && hasContent" class="di-streaming-bar">
  60. <a-spin size="small" /><span class="di-streaming-bar__tip">AI 正在生成...</span>
  61. </div>
  62. </a-modal>
  63. </template>
  64. <script lang="ts" setup>
  65. import { computed } from 'vue';
  66. import markdownIt from 'markdown-it';
  67. import type { TodoItem, ToolCallRecord } from '../types';
  68. const props = defineProps<{
  69. visible: boolean;
  70. loading: boolean;
  71. streamingText: string;
  72. error: string | null;
  73. thinking: boolean;
  74. todos: TodoItem[];
  75. executingTools: string[];
  76. toolCalls: ToolCallRecord[];
  77. }>();
  78. defineEmits<{ close: [] }>();
  79. const md = markdownIt();
  80. const hasContent = computed(() => !!props.streamingText);
  81. const hasProcess = computed(() => props.thinking || props.todos.length > 0 || props.executingTools.length > 0 || props.toolCalls.length > 0);
  82. const showProcess = computed(() => hasProcess.value);
  83. const renderedText = computed(() => {
  84. if (!props.streamingText) return '';
  85. const processed = props.streamingText.replace(/<br\s*\/?>/gi, ' \n');
  86. return md.render(processed);
  87. });
  88. </script>
  89. <style lang="less">
  90. .data-interpretation-modal {
  91. .ant-modal-content {
  92. background: linear-gradient(180deg, #0a1a3a 0%, #0d2450 100%);
  93. border: 1px solid rgba(30, 144, 255, 0.3);
  94. border-radius: 8px;
  95. box-shadow: 0 0 30px rgba(0, 120, 255, 0.15);
  96. color: #c8d6e5;
  97. }
  98. .ant-modal-body {
  99. padding: 0;
  100. max-height: 75vh;
  101. overflow-y: auto;
  102. }
  103. .ant-spin-text {
  104. color: #c8d6e5;
  105. }
  106. }
  107. </style>
  108. <style lang="less" scoped>
  109. .di-header {
  110. display: flex;
  111. justify-content: space-between;
  112. align-items: center;
  113. padding: 18px 24px;
  114. background: linear-gradient(90deg, rgba(30, 144, 255, 0.15) 0%, transparent 100%);
  115. border-bottom: 1px solid rgba(30, 144, 255, 0.15);
  116. flex-shrink: 0;
  117. &__title {
  118. font-size: 18px;
  119. font-weight: bold;
  120. color: #4da6ff;
  121. letter-spacing: 2px;
  122. }
  123. &__close {
  124. cursor: pointer;
  125. font-size: 18px;
  126. color: #8899aa;
  127. &:hover {
  128. color: #fff;
  129. }
  130. }
  131. }
  132. .di-body {
  133. padding: 18px 24px;
  134. min-height: 120px;
  135. max-height: 700px;
  136. overflow-y: auto;
  137. }
  138. .di-loading {
  139. display: flex;
  140. justify-content: center;
  141. align-items: center;
  142. padding: 40px 0;
  143. }
  144. // ── 过程状态区 ──
  145. .di-process {
  146. margin-bottom: 16px;
  147. padding-bottom: 12px;
  148. border-bottom: 1px solid rgba(30, 144, 255, 0.12);
  149. &__row {
  150. display: flex;
  151. align-items: center;
  152. gap: 8px;
  153. padding: 6px 0;
  154. font-size: 13px;
  155. color: #8899aa;
  156. }
  157. }
  158. .di-process__thinking {
  159. color: #4da6ff;
  160. font-size: 13px;
  161. }
  162. .di-process__executing {
  163. color: #c8a853;
  164. font-size: 13px;
  165. }
  166. // ── Todo 列表 ──
  167. .di-process__todos {
  168. margin: 6px 0;
  169. }
  170. .di-todo-item {
  171. display: flex;
  172. align-items: center;
  173. gap: 8px;
  174. padding: 4px 0;
  175. font-size: 13px;
  176. line-height: 1.6;
  177. &__icon {
  178. flex-shrink: 0;
  179. width: 16px;
  180. display: flex;
  181. align-items: center;
  182. justify-content: center;
  183. }
  184. &__text {
  185. color: #c8d6e5;
  186. }
  187. &--completed &__text {
  188. color: #6b7a8a;
  189. text-decoration: line-through;
  190. }
  191. &--in_progress &__text {
  192. color: #4da6ff;
  193. }
  194. }
  195. .di-todo-check {
  196. color: #27ae60;
  197. font-size: 12px;
  198. font-weight: bold;
  199. }
  200. .di-todo-dot {
  201. width: 6px;
  202. height: 6px;
  203. border-radius: 50%;
  204. background: #4a5568;
  205. display: inline-block;
  206. }
  207. // ── 工具调用记录 ──
  208. .di-process__tools {
  209. margin-top: 4px;
  210. }
  211. .di-tool-item {
  212. display: flex;
  213. align-items: center;
  214. gap: 6px;
  215. padding: 2px 0;
  216. font-size: 12px;
  217. color: #6b7a8a;
  218. &__dot {
  219. width: 6px;
  220. height: 6px;
  221. border-radius: 50%;
  222. background: #c8a853;
  223. flex-shrink: 0;
  224. &--done {
  225. background: #27ae60;
  226. }
  227. }
  228. &__name {
  229. color: #8899aa;
  230. }
  231. &__tag {
  232. font-size: 10px;
  233. padding: 0 4px;
  234. border-radius: 2px;
  235. background: rgba(200, 168, 83, 0.2);
  236. color: #c8a853;
  237. &--done {
  238. background: rgba(39, 174, 96, 0.15);
  239. color: #27ae60;
  240. }
  241. }
  242. }
  243. // ── Markdown 正文 ──
  244. .di-markdown {
  245. line-height: 1.8;
  246. color: #c8d6e5;
  247. word-break: break-word;
  248. :deep(h1),
  249. :deep(h2),
  250. :deep(h3),
  251. :deep(h4) {
  252. color: #4da6ff;
  253. margin: 12px 0 8px;
  254. }
  255. :deep(p) {
  256. margin: 6px 0;
  257. }
  258. :deep(ul),
  259. :deep(ol) {
  260. padding-left: 20px;
  261. margin: 6px 0;
  262. }
  263. :deep(strong) {
  264. color: #fff;
  265. }
  266. :deep(code) {
  267. background: rgba(30, 144, 255, 0.15);
  268. padding: 1px 5px;
  269. border-radius: 3px;
  270. font-size: 13px;
  271. }
  272. :deep(pre) {
  273. background: rgba(0, 0, 0, 0.3);
  274. padding: 12px;
  275. border-radius: 6px;
  276. overflow-x: auto;
  277. }
  278. :deep(blockquote) {
  279. border-left: 3px solid #4da6ff;
  280. padding-left: 12px;
  281. margin: 8px 0;
  282. color: #8899aa;
  283. }
  284. :deep(table) {
  285. width: 100%;
  286. border-collapse: collapse;
  287. margin: 8px 0;
  288. th,
  289. td {
  290. border: 1px solid rgba(30, 144, 255, 0.2);
  291. padding: 6px 10px;
  292. text-align: left;
  293. }
  294. th {
  295. background: rgba(30, 144, 255, 0.1);
  296. color: #4da6ff;
  297. }
  298. }
  299. }
  300. .di-streaming-bar {
  301. display: flex;
  302. align-items: center;
  303. gap: 8px;
  304. padding: 8px 24px 16px;
  305. border-top: 1px solid rgba(30, 144, 255, 0.1);
  306. &__tip {
  307. font-size: 12px;
  308. color: #8899aa;
  309. }
  310. }
  311. .di-error {
  312. padding: 40px;
  313. text-align: center;
  314. color: #e74c3c;
  315. }
  316. </style>