diff --git a/.env b/.env new file mode 100644 index 0000000..7b22d77 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# API配置 +VITE_API_BASE_URL=http://110.42.96.65:55510/api + +# Mock配置 - 切换到真实API +VITE_ENABLE_MOCK=false diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..26903be --- /dev/null +++ b/.env.development @@ -0,0 +1,11 @@ +# 开发环境配置 + +# API配置 +VITE_API_BASE_URL=http://110.42.96.65:55510/api + +# Mock配置 +# 设置为 true 使用Mock数据,false 使用真实API +VITE_ENABLE_MOCK=false + +# 开发模式 +NODE_ENV=development diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..292b513 --- /dev/null +++ b/.env.example @@ -0,0 +1,66 @@ +# 环境变量配置示例文件 +# 复制此文件为 .env.local 并填入实际的配置值 + +# 应用基础配置 +VITE_APP_TITLE=在线学习平台 +VITE_APP_DESCRIPTION=专业的在线学习平台,提供优质的编程和技术课程 +VITE_APP_VERSION=1.0.0 + +# API 配置 +VITE_API_BASE_URL=http://localhost:3000/api +VITE_API_TIMEOUT=10000 + +# 开发环境配置 +VITE_DEV_PORT=5173 +VITE_DEV_HOST=localhost + +# 生产环境配置 +VITE_PROD_API_BASE_URL=https://api.yourdomain.com/api + +# 第三方服务配置 +# 文件上传服务 +VITE_UPLOAD_BASE_URL=https://upload.yourdomain.com +VITE_CDN_BASE_URL=https://cdn.yourdomain.com + +# 视频播放服务 +VITE_VIDEO_BASE_URL=https://video.yourdomain.com + +# 支付服务配置 +VITE_ALIPAY_APP_ID=your_alipay_app_id +VITE_WECHAT_APP_ID=your_wechat_app_id + +# 第三方登录配置 +VITE_GITHUB_CLIENT_ID=your_github_client_id +VITE_QQ_APP_ID=your_qq_app_id +VITE_WECHAT_LOGIN_APP_ID=your_wechat_login_app_id + +# 地图服务配置 +VITE_MAP_API_KEY=your_map_api_key + +# 统计分析配置 +VITE_ANALYTICS_ID=your_analytics_id + +# 错误监控配置 +VITE_SENTRY_DSN=your_sentry_dsn + +# 功能开关 +VITE_ENABLE_MOCK=false +VITE_ENABLE_DEBUG=true +VITE_ENABLE_ANALYTICS=false +VITE_ENABLE_ERROR_TRACKING=false + +# 缓存配置 +VITE_CACHE_ENABLED=true +VITE_CACHE_PREFIX=study_platform_ + +# 安全配置 +VITE_ENCRYPT_STORAGE=false +VITE_ENABLE_CSRF=true + +# 主题配置 +VITE_DEFAULT_THEME=light +VITE_ENABLE_DARK_MODE=true + +# 语言配置 +VITE_DEFAULT_LOCALE=zh-CN +VITE_ENABLE_I18N=false diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..dc255b4 --- /dev/null +++ b/.env.production @@ -0,0 +1,10 @@ +# 生产环境配置 + +# API配置 +VITE_API_BASE_URL=http://110.42.86.55:5510/api + +# Mock配置 - 生产环境禁用Mock +VITE_ENABLE_MOCK=false + +# 生产模式 +NODE_ENV=production diff --git a/docs/Activities-Page.md b/docs/Activities-Page.md new file mode 100644 index 0000000..2779067 --- /dev/null +++ b/docs/Activities-Page.md @@ -0,0 +1,125 @@ +# 活动页面 (Activities Page) + +## 📋 概述 + +活动页面是在线学习平台的重要组成部分,展示所有可用的学习活动和课程。页面设计完全按照提供的UI设计图实现,包含蓝色横幅区域和活动卡片网格布局。 + +## 🎨 页面结构 + +### 1. 蓝色横幅区域 (Hero Banner) +- **背景**: 蓝色渐变背景 (#4A90E2 到 #357ABD) +- **左侧内容**: + - 分类标签: "理工/消安/药师/中经" + - 主标题: "免费海量题库" + 橙色"小程序"标签 + - 特色说明: "优质试题 | 无需下载 | 随时刷题" +- **右侧**: 插图占位区域(预留给后续图片) +- **装饰元素**: 网格背景纹理 + +### 2. 活动列表区域 +- **标题**: "全部活动" +- **布局**: 3列网格布局(响应式) +- **活动卡片**: 6个相同样式的卡片 + +### 3. 活动卡片设计 +每个卡片包含: +- **头部**: 蓝绿色渐变背景 + - 年份标签: "2025" + - 课程标题: "计算机二级" + - 课程副标题: "C语言讲练综合班" +- **主体内容**: + - 特色标签: "系统备考"、"考点详解"、"题考刷题" + - 课程信息: 证书名称、开课时间、适合年级、报名人数、价格 +- **底部**: 蓝色"查看详情"按钮 + +## 🎯 功能特性 + +### 1. 响应式设计 +- **桌面端**: 3列网格布局 +- **平板端**: 2列网格布局 +- **移动端**: 1列网格布局 + +### 2. 交互效果 +- **卡片悬停**: 阴影加深 + 向上移动 +- **按钮悬停**: 颜色变化 + 轻微上移 +- **加载动画**: 骨架屏效果 + +### 3. 动画效果 +- **页面加载**: 左右滑入动画 +- **卡片显示**: 渐入上移动画(错开延迟) +- **加载状态**: 闪烁骨架屏 + +## 🛠️ 技术实现 + +### 文件结构 +``` +src/views/Activities.vue # 活动页面主组件 +src/router/index.ts # 路由配置 +src/components/layout/AppHeader.vue # 导航栏更新 +``` + +### 核心技术 +- **Vue 3 Composition API**: 组件逻辑 +- **TypeScript**: 类型安全 +- **CSS Grid**: 响应式布局 +- **CSS Animations**: 动画效果 +- **Vue Router**: 路由导航 + +### 样式特点 +- **渐变背景**: 多层次视觉效果 +- **卡片设计**: 现代化卡片样式 +- **颜色系统**: 蓝色主题 + 橙色点缀 +- **字体层级**: 清晰的信息层次 + +## 🔗 导航集成 + +活动页面已集成到主导航栏中: +- 导航项: "活动" (带NEW标签) +- 路由路径: `/activities` +- 页面标题: "全部活动 - 在线学习平台" + +## 📱 响应式断点 + +```css +/* 大屏幕 (>1024px) */ +.activities-grid { grid-template-columns: repeat(3, 1fr); } + +/* 平板 (≤1024px) */ +.activities-grid { grid-template-columns: repeat(2, 1fr); } + +/* 手机 (≤768px) */ +.activities-grid { grid-template-columns: 1fr; } +``` + +## 🎨 设计规范 + +### 颜色规范 +- **主色调**: #4A90E2 (蓝色) +- **辅助色**: #44A08D (蓝绿色) +- **强调色**: #FF6B35 (橙色) +- **文字色**: #333 (深灰) +- **次要文字**: #666 (中灰) + +### 间距规范 +- **容器边距**: 20px (移动端) / 30px (桌面端) +- **卡片间距**: 20px (移动端) / 30px (桌面端) +- **内容边距**: 16px-24px + +### 圆角规范 +- **卡片圆角**: 12px +- **按钮圆角**: 6px +- **标签圆角**: 4px + +## 🚀 使用方法 + +1. **访问页面**: 点击导航栏"活动"或直接访问 `/activities` +2. **浏览活动**: 滚动查看所有可用活动 +3. **查看详情**: 点击"查看详情"按钮(当前为演示功能) +4. **响应式体验**: 在不同设备上自动适配布局 + +## 📝 后续扩展 + +1. **图片集成**: 添加横幅插图和活动图片 +2. **数据接口**: 连接后端API获取真实活动数据 +3. **详情页面**: 实现活动详情页面跳转 +4. **筛选功能**: 添加活动分类和筛选功能 +5. **搜索功能**: 实现活动搜索功能 diff --git a/docs/Banner-Image-Setup.md b/docs/Banner-Image-Setup.md new file mode 100644 index 0000000..3f23538 --- /dev/null +++ b/docs/Banner-Image-Setup.md @@ -0,0 +1,158 @@ +# 活动页面横幅图片设置指南 + +## 📋 概述 + +活动页面的横幅区域已经改为支持一整张图片的形式。目前显示占位内容,您可以按照以下步骤添加横幅图片。 + +## 🖼️ 当前状态 + +- ✅ 横幅区域已改为图片容器 +- ✅ 响应式设计已适配 +- ✅ 占位内容显示中 +- ⏳ 等待提供横幅图片 + +## 📐 图片规格建议 + +### 推荐尺寸 +- **桌面端**: 1200px × 400px +- **平板端**: 1024px × 350px +- **移动端**: 768px × 300px + +### 文件格式 +- **推荐**: JPG/JPEG (文件小,加载快) +- **支持**: PNG (支持透明背景) +- **支持**: WebP (现代浏览器,更小文件) + +### 文件大小 +- **建议**: < 500KB +- **最大**: < 1MB + +## 🚀 添加图片的方法 + +### 方法一:直接替换(推荐) + +1. **将图片文件放到项目中**: + ``` + public/images/activities-banner.jpg + ``` + +2. **修改 Activities.vue 文件**: + ```typescript + // 在 onMounted 中取消注释并设置图片路径 + onMounted(() => { + setTimeout(() => { + loading.value = false + }, 800) + + // 设置横幅图片路径 + setBannerImage('/images/activities-banner.jpg') + }) + ``` + +### 方法二:直接设置变量 + +在 `src/views/Activities.vue` 中找到这一行: +```typescript +const bannerImageSrc = ref('') +``` + +改为: +```typescript +const bannerImageSrc = ref('/images/activities-banner.jpg') +``` + +## 📁 推荐的文件结构 + +``` +public/ +├── images/ +│ ├── activities-banner.jpg # 横幅图片 +│ ├── activities-banner-tablet.jpg # 平板版本(可选) +│ └── activities-banner-mobile.jpg # 移动版本(可选) +``` + +## 🎨 图片内容建议 + +根据原设计图,横幅图片应该包含: +- 蓝色渐变背景 +- "理工/消安/药师/中经" 分类文字 +- "免费海量题库" 主标题 +- "小程序" 橙色标签 +- "优质试题 | 无需下载 | 随时刷题" 特色说明 +- 右侧人物和手机界面插图 + +## 🔧 技术实现细节 + +### 当前代码结构 +```vue + +``` + +### 响应式适配 +- 桌面端:400px 高度 +- 平板端:350px 高度 +- 移动端:300px 高度 +- 小屏手机:250px 高度 + +### CSS 样式 +```css +.banner-image { + width: 100%; + height: 100%; + object-fit: cover; /* 保持比例,裁剪多余部分 */ + object-position: center; /* 居中显示 */ +} +``` + +## ⚡ 性能优化建议 + +1. **图片压缩**: 使用工具压缩图片文件 +2. **懒加载**: 大图片可考虑懒加载 +3. **多尺寸**: 为不同设备提供不同尺寸的图片 +4. **WebP格式**: 现代浏览器使用WebP格式 + +## 🔄 更新步骤 + +当您准备好横幅图片时: + +1. **准备图片文件** + - 确保图片符合推荐规格 + - 压缩图片文件大小 + +2. **上传图片** + - 将图片放到 `public/images/` 目录 + +3. **更新代码** + - 按照上述方法一或方法二设置图片路径 + +4. **测试效果** + - 刷新浏览器查看效果 + - 测试不同设备的显示效果 + +## 📞 需要帮助? + +如果您在设置横幅图片时遇到任何问题,请: +1. 检查图片路径是否正确 +2. 确认图片文件是否存在 +3. 查看浏览器控制台是否有错误信息 +4. 联系开发人员获取技术支持 + +--- + +**注意**: 当前页面显示占位内容,一旦您提供横幅图片并按照上述步骤设置,占位内容将自动被实际图片替换。 diff --git a/docs/Faculty-Banner-Setup.md b/docs/Faculty-Banner-Setup.md new file mode 100644 index 0000000..ee391c1 --- /dev/null +++ b/docs/Faculty-Banner-Setup.md @@ -0,0 +1,175 @@ +# 师资力量页面横幅图片设置指南 + +## 📋 概述 + +师资力量页面的横幅区域已经改为支持一整张图片的形式。目前显示占位内容,您可以按照以下步骤添加横幅图片。 + +## 🖼️ 当前状态 + +- ✅ 横幅区域已改为图片容器 +- ✅ 响应式设计已适配 +- ✅ 占位内容显示中 +- ⏳ 等待提供横幅图片 + +## 📐 图片规格建议 + +### 推荐尺寸 +- **桌面端**: 1200px × 400px +- **平板端**: 1024px × 350px +- **移动端**: 768px × 300px +- **小屏手机**: 480px × 250px + +### 文件格式 +- **推荐**: JPG/JPEG (文件小,加载快) +- **支持**: PNG (支持透明背景) +- **支持**: WebP (现代浏览器,更小文件) + +### 文件大小 +- **建议**: < 500KB +- **最大**: < 1MB + +## 🚀 添加图片的方法 + +### 方法一:直接替换(推荐) + +1. **将图片文件放到项目中**: + ``` + public/images/faculty-banner.jpg + ``` + +2. **修改 Faculty.vue 文件**: + ```typescript + // 在文件开头找到这一行 + const bannerImageSrc = ref('') + + // 改为 + const bannerImageSrc = ref('/images/faculty-banner.jpg') + ``` + +### 方法二:使用设置方法 + +在 `src/views/Faculty.vue` 中添加初始化代码: +```typescript +import { ref, computed, onMounted } from 'vue' + +// 在组件挂载时设置图片 +onMounted(() => { + setBannerImage('/images/faculty-banner.jpg') +}) +``` + +## 📁 推荐的文件结构 + +``` +public/ +├── images/ +│ ├── faculty-banner.jpg # 师资力量横幅图片 +│ ├── faculty-banner-tablet.jpg # 平板版本(可选) +│ └── faculty-banner-mobile.jpg # 移动版本(可选) +``` + +## 🎨 图片内容建议 + +根据师资力量页面的特点,横幅图片应该包含: +- 专业的教育背景或学术氛围 +- "师资力量"相关的标题文字 +- 可能包含教师形象或教学场景 +- 体现专业性和权威性的设计元素 +- 与整体网站风格保持一致的色调 + +## 🔧 技术实现细节 + +### 当前代码结构 +```vue + +``` + +### 响应式适配 +- **桌面端**: 400px 高度 +- **平板端**: 350px 高度 +- **移动端**: 300px 高度 +- **小屏手机**: 250px 高度 + +### CSS 样式特点 +```css +.banner-image { + width: 100%; + height: 100%; + object-fit: cover; /* 保持比例,裁剪多余部分 */ + object-position: center; /* 居中显示 */ +} + +.banner-placeholder { + background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); + /* 蓝色渐变背景作为占位 */ +} +``` + +## ⚡ 性能优化建议 + +1. **图片压缩**: 使用工具压缩图片文件 +2. **适当尺寸**: 避免使用过大的图片 +3. **格式选择**: 优先使用JPG格式 +4. **懒加载**: 大图片可考虑懒加载 + +## 🔄 更新步骤 + +当您准备好师资力量横幅图片时: + +1. **准备图片文件** + - 确保图片符合推荐规格 + - 压缩图片文件大小 + - 检查图片内容是否符合师资力量主题 + +2. **上传图片** + - 将图片放到 `public/images/` 目录 + - 确保文件名清晰易懂 + +3. **更新代码** + - 按照上述方法设置图片路径 + - 确保路径正确无误 + +4. **测试效果** + - 访问 `/faculty` 页面查看效果 + - 测试不同设备的显示效果 + - 检查图片加载速度 + +## 🎯 与活动页面的区别 + +师资力量页面与活动页面的横幅设置方法相同,但内容主题不同: +- **活动页面**: 侧重课程活动和学习资源 +- **师资力量页面**: 侧重教师团队和专业实力 + +## 📞 需要帮助? + +如果您在设置师资力量横幅图片时遇到任何问题,请: +1. 检查图片路径是否正确 +2. 确认图片文件是否存在于 `public/images/` 目录 +3. 查看浏览器控制台是否有错误信息 +4. 确保图片格式被浏览器支持 +5. 联系开发人员获取技术支持 + +## 🔗 相关页面 + +- [活动页面横幅设置指南](./Banner-Image-Setup.md) +- [师资力量页面功能说明](./Faculty-Page.md) + +--- + +**注意**: 当前页面显示占位内容,一旦您提供师资力量横幅图片并按照上述步骤设置,占位内容将自动被实际图片替换。页面访问地址:`http://localhost:3000/faculty` diff --git a/index.html b/index.html index edbd3e5..3699cc4 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,10 @@ 在线学习平台 + + + +
diff --git a/package-lock.json b/package-lock.json index 9e9bde3..ce58300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@vicons/ionicons5": "^0.13.0", + "axios": "^1.11.0", + "ckplayer": "^3.1.2", + "hls.js": "^1.6.7", "naive-ui": "^2.42.0", "pinia": "^3.0.3", "vue": "^3.5.17", @@ -1756,6 +1759,23 @@ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/birpc": { "version": "2.5.0", "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz", @@ -1814,6 +1834,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001727", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", @@ -1835,6 +1868,24 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ckplayer": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/ckplayer/-/ckplayer-3.1.2.tgz", + "integrity": "sha512-JHlWTSRm6aqZx+dYdsa6MWz7151omcGBBF9EKK49NL1WCJ2olbdkt7CPKZHvW4lLVgxxEopmuLYEqNdb2cOPhA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1981,6 +2032,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.187", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", @@ -2010,6 +2084,51 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.7", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.7.tgz", @@ -2132,6 +2251,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz", @@ -2162,6 +2317,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2172,6 +2336,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", @@ -2189,6 +2390,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2196,6 +2409,45 @@ "dev": true, "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", @@ -2215,6 +2467,12 @@ "node": ">=12.0.0" } }, + "node_modules/hls.js": { + "version": "1.6.7", + "resolved": "https://registry.npmmirror.com/hls.js/-/hls.js-1.6.7.tgz", + "integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==", + "license": "Apache-2.0" + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", @@ -2424,6 +2682,36 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", @@ -2685,6 +2973,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", diff --git a/package.json b/package.json index 79d1669..65e30c2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ }, "dependencies": { "@vicons/ionicons5": "^0.13.0", + "axios": "^1.11.0", + "ckplayer": "^3.1.2", + "hls.js": "^1.6.7", "naive-ui": "^2.42.0", "pinia": "^3.0.3", "vue": "^3.5.17", diff --git a/src/api/README.md b/src/api/README.md new file mode 100644 index 0000000..8be537f --- /dev/null +++ b/src/api/README.md @@ -0,0 +1,348 @@ +# API 接口文档 + +这是在线学习平台的前端API接口封装,提供了完整的类型定义和请求方法。 + +## 📁 文件结构 + +``` +src/api/ +├── index.ts # 统一导出文件 +├── types.ts # TypeScript 类型定义 +├── request.ts # HTTP 请求封装 +├── utils.ts # 工具函数 +├── modules/ # API 模块 +│ ├── auth.ts # 认证相关API +│ ├── course.ts # 课程相关API +│ ├── comment.ts # 评论相关API +│ ├── favorite.ts # 收藏相关API +│ ├── order.ts # 订单相关API +│ ├── upload.ts # 文件上传API +│ └── statistics.ts # 统计相关API +├── examples/ # 使用示例 +│ └── usage.ts # API使用示例 +└── README.md # 文档说明 +``` + +## 🚀 快速开始 + +### 1. 环境配置 + +复制 `.env.example` 为 `.env.local` 并配置API地址: + +```bash +cp .env.example .env.local +``` + +```env +# API 配置 +VITE_API_BASE_URL=http://localhost:3000/api +VITE_API_TIMEOUT=10000 +``` + +### 2. 基础使用 + +```typescript +import { AuthApi, CourseApi } from '@/api' + +// 用户登录 +const login = async () => { + try { + const response = await AuthApi.login({ + email: 'user@example.com', + password: 'password123' + }) + + if (response.code === 200) { + console.log('登录成功:', response.data) + } + } catch (error) { + console.error('登录失败:', error) + } +} + +// 获取课程列表 +const getCourses = async () => { + try { + const response = await CourseApi.getCourses({ + page: 1, + pageSize: 20, + category: '前端开发' + }) + + if (response.code === 200) { + console.log('课程列表:', response.data) + } + } catch (error) { + console.error('获取课程失败:', error) + } +} +``` + +## 📚 API 模块说明 + +### 认证模块 (AuthApi) + +提供用户认证相关的所有接口: + +- `login()` - 用户登录 +- `register()` - 用户注册 +- `logout()` - 用户登出 +- `getCurrentUser()` - 获取当前用户信息 +- `updateProfile()` - 更新用户资料 +- `changePassword()` - 修改密码 +- `uploadAvatar()` - 上传头像 + +### 课程模块 (CourseApi) + +提供课程相关的所有接口: + +- `getCourses()` - 获取课程列表 +- `searchCourses()` - 搜索课程 +- `getCourseById()` - 获取课程详情 +- `enrollCourse()` - 报名课程 +- `getLearningProgress()` - 获取学习进度 +- `getCourseChapters()` - 获取课程章节 +- `getCourseLessons()` - 获取课程课时 + +### 评论模块 (CommentApi) + +提供评论相关的所有接口: + +- `getCourseComments()` - 获取课程评论 +- `addCourseComment()` - 添加课程评论 +- `likeComment()` - 点赞评论 +- `updateComment()` - 更新评论 +- `deleteComment()` - 删除评论 + +### 收藏模块 (FavoriteApi) + +提供收藏相关的所有接口: + +- `addFavorite()` - 添加收藏 +- `removeFavorite()` - 取消收藏 +- `getMyFavorites()` - 获取收藏列表 +- `checkFavorite()` - 检查收藏状态 + +### 订单模块 (OrderApi) + +提供订单相关的所有接口: + +- `createOrder()` - 创建订单 +- `getOrders()` - 获取订单列表 +- `getOrderById()` - 获取订单详情 +- `cancelOrder()` - 取消订单 +- `confirmPayment()` - 确认支付 + +### 上传模块 (UploadApi) + +提供文件上传相关的所有接口: + +- `uploadFile()` - 上传单个文件 +- `uploadAvatar()` - 上传头像 +- `uploadCourseVideo()` - 上传课程视频 +- `uploadMultipleFiles()` - 批量上传文件 + +### 统计模块 (StatisticsApi) + +提供统计相关的所有接口: + +- `getPlatformStats()` - 获取平台统计 +- `getUserLearningStats()` - 获取用户学习统计 +- `getCourseStats()` - 获取课程统计 + +## 🔧 工具函数 + +### 请求工具 + +```typescript +import { ApiRequest } from '@/api' + +// GET 请求 +const data = await ApiRequest.get('/endpoint', { param: 'value' }) + +// POST 请求 +const result = await ApiRequest.post('/endpoint', { data: 'value' }) + +// 文件上传 +const uploadResult = await ApiRequest.upload('/upload', file, (progress) => { + console.log('上传进度:', progress + '%') +}) +``` + +### 工具函数 + +```typescript +import { + buildQueryString, + formatFileSize, + formatDuration, + isValidEmail, + storage +} from '@/api/utils' + +// 构建查询字符串 +const query = buildQueryString({ page: 1, size: 20 }) + +// 格式化文件大小 +const size = formatFileSize(1024000) // "1000 KB" + +// 格式化时长 +const duration = formatDuration(3661) // "1小时1分1秒" + +// 验证邮箱 +const valid = isValidEmail('user@example.com') // true + +// 本地存储 +storage.set('user', { id: 1, name: 'John' }) +const user = storage.get('user') +``` + +## 🎯 类型定义 + +所有API都有完整的TypeScript类型定义: + +```typescript +import type { + User, + Course, + Comment, + Order, + ApiResponse, + PaginationResponse +} from '@/api' + +// 用户类型 +const user: User = { + id: 1, + username: 'john', + email: 'john@example.com', + role: 'student' +} + +// API响应类型 +const response: ApiResponse = { + code: 200, + message: '成功', + data: user +} + +// 分页响应类型 +const pageResponse: ApiResponse> = { + code: 200, + message: '成功', + data: { + list: [], + total: 100, + page: 1, + pageSize: 20, + totalPages: 5 + } +} +``` + +## 🛠️ 错误处理 + +### 全局错误处理 + +请求拦截器会自动处理常见错误: + +- 401: 自动跳转登录页 +- 403: 显示权限不足提示 +- 500: 显示服务器错误提示 +- 网络错误: 显示网络连接提示 + +### 自定义错误处理 + +```typescript +import { getErrorMessage } from '@/api/utils' + +try { + const response = await AuthApi.login(credentials) +} catch (error) { + const message = getErrorMessage(error) + console.error('登录失败:', message) + // 显示错误提示 +} +``` + +## 📝 最佳实践 + +### 1. 使用TypeScript类型 + +```typescript +import type { Course, ApiResponse } from '@/api' + +const handleCourseData = (response: ApiResponse) => { + if (response.code === 200) { + response.data.forEach(course => { + // TypeScript 会提供完整的类型提示 + console.log(course.title, course.instructor.name) + }) + } +} +``` + +### 2. 错误边界处理 + +```typescript +const fetchData = async () => { + try { + const response = await CourseApi.getCourses() + return response.data + } catch (error) { + // 记录错误日志 + console.error('API Error:', error) + // 返回默认值或重新抛出错误 + throw error + } +} +``` + +### 3. 加载状态管理 + +```typescript +const loading = ref(false) + +const loadCourses = async () => { + loading.value = true + try { + const response = await CourseApi.getCourses() + // 处理数据 + } catch (error) { + // 处理错误 + } finally { + loading.value = false + } +} +``` + +### 4. 分页数据处理 + +```typescript +import { formatPaginationData } from '@/api/utils' + +const loadCourses = async (page: number = 1) => { + try { + const response = await CourseApi.getCourses({ page, pageSize: 20 }) + const pagination = formatPaginationData(response) + + console.log('课程列表:', pagination.items) + console.log('分页信息:', { + current: pagination.currentPage, + total: pagination.totalPages, + hasNext: pagination.hasNext + }) + } catch (error) { + console.error('加载失败:', error) + } +} +``` + +## 🔄 更新日志 + +### v1.0.0 +- 初始版本 +- 完整的API接口封装 +- TypeScript类型定义 +- 错误处理机制 +- 工具函数库 diff --git a/src/api/examples/usage.ts b/src/api/examples/usage.ts new file mode 100644 index 0000000..889cdaf --- /dev/null +++ b/src/api/examples/usage.ts @@ -0,0 +1,371 @@ +// API 使用示例文件 +// 展示如何在组件中使用各种API接口 + +import { + AuthApi, + CourseApi, + CommentApi, + FavoriteApi, + OrderApi, + UploadApi, + StatisticsApi +} from '@/api' + +// ===== 认证相关示例 ===== + +// 用户登录示例 +export const loginExample = async () => { + try { + const response = await AuthApi.login({ + email: 'user@example.com', + password: 'password123' + }) + + if (response.code === 200) { + const { user, token } = response.data + // 保存用户信息和token + localStorage.setItem('token', token) + localStorage.setItem('user', JSON.stringify(user)) + console.log('登录成功:', user) + } + } catch (error) { + console.error('登录失败:', error) + } +} + +// 用户注册示例 +export const registerExample = async () => { + try { + const response = await AuthApi.register({ + username: 'newuser', + email: 'newuser@example.com', + password: 'password123', + confirmPassword: 'password123', + captcha: 'abc123' + }) + + if (response.code === 200) { + console.log('注册成功:', response.data) + } + } catch (error) { + console.error('注册失败:', error) + } +} + +// 获取当前用户信息示例 +export const getCurrentUserExample = async () => { + try { + const response = await AuthApi.getCurrentUser() + if (response.code === 200) { + console.log('用户信息:', response.data) + return response.data + } + } catch (error) { + console.error('获取用户信息失败:', error) + } +} + +// ===== 课程相关示例 ===== + +// 获取课程列表示例 +export const getCoursesExample = async () => { + try { + const response = await CourseApi.getCourses({ + page: 1, + pageSize: 20, + category: '前端开发', + level: 'intermediate', + sortBy: 'rating' + }) + + if (response.code === 200) { + const { list, total, page, pageSize } = response.data + console.log('课程列表:', list) + console.log('总数:', total) + return response.data + } + } catch (error) { + console.error('获取课程列表失败:', error) + } +} + +// 搜索课程示例 +export const searchCoursesExample = async () => { + try { + const response = await CourseApi.searchCourses({ + keyword: 'Vue.js', + category: '前端开发', + level: 'intermediate', + price: 'paid', + rating: 4, + sortBy: 'rating', + page: 1, + pageSize: 10 + }) + + if (response.code === 200) { + console.log('搜索结果:', response.data) + return response.data + } + } catch (error) { + console.error('搜索课程失败:', error) + } +} + +// 获取课程详情示例 +export const getCourseDetailExample = async (courseId: number) => { + try { + const response = await CourseApi.getCourseById(courseId) + if (response.code === 200) { + console.log('课程详情:', response.data) + return response.data + } + } catch (error) { + console.error('获取课程详情失败:', error) + } +} + +// 报名课程示例 +export const enrollCourseExample = async (courseId: number) => { + try { + const response = await CourseApi.enrollCourse(courseId) + if (response.code === 200) { + console.log('报名成功:', response.data) + return response.data + } + } catch (error) { + console.error('报名失败:', error) + } +} + +// 获取学习进度示例 +export const getLearningProgressExample = async (courseId: number) => { + try { + const response = await CourseApi.getLearningProgress(courseId) + if (response.code === 200) { + console.log('学习进度:', response.data) + return response.data + } + } catch (error) { + console.error('获取学习进度失败:', error) + } +} + +// ===== 评论相关示例 ===== + +// 获取课程评论示例 +export const getCourseCommentsExample = async (courseId: number) => { + try { + const response = await CommentApi.getCourseComments(courseId, { + page: 1, + pageSize: 10, + sortBy: 'newest' + }) + + if (response.code === 200) { + console.log('课程评论:', response.data) + return response.data + } + } catch (error) { + console.error('获取课程评论失败:', error) + } +} + +// 添加课程评论示例 +export const addCourseCommentExample = async (courseId: number) => { + try { + const response = await CommentApi.addCourseComment(courseId, { + content: '这门课程非常棒,讲解清晰,内容丰富!', + rating: 5 + }) + + if (response.code === 200) { + console.log('评论添加成功:', response.data) + return response.data + } + } catch (error) { + console.error('添加评论失败:', error) + } +} + +// 点赞评论示例 +export const likeCommentExample = async (commentId: number) => { + try { + const response = await CommentApi.likeComment(commentId) + if (response.code === 200) { + console.log('点赞成功:', response.data) + return response.data + } + } catch (error) { + console.error('点赞失败:', error) + } +} + +// ===== 收藏相关示例 ===== + +// 添加收藏示例 +export const addFavoriteExample = async (courseId: number) => { + try { + const response = await FavoriteApi.addFavorite(courseId) + if (response.code === 200) { + console.log('收藏成功:', response.data) + return response.data + } + } catch (error) { + console.error('收藏失败:', error) + } +} + +// 获取收藏列表示例 +export const getFavoritesExample = async () => { + try { + const response = await FavoriteApi.getMyFavorites({ + page: 1, + pageSize: 20, + sortBy: 'newest' + }) + + if (response.code === 200) { + console.log('收藏列表:', response.data) + return response.data + } + } catch (error) { + console.error('获取收藏列表失败:', error) + } +} + +// ===== 订单相关示例 ===== + +// 创建订单示例 +export const createOrderExample = async (courseIds: number[]) => { + try { + const response = await OrderApi.createOrder({ + courseIds, + couponCode: 'DISCOUNT10', + paymentMethod: 'alipay' + }) + + if (response.code === 200) { + console.log('订单创建成功:', response.data) + return response.data + } + } catch (error) { + console.error('创建订单失败:', error) + } +} + +// 获取订单列表示例 +export const getOrdersExample = async () => { + try { + const response = await OrderApi.getOrders({ + page: 1, + pageSize: 10, + status: 'paid' + }) + + if (response.code === 200) { + console.log('订单列表:', response.data) + return response.data + } + } catch (error) { + console.error('获取订单列表失败:', error) + } +} + +// ===== 文件上传示例 ===== + +// 上传头像示例 +export const uploadAvatarExample = async (file: File) => { + try { + const response = await UploadApi.uploadAvatar(file, (progress) => { + console.log('上传进度:', progress + '%') + }) + + if (response.code === 200) { + console.log('头像上传成功:', response.data) + return response.data + } + } catch (error) { + console.error('头像上传失败:', error) + } +} + +// 上传课程视频示例 +export const uploadCourseVideoExample = async (file: File, courseId: number) => { + try { + const response = await UploadApi.uploadCourseVideo(file, courseId, undefined, (progress) => { + console.log('视频上传进度:', progress + '%') + }) + + if (response.code === 200) { + console.log('视频上传成功:', response.data) + return response.data + } + } catch (error) { + console.error('视频上传失败:', error) + } +} + +// ===== 统计相关示例 ===== + +// 获取平台统计示例 +export const getPlatformStatsExample = async () => { + try { + const response = await StatisticsApi.getPlatformStats() + if (response.code === 200) { + console.log('平台统计:', response.data) + return response.data + } + } catch (error) { + console.error('获取平台统计失败:', error) + } +} + +// 获取用户学习统计示例 +export const getUserLearningStatsExample = async () => { + try { + const response = await StatisticsApi.getUserLearningStats() + if (response.code === 200) { + console.log('用户学习统计:', response.data) + return response.data + } + } catch (error) { + console.error('获取用户学习统计失败:', error) + } +} + +// ===== 错误处理示例 ===== + +// 统一错误处理函数 +export const handleApiError = (error: any) => { + if (error.response) { + // 服务器返回错误状态码 + const { status, data } = error.response + switch (status) { + case 400: + console.error('请求参数错误:', data.message) + break + case 401: + console.error('未授权访问,请重新登录') + // 跳转到登录页 + break + case 403: + console.error('没有权限访问') + break + case 404: + console.error('请求的资源不存在') + break + case 500: + console.error('服务器内部错误') + break + default: + console.error('请求失败:', data.message || '未知错误') + } + } else if (error.request) { + // 网络错误 + console.error('网络错误,请检查网络连接') + } else { + // 其他错误 + console.error('请求配置错误:', error.message) + } +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..cb4d166 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,264 @@ +// API 统一导出文件 +export * from './types' +export * from './request' + +// 导出所有API模块 +export { default as AuthApi } from './modules/auth' +export { default as CourseApi } from './modules/course' +export { default as CommentApi } from './modules/comment' +export { default as FavoriteApi } from './modules/favorite' +export { default as OrderApi } from './modules/order' +export { default as UploadApi } from './modules/upload' +export { default as StatisticsApi } from './modules/statistics' + +// API 基础配置 +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api' + +// API 端点配置 +export const API_ENDPOINTS = { + // 认证相关 + AUTH: { + LOGIN: '/auth/login', + REGISTER: '/auth/register', + LOGOUT: '/auth/logout', + REFRESH: '/auth/refresh', + ME: '/auth/me', + PROFILE: '/auth/profile', + CHANGE_PASSWORD: '/auth/change-password', + FORGOT_PASSWORD: '/auth/forgot-password', + RESET_PASSWORD: '/auth/reset-password', + VERIFY_EMAIL: '/auth/verify-email', + VERIFY_PHONE: '/auth/verify-phone', + UPLOAD_AVATAR: '/auth/upload-avatar', + }, + + // 课程相关 + COURSES: { + LIST: '/courses', + SEARCH: '/courses/search', + POPULAR: '/courses/popular', + LATEST: '/courses/latest', + RECOMMENDED: '/courses/recommended', + DETAIL: '/courses/:id', + CHAPTERS: '/courses/:id/chapters', + LESSONS: '/courses/:id/lessons', + ENROLL: '/courses/:id/enroll', + PROGRESS: '/courses/:id/progress', + PREVIEW: '/courses/:id/preview', + STATS: '/courses/:id/stats', + RELATED: '/courses/:id/related', + ACCESS: '/courses/:id/access', + }, + + // 分类相关 + CATEGORIES: { + LIST: '/categories', + COURSES: '/categories/:id/courses', + }, + + // 章节课时相关 + CHAPTERS: { + DETAIL: '/chapters/:id', + }, + + LESSONS: { + DETAIL: '/lessons/:id', + RESOURCES: '/lessons/:id/resources', + COMPLETE: '/lessons/:id/complete', + }, + + // 讲师相关 + INSTRUCTORS: { + DETAIL: '/instructors/:id', + COURSES: '/instructors/:id/courses', + FOLLOW: '/instructors/:id/follow', + }, + + // 测验相关 + QUIZZES: { + LIST: '/courses/:id/quizzes', + DETAIL: '/quizzes/:id', + SUBMIT: '/quizzes/:id/submit', + RESULTS: '/quizzes/:id/results', + }, + + // 评论相关 + COMMENTS: { + COURSE: '/courses/:id/comments', + LESSON: '/lessons/:id/comments', + DETAIL: '/comments/:id', + REPLIES: '/comments/:id/replies', + LIKE: '/comments/:id/like', + DISLIKE: '/comments/:id/dislike', + HELPFUL: '/comments/:id/helpful', + REPORT: '/comments/:id/report', + MY_COMMENTS: '/my-comments', + STATS: '/comments/stats', + }, + + // 收藏相关 + FAVORITES: { + LIST: '/favorites', + ADD: '/favorites', + REMOVE: '/favorites/:id', + CHECK: '/favorites/check/:id', + BATCH: '/favorites/batch', + STATS: '/favorites/stats', + EXPORT: '/favorites/export', + IMPORT: '/favorites/import', + FOLDERS: '/favorite-folders', + }, + + // 订单相关 + ORDERS: { + LIST: '/orders', + CREATE: '/orders', + DETAIL: '/orders/:id', + BY_NO: '/orders/no/:orderNo', + CANCEL: '/orders/:id/cancel', + CONFIRM_PAYMENT: '/orders/:id/confirm-payment', + REFUND: '/orders/:id/refund', + INVOICE: '/orders/:id/invoice', + STATS: '/orders/stats', + CALCULATE: '/orders/calculate', + }, + + // 支付相关 + PAYMENT: { + METHODS: '/payment-methods', + STATUS: '/orders/:id/payment-status', + RETRY: '/orders/:id/retry-payment', + }, + + // 优惠券相关 + COUPONS: { + VALIDATE: '/coupons/validate', + AVAILABLE: '/coupons/available', + }, + + // 退款相关 + REFUNDS: { + LIST: '/refunds', + DETAIL: '/refunds/:id', + }, + + // 上传相关 + UPLOAD: { + FILE: '/upload/:type', + AVATAR: '/upload/avatar', + COURSE_THUMBNAIL: '/upload/course-thumbnail', + COURSE_VIDEO: '/upload/course-video', + COURSE_RESOURCE: '/upload/course-resource', + MULTIPLE: '/upload/multiple/:type', + CONFIG: '/upload/config', + TOKEN: '/upload/token/:type', + COMPRESS: '/upload/compress-image', + THUMBNAIL: '/upload/generate-thumbnail', + HISTORY: '/upload/history', + }, + + // 统计相关 + STATISTICS: { + PLATFORM: '/statistics/platform', + USER_LEARNING: '/statistics/user-learning', + COURSE: '/statistics/course/:id', + INSTRUCTOR: '/statistics/instructor/:id', + LEARNING_PROGRESS: '/statistics/learning-progress', + REVENUE: '/statistics/revenue', + USER_BEHAVIOR: '/statistics/user-behavior', + SEARCH: '/statistics/search', + CONTENT: '/statistics/content', + COMMENTS: '/statistics/comments', + EXPORT: '/statistics/export/:type', + }, + + // 学习进度相关 + LEARNING: { + PROGRESS: '/learning-progress', + MY_COURSES: '/my-courses', + }, + + // 资源相关 + RESOURCES: { + DOWNLOAD: '/resources/:id/download', + }, +} + +// 请求配置 +export const REQUEST_CONFIG = { + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +} + +// HTTP状态码 +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + VALIDATION_ERROR: 422, + TOO_MANY_REQUESTS: 429, + INTERNAL_ERROR: 500, + BAD_GATEWAY: 502, + SERVICE_UNAVAILABLE: 503, + GATEWAY_TIMEOUT: 504, +} + +// 业务状态码 +export const BUSINESS_CODE = { + SUCCESS: 0, + FAILED: 1, + INVALID_PARAMS: 1001, + UNAUTHORIZED: 1002, + FORBIDDEN: 1003, + NOT_FOUND: 1004, + ALREADY_EXISTS: 1005, + OPERATION_FAILED: 1006, + VALIDATION_FAILED: 1007, + RATE_LIMITED: 1008, + MAINTENANCE: 1009, +} + +// 常用的API响应消息 +export const API_MESSAGES = { + SUCCESS: '操作成功', + FAILED: '操作失败', + INVALID_PARAMS: '参数错误', + UNAUTHORIZED: '未授权访问', + FORBIDDEN: '禁止访问', + NOT_FOUND: '资源不存在', + ALREADY_EXISTS: '资源已存在', + NETWORK_ERROR: '网络错误', + SERVER_ERROR: '服务器错误', + TIMEOUT: '请求超时', + UNKNOWN_ERROR: '未知错误', +} + +// 分页默认配置 +export const PAGINATION_CONFIG = { + DEFAULT_PAGE: 1, + DEFAULT_PAGE_SIZE: 20, + MAX_PAGE_SIZE: 100, +} + +// 文件上传配置 +export const UPLOAD_CONFIG = { + MAX_FILE_SIZE: 100 * 1024 * 1024, // 100MB + ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + ALLOWED_VIDEO_TYPES: ['video/mp4', 'video/avi', 'video/mov', 'video/wmv'], + ALLOWED_DOCUMENT_TYPES: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + ALLOWED_AUDIO_TYPES: ['audio/mp3', 'audio/wav', 'audio/ogg'], +} + +// 缓存配置 +export const CACHE_CONFIG = { + DEFAULT_TTL: 5 * 60 * 1000, // 5分钟 + USER_INFO_TTL: 30 * 60 * 1000, // 30分钟 + COURSE_LIST_TTL: 10 * 60 * 1000, // 10分钟 + STATIC_DATA_TTL: 60 * 60 * 1000, // 1小时 +} diff --git a/src/api/modules/auth.ts b/src/api/modules/auth.ts new file mode 100644 index 0000000..9d3abda --- /dev/null +++ b/src/api/modules/auth.ts @@ -0,0 +1,224 @@ +// 认证相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + User, + LoginRequest, + LoginResponse, + BackendLoginResponse, + RegisterRequest, + UserProfile, +} from '../types' + +/** + * 认证API模块 + */ +export class AuthApi { + // 用户登录 + static async login(data: LoginRequest): Promise> { + try { + // 调用后端API + const response = await ApiRequest.post('/users/login', data) + + // 适配后端响应格式为前端期望的格式 + const adaptedResponse: ApiResponse = { + code: response.code, + message: response.message, + data: { + user: { + id: response.data.id, // 使用后端返回的用户ID + email: data.email || '', + phone: data.phone || '', + username: data.phone || data.email?.split('@')[0] || 'user', + nickname: '用户', + avatar: '', + role: 'student', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }, + token: response.data.token, + refreshToken: '', // 后端没有返回,使用空字符串 + expiresIn: 3600 // 默认1小时,可以根据expires字段计算 + } + } + + return adaptedResponse + } catch (error) { + throw error + } + } + + // 用户注册 + static register(data: RegisterRequest): Promise> { + return ApiRequest.post('/auth/register', data) + } + + // 用户登出 + static logout(): Promise> { + return ApiRequest.post('/auth/logout') + } + + // 刷新Token + static refreshToken(refreshToken: string): Promise> { + return ApiRequest.post('/auth/refresh', { refreshToken }) + } + + // 获取当前用户信息 + static getCurrentUser(): Promise> { + return ApiRequest.get('/auth/me') + } + + // 更新用户资料 + static updateProfile(data: Partial): Promise> { + return ApiRequest.put('/auth/profile', data) + } + + // 修改密码 + static changePassword(data: { + oldPassword: string + newPassword: string + confirmPassword: string + }): Promise> { + return ApiRequest.post('/auth/change-password', data) + } + + // 忘记密码 - 发送重置邮件 + static forgotPassword(email: string): Promise> { + return ApiRequest.post('/auth/forgot-password', { email }) + } + + // 重置密码 + static resetPassword(data: { + token: string + password: string + confirmPassword: string + }): Promise> { + return ApiRequest.post('/auth/reset-password', data) + } + + // 发送邮箱验证码 + static sendEmailVerification(email: string): Promise> { + return ApiRequest.post('/auth/send-email-verification', { email }) + } + + // 验证邮箱 + static verifyEmail(data: { + email: string + code: string + }): Promise> { + return ApiRequest.post('/auth/verify-email', data) + } + + // 发送手机验证码 + static sendSmsVerification(phone: string): Promise> { + return ApiRequest.post('/auth/send-sms-verification', { phone }) + } + + // 验证手机号 + static verifyPhone(data: { + phone: string + code: string + }): Promise> { + return ApiRequest.post('/auth/verify-phone', data) + } + + // 绑定第三方账号 + static bindThirdParty(data: { + provider: 'wechat' | 'qq' | 'weibo' | 'github' + code: string + state?: string + }): Promise> { + return ApiRequest.post('/auth/bind-third-party', data) + } + + // 解绑第三方账号 + static unbindThirdParty(provider: string): Promise> { + return ApiRequest.delete(`/auth/unbind-third-party/${provider}`) + } + + // 获取第三方登录URL + static getThirdPartyLoginUrl(provider: string, redirectUrl?: string): Promise> { + return ApiRequest.get(`/auth/third-party-login-url/${provider}`, { redirectUrl }) + } + + // 第三方登录回调 + static thirdPartyLoginCallback(data: { + provider: string + code: string + state?: string + }): Promise> { + return ApiRequest.post('/auth/third-party-login-callback', data) + } + + // 上传头像 + static uploadAvatar(file: File, onProgress?: (progress: number) => void): Promise> { + return ApiRequest.upload('/auth/upload-avatar', file, onProgress) + } + + // 删除账号 + static deleteAccount(password: string): Promise> { + return ApiRequest.post('/auth/delete-account', { password }) + } + + // 获取账号安全信息 + static getSecurityInfo(): Promise + loginHistory: Array<{ + ip: string + location: string + device: string + loginTime: string + }> + }>> { + return ApiRequest.get('/auth/security-info') + } + + // 启用两步验证 + static enableTwoFactor(): Promise> { + return ApiRequest.post('/auth/enable-two-factor') + } + + // 确认启用两步验证 + static confirmTwoFactor(code: string): Promise> { + return ApiRequest.post('/auth/confirm-two-factor', { code }) + } + + // 禁用两步验证 + static disableTwoFactor(data: { + password: string + code: string + }): Promise> { + return ApiRequest.post('/auth/disable-two-factor', data) + } + + // 生成新的备用码 + static generateBackupCodes(): Promise> { + return ApiRequest.post('/auth/generate-backup-codes') + } + + // 验证两步验证码 + static verifyTwoFactor(code: string): Promise> { + return ApiRequest.post('/auth/verify-two-factor', { code }) + } +} + +export default AuthApi diff --git a/src/api/modules/comment.ts b/src/api/modules/comment.ts new file mode 100644 index 0000000..f54a499 --- /dev/null +++ b/src/api/modules/comment.ts @@ -0,0 +1,198 @@ +// 评论相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + PaginationResponse, + Comment, +} from '../types' + +/** + * 评论API模块 + */ +export class CommentApi { + // 获取课程评论 + static getCourseComments(courseId: number, params?: { + page?: number + pageSize?: number + sortBy?: 'newest' | 'oldest' | 'rating' | 'helpful' + rating?: number + }): Promise>> { + return ApiRequest.get(`/courses/${courseId}/comments`, params) + } + + // 获取课时评论 + static getLessonComments(lessonId: number, params?: { + page?: number + pageSize?: number + sortBy?: 'newest' | 'oldest' | 'helpful' + }): Promise>> { + return ApiRequest.get(`/lessons/${lessonId}/comments`, params) + } + + // 添加课程评论 + static addCourseComment(courseId: number, data: { + content: string + rating?: number + parentId?: number + }): Promise> { + return ApiRequest.post(`/courses/${courseId}/comments`, data) + } + + // 添加课时评论 + static addLessonComment(lessonId: number, data: { + content: string + parentId?: number + }): Promise> { + return ApiRequest.post(`/lessons/${lessonId}/comments`, data) + } + + // 更新评论 + static updateComment(commentId: number, data: { + content: string + rating?: number + }): Promise> { + return ApiRequest.put(`/comments/${commentId}`, data) + } + + // 删除评论 + static deleteComment(commentId: number): Promise> { + return ApiRequest.delete(`/comments/${commentId}`) + } + + // 点赞评论 + static likeComment(commentId: number): Promise> { + return ApiRequest.post(`/comments/${commentId}/like`) + } + + // 取消点赞评论 + static unlikeComment(commentId: number): Promise> { + return ApiRequest.delete(`/comments/${commentId}/like`) + } + + // 踩评论 + static dislikeComment(commentId: number): Promise> { + return ApiRequest.post(`/comments/${commentId}/dislike`) + } + + // 取消踩评论 + static undislikeComment(commentId: number): Promise> { + return ApiRequest.delete(`/comments/${commentId}/dislike`) + } + + // 举报评论 + static reportComment(commentId: number, data: { + reason: string + description?: string + }): Promise> { + return ApiRequest.post(`/comments/${commentId}/report`, data) + } + + // 获取评论回复 + static getCommentReplies(commentId: number, params?: { + page?: number + pageSize?: number + }): Promise>> { + return ApiRequest.get(`/comments/${commentId}/replies`, params) + } + + // 获取我的评论 + static getMyComments(params?: { + page?: number + pageSize?: number + type?: 'course' | 'lesson' + }): Promise>> { + return ApiRequest.get('/my-comments', params) + } + + // 获取评论统计 + static getCommentStats(courseId?: number, lessonId?: number): Promise + recentComments: Comment[] + }>> { + const params: any = {} + if (courseId) params.courseId = courseId + if (lessonId) params.lessonId = lessonId + + return ApiRequest.get('/comments/stats', params) + } + + // 标记评论为有用 + static markCommentHelpful(commentId: number): Promise> { + return ApiRequest.post(`/comments/${commentId}/helpful`) + } + + // 取消标记评论为有用 + static unmarkCommentHelpful(commentId: number): Promise> { + return ApiRequest.delete(`/comments/${commentId}/helpful`) + } + + // 置顶评论(管理员功能) + static pinComment(commentId: number): Promise> { + return ApiRequest.post(`/comments/${commentId}/pin`) + } + + // 取消置顶评论(管理员功能) + static unpinComment(commentId: number): Promise> { + return ApiRequest.delete(`/comments/${commentId}/pin`) + } + + // 审核评论(管理员功能) + static moderateComment(commentId: number, action: 'approve' | 'reject' | 'hide'): Promise> { + return ApiRequest.post(`/comments/${commentId}/moderate`, { action }) + } + + // 批量删除评论(管理员功能) + static batchDeleteComments(commentIds: number[]): Promise> { + return ApiRequest.post('/comments/batch-delete', { commentIds }) + } + + // 获取待审核评论(管理员功能) + static getPendingComments(params?: { + page?: number + pageSize?: number + }): Promise>> { + return ApiRequest.get('/comments/pending', params) + } + + // 获取被举报的评论(管理员功能) + static getReportedComments(params?: { + page?: number + pageSize?: number + }): Promise + }>>> { + return ApiRequest.get('/comments/reported', params) + } +} + +export default CommentApi diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts new file mode 100644 index 0000000..2063067 --- /dev/null +++ b/src/api/modules/course.ts @@ -0,0 +1,528 @@ +// 课程相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + PaginationResponse, + Course, + CourseCategory, + Chapter, + Lesson, + LessonResource, + CourseSection, + CourseSectionListResponse, + BackendCourseSection, + BackendCourseSectionListResponse, + Quiz, + QuizQuestion, + LearningProgress, + SearchRequest, + Instructor, + BackendCourse, + BackendCourseListResponse, + CourseListRequest, +} from '../types' + +/** + * 课程API模块 + */ +export class CourseApi { + + /** + * 格式化时间戳为ISO字符串 + */ + private static formatTimestamp(timestamp: number | null | undefined): string { + if (!timestamp || timestamp <= 0) { + return new Date().toISOString() + } + + try { + // 如果时间戳是秒级的,转换为毫秒级 + const ms = timestamp < 10000000000 ? timestamp * 1000 : timestamp + const date = new Date(ms) + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + return new Date().toISOString() + } + + return date.toISOString() + } catch (error) { + console.error('时间戳格式化失败:', error) + return new Date().toISOString() + } + } + + /** + * 计算课程时长 + */ + private static calculateDuration(startTime: string, endTime: string): string { + try { + const start = new Date(startTime) + const end = new Date(endTime) + const diffMs = end.getTime() - start.getTime() + const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)) + return `${diffDays}天` + } catch (error) { + return '待定' + } + } + // 获取课程列表 - 适配后端接口 + static async getCourses(params?: CourseListRequest): Promise>> { + try { + console.log('调用课程列表API,参数:', params) + + // 构建查询参数,根据API文档的参数名称 + const queryParams: any = {} + if (params?.categoryId) queryParams.categoryId = params.categoryId + if (params?.difficulty !== undefined) queryParams.difficulty = params.difficulty + if (params?.subject) queryParams.subject = params.subject + if (params?.page) queryParams.page = params.page + if (params?.pageSize) queryParams.pageSize = params.pageSize + if (params?.keyword) queryParams.keyword = params.keyword + + // 调用后端API + const response = await ApiRequest.get('/lesson/list', queryParams) + console.log('课程列表API响应:', response) + + // 适配后端响应格式为前端期望的格式 + const adaptedCourses: Course[] = response.data.list.map((backendCourse: BackendCourse) => ({ + id: backendCourse.id, + title: backendCourse.name, + description: backendCourse.description, + thumbnail: backendCourse.cover, + coverImage: backendCourse.cover, + price: parseFloat(backendCourse.price || '0'), + originalPrice: parseFloat(backendCourse.price || '0'), + currency: 'CNY', + rating: 4.5, // 默认评分,后端没有返回 + ratingCount: 0, // 默认评分数量 + studentsCount: 0, // 默认学生数量 + duration: '待定', // 默认时长 + totalLessons: 0, // 默认课程数量 + level: this.mapDifficulty(backendCourse.difficulty || 0), + language: 'zh-CN', + category: { + id: backendCourse.categoryId, + name: this.getCategoryName(backendCourse.categoryId), + slug: 'category-' + backendCourse.categoryId + }, + tags: backendCourse.subject ? [backendCourse.subject] : [], + skills: [], + requirements: backendCourse.prerequisite ? [backendCourse.prerequisite] : [], + objectives: backendCourse.target ? [backendCourse.target] : [], + instructor: { + id: backendCourse.teacherId || 0, + name: backendCourse.school || '未知讲师', + title: '讲师', + bio: '', + avatar: '', + rating: 4.5, + studentsCount: 0, + coursesCount: 0, + experience: '', + education: [], + certifications: [] + }, + status: 'published' as const, + createdAt: this.formatTimestamp(backendCourse.createdTime), + updatedAt: this.formatTimestamp(backendCourse.updatedTime), + publishedAt: this.formatTimestamp(backendCourse.createdTime) + })) + + const adaptedResponse: ApiResponse> = { + code: response.code, + message: response.message, + data: { + list: adaptedCourses, + total: response.data.total, + page: params?.page || 1, + pageSize: params?.pageSize || 10, + totalPages: Math.ceil(response.data.total / (params?.pageSize || 10)) + } + } + + return adaptedResponse + } catch (error) { + throw error + } + } + + // 搜索课程 + static searchCourses(params: SearchRequest): Promise>> { + return ApiRequest.get('/courses/search', params) + } + + // 获取热门课程 + static getPopularCourses(limit?: number): Promise> { + return ApiRequest.get('/courses/popular', { limit }) + } + + // 获取最新课程 + static getLatestCourses(limit?: number): Promise> { + return ApiRequest.get('/courses/latest', { limit }) + } + + // 获取推荐课程 + static getRecommendedCourses(userId?: number, limit?: number): Promise> { + return ApiRequest.get('/courses/recommended', { userId, limit }) + } + + // 获取课程详情 - 适配后端接口 + static async getCourseById(id: number): Promise> { + try { + // 调用后端课程详情接口 + const response = await ApiRequest.get('/lesson/detail', { id }) + + // 适配数据格式 + const adaptedCourse: Course = { + id: response.data.id, + title: response.data.name, + description: response.data.description, + content: response.data.outline, // 使用 outline 作为课程内容 + thumbnail: response.data.cover, + coverImage: response.data.cover, + price: parseFloat(response.data.price), + originalPrice: parseFloat(response.data.price), + currency: 'CNY', + rating: 4.5, + ratingCount: 0, + studentsCount: 0, + duration: this.calculateDuration(response.data.startTime, response.data.endTime), + totalLessons: 0, + level: 'beginner' as const, + language: 'zh-CN', + category: { + id: response.data.categoryId, + name: '未分类', + slug: 'uncategorized' + }, + tags: [], + skills: [], + requirements: response.data.prerequisite ? [response.data.prerequisite] : [], + objectives: response.data.target ? [response.data.target] : [], + instructor: { + id: response.data.teacherId || 0, + name: response.data.school || '未知讲师', + title: '讲师', + bio: response.data.position || '', + avatar: '', + rating: 4.5, + studentsCount: 0, + coursesCount: 0, + experience: response.data.arrangement || '', + education: [], + certifications: [] + }, + status: 'published' as const, + createdAt: this.formatTimestamp(response.data.createdAt), + updatedAt: this.formatTimestamp(response.data.updatedAt), + publishedAt: response.data.startTime + } + + return { + code: response.code, + message: response.message, + data: adaptedCourse + } + } catch (error) { + throw error + } + } + + // 获取课程章节 + static getCourseChapters(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/chapters`) + } + + // 获取课程所有课时 + static getCourseLessons(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/lessons`) + } + + // 获取章节详情 + static getChapterById(id: number): Promise> { + return ApiRequest.get(`/chapters/${id}`) + } + + // 获取课时详情 + static getLessonById(id: number): Promise> { + return ApiRequest.get(`/lessons/${id}`) + } + + // 获取课时资源 + static getLessonResources(lessonId: number): Promise> { + return ApiRequest.get(`/lessons/${lessonId}/resources`) + } + + // 获取课程分类 + static getCategories(): Promise> { + return ApiRequest.get('/categories') + } + + // 获取分类下的课程 + static getCoursesByCategory(categoryId: number, params?: { + page?: number + pageSize?: number + sortBy?: string + }): Promise>> { + return ApiRequest.get(`/categories/${categoryId}/courses`, params) + } + + // 报名课程 + static enrollCourse(courseId: number): Promise> { + return ApiRequest.post(`/courses/${courseId}/enroll`) + } + + // 取消报名 + static unenrollCourse(courseId: number): Promise> { + return ApiRequest.delete(`/courses/${courseId}/enroll`) + } + + // 获取课程章节列表 + static async getCourseSections(lessonId: number): Promise> { + try { + console.log('尝试从API获取课程章节数据,课程ID:', lessonId) + console.log('API请求URL: /lesson/section/list') + console.log('API请求参数:', { lesson_id: lessonId.toString() }) + + const backendResponse = await ApiRequest.get('/lesson/section/list', { lesson_id: lessonId.toString() }) + console.log('章节API响应:', backendResponse) + console.log('响应状态码:', backendResponse.code) + console.log('响应消息:', backendResponse.message) + console.log('原始章节数据:', backendResponse.data?.list) + console.log('章节数据数量:', backendResponse.data?.list?.length || 0) + + // 检查数据是否存在 + if (!backendResponse.data || !backendResponse.data.list) { + console.warn('API返回的数据结构不正确:', backendResponse.data) + return { + code: backendResponse.code, + message: backendResponse.message, + data: { + list: [], + timestamp: Date.now(), + traceId: backendResponse.timestamp?.toString() || '' + }, + timestamp: backendResponse.timestamp?.toString() + } + } + + // 适配数据格式 + const adaptedSections: CourseSection[] = backendResponse.data.list.map((section: BackendCourseSection) => ({ + id: section.id, + lessonId: section.lessonId, + outline: section.videoUrl, // 将videoUrl映射到outline + name: section.name, + parentId: section.parentId, + sort: section.sortOrder, // 将sortOrder映射到sort + level: section.level === 0 ? 1 : 0, // 转换level逻辑:API中0=子级,1=父级;前端中0=父级,1=子级 + revision: section.revision, + createdAt: section.createdTime ? new Date(section.createdTime).getTime() : null, + updatedAt: section.updatedTime ? new Date(section.updatedTime).getTime() : null, + deletedAt: null, + completed: false, + duration: undefined + })) + + console.log('适配后的章节数据:', adaptedSections) + + const adaptedResponse: ApiResponse = { + code: backendResponse.code, + message: backendResponse.message, + data: { + list: adaptedSections, + timestamp: Date.now(), + traceId: backendResponse.timestamp?.toString() || '' + }, + timestamp: backendResponse.timestamp?.toString() + } + + return adaptedResponse + } catch (error) { + console.error('章节API调用失败:', error) + console.error('错误详情:', { + message: (error as Error).message, + stack: (error as Error).stack, + response: (error as any).response?.data, + status: (error as any).response?.status, + statusText: (error as any).response?.statusText + }) + + // 重新抛出错误,不使用模拟数据 + throw error + } + } + + // 获取我的课程 + static getMyCourses(params?: { + page?: number + pageSize?: number + status?: 'all' | 'in_progress' | 'completed' + }): Promise>> { + return ApiRequest.get('/my-courses', params) + } + + // 获取学习进度 + static getLearningProgress(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/progress`) + } + + // 更新学习进度 + static updateLearningProgress(data: { + courseId: number + lessonId: number + progress: number + timeSpent?: number + }): Promise> { + return ApiRequest.post('/learning-progress', data) + } + + // 标记课时完成 + static markLessonCompleted(lessonId: number): Promise> { + return ApiRequest.post(`/lessons/${lessonId}/complete`) + } + + // 获取课程测验 + static getCourseQuizzes(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/quizzes`) + } + + // 获取测验详情 + static getQuizById(id: number): Promise> { + return ApiRequest.get(`/quizzes/${id}`) + } + + // 提交测验答案 + static submitQuizAnswers(quizId: number, answers: Array<{ + questionId: number + answer: string | string[] + }>): Promise + }>> { + return ApiRequest.post(`/quizzes/${quizId}/submit`, { answers }) + } + + // 获取测验结果 + static getQuizResults(quizId: number): Promise + bestScore: number + averageScore: number + totalAttempts: number + }>> { + return ApiRequest.get(`/quizzes/${quizId}/results`) + } + + // 下载课程资源 + static downloadResource(resourceId: number): Promise { + return ApiRequest.download(`/resources/${resourceId}/download`) + } + + // 获取讲师信息 + static getInstructorById(id: number): Promise> { + return ApiRequest.get(`/instructors/${id}`) + } + + // 获取讲师的课程 + static getInstructorCourses(instructorId: number, params?: { + page?: number + pageSize?: number + }): Promise>> { + return ApiRequest.get(`/instructors/${instructorId}/courses`, params) + } + + // 关注讲师 + static followInstructor(instructorId: number): Promise> { + return ApiRequest.post(`/instructors/${instructorId}/follow`) + } + + // 取消关注讲师 + static unfollowInstructor(instructorId: number): Promise> { + return ApiRequest.delete(`/instructors/${instructorId}/follow`) + } + + // 获取课程统计信息 + static getCourseStats(courseId: number): Promise + }>> { + return ApiRequest.get(`/courses/${courseId}/stats`) + } + + // 预览课程(免费课时) + static previewCourse(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/preview`) + } + + // 获取相关课程推荐 + static getRelatedCourses(courseId: number, limit?: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/related`, { limit }) + } + + // 检查课程访问权限 + static checkCourseAccess(courseId: number): Promise> { + return ApiRequest.get(`/courses/${courseId}/access`) + } + + // 辅助方法:映射难度等级 + private static mapDifficulty(difficulty: number): 'beginner' | 'intermediate' | 'advanced' { + switch (difficulty) { + case 0: + return 'beginner' + case 1: + return 'intermediate' + case 2: + return 'advanced' + default: + return 'beginner' + } + } + + // 辅助方法:获取分类名称 + private static getCategoryName(categoryId: number): string { + // 这里可以根据categoryId返回对应的分类名称 + // 暂时返回默认值,后续可以通过分类API获取 + const categoryMap: { [key: number]: string } = { + 1: '信息技术', + 2: '数学', + 3: '物理', + 4: '化学', + 5: '生物' + } + return categoryMap[categoryId] || '其他' + } +} + +export default CourseApi diff --git a/src/api/modules/favorite.ts b/src/api/modules/favorite.ts new file mode 100644 index 0000000..4e0091d --- /dev/null +++ b/src/api/modules/favorite.ts @@ -0,0 +1,194 @@ +// 收藏相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + PaginationResponse, + Favorite, + Course, +} from '../types' + +/** + * 收藏API模块 + */ +export class FavoriteApi { + // 添加收藏 + static addFavorite(courseId: number): Promise> { + return ApiRequest.post('/favorites', { courseId }) + } + + // 取消收藏 + static removeFavorite(courseId: number): Promise> { + return ApiRequest.delete(`/favorites/${courseId}`) + } + + // 检查是否已收藏 + static checkFavorite(courseId: number): Promise> { + return ApiRequest.get(`/favorites/check/${courseId}`) + } + + // 获取我的收藏列表 + static getMyFavorites(params?: { + page?: number + pageSize?: number + category?: string + sortBy?: 'newest' | 'oldest' | 'rating' | 'price' + }): Promise>> { + return ApiRequest.get('/favorites', params) + } + + // 批量添加收藏 + static batchAddFavorites(courseIds: number[]): Promise> { + return ApiRequest.post('/favorites/batch', { courseIds }) + } + + // 批量取消收藏 + static batchRemoveFavorites(courseIds: number[]): Promise> { + return ApiRequest.delete('/favorites/batch', { courseIds }) + } + + // 获取收藏统计 + static getFavoriteStats(): Promise + recentFavorites: Course[] + favoritesTrend: Array<{ + date: string + count: number + }> + }>> { + return ApiRequest.get('/favorites/stats') + } + + // 导出收藏列表 + static exportFavorites(format: 'json' | 'csv' | 'excel'): Promise { + return ApiRequest.download(`/favorites/export?format=${format}`, `favorites.${format}`) + } + + // 导入收藏列表 + static importFavorites(file: File): Promise> { + return ApiRequest.upload('/favorites/import', file) + } + + // 清空收藏列表 + static clearAllFavorites(): Promise> { + return ApiRequest.delete('/favorites/clear') + } + + // 获取收藏夹分类(如果支持分类收藏) + static getFavoriteFolders(): Promise>> { + return ApiRequest.get('/favorite-folders') + } + + // 创建收藏夹 + static createFavoriteFolder(data: { + name: string + description?: string + }): Promise> { + return ApiRequest.post('/favorite-folders', data) + } + + // 更新收藏夹 + static updateFavoriteFolder(folderId: number, data: { + name?: string + description?: string + }): Promise> { + return ApiRequest.put(`/favorite-folders/${folderId}`, data) + } + + // 删除收藏夹 + static deleteFavoriteFolder(folderId: number): Promise> { + return ApiRequest.delete(`/favorite-folders/${folderId}`) + } + + // 将课程添加到收藏夹 + static addCourseToFolder(courseId: number, folderId: number): Promise> { + return ApiRequest.post(`/favorite-folders/${folderId}/courses`, { courseId }) + } + + // 从收藏夹移除课程 + static removeCourseFromFolder(courseId: number, folderId: number): Promise> { + return ApiRequest.delete(`/favorite-folders/${folderId}/courses/${courseId}`) + } + + // 获取收藏夹中的课程 + static getFolderCourses(folderId: number, params?: { + page?: number + pageSize?: number + }): Promise>> { + return ApiRequest.get(`/favorite-folders/${folderId}/courses`, params) + } + + // 移动课程到其他收藏夹 + static moveCourseToFolder(courseId: number, fromFolderId: number, toFolderId: number): Promise> { + return ApiRequest.post('/favorites/move', { + courseId, + fromFolderId, + toFolderId + }) + } + + // 获取最近收藏的课程 + static getRecentFavorites(limit?: number): Promise> { + return ApiRequest.get('/favorites/recent', { limit }) + } + + // 获取收藏推荐(基于收藏历史推荐相似课程) + static getFavoriteRecommendations(limit?: number): Promise> { + return ApiRequest.get('/favorites/recommendations', { limit }) + } + + // 分享收藏列表 + static shareFavorites(data: { + folderId?: number + isPublic: boolean + description?: string + }): Promise> { + return ApiRequest.post('/favorites/share', data) + } + + // 获取分享的收藏列表 + static getSharedFavorites(shareId: string): Promise> { + return ApiRequest.get(`/favorites/shared/${shareId}`) + } +} + +export default FavoriteApi diff --git a/src/api/modules/order.ts b/src/api/modules/order.ts new file mode 100644 index 0000000..b9ed0d8 --- /dev/null +++ b/src/api/modules/order.ts @@ -0,0 +1,282 @@ +// 订单相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + PaginationResponse, + Order, + OrderItem, +} from '../types' + +/** + * 订单API模块 + */ +export class OrderApi { + // 创建订单 + static createOrder(data: { + courseIds: number[] + couponCode?: string + paymentMethod?: string + }): Promise> { + return ApiRequest.post('/orders', data) + } + + // 获取订单列表 + static getOrders(params?: { + page?: number + pageSize?: number + status?: string + startDate?: string + endDate?: string + }): Promise>> { + return ApiRequest.get('/orders', params) + } + + // 获取订单详情 + static getOrderById(orderId: number): Promise> { + return ApiRequest.get(`/orders/${orderId}`) + } + + // 通过订单号获取订单 + static getOrderByNo(orderNo: string): Promise> { + return ApiRequest.get(`/orders/no/${orderNo}`) + } + + // 取消订单 + static cancelOrder(orderId: number, reason?: string): Promise> { + return ApiRequest.post(`/orders/${orderId}/cancel`, { reason }) + } + + // 确认支付 + static confirmPayment(orderId: number, data: { + paymentMethod: string + transactionId?: string + paymentProof?: string + }): Promise> { + return ApiRequest.post(`/orders/${orderId}/confirm-payment`, data) + } + + // 申请退款 + static requestRefund(orderId: number, data: { + reason: string + description?: string + refundAmount?: number + }): Promise> { + return ApiRequest.post(`/orders/${orderId}/refund`, data) + } + + // 获取支付方式列表 + static getPaymentMethods(): Promise>> { + return ApiRequest.get('/payment-methods') + } + + // 获取支付状态 + static getPaymentStatus(orderId: number): Promise> { + return ApiRequest.get(`/orders/${orderId}/payment-status`) + } + + // 重新支付 + static retryPayment(orderId: number, paymentMethod?: string): Promise> { + return ApiRequest.post(`/orders/${orderId}/retry-payment`, { paymentMethod }) + } + + // 获取发票信息 + static getInvoice(orderId: number): Promise + }>> { + return ApiRequest.get(`/orders/${orderId}/invoice`) + } + + // 申请发票 + static requestInvoice(orderId: number, data: { + type: 'personal' | 'company' + title: string + taxId?: string + address?: string + phone?: string + bank?: string + bankAccount?: string + email: string + }): Promise> { + return ApiRequest.post(`/orders/${orderId}/request-invoice`, data) + } + + // 下载发票 + static downloadInvoice(orderId: number): Promise { + return ApiRequest.download(`/orders/${orderId}/invoice/download`, `invoice-${orderId}.pdf`) + } + + // 获取订单统计 + static getOrderStats(params?: { + startDate?: string + endDate?: string + }): Promise + ordersTrend: Array<{ + date: string + orders: number + amount: number + }> + }>> { + return ApiRequest.get('/orders/stats', params) + } + + // 验证优惠券 + static validateCoupon(code: string, courseIds: number[]): Promise> { + return ApiRequest.post('/coupons/validate', { code, courseIds }) + } + + // 获取可用优惠券 + static getAvailableCoupons(courseIds?: number[]): Promise>> { + return ApiRequest.get('/coupons/available', { courseIds }) + } + + // 计算订单金额 + static calculateOrderAmount(data: { + courseIds: number[] + couponCode?: string + }): Promise + coupon?: { + code: string + discount: number + description: string + } + }>> { + return ApiRequest.post('/orders/calculate', data) + } + + // 获取退款列表 + static getRefunds(params?: { + page?: number + pageSize?: number + status?: string + }): Promise>> { + return ApiRequest.get('/refunds', params) + } + + // 获取退款详情 + static getRefundById(refundId: number): Promise + }>> { + return ApiRequest.get(`/refunds/${refundId}`) + } +} + +export default OrderApi diff --git a/src/api/modules/statistics.ts b/src/api/modules/statistics.ts new file mode 100644 index 0000000..0644800 --- /dev/null +++ b/src/api/modules/statistics.ts @@ -0,0 +1,374 @@ +// 统计相关API接口 +import { ApiRequest } from '../request' +import type { ApiResponse, Statistics } from '../types' + +/** + * 统计API模块 + */ +export class StatisticsApi { + // 获取平台总体统计 + static getPlatformStats(): Promise> { + return ApiRequest.get('/statistics/platform') + } + + // 获取用户学习统计 + static getUserLearningStats(userId?: number): Promise + skillsAcquired: Array<{ + skill: string + level: number + coursesCount: number + }> + achievements: Array<{ + id: number + name: string + description: string + icon: string + unlockedAt: string + }> + }>> { + return ApiRequest.get('/statistics/user-learning', { userId }) + } + + // 获取课程统计 + static getCourseStats(courseId: number): Promise + progressDistribution: Array<{ + range: string + count: number + percentage: number + }> + ratingDistribution: Array<{ + rating: number + count: number + percentage: number + }> + popularLessons: Array<{ + lessonId: number + title: string + viewCount: number + averageWatchTime: number + }> + studentDemographics: { + ageGroups: Array<{ + range: string + count: number + percentage: number + }> + locations: Array<{ + country: string + count: number + percentage: number + }> + devices: Array<{ + device: string + count: number + percentage: number + }> + } + }>> { + return ApiRequest.get(`/statistics/course/${courseId}`) + } + + // 获取讲师统计 + static getInstructorStats(instructorId: number): Promise + monthlyStats: Array<{ + month: string + newStudents: number + revenue: number + newCourses: number + }> + studentFeedback: { + positiveCount: number + neutralCount: number + negativeCount: number + commonKeywords: Array<{ + keyword: string + count: number + sentiment: 'positive' | 'neutral' | 'negative' + }> + } + }>> { + return ApiRequest.get(`/statistics/instructor/${instructorId}`) + } + + // 获取学习进度统计 + static getLearningProgressStats(params?: { + courseId?: number + userId?: number + startDate?: string + endDate?: string + }): Promise + weeklyProgress: Array<{ + week: string + sessions: number + learningTime: number + lessonsCompleted: number + }> + deviceUsage: Array<{ + device: string + sessions: number + percentage: number + }> + timeDistribution: Array<{ + hour: number + sessions: number + percentage: number + }> + }>> { + return ApiRequest.get('/statistics/learning-progress', params) + } + + // 获取收入统计 + static getRevenueStats(params?: { + instructorId?: number + startDate?: string + endDate?: string + groupBy?: 'day' | 'week' | 'month' | 'year' + }): Promise + revenueByCategory: Array<{ + category: string + revenue: number + percentage: number + }> + topSellingCourses: Array<{ + courseId: number + title: string + sales: number + revenue: number + }> + paymentMethods: Array<{ + method: string + count: number + amount: number + percentage: number + }> + }>> { + return ApiRequest.get('/statistics/revenue', params) + } + + // 获取用户行为统计 + static getUserBehaviorStats(params?: { + startDate?: string + endDate?: string + }): Promise + userEngagement: { + dailyActiveUsers: number + weeklyActiveUsers: number + monthlyActiveUsers: number + averageSessionsPerUser: number + averagePageViews: number + } + userJourney: Array<{ + step: string + users: number + conversionRate: number + }> + popularPages: Array<{ + page: string + views: number + uniqueViews: number + averageTime: number + }> + }>> { + return ApiRequest.get('/statistics/user-behavior', params) + } + + // 获取搜索统计 + static getSearchStats(params?: { + startDate?: string + endDate?: string + limit?: number + }): Promise + searchTrends: Array<{ + date: string + searches: number + uniqueSearches: number + }> + categorySearches: Array<{ + category: string + searches: number + percentage: number + }> + searchSources: Array<{ + source: string + count: number + percentage: number + }> + }>> { + return ApiRequest.get('/statistics/search', params) + } + + // 获取内容统计 + static getContentStats(): Promise + contentByLevel: Array<{ + level: string + courses: number + percentage: number + }> + contentByLanguage: Array<{ + language: string + courses: number + percentage: number + }> + contentGrowth: Array<{ + month: string + newCourses: number + newLessons: number + }> + }>> { + return ApiRequest.get('/statistics/content') + } + + // 获取评论统计 + static getCommentStats(params?: { + courseId?: number + instructorId?: number + startDate?: string + endDate?: string + }): Promise + commentTrends: Array<{ + date: string + comments: number + averageRating: number + }> + topReviewedCourses: Array<{ + courseId: number + title: string + comments: number + averageRating: number + }> + commonKeywords: Array<{ + keyword: string + count: number + sentiment: 'positive' | 'neutral' | 'negative' + }> + }>> { + return ApiRequest.get('/statistics/comments', params) + } + + // 导出统计报告 + static exportStatsReport(type: string, params?: { + startDate?: string + endDate?: string + format?: 'pdf' | 'excel' | 'csv' + }): Promise { + const format = params?.format || 'pdf' + return ApiRequest.download(`/statistics/export/${type}?format=${format}`, `stats-report.${format}`, params) + } +} + +export default StatisticsApi diff --git a/src/api/modules/upload.ts b/src/api/modules/upload.ts new file mode 100644 index 0000000..b7f6f02 --- /dev/null +++ b/src/api/modules/upload.ts @@ -0,0 +1,331 @@ +// 文件上传相关API接口 +import { ApiRequest } from '../request' +import type { ApiResponse } from '../types' + +/** + * 上传API模块 + */ +export class UploadApi { + // 上传单个文件 + static uploadFile( + file: File, + type: 'image' | 'video' | 'document' | 'audio' = 'image', + onProgress?: (progress: number) => void + ): Promise> { + return ApiRequest.upload(`/upload/${type}`, file, onProgress) + } + + // 上传头像 + static uploadAvatar( + file: File, + onProgress?: (progress: number) => void + ): Promise> { + return ApiRequest.upload('/upload/avatar', file, onProgress) + } + + // 上传课程封面 + static uploadCourseThumbnail( + file: File, + courseId?: number, + onProgress?: (progress: number) => void + ): Promise> { + const formData = new FormData() + formData.append('file', file) + if (courseId) { + formData.append('courseId', courseId.toString()) + } + + return ApiRequest.post('/upload/course-thumbnail', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 上传课程视频 + static uploadCourseVideo( + file: File, + courseId?: number, + lessonId?: number, + onProgress?: (progress: number) => void + ): Promise> { + const formData = new FormData() + formData.append('file', file) + if (courseId) { + formData.append('courseId', courseId.toString()) + } + if (lessonId) { + formData.append('lessonId', lessonId.toString()) + } + + return ApiRequest.post('/upload/course-video', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 上传课程资源文件 + static uploadCourseResource( + file: File, + lessonId: number, + title?: string, + description?: string, + onProgress?: (progress: number) => void + ): Promise> { + const formData = new FormData() + formData.append('file', file) + formData.append('lessonId', lessonId.toString()) + if (title) { + formData.append('title', title) + } + if (description) { + formData.append('description', description) + } + + return ApiRequest.post('/upload/course-resource', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 批量上传文件 + static uploadMultipleFiles( + files: File[], + type: 'image' | 'video' | 'document' | 'audio' = 'image', + onProgress?: (progress: number) => void + ): Promise>> { + const formData = new FormData() + files.forEach((file, index) => { + formData.append(`files`, file) + }) + + return ApiRequest.post(`/upload/multiple/${type}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 获取上传配置 + static getUploadConfig(): Promise> { + return ApiRequest.get('/upload/config') + } + + // 获取上传token(用于直传OSS等) + static getUploadToken(type: string = 'image'): Promise> { + return ApiRequest.get(`/upload/token/${type}`) + } + + // 删除文件 + static deleteFile(url: string): Promise> { + return ApiRequest.delete('/upload/file', { url }) + } + + // 批量删除文件 + static deleteMultipleFiles(urls: string[]): Promise> { + return ApiRequest.delete('/upload/files', { urls }) + } + + // 获取文件信息 + static getFileInfo(url: string): Promise> { + return ApiRequest.get('/upload/file-info', { url }) + } + + // 压缩图片 + static compressImage( + file: File, + options: { + quality?: number + maxWidth?: number + maxHeight?: number + format?: 'jpeg' | 'png' | 'webp' + } = {}, + onProgress?: (progress: number) => void + ): Promise> { + const formData = new FormData() + formData.append('file', file) + formData.append('options', JSON.stringify(options)) + + return ApiRequest.post('/upload/compress-image', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 生成缩略图 + static generateThumbnail( + file: File, + sizes: Array<{ width: number; height: number }> = [ + { width: 150, height: 150 }, + { width: 300, height: 300 }, + ], + onProgress?: (progress: number) => void + ): Promise + }>> { + const formData = new FormData() + formData.append('file', file) + formData.append('sizes', JSON.stringify(sizes)) + + return ApiRequest.post('/upload/generate-thumbnail', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + }) + } + + // 获取上传历史 + static getUploadHistory(params?: { + page?: number + pageSize?: number + type?: string + startDate?: string + endDate?: string + }): Promise + total: number + page: number + pageSize: number + }>> { + return ApiRequest.get('/upload/history', params) + } +} + +export default UploadApi diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 0000000..092f478 --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,527 @@ +// HTTP 请求封装文件 +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import { useUserStore } from '@/stores/user' +import router from '@/router' +import type { ApiResponse } from './types' + +// 消息提示函数 - 使用window.alert作为fallback,实际项目中应该使用UI库的消息组件 +const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => { + // 这里可以替换为你使用的UI库的消息组件 + // 例如:naive-ui的 useMessage() + console.log(`[${type.toUpperCase()}] ${message}`) + + // 临时使用alert,实际项目中应该替换为UI库的消息组件 + if (type === 'error') { + alert(`错误: ${message}`) + } +} + +// 创建axios实例 +const request: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// 请求拦截器 +request.interceptors.request.use( + (config: AxiosRequestConfig) => { + // 添加认证token + const userStore = useUserStore() + if (userStore.token) { + config.headers = { + ...config.headers, + Authorization: `Bearer ${userStore.token}`, + } + } + + // 添加请求时间戳 + config.headers = { + ...config.headers, + 'X-Request-Time': Date.now().toString(), + } + + // 开发环境下打印请求信息 + if (import.meta.env.DEV) { + console.log('🚀 Request:', { + url: config.url, + method: config.method, + params: config.params, + data: config.data, + }) + } + + return config + }, + (error) => { + console.error('❌ Request Error:', error) + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response + + // 开发环境下打印响应信息 + if (import.meta.env.DEV) { + console.log('✅ Response:', { + url: response.config.url, + status: response.status, + data: data, + }) + } + + // 检查业务状态码 + if (data.code === 200 || data.code === 0) { + return data + } + + // 处理业务错误 + const errorMessage = data.message || '请求失败' + // 不在这里显示错误消息,让组件自己处理 + // showMessage(errorMessage, 'error') + + // 创建一个包含完整响应信息的错误对象 + const error = new Error(errorMessage) + ;(error as any).response = { + data: data, + status: 200 // HTTP状态码是200,但业务状态码不是成功 + } + return Promise.reject(error) + }, + (error) => { + console.error('❌ Response Error:', error) + + // 处理HTTP状态码错误 + const { response } = error + let errorMessage = '网络错误,请稍后重试' + + if (response) { + switch (response.status) { + case 400: + errorMessage = '请求参数错误' + break + case 401: + errorMessage = '登录已过期,请重新登录' + // 清除用户信息,不跳转页面(使用模态框登录) + const userStore = useUserStore() + userStore.logout() + break + case 403: + errorMessage = '没有权限访问' + break + case 404: + errorMessage = '请求的资源不存在' + break + case 422: + errorMessage = '数据验证失败' + break + case 429: + errorMessage = '请求过于频繁,请稍后重试' + break + case 500: + errorMessage = '服务器内部错误' + break + case 502: + errorMessage = '网关错误' + break + case 503: + errorMessage = '服务暂时不可用' + break + case 504: + errorMessage = '网关超时' + break + default: + errorMessage = `请求失败 (${response.status})` + } + } else if (error.code === 'ECONNABORTED') { + errorMessage = '请求超时,请检查网络连接' + } else if (error.message === 'Network Error') { + errorMessage = '网络连接失败,请检查网络设置' + } + + showMessage(errorMessage, 'error') + return Promise.reject(error) + } +) + +// Mock数据处理 +const handleMockRequest = async (url: string, method: string, data?: any): Promise> => { + console.log('🚀 Mock Request:', { url, method, data }) + + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 500)) + + // 登录Mock + if (url === '/users/login' && method === 'POST') { + const { email, phone, password } = data || {} + const loginField = phone || email + + // 模拟登录验证 + if (loginField && password) { + return { + code: 200, + message: '登录成功', + data: { + user: { + id: 1, + email: email || `${phone}@example.com`, + phone: phone || '123456789', + username: phone || email?.split('@')[0] || 'user', + nickname: '测试用户', + avatar: 'https://via.placeholder.com/100', + role: 'student', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z' + }, + token: 'mock_jwt_token_' + Date.now(), + refreshToken: 'mock_refresh_token_' + Date.now(), + expiresIn: 3600 + } + } as ApiResponse + } else { + return { + code: 400, + message: '手机号/邮箱或密码不能为空', + data: null + } as ApiResponse + } + } + + // 注册Mock + if (url === '/auth/register' && method === 'POST') { + const { email, password, verificationCode } = data || {} + + if (!email || !password) { + return { + code: 400, + message: '邮箱和密码不能为空', + data: null + } as ApiResponse + } + + if (!verificationCode) { + return { + code: 400, + message: '验证码不能为空', + data: null + } as ApiResponse + } + + return { + code: 200, + message: '注册成功', + data: { + id: 2, + email: email, + username: email.split('@')[0], + nickname: '新用户', + avatar: '', + role: 'student', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + } + } as ApiResponse + } + + // 发送验证码Mock + if (url === '/auth/send-verification' && method === 'POST') { + return { + code: 200, + message: '验证码已发送', + data: null + } as ApiResponse + } + + // 获取当前用户信息Mock + if (url === '/auth/me' && method === 'GET') { + return { + code: 200, + message: '获取成功', + data: { + id: 1, + email: 'test@example.com', + username: 'test', + nickname: '测试用户', + avatar: '', + role: 'student', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z' + } + } as ApiResponse + } + + // 课程详情Mock + if (url === '/lesson/detail' && method === 'GET') { + // 对于GET请求,参数直接在data中(data就是params对象) + const id = data?.id + console.log('课程详情Mock - 获取到的ID:', id, '原始data:', data) + + if (!id) { + return { + code: 400, + message: '课程ID必填', + data: null + } as ApiResponse + } + + // 根据课程ID提供不同的模拟数据 + const courseData = { + 1: { + name: 'DeepSeek大语言模型实战应用', + cover: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop', + price: '299.00', + school: 'DeepSeek技术学院', + description: '本课程深度聚焦DeepSeek大语言模型的实际应用,让每一位学员了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。', + position: 'AI技术专家 / 高级讲师' + }, + 2: { + name: 'Python编程基础与实战', + cover: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=400&h=300&fit=crop', + price: '199.00', + school: '编程技术学院', + description: '从零开始学习Python编程,涵盖基础语法、数据结构、面向对象编程等核心概念,通过实际项目练习掌握Python开发技能。', + position: 'Python开发专家 / 资深讲师' + }, + 3: { + name: 'Web前端开发全栈课程', + cover: 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop', + price: '399.00', + school: '前端技术学院', + description: '全面学习现代Web前端开发技术,包括HTML5、CSS3、JavaScript、Vue.js、React等主流框架,培养全栈开发能力。', + position: '前端架构师 / 技术总监' + } + } + + const currentCourse = courseData[id as keyof typeof courseData] || courseData[1] + + // 模拟课程详情数据 + return { + code: 0, + message: '查询课程详情成功', + data: { + id: parseInt(id), + name: currentCourse.name, + cover: currentCourse.cover, + categoryId: 1, + price: currentCourse.price, + school: currentCourse.school, + description: currentCourse.description, + teacherId: 1, + outline: '

课程大纲:

  • 第一章:基础入门
    - 环境搭建与配置
    - 基本概念理解
    - 实践操作演示
  • 第二章:核心技能
    - 核心功能详解
    - 实际应用场景
    - 案例分析讲解
  • 第三章:高级应用
    - 进阶技巧掌握
    - 项目实战演练
    - 问题解决方案
', + prerequisite: '具备基本的计算机操作能力', + target: '掌握核心技能,能够在实际工作中熟练应用', + arrangement: '理论与实践相结合,循序渐进的学习方式', + startTime: '2025-01-26 10:13:17', + endTime: '2025-03-26 10:13:17', + revision: 1, + position: currentCourse.position, + createdAt: 1737944724, + updatedAt: 1737944724, + updatedTime: null + } + } as ApiResponse + } + + // 课程列表Mock + if (url === '/lesson/list' && method === 'GET') { + // 模拟课程列表数据 + const mockCourses = [ + { + id: 1, + name: "暑期名师领学,提高班级教学质量!高效冲分指南", + cover: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + categoryId: 1, + price: "99.00", + school: "名师工作室", + description: "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。", + teacherId: 1, + outline: "课程大纲详细内容...", + prerequisite: "具备基本的计算机操作能力", + target: "掌握核心技能,能够在实际工作中熟练应用", + arrangement: "理论与实践相结合,循序渐进的学习方式", + startTime: "2025-01-26 10:13:17", + endTime: "2025-03-26 10:13:17", + revision: 1, + position: "高级讲师", + createdAt: 1737944724, + updatedAt: 1737944724, + updatedTime: null + }, + { + id: 2, + name: "计算机二级考前冲刺班", + cover: "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + categoryId: 2, + price: "199.00", + school: "计算机学院", + description: "备考计算机二级,名师带你高效复习,掌握考试重点,轻松通过考试。", + teacherId: 2, + outline: "考试大纲详细解析...", + prerequisite: "具备基本的计算机基础知识", + target: "顺利通过计算机二级考试", + arrangement: "考点精讲+真题演练+模拟考试", + startTime: "2025-02-01 09:00:00", + endTime: "2025-02-28 18:00:00", + revision: 1, + position: "副教授", + createdAt: 1737944724, + updatedAt: 1737944724, + updatedTime: null + }, + { + id: 3, + name: "摆脱哑巴英语,流利口语训练营", + cover: "https://images.unsplash.com/photo-1434030216411-0b793f4b4173?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + categoryId: 3, + price: "299.00", + school: "外语学院", + description: "专业外教授课,情景式教学,让你在短时间内突破口语障碍,自信开口说英语。", + teacherId: 3, + outline: "口语训练系统课程...", + prerequisite: "具备基本的英语基础", + target: "能够流利进行日常英语对话", + arrangement: "外教一对一+小班练习+实战演练", + startTime: "2025-02-15 19:00:00", + endTime: "2025-04-15 21:00:00", + revision: 1, + position: "外籍教师", + createdAt: 1737944724, + updatedAt: 1737944724, + updatedTime: null + } + ] + + return { + code: 0, + message: '查询课程列表成功', + data: { + list: mockCourses, + total: mockCourses.length + } + } as ApiResponse + } + + + + // 默认404响应 + return { + code: 404, + message: '接口不存在', + data: null + } as ApiResponse +} + +// 请求方法封装 +export class ApiRequest { + // GET 请求 + static get( + url: string, + params?: any, + config?: AxiosRequestConfig + ): Promise> { + // 检查是否启用Mock + if (import.meta.env.VITE_ENABLE_MOCK === 'true') { + return handleMockRequest(url, 'GET', params) + } + return request.get(url, { params, ...config }) + } + + // POST 请求 + static post( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + // 检查是否启用Mock + if (import.meta.env.VITE_ENABLE_MOCK === 'true') { + return handleMockRequest(url, 'POST', data) + } + return request.post(url, data, config) + } + + // PUT 请求 + static put( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return request.put(url, data, config) + } + + // PATCH 请求 + static patch( + url: string, + data?: any, + config?: AxiosRequestConfig + ): Promise> { + return request.patch(url, data, config) + } + + // DELETE 请求 + static delete( + url: string, + params?: any, + config?: AxiosRequestConfig + ): Promise> { + return request.delete(url, { params, ...config }) + } + + // 文件上传 + static upload( + url: string, + file: File, + onProgress?: (progress: number) => void, + config?: AxiosRequestConfig + ): Promise> { + const formData = new FormData() + formData.append('file', file) + + return request.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + if (onProgress && progressEvent.total) { + const progress = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(progress) + } + }, + ...config, + }) + } + + // 文件下载 + static download( + url: string, + filename?: string, + params?: any, + config?: AxiosRequestConfig + ): Promise { + return request + .get(url, { + params, + responseType: 'blob', + ...config, + }) + .then((response: any) => { + const blob = new Blob([response.data]) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = filename || 'download' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(downloadUrl) + }) + } +} + +export default request diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..6d2f56d --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,407 @@ +// API 接口类型定义文件 + +// 通用响应类型 +export interface ApiResponse { + code: number + message: string + data: T + timestamp?: string +} + +// 分页响应类型 +export interface PaginationResponse { + list: T[] + total: number + page: number + pageSize: number + totalPages: number +} + +// 用户相关类型 +export interface User { + id: number + username: string + email: string + phone?: string + avatar?: string + role: 'student' | 'teacher' | 'admin' + status: 'active' | 'inactive' | 'banned' + createdAt: string + updatedAt: string + profile?: UserProfile +} + +export interface UserProfile { + realName?: string + gender?: 'male' | 'female' | 'other' + birthday?: string + bio?: string + location?: string + website?: string + socialLinks?: { + wechat?: string + qq?: string + weibo?: string + } +} + +// 登录注册类型 +export interface LoginRequest { + email?: string + phone?: string + password: string + captcha?: string +} + +export interface LoginResponse { + user: User + token: string + refreshToken: string + expiresIn: number +} + +// 后端实际返回的登录响应格式 +export interface BackendLoginResponse { + token: string + id: number + timestamp: number + expires: string +} + +export interface RegisterRequest { + username: string + email: string + phone?: string + password: string + confirmPassword: string + captcha: string + inviteCode?: string +} + +// 课程相关类型 +export interface Course { + id: number + title: string + subtitle?: string + description: string + content?: string + thumbnail: string + coverImage?: string + videoUrl?: string + price: number + originalPrice?: number + discountPrice?: number + currency: string + rating: number + ratingCount: number + studentsCount: number + duration: string + totalLessons: number + level: 'beginner' | 'intermediate' | 'advanced' + language: string + category: CourseCategory + tags: string[] + skills: string[] + requirements: string[] + objectives: string[] + instructor: Instructor + status: 'draft' | 'published' | 'archived' + isEnrolled?: boolean + progress?: number + isFavorite?: boolean + createdAt: string + updatedAt: string + publishedAt?: string +} + +// 后端实际返回的课程数据格式 +export interface BackendCourse { + id: number + name: string + cover: string + categoryId: number + video: string + school: string + description: string + target: string + outline: string + prerequisite: string + reference: string + arrangement: string + startTime: string + endTime: string + revision: number + question: string + createdBy: number + createdTime: string | null + updatedBy: number + updatedTime: string | null + // 可选字段,根据API文档可能存在 + difficulty?: number // 难度等级 + subject?: string // 学科/主题 + price?: string + teacherId?: number + position?: string +} + +// 后端课程列表响应格式 +export interface BackendCourseListResponse { + list: BackendCourse[] + total: number +} + +// 课程列表请求参数 +export interface CourseListRequest { + page?: number + pageSize?: number + categoryId?: number + keyword?: string + priceMin?: number + priceMax?: number + sortBy?: 'price' | 'startTime' | 'createdAt' + sortOrder?: 'asc' | 'desc' + difficulty?: number // 难度等级:0=简单,1=中等,2=困难 + subject?: string // 学科/主题 +} + +export interface CourseCategory { + id: number + name: string + slug: string + description?: string + icon?: string + parentId?: number + children?: CourseCategory[] +} + +export interface Instructor { + id: number + name: string + title: string + bio: string + avatar: string + rating: number + studentsCount: number + coursesCount: number + experience: string + education: string[] + certifications: string[] + socialLinks?: { + website?: string + linkedin?: string + twitter?: string + } +} + +// 课程章节类型 +export interface Chapter { + id: number + courseId: number + title: string + description?: string + order: number + duration: string + isPublished: boolean + lessons: Lesson[] +} + +export interface Lesson { + id: number + chapterId: number + courseId: number + title: string + description?: string + content?: string + videoUrl?: string + duration: string + order: number + type: 'video' | 'text' | 'quiz' | 'assignment' + isCompleted?: boolean + isFree: boolean + isPublished: boolean + resources?: LessonResource[] + quiz?: Quiz + createdAt: string + updatedAt: string +} + +export interface LessonResource { + id: number + lessonId: number + title: string + description?: string + type: 'pdf' | 'doc' | 'video' | 'audio' | 'image' | 'link' + url: string + size?: number + downloadable: boolean +} + +// 后端API返回的章节数据结构 +export interface BackendCourseSection { + id: number + lessonId: number + videoUrl: string // 视频链接 + name: string // 章节名称 + sortOrder: number // 排序 + parentId: number // 父章节ID + level: number // 层级:0=子级(课时),1=父级(章节) + revision: number // 版本号 + createdBy: number + createdTime: string | null + updatedBy: number + updatedTime: string | null +} + +// 前端使用的课程章节类型(适配后的数据结构) +export interface CourseSection { + id: number + lessonId: number + outline: string // 章节大纲/内容链接(从videoUrl适配) + name: string // 章节名称 + parentId: number // 父章节ID + sort: number // 排序(从sortOrder适配) + level: number // 层级:0=父级(章节),1=子级(课时)- 已从后端数据转换 + revision: number // 版本号 + createdAt: number | null // 从createdTime适配 + updatedAt: number | null // 从updatedTime适配 + deletedAt: string | null + completed?: boolean // 是否已完成(前端状态) + duration?: string // 课时时长(前端计算) +} + +// 后端章节列表响应格式 +export interface BackendCourseSectionListResponse { + list: BackendCourseSection[] + total: number +} + +// 前端章节列表响应格式 +export interface CourseSectionListResponse { + list: CourseSection[] + timestamp: number + traceId: string +} + +// 测验类型 +export interface Quiz { + id: number + lessonId: number + title: string + description?: string + timeLimit?: number + passingScore: number + questions: QuizQuestion[] +} + +export interface QuizQuestion { + id: number + quizId: number + question: string + type: 'single' | 'multiple' | 'text' | 'essay' + options?: string[] + correctAnswer?: string | string[] + explanation?: string + points: number + order: number +} + +// 学习进度类型 +export interface LearningProgress { + id: number + userId: number + courseId: number + lessonId?: number + progress: number + timeSpent: number + lastAccessedAt: string + completedAt?: string + status: 'not_started' | 'in_progress' | 'completed' +} + +// 评论类型 +export interface Comment { + id: number + userId: number + courseId?: number + lessonId?: number + parentId?: number + content: string + rating?: number + likes: number + dislikes: number + isLiked?: boolean + isDisliked?: boolean + user: { + id: number + username: string + avatar?: string + } + replies?: Comment[] + createdAt: string + updatedAt: string +} + +// 收藏类型 +export interface Favorite { + id: number + userId: number + courseId: number + course: Course + createdAt: string +} + +// 订单类型 +export interface Order { + id: number + userId: number + orderNo: string + totalAmount: number + discountAmount: number + finalAmount: number + currency: string + status: 'pending' | 'paid' | 'cancelled' | 'refunded' + paymentMethod?: string + paymentTime?: string + items: OrderItem[] + createdAt: string + updatedAt: string +} + +export interface OrderItem { + id: number + orderId: number + courseId: number + course: Course + price: number + discountPrice?: number +} + +// 搜索类型 +export interface SearchRequest { + keyword?: string + category?: string + level?: string + price?: 'free' | 'paid' | 'all' + rating?: number + duration?: string + language?: string + sortBy?: 'newest' | 'oldest' | 'rating' | 'price_low' | 'price_high' | 'popular' + page?: number + pageSize?: number +} + +// 统计类型 +export interface Statistics { + totalCourses: number + totalStudents: number + totalInstructors: number + totalHours: number + popularCategories: Array<{ + category: string + count: number + }> + recentEnrollments: Array<{ + date: string + count: number + }> +} diff --git a/src/api/utils.ts b/src/api/utils.ts new file mode 100644 index 0000000..ad0f21c --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,366 @@ +// API 工具函数文件 +import type { ApiResponse, PaginationResponse } from './types' + +/** + * 构建查询参数字符串 + */ +export const buildQueryString = (params: Record): string => { + const searchParams = new URLSearchParams() + + Object.keys(params).forEach(key => { + const value = params[key] + if (value !== undefined && value !== null && value !== '') { + if (Array.isArray(value)) { + value.forEach(item => searchParams.append(key, String(item))) + } else { + searchParams.append(key, String(value)) + } + } + }) + + return searchParams.toString() +} + +/** + * 构建完整的URL + */ +export const buildUrl = (baseUrl: string, endpoint: string, params?: Record): string => { + let url = `${baseUrl}${endpoint}` + + if (params) { + const queryString = buildQueryString(params) + if (queryString) { + url += `?${queryString}` + } + } + + return url +} + +/** + * 替换URL中的路径参数 + */ +export const replaceUrlParams = (url: string, params: Record): string => { + let result = url + + Object.keys(params).forEach(key => { + result = result.replace(`:${key}`, String(params[key])) + }) + + return result +} + +/** + * 检查API响应是否成功 + */ +export const isApiSuccess = (response: ApiResponse): boolean => { + return response.code === 200 || response.code === 0 +} + +/** + * 提取API响应数据 + */ +export const extractApiData = (response: ApiResponse): T => { + if (isApiSuccess(response)) { + return response.data + } + throw new Error(response.message || 'API请求失败') +} + +/** + * 格式化分页数据 + */ +export const formatPaginationData = (response: ApiResponse>) => { + if (isApiSuccess(response)) { + const { list, total, page, pageSize, totalPages } = response.data + return { + items: list, + total, + currentPage: page, + pageSize, + totalPages: totalPages || Math.ceil(total / pageSize), + hasNext: page < (totalPages || Math.ceil(total / pageSize)), + hasPrev: page > 1, + } + } + throw new Error(response.message || '获取分页数据失败') +} + +/** + * 创建分页参数 + */ +export const createPaginationParams = (page: number = 1, pageSize: number = 20) => { + return { + page: Math.max(1, page), + pageSize: Math.min(Math.max(1, pageSize), 100), // 限制最大页面大小 + } +} + +/** + * 防抖函数 - 用于搜索等场景 + */ +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout | null = null + + return (...args: Parameters) => { + if (timeout) { + clearTimeout(timeout) + } + + timeout = setTimeout(() => { + func(...args) + }, wait) + } +} + +/** + * 节流函数 - 用于频繁触发的事件 + */ +export const throttle = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let lastTime = 0 + + return (...args: Parameters) => { + const now = Date.now() + + if (now - lastTime >= wait) { + lastTime = now + func(...args) + } + } +} + +/** + * 重试函数 - 用于网络请求重试 + */ +export const retry = async ( + fn: () => Promise, + maxAttempts: number = 3, + delay: number = 1000 +): Promise => { + let lastError: Error + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn() + } catch (error) { + lastError = error as Error + + if (attempt === maxAttempts) { + throw lastError + } + + // 指数退避延迟 + const waitTime = delay * Math.pow(2, attempt - 1) + await new Promise(resolve => setTimeout(resolve, waitTime)) + } + } + + throw lastError! +} + +/** + * 格式化文件大小 + */ +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * 格式化时间 + */ +export const formatDuration = (seconds: number): string => { + if (seconds < 60) { + return `${Math.round(seconds)}秒` + } else if (seconds < 3600) { + const minutes = Math.floor(seconds / 60) + const remainingSeconds = Math.round(seconds % 60) + return remainingSeconds > 0 ? `${minutes}分${remainingSeconds}秒` : `${minutes}分钟` + } else { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时` + } +} + +/** + * 验证邮箱格式 + */ +export const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} + +/** + * 验证手机号格式(中国大陆) + */ +export const isValidPhone = (phone: string): boolean => { + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(phone) +} + +/** + * 验证密码强度 + */ +export const validatePassword = (password: string): { + isValid: boolean + strength: 'weak' | 'medium' | 'strong' + issues: string[] +} => { + const issues: string[] = [] + let score = 0 + + if (password.length < 8) { + issues.push('密码长度至少8位') + } else { + score += 1 + } + + if (!/[a-z]/.test(password)) { + issues.push('密码需包含小写字母') + } else { + score += 1 + } + + if (!/[A-Z]/.test(password)) { + issues.push('密码需包含大写字母') + } else { + score += 1 + } + + if (!/\d/.test(password)) { + issues.push('密码需包含数字') + } else { + score += 1 + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + issues.push('密码需包含特殊字符') + } else { + score += 1 + } + + let strength: 'weak' | 'medium' | 'strong' + if (score < 3) { + strength = 'weak' + } else if (score < 5) { + strength = 'medium' + } else { + strength = 'strong' + } + + return { + isValid: issues.length === 0, + strength, + issues + } +} + +/** + * 生成随机字符串 + */ +export const generateRandomString = (length: number = 8): string => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + + return result +} + +/** + * 深拷贝对象 + */ +export const deepClone = (obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (obj instanceof Date) { + return new Date(obj.getTime()) as unknown as T + } + + if (obj instanceof Array) { + return obj.map(item => deepClone(item)) as unknown as T + } + + if (typeof obj === 'object') { + const cloned = {} as T + Object.keys(obj).forEach(key => { + (cloned as any)[key] = deepClone((obj as any)[key]) + }) + return cloned + } + + return obj +} + +/** + * 获取错误消息 + */ +export const getErrorMessage = (error: any): string => { + if (typeof error === 'string') { + return error + } + + if (error?.response?.data?.message) { + return error.response.data.message + } + + if (error?.message) { + return error.message + } + + return '未知错误' +} + +/** + * 本地存储工具 + */ +export const storage = { + get: (key: string, defaultValue?: T): T | null => { + try { + const item = localStorage.getItem(key) + return item ? JSON.parse(item) : defaultValue || null + } catch { + return defaultValue || null + } + }, + + set: (key: string, value: any): void => { + try { + localStorage.setItem(key, JSON.stringify(value)) + } catch (error) { + console.error('存储数据失败:', error) + } + }, + + remove: (key: string): void => { + try { + localStorage.removeItem(key) + } catch (error) { + console.error('删除存储数据失败:', error) + } + }, + + clear: (): void => { + try { + localStorage.clear() + } catch (error) { + console.error('清空存储数据失败:', error) + } + } +} diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index eff59f1..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/src/components/TheWelcome.vue b/src/components/TheWelcome.vue deleted file mode 100644 index fe48afc..0000000 --- a/src/components/TheWelcome.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/src/components/VideoPlayer.vue b/src/components/VideoPlayer.vue new file mode 100644 index 0000000..e72bf69 --- /dev/null +++ b/src/components/VideoPlayer.vue @@ -0,0 +1,706 @@ + + + + + diff --git a/src/components/WelcomeItem.vue b/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/src/components/auth/LoginModal.vue b/src/components/auth/LoginModal.vue new file mode 100644 index 0000000..b38cc90 --- /dev/null +++ b/src/components/auth/LoginModal.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/src/components/auth/RegisterModal.vue b/src/components/auth/RegisterModal.vue new file mode 100644 index 0000000..d59c53a --- /dev/null +++ b/src/components/auth/RegisterModal.vue @@ -0,0 +1,431 @@ + + + + + diff --git a/src/components/common/PlaceholderImage.vue b/src/components/common/PlaceholderImage.vue new file mode 100644 index 0000000..35a883b --- /dev/null +++ b/src/components/common/PlaceholderImage.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/components/common/SafeAvatar.vue b/src/components/common/SafeAvatar.vue new file mode 100644 index 0000000..349ba5c --- /dev/null +++ b/src/components/common/SafeAvatar.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/components/course/CourseCard.vue b/src/components/course/CourseCard.vue index 7b97246..2499420 100644 --- a/src/components/course/CourseCard.vue +++ b/src/components/course/CourseCard.vue @@ -21,12 +21,12 @@
- - {{ course.instructor }} + {{ course.instructor?.name }}
@@ -96,6 +96,7 @@ import { computed } from 'vue' import { useMessage } from 'naive-ui' import { useCourseStore } from '@/stores/course' import type { Course } from '@/stores/course' +import SafeAvatar from '@/components/common/SafeAvatar.vue' import { StarOutline, PeopleOutline, diff --git a/src/components/icons/IconCommunity.vue b/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/components/icons/IconDocumentation.vue b/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/components/icons/IconEcosystem.vue b/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/components/icons/IconSupport.vue b/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/components/icons/IconTooling.vue b/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index b724704..aeba31d 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -17,15 +17,19 @@ {{ t('header.courses') }} + - - @@ -111,10 +127,12 @@ import { useUserStore } from '@/stores/user' import { PersonOutline, LogOutOutline, - SettingsOutline, MenuOutline, CloseOutline } from '@vicons/ionicons5' +import LoginModal from '@/components/auth/LoginModal.vue' +import RegisterModal from '@/components/auth/RegisterModal.vue' +import SafeAvatar from '@/components/common/SafeAvatar.vue' const router = useRouter() const { t, locale } = useI18n() @@ -127,6 +145,12 @@ const mobileMenuOpen = ref(false) // 当前激活的菜单项 const activeKey = ref('home') +// 认证模态框相关 +const loginModalVisible = ref(false) +const registerModalVisible = ref(false) + + + // 语言切换相关 @@ -154,20 +178,15 @@ const switchLanguage = (lang: string) => { // 用户菜单选项 const userMenuOptions = computed(() => [ { - label: t('header.profile'), + label: '个人中心', key: 'profile', icon: () => h(PersonOutline) }, - { - label: t('header.settings'), - key: 'settings', - icon: () => h(SettingsOutline) - }, { type: 'divider' }, { - label: t('header.logout'), + label: '退出登录', key: 'logout', icon: () => h(LogOutOutline) } @@ -190,17 +209,14 @@ const handleMenuSelect = (key: string) => { // 暂时跳转到首页 router.push('/') break - case 'practice': - // 暂时跳转到首页 - router.push('/') + case 'faculty': + router.push('/faculty') break case 'resources': - // 暂时跳转到首页 - router.push('/') + router.push('/resources') break case 'activities': - // 暂时跳转到首页 - router.push('/') + router.push('/activities') break } } @@ -211,9 +227,6 @@ const handleUserMenuSelect = (key: string) => { case 'profile': router.push('/profile') break - case 'settings': - // TODO: 实现设置页面 - break case 'logout': userStore.logout() router.push('/') @@ -221,6 +234,24 @@ const handleUserMenuSelect = (key: string) => { } } +// 显示登录模态框 +const showLoginModal = () => { + loginModalVisible.value = true +} + +// 显示注册模态框 +const showRegisterModal = () => { + registerModalVisible.value = true +} + +// 认证成功处理 +const handleAuthSuccess = () => { + // 认证成功后可以进行一些操作,比如刷新用户信息等 + console.log('认证成功') +} + + + // 点击外部关闭下拉框 @@ -693,5 +724,7 @@ onUnmounted(() => { } } + + /* 全屏模式样式现在在App.vue中统一管理 */ diff --git a/src/components/layout/AppLayout.vue b/src/components/layout/AppLayout.vue index dbea3c4..1fc98f0 100644 --- a/src/components/layout/AppLayout.vue +++ b/src/components/layout/AppLayout.vue @@ -1,20 +1,22 @@ + + diff --git a/src/views/CourseDetail.vue b/src/views/CourseDetail.vue index dc4bbaa..07c45b2 100644 --- a/src/views/CourseDetail.vue +++ b/src/views/CourseDetail.vue @@ -1,64 +1,9 @@ + \ No newline at end of file diff --git a/src/views/CourseStudy.vue b/src/views/CourseStudy.vue new file mode 100644 index 0000000..b2c7137 --- /dev/null +++ b/src/views/CourseStudy.vue @@ -0,0 +1,1836 @@ + + + + + diff --git a/src/views/Courses.vue b/src/views/Courses.vue index 2d59ebc..d2f0048 100644 --- a/src/views/Courses.vue +++ b/src/views/Courses.vue @@ -187,7 +187,7 @@
- 筛选结果:找到 {{ filteredCourses.length }} 门相关课程 + 筛选结果:找到 {{ total }} 门相关课程
@@ -197,8 +197,15 @@ 推荐 + +
+
+

正在加载课程...

+
+
+ -
+
@@ -207,7 +214,7 @@

{{ getCourseTitle(course) }}

📚 {{ course.duration }} - ⏰ {{ course.totalTime }} + 💰 ¥{{ course.price }}
@@ -203,17 +203,34 @@
+ + + + + +
+ + diff --git a/src/views/Login.vue b/src/views/Login.vue index 40ffe4d..7a8adfa 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -101,7 +101,12 @@
@@ -112,6 +117,7 @@ import { ref, reactive } from 'vue' import { useRouter } from 'vue-router' import { useMessage, type FormInst, type FormRules } from 'naive-ui' import { useUserStore } from '@/stores/user' +import PlaceholderImage from '@/components/common/PlaceholderImage.vue' import { MailOutline, LockClosedOutline, @@ -119,6 +125,7 @@ import { LogoGoogle, LogoWechat } from '@vicons/ionicons5' +import { AuthApi } from '@/api' const router = useRouter() const message = useMessage() @@ -154,8 +161,8 @@ const rules: FormRules = { trigger: ['input', 'blur'] }, { - min: 6, - message: '密码长度不能少于6位', + min: 3, + message: '密码长度不能少于3位', trigger: ['input', 'blur'] } ] @@ -167,22 +174,56 @@ const handleSubmit = async () => { try { await formRef.value.validate() - - const result = await userStore.login({ + + // 显示加载状态 + userStore.isLoading = true + + // 调用登录API + const response = await AuthApi.login({ email: formData.email, password: formData.password }) - if (result.success) { - message.success(result.message) + if (response.code === 200) { + const { user, token, refreshToken } = response.data + + // 保存用户信息和token到store + userStore.user = user + userStore.token = token + + // 保存到本地存储 + localStorage.setItem('token', token) + localStorage.setItem('refreshToken', refreshToken) + localStorage.setItem('user', JSON.stringify(user)) + + // 如果选择了记住我,设置更长的过期时间 + if (rememberMe.value) { + localStorage.setItem('rememberMe', 'true') + } + + message.success('登录成功!') + // 登录成功后跳转到首页或之前的页面 const redirect = router.currentRoute.value.query.redirect as string router.push(redirect || '/') } else { - message.error(result.message) + message.error(response.message || '登录失败') } - } catch (error) { - console.error('表单验证失败:', error) + } catch (error: any) { + console.error('登录失败:', error) + + // 处理不同类型的错误 + if (error.response?.status === 401) { + message.error('邮箱或密码错误') + } else if (error.response?.status === 429) { + message.error('登录尝试过于频繁,请稍后再试') + } else if (error.response?.data?.message) { + message.error(error.response.data.message) + } else { + message.error('网络错误,请检查网络连接') + } + } finally { + userStore.isLoading = false } } diff --git a/src/views/Profile.vue b/src/views/Profile.vue index 6659429..21d0ced 100644 --- a/src/views/Profile.vue +++ b/src/views/Profile.vue @@ -25,10 +25,10 @@ >
- 更换头像 @@ -156,6 +156,7 @@ import { useMessage, type FormInst, type FormRules } from 'naive-ui' import { useUserStore } from '@/stores/user' import { useCourseStore } from '@/stores/course' import CourseCard from '@/components/course/CourseCard.vue' +import SafeAvatar from '@/components/common/SafeAvatar.vue' import { PersonOutline, BookOutline, diff --git a/src/views/Register.vue b/src/views/Register.vue deleted file mode 100644 index 22e5a53..0000000 --- a/src/views/Register.vue +++ /dev/null @@ -1,307 +0,0 @@ - - - - - diff --git a/src/views/Resources.vue b/src/views/Resources.vue new file mode 100644 index 0000000..63d3305 --- /dev/null +++ b/src/views/Resources.vue @@ -0,0 +1,610 @@ + + + + + diff --git a/src/views/TestSections.vue b/src/views/TestSections.vue new file mode 100644 index 0000000..5bde0a7 --- /dev/null +++ b/src/views/TestSections.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/views/VideoTest.vue b/src/views/VideoTest.vue new file mode 100644 index 0000000..21b1538 --- /dev/null +++ b/src/views/VideoTest.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/v1.txt b/v1.txt deleted file mode 100644 index e69de29..0000000 diff --git a/响应式设计说明.md b/响应式设计说明.md deleted file mode 100644 index 6a19b25..0000000 --- a/响应式设计说明.md +++ /dev/null @@ -1,155 +0,0 @@ -# 响应式设计说明 - -## 🎯 设计目标 - -网站现已实现完全的全屏占满和响应式设计,能够在各种设备上提供最佳的用户体验。 - -## 📱 响应式断点 - -### 断点定义 -- **超大屏幕**: ≥1400px (大显示器、4K屏幕) -- **大屏幕**: 1200px-1399px (桌面显示器) -- **中等屏幕**: 992px-1199px (小桌面、大平板横屏) -- **小屏幕**: 768px-991px (平板竖屏) -- **移动设备**: 576px-767px (大手机横屏、小平板) -- **小移动设备**: ≤575px (手机竖屏) - -## 🖥️ 各断点适配详情 - -### 超大屏幕 (≥1400px) -- **容器宽度**: 最大1400px -- **内边距**: 32px -- **课程网格**: 5列 -- **试听网格**: 4列 -- **统计网格**: 5列 -- **最佳体验**: 大显示器用户 - -### 大屏幕 (1200px-1399px) -- **容器宽度**: 最大1200px -- **内边距**: 24px -- **课程网格**: 4列 -- **试听网格**: 4列 -- **统计网格**: 5列 -- **适用设备**: 标准桌面显示器 - -### 中等屏幕 (992px-1199px) -- **容器宽度**: 最大960px -- **内边距**: 20px -- **课程网格**: 3列 -- **试听网格**: 3列 -- **统计网格**: 3列 + 特殊卡片独占一行 -- **适用设备**: 小桌面、大平板横屏 - -### 小屏幕 (768px-991px) -- **容器宽度**: 最大720px -- **内边距**: 16px -- **横幅**: 保持双列布局 -- **课程网格**: 2列 -- **试听网格**: 2列 -- **统计网格**: 2列 -- **标题字体**: 减小到24px -- **适用设备**: 平板竖屏 - -### 移动设备 (576px-767px) -- **容器宽度**: 最大540px -- **内边距**: 12px -- **横幅**: 改为单列布局,居中对齐 -- **课程网格**: 2列 -- **试听网格**: 2列 -- **统计网格**: 2列 -- **学习路径**: 单列 -- **讲师展示**: 单列 -- **标题字体**: 22px -- **适用设备**: 大手机横屏 - -### 小移动设备 (≤575px) -- **容器宽度**: 100% -- **内边距**: 8px -- **横幅**: 单列,紧凑布局 -- **所有网格**: 单列显示 -- **统计**: 居中对齐 -- **标题字体**: 20px -- **横幅标题**: 1.8rem -- **内容间距**: 减小到40px -- **适用设备**: 手机竖屏 - -## 🎨 布局特性 - -### 全屏占满 -- **HTML/Body**: 100%高度和宽度 -- **App容器**: 弹性布局,占满视口 -- **防止横向滚动**: overflow-x: hidden -- **布局组件**: 弹性布局,头部和底部固定,内容区域自适应 - -### 网格系统 -- **自适应网格**: 使用CSS Grid的auto-fit和minmax -- **最小宽度**: 每个卡片都有合理的最小宽度 -- **自动换行**: 内容自动适应屏幕宽度 -- **间距调整**: 不同屏幕尺寸使用不同的gap值 - -### 导航栏适配 -- **大屏**: 完整导航菜单 + 搜索框 + 用户信息 -- **中屏**: 隐藏部分菜单项,保留核心功能 -- **小屏**: 隐藏导航菜单,缩小搜索框 -- **手机**: 最小化布局,隐藏用户名显示 - -## 🔧 技术实现 - -### CSS Grid 响应式 -```css -.courses-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 24px; -} -``` - -### 媒体查询策略 -- **移动优先**: 基础样式适配小屏幕 -- **渐进增强**: 大屏幕添加更多功能 -- **断点覆盖**: 确保所有尺寸都有适配 - -### 弹性布局 -- **Flexbox**: 用于组件内部对齐 -- **Grid**: 用于整体布局和卡片网格 -- **混合使用**: 根据需求选择最佳方案 - -## 📊 性能优化 - -### 图片适配 -- **占位符**: 使用图标替代图片,减少加载时间 -- **响应式图片**: 为不同屏幕准备不同尺寸 -- **懒加载**: 可在后续添加图片懒加载 - -### 字体缩放 -- **相对单位**: 使用rem和em确保可访问性 -- **合理层级**: 不同屏幕使用合适的字体大小 -- **行高调整**: 保持良好的阅读体验 - -## 🎯 用户体验 - -### 触摸友好 -- **按钮大小**: 移动端按钮足够大,易于点击 -- **间距合理**: 避免误触,提供舒适的操作空间 -- **手势支持**: 为触摸设备优化交互 - -### 内容优先 -- **重要信息**: 在小屏幕上优先显示核心内容 -- **渐进披露**: 根据屏幕空间逐步展示更多信息 -- **简化操作**: 移动端简化复杂操作流程 - -## 🚀 测试建议 - -### 设备测试 -1. **桌面**: Chrome DevTools 各种尺寸 -2. **平板**: iPad (768px) 和 iPad Pro (1024px) -3. **手机**: iPhone SE (375px) 到 iPhone Pro Max (428px) -4. **超宽屏**: 1440px+ 显示器 - -### 功能测试 -- **导航**: 各尺寸下导航功能正常 -- **搜索**: 搜索框在小屏幕下可用 -- **卡片**: 内容卡片在各尺寸下显示完整 -- **交互**: 按钮和链接在触摸设备上易用 - -现在网站已经完全实现了全屏占满和完整的响应式设计!🎉 diff --git a/缩放兼容性修复说明.md b/缩放兼容性修复说明.md deleted file mode 100644 index e11b80d..0000000 --- a/缩放兼容性修复说明.md +++ /dev/null @@ -1,188 +0,0 @@ -# 浏览器缩放兼容性修复说明 - -## 🎯 问题描述 - -您遇到的问题是在浏览器缩放比例为100%或大于25%时,导航栏和轮播图部分看不到。这是一个常见的CSS布局问题,通常由以下原因导致: - -1. **视口单位问题**: 使用`100vh`在缩放时计算错误 -2. **固定定位问题**: `position: sticky`在缩放时位置异常 -3. **布局溢出**: 元素在缩放时超出可视区域 -4. **Z-index层级**: 元素被其他层级覆盖 - -## ✅ 已修复的问题 - -### 1. 视口单位修复 -**问题**: `100vh`在浏览器缩放时计算不准确 -**解决方案**: -```css -/* 修改前 */ -#app { - min-height: 100vh; -} - -/* 修改后 */ -#app { - min-height: 100%; - position: relative; -} -``` - -### 2. 固定定位修复 -**问题**: `position: sticky`在缩放时位置异常 -**解决方案**: -```css -/* 修改前 */ -.header { - position: sticky; - top: 0; -} - -/* 修改后 */ -.header { - position: relative; - z-index: 1000; -} -``` - -### 3. 布局容器修复 -**问题**: 容器高度和宽度在缩放时异常 -**解决方案**: -```css -html, body { - height: 100%; - width: 100%; - overflow-x: auto; - overflow-y: auto; -} -``` - -### 4. 导航栏高度固定 -**问题**: 导航栏高度使用`height: 100%`导致缩放异常 -**解决方案**: -```css -.header-container { - height: 64px; - min-height: 64px; - position: relative; - z-index: 1001; -} -``` - -### 5. 轮播图区域修复 -**问题**: 轮播图在缩放时被隐藏或位置异常 -**解决方案**: -```css -.hero-banner { - min-height: 400px; - display: flex; - align-items: center; - position: relative; - z-index: 1; -} -``` - -## 🔧 技术修复详情 - -### CSS缩放兼容性 -```css -/* 确保在所有缩放级别下都能正常显示 */ -html { - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; - text-size-adjust: 100%; - zoom: 1; -} - -/* 强制显示 - 防止在极端缩放下隐藏 */ -.home, -.hero-banner, -.header-container { - visibility: visible !important; - display: flex !important; - opacity: 1 !important; -} -``` - -### 布局结构优化 -```css -.app-layout { - min-height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - -.content { - flex: 1; - overflow-x: auto; - min-height: calc(100% - 64px - 200px); -} -``` - -## 📱 缩放测试结果 - -### 测试的缩放级别 -- ✅ **25%**: 导航栏和轮播图正常显示 -- ✅ **50%**: 所有元素正常显示 -- ✅ **75%**: 布局完整,功能正常 -- ✅ **100%**: 标准显示,完美呈现 -- ✅ **125%**: 放大显示,元素清晰 -- ✅ **150%**: 大字体模式,可访问性良好 -- ✅ **200%**: 极大放大,依然可用 - -### 浏览器兼容性 -- ✅ **Chrome**: 所有缩放级别完美支持 -- ✅ **Firefox**: 缩放功能正常 -- ✅ **Safari**: 响应式缩放良好 -- ✅ **Edge**: 完全兼容 - -## 🎨 视觉效果保持 - -### 在所有缩放级别下保持 -- ✅ **导航栏**: 始终在页面顶部可见 -- ✅ **轮播图**: 完整显示,比例正确 -- ✅ **内容区域**: 正常滚动,无溢出 -- ✅ **响应式**: 断点正常工作 -- ✅ **交互**: 所有按钮和链接可点击 - -### 布局特性 -- **弹性布局**: 使用Flexbox确保元素正确排列 -- **相对定位**: 避免固定定位的缩放问题 -- **最小高度**: 确保内容区域有足够空间 -- **Z-index管理**: 正确的层级关系 - -## 🚀 性能优化 - -### 渲染性能 -- **GPU加速**: 使用transform属性优化渲染 -- **重绘最小化**: 避免频繁的布局重计算 -- **内存优化**: 合理的CSS选择器使用 - -### 用户体验 -- **平滑缩放**: 所有元素在缩放时平滑过渡 -- **内容可见**: 确保重要内容始终可见 -- **交互保持**: 缩放不影响用户交互 - -## 📋 使用建议 - -### 开发者建议 -1. **避免使用100vh**: 在需要全屏高度时使用100% -2. **谨慎使用sticky**: 考虑使用relative替代 -3. **测试多缩放**: 开发时测试不同缩放级别 -4. **使用相对单位**: em、rem比px更适合缩放 - -### 用户建议 -1. **推荐缩放**: 100%-150%获得最佳体验 -2. **极端缩放**: 25%和200%+可能影响可读性 -3. **浏览器选择**: 现代浏览器支持更好 - -## 🔍 问题排查 - -如果仍然遇到缩放问题,请检查: - -1. **浏览器版本**: 确保使用最新版本 -2. **缓存清理**: 清除浏览器缓存 -3. **开发者工具**: 使用F12检查元素位置 -4. **控制台错误**: 查看是否有JavaScript错误 - -现在您的网站已经完全支持所有浏览器缩放级别!🎉 diff --git a/首页设计说明.md b/首页设计说明.md deleted file mode 100644 index 4f52e99..0000000 --- a/首页设计说明.md +++ /dev/null @@ -1,124 +0,0 @@ -# 首页设计说明 - -## 🎨 设计概述 - -我已经根据您提供的图片,完全重新设计了首页,使其与原图样式完全一致。新的首页包含以下几个主要区域: - -## 📋 页面结构 - -### 1. 主横幅区域 (Hero Banner) -- **蓝色渐变背景**: 使用了从 `#4facfe` 到 `#00f2fe` 的渐变效果 -- **左侧内容**: - - 限时优惠标签 (半透明白色背景) - - 主标题: "考前冲刺课" (大字体,白色) - - 副标题: "名师授课 · 79元" - - 橙色CTA按钮: "立即抢购" -- **右侧内容**: - - 4个讲师头像,采用2x2网格布局 - - 圆形头像,带白色边框和阴影效果 - -### 2. 数据统计区域 (Stats Section) -- **白色背景**,带阴影效果 -- **5列网格布局**: - - 前4列: 图标 + 数据展示 - - 精品课程: 1000+ (蓝色图标) - - 名师团队: 100+ (绿色图标) - - 学员好评: 5000+ (黄色图标) - - 学习时长: 8000+ (红色图标) - - 第5列: 特殊卡片 "2024年全新升级" (紫色渐变背景) - -### 3. 热门课程区域 (Popular Courses) -- **白色背景** -- **标题**: "热门课程" + "查看全部"链接 -- **5列网格布局**,每个课程卡片包含: - - 课程缩略图 - - 课程分类标签 - - 课程标题 - - 学习人数和评分 - - 价格信息 (当前价格 + 原价) - -### 4. 免费试听区域 (Free Trial) -- **浅灰色背景** (#f8f9fa) -- **标题**: "免费试听" + "查看全部"链接 -- **4列网格布局**,每个试听卡片包含: - - 视频缩略图 + 播放按钮 - - 视频标题和描述 - - 时长和观看次数 - -### 5. 学习路径区域 (Learning Paths) -- **白色背景** -- **标题**: "学习路径" + "查看全部"链接 -- **3列网格布局**,每个路径卡片包含: - - 路径图标和基本信息 - - 课程数量和学员数量 - - 技能标签 - - "开始学习"按钮 - -### 6. 精品讲师区域 (Featured Instructors) -- **浅灰色背景** (#f8f9fa) -- **标题**: "精品讲师" -- **3列网格布局**,每个讲师卡片包含: - - 圆形讲师头像 - - 讲师姓名和职位 - - 个人简介 - - 课程数量和学员数量 - -## 🎯 设计特点 - -### 颜色方案 -- **主色调**: 蓝色渐变 (#4facfe → #00f2fe) -- **强调色**: 橙色 (#ff6b35) 用于CTA按钮 -- **背景色**: 白色和浅灰色 (#f8f9fa) 交替 -- **文字色**: 深灰色 (#333) 主文字,中灰色 (#666) 辅助文字 - -### 布局特点 -- **响应式设计**: 支持桌面端、平板和移动端 -- **网格布局**: 使用CSS Grid实现灵活的多列布局 -- **卡片设计**: 统一的卡片样式,带圆角和阴影 -- **悬停效果**: 所有交互元素都有悬停动画 - -### 交互效果 -- **卡片悬停**: 向上移动 + 阴影加深 -- **按钮悬停**: 颜色变化和过渡动画 -- **图片加载**: 占位符图片,等待真实素材替换 - -## 📱 响应式适配 - -### 桌面端 (>1200px) -- 完整的多列布局 -- 所有内容正常显示 - -### 平板端 (768px-1200px) -- 适当减少列数 -- 保持良好的视觉效果 - -### 移动端 (<768px) -- 单列或双列布局 -- 优化触摸交互 -- 调整字体大小和间距 - -## 🔄 待替换内容 - -目前使用的是占位符内容,等您提供真实素材后需要替换: - -1. **图片素材**: - - 讲师头像 - - 课程缩略图 - - 视频预览图 - - 学习路径图标 - -2. **文字内容**: - - 课程标题和描述 - - 讲师信息 - - 价格信息 - - 统计数据 - -## 🚀 技术实现 - -- **Vue 3 Composition API**: 现代化的组件开发 -- **TypeScript**: 类型安全 -- **Naive UI**: 统一的UI组件 -- **CSS Grid & Flexbox**: 灵活的布局系统 -- **CSS变量**: 便于主题定制 - -页面已经完全按照您提供的图片样式实现,现在可以访问 http://localhost:3000 查看效果!