优化功能
This commit is contained in:
222
resources/views/admin/partials/modern-form-scripts.blade.php
Normal file
222
resources/views/admin/partials/modern-form-scripts.blade.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const previewEndpoint = '{{ route('admin.markdown.preview') }}';
|
||||
const uploadEndpoint = '{{ route('admin.uploads.markdown-image') }}';
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
const instances = new Map();
|
||||
|
||||
const slugify = (value) => {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5\s-]/g, '')
|
||||
.trim()
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
};
|
||||
|
||||
const debounce = (callback, wait = 350) => {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => callback(...args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
const renderPreview = debounce(async (editor, previewElement) => {
|
||||
if (!(previewElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdown = editor.value();
|
||||
if (!markdown || markdown.trim() === '') {
|
||||
previewElement.innerHTML = '<div class="preview-placeholder">在左侧输入 Markdown 内容,这里会实时显示预览。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
previewElement.innerHTML = '<div class="text-muted">预览生成中...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(previewEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: JSON.stringify({ markdown }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('preview-failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload || payload.success !== true) {
|
||||
throw new Error('preview-failed');
|
||||
}
|
||||
|
||||
previewElement.innerHTML = payload.html && payload.html.trim() !== ''
|
||||
? payload.html
|
||||
: '<div class="preview-placeholder">暂无可预览内容。</div>';
|
||||
} catch (_) {
|
||||
previewElement.innerHTML = '<div class="text-danger">预览失败,请稍后重试。</div>';
|
||||
}
|
||||
}, 320);
|
||||
|
||||
const uploadImage = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch(uploadEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('upload-failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload || payload.success !== true || !payload.url) {
|
||||
throw new Error('upload-failed');
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const markdownForImage = (payload, fileName = 'image') => {
|
||||
if (typeof payload.markdown === 'string' && payload.markdown.trim() !== '') {
|
||||
return `\n${payload.markdown.trim()}\n`;
|
||||
}
|
||||
|
||||
const alt = fileName.replace(/\.[^.]+$/, '');
|
||||
return `\n\n`;
|
||||
};
|
||||
|
||||
const initEditor = (textarea) => {
|
||||
if (!(textarea instanceof HTMLTextAreaElement) || instances.has(textarea.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const easyMde = new EasyMDE({
|
||||
element: textarea,
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
forceSync: true,
|
||||
status: ['lines', 'words'],
|
||||
placeholder: textarea.placeholder || '请输入 Markdown 内容',
|
||||
toolbar: [
|
||||
'bold',
|
||||
'italic',
|
||||
'heading',
|
||||
'|',
|
||||
'quote',
|
||||
'unordered-list',
|
||||
'ordered-list',
|
||||
'|',
|
||||
'link',
|
||||
'image',
|
||||
'table',
|
||||
'|',
|
||||
'preview',
|
||||
'side-by-side',
|
||||
'fullscreen',
|
||||
'|',
|
||||
'guide',
|
||||
],
|
||||
});
|
||||
|
||||
const previewSelector = textarea.getAttribute('data-preview-target');
|
||||
const previewElement = previewSelector ? document.querySelector(previewSelector) : null;
|
||||
|
||||
easyMde.codemirror.on('change', () => {
|
||||
renderPreview(easyMde, previewElement);
|
||||
});
|
||||
|
||||
renderPreview(easyMde, previewElement);
|
||||
instances.set(textarea.name, easyMde);
|
||||
};
|
||||
|
||||
const connectUploadButtons = () => {
|
||||
document.querySelectorAll('.js-md-upload-advanced-btn').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const fieldName = button.getAttribute('data-editor-target');
|
||||
const editor = fieldName ? instances.get(fieldName) : null;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originHtml = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>上传中...';
|
||||
|
||||
try {
|
||||
const payload = await uploadImage(file);
|
||||
editor.codemirror.replaceSelection(markdownForImage(payload, file.name || 'image'));
|
||||
} catch (_) {
|
||||
alert('图片上传失败,请稍后重试');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.innerHTML = originHtml;
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initSlugAutoFill = () => {
|
||||
const pairs = [
|
||||
['input[name="name"]', 'input[name="slug"]'],
|
||||
['input[name="title"]', 'input[name="slug"]'],
|
||||
];
|
||||
|
||||
for (const [sourceSelector, slugSelector] of pairs) {
|
||||
const sourceInput = document.querySelector(sourceSelector);
|
||||
const slugInput = document.querySelector(slugSelector);
|
||||
if (!(sourceInput instanceof HTMLInputElement) || !(slugInput instanceof HTMLInputElement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manualEdited = slugInput.value.trim() !== '';
|
||||
slugInput.addEventListener('input', () => {
|
||||
manualEdited = slugInput.value.trim() !== '';
|
||||
});
|
||||
|
||||
sourceInput.addEventListener('input', () => {
|
||||
if (manualEdited && slugInput.value.trim() !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggested = slugify(sourceInput.value);
|
||||
if (suggested !== '') {
|
||||
slugInput.value = suggested;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll('textarea.js-md-editor-modern').forEach(initEditor);
|
||||
connectUploadButtons();
|
||||
initSlugAutoFill();
|
||||
}());
|
||||
</script>
|
||||
Reference in New Issue
Block a user