| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- <template>
- <a-modal
- :visible="visible"
- :title="null"
- :footer="null"
- :width="620"
- :closable="false"
- :destroy-on-close="true"
- wrap-class-name="data-interpretation-modal"
- @cancel="$emit('close')"
- >
- <!-- 标题栏 -->
- <div class="di-header">
- <span class="di-header__title">数据解读</span>
- <span class="di-header__close" @click="$emit('close')">✕</span>
- </div>
- <!-- 内容区域 -->
- <div class="di-body">
- <!-- 过程状态展示区 -->
- <div v-if="showProcess" class="di-process">
- <!-- 思考中 -->
- <div v-if="thinking" class="di-process__row di-process__thinking"><a-spin size="small" /><span>AI 正在思考...</span></div>
- <!-- Todo 列表 -->
- <div v-if="todos.length > 0" class="di-process__todos">
- <div v-for="(todo, i) in todos" :key="i" class="di-todo-item" :class="`di-todo-item--${todo.status}`">
- <span class="di-todo-item__icon">
- <span v-if="todo.status === 'completed'" class="di-todo-check">✓</span>
- <a-spin v-else-if="todo.status === 'in_progress'" size="small" />
- <span v-else class="di-todo-dot"></span>
- </span>
- <span class="di-todo-item__text">{{ todo.content }}</span>
- </div>
- </div>
- <!-- 执行工具中 -->
- <!-- <div v-if="executingTools.length > 0" class="di-process__row di-process__executing">
- <a-spin size="small" />
- <span>正在执行:{{ executingTools.join(', ') }}</span>
- </div> -->
- <!-- 工具调用记录 -->
- <!-- <div v-if="toolCalls.length > 0" class="di-process__tools">
- <div v-for="(tc, i) in toolCalls" :key="i" class="di-tool-item">
- <span class="di-tool-item__dot" :class="{ 'di-tool-item__dot--done': tc.status === 'result' }"></span>
- <span class="di-tool-item__name">{{ tc.tool }}</span>
- <span v-if="tc.status === 'call'" class="di-tool-item__tag">执行中</span>
- <span v-else class="di-tool-item__tag di-tool-item__tag--done">完成</span>
- </div>
- </div> -->
- </div>
- <!-- 无过程且加载中 -->
- <div v-if="loading && !hasContent && !hasProcess" class="di-loading">
- <a-spin tip="正在分析数据..." />
- </div>
- <!-- Markdown 正文 -->
- <div v-if="hasContent" class="di-markdown" v-html="renderedText"></div>
- <!-- 错误 -->
- <div v-else-if="error && !loading" class="di-error">数据解读失败:{{ error }}</div>
- </div>
- <!-- 流式加载指示条 -->
- <div v-if="loading && hasContent" class="di-streaming-bar">
- <a-spin size="small" /><span class="di-streaming-bar__tip">AI 正在生成...</span>
- </div>
- </a-modal>
- </template>
- <script lang="ts" setup>
- import { computed } from 'vue';
- import markdownIt from 'markdown-it';
- import type { TodoItem, ToolCallRecord } from '../types';
- const props = defineProps<{
- visible: boolean;
- loading: boolean;
- streamingText: string;
- error: string | null;
- thinking: boolean;
- todos: TodoItem[];
- executingTools: string[];
- toolCalls: ToolCallRecord[];
- }>();
- defineEmits<{ close: [] }>();
- const md = markdownIt();
- const hasContent = computed(() => !!props.streamingText);
- const hasProcess = computed(() => props.thinking || props.todos.length > 0 || props.executingTools.length > 0 || props.toolCalls.length > 0);
- const showProcess = computed(() => hasProcess.value);
- const renderedText = computed(() => {
- if (!props.streamingText) return '';
- const processed = props.streamingText.replace(/<br\s*\/?>/gi, ' \n');
- return md.render(processed);
- });
- </script>
- <style lang="less">
- .data-interpretation-modal {
- .ant-modal-content {
- background: linear-gradient(180deg, #0a1a3a 0%, #0d2450 100%);
- border: 1px solid rgba(30, 144, 255, 0.3);
- border-radius: 8px;
- box-shadow: 0 0 30px rgba(0, 120, 255, 0.15);
- color: #c8d6e5;
- }
- .ant-modal-body {
- padding: 0;
- max-height: 75vh;
- overflow-y: auto;
- }
- .ant-spin-text {
- color: #c8d6e5;
- }
- }
- </style>
- <style lang="less" scoped>
- .di-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 18px 24px;
- background: linear-gradient(90deg, rgba(30, 144, 255, 0.15) 0%, transparent 100%);
- border-bottom: 1px solid rgba(30, 144, 255, 0.15);
- flex-shrink: 0;
- &__title {
- font-size: 18px;
- font-weight: bold;
- color: #4da6ff;
- letter-spacing: 2px;
- }
- &__close {
- cursor: pointer;
- font-size: 18px;
- color: #8899aa;
- &:hover {
- color: #fff;
- }
- }
- }
- .di-body {
- padding: 18px 24px;
- min-height: 120px;
- max-height: 700px;
- overflow-y: auto;
- }
- .di-loading {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 40px 0;
- }
- // ── 过程状态区 ──
- .di-process {
- margin-bottom: 16px;
- padding-bottom: 12px;
- border-bottom: 1px solid rgba(30, 144, 255, 0.12);
- &__row {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 6px 0;
- font-size: 13px;
- color: #8899aa;
- }
- }
- .di-process__thinking {
- color: #4da6ff;
- font-size: 13px;
- }
- .di-process__executing {
- color: #c8a853;
- font-size: 13px;
- }
- // ── Todo 列表 ──
- .di-process__todos {
- margin: 6px 0;
- }
- .di-todo-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 4px 0;
- font-size: 13px;
- line-height: 1.6;
- &__icon {
- flex-shrink: 0;
- width: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- &__text {
- color: #c8d6e5;
- }
- &--completed &__text {
- color: #6b7a8a;
- text-decoration: line-through;
- }
- &--in_progress &__text {
- color: #4da6ff;
- }
- }
- .di-todo-check {
- color: #27ae60;
- font-size: 12px;
- font-weight: bold;
- }
- .di-todo-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: #4a5568;
- display: inline-block;
- }
- // ── 工具调用记录 ──
- .di-process__tools {
- margin-top: 4px;
- }
- .di-tool-item {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 2px 0;
- font-size: 12px;
- color: #6b7a8a;
- &__dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: #c8a853;
- flex-shrink: 0;
- &--done {
- background: #27ae60;
- }
- }
- &__name {
- color: #8899aa;
- }
- &__tag {
- font-size: 10px;
- padding: 0 4px;
- border-radius: 2px;
- background: rgba(200, 168, 83, 0.2);
- color: #c8a853;
- &--done {
- background: rgba(39, 174, 96, 0.15);
- color: #27ae60;
- }
- }
- }
- // ── Markdown 正文 ──
- .di-markdown {
- line-height: 1.8;
- color: #c8d6e5;
- word-break: break-word;
- :deep(h1),
- :deep(h2),
- :deep(h3),
- :deep(h4) {
- color: #4da6ff;
- margin: 12px 0 8px;
- }
- :deep(p) {
- margin: 6px 0;
- }
- :deep(ul),
- :deep(ol) {
- padding-left: 20px;
- margin: 6px 0;
- }
- :deep(strong) {
- color: #fff;
- }
- :deep(code) {
- background: rgba(30, 144, 255, 0.15);
- padding: 1px 5px;
- border-radius: 3px;
- font-size: 13px;
- }
- :deep(pre) {
- background: rgba(0, 0, 0, 0.3);
- padding: 12px;
- border-radius: 6px;
- overflow-x: auto;
- }
- :deep(blockquote) {
- border-left: 3px solid #4da6ff;
- padding-left: 12px;
- margin: 8px 0;
- color: #8899aa;
- }
- :deep(table) {
- width: 100%;
- border-collapse: collapse;
- margin: 8px 0;
- th,
- td {
- border: 1px solid rgba(30, 144, 255, 0.2);
- padding: 6px 10px;
- text-align: left;
- }
- th {
- background: rgba(30, 144, 255, 0.1);
- color: #4da6ff;
- }
- }
- }
- .di-streaming-bar {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 24px 16px;
- border-top: 1px solid rgba(30, 144, 255, 0.1);
- &__tip {
- font-size: 12px;
- color: #8899aa;
- }
- }
- .di-error {
- padding: 40px;
- text-align: center;
- color: #e74c3c;
- }
- </style>
|