refactor(editor): 重构编辑器组件及样式,优化功能实现

重构编辑器工具栏样式及功能,使用wangEditor替换原有实现
优化图片和视频上传逻辑,增加自定义校验和上传处理
调整编辑器样式,修复对齐功能及段落标题样式
更新表情选择器位置逻辑,支持上下方向显示
统一组件导入方式,添加版本控制参数防止缓存
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-11-26 19:01:26 +08:00
parent 460450c339
commit 275b78b221
23 changed files with 678 additions and 320 deletions

View File

@@ -1,7 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref, inject } = Vue; const { defineComponent, ref, inject } = Vue;
import { like } from "../like/like.js";
const { like } = await import(withVer("../like/like.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemBottom = defineComponent({ export const itemBottom = defineComponent({
name: "item-bottom", name: "item-bottom",

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref } = Vue; const { defineComponent, ref } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemForum = defineComponent({ export const itemForum = defineComponent({

View File

@@ -1,7 +1,8 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref, provide, onMounted, inject } = Vue; const { defineComponent, ref, provide, onMounted, inject } = Vue;
import { report } from "../report/report.js";
const { report } = await import(withVer("../report/report.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemHead = defineComponent({ export const itemHead = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref } = Vue; const { defineComponent, ref } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemMj = defineComponent({ export const itemMj = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref, provide } = Vue; const { defineComponent, ref, provide } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemOffer = defineComponent({ export const itemOffer = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref } = Vue; const { defineComponent, ref } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemSummary = defineComponent({ export const itemSummary = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref } = Vue; const { defineComponent, ref } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemTenement = defineComponent({ export const itemTenement = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref } = Vue; const { defineComponent, ref } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const itemVote = defineComponent({ export const itemVote = defineComponent({

View File

@@ -1,8 +1,9 @@
// my-component.js // my-component.js
// 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window // 引入全局 Vue 对象(因在 HTML 中通过 script 引入Vue 已挂载到 window
const { defineComponent, ref, onMounted, nextTick } = Vue; const { defineComponent, ref, onMounted, nextTick } = Vue;
import { itemBottom } from "../item-bottom/item-bottom.js";
import { itemHead } from "../item-head/item-head.js"; const { itemBottom } = await import(withVer("../item-bottom/item-bottom.js"));
const { itemHead } = await import(withVer("../item-head/item-head.js"));
// 定义组件(直接使用模板) // 定义组件(直接使用模板)
export const latestList = defineComponent({ export const latestList = defineComponent({

View File

@@ -77,7 +77,14 @@ export const slideshowBox = defineComponent({
tabPitch.value = key; tabPitch.value = key;
}; };
return { tabItem, tabPitch, tabPitch, latestList }; const handleCheckAttest = (e) => {
if (realname.value === 0 && userInfoWin.value?.uin > 0) {
openAttest();
e.preventDefault(); // 阻止默认跳转(即使 href 为链接,也强制拦截)
}
};
return { handleCheckAttest, tabItem, tabPitch, tabPitch, latestList };
}, },
template: `<div class="box-box" :class="['box-' + tabPitch]"> <div class="slideshow-box"> <div class="tab-list flexacenter"> <!-- <div class="tab-item thread" :class="{'pitch': tabPitch == 'thread'}" @click="tabItem('thread')">帖子</div> --> <div class="tab-item offer" :class="{'pitch': tabPitch == 'offer'}" @click="tabItem('offer')">Offer</div> <div class="tab-item vote" :class="{'pitch': tabPitch == 'vote'}" @click="tabItem('vote')">投票</div> <div class="tab-item interviewexperience" :class="{'pitch': tabPitch == 'interviewexperience'}" @click="tabItem('interviewexperience')">面经</div> </div> </div> <div class="slideshow-content flexflex"> <!-- 问答 --> <!-- <div class="thread-side-box side-box"> <div class="box"> <a v-for="item in latestList.thread" :key="item.uniqid" class="item" target="_blank" :href="'/details/' + item.uniqid"> <div class="question flexacenter"> <div class="text flex1 ellipsis">{{ item.title }}</div> </div> <div class="answer flexacenter"> <div class="text flex1"> <div class="texttext">{{ item.content }}</div> </div> </div> </a> <a class="add-btn flexcenter" href="https://ask.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> --> <!-- offer --> <div class="offer-side-box side-box"> <div class="box"> <a v-for="item in latestList.offer" :key="item.uniqid" class="item flexflex" :href="'/details/' + item.uniqid" target="_blank"> <img class="school-img" :src="item.data.schoollogo" /> <div class="school-detail flex1 flexflex"> <div class="school-name one-line-display">{{ item.data.schoolname }}</div> <div class="school-brief one-line-display">{{ item.data.professional }}</div> <div class="school-offer flexacenter"> <span>{{ item.data.degree }}</span> <span class="long-string">|</span> <span>{{ item.data.semester }}</span> <span class="long-string">|</span> <span>{{ item.data.apply_results_text }}</span> </div> </div> </a> <a class="add-btn flexcenter" href="https://offer.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> <!-- 投票 --> <div class="vote-side-box side-box"> <div class="box"> <a v-for="item in latestList.vote" :key="item.uniqid" class="item flexflex vuehide" target="_blank" :href="'/details/' + item.uniqid"> <div class="name one-line-display">{{ item.title }}</div> <div class="brief">{{ item.content }}</div> </a> <a class="add-btn flexcenter" href="https://vote.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> <!-- mj --> <div class="interviewexperience-side-box side-box"> <div class="box"> <a v-for="item in latestList.interviewexperience" :key="item.thread_id" class="item flexflex" :href="'/details/' + item.uniqid" target="_blank"> <div class="school one-line-display">{{ item.data.schoolname }}</div> <div class="major one-line-display">{{ item.data.project }}</div> <div class="info"> <img class="icon" :src="item.user.avatar" /> <span class="text">{{ item.title || item.content }} </span> </div> </a> <a class="add-btn flexcenter" href="https://interviewexperience.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> </div></div>`, template: `<div class="box-box" :class="['box-' + tabPitch]"> <div class="slideshow-box"> <div class="tab-list flexacenter"> <!-- <div class="tab-item thread" :class="{'pitch': tabPitch == 'thread'}" @click="tabItem('thread')">帖子</div> --> <div class="tab-item offer" :class="{'pitch': tabPitch == 'offer'}" @click="tabItem('offer')">Offer</div> <div class="tab-item vote" :class="{'pitch': tabPitch == 'vote'}" @click="tabItem('vote')">投票</div> <div class="tab-item interviewexperience" :class="{'pitch': tabPitch == 'interviewexperience'}" @click="tabItem('interviewexperience')">面经</div> </div> </div> <div class="slideshow-content flexflex"> <!-- 问答 --> <!-- <div class="thread-side-box side-box"> <div class="box"> <a v-for="item in latestList.thread" :key="item.uniqid" class="item" target="_blank" :href="'/details/' + item.uniqid"> <div class="question flexacenter"> <div class="text flex1 ellipsis">{{ item.title }}</div> </div> <div class="answer flexacenter"> <div class="text flex1"> <div class="texttext">{{ item.content }}</div> </div> </div> </a> <a class="add-btn flexcenter" href="https://ask.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> --> <!-- offer --> <div class="offer-side-box side-box"> <div class="box"> <a v-for="item in latestList.offer" :key="item.uniqid" class="item flexflex" :href="'/details/' + item.uniqid" target="_blank"> <img class="school-img" :src="item.data.schoollogo" /> <div class="school-detail flex1 flexflex"> <div class="school-name one-line-display">{{ item.data.schoolname }}</div> <div class="school-brief one-line-display">{{ item.data.professional }}</div> <div class="school-offer flexacenter"> <span>{{ item.data.degree }}</span> <span class="long-string">|</span> <span>{{ item.data.semester }}</span> <span class="long-string">|</span> <span>{{ item.data.apply_results_text }}</span> </div> </div> </a> <a class="add-btn flexcenter" href="https://offer.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> <!-- 投票 --> <div class="vote-side-box side-box"> <div class="box"> <a v-for="item in latestList.vote" :key="item.uniqid" class="item flexflex vuehide" target="_blank" :href="'/details/' + item.uniqid"> <div class="name one-line-display">{{ item.title }}</div> <div class="brief">{{ item.content }}</div> </a> <a class="add-btn flexcenter" href="https://vote.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> <!-- mj --> <div class="interviewexperience-side-box side-box"> <div class="box"> <a v-for="item in latestList.interviewexperience" :key="item.thread_id" class="item flexflex" :href="'/details/' + item.uniqid" target="_blank"> <div class="school one-line-display">{{ item.data.schoolname }}</div> <div class="major one-line-display">{{ item.data.project }}</div> <div class="info"> <img class="icon" :src="item.user.avatar" /> <span class="text">{{ item.title || item.content }} </span> </div> </a> <a class="add-btn flexcenter" href="https://interviewexperience.gter.net" target="_blank"> <div>more</div> <img class="" style="margin-left: 8px;" src="/img/right-arrow-black.svg"> </a> </div> </div> </div></div>`,

View File

@@ -761,6 +761,15 @@
font-size: 22px; font-size: 22px;
padding: 8px; padding: 8px;
} }
.answer-discuss .input-box .bottom .operate .item .emoji-box.top {
top: inherit;
bottom: 36px;
}
.answer-discuss .input-box .bottom .operate .item .emoji-box.top::after {
bottom: -8px;
top: inherit;
transform: translateX(-50%) rotate(180deg);
}
.answer-discuss .input-box .bottom .operate .item .emoji-box::after { .answer-discuss .input-box .bottom .operate .item .emoji-box::after {
content: ""; content: "";
width: 0; width: 0;

View File

@@ -878,16 +878,17 @@
cursor: pointer; cursor: pointer;
} }
.answer-discuss .input-box .bottom .operate .item {
}
.answer-discuss .input-box .bottom .operate .item .emoji-box { .answer-discuss .input-box .bottom .operate .item .emoji-box {
width: 582px; width: 582px;
border-radius: 8px; border-radius: 8px;
background-color: #fff; background-color: #fff;
filter: drop-shadow(0 0 11px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 0 11px rgba(0, 0, 0, 0.1));
// top: 45px;
top: 36px; top: 36px;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
// left: -14px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
border: 1px solid #ebebeb; border: 1px solid #ebebeb;
@@ -895,20 +896,30 @@
flex-wrap: wrap; flex-wrap: wrap;
font-size: 22px; font-size: 22px;
padding: 8px; padding: 8px;
}
.answer-discuss .input-box .bottom .operate .item .emoji-box::after { &.top {
content: ""; top: inherit;
width: 0; bottom: 36px;
height: 0;
border-left: 8px solid transparent; &::after {
border-right: 8px solid transparent; bottom: -8px;
border-bottom: 8px solid #ffffff; top: inherit;
position: absolute; transform: translateX(-50%) rotate(180deg);
top: -8px; }
// left: 15px; }
left: 50%;
transform: translateX(-50%); &::after {
content: "";
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #ffffff;
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
}
} }
.answer-discuss .input-box .bottom .operate .item .emoji-box .emoji-icon { .answer-discuss .input-box .bottom .operate .item .emoji-box .emoji-icon {

View File

@@ -73,18 +73,44 @@
color: #555; color: #555;
font-size: 14px; font-size: 14px;
} }
#edit .edit-container .editor-toolbar { #edit .edit-container #editorwrapper {
z-index: 100;
}
#edit .edit-container #editorwrapper .bold {
font-weight: bold;
}
#edit .edit-container #editorwrapper .editor-toolbar {
height: 36px; height: 36px;
line-height: 36px;
background-color: #fbfbfb; background-color: #fbfbfb;
padding-left: 25px; padding-left: 25px;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
} }
#edit .edit-container .editor-toolbar .toolbar-item { #edit .edit-container #editorwrapper .editor-toolbar .w-e-panel-content-emotion {
width: 490px;
}
#edit .edit-container #editorwrapper .editor-toolbar .w-e-bar-item-group .w-e-bar-item-menus-container {
margin-top: 30px;
}
#edit .edit-container #editorwrapper .editor-toolbar > .w-e-bar {
background-color: transparent;
}
#edit .edit-container #editorwrapper .editor-toolbar .w-e-toolbar {
padding: 0;
}
#edit .edit-container #editorwrapper .editor-toolbar .w-e-toolbar > .w-e-bar-item {
margin-right: 40px;
}
#edit .edit-container #editorwrapper .editor-toolbar .w-e-toolbar > .w-e-bar-item > button {
color: #000000;
padding: 0 10px;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item {
cursor: pointer; cursor: pointer;
height: 30px; height: 30px !important;
line-height: 30px; line-height: 30px !important;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
@@ -93,15 +119,21 @@
line-height: 23px; line-height: 23px;
margin-right: 40px; margin-right: 40px;
position: relative; position: relative;
padding: 0 10px; padding: 0;
border-radius: 50px;
} }
#edit .edit-container .editor-toolbar .toolbar-item .icon { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-right: 5px; margin-right: 5px;
} }
#edit .edit-container .editor-toolbar .toolbar-item .file { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item > button {
padding: 0 10px;
border-radius: 50px;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item > button.active {
background-color: #f6f6bd;
}
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .file {
opacity: 0; opacity: 0;
/* 隐藏输入框 */ /* 隐藏输入框 */
background: transparent; background: transparent;
@@ -113,7 +145,7 @@
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
} }
#edit .edit-container .editor-toolbar .toolbar-item .file::after { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .file::after {
content: ""; content: "";
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -121,14 +153,14 @@
top: 0; top: 0;
left: 0; left: 0;
} }
#edit .edit-container .editor-toolbar .toolbar-item.expression.pitch .emoji-box-mask { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.expression.pitch .emoji-box-mask {
display: block; display: block;
} }
#edit .edit-container .editor-toolbar .toolbar-item.expression.pitch .emoji-box { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.expression.pitch .emoji-box {
display: flex; display: flex;
} }
#edit .edit-container .editor-toolbar .toolbar-item .link-box-mask, #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .link-box-mask,
#edit .edit-container .editor-toolbar .toolbar-item .emoji-box-mask { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .emoji-box-mask {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -137,10 +169,10 @@
z-index: 1; z-index: 1;
display: none; display: none;
} }
#edit .edit-container .editor-toolbar .toolbar-item .link-box-mask { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .link-box-mask {
background: transparent; background: transparent;
} }
#edit .edit-container .editor-toolbar .toolbar-item .emoji-box { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .emoji-box {
width: 582px; width: 582px;
border-radius: 8px; border-radius: 8px;
background-color: #fff; background-color: #fff;
@@ -156,7 +188,7 @@
font-size: 22px; font-size: 22px;
padding: 8px; padding: 8px;
} }
#edit .edit-container .editor-toolbar .toolbar-item .emoji-box::after { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .emoji-box::after {
content: ""; content: "";
width: 0; width: 0;
height: 0; height: 0;
@@ -168,17 +200,17 @@
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
#edit .edit-container .editor-toolbar .toolbar-item .emoji-box .emoji-icon { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item .emoji-box .emoji-icon {
margin: 5px; margin: 5px;
cursor: pointer; cursor: pointer;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link.pitch .link-box-mask { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link.pitch .link-box-mask {
display: block; display: block;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link.pitch .link-box { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link.pitch .link-box {
display: flex; display: flex;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box {
background-color: #ffffff; background-color: #ffffff;
border-radius: 6px; border-radius: 6px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.16862745); box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.16862745);
@@ -193,11 +225,11 @@
flex-direction: column; flex-direction: column;
padding: 18px 20px 0; padding: 18px 20px 0;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .item { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box .item {
margin-bottom: 22px; margin-bottom: 22px;
flex-direction: column; flex-direction: column;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .item .name { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box .item .name {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
@@ -206,7 +238,7 @@
line-height: 26px; line-height: 26px;
margin-bottom: 8px; margin-bottom: 8px;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .item .input { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box .item .input {
height: 36px; height: 36px;
border: 1px solid #d7d7d7; border: 1px solid #d7d7d7;
border-radius: 7px; border-radius: 7px;
@@ -214,7 +246,7 @@
font-size: 14px; font-size: 14px;
color: #000000; color: #000000;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .btn { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box .btn {
width: 72px; width: 72px;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
@@ -228,12 +260,27 @@
color: #000000; color: #000000;
margin-top: 8px; margin-top: 8px;
} }
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .btn:hover { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.link .link-box .btn:hover {
background-color: #23e0b6; background-color: #23e0b6;
} }
#edit .edit-container .editor-toolbar .toolbar-item.h2.pitch { #edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.h2.pitch {
background-color: #f6f6bd; background-color: #f6f6bd;
} }
#edit .edit-container #editorwrapper .editor-toolbar .toolbar-item.active > button {
background-color: #f6f6bd;
}
#edit .edit-container #editorwrapper #editor-container {
min-height: 500px;
max-height: 80vh;
font-size: 18px;
line-height: 26px;
color: #333333;
}
#edit .edit-container #editorwrapper #editor-container a {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
text-decoration: underline;
color: #04b0d5;
}
#edit .edit-container .content-input { #edit .edit-container .content-input {
min-height: 509px; min-height: 509px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;

View File

@@ -82,192 +82,253 @@
} }
} }
.editor-toolbar { #editor—wrapper {
height: 36px; z-index: 100;
background-color: rgba(251, 251, 251, 1);
padding-left: 25px;
position: sticky;
top: 0;
z-index: 10;
.toolbar-item { .bold {
.icon { font-weight: bold;
width: 16px; }
height: 16px;
margin-right: 5px; .editor-toolbar {
height: 36px;
line-height: 36px;
background-color: rgba(251, 251, 251, 1);
padding-left: 25px;
position: sticky;
top: 0;
z-index: 10;
.w-e-panel-content-emotion {
width: 490px;
}
.w-e-bar-item-group .w-e-bar-item-menus-container {
margin-top: 30px;
} }
cursor: pointer; > .w-e-bar {
height: 30px; background-color: transparent;
line-height: 30px; }
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; .w-e-toolbar {
font-weight: 400; padding: 0;
font-style: normal; // .w-e-bar-item {
font-size: 14px; // padding: 0 10px;
color: #000000; // }
line-height: 23px;
margin-right: 40px; > .w-e-bar-item {
position: relative; margin-right: 40px;
padding: 0 10px; > button {
border-radius: 50px; color: #000000;
padding: 0 10px;
}
}
}
.toolbar-item {
.icon {
width: 16px;
height: 16px;
margin-right: 5px;
}
.file {
opacity: 0; /* 隐藏输入框 */
background: transparent;
border: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer; cursor: pointer;
height: 30px !important;
line-height: 30px !important;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #000000;
line-height: 23px;
margin-right: 40px;
position: relative;
padding: 0;
&::after { > button {
content: ""; padding: 0 10px;
width: 100%; border-radius: 50px;
height: 100%;
&.active {
background-color: rgba(246, 246, 189, 1);
}
}
.file {
opacity: 0; /* 隐藏输入框 */
background: transparent;
border: none;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
} width: 100%;
} height: 100%;
&.expression.pitch {
.emoji-box-mask {
display: block;
}
.emoji-box {
display: flex;
}
}
.link-box-mask,
.emoji-box-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
// background-color: rgba(0, 0, 0, 0.20392157);
display: none;
}
.link-box-mask {
background: transparent;
}
.emoji-box {
width: 582px;
border-radius: 8px;
background-color: #fff;
filter: drop-shadow(0 0 11px rgba(0, 0, 0, 0.1));
top: 45px;
position: absolute;
z-index: 1;
left: 50%;
transform: translateX(-50%);
border: 1px solid #ebebeb;
display: none;
flex-wrap: wrap;
font-size: 22px;
padding: 8px;
&::after {
content: "";
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #ffffff;
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
}
.emoji-icon {
margin: 5px;
cursor: pointer; cursor: pointer;
}
}
&.link { &::after {
&.pitch { content: "";
.link-box-mask { width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
}
&.expression.pitch {
.emoji-box-mask {
display: block; display: block;
} }
.link-box { .emoji-box {
display: flex; display: flex;
} }
} }
.link-box { .link-box-mask,
background-color: rgba(255, 255, 255, 1); .emoji-box-mask {
border-radius: 6px; position: fixed;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.168627450980392); top: 0;
left: 0;
width: 336px; width: 100%;
height: 282px; height: 100%;
position: absolute;
top: 30px;
left: 0%;
z-index: 1; z-index: 1;
// background-color: rgba(0, 0, 0, 0.20392157);
display: none;
}
.link-box-mask {
background: transparent;
}
.emoji-box {
width: 582px;
border-radius: 8px;
background-color: #fff;
filter: drop-shadow(0 0 11px rgba(0, 0, 0, 0.1));
top: 45px;
position: absolute;
z-index: 1;
left: 50%;
transform: translateX(-50%);
border: 1px solid #ebebeb; border: 1px solid #ebebeb;
display: none; display: none;
flex-direction: column; flex-wrap: wrap;
padding: 18px 20px 0; font-size: 22px;
padding: 8px;
.item { &::after {
margin-bottom: 22px; content: "";
flex-direction: column; width: 0;
.name { height: 0;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; border-left: 8px solid transparent;
font-weight: 400; border-right: 8px solid transparent;
font-style: normal; border-bottom: 8px solid #ffffff;
font-size: 14px; position: absolute;
color: #555555; top: -8px;
line-height: 26px; left: 50%;
margin-bottom: 8px; transform: translateX(-50%);
}
.emoji-icon {
margin: 5px;
cursor: pointer;
}
}
&.link {
&.pitch {
.link-box-mask {
display: block;
} }
.input { .link-box {
height: 36px; display: flex;
border: 1px solid rgba(215, 215, 215, 1);
border-radius: 7px;
padding: 0 10px;
font-size: 14px;
color: #000000;
} }
} }
.btn { .link-box {
width: 72px; background-color: rgba(255, 255, 255, 1);
height: 40px; border-radius: 6px;
line-height: 40px; box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.168627450980392);
background-color: rgba(80, 227, 194, 1);
border-radius: 8px; width: 336px;
margin-left: auto; height: 282px;
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif; position: absolute;
font-weight: 650; top: 30px;
font-style: normal; left: 0%;
font-size: 16px; z-index: 1;
color: #000000;
margin-top: 8px; border: 1px solid #ebebeb;
&:hover { display: none;
background-color: rgb(35, 224, 182); flex-direction: column;
padding: 18px 20px 0;
.item {
margin-bottom: 22px;
flex-direction: column;
.name {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #555555;
line-height: 26px;
margin-bottom: 8px;
}
.input {
height: 36px;
border: 1px solid rgba(215, 215, 215, 1);
border-radius: 7px;
padding: 0 10px;
font-size: 14px;
color: #000000;
}
} }
.btn {
width: 72px;
height: 40px;
line-height: 40px;
background-color: rgba(80, 227, 194, 1);
border-radius: 8px;
margin-left: auto;
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
font-weight: 650;
font-style: normal;
font-size: 16px;
color: #000000;
margin-top: 8px;
&:hover {
background-color: rgb(35, 224, 182);
}
}
}
}
&.h2 {
&.pitch {
background-color: rgba(246, 246, 189, 1);
}
}
&.active {
> button {
background-color: rgba(246, 246, 189, 1);
} }
} }
} }
}
&.h2 { #editor-container {
&.pitch { min-height: 500px;
background-color: rgba(246, 246, 189, 1); max-height: 80vh;
} font-size: 18px;
line-height: 26px;
color: #333333;
a {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
text-decoration: underline;
color: #04b0d5;
} }
} }
} }

