18 Commits

Author SHA1 Message Date
chenchun
6f1efafd86 feat: 发布2.8版本 2025-12-17 12:10:24 +08:00
Gsh
2714a507d9 fix: 文件上传提示优化、element-plus-x版本回退 2025-12-16 22:54:43 +08:00
Gsh
9a9230786b fix: [临时方案]修复因element-plus-x 1.3.98 中Conversations组件销毁问题出现的布局路由缺陷 2025-12-16 22:00:15 +08:00
ccnetcore
4a8b58a65c build: 构建 2025-12-16 21:12:05 +08:00
ccnetcore
7d81f88658 feat: 完成包兼容 2025-12-16 21:08:26 +08:00
ccnetcore
0ce3c0bbdd feat:完成2.8 2025-12-15 23:59:04 +08:00
Gsh
981235e6e9 fix: 购买提示词优化 2025-12-15 21:28:24 +08:00
Gsh
d0ecb232a1 fix: 升级markdown包 2025-12-15 13:46:18 +08:00
Gsh
c7a52604e7 fix: 右上角导航优化 2025-12-14 21:34:20 +08:00
Gsh
da81b2d8a3 fix: 文件上传优化 2025-12-14 18:55:46 +08:00
ccnetcore
7b14fdd8de feat: 完成多message存储 2025-12-14 13:07:44 +08:00
ccnetcore
1fc2734eb7 feat: 新增忽略文件 2025-12-14 13:01:02 +08:00
ccnetcore
f3bef72ebb fix: 修复优惠 2025-12-14 11:43:21 +08:00
ccnetcore
7e6d2e829b feat: 修改优惠订单 2025-12-14 11:38:08 +08:00
Gsh
944626960b fix: 网页版增加对话文件支持 2025-12-14 00:54:34 +08:00
Gsh
c073868989 fix: 网页版增加对话图片支持 2025-12-13 18:09:12 +08:00
ccnetcore
d2981100fa feat: 支持gpt-5.2 2025-12-12 21:14:38 +08:00
chenchun
ce4f7e5711 refactor: 将 AnthropicInput.Messages 类型由 JsonElement? 更改为 IList<AnthropicMessageInput>
使用强类型消息集合,便于序列化与校验。
2025-12-12 09:40:24 +08:00
42 changed files with 2227 additions and 653 deletions

View File

