feat: 1st ver
This commit is contained in:
parent
79a950ca68
commit
d25f7447ac
149
README.md
149
README.md
|
|
@ -1,5 +1,148 @@
|
||||||
# Vue 3 + TypeScript + Vite
|
# Vue Dicts - 法语词典学习平台
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
一个基于 Vue 3 + TypeScript + Tailwind CSS 的现代化法语学习平台,集成词典查询、翻译和写作指导功能。
|
||||||
|
|
||||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
## 功能特性
|
||||||
|
|
||||||
|
- 🔍 **智能词典查询** - 支持法语/英语词汇查询,提供详细释义和例句
|
||||||
|
- 🌐 **多语言翻译** - 中法英三语互译,支持翻译历史记录
|
||||||
|
- ✍️ **AI写作指导** - 智能分析法语作文,提供语法和文体建议
|
||||||
|
- 👤 **用户认证系统** - JWT token认证,安全的用户登录注册
|
||||||
|
- 📱 **响应式设计** - 适配多种设备尺寸
|
||||||
|
- 🎨 **优雅UI设计** - 基于Tailwind CSS的现代化界面
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- Vue 3 (Composition API)
|
||||||
|
- TypeScript
|
||||||
|
- Vue Router 4
|
||||||
|
- Tailwind CSS
|
||||||
|
- Axios
|
||||||
|
- @vueuse/core
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- FastAPI (Python)
|
||||||
|
- Tortoise ORM
|
||||||
|
- Redis
|
||||||
|
- JWT Authentication
|
||||||
|
- MySQL/SQLite
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Node.js 16+
|
||||||
|
- Python 3.8+
|
||||||
|
- Redis
|
||||||
|
- MySQL (可选,默认使用SQLite)
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
1. **前端依赖**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **后端依赖**
|
||||||
|
```bash
|
||||||
|
cd backend/dict-server
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动项目
|
||||||
|
|
||||||
|
#### 方法一:使用批处理文件(Windows)
|
||||||
|
```bash
|
||||||
|
# 启动后端
|
||||||
|
start-backend.bat
|
||||||
|
|
||||||
|
# 启动前端(新开命令窗口)
|
||||||
|
start-frontend.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方法二:手动启动
|
||||||
|
```bash
|
||||||
|
# 启动后端
|
||||||
|
cd backend/dict-server
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# 启动前端(新开终端)
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
访问 http://localhost:5173 查看前端界面
|
||||||
|
后端API文档:http://localhost:8000/docs
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
vue-dicts/
|
||||||
|
├── src/ # 前端源码
|
||||||
|
│ ├── api/ # API客户端
|
||||||
|
│ ├── components/ # Vue组件
|
||||||
|
│ ├── composables/ # 组合式API
|
||||||
|
│ ├── router/ # 路由配置
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ └── style.css # 全局样式
|
||||||
|
├── backend/ # 后端源码
|
||||||
|
│ └── dict-server/ # FastAPI应用
|
||||||
|
├── models/ # 设计效果图
|
||||||
|
├── public/ # 静态资源
|
||||||
|
└── package.json # 项目配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要页面
|
||||||
|
|
||||||
|
- **首页** (`/`) - 项目介绍和快速入口
|
||||||
|
- **词典查询** (`/dict`) - 法语词汇查询功能
|
||||||
|
- **翻译** (`/trans`) - 多语言翻译功能
|
||||||
|
- **写作指导** (`/write`) - AI写作辅助功能
|
||||||
|
- **登录** (`/login`) - 用户认证页面
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 用户认证
|
||||||
|
- `POST /users/login` - 用户登录
|
||||||
|
- `POST /users/register` - 用户注册
|
||||||
|
- `POST /users/logout` - 用户登出
|
||||||
|
|
||||||
|
### 词典查询
|
||||||
|
- `GET /search` - 搜索单词
|
||||||
|
- 参数: `lang_pref` (语言偏好), `query_word` (查询词汇)
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 前端开发
|
||||||
|
- 使用 Vue 3 Composition API
|
||||||
|
- TypeScript 提供类型安全
|
||||||
|
- Tailwind CSS 用于样式
|
||||||
|
- 响应式设计适配移动端
|
||||||
|
|
||||||
|
### 后端开发
|
||||||
|
- FastAPI 提供高性能API
|
||||||
|
- Tortoise ORM 数据库操作
|
||||||
|
- JWT 认证保护接口
|
||||||
|
- Redis 缓存提升性能
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 前端部署
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 将 dist/ 目录部署到静态服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端部署
|
||||||
|
```bash
|
||||||
|
cd backend/dict-server
|
||||||
|
# 配置生产环境变量
|
||||||
|
# 启动 uvicorn 服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request 来改进项目!
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 566 KiB |
|
|
@ -9,6 +9,8 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@vueuse/core": "^13.7.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|
@ -1078,6 +1080,12 @@
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
|
||||||
|
|
@ -1285,6 +1293,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "13.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.7.0.tgz",
|
||||||
|
"integrity": "sha512-myagn09+c6BmS6yHc1gTwwsdZilAovHslMjyykmZH3JNyzI5HoWhv114IIdytXiPipdHJ2gDUx0PB93jRduJYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
"@vueuse/metadata": "13.7.0",
|
||||||
|
"@vueuse/shared": "13.7.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "13.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.7.0.tgz",
|
||||||
|
"integrity": "sha512-8okFhS/1ite8EwUdZZfvTYowNTfXmVCOrBFlA31O0HD8HKXhY+WtTRyF0LwbpJfoFPc+s9anNJIXMVrvP7UTZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "13.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.7.0.tgz",
|
||||||
|
"integrity": "sha512-Wi2LpJi4UA9kM0OZ0FCZslACp92HlVNw1KPaDY6RAzvQ+J1s7seOtcOpmkfbD5aBSmMn9NvOakc8ZxMxmDXTIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||||
|
|
@ -1292,6 +1338,23 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -1309,6 +1372,19 @@
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/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/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
|
@ -1318,6 +1394,18 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/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/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
|
@ -1331,6 +1419,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
|
@ -1340,6 +1437,20 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/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/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.2",
|
"version": "5.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||||
|
|
@ -1365,6 +1476,51 @@
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.6",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||||
|
|
@ -1426,6 +1582,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"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.npmjs.org/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/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -1440,12 +1632,109 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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": {
|
"node_modules/he": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
|
@ -1702,6 +1991,36 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
|
@ -1832,6 +2151,12 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.45.1",
|
"version": "4.45.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@vueuse/core": "^13.7.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import apiClient from './client'
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
name: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
lang_pref: 'jp' | 'fr' | 'private'
|
||||||
|
portrait?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
access_token: string
|
||||||
|
token_type: string
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
is_admin: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
id: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登录
|
||||||
|
export const login = async (credentials: LoginRequest): Promise<LoginResponse> => {
|
||||||
|
const response = await apiClient.post('/users/login', credentials)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户注册
|
||||||
|
export const register = async (userData: RegisterRequest): Promise<RegisterResponse> => {
|
||||||
|
const response = await apiClient.post('/users/register', userData)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登出
|
||||||
|
export const logout = async (): Promise<void> => {
|
||||||
|
await apiClient.post('/users/logout')
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export const getCurrentUser = async () => {
|
||||||
|
const response = await apiClient.get('/users/me')
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: 'http://localhost:8000', // 后端API地址
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器 - 添加token
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器 - 处理错误
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// token过期或无效,清除本地存储并跳转到登录页
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import apiClient from './client'
|
||||||
|
|
||||||
|
export interface WordDefinition {
|
||||||
|
word: string
|
||||||
|
part_of_speech: string
|
||||||
|
meaning: string
|
||||||
|
example: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchParams {
|
||||||
|
lang_pref: string
|
||||||
|
query_word: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索单词
|
||||||
|
export const searchWord = async (params: SearchParams): Promise<WordDefinition[]> => {
|
||||||
|
const response = await apiClient.get('/search', {
|
||||||
|
params: {
|
||||||
|
lang_pref: params.lang_pref,
|
||||||
|
query_word: params.query_word
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="bg-white h-[174px] flex items-center">
|
<header class="flex items-center bg-white h-[174px]">
|
||||||
<div class="max-w-[1030px] mx-auto flex items-center w-full">
|
<div class="flex items-center mx-auto w-full max-w-[1030px]">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<router-link to="/" class="block w-[237px] h-[61px] bg-[url('/images/LOGO.png')] bg-contain bg-no-repeat pl-[100px] leading-[60px] font-deserta text-5xl text-blue-700">
|
<router-link to="/" class="block bg-[url('/images/LOGO.png')] bg-contain bg-no-repeat pl-[100px] w-[237px] h-[61px] font-deserta text-blue-700 text-5xl leading-[60px]">
|
||||||
CiDian
|
CiDian
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<!-- Nav -->
|
<!-- Nav -->
|
||||||
<nav class="ml-[187px] flex space-x-[53px] text-xl">
|
<nav class="flex space-x-[53px] ml-[187px] text-xl">
|
||||||
<router-link v-slot="{ isActive }" to="/dict">
|
<router-link v-slot="{ isActive }" to="/dict">
|
||||||
<a
|
<a
|
||||||
:class="[
|
:class="[
|
||||||
|
|
@ -45,13 +45,28 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- User -->
|
<!-- User -->
|
||||||
<div class="flex space-x-4 ml-auto">
|
<div class="flex items-center space-x-4 ml-auto">
|
||||||
<a href="#" class="w-[42px] h-[42px] bg-gray-300 rounded-full border-2 border-primary block" />
|
<template v-if="isAuthenticated">
|
||||||
<a href="#" class="w-[25px] h-[32px] bg-[url('/images/bell.png')] bg-cover" />
|
<span class="font-inter text-gray-700">{{ user?.username }}</span>
|
||||||
<a href="#" class="w-[42px] h-[42px] bg-[url('/images/icon_任务栏收缩.png')] bg-cover" />
|
<a href="#" class="block bg-gray-300 border-2 border-blue-700 rounded-full w-[42px] h-[42px]" />
|
||||||
|
<a href="#" class="bg-[url('/images/bell.png')] bg-cover w-[25px] h-[32px]" />
|
||||||
|
<button @click="handleLogout" class="bg-[url('/images/icon_任务栏收缩.png')] bg-cover w-[42px] h-[42px]" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<router-link to="/login" class="bg-blue-700 hover:bg-blue-600 px-6 py-2 rounded-full font-inter text-white">
|
||||||
|
登录
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useAuth } from '../composables/useAuth'
|
||||||
|
|
||||||
|
const { user, isAuthenticated, logout } = useAuth()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,15 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="bg-white h-[88px] flex justify-center items-center">
|
<section class="flex justify-center items-center bg-white h-[88px]">
|
||||||
<div class="relative w-[804px]">
|
<div class="relative w-[804px]">
|
||||||
<span class="absolute left-0 top-1/2 -translate-y-1/2 bg-primary bg-blue-700 text-white rounded-full px-8 py-3 z-10">
|
<select
|
||||||
法 / 英
|
v-model="selectedLang"
|
||||||
</span>
|
class="top-1/2 left-0 z-10 absolute bg-blue-700 px-8 py-3 border-none rounded-full outline-none text-white -translate-y-1/2 appearance-none cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="fr">法语</option>
|
||||||
|
<option value="en">英语</option>
|
||||||
|
</select>
|
||||||
<input
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入单词"
|
placeholder="请输入单词"
|
||||||
class="w-full h-[56px] pl-[207px] pr-[70px] border-[5px] border-primary border-blue-700 rounded-full text-xl outline-none"
|
class="pr-[70px] pl-[207px] border-[5px] border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[56px] text-xl"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="handleSearch"
|
||||||
|
class="top-1/2 right-0 absolute bg-[length:60%] bg-[url('/images/search.png')] bg-blue-700 hover:bg-blue-600 bg-no-repeat bg-center rounded-full w-[56px] h-[56px] -translate-y-1/2"
|
||||||
/>
|
/>
|
||||||
<button class="absolute right-0 top-1/2 -translate-y-1/2 w-[56px] h-[56px] bg-primary rounded-full bg-[url('/images/search.png')] bg-no-repeat bg-center bg-[length:60%]" />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedLang = ref('fr')
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
// 跳转到词典页面并传递搜索参数
|
||||||
|
router.push({
|
||||||
|
name: 'Dict',
|
||||||
|
query: {
|
||||||
|
q: searchQuery.value.trim(),
|
||||||
|
lang: selectedLang.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { login as apiLogin, logout as apiLogout, register as apiRegister, type LoginRequest, type RegisterRequest } from '../api/auth'
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const user = ref<any>(null)
|
||||||
|
const token = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 从localStorage恢复状态
|
||||||
|
const initAuth = () => {
|
||||||
|
const savedToken = localStorage.getItem('access_token')
|
||||||
|
const savedUser = localStorage.getItem('user')
|
||||||
|
|
||||||
|
if (savedToken) {
|
||||||
|
token.value = savedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedUser) {
|
||||||
|
try {
|
||||||
|
user.value = JSON.parse(savedUser)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse saved user:', e)
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||||
|
|
||||||
|
const login = async (credentials: LoginRequest) => {
|
||||||
|
try {
|
||||||
|
const response = await apiLogin(credentials)
|
||||||
|
|
||||||
|
// 保存到状态
|
||||||
|
token.value = response.access_token
|
||||||
|
user.value = response.user
|
||||||
|
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('access_token', response.access_token)
|
||||||
|
localStorage.setItem('user', JSON.stringify(response.user))
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async (userData: RegisterRequest) => {
|
||||||
|
try {
|
||||||
|
const response = await apiRegister(userData)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await apiLogout()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
} finally {
|
||||||
|
// 无论API调用是否成功,都清除本地状态
|
||||||
|
token.value = null
|
||||||
|
user.value = null
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: computed(() => user.value),
|
||||||
|
token: computed(() => token.value),
|
||||||
|
isAuthenticated,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化认证状态
|
||||||
|
initAuth()
|
||||||
|
|
@ -1,10 +1,30 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
export default createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'Home', component: () => import('../views/HomePage.vue') },
|
{ path: '/', name: 'Home', component: () => import('../views/HomePage.vue') },
|
||||||
{ path: '/dict', name: 'Dict', component: () => import('../views/DictPage.vue') },
|
{ path: '/dict', name: 'Dict', component: () => import('../views/DictPage.vue') },
|
||||||
|
{ path: '/trans', name: 'Translation', component: () => import('../views/TranslationPage.vue') },
|
||||||
|
{ path: '/write', name: 'Writing', component: () => import('../views/WritingPage.vue') },
|
||||||
{ path: '/login', name: 'Login', component: () => import('../views/LoginPage.vue') },
|
{ path: '/login', name: 'Login', component: () => import('../views/LoginPage.vue') },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
// 需要认证的页面
|
||||||
|
const requiresAuth = ['Dict', 'Translation', 'Writing']
|
||||||
|
|
||||||
|
if (requiresAuth.includes(to.name as string) && !token) {
|
||||||
|
next('/login')
|
||||||
|
} else if (to.name === 'Login' && token) {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -1,49 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<AppHeader active="dict" />
|
<AppHeader active="dict" />
|
||||||
<SearchBar />
|
|
||||||
<AiAssist />
|
|
||||||
|
|
||||||
<!-- upper -->
|
<!-- 搜索区域 -->
|
||||||
<section class="bg-[url('/images/french-learning-illustration.png')] bg-no-repeat bg-center pt-[95px]">
|
<section class="bg-white py-12">
|
||||||
<div class="max-w-[1030px] mx-auto px-4">
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
<ul class="font-deserta text-[56px] leading-[76px]">
|
<div class="mb-8 text-center">
|
||||||
<li>YOUR ELEGANT</li>
|
<h1 class="mb-4 font-deserta text-blue-700 text-4xl">法语词典查询</h1>
|
||||||
<li class="text-primary">COMPANION</li>
|
<p class="font-inter text-gray-600">输入法语单词,获取详细释义和例句</p>
|
||||||
<li>IN FRENCH LEARNING</li>
|
</div>
|
||||||
</ul>
|
|
||||||
<router-link to="/dict" class="inline-block mt-[67px] w-[268px] h-[64px] bg-primary bg-blue-700 text-white text-2xl rounded-full text-center leading-[64px]">
|
|
||||||
SEARCH NOW
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<div class="mt-[139px]">
|
<div class="mx-auto max-w-[600px]">
|
||||||
<h3 class="text-[40px] font-deserta">INTRODUCTION</h3>
|
<div class="flex gap-4">
|
||||||
<p class="mt-[59px] text-xl leading-8">
|
<input
|
||||||
本站致力于打造一款高质量、实用而优雅的法语词典工具,集词义查询、例句拓展、词根分析于一体,助力学习者深入理解每一个法语单词背后的文化与语境。不止查词,更是探索语言之美的旅程。
|
v-model="searchQuery"
|
||||||
</p>
|
@keyup.enter="handleSearch"
|
||||||
<p class="mt-4 text-xl leading-8 text-gray-500">
|
type="text"
|
||||||
A refined and practical French dictionary designed for learners and enthusiasts. Discover accurate definitions, contextual examples, and deep linguistic insights — because learning French is more than memorizing words, it's exploring a world of meaning.
|
placeholder="请输入法语单词..."
|
||||||
</p>
|
class="flex-1 px-6 border-2 border-blue-700 focus:border-blue-500 rounded-full outline-none h-[60px] text-xl"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="handleSearch"
|
||||||
|
:disabled="loading || !searchQuery.trim()"
|
||||||
|
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 rounded-full w-[120px] h-[60px] font-inter text-white disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ loading ? '搜索中...' : '搜索' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
<select v-model="selectedLang" class="px-4 py-2 border border-gray-300 rounded">
|
||||||
|
<option value="fr">法语</option>
|
||||||
|
<option value="en">英语</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- lower -->
|
<!-- 搜索结果 -->
|
||||||
<section class="bg-[url('/images/lower.png')] bg-contain bg-no-repeat h-[437px] w-full">
|
<section class="bg-gray-50 py-12 min-h-[400px]">
|
||||||
<div class="max-w-[1030px] mx-auto flex items-center justify-between h-full">
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
<ul class="font-deserta text-[35px] leading-[53px]">
|
<div v-if="error" class="bg-red-100 mb-6 px-4 py-3 border border-red-400 rounded text-red-700">
|
||||||
<li>CUMULATIVE <span class="text-primary">WORD</span></li>
|
{{ error }}
|
||||||
<li>LOOKUP COUNT</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p class="text-muted mb-3 text-lg">累计</p>
|
|
||||||
<div class="flex items-end gap-2">
|
|
||||||
<span v-for="i in 11" :key="i" class="w-[34px] h-[60px] rounded-[17px] bg-gradient-to-b from-white to-[#d5e2f8] border border-transparent flex items-center justify-center text-2xl text-muted shadow">
|
|
||||||
0
|
|
||||||
</span>
|
|
||||||
<span class="text-primary text-[35px] ml-2">TIMES</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="searchResults.length > 0" class="space-y-6">
|
||||||
|
<h2 class="mb-6 font-deserta text-blue-700 text-2xl">搜索结果</h2>
|
||||||
|
|
||||||
|
<div v-for="(result, index) in searchResults" :key="index" class="bg-white shadow-md p-6 rounded-lg">
|
||||||
|
<div class="flex justify-between items-start mb-4">
|
||||||
|
<h3 class="font-bold text-blue-700 text-2xl">{{ result.word }}</h3>
|
||||||
|
<span class="bg-blue-100 px-3 py-1 rounded-full text-blue-700 text-sm">
|
||||||
|
{{ result.part_of_speech }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="mb-2 font-semibold text-gray-700">释义:</h4>
|
||||||
|
<p class="text-gray-800 leading-relaxed">{{ result.meaning }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result.example">
|
||||||
|
<h4 class="mb-2 font-semibold text-gray-700">例句:</h4>
|
||||||
|
<p class="bg-gray-50 p-3 rounded text-gray-600 italic">{{ result.example }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading && hasSearched" class="py-12 text-gray-500 text-center">
|
||||||
|
<div class="mb-4 text-6xl">📚</div>
|
||||||
|
<p class="text-xl">未找到相关词汇</p>
|
||||||
|
<p class="mt-2 text-gray-400">请尝试其他关键词</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!hasSearched" class="py-12 text-gray-400 text-center">
|
||||||
|
<div class="mb-4 text-6xl">🔍</div>
|
||||||
|
<p class="text-xl">开始您的法语词汇探索之旅</p>
|
||||||
|
<p class="mt-2">在上方输入框中输入法语单词进行查询</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -52,9 +86,58 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import AppHeader from '../components/AppHeader.vue'
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
import SearchBar from '../components/SearchBar.vue'
|
|
||||||
import AiAssist from '../components/AiAssist.vue'
|
|
||||||
import AppFooter from '../components/AppFooter.vue'
|
import AppFooter from '../components/AppFooter.vue'
|
||||||
|
import { searchWord, type WordDefinition } from '../api/dict'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const selectedLang = ref('fr')
|
||||||
|
const searchResults = ref<WordDefinition[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const hasSearched = ref(false)
|
||||||
|
|
||||||
|
// 从路由参数初始化搜索
|
||||||
|
onMounted(() => {
|
||||||
|
const q = route.query.q as string
|
||||||
|
const lang = route.query.lang as string
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
searchQuery.value = q
|
||||||
|
}
|
||||||
|
if (lang) {
|
||||||
|
selectedLang.value = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有查询参数,自动执行搜索
|
||||||
|
if (q) {
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchQuery.value.trim()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
hasSearched.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await searchWord({
|
||||||
|
lang_pref: selectedLang.value,
|
||||||
|
query_word: searchQuery.value.trim()
|
||||||
|
})
|
||||||
|
searchResults.value = results
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Search error:', err)
|
||||||
|
error.value = err.response?.data?.detail || '搜索失败,请稍后重试'
|
||||||
|
searchResults.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<AppHeader active="dict" />
|
<AppHeader />
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<AiAssist />
|
<AiAssist />
|
||||||
|
|
||||||
<!-- upper -->
|
<!-- upper -->
|
||||||
<section class="bg-[url('/images/french-learning-illustration.png')] bg-no-repeat bg-center pt-[95px]">
|
<section class="bg-[url('/images/french-learning-illustration.png')] bg-no-repeat bg-center pt-[95px]">
|
||||||
<div class="max-w-[1030px] mx-auto px-4">
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
<ul class="font-deserta text-[56px] leading-[76px]">
|
<ul class="font-deserta text-[56px] leading-[76px]">
|
||||||
<li>YOUR ELEGANT</li>
|
<li>YOUR ELEGANT</li>
|
||||||
<li class="text-blue-700">COMPANION</li>
|
<li class="text-blue-700">COMPANION</li>
|
||||||
<li>IN FRENCH LEARNING</li>
|
<li>IN FRENCH LEARNING</li>
|
||||||
</ul>
|
</ul>
|
||||||
<router-link to="/dict" class="font-deserta inline-block mt-[67px] w-[268px] h-[64px] bg-primary bg-blue-700 text-white text-2xl rounded-full text-center leading-[64px]">
|
<router-link to="/dict" class="inline-block bg-blue-700 bg-primary mt-[67px] rounded-full w-[268px] h-[64px] font-deserta text-white text-2xl text-center leading-[64px]">
|
||||||
SEARCH NOW
|
SEARCH NOW
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="mt-[139px]">
|
<div class="mt-[139px]">
|
||||||
<h3 class="text-[40px] font-deserta">INTRODUCTION</h3>
|
<h3 class="font-deserta text-[40px]">INTRODUCTION</h3>
|
||||||
<p class="font-inter mt-[59px] text-xl leading-8">
|
<p class="mt-[59px] font-inter text-xl leading-8">
|
||||||
本站致力于打造一款高质量、实用而优雅的法语词典工具,集词义查询、例句拓展、词根分析于一体,助力学习者深入理解每一个法语单词背后的文化与语境。不止查词,更是探索语言之美的旅程。
|
本站致力于打造一款高质量、实用而优雅的法语词典工具,集词义查询、例句拓展、词根分析于一体,助力学习者深入理解每一个法语单词背后的文化与语境。不止查词,更是探索语言之美的旅程。
|
||||||
</p>
|
</p>
|
||||||
<p class="font-serif mt-4 text-xl leading-8 text-gray-500">
|
<p class="mt-4 font-serif text-gray-500 text-xl leading-8">
|
||||||
A refined and practical French dictionary designed for learners and enthusiasts. Discover accurate definitions, contextual examples, and deep linguistic insights — because learning French is more than memorizing words, it's exploring a world of meaning.
|
A refined and practical French dictionary designed for learners and enthusiasts. Discover accurate definitions, contextual examples, and deep linguistic insights — because learning French is more than memorizing words, it's exploring a world of meaning.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -29,20 +29,20 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- lower -->
|
<!-- lower -->
|
||||||
<section class="bg-[url('/images/lower.png')] bg-contain bg-no-repeat h-[437px] w-full">
|
<section class="bg-[url('/images/lower.png')] bg-contain bg-no-repeat w-full h-[437px]">
|
||||||
<div class="max-w-[1030px] mx-auto flex items-center justify-between h-full">
|
<div class="flex justify-between items-center mx-auto max-w-[1030px] h-full">
|
||||||
<ul class="font-deserta text-[35px] leading-[53px]">
|
<ul class="font-deserta text-[35px] leading-[53px]">
|
||||||
<li>CUMULATIVE <span class="text-primary">WORD</span></li>
|
<li>CUMULATIVE <span class="text-primary">WORD</span></li>
|
||||||
<li>LOOKUP COUNT</li>
|
<li>LOOKUP COUNT</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-muted mb-3 text-lg">累计</p>
|
<p class="mb-3 text-muted text-lg">累计</p>
|
||||||
<div class="flex items-end gap-2">
|
<div class="flex items-end gap-2">
|
||||||
<span v-for="i in 11" :key="i" class="w-[34px] h-[60px] rounded-[17px] bg-gradient-to-b from-white to-[#d5e2f8] border border-transparent flex items-center justify-center text-2xl text-muted shadow">
|
<span v-for="i in 11" :key="i" class="flex justify-center items-center bg-gradient-to-b from-white to-[#d5e2f8] shadow border border-transparent rounded-[17px] w-[34px] h-[60px] text-muted text-2xl">
|
||||||
0
|
0
|
||||||
</span>
|
</span>
|
||||||
<span class="font-deserta text-primary text-[35px] ml-2">TIMES</span>
|
<span class="ml-2 font-deserta text-[35px] text-primary">TIMES</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,229 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<header class="bg-white h-[174px] flex items-center">
|
<header class="flex items-center bg-white h-[174px]">
|
||||||
<div class="max-w-[1030px] mx-auto">
|
<div class="mx-auto max-w-[1030px]">
|
||||||
<router-link to="/" class="block w-[237px] h-[61px] bg-[url('/images/LOGO.png')] bg-contain bg-no-repeat pl-[100px] leading-[31px] font-deserta text-2xl">
|
<router-link to="/" class="block bg-[url('/images/LOGO.png')] bg-contain bg-no-repeat pl-[100px] w-[237px] h-[61px] font-deserta text-2xl leading-[31px]">
|
||||||
CiDian
|
CiDian
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="relative overflow-visible">
|
<main class="relative overflow-visible">
|
||||||
<div class="max-w-[1030px] mx-auto pt-10">
|
<div class="mx-auto pt-10 max-w-[1030px]">
|
||||||
<ul class="flex flex-col items-end font-deserta text-5xl leading-[76px]">
|
<ul class="flex flex-col items-end font-deserta text-5xl leading-[76px]">
|
||||||
<li class="font-inter text-lg text-muted">BONJOUR</li>
|
<li class="font-inter text-gray-500 text-lg">BONJOUR</li>
|
||||||
<li class="text-primary">WELCOME BACK</li>
|
<li class="text-blue-700">{{ isLogin ? 'WELCOME BACK' : 'JOIN US TODAY' }}</li>
|
||||||
<li>LET'S GET STARTED</li>
|
<li>LET'S GET STARTED</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form class="mt-[77px] max-w-[401px]">
|
<!-- 切换按钮 -->
|
||||||
<label class="block mb-6 text-muted text-xl">手机号</label>
|
<div class="flex bg-gray-100 mt-8 p-1 rounded-full w-fit">
|
||||||
<input type="text" class="w-full h-[63px] border border-primary rounded-full px-8 text-2xl outline-none" />
|
<button
|
||||||
|
@click="isLogin = true"
|
||||||
|
:class="[
|
||||||
|
'px-6 py-2 rounded-full transition-all font-inter',
|
||||||
|
isLogin ? 'bg-blue-700 text-white' : 'text-gray-600 hover:text-blue-700'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="isLogin = false"
|
||||||
|
:class="[
|
||||||
|
'px-6 py-2 rounded-full transition-all font-inter',
|
||||||
|
!isLogin ? 'bg-blue-700 text-white' : 'text-gray-600 hover:text-blue-700'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="block mt-8 mb-6 text-muted text-xl">密码</label>
|
<form @submit.prevent="isLogin ? handleLogin() : handleRegister()" class="mt-[77px] max-w-[401px]">
|
||||||
<input type="password" class="w-full h-[63px] border border-primary rounded-full px-8 text-2xl outline-none" />
|
<div v-if="error" class="bg-red-100 mb-4 p-3 border border-red-400 rounded text-red-700">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div v-if="successMessage" class="bg-green-100 mb-4 p-3 border border-green-400 rounded text-green-700">
|
||||||
|
{{ successMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="mt-[72px] w-[268px] h-[63px] bg-primary text-white text-2xl rounded-full">
|
<label class="block mb-6 text-gray-500 text-xl">用户名</label>
|
||||||
LOG IN
|
<input
|
||||||
|
:value="isLogin ? loginForm.name : registerForm.username"
|
||||||
|
@input="handleUsernameInput"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="block mt-8 mb-6 text-gray-500 text-xl">密码</label>
|
||||||
|
<input
|
||||||
|
:value="isLogin ? loginForm.password : registerForm.password"
|
||||||
|
@input="handlePasswordInput"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 注册时显示确认密码 -->
|
||||||
|
<template v-if="!isLogin">
|
||||||
|
<label class="block mt-8 mb-6 text-gray-500 text-xl">确认密码</label>
|
||||||
|
<input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
class="px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="block mt-8 mb-6 text-gray-500 text-xl">语言偏好</label>
|
||||||
|
<select
|
||||||
|
v-model="registerForm.lang_pref"
|
||||||
|
class="bg-white px-8 border border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[63px] text-2xl"
|
||||||
|
>
|
||||||
|
<option value="fr">法语</option>
|
||||||
|
<option value="jp">日语</option>
|
||||||
|
<option value="private">私人</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 mt-[72px] rounded-full w-[268px] h-[63px] text-white text-2xl disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ loading ? (isLogin ? 'LOGGING IN...' : 'REGISTERING...') : (isLogin ? 'LOG IN' : 'REGISTER') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 背景图(伪元素) -->
|
<!-- 背景图 -->
|
||||||
<div class="absolute top-4 -right-[200px] w-[1200px] h-[800px] bg-[url('/images/8ba41f...png')] bg-no-repeat bg-auto bg-[position:-100px_0] -z-10" />
|
<div class="top-4 -right-[200px] -z-10 absolute bg-[position:-100px_0] bg-[url('/images/9d1ecd03bf3f17a66e03547f54973cf5a254ac10a6b97a5bfb60098680db7160.png')] bg-auto bg-no-repeat w-[1200px] h-[800px]" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="h-[465px] bg-gradient-to-t from-[#4284e8] to-transparent" />
|
<footer class="bg-gradient-to-t from-[#4284e8] to-transparent h-[465px]" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuth } from '../composables/useAuth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { login, register } = useAuth()
|
||||||
|
|
||||||
|
const isLogin = ref(true)
|
||||||
|
|
||||||
|
const loginForm = ref({
|
||||||
|
name: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const registerForm = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
lang_pref: 'fr' as 'jp' | 'fr' | 'private'
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const successMessage = ref('')
|
||||||
|
|
||||||
|
// 处理用户名输入
|
||||||
|
const handleUsernameInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (isLogin.value) {
|
||||||
|
loginForm.value.name = target.value
|
||||||
|
} else {
|
||||||
|
registerForm.value.username = target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理密码输入
|
||||||
|
const handlePasswordInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (isLogin.value) {
|
||||||
|
loginForm.value.password = target.value
|
||||||
|
} else {
|
||||||
|
registerForm.value.password = target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginForm.value.name || !loginForm.value.password) {
|
||||||
|
error.value = '请填写用户名和密码'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(loginForm.value)
|
||||||
|
router.push('/dict')
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Login error:', err)
|
||||||
|
error.value = err.response?.data?.detail || '登录失败,请检查用户名和密码'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
// 验证表单
|
||||||
|
if (!registerForm.value.username || !registerForm.value.password) {
|
||||||
|
error.value = '请填写所有必填字段'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registerForm.value.password !== confirmPassword.value) {
|
||||||
|
error.value = '两次输入的密码不一致'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registerForm.value.password.length < 6) {
|
||||||
|
error.value = '密码长度至少6位'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
successMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const username = registerForm.value.username // 保存用户名
|
||||||
|
await register(registerForm.value)
|
||||||
|
successMessage.value = '注册成功!请登录'
|
||||||
|
|
||||||
|
// 将用户名填入登录表单
|
||||||
|
loginForm.value.name = username
|
||||||
|
|
||||||
|
// 切换到登录模式并清空注册表单
|
||||||
|
isLogin.value = true
|
||||||
|
registerForm.value = {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
lang_pref: 'fr'
|
||||||
|
}
|
||||||
|
confirmPassword.value = ''
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Register error:', err)
|
||||||
|
error.value = err.response?.data?.detail || '注册失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换模式时清空错误和成功信息
|
||||||
|
const clearMessages = () => {
|
||||||
|
error.value = ''
|
||||||
|
successMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听模式切换
|
||||||
|
import { watch } from 'vue'
|
||||||
|
watch(isLogin, () => {
|
||||||
|
clearMessages()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppHeader />
|
||||||
|
|
||||||
|
<!-- 翻译区域 -->
|
||||||
|
<section class="bg-white py-12">
|
||||||
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="mb-4 font-deserta text-blue-700 text-4xl">法语翻译</h1>
|
||||||
|
<p class="font-inter text-gray-600">中法双向翻译,助力语言学习</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-[900px]">
|
||||||
|
<div class="gap-6 grid grid-cols-1 md:grid-cols-2">
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="font-semibold text-gray-700">源语言</h3>
|
||||||
|
<select v-model="sourceLang" class="px-3 py-1 border border-gray-300 rounded">
|
||||||
|
<option value="zh">中文</option>
|
||||||
|
<option value="fr">法语</option>
|
||||||
|
<option value="en">英语</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="sourceText"
|
||||||
|
placeholder="请输入要翻译的文本..."
|
||||||
|
class="p-4 border border-gray-300 focus:border-blue-500 rounded-lg outline-none w-full h-[200px] resize-none"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输出区域 -->
|
||||||
|
<div class="bg-blue-50 p-6 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="font-semibold text-gray-700">目标语言</h3>
|
||||||
|
<select v-model="targetLang" class="px-3 py-1 border border-gray-300 rounded">
|
||||||
|
<option value="zh">中文</option>
|
||||||
|
<option value="fr">法语</option>
|
||||||
|
<option value="en">英语</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 border border-gray-300 rounded-lg w-full h-[200px] overflow-y-auto">
|
||||||
|
<div v-if="loading" class="flex justify-center items-center h-full text-gray-500">
|
||||||
|
<div class="border-b-2 border-blue-700 rounded-full w-8 h-8 animate-spin"></div>
|
||||||
|
<span class="ml-2">翻译中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="translationResult" class="text-gray-800 leading-relaxed">
|
||||||
|
{{ translationResult }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 italic">
|
||||||
|
翻译结果将在这里显示
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex justify-center space-x-4 mt-6">
|
||||||
|
<button
|
||||||
|
@click="handleTranslate"
|
||||||
|
:disabled="loading || !sourceText.trim()"
|
||||||
|
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 px-8 py-3 rounded-full font-inter text-white disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ loading ? '翻译中...' : '翻译' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="swapLanguages"
|
||||||
|
class="hover:bg-blue-50 px-8 py-3 border border-blue-700 rounded-full font-inter text-blue-700"
|
||||||
|
>
|
||||||
|
⇄ 交换语言
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="clearText"
|
||||||
|
class="hover:bg-gray-50 px-8 py-3 border border-gray-300 rounded-full font-inter text-gray-700"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="bg-red-100 mt-4 px-4 py-3 border border-red-400 rounded text-red-700">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 翻译历史 -->
|
||||||
|
<section class="bg-gray-50 py-12">
|
||||||
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
|
<h2 class="mb-6 font-deserta text-blue-700 text-2xl">翻译历史</h2>
|
||||||
|
|
||||||
|
<div v-if="translationHistory.length > 0" class="space-y-4">
|
||||||
|
<div v-for="(item, index) in translationHistory" :key="index" class="bg-white shadow-sm p-4 rounded-lg">
|
||||||
|
<div class="gap-4 grid grid-cols-1 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-gray-500 text-sm">{{ item.sourceLang === 'zh' ? '中文' : item.sourceLang === 'fr' ? '法语' : '英语' }}</p>
|
||||||
|
<p class="text-gray-800">{{ item.sourceText }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-1 text-gray-500 text-sm">{{ item.targetLang === 'zh' ? '中文' : item.targetLang === 'fr' ? '法语' : '英语' }}</p>
|
||||||
|
<p class="text-gray-800">{{ item.translationResult }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-gray-400 text-xs">{{ item.timestamp }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="py-12 text-gray-400 text-center">
|
||||||
|
<div class="mb-4 text-6xl">📝</div>
|
||||||
|
<p class="text-xl">暂无翻译历史</p>
|
||||||
|
<p class="mt-2">开始翻译以建立您的历史记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
|
import AppFooter from '../components/AppFooter.vue'
|
||||||
|
|
||||||
|
interface TranslationHistoryItem {
|
||||||
|
sourceText: string
|
||||||
|
translationResult: string
|
||||||
|
sourceLang: string
|
||||||
|
targetLang: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceText = ref('')
|
||||||
|
const translationResult = ref('')
|
||||||
|
const sourceLang = ref('zh')
|
||||||
|
const targetLang = ref('fr')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const translationHistory = ref<TranslationHistoryItem[]>([])
|
||||||
|
|
||||||
|
// 模拟翻译API调用
|
||||||
|
const handleTranslate = async () => {
|
||||||
|
if (!sourceText.value.trim()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里应该调用真实的翻译API
|
||||||
|
// 现在使用模拟翻译
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 简单的模拟翻译逻辑
|
||||||
|
let result = ''
|
||||||
|
if (sourceLang.value === 'zh' && targetLang.value === 'fr') {
|
||||||
|
result = `[法语翻译] ${sourceText.value}`
|
||||||
|
} else if (sourceLang.value === 'fr' && targetLang.value === 'zh') {
|
||||||
|
result = `[中文翻译] ${sourceText.value}`
|
||||||
|
} else {
|
||||||
|
result = `[翻译结果] ${sourceText.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
translationResult.value = result
|
||||||
|
|
||||||
|
// 添加到历史记录
|
||||||
|
const historyItem: TranslationHistoryItem = {
|
||||||
|
sourceText: sourceText.value,
|
||||||
|
translationResult: result,
|
||||||
|
sourceLang: sourceLang.value,
|
||||||
|
targetLang: targetLang.value,
|
||||||
|
timestamp: new Date().toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
translationHistory.value.unshift(historyItem)
|
||||||
|
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('translationHistory', JSON.stringify(translationHistory.value.slice(0, 10))) // 只保存最近10条
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Translation error:', err)
|
||||||
|
error.value = '翻译失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const swapLanguages = () => {
|
||||||
|
const temp = sourceLang.value
|
||||||
|
sourceLang.value = targetLang.value
|
||||||
|
targetLang.value = temp
|
||||||
|
|
||||||
|
// 交换文本内容
|
||||||
|
if (translationResult.value) {
|
||||||
|
const tempText = sourceText.value
|
||||||
|
sourceText.value = translationResult.value
|
||||||
|
translationResult.value = tempText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearText = () => {
|
||||||
|
sourceText.value = ''
|
||||||
|
translationResult.value = ''
|
||||||
|
error.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从localStorage加载翻译历史
|
||||||
|
onMounted(() => {
|
||||||
|
const saved = localStorage.getItem('translationHistory')
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
translationHistory.value = JSON.parse(saved)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load translation history:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppHeader />
|
||||||
|
|
||||||
|
<!-- 写作指导主区域 -->
|
||||||
|
<section class="bg-white py-12">
|
||||||
|
<div class="mx-auto px-4 max-w-[1030px]">
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<h1 class="mb-4 font-deserta text-blue-700 text-4xl">法语写作指导</h1>
|
||||||
|
<p class="font-inter text-gray-600">AI助力法语写作,提升表达水平</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-[900px]">
|
||||||
|
<!-- 写作类型选择 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="mb-3 font-semibold text-gray-700">写作类型</h3>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
v-for="type in writingTypes"
|
||||||
|
:key="type.value"
|
||||||
|
@click="selectedType = type.value"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-full border transition-colors',
|
||||||
|
selectedType === type.value
|
||||||
|
? 'bg-blue-700 text-white border-blue-700'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:border-blue-500'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ type.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 写作主题输入 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="mb-3 font-semibold text-gray-700">写作主题或要求</h3>
|
||||||
|
<input
|
||||||
|
v-model="writingTopic"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入写作主题,如:我的假期、环境保护、法国文化等..."
|
||||||
|
class="px-4 border border-gray-300 focus:border-blue-500 rounded-lg outline-none w-full h-[50px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 写作内容区域 -->
|
||||||
|
<div class="gap-6 grid grid-cols-1 lg:grid-cols-2 mb-6">
|
||||||
|
<!-- 写作区域 -->
|
||||||
|
<div class="bg-gray-50 p-6 rounded-lg">
|
||||||
|
<h3 class="mb-4 font-semibold text-gray-700">您的法语作文</h3>
|
||||||
|
<textarea
|
||||||
|
v-model="userText"
|
||||||
|
placeholder="请在这里写下您的法语作文..."
|
||||||
|
class="p-4 border border-gray-300 focus:border-blue-500 rounded-lg outline-none w-full h-[300px] resize-none"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex justify-between items-center mt-2">
|
||||||
|
<span class="text-gray-500 text-sm">字数: {{ userText.length }}</span>
|
||||||
|
<button
|
||||||
|
@click="getWritingHelp"
|
||||||
|
:disabled="loading || !userText.trim()"
|
||||||
|
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 px-4 py-2 rounded text-white disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ loading ? '分析中...' : '获取指导' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 指导建议区域 -->
|
||||||
|
<div class="bg-blue-50 p-6 rounded-lg">
|
||||||
|
<h3 class="mb-4 font-semibold text-gray-700">AI写作指导</h3>
|
||||||
|
<div class="h-[300px] overflow-y-auto">
|
||||||
|
<div v-if="loading" class="flex justify-center items-center h-full text-gray-500">
|
||||||
|
<div class="border-b-2 border-blue-700 rounded-full w-8 h-8 animate-spin"></div>
|
||||||
|
<span class="ml-2">AI正在分析您的作文...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="writingFeedback" class="space-y-4">
|
||||||
|
<div v-for="(section, key) in writingFeedback" :key="key" class="bg-white p-4 rounded">
|
||||||
|
<h4 class="mb-2 font-semibold text-blue-700">{{ getSectionTitle(key) }}</h4>
|
||||||
|
<div v-if="Array.isArray(section)">
|
||||||
|
<ul class="space-y-1 list-disc list-inside">
|
||||||
|
<li v-for="(item, index) in section" :key="index" class="text-gray-700 text-sm">
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-700 text-sm">
|
||||||
|
{{ section }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex justify-center items-center h-full text-gray-400 italic">
|
||||||
|
AI写作指导将在这里显示
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div v-if="error" class="bg-red-100 mb-6 px-4 py-3 border border-red-400 rounded text-red-700">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速写作模板 -->
|
||||||
|
<div class="bg-white p-6 border border-gray-200 rounded-lg">
|
||||||
|
<h3 class="mb-4 font-semibold text-gray-700">写作模板参考</h3>
|
||||||
|
<div class="gap-4 grid grid-cols-1 md:grid-cols-2">
|
||||||
|
<div v-for="template in writingTemplates" :key="template.type" class="p-4 border border-gray-200 rounded">
|
||||||
|
<h4 class="mb-2 font-semibold text-blue-700">{{ template.title }}</h4>
|
||||||
|
<p class="mb-3 text-gray-600 text-sm">{{ template.description }}</p>
|
||||||
|
<button
|
||||||
|
@click="useTemplate(template.template)"
|
||||||
|
class="text-blue-600 text-sm hover:underline"
|
||||||
|
>
|
||||||
|
使用此模板
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import AppHeader from '../components/AppHeader.vue'
|
||||||
|
import AppFooter from '../components/AppFooter.vue'
|
||||||
|
|
||||||
|
const writingTypes = [
|
||||||
|
{ value: 'essay', label: '议论文' },
|
||||||
|
{ value: 'narrative', label: '记叙文' },
|
||||||
|
{ value: 'letter', label: '书信' },
|
||||||
|
{ value: 'diary', label: '日记' },
|
||||||
|
{ value: 'description', label: '描述文' },
|
||||||
|
{ value: 'report', label: '报告' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const writingTemplates = [
|
||||||
|
{
|
||||||
|
type: 'essay',
|
||||||
|
title: '议论文模板',
|
||||||
|
description: '适用于表达观点和论证',
|
||||||
|
template: `Introduction:\nDe nos jours, [主题] est un sujet qui préoccupe beaucoup de gens...\n\nDéveloppement:\nD'une part, ...\nD'autre part, ...\n\nConclusion:\nEn conclusion, je pense que...`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'letter',
|
||||||
|
title: '正式书信模板',
|
||||||
|
description: '适用于正式场合的书信写作',
|
||||||
|
template: `[Lieu], le [date]\n\nMonsieur/Madame,\n\nJ'ai l'honneur de vous écrire pour...\n\nJe vous prie d'agréer, Monsieur/Madame, l'expression de mes salutations distinguées.\n\n[Signature]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'narrative',
|
||||||
|
title: '记叙文模板',
|
||||||
|
description: '适用于叙述事件和经历',
|
||||||
|
template: `Il était une fois... / Un jour...\n\nTout d'abord...\nEnsuite...\nEnfin...\n\nCette expérience m'a appris que...`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'description',
|
||||||
|
title: '描述文模板',
|
||||||
|
description: '适用于描述人物、地点或事物',
|
||||||
|
template: `[Objet de description] se trouve/est...\n\nPhysiquement, il/elle...\nDu point de vue du caractère...\n\nEn conclusion, [objet] est vraiment...`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedType = ref('essay')
|
||||||
|
const writingTopic = ref('')
|
||||||
|
const userText = ref('')
|
||||||
|
const writingFeedback = ref<any>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const getSectionTitle = (key: string) => {
|
||||||
|
const titles: Record<string, string> = {
|
||||||
|
grammar: '语法建议',
|
||||||
|
vocabulary: '词汇改进',
|
||||||
|
structure: '结构优化',
|
||||||
|
style: '文体建议',
|
||||||
|
overall: '总体评价'
|
||||||
|
}
|
||||||
|
return titles[key] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWritingHelp = async () => {
|
||||||
|
if (!userText.value.trim()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟AI分析过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
// 模拟AI反馈
|
||||||
|
writingFeedback.value = {
|
||||||
|
overall: `您的作文整体结构清晰,主题明确。建议在以下方面进行改进:`,
|
||||||
|
grammar: [
|
||||||
|
'注意动词变位的准确性',
|
||||||
|
'检查形容词与名词的性数一致',
|
||||||
|
'确保时态使用的连贯性'
|
||||||
|
],
|
||||||
|
vocabulary: [
|
||||||
|
'可以使用更多高级词汇来丰富表达',
|
||||||
|
'避免重复使用相同的词汇',
|
||||||
|
'尝试使用同义词增加文章的多样性'
|
||||||
|
],
|
||||||
|
structure: [
|
||||||
|
'段落之间的过渡可以更加自然',
|
||||||
|
'可以增加更多的连接词',
|
||||||
|
'结论部分可以更加有力'
|
||||||
|
],
|
||||||
|
style: [
|
||||||
|
'保持正式的写作风格',
|
||||||
|
'句子长度可以适当变化',
|
||||||
|
'增加一些修辞手法会更好'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Writing help error:', err)
|
||||||
|
error.value = '获取写作指导失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTemplate = (template: string) => {
|
||||||
|
if (userText.value && !confirm('使用模板将替换当前内容,确定继续吗?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userText.value = template
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@echo off
|
||||||
|
echo Starting backend server...
|
||||||
|
cd backend/dict-server
|
||||||
|
python main.py
|
||||||
|
pause
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
@echo off
|
||||||
|
echo Starting frontend development server...
|
||||||
|
npm run dev
|
||||||
|
pause
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#2563eb', // blue-700
|
||||||
|
muted: '#6b7280', // gray-500
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
'deserta': ['Deserta', 'serif'],
|
||||||
|
'inter': ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue