feat: 更新CSS样式、添加TinyMCE插件及优化发布页面

修复移动端登录框样式问题
更新公共JS文件中的授权令牌
添加TinyMCE插件(代码、视觉块、预览等)
优化发布管理页面的编辑器和布局
调整登录组件的响应式样式
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-12-25 17:21:52 +08:00
parent f2469a1a3b
commit acafc9792a
37 changed files with 1263 additions and 129 deletions

File diff suppressed because one or more lines are too long

View File

@@ -882,6 +882,18 @@
}
}
@media screen and (max-width: 768px) {
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-header {
padding-top: 20px;
}
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-list .rule-item .rule-item-icon {
margin-right: 20px;
}
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-list .rule-item .rule-item-text {
padding: 20px 0;
}
}
@media screen and (max-width: 480px) {
.signInBox-mask .signInBox .signInBox-head .header-bi {
width: 60px;
@@ -959,6 +971,7 @@
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-header {
font-size: 18px;
padding-top: 10px;
}
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-list .rule-item .rule-item-icon {
@@ -974,7 +987,8 @@
.signInBox-mask .signInBox .signInBox-content .sign-in-box .outer-ring .rule-box .rule-list .rule-item .rule-item-text {
font-size: 13px;
padding: 15px 0;
padding: 10px 0;
line-height: 25px;
}
}
</style>

View File

@@ -1261,7 +1261,7 @@ body {
.side-box.essence-side-box .side-header {
margin-bottom: 21px !important;
}
.side-box.essence-side-box .box .item {
.side-box.essence-side-box .box .item:not(:last-of-type) {
margin-bottom: 12px;
}
.side-box.essence-side-box .box .item .dot {

View File

@@ -1526,7 +1526,7 @@ body {
margin-bottom: 21px !important;
}
.side-box.essence-side-box .box .item {
.side-box.essence-side-box .box .item:not(:last-of-type) {
margin-bottom: 12px;
}

View File

@@ -388,6 +388,9 @@
.item-box {
padding: 18px 20px 0;
}
.head-top .sign-in {
display: none !important;
}
}
@media screen and (max-width: 680px) {
#sectionIndex .matter .matter-content .info-box .right .bottom .btn {

View File

@@ -463,6 +463,10 @@
.item-box {
padding: 18px 20px 0;
}
.head-top .sign-in {
display: none !important;
}
}
@media screen and (max-width: 680px) {

565
homepage-other-V2.html Normal file
View File

@@ -0,0 +1,565 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>so猫的个人主页 -- 寄托天下</title>
<link rel="stylesheet" href="/css/public.css" />
<link rel="stylesheet" href="/css/homepage-other.css" />
<meta name="description" content="寄托天下留学论坛上查看so猫的个人主页">
<meta name="keywords" content="so猫, 寄托天下, 留学论坛">
<meta name="author" content="">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:title" content="so猫的个人主页">
<meta property="og:description" content="寄托天下留学论坛上查看so猫的个人主页">
<meta property="og:image" content="">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" content="so猫的个人主页">
<meta property="twitter:description" content="寄托天下留学论坛上查看so猫的个人主页">
<meta property="twitter:image" content="">
<!-- 网站图标 -->
<link rel="icon" href="https://www.gter.net/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="https://www.gter.net/favicon.ico" type="image/x-icon">
<style>
[v-cloak] {
display: none !important;
}
#pre-loader {
height: 70vh;
display: flex;
justify-content: center;
align-items: center;
}
#pre-loader .three-bounce>div {
display: inline-block;
width: 18px;
height: 18px;
border-radius: 100%;
top: 50%;
margin-top: -9px;
background: #aeadba;
animation: bouncedelay 1.4s infinite ease-in-out;
animation-fill-mode: both;
}
#pre-loader .three-bounce .one {
animation-delay: -0.32s;
}
#pre-loader .three-bounce .two {
animation-delay: -0.16s;
}
@keyframes bouncedelay {
0%,
100%,
80% {
transform: scale(0);
-webkit-transform: scale(0);
}
40% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
</style>
<script type="text/javascript">
var STYLEID = '2',
STATICURL = 'static/',
IMGDIR = 'https://bbs.gter.net/template/archy_plt8/image',
VERHASH = 'Z62',
charset = 'gbk',
discuz_uid = '0',
cookiepre = '4B5x_c0ae_',
cookiedomain = 'gter.net',
cookiepath = '/',
showusercard = '1',
attackevasive = '0',
disallowfloat = '',
creditnotice = ',',
defaultstyle = '',
REPORTURL = 'aHR0cDovL2Jicy5ndGVyLm5ldC9mb3J1bS5waHA/dGlkPTI0MDYzNTYmZ290bz1sYXN0cG9zdA==',
SITEURL = 'https://app.gter.net/',
JSPATH = 'static/js/';
</script>
<script src="https://app.gter.net/bottom?tpl=header&menukey=bbs"></script>
<script src="https://framework.x-php.com/gter/bbs/static/js/common.js" charset="gbk"></script>
</head>
<body>
<script>
window.__ASSET_VERSION__ = 'Z70';
window.isMobile = window.innerWidth <= 768;
</script>
<div id="ajaxwaitid"></div>
<div id="append_parent"></div>
<div class="head-top flexacenter" style="width: 1200px;margin: 20px auto 30px;z-index: 8;">
<a href="/" class="flexacenter" target="_blank">
<img class="logo" src="https://oss.gter.net/logo" alt="" />
</a>
<div class="flex1"></div>
<div class="input-box flexacenter">
<div class="placeholder">
<div class="placeholder-box" style="transition: transform .3s ease"></div>
</div>
<input class="input flex1" type="text" maxlength="140" /> <img class="icon" onclick="searchEvent()" src="https://framework.x-php.com/gter/forum/img/search-icon.svg?v=HP1TnTC4iXqb" />
<div class="search-box-history">
<div class="search-box-history-title">历史搜索</div>
<div class="search-box-history-list"></div>
</div>
</div>
<div class="post-list flexacenter"> </div>
<div class="sign-in sign-in-no flexacenter"></div>
<div class="head-more flexcenter" onclick="openHeadPop()">
<img class="more-icon" style="width: 18px;height: 15px;" src="https://framework.x-php.com/gter/forum/img/threeAcross.svg?v=HP1TnTC4iXqb" />
</div>
<div class="head-pop" style="display: none;">
<div class="head-more-pop">
<div class="head-more-userinfo flex1 flexacenter">
<div class="head-more-left flexacenter"><img class="head-more-userinfo-avatar" src="" alt="">
<div class="head-more-userinfo-username"></div>
</div>
<div class="head-more-right">
<div class="loginBtn flexcenter" onclick="go_ajax_Login()">登录/注册</div>
</div>
</div>
<div class="tab-list"><a class="tab-item flexacenter" href="https://www.gter.net" target="_blank">寄托首页</a><a class="tab-item flexacenter pitch" href="https://f.gter.net" target="_blank">论坛</a><a class="tab-item flexacenter" href="https://app.gter.net/admissionOfficer" target="_blank">招生官</a><a class="tab-item flexacenter" href="https://bbs.gter.net/thread-2345065-1-1.html" target="_blank">加群</a><a class="tab-item flexacenter" href="https://offer.gter.net" target="_blank">Offer榜</a></div>
<div class="sign-in sign-in-no flexacenter"></div>
<a class="head-more-post flexcenter" href="/publish" target="" onclick="skipLoginUrl(event)">
<div class="head-more-post-icon flexcenter"><img class="head-more-post-img" src="https://framework.x-php.com/gter/forum/img/addyellow.svg?v=HP1TnTC4iXqb" /></div>发布帖子
</a>
<img class="cross-icon" onclick="crossHeadPop()" src="https://framework.x-php.com/gter/forum/img/cross.svg?v=HP1TnTC4iXqb">
</div>
</div>
</div>
<div class="valueA" style="display: none;">https://framework.x-php.com/gter/forum/</div>
<div id="pre-loader">
<div class="three-bounce" p-id="11">
<div class="one" p-id="12"></div>
<div class="two" p-id="13"></div>
<div class="three" p-id="14"></div>
</div>
</div>
<div class="container" id="homepage-other" v-cloak>
<div class="templateValue" ref="tokenRef">IK8gQW_rhIzjn5y_27ky2ZvwRQxRrg7wAsfg1NYIwUkqAJdjHi9EmGZmMjM~</div>
<!-- <head-top></head-top> -->
<div class="head-navigation flexacenter">
<img class="icon" src="https://framework.x-php.com/gter/forum/img/index-icon.png?v=HP1TnTC4iXqb" />
<a class="text" href="/" target="_blank">论坛</a>
<img class="arrows" src="https://framework.x-php.com/gter/forum/img/arrows-gray.svg?v=HP1TnTC4iXqb" />
<div class="text one-line-display">so猫的个人主页</div>
</div>
<div class="matter flexflex">
<div class="card-user flexcenter">
<div class="name-area">
<img v-if="info.avatar" class="avatar" :src="info.avatar" alt="用户头像" />
<h3 class="username flexcenter">{{ info.nickname }}</h3>
<p class="uid flexcenter">
UID: {{ info.uin }}
<img class="icon" @click="copy(info.uin)" src="https://framework.x-php.com/gter/forum/img/copy-icon.png?v=HP1TnTC4iXqb" />
</p>
</div>
<div class="medal-area" v-if="medallist.length != 0">
<p class="title">勋章 {{ medallist.length }}</p>
<div class="list flexflex">
<img v-for="item in medallist" :key="item.medalid" :src="item.image" :alt="item.description" class="item" />
</div>
</div>
<div class="btn-area">
<div class="item msg flexcenter" @click="sendMessage()">发私信</div>
<template v-if="isManager">
<a class="item flexcenter" target="_blank" href="https://demo.gter.net/admin">用户管理</a>
<a class="item flexcenter" target="_blank" href="https://demo.gter.net/admin">内容管理</a>
</template>
</div>
</div>
<div class="matter-content flex1">
<div class="message-box">
<!-- 头部区域 -->
<div class="header flexacenter">
<img v-if="info.avatar" :src="info.avatar" alt="用户头像" class="avatar" />
<span class="username">{{ info.nickname }}</span>
<img v-if="info?.group?.image" class="icon" :src="info?.group?.image" />
</div>
<!-- 信息列表区域 -->
<div class="info-list flexflex">
<template v-if="isManager">
<div class="item flexacenter">
<span class="label">注册时间</span>
<span class="value">{{ info.register_at || '暂无' }}</span>
</div>
<div class="item flexacenter">
<span class="label">最后登录</span>
<span class="value">{{ info.lastlogintime || '暂无' }}</span>
</div>
</template>
<div class="item flexacenter">
<span class="label">在线时长</span>
<span class="value">{{ info.oltime || 0 }} 小时</span>
</div>
<template v-if="isManager">
<div class="item flexacenter">
<span class="label">上次访问 IP</span>
<span class="value">{{ info.lastloginip || '暂无' }}</span>
</div>
<div class="item flexacenter">
<span class="label">Email</span>
<span class="value">{{ info.email || '暂无' }}</span>
<span v-if="info.email" class="status blue flexacenter">已认证</span>
</div>
<div class="item flexacenter">
<span class="label">手机号</span>
<span class="value">{{ info.mobile || '暂无' }}</span>
<span v-if="info.mobile" class="status blue flexacenter">已认证</span>
</div>
</template>
<div class="item flexacenter">
<span class="label">累计签到</span>
<span class="value">{{ info.sign_count || 0 }} 天</span>
</div>
<div class="item flexacenter">
<span class="label">本月签到</span>
<span class="value">{{ info.sign_month || 0 }} 天</span>
</div>
<template v-if="isManager">
<div class="item flexacenter">
<span class="label">寄托币</span>
<span class="value">{{ info.gtercoin || 0 }}</span>
</div>
</template>
</div>
<!-- 统计标签区域 -->
<div class="stats flexacenter" v-if="creationType.length != 0">
<template v-for="(item, index) in creationType" :key="index">
<span class="item flexacenter">
<div class="text">{{ item.text }} ×</div>
<div class="num">{{ item.number }}</div>
</span>
<div class="line" v-if="index != creationType.length - 1">|</div>
</template>
</div>
<!-- Offer标签区域 -->
<div class="tags flexflex" v-if="schoolTags.length != 0">
<template v-for="(item, index) in schoolTags" :key="index">
<a v-if="item.type == 'offer'" class="item flexacenter" target="_blank" :href="'/details/' + item.uniqid">
<img class="icon" src="https://framework.x-php.com/gter/forum/img/offer-icon.png?v=HP1TnTC4iXqb" mode="heightFix" />
{{ item.school }}
</a>
<a v-else class="item flexacenter" target="_blank" :href="'/details/' + item.uniqid">
<img class="icon" src="https://framework.x-php.com/gter/forum/img/mj-icon.png?v=HP1TnTC4iXqb" mode="heightFix" />
{{ item.school }}
</a>
</template>
</div>
</div>
<div class="list-area">
<div class="classify flexacenter">
<div class="item" :class="{'pitch': item.type === classify}" v-for="item in classifyList" :key="item.type" @click="classifyChange(item.type)">{{ item.text }}</div>
</div>
<div class="issue-data flexacenter">
<div class="num">{{ count }}</div>
个创作,获得
<div class="num">{{ classify == 'all' ? totalLikes : (likeObjValue[classify] || 0) }}</div>
个赞
</div>
<div class="list-box" v-if="list.length != 0">
<template v-for="(item,index) in list" :key="index">
<item-offer v-if=" item.type == 'offer'" :itemdata="item"></item-offer>
<item-summary v-else-if="item.type == 'offer_summary'" :itemdata="item"></item-summary>
<item-vote v-else-if="item.type == 'vote'" :itemdata="item"></item-vote>
<item-mj v-else-if="item.type == 'interviewexperience'" :itemdata="item"></item-mj>
<item-tenement v-else-if="item.type == 'tenement'" :itemdata="item"></item-tenement>
<item-forum v-else :itemdata="item"></item-forum>
</template>
</div>
<load-box :loading="loading"></load-box>
<div v-if="list.length == 0 && page == 0" class="empty flexcenter">
<img class="empty-icon" src="https://framework.x-php.com/gter/forum/img/empty-icon.png?v=HP1TnTC4iXqb" />
<div class="empty-text">- 暂无内容 -</div>
</div>
<div v-if="list.length != 0 && page != 0" class="load-more flexcenter">加载更多…</div>
</div>
</div>
</div>
</div>
<script src="https://framework.x-php.com/gter/forum/js/vue.global.js?v=HP1TnTC4iXqb"></script>
<script src="https://framework.x-php.com/gter/forum/js/axios.min.js?v=HP1TnTC4iXqb"></script>
<script src="https://framework.x-php.com/gter/forum/js/public.js?v=HP1TnTC4iXqb"></script>
<!-- <script src="https://f.gter.net/js/public.js"></script> -->
<script type="module" src="https://framework.x-php.com/gter/forum/js/homepage-other.js?v=HP1TnTC4iXqb"></script>
<!-- <script type="module" src="https://f.gter.net/js/homepage-other.js"></script> -->
<script type="module" src="https://framework.x-php.com/gter/forum/../image/gter/commonCom/sign-in/sign-in.js?v=HP1TnTC4iXqb"></script>
<script src="https://app.gter.net/bottom?tpl=footer,popupnotification"></script>
<script>
if (location.href.indexOf('details') != -1 || location.href.indexOf('thread') != -1) {
const postList = document.querySelector('.head-top .post-list')
postList.innerHTML = `<a href="/publish" target="_blank" style="margin-right: 10px"> <img class="post-item" src="https://framework.x-php.com/gter/forum/img/post-thread.png?v=HP1TnTC4iXqb" /> </a> <a href="https://offer.gter.net/post" target="_blank" style="margin-right: 10px"> <img class="post-item" src="https://framework.x-php.com/gter/forum/img/post-offer.png?v=HP1TnTC4iXqb" /> </a> <a href="https://offer.gter.net/post/summary" target="_blank" style="margin-right: 10px"> <img class="post-item" src="https://framework.x-php.com/gter/forum/img/post-summary.png?v=HP1TnTC4iXqb" /> </a> <a href="https://interviewexperience.gter.net/publish" target="_blank" style="margin-right: 10px"> <img class="post-item" src="https://framework.x-php.com/gter/forum/img/post-mj.png?v=HP1TnTC4iXqb" /> </a> <a href="https://vote.gter.net/publish" target="_blank"> <img class="post-item" src="https://framework.x-php.com/gter/forum/img/post-vote.png?v=HP1TnTC4iXqb" /> </a>`
postList.style.display = 'flex'
} else if (location.href.indexOf('search') != -1) {
const box = document.querySelector(".head-top")
box.querySelector(".input-box").style.display = "none"
box.querySelector(".sign-in").style.display = "none"
} else if (location.href.indexOf("publish") != -1) {
const box = document.querySelector(".head-top")
if (box) document.body.removeChild(box)
} else {
const signInList = document.querySelectorAll('.head-top .sign-in')
signInList.forEach(element => {
element.innerHTML = `<div class="sign-in-no-box" onclick="headSignIn()">
<img class="sign-in-bj" src="https://framework.x-php.com/gter/forum/img/sign-in-bj.svg?v=HP1TnTC4iXqb" /><img class="coin-bj" src="https://framework.x-php.com/gter/forum/img/coin-bj.svg?v=HP1TnTC4iXqb" />
<img class="coin-icon" src="https://framework.x-php.com/gter/forum/img/coin-icon.png?v=HP1TnTC4iXqb" /><span class="text flex1">签到领寄托币</span>
<div class="sign-go flexcenter">
<img class="sign-go-bj" src="https://framework.x-php.com/gter/forum/img/sign-go.svg?v=HP1TnTC4iXqb" /> GO
</div>
<img class="petal1" src="https://framework.x-php.com/gter/forum/img/petal1.png?v=HP1TnTC4iXqb" />
<img class="petal2" src="https://framework.x-php.com/gter/forum/img/petal2.png?v=HP1TnTC4iXqb" />
<img class="petal3" src="https://framework.x-php.com/gter/forum/img/petal3.png?v=HP1TnTC4iXqb" />
</div>
<div class="sign-in-already-box">
<img class="sign-icon" src="https://framework.x-php.com/gter/forum/img/sign-icon.png?v=HP1TnTC4iXqb" />
<span>已签到,明天再来</span>
</div>`
element.style.display = 'flex'
})
let userInfoWinTimerCount = 0;
const userInfoWinTimer = setInterval(() => {
if (location.host == "127.0.0.1:5501") return;
if (todaysignedState) {
clearInterval(userInfoWinTimer);
if (todaysigned == 1) {
signInList.forEach(element => {
element.classList.add('sign-in-already')
element.classList.remove("sign-in-no");
})
}
}
userInfoWinTimerCount++;
if (userInfoWinTimerCount >= 3000) clearInterval(userInfoWinTimer);
}, 50);
}
function headSignIn() {
SignInComponent.initComponent();
}
const searchInput = document.querySelector('.head-top .input')
// 绑定 blur 和 focus 事件
if (searchInput) {
searchInput.addEventListener('blur', function () {
setTimeout(() => {
const historyBox = document.querySelector('.head-top .search-box-history')
if (historyBox) historyBox.style.display = 'none'
}, 300);
const inputBox = document.querySelector('.head-top .input-box')
if (inputBox) inputBox.classList.remove('pitch')
startCarousel();
})
searchInput.addEventListener('focus', () => {
const historyBox = document.querySelector('.head-top .search-box-history')
const historyItem = historyBox.querySelectorAll(".search-box-history-item")
if (historyBox && historyItem.length > 0) historyBox.style.display = 'block'
const inputBox = document.querySelector('.head-top .input-box')
if (inputBox) inputBox.classList.add('pitch')
if (carouselTimer) clearInterval(carouselTimer);
})
// 绑定回车事件
searchInput.addEventListener('keydown', (e) => {
if (e.key == 'Enter') searchEvent()
})
searchInput.addEventListener('input', (e) => {
const value = e.target.value || ''
const placeholder = document.querySelector(".head-top .placeholder")
if (value) placeholder.style.display = 'none'
else placeholder.style.display = 'block'
})
}
let historySearchList = []
// 获取历史搜索
const getHistorySearch = () => {
const data = JSON.parse(localStorage.getItem("history-search")) || [];
historySearchList = data;
let itemAll = ``
data.forEach((item, index) => itemAll += `<div class="search-box-history-item one-line-display" onclick="searchEvent('${item}')">${item}</div>`) // 绑定事件 searchEvent 点击搜索)
const historyList = document.querySelector('.search-box-history-list')
historyList.innerHTML = itemAll
};
if (location.href.indexOf("/publish") == -1 && location.href.indexOf("/search") == -1) getHistorySearch();
const searchEvent = (value) => {
if (window.innerWidth <= 480) {
redirectToExternalWebsite("/search");
return
}
const kw = value || searchInput.value || hotSearchWords[currentIndex]?.keyword || "";;
if (!kw) return;
historySearchList.unshift(kw);
historySearchList = [...new Set(historySearchList)];
if (historySearchList.length > 10) historySearchList = historySearchList.splice(0, 10);
localStorage.setItem("history-search", JSON.stringify(historySearchList));
redirectToExternalWebsite("/search/" + kw);
searchInput.value = ""
}
let hotSearchWords = [];
const renderingPlaceholder = () => {
let itemAll = ``
hotSearchWords.forEach(item => {
itemAll += `<div class="item one-line-display" >大家都在搜:${item.keyword}</div>`
})
const sliceHotSearchWords = hotSearchWords.slice(0, 2)
sliceHotSearchWords.forEach(item => {
itemAll += `<div class="item one-line-display" >大家都在搜:${item.keyword}</div>`
})
const placeholderBox = document.querySelector('.placeholder .placeholder-box')
placeholderBox.innerHTML = itemAll
}
const getWConfigg = () => {
ajaxGet("/v2/api/config/website").then((res) => {
if (res.code == 200) {
let data = res["data"] || {};
hotSearchWords = data.hotSearchWords || [];
renderingPlaceholder()
data.time = new Date().toISOString();
localStorage.setItem("wConfig", JSON.stringify(data));
}
});
};
const checkWConfig = () => {
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
if (wConfig.time) {
const time = new Date(wConfig.time);
const now = new Date();
if (now - time > 24 * 60 * 60 * 1000) getWConfigg();
else {
hotSearchWords = wConfig.hotSearchWords || [];
renderingPlaceholder()
}
} else getWConfigg();
};
checkWConfig()
const renderCurrentIndex = () => {
const placeholderBox = document.querySelector('.placeholder .placeholder-box')
if (placeholderBox) placeholderBox.style.transform = `translateY(${-currentIndex * 36}px)`
}
let currentIndex = 0; // 当前显示的关键词索引
let carouselTimer = null; // 轮播定时器
// 启动轮播函数
const startCarousel = () => {
// 清除已有的定时器
if (carouselTimer) clearInterval(carouselTimer);
// 设置新的定时器,每秒滚动一次
carouselTimer = setInterval(() => {
if (hotSearchWords.length > 1) {
if (currentIndex >= hotSearchWords.length - 1) {
currentIndex++;
setTimeout(() => {
currentIndex = 0;
}, 2300);
} else currentIndex++;
}
renderCurrentIndex()
}, 2300);
};
startCarousel();
const openHeadPop = () => {
if (window["userInfoWin"]?.uin > 0 || window["userInfoWin"]?.uid > 0) {
// 登录
const headMoreLeft = document.querySelector(".head-pop .head-more-left")
headMoreLeft.innerHTML = `<img class="head-more-userinfo-avatar" src="${window["userInfoWin"]?.avatar}" alt=""><div class="head-more-userinfo-username">${window["userInfoWin"]?.nickname}</div>`
} else {
const avatar = document.querySelector(".head-pop .head-more-userinfo-avatar")
avatar.src = "/img/defaultAvatar.png"
const headMoreRight = document.querySelector(".head-pop .head-more-right")
headMoreRight.style.display = "block"
}
document.querySelector(".head-pop").classList.add("head-pop-show");
}
const skipLoginUrl = (e) => {
if (window["userInfoWin"]?.uin > 0 || window["userInfoWin"]?.uid > 0) { }
else {
e.preventDefault();
go_ajax_Login();
}
}
const crossHeadPop = () => document.querySelector(".head-pop").classList.remove("head-pop-show");
</script>
</body>
</html>

View File

@@ -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(

View File

@@ -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,7 +23,8 @@ const editApp = createApp({
const imgElements = dom.querySelectorAll("img");
imgElements.forEach((imgEl) => {
let url = imgEl.getAttribute("src")?.trim() || "";
const urlObj = new URL(url);
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;
@@ -37,6 +34,9 @@ const editApp = createApp({
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") || null;
const obj = new URL(url);
const aid = obj.searchParams.get("aid");
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("?");
const cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
const queryIndex2 = posterurl.indexOf("?");
const cleanPosterurl = queryIndex2 !== -1 ? posterurl.substring(0, queryIndex2) : posterurl;
cleanUrl = queryIndex !== -1 ? url.substring(0, queryIndex) : url;
} catch (e) {}
result.push({
aid: Number(aid),
posterid: Number(posterid),
@@ -75,15 +87,12 @@ 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;
@@ -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,34 +173,26 @@ 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) {
try {
const res = JSON.parse(xhr.responseText);
if (res.code == 200) {
const data = res.data;
@@ -199,6 +201,10 @@ const editApp = createApp({
creationAlertBox("error", res.message || "上传失败");
reject(res);
}
} catch (e) {
creationAlertBox("error", "解析响应失败");
reject(e);
}
} else {
creationAlertBox("error", "上传失败");
reject(new Error("Upload failed"));
@@ -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 = `<video src="${newUrl}" poster="${coverUrl}" controls="controls" width="300" height="150"></video>`;
resolve({ html: videoHtml });
// Mark as processed (need to wait for insertion)
setTimeout(() => {
const editor = window.tinymce.activeEditor;
if (editor) {
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
if (insertedVideo) {
// We need to access processedNodes from the outer scope if possible,
// but this config object is defined inside initTinyMCE.
// However, processedNodes is defined in setup().
// We can't access it here easily unless we move processedNodes to higher scope or use a global/editor property.
// Let's attach it to the editor instance in setup().
if (editor.processedNodes) {
editor.processedNodes.add(insertedVideo);
}
}
}
}, 500);
creationAlertBox("success", "Video uploaded successfully");
} catch (e) {
console.error("Cover generation failed", e);
const videoHtml = `<video src="${newUrl}" controls="controls" width="300" height="150"></video>`;
resolve({ html: videoHtml });
creationAlertBox("success", "Video uploaded (cover failed)");
}
} else {
creationAlertBox("error", "Video upload failed");
resolve({ html: `<span class="error">Upload failed: ${url}</span>` }); // Or just resolve empty to cancel?
}
})
.catch((err) => {
console.error("Media resolver upload failed", err);
resolve({ html: "" }); // Or fallback to original url? resolve({ html: `<video src="${url}" controls></video>` })
});
// Return early, we will resolve asynchronously
return;
}
}
// Default behavior for other URLs or if logic skipped
resolve({ html: "" }); // Letting it empty might trigger default embed logic?
// Actually, if we return empty string, TinyMCE might just insert nothing or error.
// The default behavior is to rely on promises.
// If we want default behavior, we should probably NOT define this option or call a fallback?
// Wait, media_url_resolver replaces the default logic.
// If we don't handle it, we must return a promise that resolves to HTML.
// If we want the default behavior (creating a video tag for the URL), we have to do it ourselves.
const defaultHtml = `<video src="${url}" width="300" height="150" controls="controls"></video>`;
resolve({ html: defaultHtml });
},
// Handle pasted/dropped images
images_upload_handler: (blobInfo, progress) => {
return new Promise((resolve, reject) => {
if (blobInfo.blob().size > maxSize) {
reject({ message: "图片大小不能超过 20MB", remove: true });
return;
}
uploading(blobInfo.blob(), blobInfo.filename(), "image")
.then((res) => {
resolve(res.url + "?aid=" + res.aid);
})
.catch((err) => {
reject({ message: err.message || "Image upload failed", remove: true });
});
});
},
file_picker_callback: (callback, value, meta) => {
const input = document.createElement("input");
input.setAttribute("type", "file");
if (meta.filetype === "image") {
input.setAttribute("accept", "image/png, image/jpeg, image/jpg");
} else if (meta.filetype === "media") {
input.setAttribute("accept", "video/flv, video/mkv, video/avi, video/rm, video/rmvb, video/mpeg, video/mpg, video/ogg, video/ogv, video/mov, video/wmv, video/mp4, video/webm, video/m4v");
}
input.addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
if (file.size > maxSize) {
creationAlertBox("error", "文件大小不能超过 20MB");
return;
}
if (meta.filetype === "image") {
uploading(file, file.name, "image")
.then((res) => {
callback(res.url + "?aid=" + res.aid, { alt: file.name });
})
.catch((err) => {
console.error(err);
creationAlertBox("error", "Image upload failed");
});
} else if (meta.filetype === "media") {
creationAlertBox("info", "Uploading video, please wait...");
uploading(file, file.name, "video")
.then(async (videoRes) => {
try {
const coverFile = await getVideoFirstFrame(file);
const coverRes = await uploading(coverFile, coverFile.name, "image");
callback(videoRes.url + "?aid=" + videoRes.aid, { poster: coverRes.url + "?aid=" + coverRes.aid });
creationAlertBox("success", "Video uploaded successfully");
} catch (e) {
console.error(e);
callback(videoRes.url + "?aid=" + videoRes.aid);
creationAlertBox("success", "Video uploaded (cover generation failed)");
}
})
.catch((err) => {
console.error(err);
creationAlertBox("error", "Video upload failed");
});
}
});
input.click();
},
setup: (editor) => {
const processedNodes = new WeakSet();
editor.processedNodes = processedNodes; // Expose to editor instance for media resolver access
editor.on("change keyup", () => {
saveStatus.value = "有未保存的更改";
});
// Also try to set content on init as fallback/confirmation
editor.on("init", () => {
// Only if empty (though initialValue should handle it)
if (!editor.getContent() && initialContent) {
editor.setContent(initialContent);
}
// Mark all existing images/videos as processed to prevent auto-upload on click
// This handles the case where initial content contains external images that are already "uploaded"
// or should be treated as such (not auto-uploaded again).
const body = editor.getBody();
const imgs = body.querySelectorAll("img");
const videos = body.querySelectorAll("video");
imgs.forEach((node) => processedNodes.add(node));
videos.forEach((node) => processedNodes.add(node));
});
// Handle network images/videos paste or drop
editor.on("Paste", async (e) => {
const clipboardData = e.clipboardData || window.clipboardData;
const html = clipboardData.getData("text/html");
if (html) {
const div = document.createElement("div");
div.innerHTML = html;
const images = div.querySelectorAll("img");
const videos = div.querySelectorAll("video");
if (images.length > 0) {
for (let img of images) {
const src = img.getAttribute("src");
if (src && !src.includes("?aid=") && src.startsWith("http") && uConfigData && uConfigData.url) {
try {
const formData = new FormData();
formData.append("uploadType", "url");
formData.append("url", src);
if (uConfigData.params && uConfigData.params.data) {
formData.append("data", uConfigData.params.data);
}
const res = await ajax(uConfigData.url, formData);
if (res.code == 200 && res.data) {
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
setTimeout(() => {
const editorContent = editor.getContent();
if (editorContent.includes(src)) {
const newContent = editorContent.replace(src, newUrl);
editor.setContent(newContent);
}
}, 100);
}
} catch (err) {
console.error("Failed to upload network image:", src, err);
}
}
}
}
if (videos.length > 0) {
for (let video of videos) {
const src = video.getAttribute("src");
if (src && !src.includes("?aid=") && src.startsWith("http") && uConfigData && uConfigData.url) {
try {
creationAlertBox("info", "Uploading network video...");
const formData = new FormData();
formData.append("uploadType", "url");
formData.append("url", src);
if (uConfigData.params && uConfigData.params.data) {
formData.append("data", uConfigData.params.data);
}
const res = await ajax(uConfigData.url, formData);
if (res.code == 200 && res.data) {
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
// Generate cover from the new local URL
try {
const coverFile = await getVideoFirstFrame(newUrl);
const coverRes = await uploading(coverFile, "cover.jpg", "image");
const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
setTimeout(() => {
const editorContent = editor.getContent();
// Replace src and add poster
// This regex might need to be more robust
if (editorContent.includes(src)) {
let newContent = editorContent.replace(src, newUrl);
// Find the video tag and inject poster if not present, or replace
// Simple string replace for src is safe, but for poster we need DOM manipulation or regex
// Let's use DOM for precision
const tempDiv = document.createElement("div");
tempDiv.innerHTML = newContent;
const tempVideo = tempDiv.querySelector(`video[src*="${res.data.url}"]`); // Use partial match or ID
if (tempVideo) {
tempVideo.setAttribute("poster", coverUrl);
editor.setContent(tempDiv.innerHTML);
} else {
// Fallback: regex replace the tag
// This is tricky without unique ID.
// Let's just setContent with simple replace first, then update poster via NodeChange logic or DOM
editor.setContent(newContent);
// We can trigger a NodeChange or find it again
}
}
}, 100);
} catch (e) {
console.error("Cover generation failed", e);
// Update video without cover
setTimeout(() => {
const editorContent = editor.getContent();
if (editorContent.includes(src)) {
const newContent = editorContent.replace(src, newUrl);
editor.setContent(newContent);
}
}, 100);
}
}
} catch (err) {
console.error("Failed to upload network video:", src, err);
}
}
}
}
}
});
// Listen for NodeChange to catch inserted images/videos that might be external
editor.on("NodeChange", async (e) => {
const node = e.element;
if (!uConfigData || !uConfigData.url) return;
// Handle Image
if (node.tagName === "IMG" && !node.getAttribute("data-mce-object")) {
// Check if already processed
if (processedNodes.has(node)) return;
const src = node.getAttribute("src");
// Check local or invalid
if (!src || src.startsWith("data:") || src.includes("?aid=") || src.startsWith(location.origin) || src.startsWith("/")) {
processedNodes.add(node);
return;
}
if (src.startsWith("http")) {
// Mark as processed
processedNodes.add(node);
try {
const formData = new FormData();
formData.append("uploadType", "url");
formData.append("url", src);
if (uConfigData.params && uConfigData.params.data) {
formData.append("data", uConfigData.params.data);
}
const res = await ajax(uConfigData.url, formData);
if (res.code == 200 && res.data) {
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
editor.dom.setAttrib(node, "src", newUrl);
editor.dom.setAttrib(node, "data-mce-src", newUrl);
// Remove from processedNodes? No, because now src has ?aid= so it will be ignored anyway.
// And if we undo, node is new.
} else {
console.warn("Upload failed for", src);
}
} catch (err) {
console.error("Failed to upload network image on NodeChange:", src, err);
// Do NOT remove from processedNodes to avoid retry loop on persistent error
}
}
}
// Handle Video
// TinyMCE 6 with media_live_embeds: true uses <video> or <span class="mce-object-video">
if (node.tagName === "VIDEO" || (node.tagName === "IMG" && node.getAttribute("data-mce-object") === "video")) {
// Check if already processed
if (processedNodes.has(node)) return;
let src = node.getAttribute("src");
// If it's a placeholder img, the src usually points to a transparent pixel, real src is in data-mce-p-src
if (node.tagName === "IMG") {
src = node.getAttribute("data-mce-p-src") || src;
}
// Check local or invalid
if (!src || src.startsWith("data:") || src.includes("?aid=") || src.startsWith(location.origin) || src.startsWith("/")) {
processedNodes.add(node);
return;
}
if (src.startsWith("http")) {
// Mark as processed
processedNodes.add(node);
try {
creationAlertBox("info", "Uploading network video...");
const formData = new FormData();
formData.append("uploadType", "url");
formData.append("url", src);
if (uConfigData.params && uConfigData.params.data) {
formData.append("data", uConfigData.params.data);
}
const res = await ajax(uConfigData.url, formData);
if (res.code == 200 && res.data) {
const newUrl = `${res.data.url}?aid=${res.data.aid}`;
// Update URL first
if (node.tagName === "VIDEO") {
editor.dom.setAttrib(node, "src", newUrl);
editor.dom.setAttrib(node, "data-mce-src", newUrl);
} else {
editor.dom.setAttrib(node, "data-mce-p-src", newUrl);
}
// Generate and set poster
try {
const coverFile = await getVideoFirstFrame(newUrl);
const coverRes = await uploading(coverFile, "cover.jpg", "image");
const coverUrl = `${coverRes.url}?aid=${coverRes.aid}`;
// Instead of just setting attributes, replace the entire node with a proper video tag
// This ensures consistency and proper rendering in TinyMCE
const videoHtml = `<video src="${newUrl}" poster="${coverUrl}" controls="controls" width="300" height="150"></video>`;
editor.selection.select(node);
editor.insertContent(videoHtml);
// Mark the new video node as processed
// We need to find the newly inserted video node
// insertContent might leave the cursor after the video.
// A simple way is to query by src again, but that might be ambiguous.
// However, since we just inserted it, it should be safe.
setTimeout(() => {
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
if (insertedVideo) {
processedNodes.add(insertedVideo);
}
}, 0);
creationAlertBox("success", "Video auto-uploaded with cover");
} catch (e) {
console.error("Auto cover generation failed", e);
// Even if cover fails, we should update the src properly if it was a placeholder
if (node.tagName === "IMG") {
const videoHtml = `<video src="${newUrl}" controls="controls" width="300" height="150"></video>`;
editor.selection.select(node);
editor.insertContent(videoHtml);
setTimeout(() => {
const insertedVideo = editor.dom.select(`video[src="${newUrl}"]`)[0];
if (insertedVideo) processedNodes.add(insertedVideo);
}, 0);
}
creationAlertBox("success", "Video auto-uploaded (cover failed)");
}
}
} catch (err) {
console.error("Failed to upload network video on NodeChange:", src, err);
}
}
}
});
},
};
window.tinymce.init(editorConfig);
};
const handleTitleInput = () => {
saveStatus.value = "有未保存的更改";
};
@@ -309,9 +749,9 @@ const editApp = createApp({
});
onUnmounted(() => {
if (editor == null) return;
if (editor.destroy) editor.destroy();
editor = null;
if (window.tinymce && window.tinymce.activeEditor) {
window.tinymce.activeEditor.destroy();
}
});
return {

1
js/tinymce/icons/default/icons.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
js/tinymce/models/dom/model.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
/**
* TinyMCE version 6.8.2 (2023-12-11)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),w=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),h=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),w);if(!h)return null;let v=h.container;const _=k.backwards(h.container,h.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),w),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(h.container,h.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=4&&b.substr(0,4)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();

File diff suppressed because one or more lines are too long

4
js/tinymce/plugins/code/plugin.min.js vendored Normal file
View File

@@ -0,0 +1,4 @@
/**
* TinyMCE version 6.8.2 (2023-12-11)
*/
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
/**
* TinyMCE version 6.8.2 (2023-12-11)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=t=>e=>typeof e===t,o=t=>"string"===(t=>{const e=typeof t;return null===t?"null":"object"===e&&Array.isArray(t)?"array":"object"===e&&(o=r=t,(n=String).prototype.isPrototypeOf(o)||(null===(i=r.constructor)||void 0===i?void 0:i.name)===n.name)?"string":e;var o,r,n,i})(t),r=e("boolean"),n=t=>!(t=>null==t)(t),i=e("function"),s=e("number"),l=(!1,()=>false);class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return n(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=(t,e)=>{for(let o=0,r=t.length;o<r;o++)e(t[o],o)},c=t=>{if(null==t)throw new Error("Node cannot be null or undefined");return{dom:t}},d=c,h=(t,e)=>{const o=t.dom;if(1!==o.nodeType)return!1;{const t=o;if(void 0!==t.matches)return t.matches(e);if(void 0!==t.msMatchesSelector)return t.msMatchesSelector(e);if(void 0!==t.webkitMatchesSelector)return t.webkitMatchesSelector(e);if(void 0!==t.mozMatchesSelector)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")}};"undefined"!=typeof window?window:Function("return this;")();const m=t=>e=>(t=>t.dom.nodeType)(e)===t,g=m(1),f=m(3),v=m(9),y=m(11),p=(t,e)=>{t.dom.removeAttribute(e)},w=i(Element.prototype.attachShadow)&&i(Node.prototype.getRootNode)?t=>d(t.dom.getRootNode()):t=>v(t)?t:d(t.dom.ownerDocument),b=t=>d(t.dom.host),N=t=>{const e=f(t)?t.dom.parentNode:t.dom;if(null==e||null===e.ownerDocument)return!1;const o=e.ownerDocument;return(t=>{const e=w(t);return y(o=e)&&n(o.dom.host)?a.some(e):a.none();var o})(d(e)).fold((()=>o.body.contains(e)),(r=N,i=b,t=>r(i(t))));var r,i},S=t=>"rtl"===((t,e)=>{const o=t.dom,r=window.getComputedStyle(o).getPropertyValue(e);return""!==r||N(t)?r:((t,e)=>(t=>void 0!==t.style&&i(t.style.getPropertyValue))(t)?t.style.getPropertyValue(e):"")(o,e)})(t,"direction")?"rtl":"ltr",A=(t,e)=>((t,o)=>((t,e)=>{const o=[];for(let r=0,n=t.length;r<n;r++){const n=t[r];e(n,r)&&o.push(n)}return o})(((t,e)=>{const o=t.length,r=new Array(o);for(let n=0;n<o;n++){const o=t[n];r[n]=e(o,n)}return r})(t.dom.childNodes,d),(t=>h(t,e))))(t),E=("li",t=>g(t)&&"li"===t.dom.nodeName.toLowerCase());const T=(t,e,n)=>{u(e,(e=>{const c=d(e),m=E(c),f=((t,e)=>{return(e?(o=t,r="ol,ul",((t,e,o)=>{let n=t.dom;const s=i(o)?o:l;for(;n.parentNode;){n=n.parentNode;const t=d(n);if(h(t,r))return a.some(t);if(s(t))break}return a.none()})(o,0,n)):a.some(t)).getOr(t);var o,r,n})(c,m);var v;(v=f,(t=>a.from(t.dom.parentNode).map(d))(v).filter(g)).each((e=>{if(t.setStyle(f.dom,"direction",null),S(e)===n?p(f,"dir"):((t,e,n)=>{((t,e,n)=>{if(!(o(n)||r(n)||s(n)))throw console.error("Invalid call to Attribute.set. Key ",e,":: Value ",n,":: Element ",t),new Error("Attribute value was not simple");t.setAttribute(e,n+"")})(t.dom,e,n)})(f,"dir",n),S(f)!==n&&t.setStyle(f.dom,"direction",n),m){const e=A(f,"li[dir],li[style]");u(e,(e=>{p(e,"dir"),t.setStyle(e.dom,"direction",null)}))}}))}))},C=(t,e)=>{t.selection.isEditable()&&(T(t.dom,t.selection.getSelectedBlocks(),e),t.nodeChanged())},D=(t,e)=>o=>{const r=r=>{const n=d(r.element);o.setActive(S(n)===e),o.setEnabled(t.selection.isEditable())};return t.on("NodeChange",r),o.setEnabled(t.selection.isEditable()),()=>t.off("NodeChange",r)};t.add("directionality",(t=>{(t=>{t.addCommand("mceDirectionLTR",(()=>{C(t,"ltr")})),t.addCommand("mceDirectionRTL",(()=>{C(t,"rtl")}))})(t),(t=>{t.ui.registry.addToggleButton("ltr",{tooltip:"Left to right",icon:"ltr",onAction:()=>t.execCommand("mceDirectionLTR"),onSetup:D(t,"ltr")}),t.ui.registry.addToggleButton("rtl",{tooltip:"Right to left",icon:"rtl",onAction:()=>t.execCommand("mceDirectionRTL"),onSetup:D(t,"rtl")})})(t)}))}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
js/tinymce/plugins/link/plugin.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
/**
* TinyMCE version 6.8.2 (2023-12-11)
*/
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>t=>t.options.get(e),i=n("content_style"),s=n("content_css_cors"),c=n("body_class"),r=n("body_id");e.add("preview",(e=>{(e=>{e.addCommand("mcePreview",(()=>{(e=>{const n=(e=>{var n;let l="";const a=e.dom.encode,d=null!==(n=i(e))&&void 0!==n?n:"";l+='<base href="'+a(e.documentBaseURI.getURI())+'">';const m=s(e)?' crossorigin="anonymous"':"";o.each(e.contentCSS,(t=>{l+='<link type="text/css" rel="stylesheet" href="'+a(e.documentBaseURI.toAbsolute(t))+'"'+m+">"})),d&&(l+='<style type="text/css">'+d+"</style>");const y=r(e),u=c(e),v='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !('+(t.os.isMacOS()||t.os.isiOS()?"e.metaKey":"e.ctrlKey && !e.altKey")+")) {e.preventDefault();}}}, false);<\/script> ",p=e.getBody().dir,w=p?' dir="'+a(p)+'"':"";return"<!DOCTYPE html><html><head>"+l+'</head><body id="'+a(y)+'" class="mce-content-body '+a(u)+'"'+w+">"+e.getContent()+v+"</body></html>"})(e);e.windowManager.open({title:"Preview",size:"large",body:{type:"panel",items:[{name:"preview",type:"iframe",sandboxed:!0,transparent:!1}]},buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{preview:n}}).focus("close")})(e)}))})(e),(e=>{const t=()=>e.execCommand("mcePreview");e.ui.registry.addButton("preview",{icon:"preview",tooltip:"Preview",onAction:t}),e.ui.registry.addMenuItem("preview",{icon:"preview",text:"Preview",onAction:t})})(e)}))}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
/**
* TinyMCE version 6.8.2 (2023-12-11)
*/
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const s=(t,s,o)=>{t.dom.toggleClass(t.getBody(),"mce-visualblocks"),o.set(!o.get()),((t,s)=>{t.dispatch("VisualBlocks",{state:s})})(t,o.get())},o=("visualblocks_default_state",t=>t.options.get("visualblocks_default_state"));const e=(t,s)=>o=>{o.setActive(s.get());const e=t=>o.setActive(t.state);return t.on("VisualBlocks",e),()=>t.off("VisualBlocks",e)};t.add("visualblocks",((t,l)=>{(t=>{(0,t.options.register)("visualblocks_default_state",{processor:"boolean",default:!1})})(t);const a=(t=>{let s=!1;return{get:()=>s,set:t=>{s=t}}})();((t,o,e)=>{t.addCommand("mceVisualBlocks",(()=>{s(t,0,e)}))})(t,0,a),((t,s)=>{const o=()=>t.execCommand("mceVisualBlocks");t.ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:o,onSetup:e(t,s)}),t.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:o,onSetup:e(t,s)})})(t,a),((t,e,l)=>{t.on("PreviewFormats AfterPreviewFormats",(s=>{l.get()&&t.dom.toggleClass(t.getBody(),"mce-visualblocks","afterpreviewformats"===s.type)})),t.on("init",(()=>{o(t)&&s(t,0,l)}))})(t,0,a)}))}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
js/tinymce/themes/silver/theme.min.js vendored Normal file

