From acafc9792a58862fc68878c814355bdf948c2af8 Mon Sep 17 00:00:00 2001
From: "DESKTOP-RQ919RC\\Pc" <1300399510@qq.com>
Date: Thu, 25 Dec 2025 17:21:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0CSS=E6=A0=B7=E5=BC=8F?=
=?UTF-8?q?=E3=80=81=E6=B7=BB=E5=8A=A0TinyMCE=E6=8F=92=E4=BB=B6=E5=8F=8A?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=91=E5=B8=83=E9=A1=B5=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修复移动端登录框样式问题
更新公共JS文件中的授权令牌
添加TinyMCE插件(代码、视觉块、预览等)
优化发布管理页面的编辑器和布局
调整登录组件的响应式样式
---
component/sign-in/sign-in.js | 2 +-
component/sign-in/sign-in.txt | 16 +-
css/public.css | 2 +-
css/public.less | 2 +-
css/section.css | 3 +
css/section.less | 4 +
homepage-other-V2.html | 565 +++++++++++++++
js/public.js | 4 +-
js/publish_admin.js | 656 +++++++++++++++---
js/tinymce/icons/default/icons.min.js | 1 +
js/tinymce/langs/zh_CN.js | 1 +
js/tinymce/models/dom/model.min.js | 4 +
js/tinymce/plugins/autolink/plugin.min.js | 4 +
js/tinymce/plugins/charmap/plugin.min.js | 4 +
js/tinymce/plugins/code/plugin.min.js | 4 +
js/tinymce/plugins/codesample/plugin.min.js | 4 +
.../plugins/directionality/plugin.min.js | 4 +
js/tinymce/plugins/emoticons/js/emojis.min.js | 2 +
js/tinymce/plugins/emoticons/plugin.min.js | 4 +
js/tinymce/plugins/fullscreen/plugin.min.js | 4 +
js/tinymce/plugins/image/plugin.min.js | 4 +
js/tinymce/plugins/link/plugin.min.js | 4 +
js/tinymce/plugins/lists/plugin.min.js | 4 +
js/tinymce/plugins/media/plugin.min.js | 4 +
js/tinymce/plugins/preview/plugin.min.js | 4 +
.../plugins/searchreplace/plugin.min.js | 4 +
js/tinymce/plugins/table/plugin.min.js | 4 +
js/tinymce/plugins/template/plugin.min.js | 4 +
js/tinymce/plugins/visualblocks/plugin.min.js | 4 +
js/tinymce/plugins/visualchars/plugin.min.js | 4 +
js/tinymce/plugins/wordcount/plugin.min.js | 4 +
.../skins/content/default/content.min.css | 1 +
js/tinymce/skins/ui/oxide/content.min.css | 1 +
js/tinymce/skins/ui/oxide/skin.min.css | 1 +
js/tinymce/themes/silver/theme.min.js | 4 +
js/tinymce/tinymce.min.js | 4 +
publish_admin.html | 47 +-
37 files changed, 1263 insertions(+), 129 deletions(-)
create mode 100644 homepage-other-V2.html
create mode 100644 js/tinymce/icons/default/icons.min.js
create mode 100644 js/tinymce/langs/zh_CN.js
create mode 100644 js/tinymce/models/dom/model.min.js
create mode 100644 js/tinymce/plugins/autolink/plugin.min.js
create mode 100644 js/tinymce/plugins/charmap/plugin.min.js
create mode 100644 js/tinymce/plugins/code/plugin.min.js
create mode 100644 js/tinymce/plugins/codesample/plugin.min.js
create mode 100644 js/tinymce/plugins/directionality/plugin.min.js
create mode 100644 js/tinymce/plugins/emoticons/js/emojis.min.js
create mode 100644 js/tinymce/plugins/emoticons/plugin.min.js
create mode 100644 js/tinymce/plugins/fullscreen/plugin.min.js
create mode 100644 js/tinymce/plugins/image/plugin.min.js
create mode 100644 js/tinymce/plugins/link/plugin.min.js
create mode 100644 js/tinymce/plugins/lists/plugin.min.js
create mode 100644 js/tinymce/plugins/media/plugin.min.js
create mode 100644 js/tinymce/plugins/preview/plugin.min.js
create mode 100644 js/tinymce/plugins/searchreplace/plugin.min.js
create mode 100644 js/tinymce/plugins/table/plugin.min.js
create mode 100644 js/tinymce/plugins/template/plugin.min.js
create mode 100644 js/tinymce/plugins/visualblocks/plugin.min.js
create mode 100644 js/tinymce/plugins/visualchars/plugin.min.js
create mode 100644 js/tinymce/plugins/wordcount/plugin.min.js
create mode 100644 js/tinymce/skins/content/default/content.min.css
create mode 100644 js/tinymce/skins/ui/oxide/content.min.css
create mode 100644 js/tinymce/skins/ui/oxide/skin.min.css
create mode 100644 js/tinymce/themes/silver/theme.min.js
create mode 100644 js/tinymce/tinymce.min.js
diff --git a/component/sign-in/sign-in.js b/component/sign-in/sign-in.js
index d4b4fc8..9104519 100644
--- a/component/sign-in/sign-in.js
+++ b/component/sign-in/sign-in.js
@@ -1,5 +1,5 @@
const signTemplate = document.createElement("template");
-signTemplate.innerHTML = `
+
IK8gQW_rhIzjn5y_27ky2ZvwRQxRrg7wAsfg1NYIwUkqAJdjHi9EmGZmMjM~
+
+
+
+

+
论坛
+

+
so猫的个人主页
+
+
+
+
+
+
![用户头像]()
+
+
{{ info.nickname }}
+
+ UID: {{ info.uin }}
+
+
+
+
+
+
勋章 {{ medallist.length }}
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注册时间
+ {{ info.register_at || '暂无' }}
+
+
+ 最后登录
+ {{ info.lastlogintime || '暂无' }}
+
+
+
+
+ 在线时长
+ {{ info.oltime || 0 }} 小时
+
+
+
+
+ 上次访问 IP
+ {{ info.lastloginip || '暂无' }}
+
+
+ Email
+ {{ info.email || '暂无' }}
+ 已认证
+
+
+ 手机号
+ {{ info.mobile || '暂无' }}
+ 已认证
+
+
+
+
+ 累计签到
+ {{ info.sign_count || 0 }} 天
+
+
+
+ 本月签到
+ {{ info.sign_month || 0 }} 天
+
+
+
+
+ 寄托币
+ {{ info.gtercoin || 0 }}
+
+
+
+
+
+
+
+
+ {{ item.text }} ×
+ {{ item.number }}
+
+ |
+
+
+
+
+
+
+
+
+
+
+ 共
+
{{ count }}
+ 个创作,获得
+
{{ classify == 'all' ? totalLikes : (likeObjValue[classify] || 0) }}
+ 个赞
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
- 暂无内容 -
+
+
+
加载更多…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/public.js b/js/public.js
index 6880fbc..d07b3b8 100644
--- a/js/public.js
+++ b/js/public.js
@@ -24,7 +24,7 @@ const ajax = (url, data) => {
url = url.indexOf("https://") > -1 ? url : forumBaseURL + url;
if (data) data["v"] = vParam || "v2";
return new Promise(function (resolve, reject) {
- if (location.hostname == "127.0.0.1") axios.defaults.headers.common["Authorization"] = "n1pstcsmw6m6bcx49z705xhvduqviw29";
+ if (location.hostname == "127.0.0.1") axios.defaults.headers.common["Authorization"] = "d1329afaff3230eae2463306371e74eb";
axios
.post(url, data, {
@@ -89,7 +89,7 @@ const ajaxGet = (url) => {
url = `${url}${paramSymbol}v=${vParam || "v2"}`;
return new Promise(function (resolve, reject) {
- if (location.hostname == "127.0.0.1") axios.defaults.headers.common["Authorization"] = "n1pstcsmw6m6bcx49z705xhvduqviw29";
+ if (location.hostname == "127.0.0.1") axios.defaults.headers.common["Authorization"] = "d1329afaff3230eae2463306371e74eb";
axios
.get(
diff --git a/js/publish_admin.js b/js/publish_admin.js
index 68dfe60..8b47d68 100644
--- a/js/publish_admin.js
+++ b/js/publish_admin.js
@@ -3,19 +3,15 @@ const { createApp, ref, computed, onMounted, nextTick, onUnmounted } = Vue;
const editApp = createApp({
setup() {
- const { Editor, FileUploader } = window.textbus;
+ const LANG = location.href.indexOf("lang=en") > 0 ? "en" : "zh_CN";
const title = ref("");
const saveStatus = ref("");
const uniqid = ref("");
const info = ref({});
const token = ref("");
- let editor = null;
-
- const draftKey = "publish_admin_draft";
let uConfigData = {};
- let imageLength = 10;
- let videoLength = 5;
+ const maxSize = 20 * 1024 * 1024; // 1M
const formatTime = (d) => {
const pad = (n) => String(n).padStart(2, "0");
@@ -27,15 +23,19 @@ const editApp = createApp({
const imgElements = dom.querySelectorAll("img");
imgElements.forEach((imgEl) => {
let url = imgEl.getAttribute("src")?.trim() || "";
- const urlObj = new URL(url);
- const aid = urlObj.searchParams.get("aid");
- const queryIndex = url.indexOf("?");
- const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
- if (Number(aid)) {
- images.push({
- url: cleanUrl,
- aid: Number(aid),
- });
+ try {
+ const urlObj = new URL(url, location.origin);
+ const aid = urlObj.searchParams.get("aid");
+ const queryIndex = url.indexOf("?");
+ const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
+ if (Number(aid)) {
+ images.push({
+ url: cleanUrl,
+ aid: Number(aid),
+ });
+ }
+ } catch (e) {
+ console.error("Error parsing image URL:", url, e);
}
});
return images;
@@ -46,19 +46,31 @@ const editApp = createApp({
const result = [];
videoElements.forEach((videoEl) => {
- const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 视频地址
- const urlObj = new URL(posterurl);
- const posterid = urlObj.searchParams.get("aid");
+ const posterurl = videoEl.getAttribute("poster")?.trim() || "";
+ let posterid = null;
+ let cleanPosterurl = posterurl;
+
+ try {
+ const urlObj = new URL(posterurl, location.origin);
+ posterid = urlObj.searchParams.get("aid");
+ const queryIndex2 = posterurl.indexOf("?");
+ cleanPosterurl = queryIndex2 !== -1 ? posterurl.substring(0, queryIndex2) : posterurl;
+ } catch (e) {}
const sourceEl = videoEl.querySelector("source");
+ if (!sourceEl) return;
+
+ const url = sourceEl.getAttribute("src") || "";
+ let aid = null;
+ let cleanUrl = url;
+
+ try {
+ const obj = new URL(url, location.origin);
+ aid = obj.searchParams.get("aid");
+ const queryIndex = url.indexOf("?");
+ cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
+ } catch (e) {}
- const url = sourceEl.getAttribute("src") || null;
- const obj = new URL(url);
- const aid = obj.searchParams.get("aid");
- const queryIndex = url.indexOf("?");
- const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
- const queryIndex2 = posterurl.indexOf("?");
- const cleanPosterurl = queryIndex2 !== -1 ? posterurl.substring(0, queryIndex2) : posterurl;
result.push({
aid: Number(aid),
posterid: Number(posterid),
@@ -75,18 +87,15 @@ const editApp = createApp({
// 提交
const submit = (status) => {
const infoTarget = { ...info.value } || {};
- // 获取 HTML 内容
+
let content = "";
- if (editor && typeof editor.getHTML === 'function') {
- content = editor.getHTML();
- } else if (editor && editor.output) {
- content = editor.output.content; // Fallback if getHTML isn't direct
+ if (window.tinymce && window.tinymce.activeEditor) {
+ content = window.tinymce.activeEditor.getContent();
}
- // 创建临时 DOM 用于提取图片和视频
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
-
+
const images = extractImages(tempDiv);
const videos = extractVideos(tempDiv);
@@ -155,7 +164,8 @@ const editApp = createApp({
if (infoTarget.title) title.value = infoTarget.title;
nextTick(() => {
- initEditor();
+ // Pass content directly to init
+ initTinyMCE(infoTarget.content || "");
});
})
.catch((err) => {
@@ -163,41 +173,37 @@ const editApp = createApp({
});
};
- // 上传图片/视频 获取url
const uploading = (file, name, type) => {
return new Promise((resolve, reject) => {
const upload = () => {
let config = uConfigData;
const formData = new FormData();
- formData.append(config.requestName, file); // 文件数据
- formData.append("name", name); // 文件名
- formData.append("type", type); // 文件名
+ formData.append(config.requestName || "file", file);
+ formData.append("name", name);
+ formData.append("type", type);
if (config.params && config.params.data) {
formData.append("data", config.params.data);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", config.url, true);
- xhr.withCredentials = true; // 允许携带 Cookie
-
- // 监听上传进度
- xhr.upload.onprogress = function (event) {
- if (event.lengthComputable) {
- // const percentComplete = (event.loaded / event.total) * 100;
- // progress.value = Math.round(percentComplete);
- }
- };
+ xhr.withCredentials = true;
xhr.onload = function () {
if (xhr.status === 200) {
- const res = JSON.parse(xhr.responseText);
- if (res.code == 200) {
- const data = res.data;
- resolve(data);
- } else {
- creationAlertBox("error", res.message || "上传失败");
- reject(res);
+ try {
+ const res = JSON.parse(xhr.responseText);
+ if (res.code == 200) {
+ const data = res.data;
+ resolve(data);
+ } else {
+ creationAlertBox("error", res.message || "上传失败");
+ reject(res);
+ }
+ } catch (e) {
+ creationAlertBox("error", "解析响应失败");
+ reject(e);
}
} else {
creationAlertBox("error", "上传失败");
@@ -225,59 +231,22 @@ const editApp = createApp({
});
};
- // 自定义上传适配器
- class CustomUploader extends FileUploader {
- uploadFile(type, file) {
- // type 可能是 'image' 或 'video' 等,取决于调用方
- // uploading 函数接受 (file, name, type)
- return uploading(file, file.name, type).then(res => {
- // 构造带 aid 的 url
- return `${res.url}?aid=${res.aid}`;
- });
- }
- }
-
- const initEditor = () => {
- const editorConfig = {
- content: info.value?.content || "",
- providers: [{
- provide: FileUploader,
- useFactory: () => new CustomUploader()
- }],
- // 默认情况下,xnote 使用悬浮/气泡菜单
- // 我们不配置 toolbar 容器,让其使用默认行为
- };
-
- try {
- editor = new Editor(editorConfig);
- editor.mount(document.getElementById("editor-text-area"));
-
- // 监听内容变化
- if (editor.onChange) {
- editor.onChange.subscribe(() => {
- saveStatus.value = "有未保存的更改";
- });
- }
- } catch (error) {
- console.log("error", error);
- }
-
- // 点击空白处 focus 编辑器
- document.getElementById("editor-text-area").addEventListener("click", (e) => {
- // 如果点击的是容器本身(空白处),则聚焦
- if (e.target.id === "editor-text-area") {
- // editor.focus() 如果存在
- // Textbus editor 实例通常不需要手动 focus,除非是 command
- }
- });
- };
-
- // 提取视频第一帧作为封面
- const getVideoFirstFrame = (file) => {
- return new Promise((resolve) => {
+ // 提取视频第一帧作为封面 (支持 File 或 URL)
+ const getVideoFirstFrame = (source) => {
+ return new Promise((resolve, reject) => {
const video = document.createElement("video");
- video.src = URL.createObjectURL(file);
- video.currentTime = 1; // 截取第 1 秒
+ video.setAttribute("crossOrigin", "anonymous"); // Allow cross-origin for cover generation
+
+ if (source instanceof File) {
+ video.src = URL.createObjectURL(source);
+ } else if (typeof source === "string") {
+ video.src = source;
+ } else {
+ reject(new Error("Invalid source for video cover generation"));
+ return;
+ }
+
+ video.currentTime = 1;
video.onloadeddata = () => {
video.currentTime = 1;
};
@@ -291,9 +260,480 @@ const editApp = createApp({
resolve(coverFile);
}, "image/jpeg");
};
+ video.onerror = (e) => {
+ reject(e);
+ };
});
};
+ const initTinyMCE = (initialContent) => {
+ if (window.tinymce.get("editor-text-area")) {
+ window.tinymce.get("editor-text-area").remove();
+ }
+
+ // Calculate height based on window size to match original CSS calc(100vh - 370px)
+ // const editorHeight = window.innerHeight - 330;
+
+ const editorConfig = {
+ selector: "#editor-text-area",
+ language: LANG === "en" ? "en" : "zh_CN",
+ language_url: LANG === "en" ? undefined : "/js/tinymce/langs/zh_CN.js",
+ plugins: "image media table link lists code charmap emoticons wordcount fullscreen preview searchreplace autolink directionality visualblocks visualchars template codesample",
+ toolbar: "undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | removeformat | emoticons | fullscreen",
+ menubar: false,
+ fixed_toolbar_container: "#editor-toolbar",
+ // height: editorHeight > 300 ? editorHeight : 300, // Ensure minimum height
+ // resize: false,
+ height: "100%", // Use CSS height
+ resize: true, // Allow user to resize if needed, or rely on container
+ branding: false,
+ promotion: false,
+ convert_urls: false,
+ media_live_embeds: true, // Enable live video previews
+ initialValue: initialContent, // Set initial content here
+
+ // Add urlconverter_callback to handle network resources
+ urlconverter_callback: (url, node, on_save, name) => {
+ // Return URL as is, let image upload handler process it if needed
+ return url;
+ },
+
+ // Intercept Media Dialog URL input
+ media_url_resolver: function (data, resolve /*, reject*/) {
+ const url = data.url;
+
+ // Only intercept http/https URLs that are NOT from our domain (already uploaded)
+ if (url && (url.startsWith("http://") || url.startsWith("https://")) && !url.includes("?aid=")) {
+ // Check if it's a video file type we care about
+ const isVideo = /\.(mp4|webm|ogg|mov|mkv|avi|flv|wmv)$/i.test(url);
+
+ if (isVideo && uConfigData && uConfigData.url) {
+ creationAlertBox("info", "Uploading network video...");
+
+ const formData = new FormData();
+ formData.append("uploadType", "url");
+ formData.append("url", url);
+ if (uConfigData.params && uConfigData.params.data) {
+ formData.append("data", uConfigData.params.data);
+ }
+
+ ajax(uConfigData.url, formData)
+ .then(async (res) => {
+ if (res.code == 200 && res.data) {
+ const newUrl = `${res.data.url}?aid=${res.data.aid}`;
+
+ try {
+ // Generate cover
+ const coverFile = await getVideoFirstFrame(newUrl);
+ const coverRes = await uploading(coverFile, "cover.jpg", "image");
+ const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
+
+ // Return the full HTML embed code
+ // TinyMCE expects HTML when using resolve() for embeds
+ const videoHtml = `