View File

@@ -109,15 +109,18 @@
height: 26px; height: 26px;
border-radius: 50%; border-radius: 50%;
} }
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(6) {
margin-right: -9px;
}
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(5) {
margin-right: -9px;
}
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(4) { #appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(4) {
margin-right: -9px;
}
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(3) {
margin-right: -9px;
}
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(2) {
margin-right: -7px; margin-right: -7px;
} }
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-head .people .right .item:nth-child(2) {
margin-right: -5px;
}
#appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-list .item { #appIndex .header-content-box .header-content-left .topic-and-selectives .topic-box .topic-list .item {
cursor: pointer; cursor: pointer;
} }

View File

@@ -125,17 +125,18 @@
border-radius: 50%; border-radius: 50%;
} }
&:nth-child(6) {
margin-right: -9px;
}
&:nth-child(5) {
margin-right: -9px;
}
&:nth-child(4) { &:nth-child(4) {
margin-right: -9px;
}
&:nth-child(3) {
margin-right: -9px;
}
&:nth-child(2) {
margin-right: -7px; margin-right: -7px;
} }
&:nth-child(2) {
margin-right: -5px;
}
} }
} }
} }

View File

@@ -3,7 +3,6 @@
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400;
font-style: normal; font-style: normal;
word-break: break-word; word-break: break-word;
} }

View File

@@ -3,7 +3,7 @@
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif; font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400; // font-weight: 400;
font-style: normal; font-style: normal;
word-break: break-word; word-break: break-word;
} }

View File

@@ -194,7 +194,7 @@
<div class="bottom flexacenter"> <div class="bottom flexacenter">
<div class="operate flexacenter"> <div class="operate flexacenter">
<div class="item" :class="{ 'pitch': emojiState }" style="z-index: 2"> <div class="item" :class="{ 'pitch': emojiState }" style="z-index: 2">
<img class="icon" src="./img/smiling-face.png" @click="openEmoji()" alt="" /> <img class="icon" src="./img/smiling-face.png" @click="openEmoji($event)" alt="" />
<div class="emoji-box"> <div class="emoji-box">
<div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item)">{{ item }}</div> <div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item)">{{ item }}</div>
</div> </div>
@@ -264,7 +264,7 @@
<div class="bottom flexacenter"> <div class="bottom flexacenter">
<div class="operate flexacenter"> <div class="operate flexacenter">
<div class="item" :class="{ 'pitch': item.emojiState }" style="z-index: 2"> <div class="item" :class="{ 'pitch': item.emojiState }" style="z-index: 2">
<img class="icon" src="./img/smiling-face.png" @click="openEmoji(index)" alt="" /> <img class="icon" src="./img/smiling-face.png" @click="openEmoji($event, index)" alt="" />
<div class="emoji-box"> <div class="emoji-box">
<div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item, index)">{{ item }}</div> <div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item, index)">{{ item }}</div>
</div> </div>
@@ -335,7 +335,7 @@
<div class="bottom flexacenter"> <div class="bottom flexacenter">
<div class="operate flexacenter"> <div class="operate flexacenter">
<div class="item" :class="{ 'pitch': ite.emojiState }" style="z-index: 2"> <div class="item" :class="{ 'pitch': ite.emojiState }" style="z-index: 2">
<img class="icon" src="./img/smiling-face.png" @click="openEmoji(index, i)" alt="" /> <img class="icon" src="./img/smiling-face.png" @click="openEmoji($event, index, i)" alt="" />
<div class="emoji-box"> <div class="emoji-box">
<div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item, index, i)">{{ item }}</div> <div class="emoji-icon" v-for="item in emojiData" :key="item" @click="selectEmoji(item, index, i)">{{ item }}</div>
</div> </div>
@@ -481,7 +481,7 @@
<script src="./js/axios.min.js"></script> <script src="./js/axios.min.js"></script>
<script src="./js/public.js"></script> <script src="./js/public.js"></script>
<script type="module" src="./js/details.js"></script> <script type="module" src="./js/details.js"></script>
<script type="module" src=" https://app.gter.net/image/gter/commonCom/preview-image/preview.js"></script> <script type="module" src=" https://app.gter.net/image/gter/commonCom/preview-image/preview.js?v=${window.__ASSET_VERSION__}"></script>
</body> </body>
</html> </html>

View File

@@ -16,24 +16,7 @@
} }
</style> </style>
<style>
#editorwrapper {
/* border: 1px solid #ccc; */
z-index: 100;
/* 按需定义 */
}
#toolbar-container {
/* border-bottom: 1px solid #ccc; */
}
#editor-container {
min-height: 509px;
max-height: 80vh;
}
</style>
<script src="./js/editor.js"></script> <script src="./js/editor.js"></script>
</head> </head>
<body> <body>
@@ -54,11 +37,12 @@
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title" :maxlength="titleLength" /> <input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title" :maxlength="titleLength" />
<div class="sum">{{ info?.title?.length ? titleLength - info?.title?.length : titleLength }}</div> <div class="sum">{{ info?.title?.length ? titleLength - info?.title?.length : titleLength }}</div>
</div> </div>
<div id="editor—wrapper">
<div id="editor—wrapper" class="editor—wrapper">
<!-- 工具栏 --> <!-- 工具栏 -->
<div id="toolbar-container" class="editor-toolbar flexacenter"> <div ref="toolbarRef" id="toolbar-container" class="editor-toolbar flexacenter">
<div class="toolbar-item flexacenter h2" :class="{'pitch': isPTitle}" @click="paragraphTitle"> <!-- <div class="toolbar-item flexacenter h2" :class="{'pitch': isPTitle}" @click="paragraphTitle">
<img class="icon" src="{@/img/t-icon.png}" alt="段落标题" /> <img class="icon" src="{@/img/t-icon.png}" alt="段落标题" />
<span>段落标题</span> <span>段落标题</span>
</div> </div>
@@ -104,7 +88,7 @@
<div class="emoji-box"> <div class="emoji-box">
<div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji" @click.stop="selectEmoji(emoji)">{{ emoji }}</div> <div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji" @click.stop="selectEmoji(emoji)">{{ emoji }}</div>
</div> </div>
</div> </div> -->
</div> </div>

View File

@@ -1,15 +1,17 @@
const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue; const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch, provide } = Vue;
import { itemForum } from "../component/item-forum/item-forum.js"; const ASSET_VERSION = window.__ASSET_VERSION__ || "20251126";
import { itemOffer } from "../component/item-offer/item-offer.js"; const withVer = (p) => `${p}?v=${ASSET_VERSION}`;
import { itemSummary } from "../component/item-summary/item-summary.js"; const { itemForum } = await import(withVer("../component/item-forum/item-forum.js"));
import { itemVote } from "../component/item-vote/item-vote.js"; const { itemOffer } = await import(withVer("../component/item-offer/item-offer.js"));
import { itemMj } from "../component/item-mj/item-mj.js"; const { itemSummary } = await import(withVer("../component/item-summary/item-summary.js"));
import { itemTenement } from "../component/item-tenement/item-tenement.js"; const { itemVote } = await import(withVer("../component/item-vote/item-vote.js"));
import { latestList } from "../component/latest-list/latest-list.js"; const { itemMj } = await import(withVer("../component/item-mj/item-mj.js"));
import { slideshowBox } from "../component/slideshow-box/slideshow-box.js"; const { itemTenement } = await import(withVer("../component/item-tenement/item-tenement.js"));
import { like } from "../component/like/like.js"; const { latestList } = await import(withVer("../component/latest-list/latest-list.js"));
import { report } from "../component/report/report.js"; const { slideshowBox } = await import(withVer("../component/slideshow-box/slideshow-box.js"));
import { headTop } from "../component/head-top/head-top.js"; const { like } = await import(withVer("../component/like/like.js"));
const { report } = await import(withVer("../component/report/report.js"));
const { headTop } = await import(withVer("../component/head-top/head-top.js"));
const appSectionIndex = createApp({ const appSectionIndex = createApp({
setup() { setup() {
@@ -563,10 +565,11 @@ const appSectionIndex = createApp({
let emojiState = ref(false); let emojiState = ref(false);
let emojiMaskState = ref(false); let emojiMaskState = ref(false);
let emojiBottomDistance = ref(0);
let inputTextarea = ref(""); let inputTextarea = ref("");
// 打开 Emoji // 打开 Emoji
const openEmoji = (index, i) => { const openEmoji = (event, index, i) => {
if (!isLogin.value) { if (!isLogin.value) {
goLogin(); goLogin();
return; return;
@@ -1177,7 +1180,7 @@ const appSectionIndex = createApp({
ajax(`/v2/api/forum/postTopicShare`, { token }); ajax(`/v2/api/forum/postTopicShare`, { token });
}; };
return { uniqidRef, share, reportToken, isReplyBoxShow, matterHeight, sidebarHeight, deleteItem, maxPicture, sidebarFixed, matterRef, sidebarRef, pitchInputState, ismyself, edit, searchInput, defaultSearchText, goSearch, goPersonalHomepage, QRcode, alsoCommentsData, copyLinkClick, reportState, tokentoken, essence, recommend, hide, report, cutShow, ismanager, show, openDiscuss, commentDelete, handleInputPaste, autoResize, editCommentState, selectEditEmoji, closeEditEmoji, openEditEmoji, closeEdit, openEdit, closeEditFileUpload, postEditComment, submitAnswerComments, closePictureUpload, closeFileUpload, picture, editToken, editPicture, editInput, editEmojiState, handleFileUpload, inputTextarea, judgeLogin, handleEditFile, selectEmoji, emojiData, emojiMaskState, emojiState, closeEmoji, openEmoji, closeAnswerCommentsChild, openAnswerCommentsChild, handleAnswerText, sendMessage, TAHomePage, operateAnswerCommentsLike, closeUserInfo, openUserInfo, permissions, commentList, commentPage, commentTotalCount, picture, userInfoWin, relatedList, relatedTime, coinNubmer, coinList, coinAmount, coinSubmit, strategy, mybalance, coinsState, openCoinBox, closeCoinBox, isLikeGif, likeClick, collectClick, islike, iscollect, recentlyList, medal, count, sectionn, tags, authorInfo, info, timestamp, updatedTime }; return { emojiBottomDistance, uniqidRef, share, reportToken, isReplyBoxShow, matterHeight, sidebarHeight, deleteItem, maxPicture, sidebarFixed, matterRef, sidebarRef, pitchInputState, ismyself, edit, searchInput, defaultSearchText, goSearch, goPersonalHomepage, QRcode, alsoCommentsData, copyLinkClick, reportState, tokentoken, essence, recommend, hide, report, cutShow, ismanager, show, openDiscuss, commentDelete, handleInputPaste, autoResize, editCommentState, selectEditEmoji, closeEditEmoji, openEditEmoji, closeEdit, openEdit, closeEditFileUpload, postEditComment, submitAnswerComments, closePictureUpload, closeFileUpload, picture, editToken, editPicture, editInput, editEmojiState, handleFileUpload, inputTextarea, judgeLogin, handleEditFile, selectEmoji, emojiData, emojiMaskState, emojiState, closeEmoji, openEmoji, closeAnswerCommentsChild, openAnswerCommentsChild, handleAnswerText, sendMessage, TAHomePage, operateAnswerCommentsLike, closeUserInfo, openUserInfo, permissions, commentList, commentPage, commentTotalCount, picture, userInfoWin, relatedList, relatedTime, coinNubmer, coinList, coinAmount, coinSubmit, strategy, mybalance, coinsState, openCoinBox, closeCoinBox, isLikeGif, likeClick, collectClick, islike, iscollect, recentlyList, medal, count, sectionn, tags, authorInfo, info, timestamp, updatedTime };
}, },
}); });

View File

@@ -148,11 +148,85 @@ const editApp = createApp({
}; };
let editor = null; let editor = null;
let toolbarRef = ref(null);
const initEditor = () => { const initEditor = () => {
let infoTarget = info.value || {}; let infoTarget = info.value || {};
console.log("infoTarget", infoTarget); console.log("infoTarget", infoTarget);
// 转换图片链接
async function customCheckImageFn(src, alt, url) {
// JS 语法
if (!src) {
return;
}
// let config = uConfigData;
// // 1. 构造 FormData包含你的接口所需字段
// const formData = new FormData();
// formData.append(config.requestName, file); // 文件数据
// formData.append("name", file.name); // 文件名
// formData.append("type", "image"); // 文件名
// formData.append("data", config.params.data); // 文件名
await setTimeout(() => {
console.log("1111");
return true;
}, 2000);
// setTimeout(() => {
// return "图片网址必须以 http/https 开头";
// }, 2000);
if (src.indexOf("http") !== 0) {
// return "图片网址必须以 http/https 开头";
}
// return true;
// 返回值有三种选择:
// 1. 返回 true ,说明检查通过,编辑器将正常插入图片
// 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串)
// 3. 返回 undefined即没有任何返回说明检查未通过编辑器会阻止插入。但不会提示任何信息
}
// 【新增】判断节点的对齐方式
const getNodeAlign = (node) => {
if (!node) return "left"; // 默认居左
// 获取节点的text-align样式优先内联样式再取CSS计算样式
const inlineAlign = node.style.textAlign;
if (inlineAlign) return inlineAlign;
const computedStyle = window.getComputedStyle(node);
return computedStyle.textAlign || "left";
};
// 【新增】切换对齐方式(居中 ↔ 居左)
const toggleAlign = () => {
const editorInst = editor.value;
if (!editorInst) return;
// 禁用编辑器默认的居中命令
editorInst.off("clickToolbar", "justifyCenter");
// 获取当前选中的节点(优先段落/块级节点)
const selectedNode = getSelectedNode(editorInst);
const blockNode = DomEditor.getClosestBlock(selectedNode); // 获取块级节点p/div等
if (!blockNode) return;
// 判断当前对齐方式
const currentAlign = getNodeAlign(blockNode);
// 切换对齐:居中 → 居左;其他 → 居中
const newAlign = currentAlign === "center" ? "left" : "center";
// 设置节点对齐样式
editorInst.restoreSelection(); // 恢复选区
blockNode.style.textAlign = newAlign;
// 触发编辑器更新
editorInst.change();
editorInst.focus(); // 保持焦点
};
const editorConfig = { const editorConfig = {
placeholder: "Type here...", placeholder: "Type here...",
@@ -162,6 +236,19 @@ const editApp = createApp({
emotions: optionEmoji.value, emotions: optionEmoji.value,
}, },
["insertImage"]: {
onInsertedImage(imageNode) {
console.log("imageNode", imageNode);
// TS 语法
// onInsertedImage(imageNode) { // JS 语法
if (imageNode == null) return;
const { src, alt, url, href } = imageNode;
console.log("inserted image", src, alt, url, href);
},
// checkImage: async (src, alt, url) => await customCheckImageFn(src, alt, url), // 也支持 async 函数
},
["uploadImage"]: { ["uploadImage"]: {
server: uConfigData.url, server: uConfigData.url,
@@ -200,9 +287,12 @@ const editApp = createApp({
formData.append("name", file.name); // 文件名 formData.append("name", file.name); // 文件名
formData.append("type", "image"); // 文件名 formData.append("type", "image"); // 文件名
formData.append("data", config.params.data); // 文件名 formData.append("data", config.params.data); // 文件名
// uploading(file, file.name, "image").then((data) => {
// insertFn(data.url); // 传入图片的可访问 URL
// });
ajax(config.url, formData).then((res) => { ajax(config.url, formData).then((res) => {
const data = res.data; const data = res.data;
console.log("上传成功:", data);
insertFn(data.url); // 传入图片的可访问 URL insertFn(data.url); // 传入图片的可访问 URL
}); });
} catch (err) { } catch (err) {
@@ -210,23 +300,76 @@ const editApp = createApp({
} }
}, },
}, },
},
// 4. 链接菜单:显式启用(默认启用,补充配置防止被过滤) ["uploadVideo"]: {
link: { server: uConfigData.url,
disabled: false, // 确保不禁用
showTarget: true, // 显示「是否新窗口打开」选项 // form-data fieldName ,默认值 'wangeditor-uploaded-video'
showRel: true, // 显示「rel 属性」选项 fieldName: uConfigData.requestName,
},
// 5. 对齐菜单:显式启用(默认启用,兜底配置) // 单个文件的最大体积限制,默认为 10M
justify: { maxFileSize: maxSize, // 1M
disabled: false,
// 最多可上传几个文件,默认为 5
maxNumberOfFiles: videoLength,
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ["video/*"],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: { ...uConfigData.params },
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 自定义增加 http header
headers: { accept: "application/json, text/plain, */*", ...uConfigData.headers },
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 30 秒
timeout: 15 * 1000, // 15 秒
// 视频不支持 base64 格式插入
async customUpload(file, insertFn) {
try {
const videoUploadRes = await uploading(file, file.name, "video");
const coverFile = await getVideoFirstFrame(file);
console.log("第一帧提取成功", coverFile);
// 步骤3再上传第一帧封面type 传 'cover',按后端要求调整)
const coverUploadRes = await uploading(coverFile, coverFile.name, "image");
console.log("封面上传成功", coverUploadRes);
insertFn(videoUploadRes.url, coverUploadRes.url);
} catch (err) {
console.error("上传出错:", err);
}
},
},
["justifyCenter"]: {
onClick: (editor) => {
console.log("editor", editor);
toggleAlign(); // 替换为自定义切换逻辑
},
// 【可选】自定义居中按钮的激活状态(选中时高亮)
isActive: (editor) => {
const selectedNode = getSelectedNode(editor);
const blockNode = DomEditor.getClosestBlock(selectedNode);
return blockNode && getNodeAlign(blockNode) === "center";
},
},
}, },
onChange(editor) { onChange(editor) {
const html = editor.getHtml(); const html = editor.getHtml();
// console.log('"editor', editor);
console.log("editor content", html); console.log("editor content", html);
// 也可以同步到 <textarea> updateWHeadingStatus();
}, },
}; };
@@ -237,32 +380,31 @@ const editApp = createApp({
mode: "default", mode: "default",
}); });
setTimeout(() => {
console.log("editor", editor);
editor.addMark("bold", true); // 加粗
}, 1000);
// const toolbar = DomEditor.getToolbar(editor) // const toolbar = DomEditor.getToolbar(editor)
const toolbarConfig = { const toolbarConfig = {
// toolbarKeys: ["bold", "italic", "list"], // toolbarKeys: ["bold", "italic", "list"],
toolbarKeys: [ toolbarKeys: [
"headerSelect", // 标题 "header2", // 标题
"bold", // 粗体
"italic", // 斜体
// "justify", // 对齐方式
{ {
key: "justifyCenter", key: "group-image",
title: "对齐", title: "图片",
iconSvg: '<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>', menuKeys: ["insertImage", "uploadImage"],
menuKeys: ["justifyLeft", "justifyRight", "justifyCenter", "justifyJustify"],
}, },
{
key: "group-video",
title: "视频",
menuKeys: ["insertVideo", "uploadVideo"],
},
// "insertVideo",
"emotion", // 表情 "emotion", // 表情
// "uploadImage", // 插入图片
// "uploadVideo", // 插入视频
"insertLink", // 插入链接 "insertLink", // 插入链接
"uploadImage", // 插入图片 "bold", // 粗体
"uploadVideo", // 插入视频 "justifyCenter",
"undo", // 撤销
"redo", // 重做
"fullScreen", // 全屏
], ],
}; };
@@ -272,15 +414,44 @@ const editApp = createApp({
config: toolbarConfig, config: toolbarConfig,
mode: "default", mode: "default",
}); });
console.log("editor.commands", editor);
console.log("toolbar", toolbar); nextTick(() => {
const h2 = toolbarRef.value.querySelector('[data-menu-key="header2"]');
const h2Item = h2.parentElement;
h2Item.classList.add("toolbar-item", "flexacenter");
h2.innerHTML = '<img class="icon" src="{@/img/t-icon.png}" alt="段落标题" /> <span>段落标题</span>';
// setTimeout(() => { const image = toolbarRef.value.querySelector('[data-menu-key="group-image"]');
// const el = document.querySelector("#toolbar-container"); const imageItem = image.parentElement;
// if (el && el.children.length === 0) { imageItem.classList.add("toolbar-item", "flexacenter");
// createToolbar({ editor, selector: "#toolbar-container", config: toolbarConfig, mode: "default" }); image.innerHTML = '<img class="icon" src="{@/img/img-icon.png}" alt="图片" /> <span>图片</span>';
// }
// }, 0); const video = toolbarRef.value.querySelector('[data-menu-key="group-video"]');
const videoItem = video.parentElement;
videoItem.classList.add("toolbar-item", "flexacenter");
video.innerHTML = '<img class="icon" src="{@/img/video-icon.png}" alt="视频" /> <span>视频</span>';
const emotion = toolbarRef.value.querySelector('[data-menu-key="emotion"]');
const emotionItem = emotion.parentElement;
emotionItem.classList.add("toolbar-item", "flexacenter");
emotion.innerHTML = '<img class="icon" src="{@/img/emotion-icon.png}" alt="表情" /> <span>表情</span>';
const link = toolbarRef.value.querySelector('[data-menu-key="insertLink"]');
const linkItem = link.parentElement;
linkItem.classList.add("toolbar-item", "flexacenter");
link.innerHTML = '<img class="icon" src="{@/img/link-icon.png}" alt="链接" /> <span>链接</span>';
const bold = toolbarRef.value.querySelector('[data-menu-key="bold"]');
const boldItem = bold.parentElement;
boldItem.classList.add("toolbar-item", "flexacenter");
bold.innerHTML = '<img class="icon" src="{@/img/bold-icon.png}" alt="粗体" /> <span>粗体</span>';
const justifyCenter = toolbarRef.value.querySelector('[data-menu-key="justifyCenter"]');
const justifyCenterItem = justifyCenter.parentElement;
justifyCenterItem.classList.add("toolbar-item", "flexacenter");
justifyCenter.innerHTML = '<img class="icon" src="{@/img/justify-center-icon.png}" alt="居中" /> <span>居中</span>';
});
}; };
const restoreHtml = (formattedText, attachments) => { const restoreHtml = (formattedText, attachments) => {
@@ -370,6 +541,35 @@ const editApp = createApp({
}; };
let lastSelection = null; let lastSelection = null;
let lastSelectionW = null;
const isH1 = ref(false);
const updateWHeadingStatus = () => {
const wRoot = document.querySelector("#editor-container");
let node = null;
try {
const DomEditor = window.wangEditor && window.wangEditor.DomEditor;
if (DomEditor && editor) node = DomEditor.getSelectionNode(editor);
} catch (e) {}
if (!node) {
const sel = window.getSelection();
if (sel && sel.rangeCount) node = sel.getRangeAt(0).commonAncestorContainer;
}
let el = node && node.nodeType === 3 ? node.parentElement : node;
while (el && el !== wRoot && el && el.nodeType === 1) {
if (window.getComputedStyle(el).getPropertyValue("text-align") === "center") {
console.log("居中");
const justifyCenter = toolbarRef.value.querySelector('[data-menu-key="justifyCenter"]');
if (justifyCenter) {
const justifyCenterItem = justifyCenter.parentElement;
justifyCenterItem.classList.add("active");
}
break;
}
el = el.parentElement;
}
};
let loading = ref(false); let loading = ref(false);
@@ -553,36 +753,41 @@ const editApp = createApp({
const handleSelectionChange = () => { const handleSelectionChange = () => {
return; return;
const selection = window.getSelection(); const selection = window.getSelection();
// 确保有选中内容且选中区域在编辑器内
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
// 检查选中区域是否在编辑器内
const commonAncestor = range.commonAncestorContainer; const commonAncestor = range.commonAncestorContainer;
if (editorRef.value.contains(commonAncestor)) { if (editorRef.value.contains(commonAncestor)) {
console.log("选中区域在编辑器内", range);
lastSelection = range; lastSelection = range;
} }
const wRoot = document.querySelector("#editor-container");
if (wRoot && wRoot.contains(commonAncestor)) {
lastSelectionW = range;
updateWHeadingStatus();
}
} }
}; };
const isPTitle = ref(false); let isPTitle = ref(false);
const paragraphTitle = () => { const paragraphTitle = () => {
editorRef.value.focus(); console.log("editor", editor.addMark);
if (!lastSelection) return; // editor.addMark("bold", true); // 加粗
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(lastSelection);
// 使用try-catch确保即使命令执行失败也能恢复滚动位置 // editorRef.value.focus();
try { // if (!lastSelection) return;
document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2"); // const selection = window.getSelection();
} catch (error) { // selection.removeAllRanges();
console.error("应用段落格式失败:", error); // selection.addRange(lastSelection);
}
// 更新状态 // // 使用try-catch确保即使命令执行失败也能恢复滚动位置
setTimeout(() => updatePTitleStatus(), 100); // try {
// document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2");
// } catch (error) {
// console.error("应用段落格式失败:", error);
// }
// // 更新状态
// setTimeout(() => updatePTitleStatus(), 100);
}; };
const updatePTitleStatus = () => { const updatePTitleStatus = () => {
@@ -656,6 +861,8 @@ const editApp = createApp({
content = formatContent(content); content = formatContent(content);
console.log(content); console.log(content);
return;
const data = { const data = {
...infoTarget, ...infoTarget,
content, content,
@@ -893,11 +1100,11 @@ const editApp = createApp({
console.log("加粗"); console.log("加粗");
editor.addMark("bold", true); // 加粗 editor.addMark("bold", true); // 加粗
editor.addMark("color", "#999"); // 文本颜色 // editor.addMark("color", "#999"); // 文本颜色
console.log("editor", editor.addMark); console.log("editor", editor.addMark);
}; };
return { overstriking, linkClick, insertVideo, insertLink, linkUrl, linkText, linkState, openLink, closeLink, handleClick, uniqid, userInfoWin, titleLength, submit, emojiState, openEmoji, closeEmoji, selectEmoji, optionEmoji, isPTitle, onEditorInput, onEditorFocus, onEditorBlur, paragraphTitle, info, tagList, token, cutAnonymity, editorRef, insertImage, judgeIsEmpty, isEmpty }; return { toolbarRef, overstriking, isH1, linkClick, insertVideo, insertLink, linkUrl, linkText, linkState, openLink, closeLink, handleClick, uniqid, userInfoWin, titleLength, submit, emojiState, openEmoji, closeEmoji, selectEmoji, optionEmoji, isPTitle, onEditorInput, onEditorFocus, onEditorBlur, paragraphTitle, info, tagList, token, cutAnonymity, editorRef, insertImage, judgeIsEmpty, isEmpty };
}, },
}); });

View File

@@ -2,6 +2,8 @@ const forumBaseURL = "https://api.gter.net";
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
axios.defaults.emulateJSON = true; axios.defaults.emulateJSON = true;
const withVer = (p) => `${p}?v=${window.__ASSET_VERSION__}`;
// 导出ajax函数 // 导出ajax函数
const ajax = (url, data) => { const ajax = (url, data) => {
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
@@ -402,3 +404,17 @@ const go_ajax_Login = () => {
if (typeof ajax_login === "function") ajax_login(); if (typeof ajax_login === "function") ajax_login();
else window.open("https://passport.gter.net/?referer=" + escape(location.href), "_self"); else window.open("https://passport.gter.net/?referer=" + escape(location.href), "_self");
}; };
// const loadJsFile = (url) => {
// var xhr = new XMLHttpRequest();
// xhr.open("GET", url, true);
// xhr.onreadystatechange = function () {
// if (xhr.readyState === 4 && xhr.status === 200) {
// var scriptCode = xhr.responseText;
// var script = document.createElement("script");
// script.innerHTML = scriptCode;
// document.head.appendChild(script);
// }
// };
// xhr.send();
// };