File diff suppressed because one or more lines are too long

4
js/tinymce/tinymce.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="en">
@@ -8,10 +7,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发布主题</title>
<link href="https://framework.x-php.com/gter/forum/css/normalize.min.css" rel="stylesheet">
<link href="https://framework.x-php.com/gter/forum/css/editorStyle.css" rel="stylesheet">
<!-- <link href="https://framework.x-php.com/gter/forum/css/editorStyle.css" rel="stylesheet"> -->
<script src="https://framework.x-php.com/gter/forum/js/vue.global.js"></script>
<link rel="stylesheet" href="/css/katex.min.css">
<script src="/js/katex.min.js"></script>
<script src="https://framework.x-php.com/gter/forum/js/tinymce/tinymce.min.js"></script>
<link rel="stylesheet" href="katex/css/katex.min.css">
<script src="katex/js/katex.min.js"></script>
<!-- <link rel="stylesheet" href="/css/textbus.min.css"> -->
<!-- <script src="/js/textbus.min.js"></script> -->
@@ -24,9 +24,20 @@
color: #333;
}
#edit {
height: calc(100% - 130px);
}
#edit * {
box-sizing: border-box;
}
#top-container {
border-bottom: 1px solid #e8e8e8;
padding-left: 30px;
padding-top: 10px;
padding-bottom: 10px;
display: block;
}
#editor-toolbar {
@@ -51,11 +62,14 @@
box-shadow: 0 2px 10px rgb(0 0 0 / 12%); */
width: 100vh;
height: calc(100% - 40px);
margin: 20px auto 20px auto;
background-color: #fff;
padding: 10px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 10px rgb(0 0 0 / 12%);
display: flex;
flex-direction: column;
}
#title-container {
@@ -71,11 +85,11 @@
line-height: 1;
}
#editor-text-area {
margin-top: 20px;
/* #editor-text-area { */
.tox.tox-tinymce {
/* height: 500px; */
/* max-height: 80vh; */
height: calc(100vh - 370px);
/* height: calc(100vh - 330px) !important; */
font-size: 18px;
line-height: 1.5;
color: rgb(51, 51, 51);
@@ -83,6 +97,8 @@
.bottom-bar {
position: fixed;
left: 0;
@@ -191,17 +207,17 @@
.action-buttons .right-section .publish-btn:hover {
background-color: #40d1aa;
}
.tox-tinymce {
border: none !important;
}
</style>
</head>
<body>
<script src="https://app.gter.net/bottom?tpl=header&menukey=bbs"></script>
<div class="container" id="edit" v-cloak>
<div id="top-container">
<p>
<a href="./">&lt;&lt; 返回</a>
</p>
</div>
<a href="./" id="top-container">&lt;&lt; 返回</a>
<div style="border-bottom: 1px solid #e8e8e8;">
<div id="editor-toolbar"></div>
</div>
@@ -232,10 +248,11 @@
</div>
</div>
<script type="module" src="https://framework.x-php.com/gter/forum/js/editor.js"></script>
<script src="https://framework.x-php.com/gter/forum/js/axios.min.js"></script>
<script src="https://framework.x-php.com/gter/forum/js/public.js"></script>
<script type="module" src="https://framework.x-php.com/gter/forum/js/publish_admin.js"></script>
<script src="/js/public.js"></script>
<script type="module" src="/js/publish_admin.js"></script>
<!-- <script src="https://framework.x-php.com/gter/forum/js/public.js"></script>
<script type="module" src="https://framework.x-php.com/gter/forum/js/publish_admin.js"></script> -->
</body>
</html>