文章和文件建立关联

This commit is contained in:
2026-08-19 17:39:23 +08:00
parent f1a6c5abaa
commit 4a9ac54963
13 changed files with 256 additions and 28 deletions
+128 -7
View File
@@ -298,6 +298,8 @@ async function showPostForm(isEdit = false) {
document.getElementById('post-form-title').textContent = isEdit ? '编辑文章' : '新建文章';
document.getElementById('post-form').reset();
document.getElementById('post-id').value = '';
_coverFileID = null;
_contentFileIDs.clear();
setCover('');
mdShowEdit();
@@ -327,6 +329,13 @@ async function editPost(id) {
document.getElementById('post-title').value = post.title;
document.getElementById('post-content').value = post.content;
document.getElementById('post-summary').value = post.summary || '';
// 重置文件追踪
_coverFileID = null;
_contentFileIDs.clear();
// 从关联文件中恢复追踪
if (post.files && Array.isArray(post.files)) {
post.files.forEach(f => _contentFileIDs.add(f.id));
}
setCover(post.cover || '');
document.getElementById('post-status').value = post.status;
document.getElementById('post-istop').checked = post.is_top;
@@ -364,7 +373,8 @@ async function handlePostSubmit(e) {
category_id: parseInt(document.getElementById('post-category').value),
status: document.getElementById('post-status').value,
is_top: document.getElementById('post-istop').checked,
tags: tags
tags: tags,
file_ids: buildFileIDs()
};
try {
@@ -430,6 +440,87 @@ function mdInsertTable() {
ta.selectionStart = ta.selectionEnd = start + table.length;
}
// ==================== 图片插入对话框 ====================
let _imgFile = null; // 当前选择的图片文件
// 打开图片插入对话框
function mdInsertImage() {
_imgFile = null;
document.getElementById('img-desc').value = '';
document.getElementById('img-file-name').textContent = '未选择';
document.getElementById('img-preview-box').style.display = 'none';
document.getElementById('img-file-input').value = '';
document.getElementById('img-upload-btn').disabled = true;
document.getElementById('image-modal').classList.remove('hidden');
}
// 关闭图片插入对话框
function hideImageModal() {
document.getElementById('image-modal').classList.add('hidden');
_imgFile = null;
}
// 选择图片文件后:校验、预览、启用按钮
function onImageFileSelected(input) {
const file = input.files[0];
if (!file) return;
const err = validateImageFile(file);
if (err) {
alert(err);
input.value = '';
_imgFile = null;
document.getElementById('img-upload-btn').disabled = true;
return;
}
_imgFile = file;
document.getElementById('img-file-name').textContent = file.name;
// 预览
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('img-preview-thumb').src = e.target.result;
document.getElementById('img-preview-box').style.display = 'block';
};
reader.readAsDataURL(file);
document.getElementById('img-upload-btn').disabled = false;
}
// 确认上传并插入编辑器
async function confirmInsertImage() {
if (!_imgFile) return;
const desc = document.getElementById('img-desc').value.trim() || _imgFile.name.replace(/\.[^.]+$/, '');
const btn = document.getElementById('img-upload-btn');
btn.disabled = true;
btn.textContent = '上传中...';
try {
const data = await uploadImageFile(_imgFile);
// 记录文件 ID
if (data.id) {
_contentFileIDs.add(data.id);
}
// 插入 Markdown 图片语法到编辑器
mdShowEdit();
const ta = document.getElementById('post-content');
const imgMd = `![${desc}](${data.url})`;
const start = ta.selectionStart;
ta.value = ta.value.substring(0, start) + imgMd + ta.value.substring(ta.selectionEnd);
ta.focus();
ta.selectionStart = ta.selectionEnd = start + imgMd.length;
hideImageModal();
} catch (err) {
alert('上传失败:' + err.message);
btn.disabled = false;
btn.textContent = '上传并插入';
}
}
// 切换到编辑模式
function mdShowEdit() {
document.getElementById('post-content').classList.remove('hidden');
@@ -1478,6 +1569,11 @@ async function saveCurrentFile() {
// ==================== 文件上传与管理 ====================
// 当前封面文件 ID(上传封面时记录)
let _coverFileID = null;
// 正文中插入的图片文件 ID 集合
let _contentFileIDs = new Set();
// 允许的图片类型与大小限制(与后端保持一致)
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
@@ -1526,6 +1622,7 @@ async function handleCoverFileChange(e) {
try {
const data = await uploadImageFile(file);
_coverFileID = data.id || null;
setCover(data.url);
} catch (uErr) {
alert(uErr.message);
@@ -1553,9 +1650,19 @@ function setCover(url) {
// 移除封面图
function removeCover() {
_coverFileID = null;
setCover('');
}
// 构建提交时的 file_ids(封面 + 正文图片,去重)
function buildFileIDs() {
const ids = new Set(_contentFileIDs);
if (_coverFileID) {
ids.add(_coverFileID);
}
return Array.from(ids);
}
// 加载已上传文件列表
async function loadFiles() {
try {
@@ -1568,19 +1675,33 @@ async function loadFiles() {
return;
}
grid.innerHTML = data.data.map(f => `
grid.innerHTML = data.data.map(f => {
// 根据是否关联文章显示不同操作
let actionsHTML;
if (f.post_title) {
// 已关联文章,显示文章标题
actionsHTML = `<div class="file-actions">
<button class="btn-small btn-secondary" onclick="copyFileURL('${f.url}')">复制链接</button>
<span class="file-linked">已关联: <a href="#/post/edit/${f.post_id}" title="查看文章">${f.post_title}</a></span>
</div>`;
} else {
// 未关联,显示删除按钮
actionsHTML = `<div class="file-actions">
<button class="btn-small btn-secondary" onclick="copyFileURL('${f.url}')">复制链接</button>
<button class="btn-small btn-danger" onclick="deleteFile(${f.id})">删除</button>
</div>`;
}
return `
<div class="file-card">
<div class="file-thumb"><img src="${f.url}" alt="${f.orig_name || f.name}" loading="lazy"></div>
<div class="file-info">
<div class="file-name" title="${f.orig_name || f.name}">${f.orig_name || f.name}</div>
<div class="file-meta">${formatFileSize(f.size)} · ${f.created_at}</div>
</div>
<div class="file-actions">
<button class="btn-small btn-secondary" onclick="copyFileURL('${f.url}')">复制链接</button>
<button class="btn-small btn-danger" onclick="deleteFile(${f.id})">删除</button>
</div>
${actionsHTML}
</div>
`).join('');
`;
}).join('');
} catch (err) {
console.error('加载文件失败:', err);
}
+28 -2
View File
@@ -50,7 +50,7 @@
{{if index .PageSet "users"}}{{template "admin_modal_user" .}}{{end}}
{{if index .PageSet "themes"}}{{template "admin_modal_theme" .}}{{end}}
<script>window.__currentUser = {{.CurrentUser}};</script>
<script>window.__currentUser = {{.CurrentUser | json}};</script>
<script src="/static/admin/app.js?v=21"></script>
</body>
</html>
@@ -164,7 +164,7 @@
<button type="button" title="删除线" onclick="mdWrap('~~','~~','删除线文字')"><s>S</s></button>
<button type="button" title="标题" onclick="mdInsertLine('## ','标题')">H</button>
<button type="button" title="链接" onclick="mdWrap('[','](https://)','链接文字')">🔗</button>
<button type="button" title="图片" onclick="mdWrap('![','](https://)','图片描述')">🖼</button>
<button type="button" title="图片" onclick="mdInsertImage()">🖼</button>
<button type="button" title="行内代码" onclick="mdWrap('`','`','代码')">&lt;/&gt;</button>
<button type="button" title="代码块" onclick="mdWrap('\n```\n','\n```\n','代码块')">{ }</button>
<button type="button" title="引用" onclick="mdInsertLine('> ','引用内容')">”</button>
@@ -191,6 +191,32 @@
<button type="submit" class="btn-primary" onclick="mdShowEdit()">保存</button>
</form>
</div>
<!-- 图片插入对话框 -->
<div id="image-modal" class="modal hidden">
<div class="modal-content" style="max-width:420px">
<h3>插入图片</h3>
<div class="form-group">
<label>图片描述</label>
<input type="text" id="img-desc" placeholder="输入图片描述(alt 文本)">
</div>
<div class="form-group">
<label>选择图片</label>
<div style="display:flex;gap:8px;align-items:center">
<button type="button" class="btn-secondary" onclick="document.getElementById('img-file-input').click()">选择文件</button>
<input type="file" id="img-file-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none" onchange="onImageFileSelected(this)">
<span id="img-file-name" style="color:#888;font-size:13px">未选择</span>
</div>
<div id="img-preview-box" style="margin-top:8px;display:none">
<img id="img-preview-thumb" style="max-width:100%;max-height:200px;border-radius:4px;border:1px solid #ddd">
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="hideImageModal()">取消</button>
<button type="button" class="btn-primary" id="img-upload-btn" onclick="confirmInsertImage()" disabled>上传并插入</button>
</div>
</div>
</div>
{{end}}
{{define "admin_page_categories"}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB