First commit

This commit is contained in:
梦凌汐 2025-05-06 18:22:21 +08:00
commit a17deec331
21 changed files with 2859 additions and 0 deletions

2
.gitingore Normal file
View File

@ -0,0 +1,2 @@
/app/__pycache__
/app/.env

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory to /app
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip3 install -r requirements.txt
# Make port 5070 available to the world outside this container
EXPOSE 5070
# Define environment variable
#ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 梦凌汐
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
app/README.md Normal file
View File

@ -0,0 +1,45 @@
# CatismImage 项目
## 项目简介
CatismImage 是一个基于Web的图片存储和管理系统提供用户友好的界面来上传、管理和查看图片。
## 功能特点
- 用户认证和个性化设置
- 图片上传和管理功能
- 实时流量和存储空间监控
- 主题自定义功能(支持亮色/暗色模式)
- 喵币充值系统
- WebSocket实时数据更新
## 安装指南
1. 确保已安装Python 3.8+
2. 克隆项目仓库
3. 安装Python依赖
```
pip install -r requirements.txt
```
4. 配置数据库连接信息
5. 运行后端服务:
```
python app.py
```
6. 访问前端页面:
```
http://localhost:5000
```
## 使用说明
1. 注册/登录后获取用户Key
2. 使用上传功能添加图片
3. 在仪表板查看存储使用情况和流量统计
4. 通过充值功能获取喵币
5. 在设置中自定义主题颜色和模式
## 技术栈
- 前端HTML5, CSS3, JavaScript, Bootstrap
- 后端Python Flask
- 数据库SQLite/MySQL
- 实时通信WebSocket
## 贡献指南
欢迎提交Pull Request或报告Issue。请确保代码风格一致并通过所有测试。

82
app/app.py Normal file
View File

@ -0,0 +1,82 @@
from flask import Flask, request
from flask_cors import CORS
from routes.auth import auth_bp
from routes.images import images_bp
from routes.payment import payment_bp
from pymongo import MongoClient
from config import Config
import json
import time
app = Flask(__name__, static_folder='static')
CORS(app)
# 注册蓝图
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(images_bp, url_prefix='/images')
app.register_blueprint(payment_bp, url_prefix='/payment')
# 初始化MongoDB连接
client = MongoClient(Config.MONGO_URI)
db = client[Config.MONGO_DB]
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.route('/upload.html')
def upload():
return app.send_static_file('upload.html')
@app.route('/dashboard.html')
def dashboard():
return app.send_static_file('dashboard.html')
@app.route('/orders.html')
def orders():
return app.send_static_file('orders.html')
from flask_sock import Sock
from models import User
sock = Sock(app)
@sock.route('/ws/user-info')
def user_info_ws(ws):
try:
while True:
# Get actual user info from database
key = ws.environ.get('QUERY_STRING', '').split('key=')[1].split('&')[0] if 'key=' in ws.environ.get('QUERY_STRING', '') else None
user = User.get_user(key) if key else None
user_data = {
'nickname': user['nickname'] if user else 'Invalid User',
# 'register_time': user['register_time'].isoformat() if user else None,
'meowcoin': float(user['meowcoin']) if user else 0.0,
'traffic': {
'traffic_limit': int(user['traffic_limit']) if user else 0,
'current_month_traffic': int(user['current_month_traffic']) if user else 0,
'monthly_traffic': user.get('monthly_traffic', {}) if user else {}
},
'storage': {
'upload_count': int(user['upload_count']) if user else 0,
'storage_limit': int(user['storage_limit']) if user else 0,
'used_storage': int(user['used_storage']) if user else 0
}
}
try:
ws.send(json.dumps(user_data))
except Exception as e:
print(f"WebSocket send error: {e}")
break
time.sleep(5)
except Exception as e:
print(f"WebSocket connection error: {e}")
finally:
try:
ws.close()
except:
pass
if __name__ == '__main__':
app.run(host=Config.HOST, port=Config.PORT)

29
app/config.py Normal file
View File

@ -0,0 +1,29 @@
import os
import sys
from dotenv import load_dotenv
load_dotenv()
class Config:
# S3配置
S3_ENDPOINT = os.getenv('S3_ENDPOINT')
S3_ACCESS_KEY = os.getenv('S3_ACCESS_KEY')
S3_SECRET_KEY = os.getenv('S3_SECRET_KEY')
S3_BUCKET = os.getenv('S3_BUCKET')
# MongoDB配置
MONGO_URI = os.getenv('MONGO_URI')
MONGO_DB = os.getenv('MONGO_DB')
if(S3_ACCESS_KEY == None or S3_SECRET_KEY == None or S3_BUCKET == None or S3_ENDPOINT == None):
print("错误缺少必需的S3环境变量")
sys.exit(1)
if(MONGO_URI == None or MONGO_DB == None):
print("错误缺少必需的MongoDB环境变量")
sys.exit(1)
# 服务监听配置
HOST = '0.0.0.0'
PORT = 5000