@@ -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"
]; ];
} }

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -35,30 +35,37 @@
"@floating-ui/dom": "^1.7.2", "@floating-ui/dom": "^1.7.2",
"@floating-ui/vue": "^1.1.7", "@floating-ui/vue": "^1.1.7",
"@jsonlee_12138/enum": "^1.0.4", "@jsonlee_12138/enum": "^1.0.4",
"@shikijs/transformers": "^3.7.0",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0", "@vueuse/integrations": "^13.5.0",
"chatarea": "^6.0.3",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"deepmerge": "^4.3.1",
"dompurify": "^3.2.6",
"driver.js": "^1.3.6", "driver.js": "^1.3.6",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.10.4", "element-plus": "^2.10.4",
"fingerprintjs": "^0.5.3", "fingerprintjs": "^0.5.3",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"hook-fetch": "^2.0.4-beta.1", "hook-fetch": "^2.0.4-beta.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "11.12.0", "mammoth": "^1.11.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",
"property-information": "^7.1.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radash": "^12.1.1", "radash": "^12.1.1",
"reset-css": "^5.0.2",
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.7",
"vue-router": "4",
"xlsx": "^0.18.5",
"@shikijs/transformers": "^3.7.0",
"chatarea": "^6.0.3",
"deepmerge": "^4.3.1",
"dompurify": "^3.2.6",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21",
"mermaid": "11.12.0",
"prismjs": "^1.30.0",
"property-information": "^7.1.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
@@ -67,20 +74,46 @@
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2", "remark-rehype": "^11.1.2",
"reset-css": "^5.0.2",
"shiki": "^3.7.0", "shiki": "^3.7.0",
"ts-md5": "^2.0.1", "ts-md5": "^2.0.1",
"unified": "^11.0.5", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0"
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.7",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^4.16.2", "@antfu/eslint-config": "^4.16.2",
"@changesets/cli": "^2.29.5", "@changesets/cli": "^2.29.5",
"@chromatic-com/storybook": "^3.2.7",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"commitlint": "^19.8.1",
"cz-git": "^1.12.0",
"eslint": "^9.31.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"postcss": "8.4.31",
"postcss-html": "1.5.0",
"prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.3",
"sass-embedded": "^1.89.2",
"stylelint": "^16.21.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended-scss": "^15.0.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"typescript": "~5.8.3",
"typescript-api-pro": "^0.0.7",
"unocss": "66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-env-typed": "^0.0.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^3.0.1",
"@chromatic-com/storybook": "^3.2.7",
"@jsonlee_12138/markdown-it-mermaid": "0.0.6", "@jsonlee_12138/markdown-it-mermaid": "0.0.6",
"@storybook/addon-essentials": "^8.6.14", "@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-onboarding": "^8.6.14", "@storybook/addon-onboarding": "^8.6.14",
@@ -94,52 +127,22 @@
"@storybook/vue3": "^8.6.14", "@storybook/vue3": "^8.6.14",
"@storybook/vue3-vite": "^8.6.14", "@storybook/vue3-vite": "^8.6.14",
"@types/dom-speech-recognition": "^0.0.4", "@types/dom-speech-recognition": "^0.0.4",
"@types/fingerprintjs__fingerprintjs": "^3.0.2",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vitest/browser": "^3.2.4", "@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"@vue/tsconfig": "^0.7.0",
"commitlint": "^19.8.1",
"cz-git": "^1.12.0",
"eslint": "^9.31.0",
"esno": "^4.8.0", "esno": "^4.8.0",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"playwright": "^1.53.2", "playwright": "^1.53.2",
"postcss": "8.4.31",
"postcss-html": "1.5.0",
"prettier": "^3.6.2",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.3",
"sass": "^1.89.2", "sass": "^1.89.2",
"sass-embedded": "^1.89.2",
"storybook": "^8.6.14", "storybook": "^8.6.14",
"storybook-dark-mode": "^4.0.2", "storybook-dark-mode": "^4.0.2",
"stylelint": "^16.21.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended-scss": "^15.0.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"terser": "^5.43.1", "terser": "^5.43.1",
"typescript": "~5.8.3",
"typescript-api-pro": "^0.0.7",
"unocss": "66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vite-plugin-env-typed": "^0.0.2",
"vite-plugin-lib-inject-css": "^2.2.2", "vite-plugin-lib-inject-css": "^2.2.2",
"vite-plugin-svg-icons": "^2.0.1", "vitest": "^3.2.4"
"vitest": "^3.2.4",
"vue-tsc": "^3.0.1"
}, },
"config": { "config": {
"commitizen": { "commitizen": {

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -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;
} }

View 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`;
}

View File

@@ -0,0 +1,3 @@
export interface UploadFileResponse {
id: string;
}

View File

@@ -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';

View 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

View File

@@ -1,148 +1,751 @@
<!-- 文件上传 --> <!-- 文件上传 -->
<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 Popover from '@/components/Popover/index.vue'; import mammoth from 'mammoth';
import SvgIcon from '@/components/SvgIcon/index.vue'; import * as pdfjsLib from 'pdfjs-dist';
import * as XLSX from 'xlsx';
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];
arr.push({
uid: crypto.randomUUID(), // 不写 uid文件列表展示不出来elx 1.2.0 bug 待修复 // 验证文件大小
name: file.name, if (file.size > MAX_FILE_SIZE) {
fileSize: file.size, ElMessage.error(`文件 ${file.name} 超过 3MB 限制,已跳过`);
file, continue;
maxWidth: '200px', }
showDelIcon: true, // 显示删除图标
imgPreview: true, // 显示图片预览 const ext = getFileExtension(file.name);
imgVariant: 'square', // 图片预览的形状 const isImage = file.type.startsWith('image/');
url: URL.createObjectURL(file), // 图片预览地址 const isExcel = ['xlsx', 'xls'].includes(ext);
}); const isWord = ext === 'docx';
const isPDF = ext === 'pdf';
const isText = isTextFile(file);
// 处理图片文件
if (isImage) {
try {
// 控制参数:是否开启图片压缩
const enableImageCompression = true; // 这里可以设置为变量或从配置读取
let finalBlob: Blob = file;
let base64 = '';
let compressionLevel = 0;
const originalSize = (file.size / 1024).toFixed(2);
let finalSize = originalSize;
if (enableImageCompression) {
// 多级压缩策略:逐步降低质量和分辨率
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;
// 尝试不同级别的压缩
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;
finalBlob = compressedBlob;
break;
}
// 如果是最后一级压缩仍然超限,则跳过
if (compressionLevel === compressionLevels.length) {
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
ElMessage.error(`${file.name} 图片内容过大,请压缩后上传`);
compressedBlob = null;
break;
}
}
// 如果压缩失败,跳过此文件
if (!compressedBlob) {
continue;
}
// 计算压缩比例
finalSize = (finalBlob.size / 1024).toFixed(2);
console.log(`图片压缩: ${file.name} - 原始: ${originalSize}KB, 压缩后: ${finalSize}KB (级别${compressionLevel})`);
}
else {
// 不开启压缩时,直接转换原始文件
base64 = await blobToBase64(file);
// 检查总长度限制
const estimatedLength = Math.floor(base64.length * 0.5);
if (totalContentLength + estimatedLength > MAX_TOTAL_CONTENT_LENGTH) {
const fileSizeMB = (file.size / 1024 / 1024).toFixed(2);
ElMessage.error(`${file.name} (${fileSizeMB}MB) 超过总长度限制,已跳过`);
continue;
}
totalContentLength += estimatedLength;
console.log(`图片未压缩: ${file.name} - 大小: ${originalSize}KB`);
}
arr.push({
uid: crypto.randomUUID(),
name: file.name,
fileSize: file.size,
file,
maxWidth: '200px',
showDelIcon: true,
imgPreview: true,
imgVariant: 'square',
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;
}
} }
filesStore.setFilesList([...filesStore.filesList, ...arr]);
if (arr.length > 0) {
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>
</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="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> </div>
</Popover> </el-tooltip>
</div> </div>
</template> </template>

View File

@@ -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>

View File

@@ -797,7 +797,7 @@ function generateShareContent(): string {
👉 点击链接立即参与我的专属邀请码链接: 👉 点击链接立即参与我的专属邀请码链接:
${shareLink} ${shareLink}
🍀 未注册用户,微信扫码登录,进入用户中心👉每周邀请 即可立即参与!`; 🍀 未注册用户,微信扫码登录,进入控制台👉每周邀请 即可立即参与!`;
} }
/** /**

View File

@@ -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',
}, },

View File

@@ -13,7 +13,6 @@
.layout-blank{ .layout-blank{
height: 100vh; height: 100vh;
overflow: auto; overflow: auto;
//margin: 20px ;
} }
/* 无样式 */ /* 无样式 */
</style> </style>

View File

@@ -29,7 +29,6 @@ useWindowWidthObserver();
// 应用加载时检查是否需要显示公告弹窗 // 应用加载时检查是否需要显示公告弹窗
onMounted(() => { onMounted(() => {
console.log('announcementStore.shouldShowDialog--', announcementStore.shouldShowDialog);
// 检查是否应该显示弹窗(只有"关闭一周"且未超过7天才不显示 // 检查是否应该显示弹窗(只有"关闭一周"且未超过7天才不显示
// 数据获取已移至 SystemAnnouncementDialog 组件内部,每次打开弹窗时都会获取最新数据 // 数据获取已移至 SystemAnnouncementDialog 组件内部,每次打开弹窗时都会获取最新数据
if (announcementStore.shouldShowDialog) { if (announcementStore.shouldShowDialog) {

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,7 @@ 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 YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
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';
@@ -22,7 +22,6 @@ import { useUserStore } from '@/stores/modules/user';
import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts'; import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts';
import '@/styles/github-markdown.css'; import '@/styles/github-markdown.css';
import '@/styles/yixin-markdown.scss'; import '@/styles/yixin-markdown.scss';
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
type MessageItem = BubbleProps & { type MessageItem = BubbleProps & {
key: number; key: number;
@@ -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,9 +457,36 @@ 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">
{{ item.content }} <!-- 图片列表 -->
<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 }}
</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;

File diff suppressed because one or more lines are too long

View File

@@ -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>
<!-- 重新登录提示 --> <!-- 重新登录提示 -->

View File

@@ -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[静态路由] 预留

View File

@@ -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,
}; };
}); });
}; };

View File

@@ -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,
}; };
}); });

View File

@@ -4,8 +4,7 @@ import type { PluggableList } from 'unified';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { CustomAttrs, SanitizeOptions, TVueMarkdown } from './types'; import type { CustomAttrs, SanitizeOptions, TVueMarkdown } from './types';
import { computed, defineComponent, ref, shallowRef, toRefs, watch } from 'vue'; import { defineComponent, shallowRef, toRefs, watch } from 'vue';
import { watchDebounced } from '@vueuse/core';
// import { useMarkdownContext } from '../components/MarkdownProvider'; // import { useMarkdownContext } from '../components/MarkdownProvider';
import { render } from './hast-to-vnode'; import { render } from './hast-to-vnode';
import { useMarkdownProcessor } from './useProcessor'; import { useMarkdownProcessor } from './useProcessor';
@@ -64,24 +63,10 @@ const vueMarkdownImpl = defineComponent({
sanitizeOptions sanitizeOptions
}); });
// 防抖优化控制markdown更新频率避免流式渲染时频繁触发
const debouncedMarkdown = ref(markdown.value);
watchDebounced(
markdown,
(val) => {
debouncedMarkdown.value = val;
},
{ debounce: 50, maxWait: 200 } // 50ms防抖最多200ms必须更新一次
);
// 缓存优化使用computed缓存解析结果避免重复计算
const hast = computed(() => {
const mdast = processor.value.parse(debouncedMarkdown.value);
return processor.value.runSync(mdast) as Root;
});
return () => { return () => {
return render(hast.value, attrs, slots, customAttrs.value); const mdast = processor.value.parse(markdown.value);
const hast = processor.value.runSync(mdast) as Root;
return render(hast, attrs, slots, customAttrs.value);
}; };
} }
}); });
@@ -108,25 +93,12 @@ const vueMarkdownAsyncImpl = defineComponent({
}); });
const hast = shallowRef<Root | null>(null); const hast = shallowRef<Root | null>(null);
// 防抖优化控制markdown更新频率
const debouncedMarkdown = ref(markdown.value);
const process = async (): Promise<void> => { const process = async (): Promise<void> => {
const mdast = processor.value.parse(debouncedMarkdown.value); const mdast = processor.value.parse(markdown.value);
hast.value = (await processor.value.run(mdast)) as Root; hast.value = (await processor.value.run(mdast)) as Root;
}; };
// 使用防抖watch避免频繁触发异步处理 watch(() => [markdown.value, processor.value], process, { flush: 'sync' });
watchDebounced(
markdown,
(val) => {
debouncedMarkdown.value = val;
},
{ debounce: 50, maxWait: 200 }
);
watch(() => [debouncedMarkdown.value, processor.value], process, { flush: 'post' });
await process(); await process();

View File

@@ -1,5 +1,5 @@
import type { TVueMarkdownProps } from '@components/XMarkdownCore'; import type { TVueMarkdownProps } from '../';
import type { CodeBlockHeaderExpose } from '@components/XMarkdownCore/components/CodeBlock/shiki-header'; import type { CodeBlockHeaderExpose } from '../components/CodeBlock/shiki-header';
import type { ElxRunCodeOptions } from '../components/RunCode/type'; import type { ElxRunCodeOptions } from '../components/RunCode/type';
import type { InitShikiOptions } from './shikiHighlighter'; import type { InitShikiOptions } from './shikiHighlighter';

View File

@@ -4,8 +4,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["src/*"]
"@components/*": ["src/vue-element-plus-y/components/*"]
}, },
"typeRoots": ["./node_modules/@types", "./types"], "typeRoots": ["./node_modules/@types", "./types"],
/* Linting */ /* Linting */

View File

@@ -11,11 +11,6 @@ declare module 'vue' {
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default'] AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default'] APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default']
CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default'] CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default']
CodeBlock: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/index.vue')['default']
CodeLine: typeof import('./../src/components/YMarkdownCore/components/CodeLine/index.vue')['default']
CodeX: typeof import('./../src/components/YMarkdownCore/components/CodeX/index.vue')['default']
CopyCodeButton: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/copy-code-button.vue')['default']
CustomLoading: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/custom-loading.vue')['default']
DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default'] DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default']
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default'] DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import('element-plus/es')['ElAlert']
@@ -23,13 +18,14 @@ 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']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -62,12 +58,9 @@ declare module 'vue' {
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default'] FilesSelect: typeof import('./../src/components/FilesSelect/index.vue')['default']
HighLightCode: typeof import('./../src/components/YMarkdownCore/components/HighLightCode/index.vue')['default']
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default'] IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default'] Indexl: typeof import('./../src/components/SupportModelProducts/indexl.vue')['default']
LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default'] LoginDialog: typeof import('./../src/components/LoginDialog/index.vue')['default']
Mermaid: typeof import('./../src/components/YMarkdownCore/components/Mermaid/index.vue')['default']
MermaidToolbar: typeof import('./../src/components/YMarkdownCore/components/Mermaid/MermaidToolbar.vue')['default']
ModeList: typeof import('./../src/components/modeList/index.vue')['default'] ModeList: typeof import('./../src/components/modeList/index.vue')['default']
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default'] ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default'] NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
@@ -81,10 +74,6 @@ declare module 'vue' {
RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default'] RegistrationForm: typeof import('./../src/components/LoginDialog/components/FormLogin/RegistrationForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
RunCode: typeof import('./../src/components/YMarkdownCore/components/RunCode/index.vue')['default']
RunCodeButton: typeof import('./../src/components/YMarkdownCore/components/CodeBlock/run-code-button.vue')['default']
RunCodeContent: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/run-code-content.vue')['default']
RunCodeHeader: typeof import('./../src/components/YMarkdownCore/components/RunCode/components/run-code-header.vue')['default']
SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default'] SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default'] SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default']
@@ -93,7 +82,6 @@ declare module 'vue' {
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default'] UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default'] VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']
WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default'] WelecomeText: typeof import('./../src/components/WelecomeText/index.vue')['default']
YMarkdownAsync: typeof import('./../src/components/YMarkdownAsync/index.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']

View File

@@ -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;