From 04b1a9cfde4049be9a38b9832d5289d4a192c883 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 15 五月 2026 16:29:33 +0800
Subject: [PATCH] 加班申请模块和审批流程公共组件
---
src/components/AIChatSidebar/index.vue | 4643 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 4,189 insertions(+), 454 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index d14978c..a2a365a 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -1,47 +1,75 @@
<template>
<div class="ai-chat-sidebar-wrapper">
<!-- 鎮诞鍥炬爣 -->
- <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
- <el-tooltip content="AI 鍔╂墜" placement="left">
+ <div v-if="!hideTrigger" class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
+ <el-tooltip :content="currentAssistant.tooltip" placement="left">
<div class="trigger-icon">
- <el-icon :size="30" color="#fff"><Cpu /></el-icon>
+ <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
</div>
</el-tooltip>
</div>
<!-- 渚ц竟鏍忓璇濇 -->
<el-drawer
- v-model="visible"
- :size="drawerSize"
- direction="rtl"
- :with-header="true"
- class="ai-chat-drawer"
- :modal="false"
- :show-close="true"
- :append-to-body="false"
- @close="handleClose"
+ v-model="visible"
+ :size="computedDrawerSize"
+ :direction="drawerDirection"
+ :with-header="true"
+ class="ai-chat-drawer"
+ :modal="false"
+ modal-class="ai-chat-overlay"
+ :show-close="false"
+ :append-to-body="false"
+ :close-on-press-escape="!hideTrigger"
+ :close-on-click-modal="!hideTrigger"
+ @close="handleClose"
>
<template #header>
<div class="drawer-header">
<div class="header-left">
- <el-icon :size="20" class="header-icon"><Cpu /></el-icon>
- <span class="title">AI 鏅鸿兘鍔╂墜</span>
+ <el-icon :size="20" class="header-icon"><component :is="currentAssistant.icon" /></el-icon>
+ <span class="title">{{ currentAssistant.title }}</span>
+ </div>
+ <div v-if="showAssistantSwitch" class="assistant-switcher">
+ <el-radio-group v-model="selectedAssistantKey" size="small">
+ <el-radio-button
+ v-for="assistant in assistants"
+ :key="assistant.key"
+ :label="assistant.key"
+ >
+ {{ assistant.label }}
+ </el-radio-button>
+ </el-radio-group>
</div>
<div class="header-actions">
<el-tooltip content="浼氳瘽鍘嗗彶" placement="bottom">
- <el-button link @click="toggleHistory">
+ <el-button link class="header-action-btn" @click="handleToggleHistory">
<el-icon :size="18"><Timer /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="寮�鍚柊浼氳瘽" placement="bottom">
- <el-button link @click="newChat">
+ <el-button link class="header-action-btn" @click="handleNewChat">
<el-icon :size="18"><Plus /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-button
+ v-if="headerExtraActionText"
+ link
+ class="header-action-btn header-action-btn--text"
+ @click="handleHeaderExtraAction"
+ >
+ {{ headerExtraActionText }}
+ </el-button>
+ <div v-if="!hideTrigger" class="action-divider"></div>
+ <el-tooltip v-if="!hideTrigger" content="鍏抽棴" placement="bottom">
+ <el-button link class="header-action-btn close-btn" @click="handleManualClose">
+ <el-icon :size="18"><Close /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</template>
-
+
<div class="chat-container">
<!-- 鍘嗗彶浼氳瘽鍒楄〃 -->
<div v-if="showHistory" class="history-panel">
@@ -56,124 +84,434 @@
</div>
</template>
<div class="session-list">
- <div
- v-for="session in sessions"
- :key="session.memoryId"
- :class="['session-item', { active: uuid === session.memoryId }]"
- @click="selectSession(session)"
+ <div
+ v-for="session in sessions"
+ :key="session.memoryId"
+ :class="['session-item', { active: uuid === session.memoryId }]"
+ @click="selectSession(session)"
>
<el-icon><ChatDotSquare /></el-icon>
<span class="session-name" :title="session.lastMessage || '鏂颁細璇�'">
{{ session.lastMessage || '鏂颁細璇�' }}
</span>
- <el-button
- link
- type="danger"
- class="delete-btn"
- @click.stop="handleDeleteSession(session.memoryId)"
+ <el-button
+ link
+ type="danger"
+ class="delete-btn"
+ @click.stop="handleDeleteSession(session.memoryId)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
- <el-empty v-if="sessions.length === 0" description="鏆傛棤鍘嗗彶浼氳瘽" />
+ <el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" />
</div>
</el-skeleton>
</div>
<div v-else class="chat-main">
- <div class="message-list" ref="messageListRef">
- <div
- v-for="(message, index) in messages"
- :key="index"
- :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
- >
- <div class="avatar">
- <el-icon v-if="message.isUser"><User /></el-icon>
- <el-icon v-else><Cpu /></el-icon>
- </div>
- <div class="message-content">
- <!-- 鏂囨湰鍐呭 -->
- <div class="text-box" v-html="message.htmlContent"></div>
-
- <!-- 鍥捐〃鍐呭 -->
- <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
- <div
- v-for="(option, key) in message.chartOptions"
- :key="key"
- class="chart-item"
- :id="`ai-chart-${index}-${key}`"
- ></div>
- </div>
-
- <!-- 琛ㄦ牸鍐呭 -->
- <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
- <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
- <el-table-column
- v-for="col in message.tableData.columns"
- :key="col"
- :prop="col"
- :label="columnLabelMap[col] || col"
- min-width="100"
- show-overflow-tooltip
+ <div :class="['chat-hero', { compact: hasMessages }]">
+ <div :class="['assistant-stand', { thinking: isSending, compact: hasMessages }]">
+ <div class="assistant-halo"></div>
+ <div class="assistant-scan-ring"></div>
+ <div class="assistant-orbit assistant-orbit-a"></div>
+ <div class="assistant-orbit assistant-orbit-b"></div>
+ <div class="assistant-model-shell">
+ <div class="assistant-model-cut">
+ <img
+ v-if="currentAssistantAvatar"
+ class="assistant-model-img"
+ :src="currentAssistantAvatar"
+ :alt="currentAssistant.label"
/>
- </el-table>
+ <div v-else class="assistant-model-fallback">
+ <el-icon :size="30"><component :is="currentAssistant.icon" /></el-icon>
+ </div>
+ </div>
+ </div>
+ <div class="assistant-status">
+ <span class="assistant-status-dot"></span>
+ {{ isSending ? '鎬濊�冧腑...' : currentAssistant.label }}
+ </div>
+ <div class="assistant-base assistant-base-lg"></div>
+ <div class="assistant-base assistant-base-md"></div>
+ <div class="assistant-base assistant-base-sm"></div>
+ </div>
+
+ <div :class="['welcome-card', { compact: hasMessages }]">
+ <div class="welcome-eyebrow">鏅鸿兘鍔╂墜</div>
+ <h3 class="welcome-title">
+ 鎮ㄥソ
+ <br />
+ 鎴戞槸{{ currentAssistant.label }}鍒嗘瀽瑙h鍔╂墜
+ </h3>
+ <p class="welcome-desc">
+ {{ currentAssistant.description || '鎴戝彲浠ュ洿缁曚笟鍔¢棶棰樻彁渚涜В璇汇�佹煡璇㈠缓璁拰鍒嗘瀽鏀寔锛屽府鍔╀綘鏇村揩瀹屾垚鍒ゆ柇涓庡鐞嗐��' }}
+ </p>
+
+ <div class="quick-prompt-list">
+ <button
+ v-for="prompt in displayedQuickPrompts"
+ :key="prompt"
+ type="button"
+ class="quick-prompt-btn"
+ :disabled="isSending"
+ @click="sendQuickPrompt(prompt)"
+ >
+ {{ prompt }}
+ </button>
</div>
- <!-- 鎵撳瓧涓姩鐢� -->
- <div v-if="message.isTyping" class="typing-indicator">
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
+ <button
+ v-if="quickPrompts.length > quickPromptLimit"
+ type="button"
+ class="more-prompts-btn"
+ @click="refreshQuickPrompts"
+ >
+ <el-icon><RefreshRight /></el-icon>
+ <span>鎹竴鎹�</span>
+ </button>
+ </div>
+ </div>
+
+ <div class="message-list" ref="messageListRef">
+ <div
+ v-for="(message, index) in messages"
+ :key="index"
+ :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
+ >
+ <div class="avatar">
+ <el-icon v-if="message.isUser"><User /></el-icon>
+ <el-icon v-else><Cpu /></el-icon>
+ </div>
+ <div class="message-content">
+ <!-- 鏂囨湰鍐呭 -->
+ <div class="text-box" v-html="message.htmlContent"></div>
+
+ <div v-if="message.localUploadFiles?.length" class="message-local-file-list">
+ <div
+ v-for="(file, fileIndex) in message.localUploadFiles"
+ :key="`${file.previewId || file.name}-${fileIndex}`"
+ :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
+ @click="handleMessageFileClick(file)"
+ >
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(message.localUploadFiles)"
+ :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="message-local-file-thumb"
+ />
+ <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
+ <div class="message-local-file-meta">
+ <span
+ :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
+ :title="file.name"
+ @click.stop="openMessageAttachment(file)"
+ >
+ {{ file.name }}
+ </span>
+ <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍥捐〃鍐呭 -->
+ <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
+ <div
+ v-for="(option, key) in message.chartOptions"
+ :key="key"
+ class="chart-item"
+ :id="`ai-chart-${index}-${key}`"
+ ></div>
+ </div>
+
+ <!-- 琛ㄦ牸鍐呭 -->
+ <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
+ <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.tableData.columns"
+ :key="col"
+ :prop="col"
+ :label="columnLabelMap[col] || col"
+ min-width="100"
+ show-overflow-tooltip
+ />
+ </el-table>
+ </div>
+
+ <!-- 鎵撳瓧涓姩鐢� -->
+ <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
+ <div class="purchase-confirm-header">
+ <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '閲囪喘涓氬姟' }}</span>
+ <el-tag size="small" type="success" v-if="message.purchaseAnalysisData.confidence !== undefined">
+ 缃俊搴� {{ formatPercent(message.purchaseAnalysisData.confidence) }}
+ </el-tag>
+ </div>
+ <div class="purchase-confirm-desc">
+ {{ getPurchaseConfirmDescription(message.purchaseAnalysisData) }}
+ </div>
+ <div v-if="isPurchasePayloadEmpty(message.purchaseAnalysisData.payload)" class="purchase-empty-state">
+ <div class="empty-title">娌℃湁璇嗗埆鍒板彲鐩存帴鎻愪氦鐨勯噰璐彴璐︿俊鎭�</div>
+ <div class="empty-desc">褰撳墠鏂囦欢閲岀己灏戦噰璐悎鍚屽彿銆佷緵搴斿晢銆侀」鐩�佹棩鏈熴�佺墿鏂欐槑缁嗙瓑鍏抽敭鍐呭銆傝涓婁紶鏇村畬鏁寸殑鍚堝悓銆佽鍗曟垨鏄庣粏琛紝鎴栧湪涓嬫柟琛ュ厖鏁版嵁鍚庡啀纭銆�</div>
+ </div>
+ <div v-if="message.purchaseAnalysisData.warnings?.length" class="purchase-alert warning">
+ <strong>椋庨櫓鎻愮ず</strong>
+ <ul>
+ <li v-for="(warning, warningIndex) in message.purchaseAnalysisData.warnings" :key="warningIndex">
+ {{ formatPreviewItem(warning) }}
+ </li>
+ </ul>
+ </div>
+ <div v-if="getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length" class="purchase-alert missing">
+ <strong>闇�瑕佽ˉ鍏� {{ getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length }} 椤�</strong>
+ <el-tag
+ v-for="field in getVisiblePurchaseMissingFields(message.purchaseAnalysisData)"
+ :key="field"
+ size="small"
+ type="danger"
+ >
+ {{ field }}
+ </el-tag>
+ </div>
+ <div v-if="message.purchaseAnalysisData.preview?.length" class="purchase-preview">
+ <div class="purchase-section-title">纭鎽樿</div>
+ <ul>
+ <li v-for="(item, previewIndex) in message.purchaseAnalysisData.preview" :key="previewIndex">
+ {{ formatPreviewItem(item) }}
+ </li>
+ </ul>
+ </div>
+ <div class="purchase-section-title">琛ュ厖鎴栫‘璁ゆ暟鎹�</div>
+ <div class="payload-toolbar">
+ <el-button
+ size="small"
+ plain
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseRootField(message)"
+ >
+ <el-icon><Plus /></el-icon>
+ 鏂板椤跺眰瀛楁
+ </el-button>
+ </div>
+ <div class="payload-tree-table-wrapper">
+ <el-table
+ :data="message.payloadTreeData || []"
+ row-key="id"
+ border
+ stripe
+ size="small"
+ default-expand-all
+ :tree-props="{ children: 'children' }"
+ empty-text="鏆傛棤寰呯‘璁ゆ暟鎹�"
+ >
+ <el-table-column label="瀛楁" min-width="240">
+ <template #default="{ row }">
+ <div class="payload-key-cell">
+ <template v-if="row.parentType === 'object'">
+ <el-input
+ v-if="row.keyEditable"
+ v-model="row.key"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ placeholder="瀛楁鍚�"
+ />
+ <div v-else class="payload-fixed-key" :title="row.key">
+ <span>{{ getPurchaseFieldLabel(row.key) }}</span>
+ <small v-if="getPurchaseFieldLabel(row.key) !== row.key">{{ row.key }}</small>
+ </div>
+ </template>
+ <span v-else class="payload-array-index">{{ getPurchaseArrayItemLabel(row, message) }}</span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="绫诲瀷" width="130" align="center">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.valueType"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ @change="handlePurchaseNodeTypeChange(message, row)"
+ >
+ <el-option
+ v-for="option in purchaseValueTypeOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍊�" min-width="250">
+ <template #default="{ row }">
+ <div v-if="row.valueType === 'object'" class="payload-container-cell">
+ 瀵硅薄锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <div v-else-if="row.valueType === 'array'" class="payload-container-cell">
+ 鏁扮粍锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <el-switch
+ v-else-if="row.valueType === 'boolean'"
+ v-model="row.value"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ />
+ <span v-else-if="row.valueType === 'null'" class="payload-null-value">null</span>
+ <el-input
+ v-else
+ v-model="row.value"
+ size="small"
+ :placeholder="row.valueType === 'number' ? '璇疯緭鍏ユ暟瀛�' : '璇疯緭鍏ュ唴瀹�'"
+ :disabled="message.confirming || message.confirmed"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center">
+ <template #default="{ row }">
+ <div class="payload-row-actions">
+ <el-tooltip v-if="row.valueType === 'object'" content="鏂板瀛楁" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-else-if="row.valueType === 'array'" content="鏂板鏁扮粍椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-if="row.parentType === 'array'" content="鏂板鍚岀骇椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseSiblingNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎褰撳墠椤�" placement="top">
+ <el-button
+ :icon="Delete"
+ circle
+ size="small"
+ text
+ type="danger"
+ :disabled="message.confirming || message.confirmed"
+ @click="removePurchaseNode(message, row)"
+ />
+ </el-tooltip>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div class="payload-editor-tip">
+ 鏃ユ湡璇峰~鍐� yyyy-MM-dd锛屼緥濡� 2026-04-30銆備骇鍝佹槑缁嗗缓璁斁鍦ㄦ瘡鏉¢噰璐彴璐︾殑 productData 涓紝纭鏃朵細鑷姩鍏煎鏃ф牸寮忓苟娓呯悊瀹℃壒瀛楁銆�
+ </div>
+ <div class="purchase-confirm-actions">
+ <span v-if="message.confirmResult" :class="['confirm-result', message.confirmed ? 'success' : 'error']">
+ {{ message.confirmResult }}
+ </span>
+ <el-button
+ type="primary"
+ size="small"
+ :loading="message.confirming"
+ :disabled="message.confirmed || isSending"
+ @click="confirmPurchaseAnalysisFromTable(message)"
+ >
+ 纭骞舵墽琛�
+ </el-button>
+ </div>
+ </div>
+
+ <div v-if="message.isTyping" class="typing-indicator">
+ <span class="dot"></span>
+ <span class="dot"></span>
+ <span class="dot"></span>
+ </div>
</div>
</div>
</div>
- </div>
- <div class="input-area">
- <div class="input-actions">
- <el-button link type="primary" size="small" @click="newChat">
- <el-icon><Plus /></el-icon>鏂颁細璇�
- </el-button>
- <el-button v-if="isSending" link type="danger" size="small" @click="stopGeneration">
- <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
- </el-button>
- <el-upload
- class="file-upload-trigger"
- action="#"
- :auto-upload="false"
- :show-file-list="false"
- :on-change="handleFileChange"
- :disabled="isSending"
- >
- <el-button link type="primary" size="small" :disabled="isSending">
- <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ <div class="input-area">
+ <div class="input-actions">
+ <el-button link class="utility-action-btn" type="primary" size="small" @click="handleNewChat">
+ <el-icon><Plus /></el-icon>鏂颁細璇�
</el-button>
- </el-upload>
- </div>
- <div class="input-box">
- <div v-if="selectedFile" class="selected-file-tag">
- <el-icon><Document /></el-icon>
- <span class="file-name">{{ selectedFile.name }}</span>
- <el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
+ <el-button v-if="isSending" link class="utility-action-btn stop-action-btn" type="danger" size="small" @click="stopGeneration">
+ <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
+ </el-button>
+ <el-upload
+ v-if="currentAssistant.allowFileUpload"
+ class="file-upload-trigger"
+ action="#"
+ :auto-upload="false"
+ :show-file-list="false"
+ v-model:file-list="uploadFileList"
+ :multiple="currentAssistant.allowMultipleFileUpload"
+ :on-change="handleFileChange"
+ :disabled="isSending"
+ >
+ <el-button link class="utility-action-btn upload-action-btn" type="primary" size="small" :disabled="isSending">
+ <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ </el-button>
+ </el-upload>
</div>
- <el-input
- v-model="inputMessage"
- type="textarea"
- :rows="selectedFile ? 2 : 3"
- placeholder="璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)"
- resize="none"
- @keydown.enter.exact.prevent="sendMessage"
- />
- <el-button
- type="primary"
- class="send-btn"
- :disabled="isSending || (!inputMessage.trim() && !selectedFile)"
- @click="sendMessage"
- >
- 鍙戦��
- </el-button>
+ <div class="input-box">
+ <div v-if="selectedFiles.length" class="selected-file-list">
+ <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag">
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(selectedFileSnapshots)"
+ :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="selected-file-thumb"
+ />
+ <el-icon v-else><Document /></el-icon>
+ <div class="selected-file-meta">
+ <span class="file-name">{{ file.name }}</span>
+ <small class="file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ <el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
+ </div>
+ </div>
+ <el-input
+ v-model="inputMessage"
+ type="textarea"
+ :rows="selectedFiles.length ? 2 : 3"
+ :placeholder="currentAssistant.placeholder"
+ resize="none"
+ @keydown.enter.exact.prevent="sendMessage"
+ />
+ <el-button
+ type="primary"
+ class="send-btn"
+ :disabled="isSending || (!inputMessage.trim() && !selectedFiles.length)"
+ @click="sendMessage"
+ aria-label="鍙戦��"
+ >
+ <el-icon><Promotion /></el-icon>
+ </el-button>
+ </div>
</div>
- </div>
</div>
</div>
</el-drawer>
@@ -184,31 +522,458 @@
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
import request from '@/utils/request'
import * as echarts from 'echarts'
-import { Cpu, User, Plus, Loading, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close } from '@element-plus/icons-vue'
+import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
+import { builtInAssistants, generalAssistant } from './assistants'
+import todoAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+import salesAssistantAvatar from '@/assets/AI/閿�鍞姪鎵�.png'
+import purchaseAssistantAvatar from '@/assets/AI/閲囪喘鍔╂墜.png'
+import productionAssistantAvatar from '@/assets/AI/鐢熶骇鍔╂墜.png'
+import financeAssistantAvatar from '@/assets/AI/璐㈠姟鍔╂墜.png'
+import bossAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+
+const emit = defineEmits(['header-extra-action'])
+
+const props = defineProps({
+ assistants: {
+ type: Array,
+ default: () => []
+ },
+ defaultAssistant: {
+ type: String,
+ default: ''
+ },
+ hideTrigger: {
+ type: Boolean,
+ default: false
+ },
+ autoOpen: {
+ type: Boolean,
+ default: false
+ },
+ drawerSize: {
+ type: [String, Number],
+ default: ''
+ },
+ drawerDirection: {
+ type: String,
+ default: 'rtl'
+ },
+ headerExtraActionText: {
+ type: String,
+ default: ''
+ }
+})
+
+const hideTrigger = computed(() => props.hideTrigger)
+const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim())
+const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl')
+ ? props.drawerDirection
+ : 'rtl')
+const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
+const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
+const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
+const assistantAvatarByKey = {
+ general: todoAssistantAvatar,
+ todo: todoAssistantAvatar,
+ purchase: purchaseAssistantAvatar,
+ sales: salesAssistantAvatar,
+ production: productionAssistantAvatar,
+ finance: financeAssistantAvatar,
+ boss: bossAssistantAvatar
+}
+const currentAssistantAvatar = computed(() => {
+ const assistant = currentAssistant.value || {}
+ return assistant.avatar || assistantAvatarByKey[assistant.key] || ''
+})
+const showAssistantSwitch = computed(() => assistants.value.length > 1)
+const quickPromptLimit = 3
+const quickPromptStart = ref(0)
+const quickPrompts = computed(() => {
+ const assistant = currentAssistant.value || {}
+ if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
+ return assistant.quickPrompts
+ }
+ return generalAssistant.quickPrompts || []
+})
+const displayedQuickPrompts = computed(() => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return prompts
+
+ const result = []
+ for (let i = 0; i < quickPromptLimit; i++) {
+ result.push(prompts[(quickPromptStart.value + i) % prompts.length])
+ }
+ return result
+})
+const hasMessages = computed(() => messages.value.length > 0)
const visible = ref(false)
const windowWidth = ref(window.innerWidth)
-const drawerSize = computed(() => {
+const responsiveDrawerSize = computed(() => {
if (windowWidth.value < 768) return '100%'
- if (windowWidth.value < 1200) return '500px'
- return '600px'
+ if (windowWidth.value < 1200) return '50%'
+ return '50%'
})
+const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value)
const messageListRef = ref(null)
const isSending = ref(false)
const currentAbortController = ref(null)
const inputMessage = ref('')
-const selectedFile = ref(null)
+const selectedFiles = ref([])
+const uploadFileList = ref([])
+const selectedFileSnapshots = ref([])
const messages = ref([])
const uuid = ref('')
const chartInstances = ref({})
const resizeHandlers = ref([])
const outputState = ref({})
+const businessTypeLabelMap = {
+ purchase_ledger: '閲囪喘鍙拌处',
+ payment_registration: '浠樻鐧昏',
+ purchase_return_order: '閲囪喘閫�璐у崟',
+ unknown: '鏈煡閲囪喘涓氬姟'
+}
+const purchasePayloadFieldLabelMap = {
+ purchaseLedgers: '閲囪喘鍙拌处',
+ productData: '浜у搧鏄庣粏',
+ purchaseContractNumber: '閲囪喘鍚堝悓鍙�',
+ purchaseContractNo: '閲囪喘鍚堝悓鍙�',
+ purchaseOrderNumber: '閲囪喘鍚堝悓鍙�',
+ salesContractNo: '閿�鍞悎鍚屽彿',
+ salesContractNumber: '閿�鍞悎鍚屽彿',
+ salesOrderNumber: '閿�鍞悎鍚屽彿',
+ salesContractNoId: '閿�鍞悎鍚孖D',
+ approveUserIds: '瀹℃壒鐢ㄦ埛ID鍒楄〃',
+ entryDateStart: '褰曞叆寮�濮嬫棩鏈�',
+ entryDateEnd: '褰曞叆缁撴潫鏃ユ湡',
+ id: 'ID',
+ supplierId: '渚涘簲鍟咺D',
+ projectName: '椤圭洰鍚嶇О',
+ supplierName: '渚涘簲鍟嗗悕绉�',
+ isWhite: '鏄惁鐧藉悕鍗�',
+ recorderId: '褰曞叆浜篒D',
+ recorderName: '褰曞叆浜�',
+ contractDate: '鎵ц鏃ユ湡',
+ executionDate: '鎵ц鏃ユ湡',
+ inputPerson: '褰曞叆浜�',
+ inputDate: '褰曞叆鏃ユ湡',
+ entryDate: '褰曞叆鏃ユ湡',
+ paymentMethod: '浠樻鏂瑰紡',
+ auditors: '瀹℃壒浜�',
+ approverId: '瀹℃壒浜篒D',
+ approvalStatus: '瀹℃壒鐘舵��',
+ remark: '澶囨敞',
+ remarks: '澶囨敞',
+ attachmentMaterials: '闄勪欢鏉愭枡',
+ createdAt: '鍒涘缓鏃堕棿',
+ updatedAt: '鏇存柊鏃堕棿',
+ salesLedgerId: '閿�鍞彴璐D',
+ hasChildren: '鏄惁鏈夊瓙椤�',
+ Type: '绫诲瀷',
+ type: '绫诲瀷',
+ tempFileIds: '涓存椂鏂囦欢ID',
+ SalesLedgerFiles: '閿�鍞彴璐﹂檮浠�',
+ phoneNumber: '鑱旂郴鐢佃瘽',
+ businessPersonId: '涓氬姟鍛業D',
+ productId: '浜у搧ID',
+ productModelId: '浜у搧鍨嬪彿ID',
+ invoiceNumber: '鍙戠エ鍙风爜',
+ invoiceAmount: '鍙戠エ閲戦',
+ ticketRegistrationId: '寮�绁ㄧ櫥璁癐D',
+ contractAmount: '鍚堝悓閲戦',
+ receiptPaymentAmount: '宸叉敹浠樻閲戦',
+ unReceiptPaymentAmount: '鏈敹浠樻閲戦',
+ templateName: '妯℃澘鍚嶇О',
+ productCategory: '浜у搧绫诲埆',
+ specificationModel: '瑙勬牸鍨嬪彿',
+ unit: '鍗曚綅',
+ taxRate: '绋庣巼',
+ taxInclusiveUnitPrice: '鍚◣鍗曚环',
+ priceWithTax: '鍚◣鍗曚环',
+ quantity: '鏁伴噺',
+ taxInclusiveTotalPrice: '鍚◣鎬讳环',
+ totalPriceWithTax: '鍚◣鎬讳环',
+ invoiceType: '鍙戠エ绫诲瀷',
+ inventoryWarningQuantity: '搴撳瓨棰勮鏁伴噺',
+ isInspected: '鏄惁璐ㄦ',
+ isChecked: '鏄惁璐ㄦ'
+}
+const purchasePayloadFieldKeyMap = {
+ 閲囪喘鍙拌处: 'purchaseLedgers',
+ 浜у搧鏄庣粏: 'productData',
+ 閲囪喘鍚堝悓鍙�: 'purchaseContractNumber',
+ 閲囪喘鍗曞彿: 'purchaseContractNumber',
+ 閲囪喘璁㈠崟鍙�: 'purchaseContractNumber',
+ 閿�鍞悎鍚屽彿: 'salesContractNo',
+ 閿�鍞崟鍙�: 'salesContractNo',
+ 閿�鍞鍗曞彿: 'salesContractNo',
+ 閿�鍞悎鍚孖D: 'salesContractNoId',
+ 瀹℃壒鐢ㄦ埛ID鍒楄〃: 'approveUserIds',
+ 褰曞叆寮�濮嬫棩鏈�: 'entryDateStart',
+ 褰曞叆缁撴潫鏃ユ湡: 'entryDateEnd',
+ ID: 'id',
+ 椤圭洰鍚嶇О: 'projectName',
+ 渚涘簲鍟咺D: 'supplierId',
+ 渚涘簲鍟嗗悕绉�: 'supplierName',
+ 鏄惁鐧藉悕鍗�: 'isWhite',
+ 褰曞叆浜篒D: 'recorderId',
+ 褰曞叆浜�: 'recorderName',
+ 绛捐鏃ユ湡: 'executionDate',
+ 鎵ц鏃ユ湡: 'executionDate',
+ 褰曞叆鏃ユ湡: 'entryDate',
+ 浠樻鏂瑰紡: 'paymentMethod',
+ 瀹℃牳浜�: 'approverId',
+ 瀹℃壒浜�: 'approverId',
+ 瀹℃壒浜篒D: 'approverId',
+ 瀹℃壒鐘舵��: 'approvalStatus',
+ 澶囨敞: 'remarks',
+ 闄勪欢鏉愭枡: 'attachmentMaterials',
+ 鍒涘缓鏃堕棿: 'createdAt',
+ 鏇存柊鏃堕棿: 'updatedAt',
+ 閿�鍞彴璐D: 'salesLedgerId',
+ 鏄惁鏈夊瓙椤�: 'hasChildren',
+ 绫诲瀷: 'type',
+ 涓存椂鏂囦欢ID: 'tempFileIds',
+ 閿�鍞彴璐﹂檮浠�: 'SalesLedgerFiles',
+ 鑱旂郴鐢佃瘽: 'phoneNumber',
+ 涓氬姟鍛業D: 'businessPersonId',
+ 浜у搧ID: 'productId',
+ 浜у搧鍨嬪彿ID: 'productModelId',
+ 鍙戠エ鍙风爜: 'invoiceNumber',
+ 鍙戠エ閲戦: 'invoiceAmount',
+ 寮�绁ㄧ櫥璁癐D: 'ticketRegistrationId',
+ 鍚堝悓閲戦: 'contractAmount',
+ 宸叉敹浠樻閲戦: 'receiptPaymentAmount',
+ 鏈敹浠樻閲戦: 'unReceiptPaymentAmount',
+ 妯℃澘鍚嶇О: 'templateName',
+ 浜у搧绫诲埆: 'productCategory',
+ 浜у搧鍚嶇О: 'productCategory',
+ 瑙勬牸鍨嬪彿: 'specificationModel',
+ 鍗曚綅: 'unit',
+ 绋庣巼: 'taxRate',
+ 鍚◣鍗曚环: 'taxInclusiveUnitPrice',
+ 鏁伴噺: 'quantity',
+ 鍚◣鎬讳环: 'taxInclusiveTotalPrice',
+ 鍙戠エ绫诲瀷: 'invoiceType',
+ 搴撳瓨棰勮鏁伴噺: 'inventoryWarningQuantity',
+ 鏄惁璐ㄦ: 'isInspected',
+ purchaseLedgers: 'purchaseLedgers',
+ productData: 'productData',
+ purchaseContractNumber: 'purchaseContractNumber',
+ purchaseContractNo: 'purchaseContractNumber',
+ purchaseOrderNumber: 'purchaseContractNumber',
+ salesContractNo: 'salesContractNo',
+ salesContractNumber: 'salesContractNo',
+ salesOrderNumber: 'salesContractNo',
+ contractDate: 'executionDate',
+ inputPerson: 'recorderName',
+ inputDate: 'entryDate',
+ auditors: 'approverId',
+ remark: 'remarks',
+ productCategory: 'productCategory',
+ productName: 'productCategory',
+ specificationModel: 'specificationModel',
+ unit: 'unit',
+ taxRate: 'taxRate',
+ priceWithTax: 'taxInclusiveUnitPrice',
+ taxInclusiveUnitPrice: 'taxInclusiveUnitPrice',
+ quantity: 'quantity',
+ totalPriceWithTax: 'taxInclusiveTotalPrice',
+ taxInclusiveTotalPrice: 'taxInclusiveTotalPrice',
+ invoiceType: 'invoiceType',
+ inventoryWarningQuantity: 'inventoryWarningQuantity',
+ isInspected: 'isInspected',
+ isChecked: 'isInspected'
+}
// 鍘嗗彶浼氳瘽鐩稿叧
+const purchaseValueTypeOptions = [
+ { label: '鏂囨湰', value: 'string' },
+ { label: '鏁板瓧', value: 'number' },
+ { label: '甯冨皵', value: 'boolean' },
+ { label: '绌哄��', value: 'null' },
+ { label: '瀵硅薄', value: 'object' },
+ { label: '鏁扮粍', value: 'array' }
+]
+const purchaseContainerValueTypes = new Set(['object', 'array'])
+const purchaseHiddenFieldKeySet = new Set(['templatename', 'approvalstatus', 'phonenumber', 'type'])
+const purchaseHiddenKeyWordList = [
+ 'attachment',
+ 'file',
+ 'invoice',
+ 'ticketregistration',
+ 'receiptpayment',
+ 'payment'
+]
+const purchaseHiddenChineseKeywordList = ['闄勪欢', '寮�绁�', '鏉ョエ', '鍥炴', '浠樻']
+let purchasePayloadTreeNodeSeed = 0
+
+const shouldHidePurchaseField = (fieldKey = '') => {
+ const rawKey = String(fieldKey || '')
+ if (!rawKey) return false
+ const normalizedFieldKey = purchasePayloadFieldKeyMap[rawKey] || rawKey
+ const lowerKey = String(normalizedFieldKey).toLowerCase()
+
+ if (lowerKey.endsWith('id') || lowerKey.endsWith('ids')) return true
+ if (purchaseHiddenFieldKeySet.has(lowerKey)) return true
+ if (purchaseHiddenKeyWordList.some(keyword => lowerKey.includes(keyword))) return true
+ if (purchaseHiddenChineseKeywordList.some(keyword => rawKey.includes(keyword))) return true
+ return false
+}
+
const showHistory = ref(false)
const sessions = ref([])
const loadingSessions = ref(false)
+
+const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
+const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
+
+const getPathnameFromFilePath = (filePath = '') => {
+ const rawPath = String(filePath || '').trim()
+ if (!rawPath) return ''
+ try {
+ const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
+ return new URL(rawPath, baseOrigin).pathname || ''
+ } catch (err) {
+ return rawPath.split('?')[0]
+ }
+}
+
+const isImageFilePath = (filePath = '') => {
+ const pathname = getPathnameFromFilePath(filePath).toLowerCase()
+ return imageFilePathPattern.test(pathname)
+}
+
+const getHistoryFileName = (filePath = '', index = 0) => {
+ const pathname = getPathnameFromFilePath(filePath)
+ const fileName = pathname.split('/').filter(Boolean).pop()
+ if (!fileName) return `file-${index + 1}`
+ try {
+ return decodeURIComponent(fileName)
+ } catch (err) {
+ return fileName
+ }
+}
+
+const getImagePreviewList = (files = []) => {
+ if (!Array.isArray(files)) return []
+ return files
+ .filter(item => item?.isImage && item?.previewUrl)
+ .map(item => item.previewUrl)
+}
+
+const getImagePreviewInitialIndex = (files = [], previewUrl = '') => {
+ const list = getImagePreviewList(files)
+ const index = list.indexOf(previewUrl)
+ return index >= 0 ? index : 0
+}
+
+const formatFileSize = (size) => {
+ const bytes = Number(size)
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB`
+}
+
+const createLocalFileSnapshot = (file, index = 0) => {
+ const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null
+ const fileType = rawFile?.type || ''
+ const isImage = isImageFileType(fileType)
+ const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
+ const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
+ return {
+ previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
+ name: rawFile?.name || `file-${index + 1}`,
+ size: rawFile?.size || 0,
+ type: fileType,
+ isImage,
+ previewUrl,
+ accessUrl: '',
+ rawFile,
+ isObjectUrl: !!previewUrl
+ }
+}
+
+const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
+ const normalizedPath = String(filePath || '').trim()
+ if (!normalizedPath) return null
+ const isImage = isImageFilePath(normalizedPath)
+ return {
+ previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
+ name: getHistoryFileName(normalizedPath, fileIndex),
+ size: 0,
+ type: '',
+ isImage,
+ previewUrl: isImage ? normalizedPath : '',
+ accessUrl: normalizedPath,
+ rawFile: null,
+ isObjectUrl: false
+ }
+}
+
+const revokeLocalFileSnapshots = (snapshots = []) => {
+ const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function'
+ if (!canRevokeObjectURL) return
+ if (!Array.isArray(snapshots)) return
+ snapshots.forEach((snapshot) => {
+ if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
+ URL.revokeObjectURL(snapshot.previewUrl)
+ }
+ })
+}
+
+const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
+ if (!Array.isArray(filePaths)) return []
+ return filePaths
+ .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
+ .filter(Boolean)
+}
+
+const openMessageAttachment = (file) => {
+ const accessUrl = String(file?.accessUrl || '').trim()
+ if (!accessUrl) return
+ if (typeof window === 'undefined' || typeof window.open !== 'function') return
+ window.open(accessUrl, '_blank', 'noopener,noreferrer')
+}
+
+const handleMessageFileClick = (file) => {
+ if (!file?.accessUrl || file?.isImage) return
+ openMessageAttachment(file)
+}
+
+const revokeMessageLocalFileSnapshots = (messageList = []) => {
+ if (!Array.isArray(messageList)) return
+ messageList.forEach((msg) => {
+ if (Array.isArray(msg?.localUploadFiles)) {
+ revokeLocalFileSnapshots(msg.localUploadFiles)
+ msg.localUploadFiles = []
+ }
+ })
+}
+
+const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => {
+ if (releaseSnapshots) {
+ revokeLocalFileSnapshots(selectedFileSnapshots.value)
+ }
+ selectedFiles.value = []
+ uploadFileList.value = []
+ selectedFileSnapshots.value = []
+}
+
+const abortCurrentRequest = () => {
+ if (!currentAbortController.value) return
+
+ currentAbortController.value.abort()
+ currentAbortController.value = null
+ isSending.value = false
+
+ const lastMsg = messages.value[messages.value.length - 1]
+ if (lastMsg && !lastMsg.isUser) {
+ lastMsg.isTyping = false
+ }
+}
const toggleHistory = () => {
showHistory.value = !showHistory.value
@@ -217,10 +982,17 @@
}
}
+const handleToggleHistory = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ toggleHistory()
+}
+
const loadSessions = async () => {
loadingSessions.value = true
try {
- const res = await request.get('/xiaozhi/history/sessions')
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`)
if (res.code === 200) {
sessions.value = res.data || []
}
@@ -233,35 +1005,40 @@
const selectSession = async (session) => {
showHistory.value = false
+ clearSelectedFiles()
uuid.value = session.memoryId
- localStorage.setItem('ai_chat_uuid', uuid.value)
-
+ localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
+
// 鍔犺浇浼氳瘽娑堟伅
try {
- const res = await request.get(`/xiaozhi/history/messages/${uuid.value}`)
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
if (res.code === 200) {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
const historyMsgs = res.data || []
-
+
// 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
historyMsgs.forEach((msg, idx) => {
const isUser = msg.role === 'user'
const botMsgIndex = messages.value.length
-
+
const messageObj = {
isUser,
- content: msg.content,
+ content: msg.content || '',
htmlContent: '',
isTyping: false,
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null,
+ localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
}
-
+
messages.value.push(messageObj)
-
+
if (!isUser) {
outputState.value[botMsgIndex] = {
isPaused: false,
@@ -270,31 +1047,17 @@
blockEndPos: -1,
hasRenderedChart: false
}
-
+
// 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = msg.content.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- messageObj.type = parsedData.type || ''
- if (messageObj.type === 'todo_list' && parsedData.data) {
- messageObj.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- messageObj.chartOptions = parsedData.charts
- messageObj.chartRenderReady = true
- renderCharts(botMsgIndex, messageObj.chartOptions)
- }
- }
- } catch (err) {}
+ const extracted = extractEmbeddedSuccessJson(msg.content || '')
+ if (extracted) {
+ applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
}
-
- updateOutputState(msg.content, botMsgIndex)
- messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
+
+ updateOutputState(msg.content || '', botMsgIndex)
+ messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
} else {
- messageObj.htmlContent = convertTextToHtml(msg.content)
+ messageObj.htmlContent = convertTextToHtml(msg.content || '')
}
})
scrollToBottom()
@@ -306,7 +1069,7 @@
const handleDeleteSession = async (memoryId) => {
try {
- const res = await request.delete(`/xiaozhi/history/${memoryId}`)
+ const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`)
if (res.code === 200) {
loadSessions()
if (uuid.value === memoryId) {
@@ -329,21 +1092,54 @@
}
onMounted(() => {
- initUUID()
- // 鍒濆娆㈣繋
- if (messages.value.length === 0) {
- hello()
+ if (props.autoOpen) {
+ visible.value = true
}
+ initUUID()
window.addEventListener('resize', handleWindowResize)
})
onUnmounted(() => {
+ revokeMessageLocalFileSnapshots(messages.value)
+ clearSelectedFiles()
disposeCharts()
window.removeEventListener('resize', handleWindowResize)
})
+watch(selectedAssistantKey, (nextKey, prevKey) => {
+ if (!prevKey || nextKey === prevKey) return
+
+ abortCurrentRequest()
+ revokeMessageLocalFileSnapshots(messages.value)
+ disposeCharts()
+ messages.value = []
+ outputState.value = {}
+ sessions.value = []
+ showHistory.value = false
+ clearSelectedFiles()
+ inputMessage.value = ''
+ quickPromptStart.value = 0
+ initUUID()
+})
+
+watch(() => props.defaultAssistant, (nextKey) => {
+ if (!nextKey || nextKey === selectedAssistantKey.value) return
+ if (!assistants.value.some(item => item.key === nextKey)) return
+ selectedAssistantKey.value = nextKey
+})
+
+watch(() => props.autoOpen, (nextValue) => {
+ if (nextValue) {
+ visible.value = true
+ }
+})
+
const handleWindowResize = () => {
windowWidth.value = window.innerWidth
+}
+
+const handleHeaderExtraAction = () => {
+ emit('header-extra-action')
}
const toggleSidebar = () => {
@@ -354,29 +1150,57 @@
}
const handleClose = () => {
+ if (hideTrigger.value) return
visible.value = false
}
+const handleManualClose = () => {
+ if (hideTrigger.value) return
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ handleClose()
+}
+
const initUUID = () => {
- let storedUUID = localStorage.getItem('ai_chat_uuid')
+ let storedUUID = localStorage.getItem(currentAssistant.value.storageKey)
if (!storedUUID) {
storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4)
- localStorage.setItem('ai_chat_uuid', storedUUID)
+ localStorage.setItem(currentAssistant.value.storageKey, storedUUID)
}
uuid.value = storedUUID
}
-const hello = () => {
- sendRequest('浣犲ソ')
-}
-
const newChat = () => {
+ revokeMessageLocalFileSnapshots(messages.value)
disposeCharts()
messages.value = []
outputState.value = {}
- localStorage.removeItem('ai_chat_uuid')
+ sessions.value = []
+ showHistory.value = false
+ clearSelectedFiles()
+ quickPromptStart.value = 0
+ localStorage.removeItem(currentAssistant.value.storageKey)
initUUID()
- hello()
+}
+
+const handleNewChat = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ newChat()
+}
+
+const sendQuickPrompt = (prompt) => {
+ if (!prompt || isSending.value) return
+ inputMessage.value = prompt
+ sendMessage()
+}
+
+const refreshQuickPrompts = () => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return
+ quickPromptStart.value = (quickPromptStart.value + quickPromptLimit) % prompts.length
}
const disposeCharts = () => {
@@ -384,6 +1208,1179 @@
resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler))
chartInstances.value = {}
resizeHandlers.value = []
+}
+
+const extractEmbeddedSuccessJson = (text) => {
+ if (!text || typeof text !== 'string') return null
+
+ const startMatch = text.match(/\{\s*"success"\s*:/)
+ if (!startMatch) return null
+ const startIdx = startMatch.index ?? -1
+ if (startIdx < 0) return null
+
+ for (let i = startIdx; i < text.length; i++) {
+ if (text[i] !== '{') continue
+
+ let depth = 0
+ let inString = false
+ let escaped = false
+
+ for (let j = i; j < text.length; j++) {
+ const char = text[j]
+
+ if (inString) {
+ if (escaped) {
+ escaped = false
+ } else if (char === '\\') {
+ escaped = true
+ } else if (char === '"') {
+ inString = false
+ }
+ continue
+ }
+
+ if (char === '"') {
+ inString = true
+ continue
+ }
+
+ if (char === '{') {
+ depth++
+ } else if (char === '}') {
+ depth--
+ if (depth === 0) {
+ const candidate = text.slice(i, j + 1)
+ try {
+ const parsed = JSON.parse(candidate)
+ if (parsed?.success === true) {
+ return {
+ data: parsed,
+ startIdx: i,
+ endIdx: j + 1
+ }
+ }
+ } catch (err) {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
+ if (!messageObj || !parsedData?.success) return
+
+ messageObj.type = parsedData.type || ''
+
+ if (messageObj.type === 'todo_list' && parsedData.data) {
+ messageObj.tableData = parsedData.data
+ }
+
+ if (parsedData.action === 'confirm_required' && parsedData.businessType) {
+ messageObj.type = 'purchase_analysis_confirm'
+ messageObj.purchaseAnalysisData = parsedData
+ if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
+ initializePurchasePayloadTree(messageObj, parsedData.payload || {})
+ }
+ if (!messageObj.payloadText) {
+ const payloadFromTree = buildPurchasePayloadFromNodes(messageObj.payloadTreeData, 'object')
+ const payloadWithHidden = mergePurchasePayloadWithHidden(payloadFromTree, messageObj.payloadHiddenData)
+ messageObj.payloadText = JSON.stringify(localizePurchasePayload(payloadWithHidden), null, 2)
+ }
+ messageObj.confirmResult = ''
+ messageObj.confirmed = false
+ messageObj.confirming = false
+ }
+
+ const chartOptions = getStructuredChartOptions(parsedData)
+ if (chartOptions && Object.keys(chartOptions).length > 0) {
+ messageObj.chartOptions = chartOptions
+ messageObj.chartRenderReady = true
+
+ if (shouldRenderCharts) {
+ renderCharts(msgIndex, messageObj.chartOptions)
+ if (outputState.value[msgIndex]) {
+ outputState.value[msgIndex].hasRenderedChart = true
+ }
+ }
+ }
+}
+
+const getStructuredChartOptions = (parsedData) => {
+ if (!parsedData?.success) return null
+
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+ return parsedData.charts
+ }
+
+ if (parsedData.type === 'purchase_material_rank') {
+ return buildPurchaseMaterialRankCharts(parsedData)
+ }
+
+ return null
+}
+
+const buildPurchaseMaterialRankCharts = (parsedData) => {
+ const items = Array.isArray(parsedData?.data?.items) ? parsedData.data.items : []
+ if (!items.length) return null
+
+ const names = items.map(item => item.productCategory || '-')
+ const amounts = items.map(item => Number(item.amount) || 0)
+
+ return {
+ purchaseMaterialAmountRank: {
+ title: {
+ text: '\u91c7\u8d2d\u7269\u6599\u91d1\u989d\u6392\u884c',
+ left: 'center',
+ textStyle: {
+ fontSize: 14,
+ fontWeight: 600,
+ color: '#1a1a2e'
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ },
+ formatter(params) {
+ const dataIndex = params?.[0]?.dataIndex ?? 0
+ const item = items[dataIndex] || {}
+ const amount = Number(item.amount) || 0
+ const quantity = Number(item.quantity) || 0
+ return [
+ `${item.productCategory || '-'}`,
+ `${params?.[0]?.marker || ''} \u91d1\u989d\uff1a${formatCurrency(amount)}`,
+ `\u89c4\u683c\u578b\u53f7\uff1a${item.specificationModel || '-'}`,
+ `\u6570\u91cf\uff1a${quantity}${item.unit || ''}`
+ ].join('<br/>')
+ }
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: names.some(name => String(name).length > 6) ? 72 : 48,
+ top: 48,
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: names,
+ axisLabel: {
+ interval: 0,
+ rotate: names.some(name => String(name).length > 6) ? 28 : 0,
+ color: '#4b5563'
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '\u91d1\u989d(\u5143)',
+ axisLabel: {
+ color: '#4b5563',
+ formatter: value => formatCompactNumber(value)
+ }
+ },
+ series: [{
+ name: '\u91c7\u8d2d\u91d1\u989d',
+ type: 'bar',
+ barMaxWidth: 36,
+ data: amounts,
+ itemStyle: {
+ borderRadius: [6, 6, 0, 0],
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#2f7cf6' },
+ { offset: 1, color: '#55d7ff' }
+ ])
+ },
+ label: {
+ show: true,
+ position: 'top',
+ color: '#1f2937',
+ formatter: params => formatCompactNumber(params.value)
+ }
+ }]
+ }
+ }
+}
+
+const formatCurrency = (value) => {
+ const amount = Number(value) || 0
+ return `\u00a5${amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`
+}
+
+const formatCompactNumber = (value) => {
+ const amount = Number(value) || 0
+ if (Math.abs(amount) >= 10000) {
+ return `${(amount / 10000).toFixed(2).replace(/\.?0+$/, '')}\u4e07`
+ }
+ return amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
+}
+
+const formatPercent = (value) => {
+ const number = Number(value)
+ if (Number.isNaN(number)) return '-'
+ return `${Math.round(number * 100)}%`
+}
+
+const formatPreviewItem = (item) => {
+ if (item === null || item === undefined) return '-'
+ if (typeof item === 'string') return item
+ try {
+ return JSON.stringify(item)
+ } catch (err) {
+ return String(item)
+ }
+}
+
+const hasMeaningfulPayloadValue = (value) => {
+ if (value === null || value === undefined) return false
+ if (typeof value === 'string') return value.trim() !== ''
+ if (Array.isArray(value)) return value.some(item => hasMeaningfulPayloadValue(item))
+ if (typeof value === 'object') return Object.values(value).some(item => hasMeaningfulPayloadValue(item))
+ return true
+}
+
+const mergeMappedPayloadValue = (existingValue, incomingValue) => {
+ if (existingValue === undefined) return incomingValue
+
+ const existingHasValue = hasMeaningfulPayloadValue(existingValue)
+ const incomingHasValue = hasMeaningfulPayloadValue(incomingValue)
+
+ if (existingHasValue && !incomingHasValue) return existingValue
+ if (!existingHasValue && incomingHasValue) return incomingValue
+
+ if (
+ existingValue &&
+ incomingValue &&
+ typeof existingValue === 'object' &&
+ typeof incomingValue === 'object' &&
+ !Array.isArray(existingValue) &&
+ !Array.isArray(incomingValue)
+ ) {
+ return { ...existingValue, ...incomingValue }
+ }
+
+ return incomingValue
+}
+
+const mapPayloadKeys = (value, keyMap) => {
+ if (Array.isArray(value)) {
+ return value.map(item => mapPayloadKeys(item, keyMap))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ const mappedKey = keyMap[key] || key
+ const mappedValue = mapPayloadKeys(item, keyMap)
+ result[mappedKey] = mergeMappedPayloadValue(result[mappedKey], mappedValue)
+ return result
+ }, {})
+ }
+ return value
+}
+
+const clonePurchasePayloadValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => clonePurchasePayloadValue(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = clonePurchasePayloadValue(item)
+ return result
+ }, {})
+ }
+ return value
+}
+
+const splitPurchasePayloadByVisibility = (value) => {
+ if (Array.isArray(value)) {
+ const splitItems = value.map(item => splitPurchasePayloadByVisibility(item))
+ const visible = splitItems.map(item => item.visible)
+ const hidden = splitItems.map(item => item.hidden)
+ return { visible, hidden }
+ }
+
+ if (value && typeof value === 'object') {
+ const visible = {}
+ const hidden = {}
+
+ Object.entries(value).forEach(([key, item]) => {
+ if (shouldHidePurchaseField(key)) {
+ hidden[key] = clonePurchasePayloadValue(item)
+ return
+ }
+ const child = splitPurchasePayloadByVisibility(item)
+ visible[key] = child.visible
+ if (hasMeaningfulPayloadValue(child.hidden)) {
+ hidden[key] = child.hidden
+ }
+ })
+
+ return { visible, hidden }
+ }
+
+ return { visible: value, hidden: undefined }
+}
+
+const mergePurchasePayloadWithHidden = (visibleValue, hiddenValue) => {
+ if (hiddenValue === undefined || hiddenValue === null) return visibleValue
+ if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
+
+ if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
+ return visibleValue.map((item, index) => mergePurchasePayloadWithHidden(item, hiddenValue[index]))
+ }
+
+ if (
+ visibleValue &&
+ hiddenValue &&
+ typeof visibleValue === 'object' &&
+ typeof hiddenValue === 'object' &&
+ !Array.isArray(visibleValue) &&
+ !Array.isArray(hiddenValue)
+ ) {
+ const merged = { ...clonePurchasePayloadValue(hiddenValue) }
+ Object.entries(visibleValue).forEach(([key, item]) => {
+ merged[key] = mergePurchasePayloadWithHidden(item, merged[key])
+ })
+ return merged
+ }
+
+ return visibleValue
+}
+
+const localizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldLabelMap)
+
+const normalizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldKeyMap)
+
+const createPurchasePayloadNodeId = () => `purchase-node-${Date.now()}-${purchasePayloadTreeNodeSeed++}`
+
+const detectPurchaseValueType = (value) => {
+ if (Array.isArray(value)) return 'array'
+ if (value === null) return 'null'
+ const valueType = typeof value
+ if (valueType === 'number') return 'number'
+ if (valueType === 'boolean') return 'boolean'
+ if (valueType === 'object') return 'object'
+ return 'string'
+}
+
+const normalizePurchaseNodeValueForEdit = (value, valueType) => {
+ if (valueType === 'number') return value === null || value === undefined ? '' : String(value)
+ if (valueType === 'boolean') return Boolean(value)
+ if (valueType === 'null') return ''
+ return value === null || value === undefined ? '' : String(value)
+}
+
+const createPurchaseTreeNode = ({
+ key = '',
+ parentType = 'object',
+ keyEditable = false,
+ valueType = 'string',
+ value = '',
+ children = []
+} = {}) => ({
+ id: createPurchasePayloadNodeId(),
+ key,
+ parentType,
+ keyEditable,
+ valueType,
+ value,
+ children
+})
+
+const reorderPurchaseObjectEntries = (value) => {
+ const entries = Object.entries(value || {})
+ const productDataIndex = entries.findIndex(([key]) => key === 'productData')
+ if (productDataIndex <= -1 || productDataIndex === entries.length - 1) {
+ return entries
+ }
+ const [productDataEntry] = entries.splice(productDataIndex, 1)
+ entries.push(productDataEntry)
+ return entries
+}
+
+const buildPurchasePayloadTreeNodes = (value, parentType = 'object') => {
+ if (Array.isArray(value)) {
+ return value.map(item => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key: '',
+ parentType: 'array',
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ if (value && typeof value === 'object') {
+ return reorderPurchaseObjectEntries(value).map(([key, item]) => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key,
+ parentType,
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ return []
+}
+
+const initializePurchasePayloadTree = (messageObj, payload = {}) => {
+ const sourcePayload = payload && typeof payload === 'object' && !Array.isArray(payload)
+ ? payload
+ : {}
+ const { visible, hidden } = splitPurchasePayloadByVisibility(sourcePayload)
+ const visiblePayload = visible && typeof visible === 'object' && !Array.isArray(visible) ? visible : {}
+ messageObj.payloadTreeData = buildPurchasePayloadTreeNodes(visiblePayload, 'object')
+ messageObj.payloadHiddenData = hidden && typeof hidden === 'object' ? hidden : {}
+}
+
+const getPurchaseFieldLabel = (fieldKey) => purchasePayloadFieldLabelMap[fieldKey] || fieldKey || '瀛楁'
+
+const createPurchaseDefaultNode = (parentType = 'object') => createPurchaseTreeNode({
+ key: parentType === 'object' ? 'newField' : '',
+ parentType,
+ keyEditable: parentType === 'object',
+ valueType: 'string',
+ value: ''
+})
+
+const getPurchaseScalarNodeValue = (node) => {
+ if (node.valueType === 'null') return null
+ if (node.valueType === 'boolean') return Boolean(node.value)
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (!text) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : text
+ }
+ return node.value === null || node.value === undefined ? '' : String(node.value)
+}
+
+const buildPurchasePayloadFromNodes = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) {
+ return parentType === 'array' ? [] : {}
+ }
+
+ if (parentType === 'array') {
+ return nodes.map(node => {
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return buildPurchasePayloadFromNodes(node.children, node.valueType)
+ }
+ return getPurchaseScalarNodeValue(node)
+ })
+ }
+
+ return nodes.reduce((result, node, index) => {
+ const rawKey = String(node.key ?? '').trim()
+ const key = rawKey || `field_${index + 1}`
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ result[key] = buildPurchasePayloadFromNodes(node.children, node.valueType)
+ } else {
+ result[key] = getPurchaseScalarNodeValue(node)
+ }
+ return result
+ }, {})
+}
+
+const findPurchaseNodeLocation = (nodes, targetId, parentNode = null) => {
+ if (!Array.isArray(nodes)) return null
+ for (let index = 0; index < nodes.length; index++) {
+ const node = nodes[index]
+ if (node.id === targetId) {
+ return {
+ siblings: nodes,
+ index,
+ node,
+ parentNode
+ }
+ }
+ const next = findPurchaseNodeLocation(node.children, targetId, node)
+ if (next) return next
+ }
+ return null
+}
+
+const getPurchaseArrayItemLabel = (row, message) => {
+ const location = findPurchaseNodeLocation(message?.payloadTreeData, row.id)
+ return `[${(location?.index ?? 0) + 1}]`
+}
+
+const handlePurchaseNodeTypeChange = (message, row) => {
+ if (!message || !row) return
+ if (purchaseContainerValueTypes.has(row.valueType)) {
+ row.children = []
+ row.value = ''
+ return
+ }
+ row.children = []
+ if (row.valueType === 'boolean') {
+ row.value = false
+ } else if (row.valueType === 'null') {
+ row.value = ''
+ } else {
+ row.value = ''
+ }
+}
+
+const addPurchaseRootField = (message) => {
+ if (!message) return
+ if (!Array.isArray(message.payloadTreeData)) {
+ message.payloadTreeData = []
+ }
+ message.payloadTreeData.push(createPurchaseDefaultNode('object'))
+}
+
+const addPurchaseChildNode = (message, row) => {
+ if (!message || !row || !purchaseContainerValueTypes.has(row.valueType)) return
+ if (!Array.isArray(row.children)) {
+ row.children = []
+ }
+ row.children.push(createPurchaseDefaultNode(row.valueType))
+}
+
+const addPurchaseSiblingNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location || location.node.parentType !== 'array') return
+ location.siblings.splice(location.index + 1, 0, createPurchaseDefaultNode('array'))
+}
+
+const removePurchaseNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location) return
+ location.siblings.splice(location.index, 1)
+}
+
+const hasPurchaseNodeValidationError = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) return false
+ return nodes.some((node) => {
+ if (parentType === 'object' && !String(node.key ?? '').trim()) {
+ return true
+ }
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (text && !Number.isFinite(Number(text))) {
+ return true
+ }
+ }
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return hasPurchaseNodeValidationError(node.children, node.valueType)
+ }
+ return false
+ })
+}
+
+const purchaseDateFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'entryDate',
+ 'executionDate',
+ 'contractDate',
+ 'inputDate',
+ 'createdAt',
+ 'updatedAt'
+])
+
+const purchaseLedgerAllowedFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'id',
+ 'purchaseContractNumber',
+ 'supplierId',
+ 'supplierName',
+ 'isWhite',
+ 'recorderId',
+ 'recorderName',
+ 'salesContractNo',
+ 'salesContractNoId',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'salesLedgerId',
+ 'hasChildren',
+ 'Type',
+ 'productData',
+ 'tempFileIds',
+ 'SalesLedgerFiles',
+ 'phoneNumber',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'invoiceNumber',
+ 'invoiceAmount',
+ 'ticketRegistrationId',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'type',
+ 'paymentMethod',
+ 'approvalStatus',
+ 'templateName'
+])
+
+const purchaseApprovalFieldKeys = new Set([
+ 'approveUserIds',
+ 'approverId',
+ 'auditors',
+ '瀹℃牳浜�',
+ '瀹℃壒浜�',
+ '瀹℃壒浜篒D',
+ '瀹℃壒鐢ㄦ埛ID鍒楄〃'
+])
+
+const purchaseIntegerFieldKeys = new Set([
+ 'id',
+ 'supplierId',
+ 'recorderId',
+ 'salesContractNoId',
+ 'salesLedgerId',
+ 'Type',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'ticketRegistrationId',
+ 'type',
+ 'approvalStatus',
+ 'inventoryWarningQuantity'
+])
+
+const purchaseDecimalFieldKeys = new Set([
+ 'invoiceAmount',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'quantity',
+ 'taxRate',
+ 'taxInclusiveUnitPrice',
+ 'taxInclusiveTotalPrice',
+ 'taxExclusiveTotalPrice',
+ 'priceWithTax',
+ 'totalPriceWithTax'
+])
+
+const purchaseBooleanFieldKeys = new Set([
+ 'hasChildren',
+ 'isWhite',
+ 'isInspected',
+ 'isChecked'
+])
+
+const purchaseStringFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'purchaseContractNumber',
+ 'supplierName',
+ 'recorderName',
+ 'salesContractNo',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'phoneNumber',
+ 'invoiceNumber',
+ 'paymentMethod',
+ 'templateName',
+ 'productCategory',
+ 'specificationModel',
+ 'unit',
+ 'invoiceType'
+])
+
+const purchaseGenericArrayFieldKeys = new Set([
+ 'purchaseLedgers',
+ 'productData'
+])
+
+const purchaseStringArrayFieldKeys = new Set(['tempFileIds'])
+
+const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles'])
+
+const normalizePurchaseProductRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+ return mapPayloadKeys(record, purchasePayloadFieldKeyMap)
+}
+
+const getPurchaseProductMatchKey = (record) => {
+ if (!record || typeof record !== 'object') return ''
+ return record.purchaseContractNumber ||
+ record.purchaseContractNo ||
+ record.salesContractNo ||
+ record.salesContractNumber ||
+ ''
+}
+
+const prunePurchaseProductRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return null
+ const normalizedRecord = normalizePurchaseProductRecord(record)
+ const hasVisibleFieldValue = Object.entries(normalizedRecord).some(([key, value]) => {
+ if (shouldHidePurchaseField(key)) return false
+ return hasMeaningfulPayloadValue(value)
+ })
+ return hasVisibleFieldValue ? normalizedRecord : null
+}
+
+const normalizeAndFilterPurchaseProductData = (value) => {
+ if (!Array.isArray(value)) return value
+ return value
+ .map(item => prunePurchaseProductRecord(item))
+ .filter(Boolean)
+}
+
+const mergeLegacyProductDataIntoLedgers = (payload) => {
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload
+ if (!Array.isArray(payload.purchaseLedgers) || !Array.isArray(payload.productData) || !payload.productData.length) {
+ return payload
+ }
+
+ const ledgers = payload.purchaseLedgers.map(ledger => ({
+ ...ledger,
+ productData: normalizeAndFilterPurchaseProductData(ledger.productData) || []
+ }))
+ const unmatchedProducts = []
+
+ normalizeAndFilterPurchaseProductData(payload.productData).forEach(product => {
+ const productMatchKey = getPurchaseProductMatchKey(product)
+ const matchedLedger = ledgers.find(ledger => {
+ const ledgerKeys = [
+ ledger.purchaseContractNumber,
+ ledger.purchaseContractNo,
+ ledger.salesContractNo,
+ ledger.salesContractNumber
+ ].filter(Boolean)
+ return productMatchKey && ledgerKeys.includes(productMatchKey)
+ })
+
+ if (matchedLedger) {
+ matchedLedger.productData.push(product)
+ } else if (ledgers.length === 1) {
+ ledgers[0].productData.push(product)
+ } else {
+ unmatchedProducts.push(product)
+ }
+ })
+
+ const nextPayload = {
+ ...payload,
+ purchaseLedgers: ledgers
+ }
+
+ if (unmatchedProducts.length) {
+ nextPayload.productData = unmatchedProducts
+ } else {
+ delete nextPayload.productData
+ }
+
+ return nextPayload
+}
+
+const filterPurchaseLedgerRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+ const normalizedRecord = {
+ ...record,
+ productData: normalizeAndFilterPurchaseProductData(record.productData)
+ }
+ return Object.entries(normalizedRecord).reduce((result, [key, value]) => {
+ if (purchaseLedgerAllowedFieldKeys.has(key)) {
+ result[key] = value
+ }
+ return result
+ }, {})
+}
+
+const normalizeAttachmentMaterialsValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseAttachmentMaterialsField = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchaseAttachmentMaterialsField(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ if (key === 'attachmentMaterials') {
+ result[key] = normalizeAttachmentMaterialsValue(item)
+ } else {
+ result[key] = normalizePurchaseAttachmentMaterialsField(item)
+ }
+ return result
+ }, {})
+ }
+ return value
+}
+
+const normalizePurchaseNumericText = (text = '') => {
+ return String(text)
+ .replace(/[,\s锛宂/g, '')
+ .replace(/[楼锟ュ厓%]/g, '')
+}
+
+const parsePurchaseNumberValue = (value) => {
+ if (typeof value === 'number') {
+ return Number.isFinite(value) ? value : null
+ }
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0
+ }
+ if (typeof value !== 'string') {
+ return null
+ }
+ const text = normalizePurchaseNumericText(value.trim())
+ if (!text) return null
+ if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : null
+}
+
+const normalizePurchaseIntegerFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ if (numberValue === null) return null
+ return Math.trunc(numberValue)
+}
+
+const normalizePurchaseDecimalFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ return numberValue === null ? null : numberValue
+}
+
+const normalizePurchaseBooleanFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ if (typeof value === 'boolean') return value
+ if (typeof value === 'number') return value !== 0
+ if (typeof value !== 'string') return null
+
+ const text = value.trim().toLowerCase()
+ if (!text) return null
+ if (['true', '1', 'yes', 'y', '鏄�', '宸�', 'checked'].includes(text)) return true
+ if (['false', '0', 'no', 'n', '鍚�', '鏈�', 'unchecked'].includes(text)) return false
+ return null
+}
+
+const normalizePurchaseStringFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseStringArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ if (value === null || value === undefined || value === '') return []
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) return []
+ if (/^\[.*\]$/.test(text)) {
+ try {
+ const parsedValue = JSON.parse(text)
+ if (Array.isArray(parsedValue)) {
+ return parsedValue
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ } catch (err) {
+ // Keep as plain text when not valid JSON array.
+ }
+ }
+ const splitValues = text
+ .split(/[,\n锛宂/)
+ .map(item => item.trim())
+ .filter(Boolean)
+ return splitValues.length > 1 ? splitValues : [text]
+ }
+ const normalizedValue = normalizePurchaseStringFieldValue(value)
+ return normalizedValue === null || normalizedValue === undefined || normalizedValue === ''
+ ? []
+ : [normalizedValue]
+}
+
+const normalizePurchaseObjectArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.filter(item => item && typeof item === 'object')
+ }
+ if (value && typeof value === 'object') {
+ return [value]
+ }
+ return []
+}
+
+const normalizePurchaseValueByFieldKey = (fieldKey, value) => {
+ if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value)
+ if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value)
+ if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value)
+ if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value)
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value)
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value)
+ if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value)
+ return value
+}
+
+const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => {
+ if (purchaseGenericArrayFieldKeys.has(fieldKey)) {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+ if (value && typeof value === 'object') {
+ return [normalizePurchasePayloadFieldTypes(value)]
+ }
+ return []
+ }
+
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseStringArrayFieldValue(value)
+ }
+
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseObjectArrayFieldValue(value)
+ }
+
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = normalizePurchasePayloadFieldTypes(item, key)
+ return result
+ }, {})
+ }
+
+ return normalizePurchaseValueByFieldKey(fieldKey, value)
+}
+
+const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
+ if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
+
+ let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ sanitized = normalizePurchaseAttachmentMaterialsField(sanitized)
+ sanitized = normalizePurchasePayloadFieldTypes(sanitized)
+ if (Array.isArray(sanitized.purchaseLedgers)) {
+ sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
+ }
+ if (Array.isArray(sanitized.productData)) {
+ sanitized.productData = normalizeAndFilterPurchaseProductData(sanitized.productData)
+ }
+ if (Array.isArray(sanitized.productData) && !sanitized.productData.length) {
+ delete sanitized.productData
+ }
+
+ purchaseApprovalFieldKeys.forEach(key => {
+ if (!Array.isArray(sanitized)) {
+ delete sanitized[key]
+ }
+ })
+
+ return sanitized
+}
+
+const getVisiblePurchaseMissingFields = (analysisData) => {
+ const fields = Array.isArray(analysisData?.missingFields) ? analysisData.missingFields : []
+ const visibleFields = analysisData?.businessType === 'purchase_ledger'
+ ? fields.filter(field => {
+ if (purchaseApprovalFieldKeys.has(field)) return false
+ const normalizedField = purchasePayloadFieldKeyMap[field] || field
+ return !shouldHidePurchaseField(normalizedField) && !shouldHidePurchaseField(field)
+ })
+ : fields
+ return visibleFields.map(field => purchasePayloadFieldLabelMap[field] || field)
+}
+
+const formatDateParts = (year, month, day) => {
+ const normalizedYear = Number(year)
+ const normalizedMonth = Number(month)
+ const normalizedDay = Number(day)
+ if (!normalizedYear || !normalizedMonth || !normalizedDay) return ''
+
+ const date = new Date(normalizedYear, normalizedMonth - 1, normalizedDay)
+ if (
+ date.getFullYear() !== normalizedYear ||
+ date.getMonth() !== normalizedMonth - 1 ||
+ date.getDate() !== normalizedDay
+ ) {
+ return ''
+ }
+
+ return [
+ String(normalizedYear).padStart(4, '0'),
+ String(normalizedMonth).padStart(2, '0'),
+ String(normalizedDay).padStart(2, '0')
+ ].join('-')
+}
+
+const normalizeDateString = (value) => {
+ if (typeof value !== 'string') return value
+ const text = value.trim()
+ if (!text) return value
+
+ let match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s].*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})(?:\s.*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{4})骞�(\d{1,2})鏈�(\d{1,2})鏃�?(?:\s.*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})(?:\s.*)?$/)
+ if (match) {
+ const year = match[3].length === 2 ? Number(`20${match[3]}`) : Number(match[3])
+ return formatDateParts(year, match[1], match[2]) || value
+ }
+
+ return value
+}
+
+const normalizePurchasePayloadDates = (value, key = '') => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadDates(item, key))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [itemKey, item]) => {
+ result[itemKey] = normalizePurchasePayloadDates(item, itemKey)
+ return result
+ }, {})
+ }
+ return purchaseDateFieldKeys.has(key) ? normalizeDateString(value) : value
+}
+
+const isEmptyValue = (value) => {
+ if (value === null || value === undefined || value === '') return true
+ if (Array.isArray(value)) return value.every(item => isEmptyValue(item))
+ if (typeof value === 'object') return Object.values(value).every(item => isEmptyValue(item))
+ return false
+}
+
+const isPurchasePayloadEmpty = (payload) => isEmptyValue(payload)
+
+const getPurchaseConfirmDescription = (analysisData) => {
+ if (!analysisData) return ''
+ if (isPurchasePayloadEmpty(analysisData.payload)) {
+ return '鎴戞病鏈変粠鏂囦欢涓彁鍙栧埌瀹屾暣鐨勯噰璐笟鍔℃暟鎹紝鏆傛椂涓嶈兘鐩存帴鐢熸垚閲囪喘鍙拌处銆�'
+ }
+ return analysisData.description || '宸叉暣鐞嗗嚭寰呯‘璁ょ殑閲囪喘涓氬姟鏁版嵁锛岃鏍稿鍚庢彁浜ゃ��'
+}
+
+const confirmPurchaseAnalysis = async (message) => {
+ if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+ let payload
+ try {
+ const parsedPayload = message.payloadText?.trim() ? JSON.parse(message.payloadText) : {}
+ payload = sanitizePurchasePayloadForSubmit(
+ normalizePurchasePayloadDates(normalizePurchasePayload(parsedPayload)),
+ message.purchaseAnalysisData.businessType
+ )
+ } catch (err) {
+ message.confirmResult = '寰呮彁浜ゆ暟鎹笉鏄悎娉� JSON锛岃淇敼鍚庡啀纭'
+ message.confirmed = false
+ return
+ }
+
+ message.confirming = true
+ message.confirmResult = ''
+
+ try {
+ const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+ businessType: message.purchaseAnalysisData.businessType,
+ payload
+ })
+ message.confirmed = true
+ message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+ ElMessage.success(message.confirmResult)
+ } catch (err) {
+ message.confirmed = false
+ message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+ } finally {
+ message.confirming = false
+ }
+}
+
+const confirmPurchaseAnalysisFromTable = async (message) => {
+ if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+ if (!Array.isArray(message.payloadTreeData)) {
+ initializePurchasePayloadTree(message, message.purchaseAnalysisData.payload || {})
+ }
+ if (hasPurchaseNodeValidationError(message.payloadTreeData, 'object')) {
+ message.confirmResult = '璇峰厛琛ュ叏瀛楁鍚嶏紝骞剁‘淇濇暟瀛楀瓧娈靛~鍐欏悎娉曟暟瀛�'
+ message.confirmed = false
+ return
+ }
+
+ let payload
+ try {
+ const draftPayload = buildPurchasePayloadFromNodes(message.payloadTreeData, 'object')
+ const mergedPayload = mergePurchasePayloadWithHidden(draftPayload, message.payloadHiddenData)
+ const normalizedPayload = normalizePurchasePayload(mergedPayload)
+ payload = sanitizePurchasePayloadForSubmit(
+ normalizePurchasePayloadDates(normalizedPayload),
+ message.purchaseAnalysisData.businessType
+ )
+ message.payloadText = JSON.stringify(localizePurchasePayload(normalizedPayload), null, 2)
+ } catch (err) {
+ message.confirmResult = '寰呮彁浜ゆ暟鎹牸寮忔湁璇紝璇锋鏌ュ悗鍐嶇‘璁�'
+ message.confirmed = false
+ return
+ }
+
+ message.confirming = true
+ message.confirmResult = ''
+
+ try {
+ const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+ businessType: message.purchaseAnalysisData.businessType,
+ payload
+ })
+ message.confirmed = true
+ message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+ ElMessage.success(message.confirmResult)
+ } catch (err) {
+ message.confirmed = false
+ message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+ } finally {
+ message.confirming = false
+ }
}
const scrollToBottom = () => {
@@ -394,35 +2391,48 @@
})
}
-const handleFileChange = (file) => {
+const handleFileChange = (file, fileList = []) => {
if (!file) return
- const rawFile = file.raw
- if (rawFile) {
- // 闄愬埗鏂囦欢澶у皬锛屼緥濡� 10MB
+ const nextFiles = currentAssistant.value.allowMultipleFileUpload
+ ? fileList.map(item => item.raw).filter(Boolean)
+ : [file.raw].filter(Boolean)
+
+ const validFiles = nextFiles.filter(rawFile => {
const isLt10M = rawFile.size / 1024 / 1024 < 10
if (!isLt10M) {
- ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!')
- return
+ ElMessage.error(`${rawFile.name} 鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!`)
}
- selectedFile.value = rawFile
- }
+ return isLt10M
+ })
+
+ clearSelectedFiles()
+ selectedFiles.value = validFiles
+ uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
+ selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index))
}
-const removeSelectedFile = () => {
- selectedFile.value = null
+const removeSelectedFile = (index) => {
+ const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1)
+ revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : [])
+ selectedFiles.value.splice(index, 1)
+ uploadFileList.value.splice(index, 1)
}
-const analyzeFile = async (file, message = '') => {
+const analyzeFiles = async (files, message = '', localFileSnapshots = []) => {
+ const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
+ if (!uploadFiles.length) return
if (isSending.value) return
isSending.value = true
currentAbortController.value = new AbortController()
-
- const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}`
+
+ const fileNames = uploadFiles.map(file => file.name).join('銆�')
+ const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}`
messages.value.push({
isUser: true,
content: userMsg,
htmlContent: convertTextToHtml(userMsg),
- isTyping: false
+ isTyping: false,
+ localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : []
})
const botMsgIndex = messages.value.length
@@ -434,7 +2444,9 @@
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null
})
outputState.value[botMsgIndex] = {
@@ -448,13 +2460,15 @@
scrollToBottom()
const formData = new FormData()
- formData.append('file', file)
+ const fileFieldName = currentAssistant.value.allowMultipleFileUpload ? 'files' : 'file'
+ uploadFiles.forEach(file => formData.append(fileFieldName, file))
formData.append('memoryId', uuid.value)
if (message.trim()) {
formData.append('message', message.trim())
}
- request.post('/xiaozhi/analyze-file', formData, {
+ const analyzeUrl = currentAssistant.value.fileAnalyzeUrl || `${currentAssistant.value.apiBase}/analyze-file`
+ request.post(analyzeUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
@@ -462,34 +2476,16 @@
onDownloadProgress: (e) => {
const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
if (!fullText) return
-
+
const currentMsg = messages.value[botMsgIndex]
if (!currentMsg) return
-
+
currentMsg.content = fullText
// 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
-
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- currentMsg.type = parsedData.type || ''
- if (currentMsg.type === 'todo_list' && parsedData.data) {
- currentMsg.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- currentMsg.chartOptions = parsedData.charts
- currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
- }
- }
- } catch (err) {}
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
}
updateOutputState(fullText, botMsgIndex)
@@ -501,7 +2497,12 @@
currentMsg.isTyping = false
isSending.value = false
currentAbortController.value = null
-
+
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
+ }
+
// 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
renderCharts(botMsgIndex, currentMsg.chartOptions)
@@ -510,6 +2511,8 @@
}).catch(err => {
if (err.name === 'CanceledError' || err.name === 'AbortError') {
console.log('Analysis aborted by user')
+ isSending.value = false
+ currentAbortController.value = null
return
}
console.error('File analysis error:', err)
@@ -526,10 +2529,11 @@
const sendMessage = () => {
const msg = inputMessage.value?.trim() || ''
- if ((msg || selectedFile.value) && !isSending.value) {
- if (selectedFile.value) {
- analyzeFile(selectedFile.value, msg)
- selectedFile.value = null
+ if ((msg || selectedFiles.value.length) && !isSending.value) {
+ if (selectedFiles.value.length) {
+ const localFileSnapshots = selectedFileSnapshots.value
+ analyzeFiles([...selectedFiles.value], msg, localFileSnapshots)
+ clearSelectedFiles({ releaseSnapshots: false })
} else {
sendRequest(msg)
}
@@ -538,17 +2542,7 @@
}
const stopGeneration = () => {
- if (currentAbortController.value) {
- currentAbortController.value.abort()
- currentAbortController.value = null
- isSending.value = false
-
- // 灏嗘渶鍚庝竴鏉℃秷鎭爣璁颁负闈炴墦瀛楃姸鎬�
- const lastMsg = messages.value[messages.value.length - 1]
- if (lastMsg && !lastMsg.isUser) {
- lastMsg.isTyping = false
- }
- }
+ abortCurrentRequest()
}
const sendRequest = (message) => {
@@ -556,14 +2550,12 @@
currentAbortController.value = new AbortController()
// 鐢ㄦ埛娑堟伅
- if (messages.value.length > 0) {
- messages.value.push({
- isUser: true,
- content: message,
- htmlContent: convertTextToHtml(message),
- isTyping: false
- })
- }
+ messages.value.push({
+ isUser: true,
+ content: message,
+ htmlContent: convertTextToHtml(message),
+ isTyping: false
+ })
// 鏈哄櫒浜哄崰浣�
const botMsgIndex = messages.value.length
@@ -575,10 +2567,12 @@
chartOptions: null,
chartRenderReady: false,
type: '',
- tableData: null
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null
}
messages.value.push(botMsg)
-
+
outputState.value[botMsgIndex] = {
isPaused: false,
jsonBlockStartPos: -1,
@@ -586,86 +2580,84 @@
blockEndPos: -1,
hasRenderedChart: false
}
-
+
scrollToBottom()
- request.post('/xiaozhi/chat',
- { memoryId: uuid.value, message },
- {
- signal: currentAbortController.value.signal,
- onDownloadProgress: (e) => {
- // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
- const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
- if (!fullText) return
-
- const currentMsg = messages.value[botMsgIndex]
- if (!currentMsg) return
-
- currentMsg.content = fullText
+ request.post(`${currentAssistant.value.apiBase}/chat`,
+ { memoryId: uuid.value, message },
+ {
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
- // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- currentMsg.type = parsedData.type || ''
- if (currentMsg.type === 'todo_list' && parsedData.data) {
- currentMsg.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- currentMsg.chartOptions = parsedData.charts
- currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
+ currentMsg.content = fullText
+
+ // 灏濊瘯鎻愬彇骞惰В鏋� JSON
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+
+ // 浠庡悗寰�鍓嶆壘鏈�鍚庝竴涓� '}'
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
}
}
- } catch (err) {}
- }
- updateOutputState(fullText, botMsgIndex)
- currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
- scrollToBottom()
+ const parsedData = extractJson(fullText)
+ if (parsedData) {
+ applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
+ }
+
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
}
- }
).then(() => {
const currentMsg = messages.value[botMsgIndex]
currentMsg.isTyping = false
isSending.value = false
currentAbortController.value = null
-
+
// 鏈�缁堣В鏋�
- const fullText = currentMsg.content
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = fullText.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsedData = JSON.parse(jsonMatch[0])
- if (parsedData.success) {
- currentMsg.type = parsedData.type || ''
- if (currentMsg.type === 'todo_list' && parsedData.data) {
- currentMsg.tableData = parsedData.data
- }
- if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
- currentMsg.chartOptions = parsedData.charts
- currentMsg.chartRenderReady = true
- if (!outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
- outputState.value[botMsgIndex].hasRenderedChart = true
- }
- }
- currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
}
- } catch (err) {}
- }
-
- // 鍏滃簳娓叉煋
- if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
- renderCharts(botMsgIndex, currentMsg.chartOptions)
+ }
+
+ const finalParsed = extractJson(currentMsg.content)
+ if (finalParsed) {
+ applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
+ }
}
}).catch(err => {
if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -704,10 +2696,10 @@
const convertTextToHtml = (text) => {
if (!text) return ''
return text
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/\n/g, '<br>')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/\n/g, '<br>')
}
const convertStreamOutput = (output, msgIndex) => {
@@ -715,29 +2707,87 @@
const state = outputState.value[msgIndex]
let display = output
- const jsonRegex = /\{"success":\s*true,[\s\S]*\}/
- const jsonMatch = output.match(jsonRegex)
- if (jsonMatch) {
- try {
- const parsed = JSON.parse(jsonMatch[0])
- display = output.replace(jsonMatch[0], '').trim()
- if (!display && parsed.description) display = parsed.description
- } catch (e) {
- const start = output.search(/\{"success":\s*true/)
- display = output.substring(0, start) + '... (姝e湪鐢熸垚鏁版嵁鍥捐〃)'
+ // 灏濊瘯鎻愬彇 JSON 閮ㄥ垎
+ const extracted = extractEmbeddedSuccessJson(output)
+ const startMatch = output.match(/\{\s*"success"\s*:/)
+ const startIdx = extracted ? extracted.startIdx : (startMatch?.index ?? -1)
+
+ // 濡傛灉杩樺湪浠g爜鍧椾腑涓旀湭缁撴潫锛屾樉绀烘彁绀烘枃瀛�
+ if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
+ const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
+ const textBeforeBlock = display.substring(0, startPos).trim()
+ display = textBeforeBlock || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ return convertTextToHtml(display)
+ }
+
+ if (extracted) {
+ const parsed = extracted.data
+ display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.action === 'confirm_required'
+ ? getPurchaseConfirmDescription(parsed)
+ : parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } else if (startIdx !== -1) {
+ const lastBraceIdx = output.lastIndexOf('}')
+ if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
+ const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
+ try {
+ const parsed = JSON.parse(potentialJson)
+ // 鎴愬姛瑙f瀽锛岀Щ闄� JSON 閮ㄥ垎鏄剧ず鏂囧瓧
+ display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.action === 'confirm_required'
+ ? getPurchaseConfirmDescription(parsed)
+ : parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁锛�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛細'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } catch (e) {
+ // 瑙f瀽澶辫触锛岃鏄� JSON 杩樺湪浼犺緭涓垨鏍煎紡涓嶆纭�
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ } else {
+ // 鎵惧埌浜嗗紑濮嬩絾杩樻病鎵惧埌缁撴潫
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
}
}
- if (state.jsonBlockStartPos !== -1 && state.blockEndPos === -1) {
- display = display.substring(0, state.jsonBlockStartPos)
- } else if (state.jsBlockStartPos !== -1 && state.blockEndPos === -1) {
- display = display.substring(0, state.jsBlockStartPos)
- }
+ let html = convertTextToHtml(display)
- display = display.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
- display = display.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
+ // 杩樺師浠g爜鍧�
+ html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
+ html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
- return convertTextToHtml(display)
+ return html || '...'
}
const renderCharts = (msgIndex, chartOptions) => {
@@ -747,14 +2797,27 @@
const tryInit = (count = 0) => {
const dom = document.getElementById(id)
if (dom) {
- if (chartInstances.value[id]) chartInstances.value[id].dispose()
+ if (chartInstances.value[id]) {
+ // 濡傛灉宸茬粡鍒濆鍖栬繃锛岀洿鎺ユ洿鏂版暟鎹�
+ const chart = chartInstances.value[id]
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) chart.setOption(option)
+ return
+ }
+
const chart = echarts.init(dom)
chartInstances.value[id] = chart
- chart.setOption(chartOptions[key])
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) {
+ chart.setOption(option)
+ } else {
+ console.warn('Invalid chart option for:', id, chartOptions[key])
+ }
+
const handler = () => chart.resize()
resizeHandlers.value.push(handler)
window.addEventListener('resize', handler)
- } else if (count < 10) {
+ } else if (count < 15) { // 绋嶅井澧炲姞閲嶈瘯娆℃暟
setTimeout(() => tryInit(count + 1), 200)
}
}
@@ -763,19 +2826,198 @@
})
}
+// 鏍煎紡鍖� AI 杩斿洖鐨勫浘琛ㄩ厤缃紝灏嗗叾杞崲涓烘爣鍑嗙殑 ECharts 閰嶇疆
+const formatChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ // 濡傛灉宸茬粡鏄爣鍑� ECharts 閰嶇疆锛堝寘鍚� series锛夛紝鍒欑洿鎺ヨ繑鍥�
+ const hasSeries = rawOption.series && Array.isArray(rawOption.series)
+
+ // 灏濊瘯杞崲绠�鏄撴牸寮�
+ try {
+ const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('鍗犳瘮'))
+
+ const option = {
+ title: {
+ text: rawOption.title || '',
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: {
+ bottom: '0'
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ type: 'category',
+ data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
+ },
+ yAxis: isPie ? undefined : {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ },
+ series: rawOption.series || [{
+ name: rawOption.title || '鏁板��',
+ type: rawOption.type || 'line',
+ data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
+ smooth: true,
+ radius: isPie ? '50%' : undefined
+ }]
+ }
+
+ // 閽堝楗煎浘鐨勭壒娈婂鐞�
+ if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
+ option.series[0].data = rawOption.data
+ }
+
+ return option
+ } catch (err) {
+ console.error('Chart option conversion failed:', err)
+ return null
+ }
+}
+
+const normalizeAiChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ try {
+ const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
+ const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
+ const titleConfig = rawOption.title && typeof rawOption.title === 'object'
+ ? rawOption.title
+ : null
+ const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
+ ? rawOption.tooltip
+ : null
+ const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
+ ? rawOption.legend
+ : null
+ const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
+ ? rawOption.xAxis
+ : null
+ const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
+ ? rawOption.yAxis
+ : null
+ const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
+ const isPie = firstSeriesType === 'pie' || titleText.includes('鍗犳瘮')
+ const baseXAxisData = Array.isArray(rawOption.xAxisData)
+ ? rawOption.xAxisData
+ : (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
+ const fallbackSeries = [{
+ name: titleText || '鏁版嵁',
+ type: rawOption.type || 'line',
+ data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ }]
+ const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
+ const seriesType = seriesItem?.type || rawOption.type || 'line'
+ const nextSeries = {
+ ...seriesItem,
+ name: seriesItem?.name || titleText || `绯诲垪${index + 1}`,
+ type: seriesType
+ }
+
+ if (isPie) {
+ nextSeries.radius = nextSeries.radius || '55%'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ } else {
+ nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
+ }
+
+ return nextSeries
+ })
+ const categorySource = !isPie
+ ? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
+ : null
+ const xAxisData = categorySource
+ ? categorySource.data.map(item => item.name)
+ : baseXAxisData
+ const finalSeries = !isPie && categorySource
+ ? normalizedSeries.map(seriesItem => ({
+ ...seriesItem,
+ data: Array.isArray(seriesItem.data)
+ ? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
+ : []
+ }))
+ : normalizedSeries
+
+ return {
+ title: titleConfig || {
+ text: titleText,
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: tooltipConfig || {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: legendConfig || {
+ bottom: '0'
+ },
+ grid: isPie ? undefined : {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ ...(rawXAxisConfig || {}),
+ type: 'category',
+ data: xAxisData,
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
+ },
+ yAxis: isPie ? undefined : (rawYAxisConfig || {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ }),
+ series: finalSeries
+ }
+ } catch (err) {
+ console.error('AI chart normalization failed:', err, rawOption)
+ return formatChartOption(rawOption)
+ }
+}
+
watch(messages, () => scrollToBottom(), { deep: true })
</script>
-<style scoped lang="scss">
-.ai-chat-sidebar-wrapper {
- position: fixed;
- inset: 0;
- z-index: 2000;
- pointer-events: none;
+<style lang="scss">
+.ai-chat-overlay {
+ pointer-events: none !important;
+ background: transparent !important;
+}
- :deep(.el-drawer__container) {
- pointer-events: none;
- }
+.ai-chat-overlay .el-drawer {
+ pointer-events: auto;
+}
+</style>
+
+<style scoped lang="scss">
+$primary-blue: #0055d4;
+$secondary-blue: #2e8ce0;
+$light-blue: #7ab8ff;
+$pale-blue: #c5dcff;
+$ice-white: #e8f2ff;
+$deep-blue: #003b8e;
+$deepest-blue: #002b66;
+$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
+$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
+$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
+$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
+$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
+$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
+
+.ai-chat-sidebar-wrapper {
+ position: static;
+ z-index: 2000;
+ pointer-events: auto;
:deep(.el-drawer) {
pointer-events: auto;
@@ -785,29 +3027,82 @@
.ai-chat-trigger {
pointer-events: auto;
position: fixed;
- right: 20px;
+ right: 24px;
bottom: 100px;
- width: 60px;
- height: 60px;
- background: linear-gradient(135deg, #409eff 0%, #007aff 100%);
+ width: 56px;
+ height: 56px;
+ background: $gradient-dark;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
- box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
- transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2);
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 2001;
+ animation: triggerPulse 3s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -6px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
+ border-radius: 50%;
+ z-index: -1;
+ filter: blur(16px);
+ animation: glowPulse 2s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
+ pointer-events: none;
+ }
&:hover {
- transform: scale(1.1) translateY(-5px);
- box-shadow: 0 8px 20px rgba(0, 122, 255, 0.5);
+ transform: scale(1.12) translateY(-4px);
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3);
+
+ &::before {
+ animation: glowPulse 1s ease-in-out infinite alternate;
+ }
+
+ .trigger-icon {
+ transform: rotate(-8deg) scale(1.05);
+ filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
+ }
}
.trigger-icon {
display: flex;
align-items: center;
justify-content: center;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ color: #fff;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+ }
+}
+
+@keyframes triggerPulse {
+ 0%, 100% {
+ box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
+ }
+ 50% {
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
+ }
+}
+
+@keyframes glowPulse {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.1);
}
}
@@ -815,13 +3110,16 @@
:deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
:deep(.el-drawer__header) {
- margin-bottom: 0;
- padding: 12px 16px;
- background: #fff;
- border-bottom: 1px solid #ebeef5;
- color: #303133;
+ margin-bottom: 0 !important;
+ padding: 0 !important;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ background: $gradient-dark;
+ color: #fff;
}
}
@@ -830,27 +3128,211 @@
justify-content: space-between;
align-items: center;
width: 100%;
- padding-right: 32px;
+ padding: 12px 18px;
+ background: $gradient-dark;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -60%;
+ right: -25%;
+ width: 250px;
+ height: 250px;
+ background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 4s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -40%;
+ left: -15%;
+ width: 200px;
+ height: 200px;
+ background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 5s ease-in-out infinite alternate-reverse;
+ }
.header-left {
display: flex;
align-items: center;
- gap: 8px;
-
+ gap: 12px;
+ position: relative;
+ z-index: 1;
+
.header-icon {
- color: #409eff;
+ color: rgba(255, 255, 255, 0.95);
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
+ animation: iconFloat 3s ease-in-out infinite;
}
-
+
.title {
- font-size: 16px;
+ font-size: 17px;
font-weight: 600;
- color: #303133;
+ color: #fff;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+ letter-spacing: 0.5px;
}
}
.header-actions {
display: flex;
- gap: 8px;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+ z-index: 1;
+
+ .action-divider {
+ width: 1px;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.2);
+ margin: 0 2px;
+ }
+
+ :deep(.el-button) {
+ color: rgba(255, 255, 255, 0.85);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 8px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.25);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.close-btn {
+ background: rgba(255, 255, 255, 0.1);
+ &:hover {
+ background: rgba(245, 108, 108, 0.8);
+ border-color: rgba(245, 108, 108, 0.5);
+ }
+ }
+ }
+
+ :deep(.header-action-btn) {
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.12);
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 55%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -40%;
+ width: 60%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.28), transparent);
+ transform: rotate(24deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+
+ :deep(.header-action-btn--text) {
+ width: auto !important;
+ min-width: 104px;
+ padding: 8px 14px !important;
+ font-size: 14px;
+ font-weight: 600;
+ white-space: nowrap;
+ }
+ }
+
+ .assistant-switcher {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ padding: 0 12px;
+ position: relative;
+ z-index: 1;
+
+ :deep(.el-radio-group) {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ justify-content: center;
+ padding: 4px;
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.1);
+ }
+
+ :deep(.el-radio-button__inner) {
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.12);
+ color: rgba(255, 255, 255, 0.86);
+ box-shadow: none;
+ padding: 7px 14px;
+ font-weight: 500;
+ }
+
+ :deep(.el-radio-button:first-child .el-radio-button__inner),
+ :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-radius: 999px;
+ }
+
+ :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: #fff;
+ color: $primary-blue;
+ border-color: #fff;
+ box-shadow: 0 6px 14px rgba(0, 40, 120, 0.16);
+ }
+ }
+}
+
+@keyframes headerGlow {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.15);
+ }
+}
+
+@keyframes iconFloat {
+ 0%, 100% {
+ transform: translateY(0) rotate(0);
+ }
+ 50% {
+ transform: translateY(-2px) rotate(3deg);
}
}
@@ -858,61 +3340,122 @@
display: flex;
flex-direction: column;
height: 100%;
- background-color: #f5f7fa;
+ width: 100%;
+ background: $ice-white;
position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 128px;
+ background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
+ pointer-events: none;
+ }
}
.history-panel {
position: absolute;
inset: 0;
- background: #fff;
+ background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
z-index: 10;
display: flex;
flex-direction: column;
-
+ box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
+
.history-header {
- padding: 16px;
- border-bottom: 1px solid #ebeef5;
+ padding: 18px 20px;
+ border-bottom: 1px solid rgba(0, 85, 212, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 14px;
+ color: $deep-blue;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
+ }
}
.session-list {
flex: 1;
overflow-y: auto;
- padding: 8px;
+ padding: 12px 16px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
+ }
.session-item {
display: flex;
align-items: center;
- padding: 12px;
- margin-bottom: 4px;
- border-radius: 8px;
+ padding: 14px 16px;
+ margin-bottom: 6px;
+ border-radius: 12px;
cursor: pointer;
- transition: all 0.2s;
- gap: 10px;
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ gap: 12px;
position: relative;
border: 1px solid transparent;
+ background: #fff;
+ animation: sessionSlideIn 0.35s ease;
+
+ @keyframes sessionSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-15px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
&:hover {
- background-color: #f5f7fa;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
+ border-color: rgba(0, 85, 212, 0.12);
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
+ transform: translateX(4px);
+
.delete-btn {
opacity: 1;
+ transform: scale(1);
}
}
&.active {
- background-color: #ecf5ff;
- border-color: #d9ecff;
- color: #409eff;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border-color: rgba(0, 85, 212, 0.25);
+ color: $primary-blue;
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
+
+ .el-icon {
+ color: $primary-blue;
+ }
}
.el-icon {
- font-size: 16px;
+ font-size: 18px;
flex-shrink: 0;
+ color: $secondary-blue;
+ transition: color 0.2s;
}
.session-name {
@@ -921,14 +3464,22 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ color: #1a1a2e;
+ font-weight: 500;
}
.delete-btn {
opacity: 0;
- transition: opacity 0.2s;
- padding: 4px;
+ transform: scale(0.8);
+ transition: all 0.25s ease;
+ padding: 6px;
+ border-radius: 6px;
+ color: #c0c4cc;
+
&:hover {
- color: #f56c6c;
+ color: #fff;
+ background: rgba(245, 108, 108, 0.85);
+ transform: scale(1.1) rotate(8deg);
}
}
}
@@ -946,59 +3497,180 @@
.message-list {
flex: 1;
overflow-y: auto;
- padding: 20px;
+ padding: 24px 20px;
display: flex;
flex-direction: column;
gap: 20px;
+ background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
&::-webkit-scrollbar {
- width: 6px;
+ width: 8px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
- border-radius: 3px;
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
}
}
.message-item {
display: flex;
- gap: 12px;
+ gap: 14px;
width: 100%;
+ animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ @keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
.avatar {
- width: 36px;
- height: 36px;
- border-radius: 8px;
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
- font-size: 20px;
+ font-size: 24px;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: inherit;
+ filter: blur(10px);
+ opacity: 0.5;
+ z-index: -1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
+ animation: shimmer 3s infinite;
+ }
}
.message-content {
flex: 1;
- overflow-x: hidden; // 淇敼涓� hidden锛屽唴閮ㄥ鍣ㄥ鐞嗘粴鍔�
+ overflow-x: hidden;
display: flex;
flex-direction: column;
- max-width: calc(100% - 48px); // 鍑忓幓澶村儚鍜岄棿璺�
-
+ max-width: calc(100% - 56px);
+
.text-box {
- padding: 12px 16px;
- border-radius: 12px;
+ padding: 14px 20px;
+ border-radius: 18px;
font-size: 14px;
- line-height: 1.6;
+ line-height: 1.7;
word-break: break-word;
max-width: 100%;
width: fit-content;
overflow-x: auto;
+ transition: all 0.3s ease;
+ position: relative;
+
&::-webkit-scrollbar {
- height: 6px;
+ height: 4px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
- border-radius: 3px;
+ background: rgba(0, 85, 212, 0.25);
+ border-radius: 2px;
}
+ }
+
+ .message-local-file-list {
+ margin-top: 8px;
+ display: grid;
+ gap: 8px;
+ max-width: 100%;
+ }
+
+ .message-local-file-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(88, 117, 255, 0.2);
+ background: rgba(255, 255, 255, 0.9);
+ max-width: 100%;
+
+ &.clickable {
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: rgba(44, 109, 255, 0.38);
+ background: rgba(243, 247, 255, 0.96);
+ }
+ }
+ }
+
+ .message-local-file-thumb {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ overflow: hidden;
+ flex-shrink: 0;
+ border: 1px solid rgba(124, 148, 255, 0.26);
+ background: #f4f7ff;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .message-local-file-icon {
+ font-size: 20px;
+ color: $primary-blue;
+ flex-shrink: 0;
+ }
+
+ .message-local-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .message-local-file-name {
+ font-size: 12px;
+ color: #1f2a44;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.clickable {
+ color: $primary-blue;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .message-local-file-size {
+ font-size: 11px;
+ color: #7f8ba1;
+ line-height: 1.2;
}
}
@@ -1007,13 +3679,26 @@
align-items: flex-start;
}
.avatar {
- background-color: #409eff;
+ background: $gradient-dark;
color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
}
.text-box {
- background-color: #fff;
- color: #303133;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ background: #fff;
+ color: #1a1a2e;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 85, 212, 0.08);
+ border-top-left-radius: 6px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
+ }
}
}
@@ -1023,29 +3708,52 @@
align-items: flex-end;
}
.avatar {
- background-color: #95d475;
+ background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
}
.text-box {
- background-color: #409eff;
+ background: $gradient-dark;
color: #fff;
+ border-top-right-radius: 6px;
+ box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
+ }
}
}
}
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%) rotate(45deg);
+ }
+ 100% {
+ transform: translateX(100%) rotate(45deg);
+ }
+}
+
.charts-wrapper {
- margin-top: 10px;
+ margin-top: 12px;
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 12px;
overflow-x: auto;
width: 100%;
padding-bottom: 8px;
+
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
border-radius: 3px;
}
}
@@ -1054,153 +3762,1180 @@
width: 100%;
min-width: 300px;
height: 300px;
- background: #fff;
- border-radius: 8px;
- padding: 10px;
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 12px;
}
.table-wrapper {
- margin-top: 10px;
+ margin-top: 12px;
background: #fff;
- border-radius: 8px;
+ border-radius: 12px;
overflow: hidden;
overflow-x: auto;
width: 100%;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
- background: #dcdfe6;
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
border-radius: 3px;
}
+
.el-table {
min-width: 300px;
+ --el-table-border-color: rgba(0, 122, 255, 0.08);
+ --el-table-header-bg-color: $ice-white;
+ }
+}
+
+.purchase-confirm-card {
+ margin-top: 12px;
+ width: 100%;
+ background: #fff;
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ border-radius: 12px;
+ box-shadow: $shadow-card;
+ padding: 14px;
+ color: #1a1a2e;
+}
+
+.purchase-confirm-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 15px;
+ font-weight: 700;
+ margin-bottom: 12px;
+}
+
+.purchase-confirm-desc {
+ margin-bottom: 12px;
+ color: #374151;
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.purchase-empty-state {
+ margin-bottom: 12px;
+ padding: 12px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, rgba(255, 247, 237, 0.96), rgba(255, 255, 255, 0.98));
+ border: 1px solid rgba(245, 158, 11, 0.25);
+
+ .empty-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: #92400e;
+ margin-bottom: 6px;
+ }
+
+ .empty-desc {
+ color: #78350f;
+ font-size: 13px;
+ line-height: 1.6;
+ }
+}
+
+.purchase-alert {
+ border-radius: 8px;
+ padding: 10px 12px;
+ margin-bottom: 10px;
+ font-size: 13px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ }
+
+ &.warning {
+ background: rgba(230, 162, 60, 0.12);
+ color: #9a5b00;
+ }
+
+ &.missing {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ background: rgba(245, 108, 108, 0.1);
+ color: #b42318;
+ }
+}
+
+.purchase-preview {
+ margin-bottom: 12px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ font-size: 13px;
+ line-height: 1.7;
+ }
+}
+
+.purchase-section-title {
+ margin: 10px 0 6px;
+ font-size: 13px;
+ font-weight: 700;
+ color: $deep-blue;
+}
+
+.payload-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 8px;
+}
+
+.payload-tree-table-wrapper {
+ border: 1px solid rgba(0, 85, 212, 0.1);
+ border-radius: 10px;
+ overflow: auto;
+
+ :deep(.el-table) {
+ --el-table-header-bg-color: #f5f8ff;
+ --el-table-border-color: rgba(0, 85, 212, 0.08);
+ }
+}
+
+.payload-key-cell {
+ display: flex;
+ align-items: center;
+ min-height: 28px;
+}
+
+.payload-fixed-key {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ line-height: 1.3;
+ color: #1f2937;
+
+ small {
+ font-size: 11px;
+ color: #6b7280;
+ }
+}
+
+.payload-array-index {
+ font-size: 12px;
+ color: #475467;
+}
+
+.payload-container-cell {
+ color: #344054;
+ font-size: 12px;
+}
+
+.payload-null-value {
+ color: #6b7280;
+ font-size: 12px;
+}
+
+.payload-row-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+}
+
+.payload-editor-tip {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #6b7280;
+}
+
+.purchase-confirm-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+
+ .confirm-result {
+ flex: 1;
+ font-size: 13px;
+
+ &.success {
+ color: #1f9d55;
+ }
+
+ &.error {
+ color: #d93025;
+ }
}
}
.input-area {
- padding: 16px;
- background-color: #fff;
- border-top: 1px solid #dcdfe6;
+ padding: 18px 20px;
+ background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
+ border-top: 1px solid rgba(0, 85, 212, 0.1);
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 20px;
+ right: 20px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
+ }
.input-actions {
- display: flex;
- gap: 12px;
- margin-bottom: 8px;
- align-items: center;
-
- .file-upload-trigger {
- display: inline-flex;
+ display: flex;
+ gap: 14px;
+ margin-bottom: 12px;
align-items: center;
+
+ .file-upload-trigger {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ :deep(.utility-action-btn) {
+ position: relative;
+ height: 34px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(92, 119, 255, 0.18);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(236, 243, 255, 0.98));
+ color: $primary-blue;
+ font-weight: 600;
+ box-shadow: 0 10px 20px rgba(0, 85, 212, 0.08);
+ transition: all 0.25s ease;
+
+ .el-icon {
+ margin-right: 5px;
+ }
+
+ &:hover:not(.is-disabled) {
+ color: #fff;
+ border-color: transparent;
+ background: linear-gradient(135deg, #1f6dff 0%, #6b38ef 100%);
+ box-shadow: 0 14px 24px rgba(64, 90, 255, 0.2);
+ transform: translateY(-1px);
+ }
+ }
+
+ :deep(.stop-action-btn) {
+ border-color: rgba(255, 99, 123, 0.18);
+ color: #d33e5e;
+
+ &:hover:not(.is-disabled) {
+ background: linear-gradient(135deg, #f5536e 0%, #a33cff 100%);
+ }
+ }
}
-}
.input-box {
- padding: 12px;
+ padding: 16px;
position: relative;
background: #fff;
- border: 1px solid #dcdfe6;
- border-radius: 8px;
- margin: 0 16px 16px;
- transition: border-color 0.2s;
+ border: 2px solid rgba(0, 85, 212, 0.12);
+ border-radius: 16px;
+ margin: 0 4px;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
&:focus-within {
- border-color: #409eff;
+ border-color: $primary-blue;
+ box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
+ transform: translateY(-2px);
+ background: #fff;
+ }
+
+ .selected-file-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 12px;
}
.selected-file-tag {
display: flex;
align-items: center;
- background: #f0f7ff;
- border: 1px solid #d9ecff;
- border-radius: 4px;
- padding: 4px 8px;
- margin-bottom: 8px;
- gap: 6px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ border-radius: 10px;
+ padding: 8px 12px;
+ gap: 10px;
width: fit-content;
max-width: 100%;
+ animation: tagSlideIn 0.3s ease;
+
+ @keyframes tagSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
.el-icon {
- color: #409eff;
- font-size: 14px;
+ color: $primary-blue;
+ font-size: 18px;
+ }
+
+ .selected-file-thumb {
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ flex-shrink: 0;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .selected-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
}
.file-name {
- font-size: 12px;
- color: #606266;
+ font-size: 13px;
+ color: $deep-blue;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ font-weight: 600;
+ }
+
+ .file-size {
+ font-size: 11px;
+ color: #5f86b4;
+ line-height: 1.1;
}
.remove-file {
cursor: pointer;
- color: #909399;
- transition: color 0.2s;
+ color: $secondary-blue;
+ transition: all 0.2s;
+ padding: 4px;
+ border-radius: 50%;
+
&:hover {
- color: #f56c6c;
+ color: #fff;
+ background: rgba(245, 108, 108, 0.8);
+ transform: scale(1.1) rotate(90deg);
}
}
}
:deep(.el-textarea__inner) {
padding: 0;
+ padding-bottom: 35px;
border: none;
box-shadow: none;
background: transparent;
font-family: inherit;
font-size: 14px;
- line-height: 1.5;
- color: #303133;
+ line-height: 1.6;
+ color: #1a1a2e;
+
&::placeholder {
- color: #c0c4cc;
+ color: #7ab8ff;
+ }
+
+ &:focus {
+ box-shadow: none;
}
}
.send-btn {
position: absolute;
- right: 12px;
- bottom: 12px;
- padding: 8px 16px;
+ right: 16px;
+ bottom: 16px;
+ padding: 10px 22px;
+ background: $gradient-dark;
+ border: none;
+ border-radius: 10px;
+ font-weight: 600;
+ font-size: 14px;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ letter-spacing: 0.3px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: left 0.5s;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-1px) scale(0.98);
+ }
+
+ &:disabled {
+ background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
+ box-shadow: none;
+ cursor: not-allowed;
+ }
+
+ .el-icon {
+ font-size: 15px;
+ transform: translateY(-1px);
+ }
}
}
}
.typing-indicator {
display: flex;
- gap: 4px;
- padding: 8px 12px;
+ gap: 5px;
+ padding: 10px 14px;
background: #fff;
- border-radius: 12px;
+ border-radius: 14px;
width: fit-content;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
- margin-top: 4px;
+ box-shadow: $shadow-card;
+ margin-top: 6px;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+ border-top-left-radius: 4px;
+
.dot {
- width: 6px;
- height: 6px;
- background-color: #909399;
+ width: 7px;
+ height: 7px;
+ background: $secondary-blue;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
- &:nth-child(2) { animation-delay: 0.2s; }
- &:nth-child(3) { animation-delay: 0.4s; }
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ background: $primary-blue;
+ }
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ background: $deep-blue;
+ }
}
}
@keyframes typing {
- 0%, 80%, 100% { transform: scale(0); }
- 40% { transform: scale(1); }
+ 0%, 80%, 100% {
+ transform: scale(0.6);
+ opacity: 0.4;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
}
.code-block {
- background: #2d2d2d;
- color: #ccc;
- padding: 12px;
- border-radius: 6px;
- font-family: monospace;
- margin: 8px 0;
+ background: linear-gradient(145deg, #1a1a2e, #16213e);
+ color: #a8d8ff;
+ padding: 14px;
+ border-radius: 10px;
+ font-family: 'Fira Code', 'Consolas', monospace;
+ margin: 10px 0;
overflow-x: auto;
+ border: 1px solid rgba(90, 200, 250, 0.15);
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+
&.js-code {
- color: #f08d49;
+ color: #5ac8fa;
+ }
+}
+
+.chat-main {
+ background:
+ radial-gradient(circle at top left, rgba(46, 140, 224, 0.12) 0%, transparent 34%),
+ linear-gradient(180deg, #fff 0%, #f7fbff 46%, #fff 100%);
+}
+
+.chat-hero {
+ display: grid;
+ grid-template-columns: 176px minmax(0, 1fr);
+ gap: 14px;
+ align-items: stretch;
+ padding: 8px 18px 4px;
+
+ &.compact {
+ grid-template-columns: 132px minmax(0, 1fr);
+ gap: 10px;
+ padding: 4px 18px 2px;
+ }
+}
+
+.assistant-stand {
+ position: relative;
+ min-height: 206px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding-top: 8px;
+ overflow: hidden;
+
+ &.compact {
+ min-height: 160px;
+ padding-top: 4px;
+ }
+
+ &.thinking {
+ .assistant-halo {
+ opacity: 1;
+ transform: scale(1.12);
+ filter: blur(9px);
+ }
+
+ .assistant-scan-ring {
+ opacity: 0.95;
+ animation-duration: 1.5s;
+ }
+
+ .assistant-orbit {
+ opacity: 0.76;
+ }
+
+ .assistant-model-shell {
+ transform: translateY(-5px) scale(1.02);
+ }
+
+ .assistant-model-cut {
+ animation-duration: 2.2s;
+ }
+
+ .assistant-model-img {
+ filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22));
+ }
+
+ .assistant-status {
+ color: #6a3bee;
+ box-shadow: 0 10px 22px rgba(106, 59, 238, 0.14);
+ }
+
+ .assistant-status-dot {
+ background: #6a3bee;
+ box-shadow: 0 0 12px rgba(106, 59, 238, 0.9);
+ animation: thinkingDot 1s ease-in-out infinite;
+ }
+
+ .assistant-base-lg {
+ animation-duration: 1.8s;
+ }
+
+ .assistant-base-md {
+ animation-duration: 1.5s;
+ }
+
+ .assistant-base-sm {
+ box-shadow: 0 0 24px rgba(30, 91, 255, 0.36);
+ animation-duration: 1.25s;
+ }
+ }
+}
+
+.assistant-halo {
+ position: absolute;
+ top: 24px;
+ width: 146px;
+ height: 146px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%);
+ filter: blur(6px);
+ opacity: 0.78;
+ transition: all 0.35s ease;
+}
+
+.assistant-scan-ring {
+ position: absolute;
+ top: 44px;
+ width: 136px;
+ height: 136px;
+ border-radius: 50%;
+ border: 1px solid rgba(67, 145, 223, 0.24);
+ box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
+ opacity: 0.52;
+ animation: scanRing 4s linear infinite;
+}
+
+.assistant-orbit {
+ position: absolute;
+ top: 52px;
+ width: 156px;
+ height: 156px;
+ border-radius: 50%;
+ border: 1px dashed rgba(92, 135, 255, 0.24);
+ opacity: 0.42;
+}
+
+.assistant-orbit-a {
+ animation: orbitRotate 8.6s linear infinite;
+}
+
+.assistant-orbit-b {
+ width: 124px;
+ height: 124px;
+ top: 68px;
+ border-color: rgba(31, 122, 114, 0.24);
+ animation: orbitRotateReverse 6.2s linear infinite;
+}
+
+.assistant-model-shell {
+ position: relative;
+ z-index: 1;
+ width: 148px;
+ height: 178px;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ margin-top: 4px;
+ transition: transform 0.35s ease;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ bottom: 2px;
+ width: 164px;
+ height: 42px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ background: radial-gradient(
+ ellipse at center,
+ rgba(43, 126, 211, 0.32) 0%,
+ rgba(43, 126, 211, 0.14) 46%,
+ rgba(43, 126, 211, 0) 74%
+ );
+ filter: blur(2.6px);
+ animation: baseGlow 4.6s ease-in-out infinite;
+ z-index: 1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ bottom: 10px;
+ width: 138px;
+ height: 28px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 1px solid rgba(36, 116, 198, 0.6);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.58),
+ 0 0 22px rgba(42, 116, 196, 0.24);
+ animation: basePulse 3.2s ease-in-out infinite;
+ z-index: 4;
+ }
+}
+
+.assistant-model-cut {
+ position: relative;
+ width: 132px;
+ height: 178px;
+ z-index: 6;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ transform-origin: center 84%;
+ animation: avatarFloat 3.2s ease-in-out infinite;
+}
+
+.assistant-model-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center bottom;
+ display: block;
+ filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2));
+ transition: filter 0.35s ease;
+}
+
+.assistant-model-fallback {
+ width: 92px;
+ height: 92px;
+ border-radius: 24px;
+ color: #fff;
+ background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9));
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.assistant-base {
+ position: absolute;
+ left: 50%;
+ bottom: 8px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 1px solid rgba(36, 116, 198, 0.28);
+ background: radial-gradient(
+ ellipse at center,
+ rgba(255, 255, 255, 0.94) 0%,
+ rgba(81, 164, 233, 0.16) 58%,
+ rgba(30, 91, 255, 0.06) 100%
+ );
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
+}
+
+.assistant-status {
+ position: relative;
+ z-index: 1;
+ margin-top: 7px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ color: $deep-blue;
+ background: rgba(255, 255, 255, 0.95);
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ box-shadow: 0 8px 20px rgba(0, 85, 212, 0.08);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.assistant-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #2e8ce0;
+ box-shadow: 0 0 10px rgba(46, 140, 224, 0.72);
+}
+
+.assistant-base {
+ pointer-events: none;
+}
+
+.assistant-base-md {
+ bottom: 15px;
+ width: 104px;
+ height: 22px;
+ border-color: rgba(36, 116, 198, 0.48);
+ animation: basePulse 2.8s ease-in-out infinite;
+}
+
+.assistant-base-sm {
+ bottom: 20px;
+ width: 68px;
+ height: 14px;
+ background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 0.9));
+ border: none;
+ box-shadow: 0 0 18px rgba(45, 124, 255, 0.34);
+ animation: basePulse 2.2s ease-in-out infinite;
+}
+
+@keyframes orbitRotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes orbitRotateReverse {
+ from {
+ transform: rotate(360deg);
+ }
+ to {
+ transform: rotate(0deg);
+ }
+}
+
+@keyframes scanRing {
+ 0%, 100% {
+ transform: scale(0.96);
+ opacity: 0.42;
+ }
+ 50% {
+ transform: scale(1.04);
+ opacity: 0.86;
+ }
+}
+
+@keyframes thinkingDot {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.35);
+ }
+}
+
+.assistant-base-lg {
+ width: 142px;
+ height: 32px;
+ animation: basePulse 3.4s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 130px;
+ height: 130px;
+ transform: translate(-50%, -50%);
+ border-radius: 50%;
+ background: conic-gradient(
+ from 180deg,
+ transparent 0deg,
+ rgba(36, 116, 198, 0.65) 48deg,
+ transparent 114deg,
+ rgba(36, 116, 198, 0.55) 212deg,
+ transparent 286deg,
+ rgba(31, 122, 114, 0.45) 334deg,
+ transparent 360deg
+ );
+ -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ opacity: 0.62;
+ animation: baseSpin 9s linear infinite;
+ }
+}
+
+@keyframes avatarFloat {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-7px);
+ }
+}
+
+@keyframes basePulse {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scale(1);
+ opacity: 0.88;
+ }
+ 50% {
+ transform: translateX(-50%) scale(1.05);
+ opacity: 0.98;
+ }
+}
+
+@keyframes baseSpin {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+@keyframes baseGlow {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scaleX(1);
+ opacity: 0.82;
+ }
+ 50% {
+ transform: translateX(-50%) scaleX(1.06);
+ opacity: 0.96;
+ }
+}
+
+.welcome-card {
+ position: relative;
+ align-self: stretch;
+ min-height: 206px;
+ padding: 9px 10px 8px;
+ border-radius: 16px;
+ background:
+ linear-gradient(#fff, #fff) padding-box,
+ linear-gradient(135deg, rgba(255, 64, 96, 0.85), rgba(117, 65, 255, 0.9)) border-box;
+ border: 1px solid transparent;
+ box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
+
+ &.compact {
+ min-height: 160px;
+ padding: 8px 9px 7px;
+ border-radius: 12px;
+ box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
+
+ .welcome-eyebrow {
+ margin-bottom: 4px;
+ }
+
+ .welcome-title {
+ font-size: 16px;
+ line-height: 1.25;
+
+ br {
+ display: none;
+ }
+ }
+
+ .welcome-desc {
+ margin-top: 4px;
+ font-size: 11px;
+ line-height: 1.5;
+ }
+
+ .quick-prompt-list {
+ margin-top: 8px;
+ gap: 5px;
+ }
+
+ .quick-prompt-btn {
+ padding: 7px 9px;
+ font-size: 11px;
+ border-radius: 7px;
+ }
+
+ .more-prompts-btn {
+ margin-top: 6px;
+ font-size: 11px;
+ }
+ }
+}
+
+.welcome-eyebrow {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ color: rgba(0, 85, 212, 0.58);
+ margin-bottom: 5px;
+}
+
+.welcome-title {
+ margin: 0;
+ font-size: 20px;
+ line-height: 1.15;
+ font-weight: 800;
+ color: #172033;
+
+ br {
+ display: none;
+ }
+}
+
+.welcome-desc {
+ margin: 5px 0 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #5f6980;
+}
+
+.quick-prompt-list {
+ display: grid;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+.quick-prompt-btn {
+ width: 100%;
+ border: none;
+ border-radius: 9px;
+ padding: 7px 10px;
+ text-align: left;
+ font-size: 12px;
+ font-weight: 600;
+ color: #fff;
+ cursor: pointer;
+ background: linear-gradient(90deg, #ff4c55 0%, #7c38ef 100%);
+ box-shadow: 0 12px 22px rgba(124, 56, 239, 0.18);
+ transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 56%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -30%;
+ width: 45%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transform: rotate(22deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px) scale(1.01);
+ box-shadow: 0 16px 28px rgba(124, 56, 239, 0.24);
+
+ &::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+ }
+}
+
+.more-prompts-btn {
+ margin-top: 6px;
+ padding: 0 10px;
+ height: 26px;
+ border: 1px solid rgba(208, 65, 81, 0.12);
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96));
+ color: #d04151;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ box-shadow: 0 10px 18px rgba(208, 65, 81, 0.08);
+ transition: all 0.25s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ background: linear-gradient(135deg, #ff5570 0%, #8a3df6 100%);
+ border-color: transparent;
+ color: #fff;
+ box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18);
+ }
+}
+
+.message-list {
+ padding: 8px 18px 18px;
+ gap: 16px;
+ background: transparent;
+}
+
+.input-area {
+ padding: 12px 18px 16px;
+ background: #fff;
+ border-top: none;
+
+ &::before {
+ display: none;
+ }
+
+ .input-box {
+ padding: 14px 16px 16px;
+ border: 1px solid rgba(123, 56, 239, 0.9);
+ border-radius: 22px;
+ margin: 0;
+ transition: all 0.25s ease;
+ box-shadow: 0 14px 34px rgba(0, 85, 212, 0.08);
+
+ &:focus-within {
+ border-color: #7c38ef;
+ box-shadow: 0 0 0 3px rgba(124, 56, 239, 0.1), 0 18px 40px rgba(0, 85, 212, 0.12);
+ transform: none;
+ }
+
+ :deep(.el-textarea__inner) {
+ padding-right: 58px;
+ padding-bottom: 0;
+ min-height: 104px;
+
+ &::placeholder {
+ color: #a0a9bc;
+ }
+ }
+
+ .send-btn {
+ right: 25px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 36px;
+ min-width: 36px;
+ height: 36px;
+ padding: 0;
+ background: linear-gradient(135deg, #ff5570 0%, #7a36f2 58%, #2d79ff 100%);
+ border-radius: 50%;
+ box-shadow: 0 12px 24px rgba(109, 50, 236, 0.24);
+ transition: all 0.25s ease;
+ gap: 0;
+
+ &:hover:not(:disabled) {
+ transform: translateY(calc(-50% - 1px)) scale(1.04);
+ box-shadow: 0 16px 28px rgba(109, 50, 236, 0.3);
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-50%) scale(0.96);
+ }
+
+ .el-icon {
+ margin: 0;
+ font-size: 16px;
+ transform: translate(0, -1px);
+ }
+ }
+ }
+}
+
+@media (max-width: 767px) {
+ .chat-hero {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ padding: 14px 14px 6px;
+
+ &.compact {
+ padding: 8px 14px 4px;
+ }
+ }
+
+ .assistant-stand {
+ min-height: 184px;
+ }
+
+ .welcome-card {
+ padding: 12px 12px 10px;
+ }
+
+ .welcome-title {
+ font-size: 21px;
+ }
+
+ .message-list {
+ padding: 8px 14px 14px;
+ }
+
+ .input-area {
+ padding: 10px 14px 14px;
+ }
+
+ .input-area .input-actions {
+ gap: 10px;
+ flex-wrap: wrap;
}
}
</style>
--
Gitblit v1.9.3