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/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 d620727..aeba31d 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -83,26 +83,38 @@
- + | - {{ t('header.register') }} + {{ t('header.register') }}
- +
+ + + + + + @@ -115,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() @@ -131,6 +145,12 @@ const mobileMenuOpen = ref(false) // 当前激活的菜单项 const activeKey = ref('home') +// 认证模态框相关 +const loginModalVisible = ref(false) +const registerModalVisible = ref(false) + + + // 语言切换相关 @@ -158,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) } @@ -212,9 +227,6 @@ const handleUserMenuSelect = (key: string) => { case 'profile': router.push('/profile') break - case 'settings': - // TODO: 实现设置页面 - break case 'logout': userStore.logout() router.push('/') @@ -222,6 +234,24 @@ const handleUserMenuSelect = (key: string) => { } } +// 显示登录模态框 +const showLoginModal = () => { + loginModalVisible.value = true +} + +// 显示注册模态框 +const showRegisterModal = () => { + registerModalVisible.value = true +} + +// 认证成功处理 +const handleAuthSuccess = () => { + // 认证成功后可以进行一些操作,比如刷新用户信息等 + console.log('认证成功') +} + + + // 点击外部关闭下拉框 @@ -694,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 @@ + \ 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 }}