init
This commit is contained in:
commit
70d467d052
97 files changed
+10386
No files matched your search
@@ -0,0 +1,117 @@
|
||||
# IDE
|
||||
.vscode/
|
||||
fsys.dist
|
||||
fsys.build
|
||||
*.bin
|
||||
*.out
|
||||
|
||||
# PRJ
|
||||
#config.ini
|
||||
*.pem
|
||||
aes_key.txt
|
||||
public/users/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
source ./venv/bin/activate
|
||||
# 直接运行Python应用,HTTPS/HTTP配置在app.py中处理
|
||||
exec fsys.bin --config config.ini 1>/dev/null
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
python3 -m venv venv
|
||||
#sudo chmod +x .venv/bin/activate
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
|
||||
nuitka --onefile fsys.py
|
||||
@@ -0,0 +1,14 @@
|
||||
[server]
|
||||
host = 0.0.0.0
|
||||
port = 9200
|
||||
|
||||
[directory]
|
||||
root = ./public
|
||||
|
||||
[ssl]
|
||||
enabled = false
|
||||
cert_file = cert.pem
|
||||
key_file = key.pem
|
||||
|
||||
[cors]
|
||||
allowed_origins = *
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
# file_server.py - 极简版文件托管服务器
|
||||
from flask import Flask, send_from_directory, jsonify, request, Response, send_file
|
||||
from flask_cors import CORS
|
||||
import os
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
file_handler = RotatingFileHandler('app.log', maxBytes=10485760, backupCount=10)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s'
|
||||
))
|
||||
file_handler.setLevel(logging.INFO)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
parser = argparse.ArgumentParser(description='File Server')
|
||||
parser.add_argument('--config', type=str, default='config.ini', help='Configuration file path')
|
||||
args = parser.parse_args()
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# ==================== 配置文件加载 ====================
|
||||
CONFIG_FILE = args.config
|
||||
|
||||
file_config = {
|
||||
'server': {'host': '0.0.0.0', 'port': '5000'},
|
||||
'directory': {'root': './public'},
|
||||
'ssl': {'enabled': 'false', 'cert_file': 'cert.pem', 'key_file': 'key.pem'},
|
||||
}
|
||||
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
config_parser = configparser.ConfigParser()
|
||||
config_parser.read(CONFIG_FILE, encoding='utf-8')
|
||||
for section in ['server', 'directory', 'ssl']:
|
||||
if section in config_parser:
|
||||
for key, value in config_parser[section].items():
|
||||
if key in file_config[section]:
|
||||
file_config[section][key] = value
|
||||
|
||||
HTML_ROOT_DIR = file_config['directory']['root']
|
||||
FILESYSTEM_ENABLED = bool(HTML_ROOT_DIR and HTML_ROOT_DIR.strip())
|
||||
|
||||
if FILESYSTEM_ENABLED:
|
||||
try:
|
||||
Path(HTML_ROOT_DIR).mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Created directory: {HTML_ROOT_DIR}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directory: {str(e)}")
|
||||
FILESYSTEM_ENABLED = False
|
||||
|
||||
SSL_ENABLED = file_config['ssl'].get('enabled', 'false').lower() == 'true'
|
||||
SSL_CERT_FILE = file_config['ssl'].get('cert_file', 'cert.pem')
|
||||
SSL_KEY_FILE = file_config['ssl'].get('key_file', 'key.pem')
|
||||
|
||||
# ==================== 视频文件支持 ====================
|
||||
VIDEO_MIME_TYPES = {
|
||||
'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg',
|
||||
'.avi': 'video/x-msvideo', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska'
|
||||
}
|
||||
|
||||
AUDIO_MIME_TYPES = {
|
||||
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
|
||||
'.flac': 'audio/flac', '.aac': 'audio/aac', '.m4a': 'audio/mp4'
|
||||
}
|
||||
|
||||
def is_video_file(filename):
|
||||
return os.path.splitext(filename)[1].lower() in VIDEO_MIME_TYPES
|
||||
|
||||
def is_audio_file(filename):
|
||||
return os.path.splitext(filename)[1].lower() in AUDIO_MIME_TYPES
|
||||
|
||||
def get_media_mime_type(filename):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
return VIDEO_MIME_TYPES.get(ext) or AUDIO_MIME_TYPES.get(ext) or 'application/octet-stream'
|
||||
|
||||
def safe_path_join(base_dir, user_path):
|
||||
if not user_path or user_path.strip() == '' or user_path.strip() == '/':
|
||||
return base_dir
|
||||
if user_path.startswith('/'):
|
||||
user_path = user_path[1:]
|
||||
full_path = os.path.normpath(os.path.join(base_dir, user_path))
|
||||
base_dir_real = os.path.realpath(base_dir)
|
||||
full_path_real = os.path.realpath(full_path)
|
||||
if not full_path_real.startswith(base_dir_real):
|
||||
return None
|
||||
return full_path_real
|
||||
|
||||
def format_file_size(size):
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f}TB"
|
||||
|
||||
# ==================== API 接口 ====================
|
||||
@app.route('/api/filelist', methods=['GET'])
|
||||
def get_file_list():
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return jsonify({'error': '文件系统未启用'}), 403
|
||||
|
||||
requested_path = request.args.get('path', '')
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, requested_path)
|
||||
|
||||
if full_path is None:
|
||||
return jsonify({'error': '无效的路径'}), 403
|
||||
|
||||
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
||||
return jsonify({'error': '目录不存在'}), 404
|
||||
|
||||
items = []
|
||||
for item in os.listdir(full_path):
|
||||
if item == '__pycache__':
|
||||
continue
|
||||
|
||||
item_path = os.path.join(full_path, item)
|
||||
item_stat = os.stat(item_path)
|
||||
is_dir = os.path.isdir(item_path)
|
||||
|
||||
item_rel_path = os.path.join(requested_path, item) if requested_path else item
|
||||
|
||||
file_type = 'dir' if is_dir else ('video' if is_video_file(item) else ('audio' if is_audio_file(item) else 'file'))
|
||||
|
||||
items.append({
|
||||
'name': item,
|
||||
'type': file_type,
|
||||
'path': item_rel_path,
|
||||
'size': item_stat.st_size if not is_dir else 0,
|
||||
'size_formatted': format_file_size(item_stat.st_size) if not is_dir else '',
|
||||
'modified': datetime.datetime.fromtimestamp(item_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
items.sort(key=lambda x: (x['type'] != 'dir', x['name'].lower()))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'path': requested_path or '/',
|
||||
'parent_path': os.path.dirname(requested_path) if requested_path and os.path.dirname(requested_path) != '/' else '',
|
||||
'items': items
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Get file list error: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/video/<path:file_path>', methods=['GET'])
|
||||
def stream_video(file_path):
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return jsonify({'error': '文件系统未启用'}), 403
|
||||
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, file_path)
|
||||
if full_path is None or not os.path.exists(full_path) or not os.path.isfile(full_path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
|
||||
if not is_video_file(full_path) and not is_audio_file(full_path):
|
||||
return jsonify({'error': '不支持的文件类型'}), 400
|
||||
|
||||
file_size = os.path.getsize(full_path)
|
||||
mime_type = get_media_mime_type(full_path)
|
||||
|
||||
range_header = request.headers.get('Range', None)
|
||||
|
||||
if range_header:
|
||||
byte_range = range_header.replace('bytes=', '').split('-')
|
||||
start = int(byte_range[0]) if byte_range[0] else 0
|
||||
end = int(byte_range[1]) if len(byte_range) > 1 and byte_range[1] else file_size - 1
|
||||
|
||||
if start >= file_size:
|
||||
return jsonify({'error': '请求范围超出文件大小'}), 416
|
||||
|
||||
length = end - start + 1
|
||||
|
||||
with open(full_path, 'rb') as f:
|
||||
f.seek(start)
|
||||
data = f.read(length)
|
||||
|
||||
response = Response(data, 206, mimetype=mime_type, direct_passthrough=True)
|
||||
response.headers.add('Content-Range', f'bytes {start}-{end}/{file_size}')
|
||||
response.headers.add('Accept-Ranges', 'bytes')
|
||||
response.headers.add('Content-Length', str(length))
|
||||
response.headers.add('Cache-Control', 'no-cache')
|
||||
return response
|
||||
else:
|
||||
return send_file(full_path, mimetype=mime_type, as_attachment=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Video streaming error: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ==================== 极简版HTML页面(使用字符串拼接,避免花括号问题) ====================
|
||||
def get_html_template(path, breadcrumb_html):
|
||||
"""生成HTML页面"""
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
|
||||
<title>文件浏览</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
}}
|
||||
.header {{
|
||||
background: #4A90D9;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
.header h1 {{ font-size: 18px; font-weight: 600; }}
|
||||
.header a {{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 4px 10px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.header a:active {{ background: rgba(255,255,255,0.4); }}
|
||||
.breadcrumb {{
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}}
|
||||
.breadcrumb a {{ color: #4A90D9; text-decoration: none; padding: 2px 6px; }}
|
||||
.breadcrumb .sep {{ color: #999; margin: 0 4px; }}
|
||||
.breadcrumb .current {{ color: #333; font-weight: 500; }}
|
||||
.search-box {{
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}}
|
||||
.search-box input {{
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}}
|
||||
.search-box input:focus {{ border-color: #4A90D9; }}
|
||||
.stats {{
|
||||
padding: 6px 16px;
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.container {{
|
||||
padding: 8px 12px 60px 12px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.item {{
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}}
|
||||
.item:active {{ background: #f0f0f0; }}
|
||||
.item .icon {{ font-size: 24px; flex-shrink: 0; width: 36px; text-align: center; }}
|
||||
.item .info {{ flex: 1; min-width: 0; }}
|
||||
.item .name {{ font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
|
||||
.item .meta {{ font-size: 12px; color: #999; display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||
.item .actions {{ display: flex; gap: 4px; flex-shrink: 0; }}
|
||||
.item .actions button {{
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
.item .actions button:active {{ transform: scale(0.95); }}
|
||||
.btn-play {{ background: #4CAF50; color: white; }}
|
||||
.btn-download {{ background: #4A90D9; color: white; }}
|
||||
.loading {{ text-align: center; padding: 40px; color: #999; }}
|
||||
.loading .spinner {{ display: inline-block; animation: spin 1s linear infinite; font-size: 28px; }}
|
||||
@keyframes spin {{ 100% {{ transform: rotate(360deg); }} }}
|
||||
.empty {{ text-align: center; padding: 60px 20px; color: #999; }}
|
||||
.empty .icon {{ font-size: 48px; display: block; margin-bottom: 12px; }}
|
||||
.error {{ text-align: center; padding: 40px 20px; color: #e74c3c; }}
|
||||
@media (max-width: 480px) {{
|
||||
.item {{ padding: 8px 10px; }}
|
||||
.item .icon {{ font-size: 20px; width: 30px; }}
|
||||
.item .name {{ font-size: 13px; }}
|
||||
.item .meta {{ font-size: 11px; gap: 6px; }}
|
||||
.item .actions button {{ padding: 3px 8px; font-size: 11px; }}
|
||||
.header h1 {{ font-size: 16px; }}
|
||||
}}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
body {{ background: #1a1a2e; color: #e0e0e0; }}
|
||||
.header {{ background: #2d2d5e; }}
|
||||
.breadcrumb {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.breadcrumb .current {{ color: #e0e0e0; }}
|
||||
.stats {{ background: #16213e; color: #aaa; border-color: #2a2a4a; }}
|
||||
.search-box {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.search-box input {{ background: #2a2a4a; border-color: #3a3a5a; color: #e0e0e0; }}
|
||||
.item {{ background: #16213e; border-color: #2a2a4a; }}
|
||||
.item:active {{ background: #2a2a4a; }}
|
||||
.item .meta {{ color: #888; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📁 文件浏览</h1>
|
||||
<div>
|
||||
<a href="/">🏠</a>
|
||||
<a href="#" id="btnRefresh" style="margin-left:6px;">🔄</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="/">根目录</a>
|
||||
{breadcrumb_html}
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchInput" placeholder="搜索文件..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<span>📊 共 <span id="totalItems">0</span> 项</span>
|
||||
<span>📁 <span id="totalDirs">0</span></span>
|
||||
<span>📄 <span id="totalFiles">0</span></span>
|
||||
<span>🎬 <span id="totalVideos">0</span></span>
|
||||
<span>🎵 <span id="totalAudios">0</span></span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="loading" id="loading">
|
||||
<span class="spinner">⏳</span>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<div id="fileList"></div>
|
||||
<div class="empty" id="empty" style="display:none;">
|
||||
<span class="icon">📭</span>
|
||||
<div>此目录为空</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const currentPath = "{path}";
|
||||
let items = [];
|
||||
let searchQuery = "";
|
||||
|
||||
const fileList = document.getElementById("fileList");
|
||||
const loading = document.getElementById("loading");
|
||||
const empty = document.getElementById("empty");
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
const totalItems = document.getElementById("totalItems");
|
||||
const totalDirs = document.getElementById("totalDirs");
|
||||
const totalFiles = document.getElementById("totalFiles");
|
||||
const totalVideos = document.getElementById("totalVideos");
|
||||
const totalAudios = document.getElementById("totalAudios");
|
||||
const btnRefresh = document.getElementById("btnRefresh");
|
||||
|
||||
function formatSize(bytes) {{
|
||||
if (bytes === 0) return "0B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + sizes[i];
|
||||
}}
|
||||
|
||||
function getIcon(item) {{
|
||||
if (item.type === "dir") return "📁";
|
||||
if (item.type === "video") return "🎬";
|
||||
if (item.type === "audio") return "🎵";
|
||||
const ext = item.name.split(".").pop().toLowerCase();
|
||||
const icons = {{
|
||||
"pdf": "📄", "doc": "📝", "docx": "📝", "xls": "📊", "xlsx": "📊",
|
||||
"zip": "📦", "rar": "📦", "7z": "📦",
|
||||
"jpg": "🖼️", "jpeg": "🖼️", "png": "🖼️", "gif": "🖼️",
|
||||
"txt": "📃", "json": "📋", "html": "🌐", "css": "🎨", "js": "⚡"
|
||||
}};
|
||||
return icons[ext] || "📄";
|
||||
}}
|
||||
|
||||
function navigate(path, type) {{
|
||||
if (type === "dir") window.location.href = "/" + path;
|
||||
else if (type === "video" || type === "audio") window.location.href = "/video/" + path;
|
||||
else window.location.href = "/" + path;
|
||||
}}
|
||||
|
||||
function playMedia(path) {{ window.location.href = "/video/" + path; }}
|
||||
function downloadFile(path) {{ window.location.href = "/" + path + "?download=true"; }}
|
||||
|
||||
function render() {{
|
||||
const filtered = items.filter(item => {{
|
||||
if (!searchQuery) return true;
|
||||
return item.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
}});
|
||||
|
||||
totalItems.textContent = filtered.length;
|
||||
totalDirs.textContent = filtered.filter(i => i.type === "dir").length;
|
||||
totalFiles.textContent = filtered.filter(i => i.type === "file").length;
|
||||
totalVideos.textContent = filtered.filter(i => i.type === "video").length;
|
||||
totalAudios.textContent = filtered.filter(i => i.type === "audio").length;
|
||||
|
||||
if (filtered.length === 0) {{
|
||||
fileList.innerHTML = "";
|
||||
empty.style.display = "block";
|
||||
loading.style.display = "none";
|
||||
return;
|
||||
}}
|
||||
empty.style.display = "none";
|
||||
loading.style.display = "none";
|
||||
|
||||
fileList.innerHTML = filtered.map(item => `
|
||||
<div class="item">
|
||||
<span class="icon">${{getIcon(item)}}</span>
|
||||
<div class="info" onclick="navigate('${{item.path}}', '${{item.type}}')">
|
||||
<div class="name">${{item.name}}</div>
|
||||
<div class="meta">
|
||||
${{item.type !== "dir" ? `<span>${{formatSize(item.size)}}</span>` : '<span>📁 文件夹</span>'}}
|
||||
<span>${{item.modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
${{(item.type === "video" || item.type === "audio") ?
|
||||
`<button class="btn-play" onclick="event.stopPropagation(); playMedia('${{item.path}}')">▶</button>` : ''}}
|
||||
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${{item.path}}')">⬇</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
}}
|
||||
|
||||
async function loadData() {{
|
||||
loading.style.display = "block";
|
||||
fileList.innerHTML = "";
|
||||
empty.style.display = "none";
|
||||
|
||||
try {{
|
||||
const url = `/api/filelist?path=${{encodeURIComponent(currentPath)}}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success) {{
|
||||
items = data.items || [];
|
||||
render();
|
||||
}} else {{
|
||||
throw new Error(data.error || "加载失败");
|
||||
}}
|
||||
}} catch (e) {{
|
||||
fileList.innerHTML = `<div class="error">❌ 加载失败: ${{e.message}}</div>`;
|
||||
loading.style.display = "none";
|
||||
}}
|
||||
}}
|
||||
|
||||
let timeout;
|
||||
searchInput.addEventListener("input", function() {{
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {{
|
||||
searchQuery = this.value.trim();
|
||||
render();
|
||||
}}, 200);
|
||||
}});
|
||||
|
||||
document.addEventListener("keydown", function(e) {{
|
||||
if ((e.ctrlKey && e.key === "f") || (e.key === "/" && !e.ctrlKey && !e.metaKey)) {{
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}}
|
||||
if (e.key === "Escape") {{
|
||||
searchInput.value = "";
|
||||
searchQuery = "";
|
||||
render();
|
||||
searchInput.blur();
|
||||
}}
|
||||
}});
|
||||
|
||||
btnRefresh.addEventListener("click", loadData);
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
# ==================== 主路由 ====================
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def serve_file(path=''):
|
||||
try:
|
||||
if not FILESYSTEM_ENABLED:
|
||||
return "文件系统未启用", 403
|
||||
|
||||
full_path = safe_path_join(HTML_ROOT_DIR, path)
|
||||
if full_path is None:
|
||||
return "无效的路径", 403
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return "路径不存在", 404
|
||||
|
||||
# 文件处理
|
||||
if os.path.isfile(full_path):
|
||||
if request.args.get('download') == 'true':
|
||||
return send_file(full_path, as_attachment=True, download_name=os.path.basename(full_path))
|
||||
|
||||
if is_video_file(full_path) or is_audio_file(full_path):
|
||||
return send_file(full_path, mimetype=get_media_mime_type(full_path))
|
||||
|
||||
# 普通文件
|
||||
with open(full_path, 'rb') as f:
|
||||
content = f.read()
|
||||
ext = os.path.splitext(full_path)[1].lower()
|
||||
mime_map = {
|
||||
'.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.txt': 'text/plain',
|
||||
'.pdf': 'application/pdf', '.svg': 'image/svg+xml', '.ico': 'image/x-icon'
|
||||
}
|
||||
return Response(content, mimetype=mime_map.get(ext, 'application/octet-stream'))
|
||||
|
||||
# 目录处理 - 生成面包屑
|
||||
breadcrumb = ''
|
||||
if path:
|
||||
parts = path.split('/')
|
||||
current = ''
|
||||
for i, part in enumerate(parts):
|
||||
if i == len(parts) - 1:
|
||||
breadcrumb += f'<span class="sep">›</span><span class="current">{part}</span>'
|
||||
else:
|
||||
current += part + '/'
|
||||
breadcrumb += f'<span class="sep">›</span><a href="/{current}">{part}</a>'
|
||||
|
||||
# 使用函数生成HTML,避免字符串格式化问题
|
||||
html = get_html_template(path, breadcrumb)
|
||||
return Response(html, mimetype='text/html')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return f"服务器内部错误: {str(e)}", 500
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'filesystem': {'enabled': FILESYSTEM_ENABLED, 'root': HTML_ROOT_DIR if FILESYSTEM_ENABLED else None}
|
||||
})
|
||||
|
||||
# ==================== 主程序 ====================
|
||||
if __name__ == '__main__':
|
||||
host = file_config['server']['host']
|
||||
port = int(file_config['server']['port'])
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("📁 文件托管服务器 (极简版)")
|
||||
print("="*50)
|
||||
print(f"地址: http://{host}:{port}")
|
||||
print(f"目录: {os.path.abspath(HTML_ROOT_DIR) if FILESYSTEM_ENABLED else '未启用'}")
|
||||
print("="*50)
|
||||
print("\n快捷键: / 或 Ctrl+F 搜索, ESC 清空搜索")
|
||||
print("\n")
|
||||
|
||||
if SSL_ENABLED and os.path.exists(SSL_CERT_FILE) and os.path.exists(SSL_KEY_FILE):
|
||||
app.run(host=host, port=port, ssl_context=(SSL_CERT_FILE, SSL_KEY_FILE), debug=False)
|
||||
else:
|
||||
app.run(host=host, port=port, debug=False)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,530 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||||
<title>中国传统色彩 - 移动端适配版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 移动端选项卡优化 */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 14px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
background: none;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 移动端:色彩网格改为2列布局,卡片更易点击 */
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 针对小屏手机强制2列 */
|
||||
@media (max-width: 480px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.tab {
|
||||
font-size: 0.85rem;
|
||||
padding: 12px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕自动调整 */
|
||||
@media (min-width: 481px) and (max-width: 768px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.color-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-card:active {
|
||||
transform: scale(0.97);
|
||||
transition: 0.05s;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.color-code {
|
||||
font-size: 0.7rem;
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Toast 提示优化 - 移动端居中底部 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 40px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transition: all 0.25s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toast {
|
||||
white-space: normal;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
padding: 10px 18px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
/* 配色方案区域 - 移动端卡片布局优化 */
|
||||
#palettes-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#palettes-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-section {
|
||||
margin-bottom: 0;
|
||||
background: #fefefe;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.palette-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin: 0;
|
||||
padding: 12px 16px 8px 16px;
|
||||
background: #fafcff;
|
||||
border-bottom: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0 0 16px 16px;
|
||||
overflow: hidden;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-palette:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.palette-color {
|
||||
flex: 1;
|
||||
min-width: 0; /* 防止flex溢出 */
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
/* 移动端让颜色代码折行更友好 */
|
||||
.palette-color span:first-child {
|
||||
font-size: 0.7rem;
|
||||
font-family: monospace;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 30px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.palette-color span:last-child {
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.9;
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* 说明文字区域移动端优化 */
|
||||
.palettes-desc {
|
||||
background: #f1f5f9;
|
||||
margin: 0 16px 16px 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.palettes-desc strong {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* 增加触摸反馈 */
|
||||
.color-card, .color-palette {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 滚动条隐藏但保持功能 (移动端美观) */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎨 中国传统色彩</h1>
|
||||
<p>轻触颜色块,复制色值</p>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="traditional">中国传统色</button>
|
||||
<button class="tab" data-tab="chinese">中文色彩</button>
|
||||
<button class="tab" data-tab="palettes">色彩搭配</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="traditional">
|
||||
<div class="color-grid" id="traditional-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="chinese">
|
||||
<div class="color-grid" id="chinese-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="palettes">
|
||||
<div class="palettes-desc">
|
||||
💡 <strong>主色</strong>(第一色)品牌核心色 · <strong>辅助色</strong>(第二色)点缀强调 · <strong>中性色</strong>(第三色)文字/背景<br>
|
||||
✨ 点击配色块,一键复制整套色彩代码(三个色值)。
|
||||
</div>
|
||||
<div id="palettes-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast">🎨 颜色代码已复制</div>
|
||||
|
||||
<script>
|
||||
// 原始数据保持不变(完全未修改接口/功能)
|
||||
const traditionalColors = [
|
||||
{ name: '蔚蓝', hex: '#70f3ff' }, { name: '蓝', hex: '#44cef6' }, { name: '碧蓝', hex: '#3eede7' }, { name: '石青', hex: '#1685a9' }, { name: '靛青', hex: '#177cb0' },
|
||||
{ name: '靛蓝', hex: '#065279' }, { name: '花青', hex: '#003472' }, { name: '宝蓝', hex: '#4b5cc4' }, { name: '蓝灰色', hex: '#a1afc9' }, { name: '藏青', hex: '#2e4e7e' },
|
||||
{ name: '藏蓝', hex: '#3b2e7e' }, { name: '黛', hex: '#4a4266' }, { name: '黛绿', hex: '#426666' }, { name: '黛蓝', hex: '#425066' }, { name: '黛紫', hex: '#574266' },
|
||||
{ name: '紫色', hex: '#8d4bbb' }, { name: '紫酱', hex: '#815463' }, { name: '酱紫', hex: '#815476' }, { name: '紫檀', hex: '#4c221b' }, { name: '绀青', hex: '#003371' },
|
||||
{ name: '紫棠', hex: '#56004f' }, { name: '青莲', hex: '#801dae' }, { name: '群青', hex: '#4c8dae' }, { name: '雪青', hex: '#b0a4e3' }, { name: '丁香色', hex: '#cca4e3' },
|
||||
{ name: '藕色', hex: '#edd1d8' }, { name: '藕荷色', hex: '#e4c6d0' }, { name: '朱砂', hex: '#ff461f' }, { name: '火红', hex: '#ff2d51' }, { name: '朱膘', hex: '#f36838' },
|
||||
{ name: '妃色', hex: '#ed5736' }, { name: '洋红', hex: '#ff4777' }, { name: '品红', hex: '#f00056' }, { name: '粉红', hex: '#ffb3a7' }, { name: '桃红', hex: '#f47983' },
|
||||
{ name: '海棠红', hex: '#db5a6b' }, { name: '樱桃色', hex: '#c93756' }, { name: '酡颜', hex: '#f9906f' }, { name: '银红', hex: '#f05654' }, { name: '大红', hex: '#ff2121' },
|
||||
{ name: '石榴红', hex: '#f20c00' }, { name: '绛紫', hex: '#8c4356' }, { name: '绯红', hex: '#c83c23' }, { name: '胭脂', hex: '#9d2933' }, { name: '朱红', hex: '#ff4c00' },
|
||||
{ name: '丹', hex: '#ff4e20' }, { name: '彤', hex: '#f35336' }, { name: '酡红', hex: '#dc3023' }, { name: '炎', hex: '#ff3300' }, { name: '茜色', hex: '#cb3a56' },
|
||||
{ name: '绾', hex: '#a98175' }, { name: '檀', hex: '#b36d61' }, { name: '嫣红', hex: '#ef7a82' }, { name: '枣红', hex: '#c32136' }, { name: '殷红', hex: '#be002f' },
|
||||
{ name: '赫赤', hex: '#c91f37' }, { name: '银朱', hex: '#bf242a' }, { name: '赤', hex: '#c3272b' }, { name: '栗色', hex: '#60281e' }, { name: '玄色', hex: '#622a1d' },
|
||||
{ name: '松花色', hex: '#bce672' }, { name: '柳黄', hex: '#c9dd22' }, { name: '嫩绿', hex: '#bddd22' }, { name: '柳绿', hex: '#afdd22' }, { name: '葱黄', hex: '#a3d900' },
|
||||
{ name: '葱绿', hex: '#9ed900' }, { name: '豆绿', hex: '#9ed048' }, { name: '豆青', hex: '#96ce54' }, { name: '油绿', hex: '#00bc12' }, { name: '葱倩', hex: '#0eb83a' },
|
||||
{ name: '葱青', hex: '#0eb83a' }, { name: '青葱', hex: '#0aa344' }, { name: '石绿', hex: '#16a951' }, { name: '松柏绿', hex: '#21a675' }, { name: '松花绿', hex: '#057748' },
|
||||
{ name: '绿沈', hex: '#0c8918' }, { name: '绿色', hex: '#00e500' }, { name: '草绿', hex: '#40de5a' }, { name: '青翠', hex: '#00e079' }, { name: '青色', hex: '#00e09e' },
|
||||
{ name: '翡翠色', hex: '#3de1ad' }, { name: '碧绿', hex: '#2add9c' }, { name: '玉色', hex: '#2edfa3' }, { name: '缥', hex: '#7fecad' }, { name: '艾绿', hex: '#a4e2c6' },
|
||||
{ name: '碧色', hex: '#1bd1a5' }, { name: '青碧', hex: '#48c0a3' }, { name: '铜绿', hex: '#549688' }, { name: '竹青', hex: '#789262' }, { name: '墨灰', hex: '#758a99' },
|
||||
{ name: '墨色', hex: '#50616d' }, { name: '鸦青', hex: '#424c50' }, { name: '黯', hex: '#41555d' }, { name: '樱草色', hex: '#eaff56' }, { name: '鹅黄', hex: '#fff143' },
|
||||
{ name: '鸭黄', hex: '#faff72' }, { name: '杏黄', hex: '#ffa631' }, { name: '橙黄', hex: '#ffa400' }, { name: '橙色', hex: '#fa8c35' }, { name: '杏红', hex: '#ff8c31' },
|
||||
{ name: '橘黄', hex: '#ff8936' }, { name: '橘红', hex: '#ff7500' }, { name: '藤黄', hex: '#ffb61e' }, { name: '姜黄', hex: '#ffc773' }, { name: '雌黄', hex: '#ffc64b' },
|
||||
{ name: '赤金', hex: '#f2be45' }, { name: '缃色', hex: '#f0c239' }, { name: '雄黄', hex: '#e9bb1d' }, { name: '秋香色', hex: '#d9b611' }, { name: '金色', hex: '#eacd76' },
|
||||
{ name: '牙色', hex: '#eedeb0' }, { name: '枯黄', hex: '#d3b17d' }, { name: '黄栌', hex: '#e29c45' }, { name: '乌金', hex: '#a78e44' }, { name: '昏黄', hex: '#c89b40' },
|
||||
{ name: '棕黄', hex: '#ae7000' }, { name: '琥珀', hex: '#ca6924' }, { name: '棕色', hex: '#b25d25' }, { name: '茶色', hex: '#b35c44' }, { name: '棕红', hex: '#9b4400' },
|
||||
{ name: '赭', hex: '#9c5333' }, { name: '驼色', hex: '#a88462' }, { name: '秋色', hex: '#896c39' }, { name: '棕绿', hex: '#827100' }, { name: '褐色', hex: '#6e511e' },
|
||||
{ name: '棕黑', hex: '#7c4b00' }, { name: '赭色', hex: '#955539' }, { name: '赭石', hex: '#845a33' }, { name: '精白', hex: '#ffffff' }, { name: '银白', hex: '#e9e7ef' },
|
||||
{ name: '铅白', hex: '#f0f0f4' }, { name: '霜色', hex: '#e9f1f6' }, { name: '雪白', hex: '#f0fcff' }, { name: '莹白', hex: '#e3f9fd' }, { name: '月白', hex: '#d6ecf0' },
|
||||
{ name: '象牙白', hex: '#fffbf0' }, { name: '缟', hex: '#f2ecde' }, { name: '鱼肚白', hex: '#fcefe8' }, { name: '白粉', hex: '#fff2df' }, { name: '荼白', hex: '#f3f9f1' },
|
||||
{ name: '鸭卵青', hex: '#e0eee8' }, { name: '素', hex: '#e0f0e9' }, { name: '青白', hex: '#c0ebd7' }, { name: '蟹壳青', hex: '#bbcdc5' }, { name: '花白', hex: '#c2ccd0' },
|
||||
{ name: '老银', hex: '#bacac6' }, { name: '灰色', hex: '#808080' }, { name: '苍色', hex: '#75878a' }, { name: '水色', hex: '#88ada6' }, { name: '黝', hex: '#6b6882' },
|
||||
{ name: '乌色', hex: '#725e82' }, { name: '玄青', hex: '#3d3b4f' }, { name: '乌黑', hex: '#392f41' }, { name: '黎', hex: '#75664d' }, { name: '黧', hex: '#5d513c' },
|
||||
{ name: '黝黑', hex: '#665757' }, { name: '缁色', hex: '#493131' }, { name: '煤黑', hex: '#312520' }, { name: '漆黑', hex: '#161823' }, { name: '黑色', hex: '#000000' }
|
||||
];
|
||||
|
||||
const chineseColors = [
|
||||
{ name: '鸨色', hex: '#f7acbc' }, { name: '踯躅色', hex: '#ef5b9c' }, { name: '桜色', hex: '#feeeed' }, { name: '蔷薇色', hex: '#f05b72' }, { name: '韩红', hex: '#f15b6c' },
|
||||
{ name: '珊瑚色', hex: '#f8aba6' }, { name: '红梅色', hex: '#f69c9f' }, { name: '桃色', hex: '#f58f98' }, { name: '薄柿', hex: '#ca8687' }, { name: '薄红梅', hex: '#f391a9' },
|
||||
{ name: '曙色', hex: '#bd6758' }, { name: '红色', hex: '#d71345' }, { name: '赤丹', hex: '#d64f44' }, { name: '红赤', hex: '#d93a49' }, { name: '臙脂色', hex: '#b3424a' },
|
||||
{ name: '真赭', hex: '#c76968' }, { name: '今様色', hex: '#bb505d' }, { name: '梅染', hex: '#987165' }, { name: '退红色', hex: '#ac6767' }, { name: '苏芳', hex: '#973c3f' },
|
||||
{ name: '茜色', hex: '#b22c46' }, { name: '红', hex: '#a7324a' }, { name: '银朱', hex: '#aa363d' }, { name: '赤', hex: '#ed1941' }, { name: '朱色', hex: '#f26522' },
|
||||
{ name: '洗朱', hex: '#d2553d' }, { name: '红桦色', hex: '#b4534b' }, { name: '红绯', hex: '#ef4136' }, { name: '桦色', hex: '#c63c26' }, { name: '铅丹色', hex: '#f3715c' },
|
||||
{ name: '赭', hex: '#a7573b' }, { name: '绯色', hex: '#aa2116' }, { name: '丹', hex: '#b64533' }, { name: '土', hex: '#b54334' }, { name: '焦香', hex: '#853f04' },
|
||||
{ name: '真红', hex: '#840228' }, { name: '绯', hex: '#7a1723' }, { name: '红海老茶', hex: '#a03939' }, { name: '浅苏芳', hex: '#8a2e3b' }, { name: '鸢色', hex: '#8e453f' },
|
||||
{ name: '小豆色', hex: '#8f4b4a' }, { name: '弁柄色', hex: '#892f1b' }, { name: '栗梅', hex: '#6b2c25' }, { name: '海老茶', hex: '#733a31' }, { name: '深绯', hex: '#54211d' },
|
||||
{ name: '赤铜色', hex: '#78331e' }, { name: '赤褐色', hex: '#53261f' }, { name: '金赤', hex: '#f15a22' }, { name: '赤茶', hex: '#b4533c' }, { name: '赤锖色', hex: '#84331f' },
|
||||
{ name: '黄丹', hex: '#f47a55' }, { name: '赤橙', hex: '#f15a22' }, { name: '柿色', hex: '#f3704b' }, { name: '肉桂色', hex: '#da765b' }, { name: '炼瓦色', hex: '#ae5039' },
|
||||
{ name: '锖色', hex: '#6a3427' }, { name: '桧皮色', hex: '#8f4b38' }, { name: '栗色', hex: '#8e3e1f' }, { name: '黄赤', hex: '#f36c21' }, { name: '代赭', hex: '#b4532a' },
|
||||
{ name: '骆驼色', hex: '#b7704f' }, { name: '黄茶', hex: '#de773f' }, { name: '洗柿', hex: '#c99979' }, { name: '赤白橡', hex: '#deab8a' }, { name: '肌色', hex: '#fedcbd' },
|
||||
{ name: '橙色', hex: '#f47920' }, { name: '灰茶', hex: '#905a3d' }, { name: '茶色', hex: '#8f4b2e' }, { name: '桦茶色', hex: '#87481f' }, { name: '枯茶', hex: '#5f3c23' },
|
||||
{ name: '焦茶', hex: '#6b473c' }, { name: '柑子色', hex: '#faa755' }, { name: '杏色', hex: '#fab27b' }, { name: '蜜柑色', hex: '#f58220' }, { name: '褐色', hex: '#843900' },
|
||||
{ name: '路考茶', hex: '#905d1d' }, { name: '饴色', hex: '#8a5d19' }, { name: '丁子色', hex: '#8c531b' }, { name: '丁子茶', hex: '#826858' }, { name: '黄栌', hex: '#64492b' },
|
||||
{ name: '土器色', hex: '#ae6642' }, { name: '黄枯茶', hex: '#56452d' }, { name: '狐色', hex: '#96582a' }, { name: '黄橡', hex: '#705628' }, { name: '银煤竹', hex: '#4a3113' },
|
||||
{ name: '涅色', hex: '#412f1f' }, { name: '胡桃色', hex: '#845538' }, { name: '香色', hex: '#8e7437' }, { name: '国防色', hex: '#69541b' }, { name: '练色', hex: '#d5c59f' },
|
||||
{ name: '肉色', hex: '#cd9a5b' }, { name: '人色', hex: '#cd9a5b' }, { name: '土色', hex: '#b36d41' }, { name: '小麦色', hex: '#df9464' }, { name: '琥珀色', hex: '#b76f40' },
|
||||
{ name: '木兰色', hex: '#ad8b3d' }, { name: '栀子色', hex: '#dea32c' }, { name: '朽叶', hex: '#d1923f' }, { name: '萱草色', hex: '#c88400' }, { name: '黄金', hex: '#c37e00' },
|
||||
{ name: '金色', hex: '#c37e00' }, { name: '金茶', hex: '#e0861a' }, { name: '卵色', hex: '#ffce7b' }, { name: '山吹色', hex: '#fcaf17' }, { name: '黄土色', hex: '#ba8448' },
|
||||
{ name: '朽叶色', hex: '#896a45' }, { name: '空五倍子色', hex: '#76624c' }, { name: '莺茶', hex: '#6d5826' }, { name: '向日葵色', hex: '#ffc20e' }, { name: '郁金色', hex: '#fdb933' },
|
||||
{ name: '砂色', hex: '#d3c6a6' }, { name: '芥子色', hex: '#c7a252' }, { name: '淡黄', hex: '#dec674' }, { name: '亜麻色', hex: '#b69968' }, { name: '枯色', hex: '#c1a173' }
|
||||
];
|
||||
|
||||
const palettes = [
|
||||
{ title: '清新自然', colors: ['#FFFFCC', '#CCFFFF', '#FFCCCC'] }, { title: '雅致灰调', colors: ['#99CCCC', '#FFCC99', '#FFCCCC'] }, { title: '粉紫梦境', colors: ['#FF9999', '#996699', '#FFCCCC'] },
|
||||
{ title: '大地色系', colors: ['#CC9999', '#FFFFCC', '#CCCC99'] }, { title: '柔和暖阳', colors: ['#FFCCCC', '#FFFF99', '#CCCCFF'] }, { title: '海蓝印象', colors: ['#0099CC', '#CCCCCC', '#FF6666'] },
|
||||
{ title: '珊瑚橘粉', colors: ['#FF9966', '#FF6666', '#FFCCCC'] }, { title: '木质复古', colors: ['#CC9966', '#666666', '#CC9999'] }, { title: '翠绿春意', colors: ['#FF6666', '#FFFF66', '#99CC66'] },
|
||||
{ title: '深海静寂', colors: ['#CC3333', '#CCCCCC', '#003366'] }, { title: '紫罗兰夜', colors: ['#993333', '#CCCC00', '#663366'] }, { title: '燕麦拿铁', colors: ['#CCCC99', '#666666', '#CC9999'] },
|
||||
{ title: '柠檬水岸', colors: ['#FF6666', '#FFFF00', '#0066CC'] }, { title: '红黑经典', colors: ['#CC0033', '#333333', '#CCCC00'] }, { title: '森林与酒', colors: ['#336633', '#990033', '#FFCC99'] }
|
||||
];
|
||||
|
||||
function showToast(msg) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg || '🎨 颜色代码已复制';
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
function copyToClipboard(text, customMsg) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(customMsg || `✅ 已复制: ${text}`);
|
||||
}).catch(() => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
showToast(customMsg || `📋 已复制: ${text}`);
|
||||
});
|
||||
}
|
||||
|
||||
function getContrastColor(hex) {
|
||||
let r = parseInt(hex.slice(1, 3), 16);
|
||||
let g = parseInt(hex.slice(3, 5), 16);
|
||||
let b = parseInt(hex.slice(5, 7), 16);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness > 128 ? '#1e1e2f' : '#ffffff';
|
||||
}
|
||||
|
||||
function createColorCard(color) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'color-card';
|
||||
card.onclick = () => copyToClipboard(color.hex, `🎨 ${color.name} → ${color.hex}`);
|
||||
const textColor = getContrastColor(color.hex);
|
||||
card.innerHTML = `
|
||||
<div class="color-swatch" style="background: ${color.hex}; color: ${textColor};">${color.hex}</div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">${color.name}</div>
|
||||
<div class="color-code">${color.hex}</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function createPalette(palette) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'palette-section';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'palette-title';
|
||||
title.textContent = palette.title;
|
||||
const paletteDiv = document.createElement('div');
|
||||
paletteDiv.className = 'color-palette';
|
||||
paletteDiv.onclick = () => {
|
||||
const allCodes = palette.colors.join(' ');
|
||||
copyToClipboard(allCodes, `🎨 配色方案「${palette.title}」已复制: ${allCodes}`);
|
||||
};
|
||||
const roles = ['主色', '辅色', '中性'];
|
||||
palette.colors.forEach((color, idx) => {
|
||||
const colorDiv = document.createElement('div');
|
||||
colorDiv.className = 'palette-color';
|
||||
colorDiv.style.background = color;
|
||||
colorDiv.style.color = getContrastColor(color);
|
||||
const codeSpan = document.createElement('span');
|
||||
codeSpan.textContent = color;
|
||||
const roleSpan = document.createElement('span');
|
||||
roleSpan.textContent = roles[idx] || '';
|
||||
colorDiv.appendChild(codeSpan);
|
||||
colorDiv.appendChild(roleSpan);
|
||||
paletteDiv.appendChild(colorDiv);
|
||||
});
|
||||
section.appendChild(title);
|
||||
section.appendChild(paletteDiv);
|
||||
return section;
|
||||
}
|
||||
|
||||
function init() {
|
||||
const traditionalGrid = document.getElementById('traditional-grid');
|
||||
traditionalColors.forEach(c => traditionalGrid.appendChild(createColorCard(c)));
|
||||
const chineseGrid = document.getElementById('chinese-grid');
|
||||
chineseColors.forEach(c => chineseGrid.appendChild(createColorCard(c)));
|
||||
const palettesContainer = document.getElementById('palettes-container');
|
||||
palettes.forEach(p => palettesContainer.appendChild(createPalette(p)));
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
const tabId = tab.dataset.tab;
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,204 @@
|
||||
const msgTip = document.getElementById('msgTip');
|
||||
const pwdTip = document.getElementById('pwdTip');
|
||||
const emlTip = document.getElementById('emlTip');
|
||||
const agreeTip = document.getElementById('agreeTip');
|
||||
const form = document.getElementById('form');
|
||||
|
||||
const usernameInput = document.getElementById('loginUsername');
|
||||
const passwordInput = document.getElementById('loginPassword');
|
||||
const emailInput = document.getElementById('loginEmail');
|
||||
const agreeBox = document.getElementById('agreeBox');
|
||||
const protocol = document.getElementById('agreement');
|
||||
const submit = document.getElementById('loginBtn');
|
||||
|
||||
const resetUser = document.getElementById('resetUser');
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
|
||||
resetUser.addEventListener('click', function() {
|
||||
clearTipStyle();
|
||||
})
|
||||
|
||||
function clearTipStyle() {
|
||||
msgTip.innerHTML = '';
|
||||
pwdTip.innerHTML = '';
|
||||
agreeTip.innerHTML = '';
|
||||
emlTip.innerHTML = '';
|
||||
|
||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
agreeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
protocol.classList.remove('tip-red');
|
||||
protocol.classList.remove('tip-green');
|
||||
}
|
||||
clearTipStyle();
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkUsername() {
|
||||
const val = usernameInput.value.trim();
|
||||
msgTip.innerHTML = '';
|
||||
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
msgTip.innerHTML = '用户名不可为空';
|
||||
msgTip.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
if (val.length > 20) {
|
||||
msgTip.innerHTML = '用户名不得长于20字符!';
|
||||
msgTip.classList.add('tip-yellow');
|
||||
return false;
|
||||
}
|
||||
|
||||
msgTip.innerHTML = '√ 用户名格式正确';
|
||||
msgTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkPwd() {
|
||||
const val = passwordInput.value.trim();
|
||||
pwdTip.innerHTML = '';
|
||||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
pwdTip.innerHTML = '密码不可为空!';
|
||||
pwdTip.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
|
||||
pwdTip.innerHTML = '河狸';
|
||||
pwdTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string|false}
|
||||
*/
|
||||
function checkEml() {
|
||||
const val = emailInput.value.trim();
|
||||
const emlStand = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
emlTip.innerHTML = '';
|
||||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||||
|
||||
if (!val) {
|
||||
emlTip.innerHTML = '邮箱不可为空!';
|
||||
emlTip.classList.add('tip-red');
|
||||
return false;
|
||||
} else if (!emlStand.test(val)) {
|
||||
emlTip.innerHTML = '邮箱格式不正确!';
|
||||
emlTip.classList.add('tip-yellow');
|
||||
return false;
|
||||
}
|
||||
|
||||
emlTip.innerHTML = '河狸';
|
||||
emlTip.classList.add('tip-green');
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkAgree() {
|
||||
agreeTip.innerHTML = '';
|
||||
agreeTip.classList.remove('tip-red');
|
||||
protocol.classList.remove('tip-green', 'tip-red');
|
||||
if (!agreeBox.checked) {
|
||||
agreeTip.innerHTML = '请同意用户协议!';
|
||||
agreeTip.classList.add('tip-red');
|
||||
protocol.classList.add('tip-red');
|
||||
return false;
|
||||
}
|
||||
protocol.classList.add('tip-green');
|
||||
return true;
|
||||
}
|
||||
|
||||
usernameInput.addEventListener('focus', checkUsername);
|
||||
passwordInput.addEventListener('focus', checkPwd);
|
||||
|
||||
usernameInput.addEventListener('input', checkUsername);
|
||||
passwordInput.addEventListener('input', checkPwd);
|
||||
|
||||
usernameInput.addEventListener('blur', checkUsername);
|
||||
passwordInput.addEventListener('blur', checkPwd);
|
||||
|
||||
emailInput.addEventListener('focus', checkEml);
|
||||
emailInput.addEventListener('input', checkEml);
|
||||
emailInput.addEventListener('blur', checkEml);
|
||||
|
||||
agreeBox.addEventListener('change', checkAgree);
|
||||
|
||||
registerBtn.addEventListener('click', function() {
|
||||
location.href = 'register.html';
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
function htmlEscape(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
clearTipStyle();
|
||||
|
||||
const userOk = checkUsername();
|
||||
const pwdOk = checkPwd();
|
||||
const emlOk = checkEml();
|
||||
const agreeOk = checkAgree();
|
||||
|
||||
if (!(userOk && pwdOk && emlOk && agreeOk)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsonBody = JSON.stringify({
|
||||
username: userOk,
|
||||
password: pwdOk
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: jsonBody
|
||||
});
|
||||
|
||||
const responseText = await res.text();
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = JSON.parse(responseText);
|
||||
} catch (err) {
|
||||
throw new Error(`服务器返回了非 JSON 内容(HTTP ${res.status}):${responseText.slice(0, 100)}`);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.message || `登录失败(HTTP ${res.status})`);
|
||||
}
|
||||
|
||||
if (result.code === 0) {
|
||||
alert(`${result.message},账号:${result.data.username}`);
|
||||
sessionStorage.setItem('token', result.data.token);
|
||||
location.href = 'main.html';
|
||||
} else {
|
||||
alert(result.message || '登录失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message || '接口请求失败,请检查后端服务是否启动');
|
||||
console.error('登录请求异常:', err);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
let sendCode = "";
|
||||
let codeTarget = "";
|
||||
|
||||
getCode.onclick = function () {
|
||||
const phoneMail = email.value.trim();
|
||||
|
||||
if (phoneMail === "") {
|
||||
alert("请先填写邮箱号!");
|
||||
return;
|
||||
}
|
||||
|
||||
sendCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
codeTarget = phoneMail;
|
||||
|
||||
alert(`验证码已发送至 ${phoneMail},您的验证码:${sendCode}`);
|
||||
};
|
||||
|
||||
regForm.onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const uname = username.value.trim();
|
||||
const emailVal = email.value.trim();
|
||||
const codeVal = code.value.trim();
|
||||
const pwdVal = psw.value;
|
||||
const repwdVal = repwd.value;
|
||||
|
||||
if (uname === "") {
|
||||
alert("请填写用户名");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailVal === "") {
|
||||
alert("请填写邮箱号");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendCode === "") {
|
||||
alert("请先点击获取验证码!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailVal !== codeTarget) {
|
||||
alert("邮箱已修改,请重新获取验证码!");
|
||||
sendCode = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^\d{6}$/.test(codeVal)) {
|
||||
alert("验证码必须是6位数字");
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeVal !== sendCode) {
|
||||
alert("验证码错误,请核对!");
|
||||
code.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (pwdVal.length < 6) {
|
||||
alert("密码至少6位字符");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pwdVal !== repwdVal) {
|
||||
alert("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
|
||||
alert("注册成功!");
|
||||
regForm.reset();
|
||||
sendCode = "";
|
||||
codeTarget = "";
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>登录</title>
|
||||
<style>
|
||||
.tip-red {color:red;}
|
||||
.tip-yellow {color: orange;}
|
||||
.tip-green {color: greenyellow;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>用户登录</h2>
|
||||
<form action="" id="form" method="post">
|
||||
<div>
|
||||
<label>用户名/推荐邮箱:</label>
|
||||
<input type="text" id="loginUsername" name="username" placeholder="请输入用户名">
|
||||
<span id="msgTip" ></span>
|
||||
<br>
|
||||
<label>密码:</label>
|
||||
<input type="password" id="loginPassword" name="password" placeholder="请输入密码">
|
||||
<span id="pwdTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<label>验证邮箱:</label>
|
||||
<input type="email" id="loginEmail" name="email" placeholder="请输入验证邮箱">
|
||||
<span id="emlSub">
|
||||
<input type="submit" value="发送验证码">
|
||||
</span>
|
||||
<span id="emlTip"></span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" id="registerBtn">注册</button>
|
||||
<input type="submit" id="loginBtn" value="登录">
|
||||
<input type="reset" id="resetUser" value="重置">
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="agreeBox" name="agree" value="access">
|
||||
<label id="agreement">同意该系统相关用户使用条例</label>
|
||||
<span id="agreeTip"></span>
|
||||
</div>
|
||||
</form>
|
||||
<script src="js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- 移动端适配核心标签 -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>DNFS · 树状笔记</title>
|
||||
<style></style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 文章主标题 -->
|
||||
<div id="article-title">
|
||||
<h1>
|
||||
<p>Python</p>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 文章详情 -->
|
||||
<div id="publish-time">
|
||||
<p>发布时间:2026-07-11</p>
|
||||
</div>
|
||||
<div>
|
||||
<span id="visit-count">浏览:126</span>
|
||||
<span id="like-count">点赞:28</span>
|
||||
<span id="share-count">分享:7</span>
|
||||
</div>
|
||||
|
||||
<!-- 文章正文-->
|
||||
<div id="article-content">
|
||||
<p>
|
||||
Python 的 <code>socket</code>
|
||||
模块是网络编程的核心,其函数可分为<strong>核心套接字操作</strong>、<strong>套接字选项设置</strong>、<strong>辅助工具函数</strong>三大类。本文将逐一拆解所有函数的语法、参数、用法和实战示例,覆盖从基础到高级的所有场景。
|
||||
</p>
|
||||
|
||||
<h2>一、模块初始化与核心对象</h2>
|
||||
<h3>1. socket.socket():创建套接字对象</h3>
|
||||
<p><strong>语法</strong>:</p>
|
||||
<pre><code>socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)</code></pre>
|
||||
<p><strong>作用</strong>:创建一个套接字对象,是所有网络操作的基础。</p>
|
||||
<p><strong>参数说明</strong>:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>参数</th>
|
||||
<th>取值</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>family</code></td>
|
||||
<td><code>AF_INET</code>(默认)</td>
|
||||
<td>IPv4 协议族(最常用)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>AF_INET6</code></td>
|
||||
<td>IPv6 协议族</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>AF_UNIX</code></td>
|
||||
<td>Unix 域套接字(本地进程通信)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>type</code></td>
|
||||
<td><code>SOCK_STREAM</code>(默认)</td>
|
||||
<td>TCP 套接字(面向连接、可靠)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>SOCK_DGRAM</code></td>
|
||||
<td>UDP 套接字(无连接、数据报)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><code>SOCK_RAW</code></td>
|
||||
<td>原始套接字(可操作 IP 层,用于抓包)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>proto</code></td>
|
||||
<td>0(默认)</td>
|
||||
<td>协议编号,默认自动匹配 type(TCP=6,UDP=17)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fileno</code></td>
|
||||
<td>None(默认)</td>
|
||||
<td>复用已有文件描述符,无需手动指定</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="other-recommend">
|
||||
<h3>其他推荐</h3>
|
||||
</div>
|
||||
<div class="recommend-list" id="recommend-list">
|
||||
<ul>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
<li><a href="https://www.baidu.com">百度推荐</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<title>注册页面</title>
|
||||
</head>
|
||||
<body>
|
||||
<h3>注册页面</h3>
|
||||
<form>
|
||||
用户名/推荐邮箱:<input type="text" id="username" placeholder="请输入用户名"><br>
|
||||
邮箱号:<input type="text" id="email" placeholder="请输入邮箱号"><br>
|
||||
<div class="code-box">验证码:
|
||||
<input type="text" id="code" placeholder="请输入6位验证码">
|
||||
<input type="button" id="getCode" value="获取验证码">
|
||||
</div>
|
||||
密码:<input type="password" id="psw"><br>
|
||||
验证密码:<input type="password" id="repwd" placeholder="请再次输入密码"><br>
|
||||
<input type="submit" value="提交">
|
||||
<input type="button" value="返回登录" onclick="location.href='login.html'">
|
||||
<script src="register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
nuitka
|
||||
Flask>=2.3.0,<3.0.0
|
||||
Flask-CORS>=4.0.0,<5.0.0
|
||||
Flask-SQLAlchemy>=3.1.0,<4.0.0
|
||||
Flask-Limiter>=3.3.0,<4.0.0
|
||||
Flask-SocketIO>=5.3.0,<6.0.0
|
||||
python-socketio>=5.10.0,<6.0.0
|
||||
Werkzeug>=2.3.0,<3.0.0
|
||||
requests>=2.31.0,<3.0.0
|
||||
websocket-client>=1.6.0,<2.0.0
|
||||
python-engineio>=4.7.0,<5.0.0
|
||||
Reference in new issue
Block a user