136
app/models.py Normal file
View File

@ -0,0 +1,136 @@
from pymongo import MongoClient
from config import Config
import random
import string
import datetime
from bson.int64 import Int64
from bson.decimal128 import Decimal128
client = MongoClient(Config.MONGO_URI)
db = client[Config.MONGO_DB]
class User:
collection = db['users']
@staticmethod
def generate_key(length=12):
"""生成由小写字母和数字组成的随机key"""
chars = string.ascii_lowercase + string.digits
return ''.join(random.choice(chars) for _ in range(length))
@classmethod
def create_user(cls, nickname):
"""创建新用户并返回生成的key"""
key = cls.generate_key()
user_data = {
'key': key,
'nickname': nickname,
'register_time': datetime.datetime.utcnow(),
'upload_count': Int64(0),
'storage_limit': Int64(100 * 1024 * 1024), # 默认100MB
'used_storage': Int64(0),
'traffic_limit': Int64(1000 * 1024 * 1024), # 默认1000MB流量限制
'monthly_traffic': {
f"{datetime.datetime.utcnow().year}-{datetime.datetime.utcnow().month}": Int64(0)
},
'meowcoin': 0.0,
'bills': [],
'custom': {
'theme_preference': 'light',
'theme_color': '#3296fa'
}
}
cls.collection.insert_one(user_data)
return key
@classmethod
def increment_upload_count(cls, key):
"""增加用户上传图片计数"""
return cls.collection.update_one(
{'key': key},
{'$inc': {'upload_count': 1}}
)
@classmethod
def decrement_upload_count(cls, key):
"""减少用户上传图片计数"""
return cls.collection.update_one(
{'key': key},
{'$inc': {'upload_count': -1}}
)
@classmethod
def get_user(cls, key):
"""根据key获取用户信息"""
user = cls.collection.find_one({'key': key})
if user:
user['register_time'] = user.get('register_time', datetime.datetime.utcnow())
user['upload_count'] = Int64(user.get('upload_count', 0))
user['storage_limit'] = Int64(user.get('storage_limit', 100 * 1024 * 1024))
user['used_storage'] = Int64(user.get('used_storage', 0))
user['traffic_limit'] = Int64(user.get('traffic_limit', 1000 * 1024 * 1024))
# 获取当月流量
now = datetime.datetime.utcnow()
month_key = f"{now.year}-{now.month}"
user['monthly_traffic'] = user.get('monthly_traffic', {})
user['current_month_traffic'] = Int64(user['monthly_traffic'].get(month_key, 0))
user['meowcoin'] = float(user.get('meowcoin', 0.0))
user['bills'] = user.get('bills', [])
user['custom'] = user.get('custom', {
'theme_preference': 'light',
'theme_color': '#3296fa'
})
return user
@classmethod
def update_nickname(cls, key, new_nickname):
"""更新用户昵称"""
return cls.collection.update_one(
{'key': key},
{'$set': {'nickname': new_nickname}}
)
@classmethod
def update_storage(cls, key, size):
"""更新用户存储使用情况"""
return cls.collection.update_one(
{'key': key},
{'$inc': {'used_storage': size}}
)
@classmethod
def update_traffic(cls, key, size):
"""更新用户流量使用情况"""
now = datetime.datetime.utcnow()
month_key = f"{now.year}-{now.month}"
return cls.collection.update_one(
{'key': key},
{'$inc': {
f'monthly_traffic.{month_key}': size
}}
)
@classmethod
def update_meowcoin(cls, key, amount):
"""更新用户meowcoin余额"""
return cls.collection.update_one(
{'key': key},
{'$inc': {'meowcoin': float(amount)}}
)
@classmethod
def add_bill(cls, key, trade_id, user_id, plan_id, show_amount, discount, total_amount):
"""新增账单记录"""
bill = {
'trade_id': trade_id,
'user_id': user_id,
'plan_id': plan_id,
'amount': show_amount,
'discount': discount,
'actual_amount': total_amount,
'time': datetime.datetime.utcnow()
}
return cls.collection.update_one(
{'key': key},
{'$push': {'bills': bill}}
)

9
app/requirements.txt Normal file
View File

@ -0,0 +1,9 @@
boto3==1.38.8
botocore==1.38.8
Flask==3.1.0
flask_cors==5.0.1
flask_sock==0.7.0
Pillow==9.4.0
Pillow==11.2.1
pymongo==4.12.1
python-dotenv==1.1.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

104
app/routes/auth.py Normal file
View File

