diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingGetListInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingGetListInput.cs new file mode 100644 index 00000000..71f39c56 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingGetListInput.cs @@ -0,0 +1,14 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos; + +/// +/// 排行榜查询输入 +/// +public class RankingGetListInput +{ + /// + /// 排行榜类型:0-模型,1-工具,不传返回全部 + /// + public RankingTypeEnum? Type { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingItemDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingItemDto.cs new file mode 100644 index 00000000..12b478d8 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/RankingItemDto.cs @@ -0,0 +1,41 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos; + +/// +/// 排行榜项DTO +/// +public class RankingItemDto +{ + public Guid Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } = null!; + + /// + /// 描述 + /// + public string Description { get; set; } = null!; + + /// + /// Logo地址 + /// + public string? LogoUrl { get; set; } + + /// + /// 得分 + /// + public decimal Score { get; set; } + + /// + /// 提供者 + /// + public string Provider { get; set; } = null!; + + /// + /// 排行榜类型 + /// + public RankingTypeEnum Type { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IRankingService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IRankingService.cs new file mode 100644 index 00000000..d689d9eb --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IRankingService.cs @@ -0,0 +1,16 @@ +using Yi.Framework.AiHub.Application.Contracts.Dtos; + +namespace Yi.Framework.AiHub.Application.Contracts.IServices; + +/// +/// 排行榜服务接口 +/// +public interface IRankingService +{ + /// + /// 获取排行榜列表(全量返回) + /// + /// 查询条件 + /// 排行榜列表 + Task> GetListAsync(RankingGetListInput input); +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/RankingService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/RankingService.cs new file mode 100644 index 00000000..92b13c84 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/RankingService.cs @@ -0,0 +1,38 @@ +using Mapster; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Application.Services; +using Yi.Framework.AiHub.Application.Contracts.Dtos; +using Yi.Framework.AiHub.Application.Contracts.IServices; +using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.AiHub.Application.Services; + +/// +/// 排行榜服务 +/// +public class RankingService : ApplicationService, IRankingService +{ + private readonly ISqlSugarRepository _repository; + + public RankingService(ISqlSugarRepository repository) + { + _repository = repository; + } + + /// + /// 获取排行榜列表(全量返回,按得分降序) + /// + [HttpGet("ranking/list")] + [AllowAnonymous] + public async Task> GetListAsync([FromQuery] RankingGetListInput input) + { + var query = _repository._DbQueryable + .WhereIF(input.Type.HasValue, x => x.Type == input.Type!.Value) + .OrderByDescending(x => x.Score); + + var entities = await query.ToListAsync(); + return entities.Adapt>(); + } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/RankingTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/RankingTypeEnum.cs new file mode 100644 index 00000000..d233b944 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/RankingTypeEnum.cs @@ -0,0 +1,17 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Enums; + +/// +/// 排行榜类型枚举 +/// +public enum RankingTypeEnum +{ + /// + /// 模型 + /// + Model = 0, + + /// + /// 工具 + /// + Tool = 1 +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/RankingItemAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/RankingItemAggregateRoot.cs new file mode 100644 index 00000000..ef36886b --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/RankingItemAggregateRoot.cs @@ -0,0 +1,42 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 排行榜项聚合根 +/// +[SugarTable("Ai_RankingItem")] +public class RankingItemAggregateRoot : FullAuditedAggregateRoot +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 描述 + /// + public string? Description { get; set; } + + /// + /// Logo地址 + /// + public string? LogoUrl { get; set; } + + /// + /// 得分 + /// + public decimal Score { get; set; } + + /// + /// 提供者 + /// + public string? Provider { get; set; } + + /// + /// 排行榜类型:0-模型,1-工具 + /// + public RankingTypeEnum Type { get; set; } +} diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 5529f1e8..afaa6969 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -361,7 +361,7 @@ namespace Yi.Abp.Web var app = context.GetApplicationBuilder(); app.UseRouting(); - //app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); diff --git a/Yi.Ai.Vue3/.claude/settings.local.json b/Yi.Ai.Vue3/.claude/settings.local.json index 2379a8d2..89b03152 100644 --- a/Yi.Ai.Vue3/.claude/settings.local.json +++ b/Yi.Ai.Vue3/.claude/settings.local.json @@ -8,7 +8,9 @@ "Bash(timeout /t 5 /nobreak)", "Bash(git checkout:*)", "Bash(npm install marked --save)", - "Bash(pnpm add marked)" + "Bash(pnpm add marked)", + "Bash(pnpm lint:*)", + "Bash(pnpm list:*)" ], "deny": [], "ask": [] diff --git a/Yi.Ai.Vue3/CLAUDE.md b/Yi.Ai.Vue3/CLAUDE.md new file mode 100644 index 00000000..25c6ed7a --- /dev/null +++ b/Yi.Ai.Vue3/CLAUDE.md @@ -0,0 +1,367 @@ +# CLAUDE.md + +本文件为 Claude Code (claude.ai/code) 提供本项目代码开发指导。 + +## 项目简介 + +**意心AI** - 基于 Vue 3.5 + TypeScript 开发的企业级 AI 聊天应用模板,仿豆包/通义 AI 平台。支持流式对话、AI 模型库、文件上传、Mermaid 图表渲染等功能。 + +## 技术栈 + +- **框架**: Vue 3.5+ (Composition API) + TypeScript 5.8+ +- **构建工具**: Vite 6.3+ +- **UI 组件**: Element Plus 2.10.4 + vue-element-plus-x 1.3.7 +- **状态管理**: Pinia 3.0 + pinia-plugin-persistedstate +- **HTTP 请求**: hook-fetch(支持流式/SSE,替代 Axios) +- **CSS**: UnoCSS + SCSS +- **路由**: Vue Router 4 + +## 常用命令 + +```bash +# 安装依赖(必须用 pnpm) +pnpm install + +# 启动开发服务器(端口 17001) +pnpm dev + +# 生产构建 +pnpm build + +# 预览生产构建 +pnpm preview + +# 代码检查与修复 +pnpm lint # ESLint 检查 +pnpm fix # ESLint 自动修复 +pnpm lint:stylelint # 样式检查 + +# 规范提交(使用 cz-git) +pnpm cz +``` + +## 如何新增页面 + +### 1. 创建页面文件 + +页面文件统一放在 `src/pages/` 目录下: + +``` +src/pages/ +├── chat/ # 功能模块文件夹 +│ ├── index.vue # 父级布局页 +│ ├── conversation/ # 子页面文件夹 +│ │ └── index.vue # 子页面 +│ └── image/ +│ └── index.vue +├── console/ +│ └── index.vue +└── your-page/ # 新增页面在这里创建 + └── index.vue +``` + +**单页面示例** (`src/pages/your-page/index.vue`): + +```vue + + + + + +``` + +### 2. 配置路由 + +路由配置在 `src/routers/modules/staticRouter.ts`。 + +**新增独立页面**(添加到 `layoutRouter` 的 children 中): + +```typescript +{ + path: 'your-page', // URL 路径,最终为 /your-page + name: 'yourPage', // 路由名称,必须唯一 + component: () => import('@/pages/your-page/index.vue'), + meta: { + title: '页面标题', // 页面标题,会显示在浏览器标签 + keepAlive: 0, // 是否缓存页面:0=缓存,1=不缓存 + isDefaultChat: false, // 是否为默认聊天页 + layout: 'default', // 布局类型:default/blankPage + }, +} +``` + +**新增带子路由的功能模块**: + +```typescript +{ + path: 'module-name', + name: 'moduleName', + component: () => import('@/pages/module-name/index.vue'), // 父级布局页 + redirect: '/module-name/sub-page', // 默认重定向 + meta: { + title: '模块标题', + icon: 'HomeFilled', // Element Plus 图标名称 + }, + children: [ + { + path: 'sub-page', + name: 'subPage', + component: () => import('@/pages/module-name/sub-page/index.vue'), + meta: { + title: '子页面标题', + }, + }, + // 带参数的路由 + { + path: 'detail/:id', + name: 'detailPage', + component: () => import('@/pages/module-name/detail/index.vue'), + meta: { + title: '详情页', + }, + }, + ], +} +``` + +**无需布局的独立页面**(添加到 `staticRouter`): + +```typescript +{ + path: '/test/page', + name: 'testPage', + component: () => import('@/pages/test/page.vue'), + meta: { + title: '测试页面', + }, +} +``` + +### 3. 页面 Meta 配置说明 + +| 属性 | 类型 | 说明 | +|------|------|------| +| title | string | 页面标题,显示在浏览器标签页 | +| keepAlive | number | 0=缓存页面,1=不缓存 | +| layout | string | 布局类型:'default' 使用默认布局,'blankPage' 使用空白布局 | +| isDefaultChat | boolean | 是否为默认聊天页面 | +| icon | string | Element Plus 图标名称,用于菜单显示 | +| isHide | string | '0'=在菜单中隐藏,'1'=显示 | +| isKeepAlive | string | '0'=缓存,'1'=不缓存(字符串版) | + +### 4. 布局说明 + +布局组件位于 `src/layouts/`: + +- **default**: 默认布局,包含侧边栏、顶部导航等 +- **blankPage**: 空白布局,仅包含路由出口 + +在路由 meta 中通过 `layout` 字段指定: + +```typescript +meta: { + layout: 'default', // 使用默认布局(有侧边栏) + // 或 + layout: 'blankPage', // 使用空白布局(全屏页面) +} +``` + +### 5. 页面跳转示例 + +```typescript +// 在 script setup 中使用 +const router = useRouter() + +// 跳转页面 +router.push('/chat/conversation') +router.push({ name: 'chatConversation' }) +router.push({ path: '/chat/conversation/:id', params: { id: '123' } }) + +// 获取路由参数 +const route = useRoute() +console.log(route.params.id) +``` + +### 6. 完整新增页面示例 + +假设要新增一个"数据统计"页面: + +**步骤 1**: 创建页面文件 `src/pages/statistics/index.vue` + +```vue + + + + + +``` + +**步骤 2**: 在 `src/routers/modules/staticRouter.ts` 中添加路由 + +```typescript +{ + path: 'statistics', + name: 'statistics', + component: () => import('@/pages/statistics/index.vue'), + meta: { + title: '意心Ai-数据统计', + keepAlive: 0, + isDefaultChat: false, + layout: 'default', + }, +} +``` + +**步骤 3**: 在菜单中添加入口(如需要) + +如需在侧边栏显示,需在相应的位置添加菜单配置。 + +## 核心架构说明 + +### HTTP 请求封装 + +位于 `src/utils/request.ts`,使用 hook-fetch: + +```typescript +import { get, post, put, del } from '@/utils/request' + +// GET 请求 +const { data } = await get('/api/endpoint').json() + +// POST 请求 +const result = await post('/api/endpoint', { key: 'value' }).json() +``` + +特点: +- 自动附加 JWT Token 到请求头 +- 自动处理 401/403 错误 +- 支持 Server-Sent Events (SSE) 流式响应 + +### 状态管理 + +Pinia stores 位于 `src/stores/modules/`: + +| Store | 用途 | +|-------|------| +| user.ts | 用户认证、登录状态 | +| chat.ts | 聊天消息、流式输出 | +| session.ts | 会话列表、当前会话 | +| model.ts | AI 模型配置 | + +使用方式: + +```typescript +const userStore = useUserStore() +userStore.setToken(token, refreshToken) +userStore.logout() +``` + +### 自动导入 + +项目已配置 `unplugin-auto-import`,以下 API 无需手动 import: + +- Vue API: `ref`, `reactive`, `computed`, `watch`, `onMounted` 等 +- Vue Router: `useRoute`, `useRouter` +- Pinia: `createPinia`, `storeToRefs` +- VueUse: `useFetch`, `useStorage` 等 + +### 路径别名 + +| 别名 | 对应路径 | +|------|----------| +| `@/` | `src/` | +| `@components/` | `src/vue-element-plus-y/components/` | + +### 环境变量 + +开发配置在 `.env.development`: + +``` +VITE_WEB_BASE_API=/dev-api # API 前缀 +VITE_API_URL=http://localhost:19001/api/app # 后端地址 +VITE_SSO_SEVER_URL=http://localhost:18001 # SSO 地址 +``` + +Vite 开发服务器会自动将 `/dev-api` 代理到后端 API。 + +## 代码规范 + +### 提交规范 + +使用 `pnpm cz` 进行规范提交,类型包括: +- `feat`: 新功能 +- `fix`: 修复 +- `docs`: 文档 +- `style`: 代码格式 +- `refactor`: 重构 +- `perf`: 性能优化 +- `test`: 测试 +- `build`: 构建 +- `ci`: CI/CD +- `revert`: 回滚 +- `chore`: 其他 + +### Git Hooks + +- **pre-commit**: 自动运行 ESLint 修复 +- **commit-msg**: 校验提交信息格式 + +## 构建优化 + +Vite 配置中的代码分割策略(`vite.config.ts`): + +| Chunk 名称 | 包含内容 | +|-----------|---------| +| vue-vendor | Vue 核心库、Vue Router | +| pinia | Pinia 状态管理 | +| element-plus | Element Plus UI 库 | +| markdown | Markdown 解析相关 | +| utils | Lodash、VueUse 工具库 | +| highlight | 代码高亮库 | +| echarts | 图表库 | +| pdf | PDF 处理库 | + +## 后端集成 + +后端为 .NET 8 项目,本地启动命令: + +```bash +cd E:\devDemo\Yi\Yi.Abp.Net8\src\Yi.Abp.Web +dotnet run +``` + +前端开发时,后端默认运行在 `http://localhost:19001`。 diff --git a/Yi.Ai.Vue3/src/api/ranking/index.ts b/Yi.Ai.Vue3/src/api/ranking/index.ts new file mode 100644 index 00000000..94b5f82b --- /dev/null +++ b/Yi.Ai.Vue3/src/api/ranking/index.ts @@ -0,0 +1,15 @@ +import type { RankingGetListInput, RankingItemDto } from './types'; +import { get } from '@/utils/request'; + +// 获取排行榜列表(公开接口,无需登录) +export function getRankingList(params?: RankingGetListInput) { + const queryParams = new URLSearchParams(); + if (params?.type !== undefined) { + queryParams.append('Type', params.type.toString()); + } + + const queryString = queryParams.toString(); + const url = queryString ? `/ranking/list?${queryString}` : '/ranking/list'; + + return get(url).json(); +} diff --git a/Yi.Ai.Vue3/src/api/ranking/types.ts b/Yi.Ai.Vue3/src/api/ranking/types.ts new file mode 100644 index 00000000..ad3e5ca3 --- /dev/null +++ b/Yi.Ai.Vue3/src/api/ranking/types.ts @@ -0,0 +1,21 @@ +// 排行榜类型枚举 +export enum RankingTypeEnum { + Model = 0, + Tool = 1, +} + +// 排行榜项 +export interface RankingItemDto { + id: string; + name: string; + description: string; + logoUrl?: string; + score: number; + provider: string; + type: RankingTypeEnum; +} + +// 排行榜查询参数 +export interface RankingGetListInput { + type?: RankingTypeEnum; +} diff --git a/Yi.Ai.Vue3/src/layouts/components/Header/components/ModelLibraryBtn.vue b/Yi.Ai.Vue3/src/layouts/components/Header/components/ModelLibraryBtn.vue index 73347ac9..c557aee7 100644 --- a/Yi.Ai.Vue3/src/layouts/components/Header/components/ModelLibraryBtn.vue +++ b/Yi.Ai.Vue3/src/layouts/components/Header/components/ModelLibraryBtn.vue @@ -1,11 +1,5 @@