Files
python_script/99demo/pin-img/pin-tu.py
2026-06-16 03:30:57 +08:00

687 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app.py
from flask import Flask, render_template_string, send_from_directory, request
import os
import re
from werkzeug.utils import secure_filename
# --- 配置 ---
# 请在这里修改为你存放图片的文件夹路径
ROOT_IMAGE_FOLDER = r'\\192.168.31.92\ubuntuShare2T\学习资料' # 使用原始字符串以避免转义问题
PORT = 80 # 你可以修改这个端口号如果5000被占用了
# --- 配置结束 ---
app = Flask(__name__)
def atoi(text):
"""辅助函数:将文本中的数字部分转换为整数,用于排序"""
return int(text) if text.isdigit() else text
def natural_keys(text):
"""
辅助函数:生成用于自然排序的键。
例如: '1002' -> ['1002'] (其中 '1002' 被转换为整数)
'img_10' -> ['img_', 10]
这样可以确保 '1001.jpg' 排在 '1002.png' 之前。
"""
return [atoi(c) for c in re.split(r'(\d+)', text)]
def find_media_in_folder(folder_path):
"""在指定文件夹中查找所有图片和视频文件"""
allowed_image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
allowed_video_exts = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mkv', '.mpg', '.mpeg', '.3gp',
'.3g2'}
try:
files = os.listdir(folder_path)
except PermissionError:
print(f"权限不足,无法访问: {folder_path}")
return [], []
image_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
1].lower() in allowed_image_exts]
video_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
1].lower() in allowed_video_exts]
return sorted(image_files, key=natural_keys), sorted(video_files, key=natural_keys)
def get_subfolders(folder_path):
"""获取指定文件夹下的所有子文件夹"""
try:
items = os.listdir(folder_path)
except PermissionError:
print(f"权限不足,无法访问: {folder_path}")
return []
subfolders = [item for item in items if os.path.isdir(os.path.join(folder_path, item))]
return sorted(subfolders, key=natural_keys)
@app.route('/')
def index():
# 默认访问根目录subpath为空字符串
return list_folder_content('')
@app.route('/browse/')
@app.route('/browse/<path:subpath>')
def list_folder_content(subpath=''):
"""
递归浏览文件夹内容的主函数
subpath: URL中传递的子路径
"""
# 从查询参数获取显示模式,默认为 'list'
mode = request.args.get('mode', 'list')
if mode not in ['list', 'grid', 'manga']:
mode = 'list'
# 构建当前要浏览的物理路径
# 注意:这里使用全局变量 ROOT_IMAGE_FOLDER
current_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, subpath))
# 检测是否为漫画路径
is_manga_path = '漫画' in subpath
# 安全检查,防止路径穿越
if not current_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
return "Access Denied", 403
# 查找当前文件夹下的媒体文件和子文件夹
image_files, video_files = find_media_in_folder(current_path)
subfolders = get_subfolders(current_path)
# 构建面包屑导航
path_parts = [p for p in subpath.split('/') if p]
breadcrumbs = [{'name': 'Home', 'url': '/'}]
cumulative_path = ''
for part in path_parts:
cumulative_path += part + '/'
breadcrumbs.append({'name': part, 'url': f'/browse/{cumulative_path.rstrip("/")}'})
html_template = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件浏览器 - {{ current_path_name }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
}
/* 顶部地址栏 - Windows风格 */
.address-bar {
display: flex;
align-items: center;
background: #fff;
border-bottom: 1px solid #ddd;
padding: 8px 16px;
gap: 2px;
flex-wrap: wrap;
}
.address-bar a, .address-bar span.current {
padding: 4px 10px;
border-radius: 3px;
font-size: 14px;
color: #333;
text-decoration: none;
}
.address-bar a:hover {
background: #e5e5e5;
}
.address-bar span.current {
color: #000;
font-weight: 600;
}
.address-bar .sep {
color: #999;
font-size: 14px;
padding: 0 2px;
user-select: none;
}
.address-bar .home-btn {
font-size: 16px;
padding: 4px 8px;
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid #ddd;
gap: 12px;
}
.mode-btn {
padding: 5px 14px;
border: 1px solid #ccc;
background: #fff;
border-radius: 3px;
cursor: pointer;
font-size: 13px;
text-decoration: none;
color: #333;
}
.mode-btn:hover {
background: #f0f0f0;
}
.mode-btn.active {
background: #0078d4;
color: #fff;
border-color: #0078d4;
}
/* 主内容区 */
.main-content {
padding: 16px;
max-width: 1400px;
margin: 0 auto;
}
.section {
margin-bottom: 28px;
}
.section-title {
font-size: 14px;
color: #555;
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
/* 网格模式 */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.grid-item {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
.grid-item:hover {
border-color: #0078d4;
background: #f0f8ff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.grid-item .icon {
font-size: 48px;
margin-bottom: 8px;
line-height: 1;
}
.grid-item img.thumb {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 8px;
}
.grid-item .name {
font-size: 12px;
word-break: break-all;
line-height: 1.4;
max-height: 2.8em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 列表模式 */
.list-container {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
}
.list-item {
display: flex;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid #f0f0f0;
text-decoration: none;
color: #333;
transition: background 0.15s;
}
.list-item:last-child { border-bottom: none; }
.list-item:hover {
background: #f5f5f5;
}
.list-item .icon {
font-size: 24px;
margin-right: 12px;
width: 28px;
text-align: center;
flex-shrink: 0;
}
.list-item .thumb {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
margin-right: 12px;
flex-shrink: 0;
border: 1px solid #eee;
}
.list-item .info {
flex: 1;
min-width: 0;
}
.list-item .name {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-item .type {
font-size: 12px;
color: #999;
margin-top: 2px;
}
/* Modal / Lightbox */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
padding: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.modal-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
align-items: center;
}
.modal-content img {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.modal-content video {
max-width: 90vw;
max-height: 85vh;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
background: #000;
}
.modal-close {
position: absolute;
top: -40px;
right: 0;
color: #fff;
font-size: 32px;
cursor: pointer;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
user-select: none;
}
.modal-close:hover {
background: rgba(255,255,255,0.2);
}
.modal-title {
color: #fff;
margin-top: 10px;
font-size: 14px;
text-align: center;
max-width: 80vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.img-trigger, .video-trigger {
cursor: pointer;
}
/* 漫画模式 */
.manga-container {
display: flex;
flex-direction: column;
align-items: center;
background: #1a1a1a;
padding: 0;
border-radius: 0;
}
.manga-item {
width: 100%;
max-width: 900px;
line-height: 0;
}
.manga-item img {
width: 100%;
height: auto;
display: block;
border: none;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<!-- 地址栏 -->
<div class="address-bar">
<a href="/" class="home-btn" title="主页">🏠</a>
{% for crumb in breadcrumbs %}
{% if not loop.first %}
<span class="sep"></span>
{% if loop.last %}
<span class="current">{{ crumb.name }}</span>
{% else %}
<a href="{{ crumb.url }}">{{ crumb.name }}</a>
{% endif %}
{% endif %}
{% endfor %}
</div>
<!-- 工具栏 -->
<div class="toolbar">
<a href="?mode=list" class="mode-btn {% if mode == 'list' %}active{% endif %}">📋 列表</a>
<a href="?mode=grid" class="mode-btn {% if mode == 'grid' %}active{% endif %}">⊞ 网格</a>
{% if is_manga_path %}
<a href="?mode=manga" class="mode-btn {% if mode == 'manga' %}active{% endif %}">📖 漫画</a>
{% endif %}
</div>
<div class="main-content">
<!-- 文件夹 -->
{% if subfolders %}
<div class="section">
<div class="section-title"><span>📁</span> 文件夹 <span style="color:#999;font-weight:400;">({{ subfolders|length }})</span></div>
{% if mode == 'grid' %}
<div class="grid-container">
{% for folder in subfolders %}
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="grid-item">
<div class="icon">📁</div>
<div class="name">{{ folder }}</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="list-container">
{% for folder in subfolders %}
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="list-item">
<div class="icon">📁</div>
<div class="info">
<div class="name">{{ folder }}</div>
<div class="type">文件夹</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<!-- 图片 -->
{% if images %}
<div class="section">
<div class="section-title"><span>📷</span> 图片 <span style="color:#999;font-weight:400;">({{ images|length }})</span></div>
{% if mode == 'manga' %}
<div class="manga-container">
{% for image in images %}
<div class="manga-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
<img src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
</div>
{% endfor %}
</div>
{% elif mode == 'grid' %}
<div class="grid-container">
{% for image in images %}
<div class="grid-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
<div class="name">{{ image }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="list-container">
{% for image in images %}
<div class="list-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="" loading="lazy">
<div class="info">
<div class="name">{{ image }}</div>
<div class="type">图片文件</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<!-- 视频 -->
{% if videos %}
<div class="section">
<div class="section-title"><span>🎬</span> 视频 <span style="color:#999;font-weight:400;">({{ videos|length }})</span></div>
{% if mode == 'grid' %}
<div class="grid-container">
{% for video in videos %}
<div class="grid-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
<div class="icon">🎬</div>
<div class="name">{{ video }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="list-container">
{% for video in videos %}
<div class="list-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
<div class="icon">🎬</div>
<div class="info">
<div class="name">{{ video }}</div>
<div class="type">视频文件</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- 图片/视频预览弹窗 -->
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-close" onclick="closeModal()">&times;</div>
<img id="modalImg" style="display:none;" alt="">
<video id="modalVideo" style="display:none;" controls playsinline></video>
<div class="modal-title" id="modalTitle"></div>
</div>
</div>
<script>
const overlay = document.getElementById('modalOverlay');
const modalImg = document.getElementById('modalImg');
const modalVideo = document.getElementById('modalVideo');
const modalTitle = document.getElementById('modalTitle');
function openImage(src, name) {
modalVideo.style.display = 'none';
modalVideo.pause();
modalVideo.src = '';
modalImg.style.display = 'block';
modalImg.src = src;
modalTitle.textContent = name || '';
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function openVideo(src, name) {
modalImg.style.display = 'none';
modalImg.src = '';
modalVideo.style.display = 'block';
modalVideo.src = src;
modalVideo.play();
modalTitle.textContent = name || '';
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(e) {
if (e && e.target !== overlay && !e.target.classList.contains('modal-close')) return;
overlay.classList.remove('active');
modalVideo.pause();
modalVideo.src = '';
modalImg.src = '';
document.body.style.overflow = '';
}
document.querySelectorAll('.img-trigger').forEach(el => {
el.addEventListener('click', () => openImage(el.dataset.src, el.dataset.name));
});
document.querySelectorAll('.video-trigger').forEach(el => {
el.addEventListener('click', () => openVideo(el.dataset.src, el.dataset.name));
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
});
</script>
</body>
</html>
'''
# 获取当前路径的名称用于显示
current_path_name = os.path.basename(current_path) if current_path != ROOT_IMAGE_FOLDER else 'Root Directory'
return render_template_string(
html_template,
images=image_files,
videos=video_files,
subfolders=subfolders,
current_path_name=current_path_name,
current_subpath=subpath + '/' if subpath else '', # 传递给模板,用于构建文件路径
breadcrumbs=breadcrumbs,
mode=mode, # 将模式传递给模板
is_manga_path=is_manga_path
)
# 新增:用于在新页面播放视频的路由
@app.route('/play_video/<path:full_path>')
def play_video(full_path):
"""
在新页面播放单个视频
full_path: 视频文件的完整路径(包含子文件夹)
"""
# 安全检查
full_file_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, full_path))
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
return "Access Denied", 403
# 获取视频文件名
video_filename = os.path.basename(full_path)
# 将 video_url 的生成移到模板内部
video_player_template = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在播放: {{ video_filename }}</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #000; /* 黑色背景 */
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* 全屏高度 */
}
.video-container {
width: 100%;
height: 100%;
max-width: 90%; /* 最大宽度 */
max-height: 90vh; /* 最大高度 */
}
.video-player {
width: 100%;
height: 100%;
object-fit: contain; /* 保持视频比例 */
}
</style>
</head>
<body>
<div class="video-container">
<video class="video-player" controls autoplay playsinline preload="none">
<!-- 在模板内部使用 url_for -->
<source src="{{ url_for('serve_media', full_path=full_path) }}" type="video/{{ video_filename.split('.')[-1].lower() }}">
您的浏览器不支持视频播放。
</video>
</div>
</body>
</html>
'''
# 注意:传递给 render_template_string 的变量,可以在模板内部直接使用
return render_template_string(
video_player_template,
video_filename=video_filename,
full_path=full_path # 将 full_path 也传递给模板,以便在模板内使用 url_for
)
# 统一的媒体文件服务路由,处理图片和视频
@app.route('/media_file/<path:full_path>')
def serve_media(full_path):
"""
提供媒体文件(图片/视频)服务
full_path: 包含子文件夹路径和文件名
"""
# 从完整路径中分离出文件夹和文件名
folder_path = os.path.dirname(full_path)
filename = os.path.basename(full_path)
# 构建完整的物理路径
base_dir = os.path.join(ROOT_IMAGE_FOLDER, folder_path)
# 安全检查
full_file_path = os.path.normpath(os.path.join(base_dir, filename))
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
return "Access Denied", 403
return send_from_directory(base_dir, filename)
if __name__ == '__main__':
# 确保根图片文件夹存在
if not os.path.exists(ROOT_IMAGE_FOLDER):
print(f"错误:找不到根图片文件夹 '{ROOT_IMAGE_FOLDER}'")
exit(1)
print(f"Web服务即将启动请访问 http://127.0.0.1:{PORT}")
print(f"根目录路径: {os.path.abspath(ROOT_IMAGE_FOLDER)}")
app.run(debug=True, host='0.0.0.0', port=PORT)