@ -0,0 +1,104 @@
from flask import Blueprint, request, jsonify
from models import User
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/register', methods=['POST'])
def register():
data = request.get_json()
nickname = data.get('nickname')
if not nickname:
return jsonify({'error': '请输入昵称'}), 400
key = User.create_user(nickname)
return jsonify({'key': key}), 201
@auth_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
key = data.get('key')
if not key:
return jsonify({'error': 'Key is required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': '密钥无效'}), 401
return jsonify({'nickname': user['nickname']}), 200
@auth_bp.route('/userinfo', methods=['GET'])
def user_info():
key = request.args.get('key')
if not key:
return jsonify({'error': 'Key is required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': 'Invalid key'}), 401
return jsonify({
'nickname': user['nickname'],
'register_time': user['register_time'],
'upload_count': user['upload_count'],
'storage_limit': user['storage_limit'],
'used_storage': user['used_storage'],
'traffic_limit': user['traffic_limit'],
'current_month_traffic': user['current_month_traffic'],
'monthly_traffic': user.get('monthly_traffic', {})
}), 200
@auth_bp.route('/update_nickname', methods=['POST'])
def update_nickname():
data = request.get_json()
key = data.get('key')
new_nickname = data.get('new_nickname')
if not key or not new_nickname:
return jsonify({'error': 'Key and new nickname are required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': '密钥无效'}), 401
User.update_nickname(key, new_nickname)
return jsonify({'message': 'Nickname updated successfully'}), 200
@auth_bp.route('/get_theme', methods=['GET'])
def get_theme():
key = request.args.get('key')
if not key:
return jsonify({'error': 'Key is required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': '密钥无效'}), 401
return jsonify({
'theme_mode': user['custom'].get('theme_preference', 'light'),
'theme_color': user['custom'].get('theme_color', '#3296fa')
}), 200
@auth_bp.route('/update_theme', methods=['POST'])
def update_theme():
data = request.get_json()
key = data.get('key')
theme_preference = data.get('theme_mode')
theme_color = data.get('theme_color')
if not key or not theme_preference or not theme_color:
return jsonify({'error': 'Key, theme_preference and theme_color are required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': '密钥无效'}), 401
User.collection.update_one(
{'key': key},
{'$set': {
'custom.theme_preference': theme_preference,
'custom.theme_color': theme_color
}}
)
return jsonify({'message': 'Theme updated successfully'}), 200

293
app/routes/images.py Normal file
View File

