Merge branch 'ai-hub' into markdown
# Conflicts: # Yi.Ai.Vue3/package.json # Yi.Ai.Vue3/pnpm-lock.yaml # Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/index.vue
This commit is contained in:
@@ -10,6 +10,7 @@ public class PremiumPackageConst
|
|||||||
"claude-haiku-4-5-20251001",
|
"claude-haiku-4-5-20251001",
|
||||||
"claude-opus-4-5-20251101",
|
"claude-opus-4-5-20251101",
|
||||||
"gemini-3-pro-preview",
|
"gemini-3-pro-preview",
|
||||||
"gpt-5.1-codex-max"
|
"gpt-5.1-codex-max",
|
||||||
|
"gpt-5.2"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ public sealed class AnthropicInput
|
|||||||
|
|
||||||
[JsonPropertyName("max_tokens")] public int? MaxTokens { get; set; }
|
[JsonPropertyName("max_tokens")] public int? MaxTokens { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("messages")] public JsonElement? Messages { get; set; }
|
[JsonPropertyName("messages")] public IList<AnthropicMessageInput> Messages { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("tools")] public IList<AnthropicMessageTool>? Tools { get; set; }
|
[JsonPropertyName("tools")] public IList<AnthropicMessageTool>? Tools { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,28 @@ public class ThorChatMessage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于数据存储
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string MessagesStore
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Content is not null)
|
||||||
|
{
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Contents is not null && Contents.Any())
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(Contents);
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【可选】参与者的可选名称。提供模型信息以区分相同角色的参与者。
|
/// 【可选】参与者的可选名称。提供模型信息以区分相同角色的参与者。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ public static class GoodsTypeEnumExtensions
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 计算折扣金额(仅用于尊享包)
|
/// 计算折扣金额(仅用于尊享包)
|
||||||
/// 规则:每累加充值10元,减少2.5元,最多减少50元
|
/// 规则:每累加充值10元,减少10元,最多减少50元
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="goodsType">商品类型</param>
|
/// <param name="goodsType">商品类型</param>
|
||||||
/// <param name="totalRechargeAmount">用户累加充值金额</param>
|
/// <param name="totalRechargeAmount">用户累加充值金额</param>
|
||||||
@@ -271,11 +271,10 @@ public static class GoodsTypeEnumExtensions
|
|||||||
{
|
{
|
||||||
return 0m;
|
return 0m;
|
||||||
}
|
}
|
||||||
|
// 每满 10 元减 10 元
|
||||||
// 每10元减2.5元
|
var discountTimes = Math.Floor(totalRechargeAmount / 10m);
|
||||||
var discountAmount = Math.Floor(totalRechargeAmount / 2.5m);
|
var discountAmount = discountTimes * 10m;
|
||||||
|
// 最多减少 50 元
|
||||||
// 最多减少50元
|
|
||||||
return Math.Min(discountAmount, 50m);
|
return Math.Min(discountAmount, 50m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ public class AiGateWayManager : DomainService
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Ai对话异常");
|
_logger.LogError(e, $"Ai对话异常");
|
||||||
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
var model = new ThorChatCompletionsResponse()
|
var model = new ThorChatCompletionsResponse()
|
||||||
{
|
{
|
||||||
Choices = new List<ThorChatChoiceResponse>()
|
Choices = new List<ThorChatChoiceResponse>()
|
||||||
@@ -275,7 +275,7 @@ public class AiGateWayManager : DomainService
|
|||||||
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||||
new MessageInputDto
|
new MessageInputDto
|
||||||
{
|
{
|
||||||
Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.MessagesStore ?? string.Empty,
|
||||||
ModelId = request.Model,
|
ModelId = request.Model,
|
||||||
TokenUsage = tokenUsage,
|
TokenUsage = tokenUsage,
|
||||||
}, tokenId);
|
}, tokenId);
|
||||||
@@ -365,7 +365,7 @@ public class AiGateWayManager : DomainService
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
var errorContent = $"图片生成Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
var errorContent = $"图片生成Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
throw new UserFriendlyException(errorContent);
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,7 +478,7 @@ public class AiGateWayManager : DomainService
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
var errorContent = $"嵌入Ai异常,异常信息:\n当前Ai模型:{input.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
var errorContent = $"嵌入Ai异常,异常信息:\n当前Ai模型:{input.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
throw new UserFriendlyException(errorContent);
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -595,7 +595,7 @@ public class AiGateWayManager : DomainService
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Ai对话异常");
|
_logger.LogError(e, $"Ai对话异常");
|
||||||
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
throw new UserFriendlyException(errorContent);
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,7 +754,7 @@ public class AiGateWayManager : DomainService
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Ai响应异常");
|
_logger.LogError(e, $"Ai响应异常");
|
||||||
var errorContent = $"响应Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
var errorContent = $"响应Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||||
throw new UserFriendlyException(errorContent);
|
throw new UserFriendlyException(errorContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ VITE_WEB_BASE_API = '/dev-api'
|
|||||||
VITE_API_URL = http://localhost:19001/api/app
|
VITE_API_URL = http://localhost:19001/api/app
|
||||||
#VITE_API_URL=http://data.ccnetcore.com:19001/api/app
|
#VITE_API_URL=http://data.ccnetcore.com:19001/api/app
|
||||||
|
|
||||||
|
# 文件上传接口域名
|
||||||
|
VITE_FILE_UPLOAD_API = https://ai.ccnetcore.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# SSO单点登录url
|
# SSO单点登录url
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ VITE_WEB_BASE_API = '/prod-api'
|
|||||||
# 本地接口
|
# 本地接口
|
||||||
VITE_API_URL = http://data.ccnetcore.com:19001/api/app
|
VITE_API_URL = http://data.ccnetcore.com:19001/api/app
|
||||||
|
|
||||||
|
# 文件上传接口域名
|
||||||
|
VITE_FILE_UPLOAD_API = https://ai.ccnetcore.com
|
||||||
|
|
||||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
VITE_BUILD_COMPRESS = gzip
|
VITE_BUILD_COMPRESS = gzip
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"globals": {
|
|
||||||
"Component": true,
|
|
||||||
"ComponentPublicInstance": true,
|
|
||||||
"ComputedRef": true,
|
|
||||||
"DirectiveBinding": true,
|
|
||||||
"EffectScope": true,
|
|
||||||
"ElMessage": true,
|
|
||||||
"ElMessageBox": true,
|
|
||||||
"ExtractDefaultPropTypes": true,
|
|
||||||
"ExtractPropTypes": true,
|
|
||||||
"ExtractPublicPropTypes": true,
|
|
||||||
"InjectionKey": true,
|
|
||||||
"MaybeRef": true,
|
|
||||||
"MaybeRefOrGetter": true,
|
|
||||||
"PropType": true,
|
|
||||||
"Ref": true,
|
|
||||||
"Slot": true,
|
|
||||||
"Slots": true,
|
|
||||||
"VNode": true,
|
|
||||||
"WritableComputedRef": true,
|
|
||||||
"computed": true,
|
|
||||||
"createApp": true,
|
|
||||||
"customRef": true,
|
|
||||||
"defineAsyncComponent": true,
|
|
||||||
"defineComponent": true,
|
|
||||||
"effectScope": true,
|
|
||||||
"getCurrentInstance": true,
|
|
||||||
"getCurrentScope": true,
|
|
||||||
"h": true,
|
|
||||||
"inject": true,
|
|
||||||
"isProxy": true,
|
|
||||||
"isReactive": true,
|
|
||||||
"isReadonly": true,
|
|
||||||
"isRef": true,
|
|
||||||
"markRaw": true,
|
|
||||||
"nextTick": true,
|
|
||||||
"onActivated": true,
|
|
||||||
"onBeforeMount": true,
|
|
||||||
"onBeforeUnmount": true,
|
|
||||||
"onBeforeUpdate": true,
|
|
||||||
"onDeactivated": true,
|
|
||||||
"onErrorCaptured": true,
|
|
||||||
"onMounted": true,
|
|
||||||
"onRenderTracked": true,
|
|
||||||
"onRenderTriggered": true,
|
|
||||||
"onScopeDispose": true,
|
|
||||||
"onServerPrefetch": true,
|
|
||||||
"onUnmounted": true,
|
|
||||||
"onUpdated": true,
|
|
||||||
"onWatcherCleanup": true,
|
|
||||||
"provide": true,
|
|
||||||
"reactive": true,
|
|
||||||
"readonly": true,
|
|
||||||
"ref": true,
|
|
||||||
"resolveComponent": true,
|
|
||||||
"shallowReactive": true,
|
|
||||||
"shallowReadonly": true,
|
|
||||||
"shallowRef": true,
|
|
||||||
"toRaw": true,
|
|
||||||
"toRef": true,
|
|
||||||
"toRefs": true,
|
|
||||||
"toValue": true,
|
|
||||||
"triggerRef": true,
|
|
||||||
"unref": true,
|
|
||||||
"useAttrs": true,
|
|
||||||
"useCssModule": true,
|
|
||||||
"useCssVars": true,
|
|
||||||
"useId": true,
|
|
||||||
"useModel": true,
|
|
||||||
"useSlots": true,
|
|
||||||
"useTemplateRef": true,
|
|
||||||
"watch": true,
|
|
||||||
"watchEffect": true,
|
|
||||||
"watchPostEffect": true,
|
|
||||||
"watchSyncEffect": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
Yi.Ai.Vue3/.gitignore
vendored
7
Yi.Ai.Vue3/.gitignore
vendored
@@ -23,3 +23,10 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
/.eslintrc-auto-import.json
|
||||||
|
/types/auto-imports.d.ts
|
||||||
|
/types/components.d.ts
|
||||||
|
/types/import_meta.d.ts
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<!-- 加载动画容器 -->
|
<!-- 加载动画容器 -->
|
||||||
<div id="yixinai-loader" class="loader-container">
|
<div id="yixinai-loader" class="loader-container">
|
||||||
<div class="loader-title">意心Ai 2.7</div>
|
<div class="loader-title">意心Ai 2.8</div>
|
||||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒</div>
|
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒</div>
|
||||||
<div class="loader-logo">
|
<div class="loader-logo">
|
||||||
<div class="pulse-box"></div>
|
<div class="pulse-box"></div>
|
||||||
|
|||||||
@@ -51,8 +51,10 @@
|
|||||||
"hook-fetch": "^2.0.4-beta.1",
|
"hook-fetch": "^2.0.4-beta.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"mammoth": "^1.11.0",
|
||||||
"mermaid": "11.12.0",
|
"mermaid": "11.12.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
"pdfjs-dist": "^5.4.449",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
@@ -74,7 +76,8 @@
|
|||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-element-plus-x": "1.3.7",
|
"vue-element-plus-x": "1.3.7",
|
||||||
"vue-router": "4"
|
"vue-router": "4",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^4.16.2",
|
"@antfu/eslint-config": "^4.16.2",
|
||||||
|
|||||||
697
Yi.Ai.Vue3/pnpm-lock.yaml
generated
697
Yi.Ai.Vue3/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -220,4 +220,16 @@ export interface ChatMessageVo {
|
|||||||
* 用户id
|
* 用户id
|
||||||
*/
|
*/
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
/**
|
||||||
|
* 用户消息中的图片列表(前端扩展字段)
|
||||||
|
*/
|
||||||
|
images?: Array<{ url: string; name?: string }>;
|
||||||
|
/**
|
||||||
|
* 用户消息中的文件列表(前端扩展字段)
|
||||||
|
*/
|
||||||
|
files?: Array<{ name: string; size: number }>;
|
||||||
|
/**
|
||||||
|
* 创建时间(前端显示用)
|
||||||
|
*/
|
||||||
|
creationTime?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
34
Yi.Ai.Vue3/src/api/file/index.ts
Normal file
34
Yi.Ai.Vue3/src/api/file/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { UploadFileResponse } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
* @param file 文件对象
|
||||||
|
* @returns 返回文件ID数组
|
||||||
|
*/
|
||||||
|
export async function uploadFile(file: File): Promise<UploadFileResponse[]> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const uploadApiUrl = import.meta.env.VITE_FILE_UPLOAD_API;
|
||||||
|
|
||||||
|
const response = await fetch(`${uploadApiUrl}/prod-api/file`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('文件上传失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文件URL
|
||||||
|
* @param fileId 文件ID
|
||||||
|
* @returns 文件访问URL
|
||||||
|
*/
|
||||||
|
export function getFileUrl(fileId: string): string {
|
||||||
|
return `https://ccnetcore.com/prod-api/file/${fileId}/true`;
|
||||||
|
}
|
||||||
3
Yi.Ai.Vue3/src/api/file/types.ts
Normal file
3
Yi.Ai.Vue3/src/api/file/types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface UploadFileResponse {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './announcement'
|
export * from './announcement'
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
export * from './chat';
|
export * from './chat';
|
||||||
|
export * from './file';
|
||||||
export * from './model';
|
export * from './model';
|
||||||
export * from './pay';
|
export * from './pay';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
|
|||||||
7
Yi.Ai.Vue3/src/assets/icons/System/notification-fill.svg
Normal file
7
Yi.Ai.Vue3/src/assets/icons/System/notification-fill.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M12 2C16.9706 2 21 6.04348 21 11.0314V20H3V11.0314C3 6.04348 7.02944 2 12 2ZM9.5 21H14.5C14.5 22.3807 13.3807 23.5 12 23.5C10.6193 23.5 9.5 22.3807 9.5 21Z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 322 B |
@@ -1,148 +1,725 @@
|
|||||||
<!-- 文件上传 -->
|
<!-- 文件上传 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
import type { FileItem } from '@/stores/modules/files';
|
||||||
import { useFileDialog } from '@vueuse/core';
|
import { useFileDialog } from '@vueuse/core';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import mammoth from 'mammoth';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
import Popover from '@/components/Popover/index.vue';
|
import Popover from '@/components/Popover/index.vue';
|
||||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
import { useFilesStore } from '@/stores/modules/files';
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
|
|
||||||
type FilesList = FilesCardProps & {
|
// 配置 PDF.js worker - 使用稳定的 CDN
|
||||||
file: File;
|
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
|
||||||
};
|
|
||||||
|
|
||||||
const filesStore = useFilesStore();
|
const filesStore = useFilesStore();
|
||||||
|
|
||||||
/* 弹出面板 开始 */
|
// 文件大小限制 3MB
|
||||||
const popoverStyle = ref({
|
const MAX_FILE_SIZE = 3 * 1024 * 1024;
|
||||||
padding: '4px',
|
|
||||||
height: 'fit-content',
|
// 单个文件内容长度限制
|
||||||
background: 'var(--el-bg-color, #fff)',
|
const MAX_TEXT_FILE_LENGTH = 50000; // 文本文件最大字符数
|
||||||
border: '1px solid var(--el-border-color-light)',
|
const MAX_WORD_LENGTH = 30000; // Word 文档最大字符数
|
||||||
borderRadius: '8px',
|
const MAX_EXCEL_ROWS = 100; // Excel 最大行数
|
||||||
boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
|
const MAX_PDF_PAGES = 10; // PDF 最大页数
|
||||||
});
|
|
||||||
const popoverRef = ref();
|
// 整个消息总长度限制(所有文件内容加起来,预估 token 安全限制)
|
||||||
/* 弹出面板 结束 */
|
// 272000 tokens * 0.55 安全系数 ≈ 150000 字符
|
||||||
|
const MAX_TOTAL_CONTENT_LENGTH = 150000;
|
||||||
|
|
||||||
const { reset, open, onChange } = useFileDialog({
|
const { reset, open, onChange } = useFileDialog({
|
||||||
// 允许所有图片文件,文档文件,音视频文件
|
// 支持图片、文档、文本文件等
|
||||||
accept: 'image/*,video/*,audio/*,application/*',
|
accept: 'image/*,.txt,.log,.csv,.tsv,.md,.markdown,.json,.xml,.yaml,.yml,.toml,.ini,.conf,.config,.properties,.prop,.env,'
|
||||||
directory: false, // 是否允许选择文件夹
|
+ '.js,.jsx,.ts,.tsx,.vue,.html,.htm,.css,.scss,.sass,.less,.styl,'
|
||||||
multiple: true, // 是否允许多选
|
+ '.java,.c,.cpp,.h,.hpp,.cs,.py,.rb,.go,.rs,.swift,.kt,.php,.sh,.bash,.zsh,.fish,.bat,.cmd,.ps1,'
|
||||||
|
+ '.sql,.graphql,.proto,.thrift,'
|
||||||
|
+ '.dockerfile,.gitignore,.gitattributes,.editorconfig,.npmrc,.nvmrc,'
|
||||||
|
+ '.sln,.csproj,.vbproj,.fsproj,.props,.targets,'
|
||||||
|
+ '.xlsx,.xls,.csv,.docx,.pdf',
|
||||||
|
directory: false,
|
||||||
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
onChange((files) => {
|
/**
|
||||||
|
* 压缩图片
|
||||||
|
* @param {File} file - 原始图片文件
|
||||||
|
* @param {number} maxWidth - 最大宽度,默认 1024px
|
||||||
|
* @param {number} maxHeight - 最大高度,默认 1024px
|
||||||
|
* @param {number} quality - 压缩质量,0-1之间,默认 0.8
|
||||||
|
* @returns {Promise<Blob>} 压缩后的图片 Blob
|
||||||
|
*/
|
||||||
|
function compressImage(file: File, maxWidth = 1024, maxHeight = 1024, quality = 0.8): Promise<Blob> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
let width = img.width;
|
||||||
|
let height = img.height;
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
if (width > maxWidth || height > maxHeight) {
|
||||||
|
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
||||||
|
width = width * ratio;
|
||||||
|
height = height * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// 转换为 Blob
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reject(new Error('压缩失败'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
file.type,
|
||||||
|
quality,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Blob 转换为 base64 格式
|
||||||
|
* @param {Blob} blob - 要转换的 Blob 对象
|
||||||
|
* @returns {Promise<string>} base64 编码的字符串(包含 data:xxx;base64, 前缀)
|
||||||
|
*/
|
||||||
|
function blobToBase64(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文本文件内容
|
||||||
|
* @param {File} file - 文本文件
|
||||||
|
* @returns {Promise<string>} 文件内容字符串
|
||||||
|
*/
|
||||||
|
function readTextFile(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为文本文件
|
||||||
|
* 通过 MIME 类型或文件扩展名判断
|
||||||
|
* @param {File} file - 要判断的文件
|
||||||
|
* @returns {boolean} 是否为文本文件
|
||||||
|
*/
|
||||||
|
function isTextFile(file: File): boolean {
|
||||||
|
// 通过 MIME type 判断
|
||||||
|
if (file.type.startsWith('text/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过扩展名判断(更全面的列表)
|
||||||
|
const textExtensions = [
|
||||||
|
// 通用文本
|
||||||
|
'txt',
|
||||||
|
'log',
|
||||||
|
'md',
|
||||||
|
'markdown',
|
||||||
|
'rtf',
|
||||||
|
// 配置文件
|
||||||
|
'json',
|
||||||
|
'xml',
|
||||||
|
'yaml',
|
||||||
|
'yml',
|
||||||
|
'toml',
|
||||||
|
'ini',
|
||||||
|
'conf',
|
||||||
|
'config',
|
||||||
|
'properties',
|
||||||
|
'prop',
|
||||||
|
'env',
|
||||||
|
// 前端
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'vue',
|
||||||
|
'html',
|
||||||
|
'htm',
|
||||||
|
'css',
|
||||||
|
'scss',
|
||||||
|
'sass',
|
||||||
|
'less',
|
||||||
|
'styl',
|
||||||
|
// 编程语言
|
||||||
|
'java',
|
||||||
|
'c',
|
||||||
|
'cpp',
|
||||||
|
'h',
|
||||||
|
'hpp',
|
||||||
|
'cs',
|
||||||
|
'py',
|
||||||
|
'rb',
|
||||||
|
'go',
|
||||||
|
'rs',
|
||||||
|
'swift',
|
||||||
|
'kt',
|
||||||
|
'php',
|
||||||
|
// 脚本
|
||||||
|
'sh',
|
||||||
|
'bash',
|
||||||
|
'zsh',
|
||||||
|
'fish',
|
||||||
|
'bat',
|
||||||
|
'cmd',
|
||||||
|
'ps1',
|
||||||
|
// 数据库/API
|
||||||
|
'sql',
|
||||||
|
'graphql',
|
||||||
|
'proto',
|
||||||
|
'thrift',
|
||||||
|
// 版本控制/工具
|
||||||
|
'dockerfile',
|
||||||
|
'gitignore',
|
||||||
|
'gitattributes',
|
||||||
|
'editorconfig',
|
||||||
|
'npmrc',
|
||||||
|
'nvmrc',
|
||||||
|
// .NET 项目文件
|
||||||
|
'sln',
|
||||||
|
'csproj',
|
||||||
|
'vbproj',
|
||||||
|
'fsproj',
|
||||||
|
'props',
|
||||||
|
'targets',
|
||||||
|
// 数据文件
|
||||||
|
'csv',
|
||||||
|
'tsv',
|
||||||
|
];
|
||||||
|
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? textExtensions.includes(ext) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Excel 文件,提取前 N 行数据转为 CSV 格式
|
||||||
|
* @param {File} file - Excel 文件 (.xlsx, .xls)
|
||||||
|
* @returns {Promise<{content: string, totalRows: number, extractedRows: number}>}
|
||||||
|
* - content: CSV 格式的文本内容
|
||||||
|
* - totalRows: 文件总行数
|
||||||
|
* - extractedRows: 实际提取的行数(受 MAX_EXCEL_ROWS 限制)
|
||||||
|
*/
|
||||||
|
async function parseExcel(file: File): Promise<{ content: string; totalRows: number; extractedRows: number }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' });
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
let totalRows = 0;
|
||||||
|
let extractedRows = 0;
|
||||||
|
|
||||||
|
workbook.SheetNames.forEach((sheetName, index) => {
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
||||||
|
// 获取工作表的范围
|
||||||
|
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
|
||||||
|
const sheetTotalRows = range.e.r - range.s.r + 1;
|
||||||
|
totalRows += sheetTotalRows;
|
||||||
|
|
||||||
|
// 限制行数
|
||||||
|
const rowsToExtract = Math.min(sheetTotalRows, MAX_EXCEL_ROWS);
|
||||||
|
extractedRows += rowsToExtract;
|
||||||
|
|
||||||
|
// 创建新的范围,只包含前 N 行
|
||||||
|
const limitedRange = {
|
||||||
|
s: { r: range.s.r, c: range.s.c },
|
||||||
|
e: { r: range.s.r + rowsToExtract - 1, c: range.e.c },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取限制范围内的数据
|
||||||
|
const limitedData: any[][] = [];
|
||||||
|
for (let row = limitedRange.s.r; row <= limitedRange.e.r; row++) {
|
||||||
|
const rowData: any[] = [];
|
||||||
|
for (let col = limitedRange.s.c; col <= limitedRange.e.c; col++) {
|
||||||
|
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
|
||||||
|
const cell = worksheet[cellAddress];
|
||||||
|
rowData.push(cell ? cell.v : '');
|
||||||
|
}
|
||||||
|
limitedData.push(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 CSV
|
||||||
|
const csvData = limitedData.map(row => row.join(',')).join('\n');
|
||||||
|
|
||||||
|
if (workbook.SheetNames.length > 1) {
|
||||||
|
result += `=== Sheet: ${sheetName} ===\n`;
|
||||||
|
}
|
||||||
|
result += csvData;
|
||||||
|
if (index < workbook.SheetNames.length - 1) {
|
||||||
|
result += '\n\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve({ content: result, totalRows, extractedRows });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Word 文档,提取纯文本内容
|
||||||
|
* @param {File} file - Word 文档 (.docx)
|
||||||
|
* @returns {Promise<{content: string, totalLength: number, extracted: boolean}>}
|
||||||
|
* - content: 提取的文本内容
|
||||||
|
* - totalLength: 原始文本总长度
|
||||||
|
* - extracted: 是否被截断(超过 MAX_WORD_LENGTH)
|
||||||
|
*/
|
||||||
|
async function parseWord(file: File): Promise<{ content: string; totalLength: number; extracted: boolean }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = e.target?.result as ArrayBuffer;
|
||||||
|
const result = await mammoth.extractRawText({ arrayBuffer });
|
||||||
|
const fullText = result.value;
|
||||||
|
const totalLength = fullText.length;
|
||||||
|
|
||||||
|
if (totalLength > MAX_WORD_LENGTH) {
|
||||||
|
const truncated = fullText.substring(0, MAX_WORD_LENGTH);
|
||||||
|
resolve({ content: truncated, totalLength, extracted: true });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resolve({ content: fullText, totalLength, extracted: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 PDF 文件,提取前 N 页的文本内容
|
||||||
|
* @param {File} file - PDF 文件
|
||||||
|
* @returns {Promise<{content: string, totalPages: number, extractedPages: number}>}
|
||||||
|
* - content: 提取的文本内容
|
||||||
|
* - totalPages: 文件总页数
|
||||||
|
* - extractedPages: 实际提取的页数(受 MAX_PDF_PAGES 限制)
|
||||||
|
*/
|
||||||
|
async function parsePDF(file: File): Promise<{ content: string; totalPages: number; extractedPages: number }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const typedArray = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||||
|
const pdf = await pdfjsLib.getDocument(typedArray).promise;
|
||||||
|
const totalPages = pdf.numPages;
|
||||||
|
const pagesToExtract = Math.min(totalPages, MAX_PDF_PAGES);
|
||||||
|
|
||||||
|
let fullText = '';
|
||||||
|
for (let i = 1; i <= pagesToExtract; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const textContent = await page.getTextContent();
|
||||||
|
const pageText = textContent.items.map((item: any) => item.str).join(' ');
|
||||||
|
fullText += `${pageText}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ content: fullText, totalPages, extractedPages: pagesToExtract });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名
|
||||||
|
* @param {string} filename - 文件名
|
||||||
|
* @returns {string} 小写的扩展名,无点号
|
||||||
|
*/
|
||||||
|
function getFileExtension(filename: string): string {
|
||||||
|
return filename.split('.').pop()?.toLowerCase() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(async (files) => {
|
||||||
if (!files)
|
if (!files)
|
||||||
return;
|
return;
|
||||||
const arr = [] as FilesList[];
|
|
||||||
|
const arr = [] as FileItem[];
|
||||||
|
let totalContentLength = 0; // 跟踪总内容长度
|
||||||
|
|
||||||
|
// 先计算已有文件的总内容长度
|
||||||
|
filesStore.filesList.forEach((f) => {
|
||||||
|
if (f.fileType === 'text' && f.fileContent) {
|
||||||
|
totalContentLength += f.fileContent.length;
|
||||||
|
}
|
||||||
|
// 图片 base64 也计入(虽然转 token 时不同,但也要计算)
|
||||||
|
if (f.fileType === 'image' && f.base64) {
|
||||||
|
// base64 转 token 比例约 1:1.5,这里保守估计
|
||||||
|
totalContentLength += Math.floor(f.base64.length * 0.5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (let i = 0; i < files!.length; i++) {
|
for (let i = 0; i < files!.length; i++) {
|
||||||
const file = files![i];
|
const file = files![i];
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
ElMessage.error(`文件 ${file.name} 超过 3MB 限制,已跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = getFileExtension(file.name);
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isExcel = ['xlsx', 'xls'].includes(ext);
|
||||||
|
const isWord = ext === 'docx';
|
||||||
|
const isPDF = ext === 'pdf';
|
||||||
|
const isText = isTextFile(file);
|
||||||
|
|
||||||
|
// 处理图片文件
|
||||||
|
if (isImage) {
|
||||||
|
try {
|
||||||
|
// 多级压缩策略:逐步降低质量和分辨率
|
||||||
|
const compressionLevels = [
|
||||||
|
{ maxWidth: 800, maxHeight: 800, quality: 0.6 },
|
||||||
|
{ maxWidth: 600, maxHeight: 600, quality: 0.5 },
|
||||||
|
{ maxWidth: 400, maxHeight: 400, quality: 0.4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let compressedBlob: Blob | null = null;
|
||||||
|
let base64 = '';
|
||||||
|
let compressionLevel = 0;
|
||||||
|
|
||||||
|
// 尝试不同级别的压缩
|
||||||
|
for (const level of compressionLevels) {
|
||||||
|
compressionLevel++;
|
||||||
|
compressedBlob = await compressImage(file, level.maxWidth, level.maxHeight, level.quality);
|
||||||
|
base64 = await blobToBase64(compressedBlob);
|
||||||
|
|
||||||
|
// 检查是否满足总长度限制
|
||||||
|
const estimatedLength = Math.floor(base64.length * 0.5);
|
||||||
|
if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) {
|
||||||
|
// 满足限制,使用当前压缩级别
|
||||||
|
totalContentLength += estimatedLength;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是最后一级压缩仍然超限,则跳过
|
||||||
|
if (compressionLevel === compressionLevels.length) {
|
||||||
|
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
ElMessage.error(`${file.name} (${fileSizeMB}MB) 即使压缩后仍超过总内容限制,已跳过`);
|
||||||
|
compressedBlob = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果压缩失败,跳过此文件
|
||||||
|
if (!compressedBlob) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算压缩比例
|
||||||
|
const originalSize = (file.size / 1024).toFixed(2);
|
||||||
|
const compressedSize = (compressedBlob.size / 1024).toFixed(2);
|
||||||
|
console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${compressedSize}KB (级别${compressionLevel})`);
|
||||||
|
|
||||||
arr.push({
|
arr.push({
|
||||||
uid: crypto.randomUUID(), // 不写 uid,文件列表展示不出来,elx 1.2.0 bug 待修复
|
uid: crypto.randomUUID(),
|
||||||
name: file.name,
|
name: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
file,
|
file,
|
||||||
maxWidth: '200px',
|
maxWidth: '200px',
|
||||||
showDelIcon: true, // 显示删除图标
|
showDelIcon: true,
|
||||||
imgPreview: true, // 显示图片预览
|
imgPreview: true,
|
||||||
imgVariant: 'square', // 图片预览的形状
|
imgVariant: 'square',
|
||||||
url: URL.createObjectURL(file), // 图片预览地址
|
url: base64, // 使用压缩后的 base64 作为预览地址
|
||||||
|
isUploaded: true,
|
||||||
|
base64,
|
||||||
|
fileType: 'image',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('压缩图片失败:', error);
|
||||||
|
ElMessage.error(`${file.name} 压缩失败`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 Excel 文件
|
||||||
|
else if (isExcel) {
|
||||||
|
try {
|
||||||
|
const result = await parseExcel(file);
|
||||||
|
|
||||||
|
// 动态裁剪内容以适应剩余空间
|
||||||
|
let finalContent = result.content;
|
||||||
|
let wasTruncated = result.totalRows > MAX_EXCEL_ROWS;
|
||||||
|
|
||||||
|
// 如果超过总内容限制,裁剪内容
|
||||||
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
||||||
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
||||||
|
// 至少保留1000字符才有意义
|
||||||
|
finalContent = result.content.substring(0, remainingSpace);
|
||||||
|
wasTruncated = true;
|
||||||
|
} else if (remainingSpace <= 1000) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalContentLength += finalContent.length;
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
uid: crypto.randomUUID(),
|
||||||
|
name: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
file,
|
||||||
|
maxWidth: '200px',
|
||||||
|
showDelIcon: true,
|
||||||
|
imgPreview: false,
|
||||||
|
isUploaded: true,
|
||||||
|
fileContent: finalContent,
|
||||||
|
fileType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
if (wasTruncated) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Excel 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总行数: ${result.totalRows}, 已提取: ${result.extractedRows} 行, 内容长度: ${finalContent.length} 字符`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('解析 Excel 失败:', error);
|
||||||
|
ElMessage.error(`${file.name} 解析失败`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 Word 文档
|
||||||
|
else if (isWord) {
|
||||||
|
try {
|
||||||
|
const result = await parseWord(file);
|
||||||
|
|
||||||
|
// 动态裁剪内容以适应剩余空间
|
||||||
|
let finalContent = result.content;
|
||||||
|
let wasTruncated = result.extracted;
|
||||||
|
|
||||||
|
// 如果超过总内容限制,裁剪内容
|
||||||
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
||||||
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
||||||
|
finalContent = result.content.substring(0, remainingSpace);
|
||||||
|
wasTruncated = true;
|
||||||
|
} else if (remainingSpace <= 1000) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalContentLength += finalContent.length;
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
uid: crypto.randomUUID(),
|
||||||
|
name: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
file,
|
||||||
|
maxWidth: '200px',
|
||||||
|
showDelIcon: true,
|
||||||
|
imgPreview: false,
|
||||||
|
isUploaded: true,
|
||||||
|
fileContent: finalContent,
|
||||||
|
fileType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
if (wasTruncated) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Word 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总长度: ${result.totalLength}, 已提取: ${finalContent.length} 字符`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('解析 Word 失败:', error);
|
||||||
|
ElMessage.error(`${file.name} 解析失败`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 PDF 文件
|
||||||
|
else if (isPDF) {
|
||||||
|
try {
|
||||||
|
const result = await parsePDF(file);
|
||||||
|
|
||||||
|
// 动态裁剪内容以适应剩余空间
|
||||||
|
let finalContent = result.content;
|
||||||
|
let wasTruncated = result.totalPages > MAX_PDF_PAGES;
|
||||||
|
|
||||||
|
// 如果超过总内容限制,裁剪内容
|
||||||
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
||||||
|
if (result.content.length > remainingSpace && remainingSpace > 1000) {
|
||||||
|
finalContent = result.content.substring(0, remainingSpace);
|
||||||
|
wasTruncated = true;
|
||||||
|
} else if (remainingSpace <= 1000) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalContentLength += finalContent.length;
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
uid: crypto.randomUUID(),
|
||||||
|
name: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
file,
|
||||||
|
maxWidth: '200px',
|
||||||
|
showDelIcon: true,
|
||||||
|
imgPreview: false,
|
||||||
|
isUploaded: true,
|
||||||
|
fileContent: finalContent,
|
||||||
|
fileType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
if (wasTruncated) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`PDF 解析: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 总页数: ${result.totalPages}, 已提取: ${result.extractedPages} 页, 内容长度: ${finalContent.length} 字符`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('解析 PDF 失败:', error);
|
||||||
|
ElMessage.error(`${file.name} 解析失败`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理文本文件
|
||||||
|
else if (isText) {
|
||||||
|
try {
|
||||||
|
// 读取文本文件内容
|
||||||
|
const content = await readTextFile(file);
|
||||||
|
|
||||||
|
// 限制单个文本文件长度
|
||||||
|
let finalContent = content;
|
||||||
|
let truncated = false;
|
||||||
|
if (content.length > MAX_TEXT_FILE_LENGTH) {
|
||||||
|
finalContent = content.substring(0, MAX_TEXT_FILE_LENGTH);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态裁剪内容以适应剩余空间
|
||||||
|
const remainingSpace = MAX_TOTAL_CONTENT_LENGTH - totalContentLength;
|
||||||
|
if (finalContent.length > remainingSpace && remainingSpace > 1000) {
|
||||||
|
finalContent = finalContent.substring(0, remainingSpace);
|
||||||
|
truncated = true;
|
||||||
|
} else if (remainingSpace <= 1000) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.error(`${file.name} (${fileSizeKB}KB) 会超过总内容限制,已跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalContentLength += finalContent.length;
|
||||||
|
|
||||||
|
arr.push({
|
||||||
|
uid: crypto.randomUUID(),
|
||||||
|
name: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
file,
|
||||||
|
maxWidth: '200px',
|
||||||
|
showDelIcon: true,
|
||||||
|
imgPreview: false,
|
||||||
|
isUploaded: true,
|
||||||
|
fileContent: finalContent,
|
||||||
|
fileType: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
if (truncated) {
|
||||||
|
const fileSizeKB = (file.size / 1024).toFixed(2);
|
||||||
|
ElMessage.warning(`${file.name} (${fileSizeKB}KB) 内容过大,已自动截取部分内容`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`文本文件读取: ${file.name} - 大小: ${(file.size / 1024).toFixed(2)}KB, 内容长度: ${content.length} 字符`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('读取文件失败:', error);
|
||||||
|
ElMessage.error(`${file.name} 读取失败`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不支持的文件类型
|
||||||
|
else {
|
||||||
|
ElMessage.warning(`${file.name} 不是支持的文件类型`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arr.length > 0) {
|
||||||
filesStore.setFilesList([...filesStore.filesList, ...arr]);
|
filesStore.setFilesList([...filesStore.filesList, ...arr]);
|
||||||
|
ElMessage.success(`已添加 ${arr.length} 个文件`);
|
||||||
|
}
|
||||||
|
|
||||||
// 重置文件选择器
|
// 重置文件选择器
|
||||||
nextTick(() => reset());
|
nextTick(() => reset());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开文件选择对话框
|
||||||
|
*/
|
||||||
function handleUploadFiles() {
|
function handleUploadFiles() {
|
||||||
open();
|
open();
|
||||||
popoverRef.value.hide();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="files-select">
|
<div class="files-select">
|
||||||
<Popover
|
<!-- 直接点击上传,添加 tooltip 提示 -->
|
||||||
ref="popoverRef"
|
<el-tooltip
|
||||||
placement="top-start"
|
content="上传文件或图片(支持 Excel、Word、PDF、代码文件等,最大3MB)"
|
||||||
:offset="[4, 0]"
|
placement="top"
|
||||||
popover-class="popover-content"
|
|
||||||
:popover-style="popoverStyle"
|
|
||||||
trigger="clickTarget"
|
|
||||||
>
|
>
|
||||||
<template #trigger>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
class="flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px border-1px border-[rgba(0,0,0,0.08)] border-solid hover:bg-[rgba(0,0,0,.04)]"
|
||||||
|
@click="handleUploadFiles"
|
||||||
>
|
>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Paperclip />
|
<Paperclip />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</el-tooltip>
|
||||||
|
|
||||||
<div class="popover-content-box">
|
|
||||||
<div
|
|
||||||
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
|
||||||
@click="handleUploadFiles"
|
|
||||||
>
|
|
||||||
<el-icon>
|
|
||||||
<Upload />
|
|
||||||
</el-icon>
|
|
||||||
<div class="font-size-14px">
|
|
||||||
上传文件或图片
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
placement="right-end"
|
|
||||||
:offset="[8, 4]"
|
|
||||||
popover-class="popover-content"
|
|
||||||
:popover-style="popoverStyle"
|
|
||||||
trigger="hover"
|
|
||||||
:hover-delay="100"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<div
|
|
||||||
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
|
||||||
>
|
|
||||||
<SvgIcon name="code" size="16" />
|
|
||||||
<div class="font-size-14px">
|
|
||||||
上传代码
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-icon class="ml-auto">
|
|
||||||
<ArrowRight />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="popover-content-box">
|
|
||||||
<div
|
|
||||||
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
ElMessage.warning('暂未开放');
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
代码文件
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="popover-content-item flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-14px hover:bg-[rgba(0,0,0,.04)]"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
ElMessage.warning('暂未开放');
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
代码文件夹
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -510,7 +510,12 @@ function onClose() {
|
|||||||
|
|
||||||
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
|
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
|
||||||
<div>
|
<div>
|
||||||
<p>充值后,加客服微信回复账号名,可专享vip售后服务</p>
|
<p style="color: #f97316;font-weight: 800">
|
||||||
|
全站任意充值,每累计充值10元永久优惠尊享包10元,最高可优惠50元
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 10px;">
|
||||||
|
充值后,加客服微信回复账号名,可专享vip售后服务
|
||||||
|
</p>
|
||||||
<p style="margin-top: 10px;">
|
<p style="margin-top: 10px;">
|
||||||
客服微信号:chengzilaoge520 或扫描右侧二维码
|
客服微信号:chengzilaoge520 或扫描右侧二维码
|
||||||
</p>
|
</p>
|
||||||
@@ -692,7 +697,13 @@ function onClose() {
|
|||||||
|
|
||||||
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
|
<div style="display: flex;justify-content: space-between;margin-top: 15px;">
|
||||||
<div>
|
<div>
|
||||||
<p>充值后,加客服微信回复账号名,可专享vip售后服务</p>
|
<p style="color: #f97316;font-weight: 800">
|
||||||
|
全站任意充值,每累计充值10元永久优惠尊享包10元,最高可优惠50元
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 10px;">
|
||||||
|
充值后,加客服微信回复账号名,可专享vip售后服务
|
||||||
|
</p>
|
||||||
<p style="margin-top: 10px;">
|
<p style="margin-top: 10px;">
|
||||||
客服微信号:chengzilaoge520 或扫描右侧二维码
|
客服微信号:chengzilaoge520 或扫描右侧二维码
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ function generateShareContent(): string {
|
|||||||
👉 点击链接立即参与我的专属邀请码链接:
|
👉 点击链接立即参与我的专属邀请码链接:
|
||||||
${shareLink}
|
${shareLink}
|
||||||
|
|
||||||
🍀 未注册用户,微信扫码登录,进入用户中心👉每周邀请 即可立即参与!`;
|
🍀 未注册用户,微信扫码登录,进入控制台👉每周邀请 即可立即参与!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export function useGuideTour() {
|
|||||||
{
|
{
|
||||||
element: '[data-tour="user-avatar"]',
|
element: '[data-tour="user-avatar"]',
|
||||||
popover: {
|
popover: {
|
||||||
title: '用户中心',
|
title: '控制台',
|
||||||
description: '点击头像可以进入用户中心,管理您的账户信息、查看使用统计、API密钥等。接下来将为您详细介绍用户中心的各项功能。',
|
description: '点击头像可以进入控制台,管理您的账户信息、查看使用统计、API密钥等。接下来将为您详细介绍用户中心的各项功能。',
|
||||||
side: 'bottom',
|
side: 'bottom',
|
||||||
align: 'end',
|
align: 'end',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
.layout-blank{
|
.layout-blank{
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
//margin: 20px ;
|
|
||||||
}
|
}
|
||||||
/* 无样式 */
|
/* 无样式 */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ useWindowWidthObserver();
|
|||||||
|
|
||||||
// 应用加载时检查是否需要显示公告弹窗
|
// 应用加载时检查是否需要显示公告弹窗
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('announcementStore.shouldShowDialog--', announcementStore.shouldShowDialog);
|
|
||||||
// 检查是否应该显示弹窗(只有"关闭一周"且未超过7天才不显示)
|
// 检查是否应该显示弹窗(只有"关闭一周"且未超过7天才不显示)
|
||||||
// 数据获取已移至 SystemAnnouncementDialog 组件内部,每次打开弹窗时都会获取最新数据
|
// 数据获取已移至 SystemAnnouncementDialog 组件内部,每次打开弹窗时都会获取最新数据
|
||||||
if (announcementStore.shouldShowDialog) {
|
if (announcementStore.shouldShowDialog) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function openTutorial() {
|
|||||||
@click="openTutorial"
|
@click="openTutorial"
|
||||||
>
|
>
|
||||||
<!-- PC端显示文字 -->
|
<!-- PC端显示文字 -->
|
||||||
<span class="pc-text">AI使用教程</span>
|
<span class="pc-text">文档</span>
|
||||||
<!-- 移动端显示图标 -->
|
<!-- 移动端显示图标 -->
|
||||||
<svg
|
<svg
|
||||||
class="mobile-icon w-6 h-6"
|
class="mobile-icon w-6 h-6"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Bell } from '@element-plus/icons-vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useAnnouncementStore } from '@/stores';
|
import { useAnnouncementStore } from '@/stores';
|
||||||
|
|
||||||
@@ -30,14 +29,27 @@ function openAnnouncement() {
|
|||||||
<!-- :max="99" -->
|
<!-- :max="99" -->
|
||||||
<div
|
<div
|
||||||
class="announcement-btn"
|
class="announcement-btn"
|
||||||
|
title="查看公告"
|
||||||
@click="openAnnouncement"
|
@click="openAnnouncement"
|
||||||
>
|
>
|
||||||
<!-- PC端显示文字 -->
|
<!-- PC端显示文字 -->
|
||||||
<span class="pc-text">公告</span>
|
<span class="pc-text">公告</span>
|
||||||
<!-- 移动端显示图标 -->
|
<!-- 移动端显示图标 -->
|
||||||
<el-icon class="mobile-icon" :size="20">
|
<svg
|
||||||
<Bell />
|
class="mobile-icon"
|
||||||
</el-icon>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
|
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</el-badge>
|
</el-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
import { ChatLineRound } from '@element-plus/icons-vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import Popover from '@/components/Popover/index.vue';
|
import Popover from '@/components/Popover/index.vue';
|
||||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||||
import { useGuideTourStore, useUserStore } from '@/stores';
|
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores';
|
||||||
import { useSessionStore } from '@/stores/modules/session';
|
import { useSessionStore } from '@/stores/modules/session';
|
||||||
import { showProductPackage } from '@/utils/product-package';
|
|
||||||
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -17,15 +16,9 @@ const router = useRouter();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const guideTourStore = useGuideTourStore();
|
const guideTourStore = useGuideTourStore();
|
||||||
|
const announcementStore = useAnnouncementStore();
|
||||||
const { startUserCenterTour } = useGuideTour();
|
const { startUserCenterTour } = useGuideTour();
|
||||||
|
|
||||||
// const src = computed(
|
|
||||||
// () => userStore.userInfo?.avatar ?? 'https://avatars.githubusercontent.com/u/76239030',
|
|
||||||
// );
|
|
||||||
const src = computed(
|
|
||||||
() => userStore.userInfo?.user?.icon ? `${import.meta.env.VITE_WEB_BASE_API}/file/${userStore.userInfo.user.icon}` : `@/assets/images/logo.png`,
|
|
||||||
);
|
|
||||||
|
|
||||||
/* 弹出面板 开始 */
|
/* 弹出面板 开始 */
|
||||||
const popoverStyle = ref({
|
const popoverStyle = ref({
|
||||||
width: '200px',
|
width: '200px',
|
||||||
@@ -36,21 +29,32 @@ const popoverRef = ref();
|
|||||||
|
|
||||||
// 弹出面板内容
|
// 弹出面板内容
|
||||||
const popoverList = ref([
|
const popoverList = ref([
|
||||||
// {
|
|
||||||
// key: '1',
|
|
||||||
// title: '收藏夹',
|
|
||||||
// icon: 'book-mark-fill',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// key: '2',
|
|
||||||
// title: '设置',
|
|
||||||
// icon: 'settings-4-fill',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: '5',
|
key: '5',
|
||||||
title: '用户中心',
|
title: '控制台',
|
||||||
icon: 'settings-4-fill',
|
icon: 'settings-4-fill',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '7',
|
||||||
|
title: '公告',
|
||||||
|
icon: 'notification-fill',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '8',
|
||||||
|
title: '模型库',
|
||||||
|
icon: 'apps-fill',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '9',
|
||||||
|
title: '文档',
|
||||||
|
icon: 'book-fill',
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: '6',
|
key: '6',
|
||||||
title: '新手引导',
|
title: '新手引导',
|
||||||
@@ -126,6 +130,21 @@ function handleClick(item: any) {
|
|||||||
case '6':
|
case '6':
|
||||||
handleStartTutorial();
|
handleStartTutorial();
|
||||||
break;
|
break;
|
||||||
|
case '7':
|
||||||
|
// 打开公告
|
||||||
|
popoverRef.value?.hide?.();
|
||||||
|
announcementStore.openDialog();
|
||||||
|
break;
|
||||||
|
case '8':
|
||||||
|
// 打开模型库
|
||||||
|
popoverRef.value?.hide?.();
|
||||||
|
router.push('/model-library');
|
||||||
|
break;
|
||||||
|
case '9':
|
||||||
|
// 打开文档
|
||||||
|
popoverRef.value?.hide?.();
|
||||||
|
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||||
|
break;
|
||||||
case '4':
|
case '4':
|
||||||
popoverRef.value?.hide?.();
|
popoverRef.value?.hide?.();
|
||||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||||
@@ -200,11 +219,6 @@ function openVipGuide() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹出面板 结束 */
|
|
||||||
function onProductPackage() {
|
|
||||||
showProductPackage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
|
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
|
||||||
watch(dialogVisible, (newVal) => {
|
watch(dialogVisible, (newVal) => {
|
||||||
if (newVal && externalInviteCode.value) {
|
if (newVal && externalInviteCode.value) {
|
||||||
@@ -287,19 +301,17 @@ watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ 暴露方法供外部调用 ============
|
||||||
|
defineExpose({
|
||||||
|
openDialog,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 ">
|
||||||
<el-button
|
|
||||||
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
|
|
||||||
data-tour="buy-btn"
|
|
||||||
@click="onProductPackage"
|
|
||||||
>
|
|
||||||
<span>立即购买</span>
|
|
||||||
</el-button>
|
|
||||||
<!-- 用户信息区域 -->
|
<!-- 用户信息区域 -->
|
||||||
<div class=" cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="onProductPackage">
|
<div class="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openDialog">
|
||||||
<div class="text-sm font-semibold text-gray-800">
|
<div class="text-sm font-semibold text-gray-800">
|
||||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +394,7 @@ watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
|||||||
</div>
|
</div>
|
||||||
<nav-dialog
|
<nav-dialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
title="用户中心"
|
title="控制台"
|
||||||
:nav-items="navItems"
|
:nav-items="navItems"
|
||||||
:default-active="activeNav"
|
:default-active="activeNav"
|
||||||
@confirm="handleConfirm"
|
@confirm="handleConfirm"
|
||||||
@@ -453,44 +465,4 @@ watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-btn {
|
|
||||||
background: linear-gradient(90deg, #FFD700, #FFC107);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
|
|
||||||
background: linear-gradient(90deg, #FFC107, #FFD700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-rocket {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-bounce {
|
|
||||||
animation: bounce 1.2s infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//移动端,屏幕小于756px
|
|
||||||
@media screen and (max-width: 756px) {
|
|
||||||
.buy-btn {
|
|
||||||
background: linear-gradient(90deg, #FFD700, #FFC107);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 9999px;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
font-size: 12px;
|
|
||||||
max-width: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-4px); }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { showProductPackage } from '@/utils/product-package';
|
||||||
|
|
||||||
|
// 点击购买按钮
|
||||||
|
function onProductPackage() {
|
||||||
|
showProductPackage();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="buy-btn-container">
|
||||||
|
<el-button
|
||||||
|
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
|
||||||
|
data-tour="buy-btn"
|
||||||
|
@click="onProductPackage"
|
||||||
|
>
|
||||||
|
<span>立即购买</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.buy-btn-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 22px 0 0;
|
||||||
|
|
||||||
|
.buy-btn {
|
||||||
|
background: linear-gradient(90deg, #FFD700, #FFC107);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
|
||||||
|
background: linear-gradient(90deg, #FFC107, #FFD700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-bounce {
|
||||||
|
animation: bounce 1.2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端,屏幕小于756px
|
||||||
|
@media screen and (max-width: 756px) {
|
||||||
|
.buy-btn-container {
|
||||||
|
margin: 0 ;
|
||||||
|
|
||||||
|
.buy-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 60px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-4px); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '@/stores';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 打开用户中心对话框(通过调用 Avatar 组件的方法)
|
||||||
|
function openConsole() {
|
||||||
|
// 触发事件,由父组件处理
|
||||||
|
emit('open-console');
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['open-console']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="console-btn-container" data-tour="console-btn">
|
||||||
|
<div
|
||||||
|
class="console-btn"
|
||||||
|
title="打开控制台"
|
||||||
|
@click="openConsole"
|
||||||
|
>
|
||||||
|
<!-- PC端显示文字 -->
|
||||||
|
<span class="pc-text">控制台</span>
|
||||||
|
<!-- 移动端显示图标 -->
|
||||||
|
<svg
|
||||||
|
class="mobile-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||||
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.console-btn-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.console-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #606266;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #909399;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PC端显示文字,隐藏图标
|
||||||
|
.pc-text {
|
||||||
|
display: inline;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端显示图标,隐藏文字
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.console-btn-container {
|
||||||
|
.console-btn {
|
||||||
|
.pc-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-icon {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,7 +7,9 @@ import { useSessionStore } from '@/stores/modules/session';
|
|||||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||||
import Avatar from './components/Avatar.vue';
|
import Avatar from './components/Avatar.vue';
|
||||||
|
import BuyBtn from './components/BuyBtn.vue';
|
||||||
import Collapse from './components/Collapse.vue';
|
import Collapse from './components/Collapse.vue';
|
||||||
|
import ConsoleBtn from './components/ConsoleBtn.vue';
|
||||||
import CreateChat from './components/CreateChat.vue';
|
import CreateChat from './components/CreateChat.vue';
|
||||||
import LoginBtn from './components/LoginBtn.vue';
|
import LoginBtn from './components/LoginBtn.vue';
|
||||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||||
@@ -17,6 +19,8 @@ const userStore = useUserStore();
|
|||||||
const designStore = useDesignStore();
|
const designStore = useDesignStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
|
const avatarRef = ref();
|
||||||
|
|
||||||
const currentSession = computed(() => sessionStore.currentSession);
|
const currentSession = computed(() => sessionStore.currentSession);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -43,6 +47,11 @@ function handleCtrlK(event: KeyboardEvent) {
|
|||||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||||
passive: false,
|
passive: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 打开控制台
|
||||||
|
function handleOpenConsole() {
|
||||||
|
avatarRef.value?.openDialog?.();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -75,7 +84,9 @@ onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtr
|
|||||||
<AnnouncementBtn />
|
<AnnouncementBtn />
|
||||||
<ModelLibraryBtn />
|
<ModelLibraryBtn />
|
||||||
<AiTutorialBtn />
|
<AiTutorialBtn />
|
||||||
<Avatar v-show="userStore.userInfo" />
|
<ConsoleBtn @open-console="handleOpenConsole" />
|
||||||
|
<BuyBtn v-show="userStore.userInfo" />
|
||||||
|
<Avatar v-show="userStore.userInfo" ref="avatarRef" />
|
||||||
<LoginBtn v-show="!userStore.userInfo" />
|
<LoginBtn v-show="!userStore.userInfo" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,8 +42,12 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-show="layout === 'blankPage'">
|
||||||
<component :is="LayoutComponent[layout]" />
|
<LayoutBlankPage />
|
||||||
|
<!-- <component :is="LayoutComponent[layout]" /> -->
|
||||||
|
</div>
|
||||||
|
<div v-show="layout !== 'blankPage'">
|
||||||
|
<LayoutVertical />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ElMessage } from 'element-plus';
|
|||||||
import { nextTick, ref, watch } from 'vue';
|
import { nextTick, ref, watch } from 'vue';
|
||||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||||
import WelecomeText from '@/components/WelecomeText/index.vue';
|
import WelecomeText from '@/components/WelecomeText/index.vue';
|
||||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
|
||||||
import { useGuideTourStore, useUserStore } from '@/stores';
|
import { useGuideTourStore, useUserStore } from '@/stores';
|
||||||
import { useFilesStore } from '@/stores/modules/files';
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
|
|
||||||
@@ -135,6 +134,8 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||||
|
<FilesSelect />
|
||||||
|
|
||||||
<ModelSelect />
|
<ModelSelect />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
|||||||
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
||||||
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
||||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||||||
import { ArrowLeftBold, ArrowRightBold, Loading } from '@element-plus/icons-vue';
|
import { ArrowLeftBold, ArrowRightBold, Document, Loading } from '@element-plus/icons-vue';
|
||||||
import { ElIcon, ElMessage } from 'element-plus';
|
import { ElIcon, ElMessage } from 'element-plus';
|
||||||
import { useHookFetch } from 'hook-fetch/vue';
|
import { useHookFetch } from 'hook-fetch/vue';
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
@@ -13,7 +13,6 @@ import { Sender } from 'vue-element-plus-x';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { send } from '@/api';
|
import { send } from '@/api';
|
||||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
|
||||||
import { useGuideTourStore } from '@/stores';
|
import { useGuideTourStore } from '@/stores';
|
||||||
import { useChatStore } from '@/stores/modules/chat';
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
import { useFilesStore } from '@/stores/modules/files';
|
import { useFilesStore } from '@/stores/modules/files';
|
||||||
@@ -31,6 +30,8 @@ type MessageItem = BubbleProps & {
|
|||||||
thinkingStatus?: ThinkingStatus;
|
thinkingStatus?: ThinkingStatus;
|
||||||
thinlCollapse?: boolean;
|
thinlCollapse?: boolean;
|
||||||
reasoning_content?: string;
|
reasoning_content?: string;
|
||||||
|
images?: Array<{ url: string; name?: string }>; // 用户消息中的图片列表
|
||||||
|
files?: Array<{ name: string; size: number }>; // 用户消息中的文件列表
|
||||||
};
|
};
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -115,7 +116,11 @@ watch(
|
|||||||
{ immediate: true, deep: true },
|
{ immediate: true, deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 封装数据处理逻辑
|
/**
|
||||||
|
* 处理流式响应的数据块
|
||||||
|
* 解析 AI 返回的数据,更新消息内容和思考状态
|
||||||
|
* @param {AnyObject} chunk - 流式响应的数据块
|
||||||
|
*/
|
||||||
function handleDataChunk(chunk: AnyObject) {
|
function handleDataChunk(chunk: AnyObject) {
|
||||||
try {
|
try {
|
||||||
// 安全获取 delta 和 content
|
// 安全获取 delta 和 content
|
||||||
@@ -171,34 +176,130 @@ function handleDataChunk(chunk: AnyObject) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封装错误处理逻辑
|
/**
|
||||||
|
* 处理错误信息
|
||||||
|
* @param {any} err - 错误对象
|
||||||
|
*/
|
||||||
function handleError(err: any) {
|
function handleError(err: any) {
|
||||||
console.error('Fetch error:', err);
|
console.error('Fetch error:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息并处理流式响应
|
||||||
|
* 支持发送文本、图片和文件
|
||||||
|
* @param {string} chatContent - 用户输入的文本内容
|
||||||
|
*/
|
||||||
async function startSSE(chatContent: string) {
|
async function startSSE(chatContent: string) {
|
||||||
if (isSending.value)
|
if (isSending.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// 检查是否有未上传完成的文件
|
||||||
|
const hasUnuploadedFiles = filesStore.filesList.some(f => !f.isUploaded);
|
||||||
|
if (hasUnuploadedFiles) {
|
||||||
|
ElMessage.warning('文件正在上传中,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isSending.value = true;
|
isSending.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 清空输入框
|
// 清空输入框
|
||||||
inputValue.value = '';
|
inputValue.value = '';
|
||||||
addMessage(chatContent, true);
|
|
||||||
|
// 获取当前上传的图片和文件(在清空之前保存)
|
||||||
|
const imageFiles = filesStore.filesList.filter(f => f.isUploaded && f.fileType === 'image');
|
||||||
|
const textFiles = filesStore.filesList.filter(f => f.isUploaded && f.fileType === 'text');
|
||||||
|
|
||||||
|
const images = imageFiles.map(f => ({
|
||||||
|
url: f.base64!, // 使用base64作为URL
|
||||||
|
name: f.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const files = textFiles.map(f => ({
|
||||||
|
name: f.name!,
|
||||||
|
size: f.fileSize!,
|
||||||
|
}));
|
||||||
|
|
||||||
|
addMessage(chatContent, true, images, files);
|
||||||
addMessage('', false);
|
addMessage('', false);
|
||||||
|
|
||||||
|
// 立即清空文件列表(不要等到响应完成)
|
||||||
|
filesStore.clearFilesList();
|
||||||
|
|
||||||
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
// 这里有必要调用一下 BubbleList 组件的滚动到底部 手动触发 自动滚动
|
||||||
bubbleListRef.value?.scrollToBottom();
|
bubbleListRef.value?.scrollToBottom();
|
||||||
|
|
||||||
|
// 组装消息内容,支持图片和文件
|
||||||
|
const messagesContent = bubbleItems.value.slice(0, -1).slice(-6).map((item: MessageItem) => {
|
||||||
|
const baseMessage: any = {
|
||||||
|
role: item.role,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果是用户消息且有附件(图片或文件),组装成数组格式
|
||||||
|
if (item.role === 'user' && item.key === bubbleItems.value.length - 2) {
|
||||||
|
// 当前发送的消息
|
||||||
|
const contentArray: any[] = [];
|
||||||
|
|
||||||
|
// 添加文本内容
|
||||||
|
if (item.content) {
|
||||||
|
contentArray.push({
|
||||||
|
type: 'text',
|
||||||
|
text: item.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文本文件内容(使用XML格式)
|
||||||
|
if (textFiles.length > 0) {
|
||||||
|
let fileContent = '\n\n';
|
||||||
|
textFiles.forEach((fileItem, index) => {
|
||||||
|
fileContent += `<ATTACHMENT_FILE>\n`;
|
||||||
|
fileContent += `<FILE_INDEX>File ${index + 1}</FILE_INDEX>\n`;
|
||||||
|
fileContent += `<FILE_NAME>${fileItem.name}</FILE_NAME>\n`;
|
||||||
|
fileContent += `<FILE_CONTENT>\n${fileItem.fileContent}\n</FILE_CONTENT>\n`;
|
||||||
|
fileContent += `</ATTACHMENT_FILE>\n`;
|
||||||
|
if (index < textFiles.length - 1) {
|
||||||
|
fileContent += '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
contentArray.push({
|
||||||
|
type: 'text',
|
||||||
|
text: fileContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加图片内容(使用之前保存的 imageFiles)
|
||||||
|
imageFiles.forEach((fileItem) => {
|
||||||
|
if (fileItem.base64) {
|
||||||
|
contentArray.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: fileItem.base64, // 使用base64
|
||||||
|
name: fileItem.name, // 保存图片名称
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有图片或文件,使用数组格式
|
||||||
|
if (contentArray.length > 1 || imageFiles.length > 0 || textFiles.length > 0) {
|
||||||
|
baseMessage.content = contentArray;
|
||||||
|
} else {
|
||||||
|
baseMessage.content = item.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他消息保持原样
|
||||||
|
baseMessage.content = (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
|
||||||
|
? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
|
||||||
|
: item.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseMessage;
|
||||||
|
});
|
||||||
|
|
||||||
// 使用 for-await 处理流式响应
|
// 使用 for-await 处理流式响应
|
||||||
for await (const chunk of stream({
|
for await (const chunk of stream({
|
||||||
messages: bubbleItems.value.slice(0, -1).slice(-6).map((item: MessageItem) => ({
|
messages: messagesContent,
|
||||||
role: item.role,
|
|
||||||
content: (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
|
|
||||||
? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
|
|
||||||
: item.content,
|
|
||||||
})),
|
|
||||||
sessionId: route.params?.id !== 'not_login' ? String(route.params?.id) : 'not_login',
|
sessionId: route.params?.id !== 'not_login' ? String(route.params?.id) : 'not_login',
|
||||||
stream: true,
|
stream: true,
|
||||||
userId: userStore.userInfo?.userId,
|
userId: userStore.userInfo?.userId,
|
||||||
@@ -228,10 +329,18 @@ async function startSSE(chatContent: string) {
|
|||||||
latest.thinkingStatus = 'end';
|
latest.thinkingStatus = 'end';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存聊天记录到 chatMap(本地缓存,刷新后可恢复)
|
||||||
|
if (route.params?.id && route.params.id !== 'not_login') {
|
||||||
|
chatStore.chatMap[`${route.params.id}`] = bubbleItems.value as any;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 中断请求
|
/**
|
||||||
|
* 中断正在进行的请求
|
||||||
|
* 停止流式响应并重置状态
|
||||||
|
*/
|
||||||
async function cancelSSE() {
|
async function cancelSSE() {
|
||||||
try {
|
try {
|
||||||
cancel(); // 直接调用,无需参数
|
cancel(); // 直接调用,无需参数
|
||||||
@@ -250,8 +359,14 @@ async function cancelSSE() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加消息 - 维护聊天记录
|
/**
|
||||||
function addMessage(message: string, isUser: boolean) {
|
* 添加消息到聊天列表
|
||||||
|
* @param {string} message - 消息内容
|
||||||
|
* @param {boolean} isUser - 是否为用户消息
|
||||||
|
* @param {Array<{url: string, name?: string}>} images - 图片列表(可选)
|
||||||
|
* @param {Array<{name: string, size: number}>} files - 文件列表(可选)
|
||||||
|
*/
|
||||||
|
function addMessage(message: string, isUser: boolean, images?: Array<{ url: string; name?: string }>, files?: Array<{ name: string; size: number }>) {
|
||||||
const i = bubbleItems.value.length;
|
const i = bubbleItems.value.length;
|
||||||
const obj: MessageItem = {
|
const obj: MessageItem = {
|
||||||
key: i,
|
key: i,
|
||||||
@@ -268,14 +383,26 @@ function addMessage(message: string, isUser: boolean) {
|
|||||||
thinkingStatus: 'start',
|
thinkingStatus: 'start',
|
||||||
thinlCollapse: false,
|
thinlCollapse: false,
|
||||||
noStyle: !isUser,
|
noStyle: !isUser,
|
||||||
|
images: images && images.length > 0 ? images : undefined,
|
||||||
|
files: files && files.length > 0 ? files : undefined,
|
||||||
};
|
};
|
||||||
bubbleItems.value.push(obj);
|
bubbleItems.value.push(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 展开收起 事件展示
|
/**
|
||||||
|
* 处理思考链展开/收起状态变化
|
||||||
|
* @param {Object} payload - 状态变化的载荷
|
||||||
|
* @param {boolean} payload.value - 展开/收起状态
|
||||||
|
* @param {ThinkingStatus} payload.status - 思考状态
|
||||||
|
*/
|
||||||
function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
|
function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件卡片
|
||||||
|
* @param {FilesCardProps} _item - 文件卡片项(未使用)
|
||||||
|
* @param {number} index - 要删除的文件索引
|
||||||
|
*/
|
||||||
function handleDeleteCard(_item: FilesCardProps, index: number) {
|
function handleDeleteCard(_item: FilesCardProps, index: number) {
|
||||||
filesStore.deleteFileByIndex(index);
|
filesStore.deleteFileByIndex(index);
|
||||||
}
|
}
|
||||||
@@ -296,12 +423,24 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 复制
|
/**
|
||||||
|
* 复制消息内容到剪贴板
|
||||||
|
* @param {any} item - 消息项
|
||||||
|
*/
|
||||||
function copy(item: any) {
|
function copy(item: any) {
|
||||||
navigator.clipboard.writeText(item.content || '')
|
navigator.clipboard.writeText(item.content || '')
|
||||||
.then(() => ElMessage.success('已复制到剪贴板'))
|
.then(() => ElMessage.success('已复制到剪贴板'))
|
||||||
.catch(() => ElMessage.error('复制失败'));
|
.catch(() => ElMessage.error('复制失败'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片预览
|
||||||
|
* 在新窗口中打开图片
|
||||||
|
* @param {string} url - 图片 URL
|
||||||
|
*/
|
||||||
|
function handleImagePreview(url: string) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -318,10 +457,37 @@ function copy(item: any) {
|
|||||||
<template #content="{ item }">
|
<template #content="{ item }">
|
||||||
<!-- chat 内容走 markdown -->
|
<!-- chat 内容走 markdown -->
|
||||||
<YMarkdown v-if="item.content && (item.role === 'assistant' || item.role === 'system')" class="markdown-body" :markdown="item.content" :themes="{ light: 'github-light', dark: 'github-dark' }" default-theme-mode="dark" />
|
<YMarkdown v-if="item.content && (item.role === 'assistant' || item.role === 'system')" class="markdown-body" :markdown="item.content" :themes="{ light: 'github-light', dark: 'github-dark' }" default-theme-mode="dark" />
|
||||||
<!-- user 内容 纯文本 -->
|
<!-- user 内容 纯文本 + 图片 + 文件 -->
|
||||||
<div v-if="item.content && item.role === 'user'" class="user-content">
|
<div v-if="item.role === 'user'" class="user-content-wrapper">
|
||||||
|
<!-- 图片列表 -->
|
||||||
|
<div v-if="item.images && item.images.length > 0" class="user-images">
|
||||||
|
<img
|
||||||
|
v-for="(image, index) in item.images"
|
||||||
|
:key="index"
|
||||||
|
:src="image.url"
|
||||||
|
:alt="image.name || '图片'"
|
||||||
|
class="user-image"
|
||||||
|
@click="() => handleImagePreview(image.url)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div v-if="item.files && item.files.length > 0" class="user-files">
|
||||||
|
<div
|
||||||
|
v-for="(file, index) in item.files"
|
||||||
|
:key="index"
|
||||||
|
class="user-file-item"
|
||||||
|
>
|
||||||
|
<el-icon class="file-icon">
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
<span class="file-name">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 文本内容 -->
|
||||||
|
<div v-if="item.content" class="user-content">
|
||||||
{{ item.content }}
|
{{ item.content }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 自定义底部 -->
|
<!-- 自定义底部 -->
|
||||||
@@ -376,7 +542,7 @@ function copy(item: any) {
|
|||||||
</template>
|
</template>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||||
<!-- <FilesSelect /> -->
|
<FilesSelect />
|
||||||
<ModelSelect />
|
<ModelSelect />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -422,6 +588,57 @@ function copy(item: any) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
.user-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.user-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.user-image {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.user-files {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.user-file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
.file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.file-size {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.user-content {
|
.user-content {
|
||||||
// 换行
|
// 换行
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
175
Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/上传文件与图片需求.text
Normal file
175
Yi.Ai.Vue3/src/pages/chat/layouts/chatWithId/上传文件与图片需求.text
Normal file
File diff suppressed because one or more lines are too long
@@ -188,8 +188,8 @@ function contactCustomerService() {
|
|||||||
|
|
||||||
<!-- 更多信息提示 -->
|
<!-- 更多信息提示 -->
|
||||||
<div class="mb-6 text-gray-600 text-sm">
|
<div class="mb-6 text-gray-600 text-sm">
|
||||||
更多订单信息和会员详情<br>请前往 <strong>用户中心 → 充值记录</strong> 查看。<br>
|
更多订单信息和会员详情<br>请前往 <strong>控制台 → 充值记录</strong> 查看。<br>
|
||||||
用户中心在首页右上角个人头像点击下拉菜单。
|
控制台在首页右上角个人头像点击下拉菜单。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 重新登录提示 -->
|
<!-- 重新登录提示 -->
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/pages/products/index.vue'),
|
component: () => import('@/pages/products/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '产品页面',
|
title: '产品页面',
|
||||||
keepAlive: true,
|
keepAlive: 0,
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
},
|
},
|
||||||
@@ -49,7 +49,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/pages/modelLibrary/index.vue'),
|
component: () => import('@/pages/modelLibrary/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '模型库',
|
title: '模型库',
|
||||||
keepAlive: true,
|
keepAlive: 0,
|
||||||
isDefaultChat: false,
|
isDefaultChat: false,
|
||||||
layout: 'blankPage',
|
layout: 'blankPage',
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/pages/payResult/index.vue'),
|
component: () => import('@/pages/payResult/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '支付结果',
|
title: '支付结果',
|
||||||
keepAlive: true, // 如果需要缓存
|
keepAlive: 0, // 如果需要缓存
|
||||||
isDefaultChat: false, // 根据实际情况设置
|
isDefaultChat: false, // 根据实际情况设置
|
||||||
layout: 'blankPage', // 如果需要自定义布局
|
layout: 'blankPage', // 如果需要自定义布局
|
||||||
},
|
},
|
||||||
@@ -88,6 +88,7 @@ export const layoutRouter: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// staticRouter[静态路由] 预留
|
// staticRouter[静态路由] 预留
|
||||||
|
|||||||
@@ -17,17 +17,101 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
// 会议ID对应-聊天记录 map对象
|
// 会议ID对应-聊天记录 map对象
|
||||||
const chatMap = ref<Record<string, ChatMessageVo[]>>({});
|
const chatMap = ref<Record<string, ChatMessageVo[]>>({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析消息内容,提取文本、图片和文件信息
|
||||||
|
* @param content - 消息内容,可能是字符串或数组格式的JSON字符串
|
||||||
|
* @returns 解析后的文本内容、图片列表和文件列表
|
||||||
|
*/
|
||||||
|
function parseMessageContent(content: string | any): {
|
||||||
|
text: string;
|
||||||
|
images: Array<{ url: string; name?: string }>;
|
||||||
|
files: Array<{ name: string; size: number }>;
|
||||||
|
} {
|
||||||
|
let text = '';
|
||||||
|
const images: Array<{ url: string; name?: string }> = [];
|
||||||
|
const files: Array<{ name: string; size: number }> = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果 content 是字符串,尝试解析为 JSON
|
||||||
|
let contentArray: any;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
// 尝试解析 JSON 数组格式
|
||||||
|
if (content.trim().startsWith('[')) {
|
||||||
|
contentArray = JSON.parse(content);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 普通文本
|
||||||
|
text = content;
|
||||||
|
return { text, images, files };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
contentArray = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是数组,直接返回
|
||||||
|
if (!Array.isArray(contentArray)) {
|
||||||
|
text = String(content);
|
||||||
|
return { text, images, files };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历数组,提取文本和图片
|
||||||
|
for (const item of contentArray) {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
text += item.text || '';
|
||||||
|
}
|
||||||
|
else if (item.type === 'image_url') {
|
||||||
|
if (item.image_url?.url) {
|
||||||
|
images.push({
|
||||||
|
url: item.image_url.url,
|
||||||
|
name: item.image_url.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从文本中提取文件信息(如果有 ATTACHMENT_FILE 标签)
|
||||||
|
const fileMatches = text.matchAll(/<ATTACHMENT_FILE>[\s\S]*?<FILE_NAME>(.*?)<\/FILE_NAME>[\s\S]*?<\/ATTACHMENT_FILE>/g);
|
||||||
|
for (const match of fileMatches) {
|
||||||
|
const fileName = match[1];
|
||||||
|
files.push({
|
||||||
|
name: fileName,
|
||||||
|
size: 0, // 从历史记录中无法获取文件大小
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从文本中移除 ATTACHMENT_FILE 标签及其内容,只保留文件卡片显示
|
||||||
|
text = text.replace(/<ATTACHMENT_FILE>[\s\S]*?<\/ATTACHMENT_FILE>/g, '').trim();
|
||||||
|
|
||||||
|
return { text, images, files };
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('解析消息内容失败:', error);
|
||||||
|
// 解析失败,返回原始内容
|
||||||
|
return {
|
||||||
|
text: String(content),
|
||||||
|
images: [],
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setChatMap = (id: string, data: ChatMessageVo[]) => {
|
const setChatMap = (id: string, data: ChatMessageVo[]) => {
|
||||||
chatMap.value[id] = data?.map((item: ChatMessageVo) => {
|
chatMap.value[id] = data?.map((item: ChatMessageVo) => {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
const thinkContent = extractThkContent(item.content as string);
|
|
||||||
|
// 解析消息内容
|
||||||
|
const { text, images, files } = parseMessageContent(item.content as string);
|
||||||
|
|
||||||
|
// 处理思考内容
|
||||||
|
const thinkContent = extractThkContent(text);
|
||||||
|
const finalContent = extractThkContentAfter(text);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
key: item.id,
|
key: item.id,
|
||||||
placement: isUser ? 'end' : 'start',
|
placement: isUser ? 'end' : 'start',
|
||||||
isMarkdown: !isUser,
|
isMarkdown: !isUser,
|
||||||
// variant: 'shadow',
|
|
||||||
// shape: 'corner',
|
|
||||||
avatar: isUser
|
avatar: isUser
|
||||||
? getUserProfilePicture()
|
? getUserProfilePicture()
|
||||||
: systemProfilePicture,
|
: systemProfilePicture,
|
||||||
@@ -35,8 +119,11 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
typing: false,
|
typing: false,
|
||||||
reasoning_content: thinkContent,
|
reasoning_content: thinkContent,
|
||||||
thinkingStatus: 'end',
|
thinkingStatus: 'end',
|
||||||
content: extractThkContentAfter(item.content as string),
|
content: finalContent,
|
||||||
thinlCollapse: false,
|
thinlCollapse: false,
|
||||||
|
// 保留图片和文件信息(优先使用解析出来的,如果没有则使用原有的)
|
||||||
|
images: images.length > 0 ? images : item.images,
|
||||||
|
files: files.length > 0 ? files : item.files,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
|
|||||||
// 对话聊天的文件上传列表
|
// 对话聊天的文件上传列表
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export interface FileItem extends FilesCardProps {
|
||||||
|
file: File;
|
||||||
|
fileId?: string; // 上传后返回的文件ID
|
||||||
|
isUploaded?: boolean; // 是否已上传
|
||||||
|
uploadProgress?: number; // 上传进度
|
||||||
|
base64?: string; // 图片的base64编码
|
||||||
|
fileContent?: string; // 文本文件的内容
|
||||||
|
fileType?: 'image' | 'text'; // 文件类型
|
||||||
|
}
|
||||||
|
|
||||||
export const useFilesStore = defineStore('files', () => {
|
export const useFilesStore = defineStore('files', () => {
|
||||||
const filesList = ref<FilesCardProps & { file: File }[]>([]);
|
const filesList = ref<FileItem[]>([]);
|
||||||
|
|
||||||
// 设置文件列表
|
// 设置文件列表
|
||||||
const setFilesList = (list: FilesCardProps & { file: File }[]) => {
|
const setFilesList = (list: FileItem[]) => {
|
||||||
filesList.value = list;
|
filesList.value = list;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,9 +25,24 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
filesList.value.splice(index, 1);
|
filesList.value.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新文件上传状态
|
||||||
|
const updateFileUploadStatus = (index: number, fileId: string) => {
|
||||||
|
if (filesList.value[index]) {
|
||||||
|
filesList.value[index].fileId = fileId;
|
||||||
|
filesList.value[index].isUploaded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空文件列表
|
||||||
|
const clearFilesList = () => {
|
||||||
|
filesList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filesList,
|
filesList,
|
||||||
setFilesList,
|
setFilesList,
|
||||||
deleteFileByIndex,
|
deleteFileByIndex,
|
||||||
|
updateFileUploadStatus,
|
||||||
|
clearFilesList,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
1
Yi.Ai.Vue3/types/components.d.ts
vendored
1
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -23,7 +23,6 @@ declare module 'vue' {
|
|||||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
|
||||||
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
|
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
|
|||||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -6,6 +6,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_WEB_ENV: string;
|
readonly VITE_WEB_ENV: string;
|
||||||
readonly VITE_WEB_BASE_API: string;
|
readonly VITE_WEB_BASE_API: string;
|
||||||
readonly VITE_API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
|
readonly VITE_FILE_UPLOAD_API: string;
|
||||||
readonly VITE_BUILD_COMPRESS: string;
|
readonly VITE_BUILD_COMPRESS: string;
|
||||||
readonly VITE_SSO_SEVER_URL: string;
|
readonly VITE_SSO_SEVER_URL: string;
|
||||||
readonly VITE_APP_VERSION: string;
|
readonly VITE_APP_VERSION: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user