|
@@ -105,6 +105,29 @@
|
|
|
<div v-if="message.wordDownloadUrl" class="word-download-area">
|
|
<div v-if="message.wordDownloadUrl" class="word-download-area">
|
|
|
<a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
|
|
<a-button type="link" @click="emit('download-word', message.wordDownloadUrl)"> 下载报告文件 (.docx) </a-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文件操作卡片 -->
|
|
|
|
|
+ <div v-if="message.downloadFiles?.length" class="download-file-cards">
|
|
|
|
|
+ <div v-for="(file, fIdx) in message.downloadFiles" :key="fIdx" class="download-file-card">
|
|
|
|
|
+ <div class="file-card-info">
|
|
|
|
|
+ <div class="file-card-icon"></div>
|
|
|
|
|
+ <div class="file-card-text">
|
|
|
|
|
+ <span class="file-card-name">{{ file.filename }}</span>
|
|
|
|
|
+ <span class="file-card-hint">点击操作文件</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="file-card-actions">
|
|
|
|
|
+ <button class="file-action-btn download-btn" @click="handleDownloadFile(file.url, file.filename)">
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-download-icon-btn')" width="14" height="14" />
|
|
|
|
|
+ <span>下载</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button v-if="isEditableFile(file.filename)" class="file-action-btn edit-btn" @click="handleEditFile(file.url, file.filename)">
|
|
|
|
|
+ <img :src="getCssUrl('--img-chat-edit-icon')" width="14" height="14" />
|
|
|
|
|
+ <span>编辑</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
<div v-if="message.isLoading" class="loading-dots">
|
|
<div v-if="message.isLoading" class="loading-dots">
|
|
|
<span class="dot"></span>
|
|
<span class="dot"></span>
|
|
|
<span class="dot"></span>
|
|
<span class="dot"></span>
|
|
@@ -128,31 +151,47 @@
|
|
|
<div
|
|
<div
|
|
|
v-for="(_, sIdx) in message.pendingApprovalData.ask_user.questions"
|
|
v-for="(_, sIdx) in message.pendingApprovalData.ask_user.questions"
|
|
|
:key="sIdx"
|
|
:key="sIdx"
|
|
|
- :class="['ask-user-step-dot', {
|
|
|
|
|
- active: sIdx === (askUserCurrentStep[index] ?? 0),
|
|
|
|
|
- completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim())
|
|
|
|
|
- }]"
|
|
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'ask-user-step-dot',
|
|
|
|
|
+ {
|
|
|
|
|
+ active: sIdx === (askUserCurrentStep[index] ?? 0),
|
|
|
|
|
+ completed: sIdx < (askUserCurrentStep[index] ?? 0) || !!(askUserAnswers[index]?.[sIdx] && askUserAnswers[index][sIdx].trim()),
|
|
|
|
|
+ },
|
|
|
|
|
+ ]"
|
|
|
@click="askUserCurrentStep[index] = sIdx"
|
|
@click="askUserCurrentStep[index] = sIdx"
|
|
|
></div>
|
|
></div>
|
|
|
- <span class="ask-user-step-text">{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span>
|
|
|
|
|
|
|
+ <span class="ask-user-step-text"
|
|
|
|
|
+ >{{ (askUserCurrentStep[index] ?? 0) + 1 }} / {{ message.pendingApprovalData.ask_user.questions.length }}</span
|
|
|
|
|
+ >
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 当前问题 -->
|
|
<!-- 当前问题 -->
|
|
|
- <div
|
|
|
|
|
- v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]"
|
|
|
|
|
- class="ask-user-question"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]" class="ask-user-question">
|
|
|
<div class="ask-user-question-text">
|
|
<div class="ask-user-question-text">
|
|
|
{{ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].question }}
|
|
{{ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].question }}
|
|
|
- <span v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false" class="ask-user-required">*</span>
|
|
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].required !== false"
|
|
|
|
|
+ class="ask-user-required"
|
|
|
|
|
+ >*</span
|
|
|
|
|
+ >
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- multiple_choice 类型 -->
|
|
<!-- multiple_choice 类型 -->
|
|
|
- <div v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'" class="ask-user-choices">
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'multiple_choice'"
|
|
|
|
|
+ class="ask-user-choices"
|
|
|
|
|
+ >
|
|
|
<div
|
|
<div
|
|
|
v-for="(choice, cIdx) in message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].choices"
|
|
v-for="(choice, cIdx) in message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].choices"
|
|
|
:key="cIdx"
|
|
:key="cIdx"
|
|
|
- :class="['ask-user-choice', { selected: askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) && !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0] }]"
|
|
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'ask-user-choice',
|
|
|
|
|
+ {
|
|
|
|
|
+ selected:
|
|
|
|
|
+ askUserAnswers[index]?.[askUserCurrentStep[index] ?? 0] === getChoiceValue(choice) &&
|
|
|
|
|
+ !askUserOtherSelected[index]?.[askUserCurrentStep[index] ?? 0],
|
|
|
|
|
+ },
|
|
|
|
|
+ ]"
|
|
|
@click="selectChoice(index, askUserCurrentStep[index] ?? 0, choice, message.pendingApprovalData.ask_user.questions.length)"
|
|
@click="selectChoice(index, askUserCurrentStep[index] ?? 0, choice, message.pendingApprovalData.ask_user.questions.length)"
|
|
|
>
|
|
>
|
|
|
<span>{{ getChoiceValue(choice) }}</span>
|
|
<span>{{ getChoiceValue(choice) }}</span>
|
|
@@ -174,7 +213,10 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- text 类型 -->
|
|
<!-- text 类型 -->
|
|
|
- <div v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'" class="ask-user-text">
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-else-if="message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0].type === 'text'"
|
|
|
|
|
+ class="ask-user-text"
|
|
|
|
|
+ >
|
|
|
<textarea
|
|
<textarea
|
|
|
class="ask-user-textarea"
|
|
class="ask-user-textarea"
|
|
|
placeholder="请输入..."
|
|
placeholder="请输入..."
|
|
@@ -186,23 +228,33 @@
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
<!-- 操作按钮 -->
|
|
|
<div class="approval-actions">
|
|
<div class="approval-actions">
|
|
|
- <button
|
|
|
|
|
- v-if="(askUserCurrentStep[index] ?? 0) > 0"
|
|
|
|
|
- class="approval-btn nav-btn"
|
|
|
|
|
- @click="goPrevStep(index)"
|
|
|
|
|
- >上一步</button>
|
|
|
|
|
|
|
+ <button v-if="(askUserCurrentStep[index] ?? 0) > 0" class="approval-btn nav-btn" @click="goPrevStep(index)">上一步</button>
|
|
|
<button
|
|
<button
|
|
|
v-if="(askUserCurrentStep[index] ?? 0) < message.pendingApprovalData.ask_user.questions.length - 1"
|
|
v-if="(askUserCurrentStep[index] ?? 0) < message.pendingApprovalData.ask_user.questions.length - 1"
|
|
|
class="approval-btn approve-btn"
|
|
class="approval-btn approve-btn"
|
|
|
- :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
|
|
|
|
|
|
|
+ :disabled="
|
|
|
|
|
+ !isCurrentStepAnswered(
|
|
|
|
|
+ index,
|
|
|
|
|
+ askUserCurrentStep[index] ?? 0,
|
|
|
|
|
+ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
|
|
|
|
|
+ )
|
|
|
|
|
+ "
|
|
|
@click="goNextStep(index, message.pendingApprovalData.ask_user.questions.length)"
|
|
@click="goNextStep(index, message.pendingApprovalData.ask_user.questions.length)"
|
|
|
- >下一步</button>
|
|
|
|
|
|
|
+ >下一步</button
|
|
|
|
|
+ >
|
|
|
<button
|
|
<button
|
|
|
v-if="(askUserCurrentStep[index] ?? 0) === message.pendingApprovalData.ask_user.questions.length - 1"
|
|
v-if="(askUserCurrentStep[index] ?? 0) === message.pendingApprovalData.ask_user.questions.length - 1"
|
|
|
class="approval-btn approve-btn"
|
|
class="approval-btn approve-btn"
|
|
|
- :disabled="!isCurrentStepAnswered(index, askUserCurrentStep[index] ?? 0, message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0])"
|
|
|
|
|
|
|
+ :disabled="
|
|
|
|
|
+ !isCurrentStepAnswered(
|
|
|
|
|
+ index,
|
|
|
|
|
+ askUserCurrentStep[index] ?? 0,
|
|
|
|
|
+ message.pendingApprovalData.ask_user.questions[askUserCurrentStep[index] ?? 0]
|
|
|
|
|
+ )
|
|
|
|
|
+ "
|
|
|
@click="handleSubmitAskUser(index)"
|
|
@click="handleSubmitAskUser(index)"
|
|
|
- >提交</button>
|
|
|
|
|
|
|
+ >提交</button
|
|
|
|
|
+ >
|
|
|
<button class="approval-btn reject-btn" @click="handleCancelAskUser">取消</button>
|
|
<button class="approval-btn reject-btn" @click="handleCancelAskUser">取消</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -263,7 +315,9 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
|
import dayjs from 'dayjs';
|
|
import dayjs from 'dayjs';
|
|
|
- import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
|
|
|
|
|
|
|
+ import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv, isMdFile, isTextFile } from './utils';
|
|
|
|
|
+ import { getToken } from '/@/utils/auth';
|
|
|
|
|
+ import { message } from 'ant-design-vue';
|
|
|
import type { Message, AttachedFile, ThinkingStep } from './types';
|
|
import type { Message, AttachedFile, ThinkingStep } from './types';
|
|
|
// import katex from 'katex';
|
|
// import katex from 'katex';
|
|
|
// import 'katex/dist/katex.min.css';
|
|
// import 'katex/dist/katex.min.css';
|
|
@@ -336,10 +390,53 @@
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
|
(e: 'file-preview', file: AttachedFile): void;
|
|
(e: 'file-preview', file: AttachedFile): void;
|
|
|
(e: 'download-word', url: string): void;
|
|
(e: 'download-word', url: string): void;
|
|
|
|
|
+ (e: 'open-file-preview', file: AttachedFile): void;
|
|
|
(e: 'ask-user-submit', answers: string[]): void;
|
|
(e: 'ask-user-submit', answers: string[]): void;
|
|
|
(e: 'ask-user-cancel'): void;
|
|
(e: 'ask-user-cancel'): void;
|
|
|
}>();
|
|
}>();
|
|
|
|
|
|
|
|
|
|
+ const isEditableFile = (filename: string) => isMdFile(filename) || isTextFile(filename);
|
|
|
|
|
+
|
|
|
|
|
+ const handleDownloadFile = (url: string, filename: string) => {
|
|
|
|
|
+ if (!url) return;
|
|
|
|
|
+ const a = document.createElement('a');
|
|
|
|
|
+ a.href = url;
|
|
|
|
|
+ a.download = filename;
|
|
|
|
|
+ document.body.appendChild(a);
|
|
|
|
|
+ a.click();
|
|
|
|
|
+ document.body.removeChild(a);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleEditFile = async (url: string, filename: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(url, { headers: { 'X-Access-Token': getToken() } });
|
|
|
|
|
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
+ const content = await resp.text();
|
|
|
|
|
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
|
|
|
+ const mimeMap: Record<string, string> = {
|
|
|
|
|
+ md: 'text/markdown',
|
|
|
|
|
+ txt: 'text/plain',
|
|
|
|
|
+ json: 'application/json',
|
|
|
|
|
+ xml: 'text/xml',
|
|
|
|
|
+ yaml: 'text/yaml',
|
|
|
|
|
+ yml: 'text/yaml',
|
|
|
|
|
+ csv: 'text/csv',
|
|
|
|
|
+ };
|
|
|
|
|
+ const file: AttachedFile = {
|
|
|
|
|
+ id: `report-${Date.now()}`,
|
|
|
|
|
+ name: filename,
|
|
|
|
|
+ size: content.length,
|
|
|
|
|
+ type: mimeMap[ext] || 'text/plain',
|
|
|
|
|
+ content,
|
|
|
|
|
+ uploadTime: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ emit('open-file-preview', file);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('获取文件内容失败:', err);
|
|
|
|
|
+ message.error('获取文件内容失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// ask_user 答案状态(按消息索引存储)
|
|
// ask_user 答案状态(按消息索引存储)
|
|
|
const askUserAnswers = reactive<Record<number, string[]>>({});
|
|
const askUserAnswers = reactive<Record<number, string[]>>({});
|
|
|
const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
|
|
const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
|
|
@@ -645,7 +742,7 @@
|
|
|
.message-body {
|
|
.message-body {
|
|
|
padding: 12px 12px 4px 12px;
|
|
padding: 12px 12px 4px 12px;
|
|
|
border-radius: 6px;
|
|
border-radius: 6px;
|
|
|
- width: calc(100% - 50px);
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
|
|
@@ -1269,6 +1366,112 @@
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .download-file-cards {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .download-file-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ background: linear-gradient(135deg, rgba(0, 47, 74, 0.8) 0%, rgba(0, 60, 90, 0.6) 100%);
|
|
|
|
|
+ border: 1px solid rgba(0, 84, 130, 0.6);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: rgba(10, 132, 255, 0.4);
|
|
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-icon {
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ background-image: var(--img-chat-download-icon);
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ background-size: 100% 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-text {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-hint {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #6e7681;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-card-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .file-action-btn {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 7px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+
|
|
|
|
|
+ &.download-btn {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.08);
|
|
|
|
|
+ color: #c9d1d9;
|
|
|
|
|
+ border: 1px solid rgba(63, 80, 106, 0.5);
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.14);
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ border-color: rgba(63, 80, 106, 0.8);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.edit-btn {
|
|
|
|
|
+ background: linear-gradient(135deg, #0a84ff 0%, #0066cc 100%);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border: 1px solid rgba(10, 132, 255, 0.5);
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(10, 132, 255, 0.2);
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: linear-gradient(135deg, #3a9bfd 0%, #0a84ff 100%);
|
|
|
|
|
+ box-shadow: 0 3px 10px rgba(10, 132, 255, 0.35);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.user-message {
|
|
.user-message {
|