新增 word 文件预览功能
CI — Docker Build & Push / Build & Push Image (push) Failing after 14m37s

This commit is contained in:
2026-04-28 11:11:01 +09:00
parent bd1acebcf3
commit 9c38f9ed9a
6 changed files with 80 additions and 3 deletions
+3 -1
View File
@@ -25,7 +25,9 @@
"Bash(git -C /d/3.Project/HTY1024/ai-app-database commit -m 'fix\\(ci\\): 将 Secret 名从 GITEA_TOKEN 改为 REGISTRY_TOKEN:*)",
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -3)",
"Bash(git -C /d/3.Project/HTY1024/ai-app-database add scripts/ .gitattributes)",
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -4)"
"Bash(git -C /d/3.Project/HTY1024/ai-app-database log --oneline -4)",
"Bash(xargs ls:*)",
"Bash(find /d/3.Project/HTY1024/ai-app-database/app -name \"__init__.py\" -exec head -30 {} \\\\;)"
]
}
}
+32 -1
View File
@@ -404,7 +404,7 @@ def progress(resource_id):
@resources_bp.route('/<int:resource_id>/preview')
@login_required
def preview(resource_id):
"""直接返回文件内容(用于 iframe预览)"""
"""直接返回文件内容(用于文本/Word预览)"""
res = db.get_or_404(Resource, resource_id)
if res.user_id != current_user.id and not current_user.is_admin:
abort(403)
@@ -413,6 +413,37 @@ def preview(resource_id):
abs_path = os.path.join(current_app.root_path, 'static', res.file_path)
if not os.path.exists(abs_path):
abort(404)
ext = (res.original_name or res.filename or '').rsplit('.', 1)[-1].lower()
# Word 文档(.docx)使用 mammoth 转换为 HTML
if ext == 'docx':
try:
import mammoth
with open(abs_path, 'rb') as f:
result = mammoth.convert_to_html(f)
return Response(result.value, mimetype='text/html; charset=utf-8')
except ImportError:
return Response(
'<div style="padding:1rem;color:#dc3545">'
'服务器未安装 mammoth 库,无法预览 .docx 文件。请运行 '
'<code>pip install mammoth</code> 后重试。</div>',
mimetype='text/html; charset=utf-8'
)
except Exception as e:
return Response(
f'<div style="padding:1rem;color:#dc3545">无法解析 Word 文档:{e}</div>',
mimetype='text/html; charset=utf-8'
)
# 旧版 .doc 二进制格式不支持在线预览
if ext == 'doc':
return Response(
'<div style="padding:1rem;color:#6c757d">'
'不支持在线预览旧版 .doc 文件,请下载后查看,或将文件转换为 .docx 格式。</div>',
mimetype='text/html; charset=utf-8'
)
try:
with open(abs_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
+40
View File
@@ -155,6 +155,20 @@
</div>
{% elif resource.resource_type == 'text' %}
{%- set _name = (resource.original_name or resource.filename or '') -%}
{%- set _ext = _name.rsplit('.', 1)[-1].lower() if '.' in _name else '' -%}
{% if _ext in ('doc', 'docx') %}
<div class="text-preview-toolbar p-2 border-bottom d-flex gap-2 align-items-center">
<i class="bi bi-file-earmark-word text-primary"></i>
<span class="small text-muted">Word 文档预览</span>
</div>
<div class="word-preview p-4 bg-white" id="textContent"
style="max-height:75vh;overflow:auto;line-height:1.7">
<div class="text-center py-4 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>加载中…
</div>
</div>
{% else %}
<div class="text-preview-toolbar p-2 border-bottom d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="changeFont(1)">A+</button>
<button class="btn btn-sm btn-outline-secondary" onclick="changeFont(-1)">A-</button>
@@ -165,6 +179,7 @@
<div class="spinner-border spinner-border-sm me-2"></div>加载中…
</div>
</div>
{% endif %}
{% else %}
<div class="d-flex flex-column align-items-center justify-content-center p-5 text-muted">
@@ -195,6 +210,30 @@
<script>
// ── 文本内容加载 ────────────────────────────────────────────────────────────
{% if resource.resource_type == 'text' and resource.file_path and resource.download_status in ('na','done') %}
{%- set _name = (resource.original_name or resource.filename or '') -%}
{%- set _ext = _name.rsplit('.', 1)[-1].lower() if '.' in _name else '' -%}
{% if _ext in ('doc', 'docx') %}
// Word 文档:服务端已转换为 HTML,直接渲染
fetch("{{ url_for('resources.preview', resource_id=resource.id) }}")
.then(r => r.text())
.then(html => {
const container = document.getElementById('textContent');
container.innerHTML = html;
// 让文档内的图片自适应宽度
container.querySelectorAll('img').forEach(img => {
img.style.maxWidth = '100%';
img.style.height = 'auto';
});
// 表格基础样式
container.querySelectorAll('table').forEach(t => {
t.classList.add('table', 'table-bordered');
});
})
.catch(() => {
document.getElementById('textContent').innerHTML =
'<div class="text-danger p-3">文档加载失败</div>';
});
{% else %}
fetch("{{ url_for('resources.preview', resource_id=resource.id) }}")
.then(r => r.text())
.then(text => {
@@ -222,6 +261,7 @@ function toggleWrap() {
pre.style.whiteSpace = pre.style.whiteSpace === 'pre' ? 'pre-wrap' : 'pre';
}
{% endif %}
{% endif %}
// ── 下载进度轮询 ────────────────────────────────────────────────────────────
{% if resource.download_status in ('pending','downloading') %}
+3
View File
@@ -15,6 +15,8 @@ MIME_TYPE_MAP = {
'text/xml': 'text',
'application/json': 'text',
'application/xml': 'text',
'application/msword': 'text',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'text',
# 图片
'image/jpeg': 'image',
'image/png': 'image',
@@ -45,6 +47,7 @@ MIME_TYPE_MAP = {
EXT_TYPE_MAP = {
'txt': 'text', 'md': 'text', 'csv': 'text', 'json': 'text',
'xml': 'text', 'log': 'text', 'html': 'text', 'htm': 'text',
'doc': 'text', 'docx': 'text',
'jpg': 'image', 'jpeg': 'image', 'png': 'image', 'gif': 'image',
'webp': 'image', 'bmp': 'image', 'svg': 'image', 'ico': 'image',
'mp3': 'audio', 'wav': 'audio', 'ogg': 'audio', 'flac': 'audio',
+1 -1
View File
@@ -29,7 +29,7 @@ class Config:
WTF_CSRF_TIME_LIMIT = 3600
# 允许的文件类型
ALLOWED_TEXT_EXT = {'txt', 'md', 'csv', 'json', 'xml', 'log', 'html', 'htm'}
ALLOWED_TEXT_EXT = {'txt', 'md', 'csv', 'json', 'xml', 'log', 'html', 'htm', 'doc', 'docx'}
ALLOWED_IMAGE_EXT = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'}
ALLOWED_AUDIO_EXT = {'mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma'}
ALLOWED_VIDEO_EXT = {'mp4', 'webm', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'm4v'}
+1
View File
@@ -13,5 +13,6 @@ requests==2.32.3
python-dotenv==1.0.1
python-magic-bin==0.4.14; sys_platform == 'win32'
python-magic==0.4.27; sys_platform != 'win32'
mammoth==1.8.0
# WSGI 服务器(容器生产环境)
gunicorn==23.0.0