@ -0,0 +1,293 @@
from flask import Blueprint, request, jsonify, Response
import boto3
from botocore.client import Config
from config import Config as AppConfig
from models import User
from pymongo import MongoClient
from bson import ObjectId
import datetime
# 初始化MongoDB连接
client = MongoClient(AppConfig.MONGO_URI)
db = client[AppConfig.MONGO_DB]
images_bp = Blueprint('images', __name__)
# 初始化S3客户端
s3 = boto3.client(
's3',
endpoint_url=AppConfig.S3_ENDPOINT,
aws_access_key_id=AppConfig.S3_ACCESS_KEY,
aws_secret_access_key=AppConfig.S3_SECRET_KEY,
config=Config(signature_version='s3v4')
)
@images_bp.route('/upload', methods=['POST'])
def upload_image():
# 验证用户key
key = request.form.get('key')
if not key:
return jsonify({'error': 'Key is required'}), 400
user = User.get_user(key)
if not user:
return jsonify({'error': '用户账户无效'}), 401
# 处理上传的文件
if 'file' not in request.files:
return jsonify({'error': '文件上传失败'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '未选择文件'}), 400
# 检查文件类型
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
if '.' not in file.filename or file.filename.split('.')[-1].lower() not in allowed_extensions:
return jsonify({'error': '不支持的文件类型'}), 400
# 检查文件内容是否为有效图片
try:
from PIL import Image
import io
# 直接使用PIL验证图片内容
try:
file.seek(0)
img = Image.open(file)
img.verify()
file.seek(0)
except Exception:
return jsonify({'error': '无效的图片文件'}), 400
except Exception:
return jsonify({'error': '无效的图片文件'}), 400
# 获取文件大小并检查存储空间
file.seek(0, 2) # 移动到文件末尾
file_size = file.tell() # 获取文件大小(字节)
file.seek(0) # 重置文件指针
if user['used_storage'] + file_size > user['storage_limit']:
return jsonify({'error': '存储空间不足'}), 403
# 生成唯一文件名: 用户key_时间戳.扩展名
import time
filename = f"{key}_{int(time.time())}.{file.filename.split('.')[-1]}"
try:
# 生成缩略图并上传
from PIL import Image
import io
# 创建缩略图
file.seek(0) # Rewind file pointer before processing
img = Image.open(file)
img.thumbnail((128, 128))
# 转换为webp格式
thumb_filename = f"thumb_{filename.split('.')[0]}.webp"
thumb_buffer = io.BytesIO()
img.save(thumb_buffer, format='WEBP')
thumb_buffer.seek(0)
# 上传缩略图
try:
s3.upload_fileobj(
thumb_buffer,
AppConfig.S3_BUCKET,
thumb_filename,
ExtraArgs={'ACL': 'public-read'}
)
finally:
thumb_buffer.close()
# 获取文件大小
file.seek(0, 2) # 移动到文件末尾
file_size = file.tell() # 获取文件大小(字节)
file.seek(0) # 重置文件指针
# 存储图片信息到MongoDB并立即返回响应
image_id = db.images.insert_one({
'filename': filename,
'thumb_filename': thumb_filename,
'owner_key': key,
'created_at': datetime.datetime.utcnow(),
'size': file_size,
'upload_status': 'pending'
}).inserted_id
# 立即返回响应
response = jsonify({
'url': f'/images/{str(image_id)}',
'thumb_url': f'/images/thumb/{str(image_id)}',
'message': '图片正在上传中'
})
# 同步上传原文件到S3
try:
s3.upload_fileobj(
file,
AppConfig.S3_BUCKET,
filename,
ExtraArgs={'ACL': 'public-read'}
)
# 更新上传状态和用户信息
db.images.update_one(
{'_id': image_id},
{'$set': {'upload_status': 'completed'}}
)
User.increment_upload_count(key)
User.update_storage(key, file_size)
return response, 200
except Exception as e:
db.images.update_one(
{'_id': image_id},
{'$set': {'upload_status': 'failed', 'error': str(e)}}
)
return jsonify({'error': str(e)}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@images_bp.route('/<image_id>', methods=['GET'])
def get_image(image_id):
try:
# 验证image_id格式
if not ObjectId.is_valid(image_id):
return jsonify({'error': 'Invalid image ID format'}), 400
# 从MongoDB获取图片信息
image_data = db.images.find_one({'_id': ObjectId(image_id)})
if not image_data:
return jsonify({'error': 'Image not found'}), 404
# 获取用户key并验证流量限制
owner_key = image_data['owner_key']
if owner_key:
owner_user = User.get_user(owner_key)
if owner_user:
# 检查当月流量是否超出限制
if owner_user['current_month_traffic'] + image_data['size'] > owner_user['traffic_limit']:
return jsonify({'error': '本月流量已用完,无法访问原图'}), 403
# 从S3获取图片并返回
response = s3.get_object(
Bucket=AppConfig.S3_BUCKET,
Key=image_data['filename']
)
# 记录流量使用
owner_user = User.get_user(owner_key)
if owner_user:
User.update_traffic(owner_key, image_data['size'])
return Response(
response['Body'].read(),
mimetype=response['ContentType']
)
except Exception as e:
import logging
logging.error(f"Error getting image {image_id}: {str(e)}")
return jsonify({'error': str(e)}), 500
@images_bp.route('/thumb/<image_id>', methods=['GET'])
def get_thumbnail(image_id):
try:
# 验证image_id格式
if not ObjectId.is_valid(image_id):
return jsonify({'error': 'Invalid image ID format'}), 400
# 从MongoDB获取图片信息
image_data = db.images.find_one({'_id': ObjectId(image_id)})
if not image_data:
return jsonify({'error': 'Image not found'}), 404
# 从S3获取缩略图并返回
response = s3.get_object(
Bucket=AppConfig.S3_BUCKET,
Key=image_data['thumb_filename']
)
return Response(
response['Body'].read(),
mimetype=response['ContentType']
)
except Exception as e:
import logging
logging.error(f"Error getting thumbnail {image_id}: {str(e)}")
return jsonify({'error': str(e)}), 500
@images_bp.route('/user_images', methods=['GET'])
def get_user_images():
try:
user_key = request.args.get('key')
if not user_key:
return jsonify({'error': 'User key is required'}), 400
# 验证用户是否存在
user = User.get_user(user_key)
if not user:
return jsonify({'error': 'User not found'}), 404
# 获取用户上传的所有图片
images = list(db.images.find({'owner_key': user_key}))
# 转换ObjectId为字符串并添加后端转发URL
for img in images:
img['_id'] = str(img['_id'])
img['url'] = f'/images/{img["_id"]}'
img['thumb_url'] = f'/images/thumb/{img["_id"]}'
img['created_at'] = (img['created_at'] + datetime.timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S')
return jsonify({'images': images})
except Exception as e:
import logging
logging.error(f"Error getting user images: {str(e)}")
return jsonify({'error': str(e)}), 500
@images_bp.route('/<image_id>', methods=['DELETE'])
def delete_image(image_id):
try:
# 验证用户key
key = request.json.get('key')
if not key:
return jsonify({'error': 'Key is required'}), 400
# 验证用户是否存在
user = User.get_user(key)
if not user:
return jsonify({'error': 'Invalid key'}), 401
# 验证image_id格式
if not ObjectId.is_valid(image_id):
return jsonify({'error': 'Invalid image ID format'}), 400
# 查找图片记录
image_data = db.images.find_one({'_id': ObjectId(image_id)})
if not image_data:
return jsonify({'error': 'Image not found'}), 404
# 验证图片所有者
if image_data['owner_key'] != key:
return jsonify({'error': 'Unauthorized to delete this image'}), 403
# 从S3删除原文件和缩略图
s3.delete_object(Bucket=AppConfig.S3_BUCKET, Key=image_data['filename'])
s3.delete_object(Bucket=AppConfig.S3_BUCKET, Key=image_data['thumb_filename'])
# 从数据库删除记录
# 获取图片大小
image_size = image_data['size']
# 更新用户上传计数和存储使用情况
User.decrement_upload_count(key)
User.update_storage(key, -image_size)
db.images.delete_one({'_id': ObjectId(image_id)})
return jsonify({'message': 'Image deleted successfully'}), 200
except Exception as e:
import logging
logging.error(f"Error deleting image {image_id}: {str(e)}")
return jsonify({'error': str(e)}), 500

91
app/routes/payment.py Normal file
View File

@ -0,0 +1,91 @@
from flask import Blueprint, request, jsonify
from models import User
import datetime
payment_bp = Blueprint('payment', __name__)
@payment_bp.route('/orders', methods=['GET'])
def get_orders():
start_date = request.args.get('start')
end_date = request.args.get('end')
user_key = request.args.get('key')
if not all([start_date, end_date, user_key]):
return jsonify({'error': 'Missing required parameters'}), 400
# 验证用户权限
user = User.get_user(user_key)
if not user:
return jsonify({'error': 'Invalid user key'}), 401
# 筛选账单数据
try:
start = datetime.datetime.strptime(start_date, '%Y-%m-%d')
end = datetime.datetime.strptime(end_date, '%Y-%m-%d')
end += datetime.timedelta(days=1)
filtered_bills = [
{
'order_id': bill.get('trade_id', ''),
'created_at': bill['time'].isoformat(),
'amount': bill.get('actual_amount', '0'),
'status': 'completed'
}
for bill in user.get('bills', [])
if start <= bill['time'] <= end
]
return jsonify(filtered_bills), 200
except ValueError:
return jsonify({'error': 'Invalid date format, use YYYY-MM-DD'}), 400
@payment_bp.route('/ifdian_callback', methods=['POST'])
def ifdian_callback():
data = request.get_json()
# 验证请求格式
if not data or 'data' not in data or 'order' not in data['data']:
return jsonify({'ec': 200, 'error': 'Invalid request format'}), 400
order = data['data']['order']
# 如果product_type为0直接返回
if order.get('product_type', 0) == 0:
return jsonify({'message': 'product_type is 0, no action needed'}), 200
# 处理product_type为1的情况
if order.get('product_type', 0) == 1:
key = order.get('remark', '')
if not key:
return jsonify({'ec': 200, 'error': 'Missing user key in remark'}), 400
# 获取用户
user = User.get_user(key)
if not user:
return jsonify({'ec': 200, 'error': 'User not found'}), 404
try:
# 将show_amount转换为浮点数MeowCoin
meowcoin_amount = float(order.get('show_amount', '0'))
# 更新用户meowcoin余额
User.update_meowcoin(key, meowcoin_amount)
# 添加账单记录
User.add_bill(
key=key,
trade_id=order.get('out_trade_no', ''),
user_id=order.get('user_id', ''),
plan_id=order.get('plan_id', ''),
show_amount=order.get('show_amount', '0'),
discount=order.get('discount', '0'),
total_amount=order.get('total_amount', '0')
)
return jsonify({'ec': 200}), 200
except ValueError:
return jsonify({'ec': 200, 'error': 'Invalid amount format'}), 400
except Exception as e:
return jsonify({'ec': 200, 'error': str(e)}), 500

6
app/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

425
app/static/css/theme.css Normal file
View File

@ -0,0 +1,425 @@
:root {
--bg-color: #ffffff;
--text-color: #212529;
--card-bg: #f8f9fa;
--border-color: #dee2e6;
}
[data-theme="dark"] {
--bg-color: #212529;
--text-color: #f8f9fa;
--card-bg: #343a40;
--border-color: #495057;
--user-info-bg: #2c3034;
--form-label-color: #adb5bd;
--drop-zone-border: #495057;
--drop-zone-text: #dee2e6;
--image-list-bg: #343a40;
--image-list-text: #f8f9fa;
--storage-text: #e9ecef;
--image-count-text: #e9ecef;
--image-meta-text: #ced4da;
--modal-bg: #343a40;
--modal-text: #f8f9fa;
--modal-close: #f8f9fa;
--loading-text: #e9ecef;
--input-bg: #343a40;
--input-text: #f8f9fa;
--input-border: #495057;
--pagination-active-bg: #495057;
--pagination-active-text: #ffffff;
--btn-active-bg: #0a58ca;
--btn-active-border: #0a58ca;
--btn-focus-shadow: rgba(10, 88, 202, 0.5);
--progress-bg: #495057;
.background-layer::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
pointer-events: none;
}
.modal-content {
background-color: rgba(0, 0, 0, 0.3);
color: #e0e0e0;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
.table-responsive {
color: #e0e0e0;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
border-radius: 8px;
}
.table-striped > tbody > tr:nth-of-type(odd) {
background-color: #2d2d2d !important;
color: #e0e0e0;
}
.table-striped > tbody > tr:nth-of-type(even) {
background-color: #1e1e1e !important;
color: #e0e0e0;
}
.table th {
background-color: rgba(52, 58, 64, 0.7);
color: #f8f9fa;
border-color: #495057;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.table td {
border-color: #495057;
color: #e0e0e0;
background-color: rgba(45, 45, 45, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.form-control {
background-color: rgba(0, 0, 0, 0.4) !important;
color: #e0e0e0;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.form-label {
color: #e0e0e0 !important;
}
}
body {
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
min-height: 100vh;
position: relative;
}
#imageListContainer h2,
.card-body,
.card-text,
#storageUsage,
#imageCount,
.drop-zone p,
#loadingIndicator p,
.modal-content,
.table,
.table th,
.table td,
.form-label,
.form-control {
transition: color 0.3s ease;
}
:root {
--theme-color: #787878;
--theme-color-light: #787878;
--theme-color-lighter: #787878;
--theme-color-dark: #787878;
--theme-color-darker: #787878;
}
.background-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(125deg, var(--theme-color-darker), var(--theme-color-dark), var(--theme-color), var(--theme-color-light), var(--theme-color-lighter));
background-size: 500%;
animation: bgAnimation 15s linear infinite;
z-index: -1;
}
@keyframes bgAnimation {
0%{
background-position: 0% 50%;
}
50%{
background-position: 100% 50%;
}
100%{
background-position: 0% 50%;
}
}
#loginBox {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-info {
background-color: var(--user-info-bg, var(--card-bg));
transition: background-color 0.3s;
}
.form-label {
color: var(--form-label-color, var(--text-color));
}
.form-control {
background-color: var(--input-bg, var(--card-bg));
color: var(--input-text, var(--text-color));
border-color: var(--input-border, var(--border-color));
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
}
.nav-link.active {
background-color: var(--pagination-active-bg, #0d6efd) !important;
color: var(--pagination-active-text, #ffffff) !important;
border-color: var(--pagination-active-bg, #0d6efd) !important;
}
.btn-primary:active, .btn-primary.active {
background-color: var(--btn-active-bg, #0b5ed7);
border-color: var(--btn-active-border, #0a58ca);
}
.btn:hover, .btn-primary:hover, #themeDropdown:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
opacity: 0.9;
transition: all 0.2s ease;
}
#themeDropdown {
background-color: #4a6bff;
color: white;
border: 2px solid #2a4bdf;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] #themeDropdown {
background-color: #6b8cff;
border-color: #4a6bff;
}
#themeDropdown:hover {
background-color: #3a5bef;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] #themeDropdown:hover {
background-color: #5a7bff;
}
.btn-primary:focus {
box-shadow: 0 0 0 0.25rem var(--btn-focus-shadow, rgba(49, 132, 253, 0.5));
}
.drop-zone {
border-color: var(--drop-zone-border, var(--border-color));
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.drop-zone:hover {
background-color: rgba(0, 123, 255, 0.05);
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.drop-zone p {
color: var(--drop-zone-text, var(--text-color));
}
#storageUsage,
#imageCount {
color: var(--storage-text, var(--text-color));
}
.card-body {
color: var(--text-color);
}
.card-text {
color: var(--image-meta-text, var(--text-color));
}
.card-text {
color: var(--image-meta-text, var(--text-color));
}
.modal-content {
color: var(--modal-text, var(--text-color));
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
background-color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
}
.btn-close {
filter: invert(0);
}
.selected-image {
box-shadow: 0 0 15px rgba(0, 123, 255, 0.5);
border: 3px solid #007bff;
transition: all 0.2s ease;
}
[data-theme="light"] .btn-close {
filter: invert(0);
}
[data-theme="dark"] .btn-close {
filter: invert(1);
}
[data-theme="dark"] .image-action-menu {
background-color: var(--card-bg) !important;
color: var(--text-color);
border: 1px solid var(--border-color);
}
[data-theme="dark"] .image-action-menu .btn {
color: #f8f9fa;
}
[data-theme="dark"] .image-action-menu .btn:hover {
background-color: #3d3d3d;
}
[data-theme="dark"] .image-action-menu .text-secondary {
color: #adb5bd !important;
}
[data-theme="dark"] .image-action-menu .text-danger {
color: #ff6b6b !important;
}
#loadingIndicator p {
color: var(--loading-text, var(--text-color));
}
#imageListContainer {
background-color: var(--bg-color);
color: var(--text-color);
padding: 20px;
border-radius: 8px;
margin-bottom: 10px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
background-color: rgba(255, 255, 255, 0.5);
}
#imageListContainer:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
#imageListContainer h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-color);
}
[data-theme="dark"] #imageListContainer {
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
.user-info {
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
background-color: rgba(255, 255, 255, 0.5);
}
.user-info:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] .user-info {
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
.card {
/* margin-bottom: 20px; */
transition: transform 0.3s ease;
border-radius: 8px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
background-color: rgba(255, 255, 255, 0.5);
}
[data-theme="dark"] .card {
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 0 32px 0 rgba(0, 0, 0, 0.3);
}
.card:hover {
opacity: 0.8;
transform: scale(1.02) translateY(-3px);
box-shadow: 0 0 rgba(255, 218, 34, 0.05);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.card img {
object-fit: cover;
transition: all 0.3s ease;
transform: scale(1);
border-radius: 8px;
}
#editNicknameForm {
transition: all 0.3s ease;
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.progress {
background-color: var(--progress-bg, #e9ecef);
}
.toast {
margin-top: 10px;
margin-bottom: 10px;
}

221
app/static/dashboard.html Normal file
View File

@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CatismImage - 仪表盘</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/theme.css" rel="stylesheet">
<link href="/static/css/theme.css" rel="stylesheet">
</head>
<body>
<div class="background-layer"></div>
<div class="container py-4">
<div class="row">
<div class="col-md-4">
<div class="user-info">
<h3>用户信息 <button class="btn btn-sm btn-danger" id="logoutBtn">退出登录</button>
<div class="btn-group ms-2">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
主题
</button>
<ul class="dropdown-menu" aria-labelledby="themeDropdown">
<li><a class="dropdown-item" href="#" data-theme="light">浅色</a></li>
<li><a class="dropdown-item" href="#" data-theme="dark">深色</a></li>
<li><a class="dropdown-item" href="#" id="themeSettingsBtn">更多设置</a></li>
</ul>
</div></h3>
<div class="mb-3">
<div class="row">
<div class="col-12 col-lg-7 mb-2">
<label class="form-label">登录密钥</label>
<div class="d-flex align-items-center">
<span id="userKey"></span>
<button class="btn btn-sm btn-outline-secondary ms-2" id="copyKeyBtn">复制</button>
</div>
</div>
<div class="col-12 col-lg-5">
<label class="form-label">昵称</label>
<div class="d-flex align-items-center">
<span id="userNickname"></span>
<button class="btn btn-sm btn-outline-primary ms-2" id="editNicknameBtn" data-bs-toggle="modal" data-bs-target="#editNicknameModal">修改</button>
</div>
</div>
</div>
</div>
<div class="mb-3">
<h5 class="mb-2">用量情况</h5>
<div class="col-12 col-lg-12 mb-2">
<label class="form-label">MeowCoin余额</label>
<div class="d-flex align-items-center">
<span id="userMeowcoin">Loading...</span>
<button class="btn btn-sm btn-primary ms-2" id="rechargeBtn">充值</button>
<button class="btn btn-sm btn-info ms-2" id="queryBillBtn">查询</button>
</div>
</div>
<div class="my-2" id="trafficUsage"></div>
<div class="mb-3" id="storageUsage"></div>
</div>
<div class="mt-4">
<h4>上传图片</h4>
<form id="uploadForm">
<div class="my-3">
<div class="drop-zone" id="dropZone">
<input class="form-control" type="file" id="imageFile" accept="image/*" multiple required style="display: none;">
<p style="margin-top: 10px; cursor: pointer;">点击或拖拽图片到此处</p>
<div id="uploadArea"></div>
</div>
</div>
</form>
<div id="uploadResult" class="mt-4 d-none">
<div class="alert alert-success">
<h5>上传成功!</h5>
<p>图片URL: <a href="#" id="imageUrl" target="_blank"></a></p>
</div>
<div class="text-center">
<img id="uploadPreviewImage" class="img-thumbnail" style="max-height: 300px;">
</div>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div id="imageListContainer">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>我的图片</h3>
<button class="btn btn-sm btn-outline-secondary d-md-none my-2" type="button" data-bs-toggle="collapse" data-bs-target="#imageListCollapse" aria-expanded="false" aria-controls="imageListCollapse">
显示/隐藏
</button>
</div>
<div class="collapse d-md-block mx-2" id="imageListCollapse">
<div class="row" id="imageList">
<!-- 图片列表将通过JS动态加载 -->
</div>
</div>
</div>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/dashboard.js"></script>
</body>
</html>
<!-- 主题设置模态框 -->
<div class="modal fade" id="themeSettingsModal" tabindex="-1" aria-labelledby="themeSettingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="themeSettingsModalLabel">主题设置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">主题模式</label>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle mx-2" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
选择主题
</button>
<ul class="dropdown-menu" aria-labelledby="themeDropdown">
<li><a class="dropdown-item" href="#" data-theme="light">浅色</a></li>
<li><a class="dropdown-item" href="#" data-theme="dark">深色</a></li>
</ul>
</div>
<div class="mb-3">
<label for="themeColorPicker" class="form-label">主题颜色</label>
<input type="color" class="form-control form-control-color w-50" id="themeColorPicker" value="#0d6efd">
</div>
</div>
</div>
</div>
</div>
<!-- 昵称修改模态框 -->
<div class="modal fade" id="editNicknameModal" tabindex="-1" aria-labelledby="editNicknameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editNicknameModalLabel">修改昵称</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newNickname" class="form-label">新昵称</label>
<input type="text" class="form-control" id="newNickname">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveNicknameBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 充值模态框 -->
<div class="modal fade" id="rechargeModal" tabindex="-1" aria-labelledby="rechargeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="rechargeModalLabel">充值 MeowCoin</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="rechargeAmount" class="form-label">充值金额</label>
<input type="number" class="form-control" id="rechargeAmount" min="1" step="1">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmRechargeBtn">确认充值</button>
</div>
</div>
</div>
</div>
<!-- 账单查询模态框 -->
<div class="modal fade" id="billQueryModal" tabindex="-1" aria-labelledby="billQueryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-fullscreen-lg-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="billQueryModalLabel">账单查询</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-5">
<label for="startDate" class="form-label">开始日期</label>
<input type="date" class="form-control" id="startDate">
</div>
<div class="col-md-5">
<label for="endDate" class="form-label">结束日期</label>
<input type="date" class="form-control" id="endDate">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary w-100" id="queryBillSubmit">查询</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>订单号</th>
<th>时间</th>
<th>金额</th>
<th>状态</th>
</tr>
</thead>
<tbody id="billTableBody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

109
app/static/index.html Normal file
View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CatismImage - 轻量好用的图床</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/theme.css" rel="stylesheet">
<script>
// 检测系统主题偏好
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
});
</script>
</head>
<body>
<div class="background-layer"></div>
<div class="container" id="loginBox">
<div class="row justify-content-center">
<h1 class="text-center mb-4">Catism Image</h1>
<h4 class="text-center mb-4">立志做真正轻量好用的图床</h4>
<div class="col-md-6 mt-4">
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="authTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button" role="tab">登录</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab">注册</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="authTabsContent">
<div class="tab-pane fade show active" id="login" role="tabpanel">
<form id="loginForm">
<div class="mb-3">
<label for="loginKey" class="form-label">密钥</label>
<input type="text" class="form-control" id="loginKey" required>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
<div class="tab-pane fade" id="register" role="tabpanel">
<form id="registerForm">
<div class="mb-3">
<label for="registerNickname" class="form-label">昵称</label>
<input type="text" class="form-control" id="registerNickname" required>
</div>
<button type="submit" class="btn btn-primary">注册</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/app.js"></script>
<div id="toastContainer" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11"></div>
<div class="position-fixed bottom-0 start-0 p-3" style="z-index: 11">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
主题
</button>
<ul class="dropdown-menu" aria-labelledby="themeDropdown">
<li><a class="dropdown-item" href="#" data-theme="light">浅色</a></li>
<li><a class="dropdown-item" href="#" data-theme="dark">深色</a></li>
<li><a class="dropdown-item" href="#" data-theme="auto">自动</a></li>
</ul>
</div>
</div>
<script>
document.querySelectorAll('[data-theme]').forEach(item => {
item.addEventListener('click', e => {
if (e.target.tagName === 'A') {
e.preventDefault();
const theme = e.target.getAttribute('data-theme');
if (theme === 'auto') {
localStorage.removeItem('manualTheme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
} else {
localStorage.setItem('manualTheme', theme);
document.documentElement.setAttribute('data-theme', theme);
}
}
});
});
// 防止输入框点击切换主题
document.querySelectorAll('input').forEach(input => {
input.addEventListener('click', e => {
e.stopPropagation();
});
});
</script>
</body>
</html>

90
app/static/js/app.js Normal file
View File

@ -0,0 +1,90 @@
// 登录表单处理
function showToast(message, type) {
const toastContainer = document.getElementById('toastContainer');
const toastEl = document.createElement('div');
toastEl.className = `toast show align-items-center text-white bg-${type} border-0`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toastEl);
// 自动移除Toast
setTimeout(() => {
toastEl.classList.remove('show');
setTimeout(() => {
toastEl.remove();
}, 300);
}, 3000);
}
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const key = document.getElementById('loginKey').value;
try {
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key })
});
const data = await response.json();
if (response.ok) {
showToast(`欢迎回来, ${data.nickname}!`, 'success');
// 保存key到localStorage
localStorage.setItem('userKey', key);
// 登录成功后跳转到仪表盘
window.location.href = '/dashboard.html';
} else {
showToast(`登录失败: ${data.error}`, 'danger');
}
} catch (error) {
console.error('登录请求出错:', error);
showToast('登录请求出错', 'danger');
}
});
// 注册表单处理
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const nickname = document.getElementById('registerNickname').value;
try {
const response = await fetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ nickname })
});
const data = await response.json();
if (response.ok) {
showToast(`注册成功! 您的密钥是: ${data.key}`, 'success');
// 自动填充登录表单
document.getElementById('loginKey').value = data.key;
// 切换到登录标签页
document.getElementById('login-tab').click();
} else {
showToast(`注册失败: ${data.error}`, 'danger');
}
} catch (error) {
console.error('注册请求出错:', error);
showToast('注册请求出错', 'danger');
}
});

7
app/static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1169
app/static/js/dashboard.js Normal file

File diff suppressed because it is too large Load Diff