layout v0.0.1
This commit is contained in:
parent
f7a295ba08
commit
a03c5229f3
13 changed files with 415 additions and 87 deletions
8
db_scripts/test.py
Normal file
8
db_scripts/test.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
backup_dir = os.getenv('BACKUP_DIR', '/default/backup/path')
|
||||||
|
print(backup_dir)
|
||||||
|
|
@ -5,19 +5,30 @@ from bson.objectid import ObjectId
|
||||||
from bson.json_util import dumps
|
from bson.json_util import dumps
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from bson import errors as bson_errors
|
from bson import errors as bson_errors
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
import jwt
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables before initializing app
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
cors = CORS(app, resources={
|
cors = CORS(app, resources={
|
||||||
r"/api/*": {
|
r"/api/*": {
|
||||||
"origins": ["http://localhost:3000", "http://localhost:3001"],
|
"origins": ["http://localhost:3000", "http://localhost:3001"],
|
||||||
"methods": ["GET", "POST", "DELETE", "OPTIONS"],
|
"methods": ["GET", "POST", "DELETE", "OPTIONS"],
|
||||||
"allow_headers": ["Content-Type"]
|
"allow_headers": ["Content-Type", "Authorization"] # Allow Authorization header
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.config['CORS_HEADERS'] = 'Content-Type'
|
app.config['CORS_HEADERS'] = 'Content-Type'
|
||||||
|
|
||||||
|
# Load JWT secret key from environment variable
|
||||||
|
SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'default_secret_key')
|
||||||
|
TOKEN_LV1 = os.getenv('JWT_TOKEN_LV1', 'default_token_lv1')
|
||||||
|
TOKEN_LV2 = os.getenv('JWT_TOKEN_LV2', 'default_token_lv2')
|
||||||
|
|
||||||
client = MongoClient('localhost', 27017)
|
client = MongoClient('localhost', 27017)
|
||||||
|
|
||||||
db = client.flask_db
|
db = client.flask_db
|
||||||
|
|
@ -26,6 +37,20 @@ todos = db.todos
|
||||||
# 确保UUID字段的唯一性
|
# 确保UUID字段的唯一性
|
||||||
todos.create_index('uuid', unique=True)
|
todos.create_index('uuid', unique=True)
|
||||||
|
|
||||||
|
def check_permission(token, required_level):
|
||||||
|
print(f"SECRET: {SECRET_KEY}")
|
||||||
|
print(f"TOKEN1: {TOKEN_LV1}")
|
||||||
|
print(f"TOKEN2: {TOKEN_LV2}")
|
||||||
|
print(f"TOKENIN: {token}")
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
|
||||||
|
print(f"PAYLOAD: {payload}")
|
||||||
|
return payload.get('level', 0) >= required_level
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return False
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return False
|
||||||
|
|
||||||
@app.route('/api/todos', methods=['GET'])
|
@app.route('/api/todos', methods=['GET'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def get_todos():
|
def get_todos():
|
||||||
|
|
@ -35,6 +60,10 @@ def get_todos():
|
||||||
@app.route('/api/todos', methods=['POST'])
|
@app.route('/api/todos', methods=['POST'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def create_todo():
|
def create_todo():
|
||||||
|
token = request.headers.get('Authorization', '').split(' ')[-1]
|
||||||
|
if not check_permission(token, 1):
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
content = request.json.get('content')
|
content = request.json.get('content')
|
||||||
degree = request.json.get('degree')
|
degree = request.json.get('degree')
|
||||||
timestamp = datetime.now()
|
timestamp = datetime.now()
|
||||||
|
|
@ -50,6 +79,10 @@ def create_todo():
|
||||||
@app.route('/api/todos/<uuid>', methods=['DELETE'])
|
@app.route('/api/todos/<uuid>', methods=['DELETE'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def delete_todo(uuid):
|
def delete_todo(uuid):
|
||||||
|
token = request.headers.get('Authorization', '').split(' ')[-1]
|
||||||
|
if not check_permission(token, 2):
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
result = todos.delete_one({"uuid": uuid})
|
result = todos.delete_one({"uuid": uuid})
|
||||||
if result.deleted_count > 0:
|
if result.deleted_count > 0:
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
|
||||||
5
src/backend/initialize_tools/jwt_secret_gen.py
Normal file
5
src/backend/initialize_tools/jwt_secret_gen.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
# Generate a random secret key for JWT
|
||||||
|
jwt_secret_key = secrets.token_urlsafe(32)
|
||||||
|
print(jwt_secret_key)
|
||||||
35
src/backend/initialize_tools/jwt_token_gen.py
Normal file
35
src/backend/initialize_tools/jwt_token_gen.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import jwt
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# 加载密钥
|
||||||
|
jwt_secret_key = os.getenv('JWT_SECRET_KEY', '')
|
||||||
|
|
||||||
|
# 生成 JWT 的函数
|
||||||
|
def generate_token(level):
|
||||||
|
payload = {
|
||||||
|
'level': level, # 权限等级
|
||||||
|
'exp': datetime.utcnow() + timedelta(days=365 * 10 + 3) # 令牌过期时间
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, jwt_secret_key, algorithm='HS256')
|
||||||
|
return token
|
||||||
|
|
||||||
|
def generate_static_token():
|
||||||
|
payload = {
|
||||||
|
'level': 1, # 权限等级
|
||||||
|
'exp': 1024 # 令牌过期时间
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, jwt_secret_key, algorithm='HS256')
|
||||||
|
return token
|
||||||
|
|
||||||
|
# 生成不同权限等级的令牌
|
||||||
|
token_level_1 = generate_token(1)
|
||||||
|
token_level_2 = generate_token(2)
|
||||||
|
|
||||||
|
print("Token for level 1:", token_level_1)
|
||||||
|
print("Token for level 2:", token_level_2)
|
||||||
|
print("Static Token:", generate_static_token())
|
||||||
43
src/frontend/package-lock.json
generated
43
src/frontend/package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"framer-motion": "^12.4.0",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
|
|
@ -872,6 +873,33 @@
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.0.tgz",
|
||||||
|
"integrity": "sha512-QX92ThRniev3YTkjXOV/7m9fXBRQ5xDDRWDinuU//Xkjh+q9ppg3Nb0b95xgJYd2AE0rgJ7eoihY+Z+jjSv22w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.0.0",
|
||||||
|
"motion-utils": "^12.0.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
|
|
@ -1177,6 +1205,21 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"framer-motion": "^12.4.0",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0"
|
||||||
"axios": "^1.6.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/react": "^18.2.45",
|
"@types/react": "^18.2.45",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"tailwindcss": "^3.3.6"
|
"tailwindcss": "^3.3.6",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
src/frontend/public/todo-icon.png
Normal file
BIN
src/frontend/public/todo-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5 KiB |
100
src/frontend/src/app/components/TodoForm.tsx
Normal file
100
src/frontend/src/app/components/TodoForm.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { TodoAPI } from '../utils/api';
|
||||||
|
|
||||||
|
type TodoFormProps = {
|
||||||
|
onTodoAdded: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TodoForm({ onTodoAdded }: TodoFormProps) {
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await TodoAPI.create({ content, degree });
|
||||||
|
setContent('');
|
||||||
|
setDegree('Important');
|
||||||
|
onTodoAdded();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating todo:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.form
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="bg-white rounded-xl shadow-lg p-6 mb-8 space-y-6"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="content" className="block text-gray-700 font-semibold mb-2">
|
||||||
|
待办事项内容
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="输入待办事项..."
|
||||||
|
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-gray-700 font-semibold">重要程度</p>
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<label className="relative flex items-center cursor-pointer hover:opacity-80 transition-opacity">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="degree"
|
||||||
|
value="Important"
|
||||||
|
checked={degree === 'Important'}
|
||||||
|
onChange={(e) => setDegree(e.target.value as 'Important')}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div className={`w-5 h-5 border-2 rounded-full mr-3 ${degree === 'Important' ? 'border-red-500 bg-red-500' : 'border-gray-300'}`}>
|
||||||
|
{degree === 'Important' && (
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">重要</span>
|
||||||
|
</label>
|
||||||
|
<label className="relative flex items-center cursor-pointer hover:opacity-80 transition-opacity">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="degree"
|
||||||
|
value="Unimportant"
|
||||||
|
checked={degree === 'Unimportant'}
|
||||||
|
onChange={(e) => setDegree(e.target.value as 'Unimportant')}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div className={`w-5 h-5 border-2 rounded-full mr-3 ${degree === 'Unimportant' ? 'border-gray-500 bg-gray-500' : 'border-gray-300'}`}>
|
||||||
|
{degree === 'Unimportant' && (
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700">普通</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
添加待办事项
|
||||||
|
</motion.button>
|
||||||
|
</motion.form>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/frontend/src/app/components/TodoItem.tsx
Normal file
55
src/frontend/src/app/components/TodoItem.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Todo } from '../utils/api';
|
||||||
|
|
||||||
|
type TodoItemProps = {
|
||||||
|
todo: Todo;
|
||||||
|
onDelete: (uuid: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TodoItem({ todo, onDelete }: TodoItemProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="bg-white rounded-xl shadow-sm hover:shadow-md p-6 flex justify-between items-start transition-all duration-200 border border-gray-100"
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-lg font-semibold text-gray-800 leading-relaxed">{todo.content}</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-3 py-1 text-sm rounded-full font-medium transition-colors ${todo.degree === 'Important' ? 'bg-red-50 text-red-600 ring-1 ring-red-500/10' : 'bg-gray-50 text-gray-600 ring-1 ring-gray-500/10'}`}
|
||||||
|
>
|
||||||
|
{todo.degree === 'Important' ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20" width="12" height="12">
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||||
|
</svg>
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20" width="12" height="12">
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||||
|
</svg>
|
||||||
|
重要
|
||||||
|
</>
|
||||||
|
) : '普通'}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{new Date(todo.timestamp.$date).toLocaleString('zh-CN')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => onDelete(todo.uuid)}
|
||||||
|
className="text-gray-400 hover:text-red-500 p-2 rounded-lg hover:bg-red-50/50 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/frontend/src/app/components/TodoList.tsx
Normal file
45
src/frontend/src/app/components/TodoList.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Todo } from '../utils/api';
|
||||||
|
import TodoItem from '../components/TodoItem';
|
||||||
|
|
||||||
|
type TodoListProps = {
|
||||||
|
todos: Todo[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onDelete: (uuid: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TodoList({ todos, isLoading, onDelete }: TodoListProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12 bg-white/50 backdrop-blur-sm rounded-xl shadow-sm">
|
||||||
|
<div className="animate-spin rounded-full h-14 w-14 border-4 border-blue-100 border-b-blue-600 mx-auto"></div>
|
||||||
|
<p className="mt-6 text-gray-600 font-medium">加载中...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todos.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-16 bg-white/50 backdrop-blur-sm rounded-xl shadow-sm">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-lg font-medium">暂无待办事项</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<AnimatePresence>
|
||||||
|
{todos.map((todo) => (
|
||||||
|
<TodoItem key={todo.uuid} todo={todo} onDelete={onDelete} />
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import { TodoAPI, Todo } from './utils/api';
|
||||||
|
import Image from 'next/image';
|
||||||
interface Todo {
|
import TodoForm from './components/TodoForm';
|
||||||
uuid: string;
|
import TodoList from './components/TodoList';
|
||||||
content: string;
|
|
||||||
degree: 'Important' | 'Unimportant';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [todos, setTodos] = useState<Todo[]>([]);
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
|
const [degree, setDegree] = useState<'Important' | 'Unimportant'>('Important');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTodos();
|
fetchTodos();
|
||||||
|
|
@ -20,17 +18,22 @@ export default function Home() {
|
||||||
|
|
||||||
const fetchTodos = async () => {
|
const fetchTodos = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('http://localhost:5000/api/todos');
|
setIsLoading(true);
|
||||||
setTodos(response.data);
|
const data = await TodoAPI.getAll();
|
||||||
|
setTodos(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching todos:', error);
|
console.error('Error fetching todos:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post('http://localhost:5000/api/todos', { content, degree });
|
await TodoAPI.create({ content, degree });
|
||||||
setContent('');
|
setContent('');
|
||||||
setDegree('Important');
|
setDegree('Important');
|
||||||
fetchTodos();
|
fetchTodos();
|
||||||
|
|
@ -40,9 +43,9 @@ export default function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (uuid: string) => {
|
const handleDelete = async (uuid: string) => {
|
||||||
if (!confirm('Are you sure you want to delete this entry?')) return;
|
if (!confirm('确定要删除这条待办事项吗?')) return;
|
||||||
try {
|
try {
|
||||||
await axios.delete(`http://localhost:5000/api/todos/${uuid}`);
|
await TodoAPI.delete(uuid);
|
||||||
fetchTodos();
|
fetchTodos();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting todo:', error);
|
console.error('Error deleting todo:', error);
|
||||||
|
|
@ -50,78 +53,19 @@ export default function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="max-w-4xl mx-auto p-4">
|
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-8 px-4 sm:px-6 lg:px-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">FlaskTODO</h1>
|
<div className="max-w-4xl mx-auto">
|
||||||
<hr className="mb-6" />
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4 flex items-center justify-center gap-2">
|
||||||
<form onSubmit={handleSubmit} className="mb-8 space-y-4">
|
<Image src="/todo-icon.png" alt="Todo Icon" width={40} height={40} className="rounded-lg" />
|
||||||
<div>
|
FlaskTODO
|
||||||
<label htmlFor="content" className="block font-bold mb-2">
|
</h1>
|
||||||
Todo content
|
<p className="text-lg text-gray-600">管理你的待办事项</p>
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="content"
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
placeholder="Todo Content"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<TodoForm onTodoAdded={fetchTodos} />
|
||||||
<p className="font-bold">Degree</p>
|
|
||||||
<div className="space-x-4">
|
|
||||||
<label className="inline-flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="degree"
|
|
||||||
value="Important"
|
|
||||||
checked={degree === 'Important'}
|
|
||||||
onChange={(e) => setDegree(e.target.value as 'Important')}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Important
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="degree"
|
|
||||||
value="Unimportant"
|
|
||||||
checked={degree === 'Unimportant'}
|
|
||||||
onChange={(e) => setDegree(e.target.value as 'Unimportant')}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
Unimportant
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<TodoList todos={todos} isLoading={isLoading} onDelete={handleDelete} />
|
||||||
type="submit"
|
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr className="mb-6" />
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{todos.map((todo) => (
|
|
||||||
<div key={todo.uuid} className="bg-gray-100 p-4 rounded">
|
|
||||||
<p>
|
|
||||||
[{todo.uuid}]: {todo.content} <i>({todo.degree})</i>
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(todo.uuid)}
|
|
||||||
className="mt-2 bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600"
|
|
||||||
>
|
|
||||||
Delete Todo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
13
src/frontend/src/app/types/todo.ts
Normal file
13
src/frontend/src/app/types/todo.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export type Todo = {
|
||||||
|
uuid: string;
|
||||||
|
content: string;
|
||||||
|
degree: 'Important' | 'Unimportant';
|
||||||
|
timestamp: {
|
||||||
|
$date: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TodoFormData = {
|
||||||
|
content: string;
|
||||||
|
degree: 'Important' | 'Unimportant';
|
||||||
|
};
|
||||||
46
src/frontend/src/app/utils/api.ts
Normal file
46
src/frontend/src/app/utils/api.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:5000/api';
|
||||||
|
|
||||||
|
// 这里使用一个简单的token,实际项目中应该通过登录获取
|
||||||
|
const TOKEN_LV1 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6MSwiZXhwIjoyMDU0NTIyMzYzfQ.omSk_0tirDTm3mNo9Mf9gKUnLs983pKC2auwI8WyA-s';
|
||||||
|
const TOKEN_LV2 = 'default_token_lv2';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${TOKEN_LV1}` // 默认使用level 1的token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Todo {
|
||||||
|
uuid: string;
|
||||||
|
content: string;
|
||||||
|
degree: string;
|
||||||
|
timestamp: {
|
||||||
|
$date: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TodoAPI = {
|
||||||
|
async getAll(): Promise<Todo[]> {
|
||||||
|
const response = await api.get('/todos');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(data: { content: string; degree: string }): Promise<{ id: string }> {
|
||||||
|
const response = await api.post('/todos', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(uuid: string): Promise<void> {
|
||||||
|
// 删除操作需要level 2的权限
|
||||||
|
const response = await api.delete(`/todos/${uuid}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${TOKEN_LV2}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue