feat(editor): 新增视频上传功能并优化编辑器体验
- 添加视频上传功能,支持提取视频第一帧作为封面 - 优化图片和视频上传的数量限制检查 - 修复编辑器内容为空判断逻辑,增加视频元素检测 - 改进链接插入功能,自动填充选中文本 - 调整表情选择插入方式,使用execCommand实现 - 优化附件提取逻辑,支持视频元素解析 - 添加编辑器点击事件处理,更新选区状态 - 修复样式问题,调整按钮悬停效果
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -332,7 +332,7 @@
|
||||
border-color: #50e3c2 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="coins-area flexcenter">
|
||||
<div class="coins-box flexcenter">
|
||||
<img class="fork closeCoinBox" src="https://app.gter.net/image/gter/commonCom/bi/img/fork-icon.png" />
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
</div>
|
||||
<div v-else class="slideshow-box">
|
||||
<div class="tab-list flexacenter">
|
||||
<div class="tab-item newest" :class="{'pitch': postsTab == 'newest'}" @click="tabPostsItem('newest')">最新</div>
|
||||
<div class="tab-item essence" :class="{'pitch': postsTab == 'essence'}" @click="tabPostsItem('essence')">精华</div>
|
||||
<div class="tab-item newest" :class="{'pitch': postsTab == 'newest'}" @click="tabPostsItem('newest')">最新
|
||||
</div>
|
||||
<div class="tab-item essence" :class="{'pitch': postsTab == 'essence'}" @click="tabPostsItem('essence')">精华
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slideshow-content flexflex">
|
||||
@@ -18,10 +20,13 @@
|
||||
<div class="newest-side-box side-box">
|
||||
<img class="bounding" src="/img/bounding-circle-green.svg" alt="" />
|
||||
<div class="box">
|
||||
<a v-for="(item, index) in latestList" :key="index" class="item flexacenter vuehide" :href="'/details/' + item.uniqid" target="_blank">
|
||||
<div class="dot dot-green"></div>
|
||||
<div class="text one-line-display">{{ item.title || item.content }}</div>
|
||||
</a>
|
||||
<template v-for="(item, index) in latestList" :key="index">
|
||||
<a v-if="item.title || item.content" class="item flexacenter vuehide"
|
||||
:href="'/details/' + item.uniqid" target="_blank">
|
||||
<div class="dot dot-green"></div>
|
||||
<div class="text one-line-display">{{ item.title || item.content }}</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,11 +34,15 @@
|
||||
<div class="essence-side-box side-box">
|
||||
<img class="bounding" src="/img/bounding-circle-blue.svg" alt="" />
|
||||
<div class="box">
|
||||
<a v-for="(item, index) in topList" :key="index" class="item flexacenter vuehide" :href="'/details/' + item.uniqid" target="_blank">
|
||||
<div class="dot"></div>
|
||||
<div class="text one-line-display">{{ item.title || item.content }}</div>
|
||||
</a>
|
||||
<template v-for="(item, index) in topList" :key="index">
|
||||
<a v-if="item.title || item.content" class="item flexacenter vuehide"
|
||||
:href="'/details/' + item.uniqid" target="_blank">
|
||||
<div class="dot"></div>
|
||||
<div class="text one-line-display">{{ item.title || item.content }}</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,6 +172,7 @@
|
||||
}
|
||||
#details .matter .matter-left .html img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
#details .matter .matter-left .html video {
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
video {
|
||||
|
||||
14
css/edit.css
14
css/edit.css
@@ -77,6 +77,9 @@
|
||||
height: 36px;
|
||||
background-color: #fbfbfb;
|
||||
padding-left: 25px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
#edit .edit-container .editor-toolbar .toolbar-item {
|
||||
cursor: pointer;
|
||||
@@ -132,7 +135,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: rgba(0, 0, 0, 0.20392157);
|
||||
display: none;
|
||||
}
|
||||
#edit .edit-container .editor-toolbar .toolbar-item .link-box-mask {
|
||||
@@ -227,7 +229,7 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
#edit .edit-container .editor-toolbar .toolbar-item.link .link-box .btn:hover {
|
||||
background-color: #c2eff6;
|
||||
background-color: #23e0b6;
|
||||
}
|
||||
#edit .edit-container .editor-toolbar .toolbar-item.h2.pitch {
|
||||
background-color: #f6f6bd;
|
||||
@@ -334,12 +336,16 @@
|
||||
color: #7f7f7f;
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
#edit .edit-container .action-buttons .right-section .draft-btn .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
#edit .edit-container .action-buttons .right-section .draft-btn:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
#edit .edit-container .action-buttons .right-section .publish-btn {
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
@@ -350,4 +356,8 @@
|
||||
font-size: 16px;
|
||||
color: #000000;
|
||||
background-color: #50e3c2;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
#edit .edit-container .action-buttons .right-section .publish-btn:hover {
|
||||
background-color: #40d1aa;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
height: 36px;
|
||||
background-color: rgba(251, 251, 251, 1);
|
||||
padding-left: 25px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
.toolbar-item {
|
||||
.icon {
|
||||
width: 16px;
|
||||
@@ -146,7 +150,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: rgba(0, 0, 0, 0.20392157);
|
||||
// background-color: rgba(0, 0, 0, 0.20392157);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -254,7 +258,7 @@
|
||||
color: #000000;
|
||||
margin-top: 8px;
|
||||
&:hover {
|
||||
background-color: rgba(194, 239, 246, 1);
|
||||
background-color: rgb(35, 224, 182);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,12 +386,17 @@
|
||||
color: #7f7f7f;
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(235, 235, 235);
|
||||
}
|
||||
}
|
||||
.publish-btn {
|
||||
width: 150px;
|
||||
@@ -399,6 +408,11 @@
|
||||
font-size: 16px;
|
||||
color: #000000;
|
||||
background-color: rgba(80, 227, 194, 1);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 209, 170, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<body>
|
||||
<div class="container" id="details" v-cloak>
|
||||
<div class="templateValue" ref="uniqidRef">i8aD88Xy1aK9</div>
|
||||
|
||||
<div class="head-top flexacenter">
|
||||
<img class="logo" src="https://oss.gter.net/logo" alt="" />
|
||||
<div class="flex1"></div>
|
||||
@@ -38,8 +40,6 @@
|
||||
<img class="icon" src="./img/index-icon.png" />
|
||||
<a class="text textA" target="_blank" href="./">首页</a>
|
||||
<img class="arrows" src="./img/arrows-gray.svg" />
|
||||
<!-- <a class="text textA" target="_blank" href="./">首页</a>
|
||||
<img class="arrows" src="./img/arrows-gray.svg" /> -->
|
||||
<div class="text one-line-display">{{ info.title || info.content }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
196
edit.html
196
edit.html
@@ -1,102 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>发布帖子 - 轻论坛</title>
|
||||
<link rel="stylesheet" href="./css/public.css" />
|
||||
<link rel="stylesheet" href="./css/edit.css" />
|
||||
<script src="./js/vue.global.js"></script>
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" id="edit" v-cloak>
|
||||
<div class="edit-head flexacenter">
|
||||
<div class="edit-head-container flexacenter">
|
||||
<img class="icon" src="./img/edit-logo-icon.png" />
|
||||
<div class="dot"></div>
|
||||
<div class="title">发帖</div>
|
||||
<div class="hint">发帖奖励 3 个寄托币/篇,每天最高奖励3篇</div>
|
||||
<div class="flex1"></div>
|
||||
<img v-if="userInfoWin.avatar" class="avatar" :src="userInfoWin.avatar" />
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>发布帖子 - 轻论坛</title>
|
||||
<link rel="stylesheet" href="./css/public.css" />
|
||||
<link rel="stylesheet" href="./css/edit.css" />
|
||||
<script src="./js/vue.global.js"></script>
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" id="edit" v-cloak>
|
||||
<div class="edit-head flexacenter">
|
||||
<div class="edit-head-container flexacenter">
|
||||
<a class="" href="/" target="_blank">
|
||||
<img class="icon" src="{@/img/edit-logo-icon.png}" />
|
||||
</a>
|
||||
<div class="dot"></div>
|
||||
<div class="title">发帖</div>
|
||||
<div class="hint">发帖奖励 3 个寄托币/篇,每天最高奖励3篇</div>
|
||||
<div class="flex1"></div>
|
||||
<img v-if="userInfoWin.avatar" class="avatar" :src="userInfoWin.avatar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-container">
|
||||
<!-- 标题输入 -->
|
||||
<div class="title-box">
|
||||
<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>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="editor-toolbar flexacenter">
|
||||
<div class="toolbar-item flexacenter h2" :class="{'pitch': isPTitle}" @click="paragraphTitle">
|
||||
<img class="icon" src="{@/img/t-icon.png}" alt="段落标题" />
|
||||
<span>段落标题</span>
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter">
|
||||
<img class="icon" src="{@/img/img-icon.png}" alt="图片" />
|
||||
<span>图片</span>
|
||||
<input class="file" type="file" @change="insertImage" accept=".png, .jpg, .jpeg" />
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter">
|
||||
<img class="icon" src="{@/img/video-icon.png}" alt="视频" />
|
||||
<span>视频</span>
|
||||
<input class="file" type="file" @change="insertVideo" accept=".mp4, .avi, .mov" />
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter expression" :class="{'pitch': emojiState}" @click="openEmoji">
|
||||
<img class="icon" src="{@/img/smiling-face-round-black.png}" alt="表情" />
|
||||
<span>表情</span>
|
||||
<div class="emoji-box-mask" @click.stop="closeEmoji"></div>
|
||||
<div class="emoji-box">
|
||||
<div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji"
|
||||
@click.stop="selectEmoji(emoji)">{{ emoji }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter link" :class="{'pitch': linkState}" @click="openLink">
|
||||
<img class="icon" src="{@/img/link-icon.png}" alt="链接" />
|
||||
<span>链接</span>
|
||||
<div class="link-box-mask" @click.stop="closeLink"></div>
|
||||
<div class="link-box flexflex" @click.stop="linkClick">
|
||||
<div class="item flexflex">
|
||||
<div class="name">请输入链接地址:</div>
|
||||
<input class="input" type="text" v-model="linkUrl" />
|
||||
</div>
|
||||
<div class="item flexflex">
|
||||
<div class="name">请输入链接文字:</div>
|
||||
<input class="input" type="text" v-model="linkText" />
|
||||
</div>
|
||||
<div class="btn flexcenter" @click.stop="insertLink">OK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-container">
|
||||
<!-- 标题输入 -->
|
||||
<div class="title-box">
|
||||
<input class="title-input" type="title" placeholder="输入标题(非必填)" v-model="info.title" :maxlength="titleLength" />
|
||||
|
||||
<!-- 内容编辑区 -->
|
||||
<div class="content-input" id="editor" contenteditable="true" :class="{ 'empty': isEmpty }"
|
||||
placeholder="输入正文" ref="editorRef" @input="onEditorInput" @focus="onEditorFocus" @blur="onEditorBlur"
|
||||
v-html="info.content" @click="handleClick"></div>
|
||||
|
||||
<!-- 标签选择 -->
|
||||
<!-- <div class="tags-list flexacenter">
|
||||
<div class="tag-item" v-for="item in tagList" :key="item.tagId" @click="insertLabel(item.tagId)">#{{ item.title }}</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons flexacenter">
|
||||
<div class="left-section flexacenter" @click="cutAnonymity">
|
||||
<img v-if="info.anonymous == 1" class="icon-pitch" src="{@/img/tick-box.svg}" />
|
||||
<div v-else class="icon"></div>
|
||||
<div class="text">匿名发布</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="editor-toolbar flexacenter">
|
||||
<div class="toolbar-item flexacenter" @click="paragraphTitle">
|
||||
<img class="icon" src="./img/t-icon.png" alt="段落标题" />
|
||||
<span>段落标题</span>
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter" @click="insertImage">
|
||||
<img class="icon" src="./img/img-icon.png" alt="图片" />
|
||||
<span>图片</span>
|
||||
<input class="file" type="file" @change="insertImage" accept=".png, .jpg, .jpeg" />
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter expression" :class="{'pitch': emojiState}" @click="openEmoji">
|
||||
<img class="icon" src="./img/smiling-face-round-black.png" alt="表情" />
|
||||
<span>表情</span>
|
||||
<div class="emoji-box-mask" @click.stop="closeEmoji"></div>
|
||||
<div class="emoji-box">
|
||||
<div class="emoji-icon" v-for="emoji in optionEmoji" :key="emoji" @click.stop="selectEmoji(emoji)">{{ emoji }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-item flexacenter link" :class="{'pitch': linkState}" @click="openLink">
|
||||
<img class="icon" src="./img/link-icon.png" alt="表情" />
|
||||
<span>链接</span>
|
||||
<div class="link-box-mask" @click.stop="closeLink"></div>
|
||||
<div class="link-box flexflex">
|
||||
<div class="item flexflex">
|
||||
<div class="name">请输入链接地址:</div>
|
||||
<input class="input" type="text" v-model="linkUrl" />
|
||||
</div>
|
||||
<div class="item flexflex">
|
||||
<div class="name">请输入链接文字:</div>
|
||||
<input class="input" type="text" v-model="linkText" />
|
||||
</div>
|
||||
<div class="btn flexcenter" @click.stop="insertLink">OK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容编辑区 -->
|
||||
<div class="content-input" id="editor" contenteditable="true" :class="{ 'empty': isEmpty }" placeholder="输入正文" ref="editorRef" @input="onEditorInput" @focus="onEditorFocus" @blur="onEditorBlur" v-html="info.content"></div>
|
||||
|
||||
<!-- 标签选择 -->
|
||||
<div class="tags-list flexacenter">
|
||||
<div class="tag-item" v-for="item in tagList" :key="item.tagId" @click="insertLabel(item.tagId)">#{{ item.title }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons flexacenter">
|
||||
<div class="left-section flexacenter" @click="cutAnonymity">
|
||||
<img v-if="info.anonymous == 1" class="icon-pitch" src="./img/tick-box.svg" />
|
||||
<div v-else class="icon"></div>
|
||||
<div class="text">匿名发布</div>
|
||||
</div>
|
||||
<div class="right-section flexcenter">
|
||||
<div class="draft-btn flexcenter" @click="submit(0)">
|
||||
<img class="icon" src="./img/draft-icon.png" />
|
||||
存草稿
|
||||
</div>
|
||||
<div class="publish-btn flexcenter" @click="submit(1)">发表帖子</div>
|
||||
<div class="right-section flexcenter">
|
||||
<div v-if="!uniqid" class="draft-btn flexcenter" @click="submit(0)">
|
||||
<img class="icon" src="{@/img/draft-icon.png}" />
|
||||
存草稿
|
||||
</div>
|
||||
<div class="publish-btn flexcenter" @click="submit(1)">发表帖子</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./js/axios.min.js"></script>
|
||||
<script src="./js/public.js"></script>
|
||||
<script type="module" src="./js/edit.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<script src="./js/axios.min.js"></script>
|
||||
<script src="./js/public.js"></script>
|
||||
<script type="module" src="./js/edit.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
img/video-icon.png
Normal file
BIN
img/video-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
446
js/details.js
446
js/details.js
@@ -8,28 +8,18 @@ import { itemTenement } from "../component/item-tenement/item-tenement.js";
|
||||
import { latestList } from "../component/latest-list/latest-list.js";
|
||||
import { slideshowBox } from "../component/slideshow-box/slideshow-box.js";
|
||||
import { like } from "../component/like/like.js";
|
||||
import { report } from "../component/report/report.js";
|
||||
import { headTop } from "../component/head-top/head-top.js";
|
||||
|
||||
const appSectionIndex = createApp({
|
||||
setup() {
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfoWin();
|
||||
|
||||
setTimeout(() => (permissions.value = window["permissions"] || ["comment.edit", "comment.delete", "offercollege.hide", "offersummary.hide", "mj.hide", "topic:manager", "topic:hide"]), 1000);
|
||||
});
|
||||
|
||||
let isLogin = ref(true);
|
||||
let realname = ref(1); // 是否已经实名
|
||||
let userInfoWin = ref({
|
||||
authority: ["comment.edit", "comment.delete", "offercollege.hide", "offersummary.hide", "mj.hide", "topic:manager", "topic:hide"],
|
||||
avatar: "https://nas.gter.net:9008/avatar/97K4EWIMLrsbGTWXslC2WFVSEKWOikN42jDKLNjtax7HL4xtfMOJSdU9oWFhY2E~/middle?random=1761733169",
|
||||
groupid: 3,
|
||||
nickname: "肖荣豪",
|
||||
realname: 1,
|
||||
token: "01346a38444d71aaadb3adad52b52c39",
|
||||
uid: 500144,
|
||||
uin: 4238049,
|
||||
});
|
||||
let isLogin = ref(false);
|
||||
let realname = ref(0); // 是否已经实名
|
||||
let userInfoWin = ref({});
|
||||
|
||||
let permissions = ref([]);
|
||||
|
||||
@@ -42,6 +32,7 @@ const appSectionIndex = createApp({
|
||||
userInfoWin.value = user;
|
||||
if (user?.uin > 0 || user?.uid > 0) isLogin.value = true;
|
||||
permissions.value = user?.authority || [];
|
||||
ismanager.value = permissions.value.indexOf("topic:manager") >= 0;
|
||||
};
|
||||
document.addEventListener("getUser", checkUser);
|
||||
};
|
||||
@@ -78,28 +69,65 @@ const appSectionIndex = createApp({
|
||||
let timestamp = ref("");
|
||||
let updatedTime = ref("");
|
||||
let token = "";
|
||||
let tokentoken = ref("");
|
||||
let uniqid = "";
|
||||
|
||||
let sectionn = ref([]);
|
||||
let tags = ref([]);
|
||||
|
||||
let uniqidRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
const params = getUrlParams();
|
||||
uniqid = params.uniqid || "";
|
||||
uniqid = uniqidRef.value.innerText;
|
||||
|
||||
init();
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
checkWConfig();
|
||||
});
|
||||
|
||||
const checkWConfig = () => {
|
||||
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
|
||||
console.log("wConfig", wConfig);
|
||||
|
||||
if (wConfig.time) {
|
||||
const time = new Date(wConfig.time);
|
||||
const now = new Date();
|
||||
if (now - time > 24 * 60 * 60 * 1000) getWConfig();
|
||||
else {
|
||||
const config = wConfig.config || {};
|
||||
maxPicture.value = config.topic_image_count;
|
||||
}
|
||||
} else {
|
||||
getWConfig();
|
||||
}
|
||||
};
|
||||
|
||||
const getWConfig = () => {
|
||||
ajaxGet("/v2/api/config/website").then((res) => {
|
||||
if (res.code == 200) {
|
||||
let data = res["data"] || {};
|
||||
const config = data.config || {};
|
||||
maxPicture.value = config.topic_image_count;
|
||||
|
||||
data.time = new Date().toISOString();
|
||||
localStorage.setItem("wConfig", JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
ajaxget(`/v2/api/forum/getTopicDetails?uniqid=${uniqid}`).then((res) => {
|
||||
ajaxGet(`/v2/api/forum/getTopicDetails?uniqid=${uniqid}`).then((res) => {
|
||||
if (res.code != 200) {
|
||||
creationAlertBox("error", res.message || "主题不存在");
|
||||
setTimeout(() => redirectToExternalWebsite(`/`), 3000);
|
||||
return;
|
||||
}
|
||||
const data = res.data;
|
||||
|
||||
console.log(data, "data");
|
||||
let targetInfo = data.info;
|
||||
console.log("data", data);
|
||||
|
||||
if (!targetInfo.hidden) targetInfo.hidden = 0;
|
||||
|
||||
@@ -114,30 +142,129 @@ const appSectionIndex = createApp({
|
||||
authorInfo.value = Array.isArray(data.authorInfo) ? null : data.authorInfo;
|
||||
ismyself.value = data.ismyself || false;
|
||||
|
||||
const sectionNameSet = new Set(targetInfo.sectionn.map((item) => item.name));
|
||||
const newTag = targetInfo.tags.filter((tagName) => !sectionNameSet.has(tagName));
|
||||
|
||||
sectionn.value = targetInfo.sectionn;
|
||||
tags.value = targetInfo.tags;
|
||||
tags.value = newTag;
|
||||
|
||||
timestamp.value = strtimeago(targetInfo.release_at, 4);
|
||||
updatedTime.value = targetInfo.updated_at ? strtimeago(targetInfo.updated_at, 4) : null;
|
||||
|
||||
// targetInfo.content = "[attach]976054[/attach]\n\n[attachimg]1008585[/attachimg]\n[attach]850105[/attach]";
|
||||
// targetInfo.attachments = {
|
||||
// images: [
|
||||
// {
|
||||
// aid: 1008585,
|
||||
// url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokcHeD1NFX9ddrB_WbUGy8P79gQxcXHOeQ4soV7NkzNDQyOQ~~",
|
||||
// thumb: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokcHeD1NFX9ddrB_WbUWy8K_hyVFweFbPD7IZK4sVMAmnF5Vzp9Fkg0jQ0Mjk~",
|
||||
// },
|
||||
// ],
|
||||
// files: [],
|
||||
// videos: [
|
||||
// {
|
||||
// aid: 976054,
|
||||
// posterurl: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokc36P1NFX9ddrB_WbUWy8K_hyVFweFrPD7IZK4sVMAm3Btwy-9Fkg0jQ0Mjk~",
|
||||
// url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-ZwscHvqqsgFptxhXa6RWi26P-BuTQFFE7SQttkb8LQ0NDI5",
|
||||
// thumb: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-ZwscHvqqsgFptxhXa6QWi2uePJ5Bg8VFLPIqoYV7MtbCG2RtAz_-kVNNDQyOQ~~",
|
||||
// },
|
||||
// {
|
||||
// aid: 850105,
|
||||
// posterurl: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG_Y2l-U_pokc32G1NFX9ddrB_WbUWy8K_hyVFweFrPD7IZK4sVMA2eU5Vvl9Fkg0jQ0Mjk~",
|
||||
// url: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d-5otdXrqqsgFptxhXa6RWi26P-BuTQNAEuHBs9kb8LQ0NDI5",
|
||||
// thumb: "https://o.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d-5otdXrqqsgFptxhXa6QWi2uePJ5Bg8VFLPIqoYV7MsICzzBsFj_-kVNNDQyOQ~~",
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
if (targetInfo.content) targetInfo.content = restoreHtml(targetInfo.content, targetInfo.attachments);
|
||||
|
||||
info.value = targetInfo;
|
||||
|
||||
token = data.token;
|
||||
tokentoken.value = data.token;
|
||||
|
||||
getAuthorInfo();
|
||||
getTopicOperation();
|
||||
if (info.value["anonymous"] == 0) getAuthorInfo();
|
||||
|
||||
getCoinConfig();
|
||||
isLogin.value = data.islogin;
|
||||
|
||||
if (isLogin.value) getTopicOperation();
|
||||
|
||||
getRelatedTopics();
|
||||
|
||||
getComment();
|
||||
getQrcode();
|
||||
});
|
||||
};
|
||||
|
||||
const restoreHtml = (formattedText, attachments, type) => {
|
||||
const imageList = attachments?.images || [];
|
||||
const filesList = attachments?.files || [];
|
||||
const videosList = attachments?.videos || [];
|
||||
|
||||
let html = formattedText;
|
||||
|
||||
// 1. 还原换行符为<br>标签
|
||||
html = html.replace(/\n/g, "<br>");
|
||||
|
||||
// 2. 还原块级标签的换行标记
|
||||
html = html.replace(/<br><div>/g, "<div>");
|
||||
html = html.replace(/<\/div><br>/g, "</div>");
|
||||
|
||||
// 3. 还原标签标记为span.blue
|
||||
html = html.replace(/\[tag\]([^[]+)\[\/tag\]/gi, '<a class="blue" href="/tag/$1" target="_blank">#$1</a> <span class="fill"></span> ');
|
||||
|
||||
// 4. 还原粗体标记为h2标签
|
||||
html = html.replace(/\[b\]([^[]+)\[\/b\]/gi, "<h2>$1</h2>");
|
||||
|
||||
// 5. 统一在单次遍历中按出现顺序替换 attach/attachimg
|
||||
const byAid = new Map();
|
||||
imageList.forEach((e) => byAid.set(Number(e.aid), { type: "image", ...e }));
|
||||
filesList.forEach((e) => byAid.set(Number(e.aid), { type: "file", ...e }));
|
||||
videosList.forEach((e) => byAid.set(Number(e.aid), { type: "video", ...e }));
|
||||
|
||||
html = html.replace(/\[(attachimg|attach)\](\d+)\[\/\1\]/gi, (match, tag, aidStr) => {
|
||||
const aid = Number(aidStr);
|
||||
const item = byAid.get(aid);
|
||||
if (!item) return match;
|
||||
byAid.delete(aid);
|
||||
if (item.type === "image") {
|
||||
return `<img src="${item.url}" data-aid="${aid}"><br/>`;
|
||||
}
|
||||
if (item.type === "file") {
|
||||
return `<div class="flexacenter"><a href="${item.url}" download="${item.filename}">${item.filename}</a>【点击下载附件】</div>`;
|
||||
}
|
||||
return `<video src="${item.url}" width="400" height="400" preload="none" poster="${item.posterurl}" controls></video><br/>`;
|
||||
});
|
||||
|
||||
// 6. 还原填充标签
|
||||
html = html.replace(/(<span class="blue">[^<]+<\/span>)\s+/gi, '$1 <span class="fill"></span> ');
|
||||
|
||||
// 7. 清理多余的<br>标签
|
||||
html = html.replace(/<br><br>/g, "<br>");
|
||||
|
||||
if (type != "comment") {
|
||||
byAid.forEach((item, aid) => {
|
||||
if (item.type === "image") html += `<img src="${item.url}" data-aid="${aid}"><br/>`;
|
||||
else if (item.type === "file") html += `<div class="flexacenter"><a href="${item.url}" download="${item.name || item.filename}">${item.name || item.filename}</a>【点击下载附件】</div><br/>`;
|
||||
else html += `<video src="${item.url}" width="400" height="400" preload="none" poster="${item.posterurl}" controls></video><br/>`;
|
||||
});
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
let QRcode = ref("");
|
||||
const getQrcode = () => {
|
||||
ajaxGet(`/v2/api/forum/getQrcode?token=${token}`).then((res) => {
|
||||
if (res.code != 200) return;
|
||||
const data = res.data || [];
|
||||
QRcode.value = data.url || "";
|
||||
});
|
||||
};
|
||||
|
||||
let count = ref(0);
|
||||
let medal = ref([]);
|
||||
const getAuthorInfo = () => {
|
||||
ajaxget(`/v2/api/forum/getSpaceDetail?uid=${authorInfo.value.uid || 0}&uin=${authorInfo.value.uin || 0}`).then((res) => {
|
||||
ajaxGet(`/v2/api/forum/getSpaceDetail?uid=${authorInfo.value.uid || 0}&uin=${authorInfo.value.uin || 0}`).then((res) => {
|
||||
const data = res.data;
|
||||
const countList = data.count || [];
|
||||
count.value = countList.reduce((sum, item) => {
|
||||
@@ -153,7 +280,7 @@ const appSectionIndex = createApp({
|
||||
|
||||
let recentlyList = ref([]);
|
||||
const getCreationList = (token) => {
|
||||
ajaxget(`/v2/api/forum/getSpaceTopicList?token=${token}&simple=1`).then((res) => {
|
||||
ajaxGet(`/v2/api/forum/getSpaceTopicList?token=${token}&simple=1`).then((res) => {
|
||||
const data = res.data;
|
||||
recentlyList.value = data.data || [];
|
||||
recentlyList.value = recentlyList.value.slice(0, 8);
|
||||
@@ -167,19 +294,30 @@ const appSectionIndex = createApp({
|
||||
ajax(`/v2/api/forum/getTopicOperation`, {
|
||||
token,
|
||||
actions: ["like", "collection"],
|
||||
}).then((res) => {
|
||||
const data = res.data;
|
||||
const like = data.like;
|
||||
const collection = data.collection;
|
||||
})
|
||||
.then((res) => {
|
||||
console.log("res", res);
|
||||
|
||||
islike.value = like.status;
|
||||
iscollect.value = collection.status;
|
||||
});
|
||||
const data = res.data;
|
||||
const like = data.like;
|
||||
const collection = data.collection;
|
||||
|
||||
islike.value = like.status;
|
||||
iscollect.value = collection.status;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("err", err);
|
||||
});
|
||||
};
|
||||
|
||||
let isLikeGif = ref(false);
|
||||
|
||||
const likeClick = () => {
|
||||
if (!isLogin.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
ajax(`/v2/api/forum/postTopicLike`, {
|
||||
token,
|
||||
}).then((res) => {
|
||||
@@ -217,19 +355,29 @@ const appSectionIndex = createApp({
|
||||
let defaultcoinnum = 0;
|
||||
|
||||
const getCoinConfig = () => {
|
||||
ajaxget(`/v2/api/forum/getCoinConfig`).then((res) => {
|
||||
if (!isLogin.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
ajaxGet(`/v2/api/forum/getCoinConfig`).then((res) => {
|
||||
const data = res.data;
|
||||
strategy.value = data.config.strategy.url || 0;
|
||||
mybalance.value = data.mybalance || 0;
|
||||
defaultcoinnum = data.defaultcoinnum || 0;
|
||||
|
||||
// openCoinBox();
|
||||
});
|
||||
};
|
||||
|
||||
let coinsState = ref(false);
|
||||
const openCoinBox = () => {
|
||||
coinsState.value = true;
|
||||
document.body.style.overflow = "hidden";
|
||||
if (!coinListRequest) getCoinRankList();
|
||||
BiComponent.initComponent();
|
||||
|
||||
// getCoinConfig();
|
||||
// coinsState.value = true;
|
||||
// document.body.style.overflow = "hidden";
|
||||
// if (!coinListRequest) getCoinRankList();
|
||||
};
|
||||
|
||||
const closeCoinBox = () => {
|
||||
@@ -270,7 +418,7 @@ const appSectionIndex = createApp({
|
||||
let coinList = ref([]);
|
||||
let coinListRequest = false; // 控制请求次数
|
||||
const getCoinRankList = () => {
|
||||
ajaxget(`/v2/api/forum/getCoinRankList?token=${token}&limit=1000`).then((res) => {
|
||||
ajaxGet(`/v2/api/forum/getCoinRankList?token=${token}&limit=1000`).then((res) => {
|
||||
const data = res.data;
|
||||
coinNubmer.value = data.nubmer;
|
||||
coinList.value = data.data;
|
||||
@@ -281,7 +429,7 @@ const appSectionIndex = createApp({
|
||||
let relatedList = ref([]);
|
||||
let relatedTime = ref("");
|
||||
const getRelatedTopics = () => {
|
||||
ajaxget(`/v2/api/forum/getRelatedTopics?uniqid=${uniqid}&limit=8`).then((res) => {
|
||||
ajaxGet(`/v2/api/forum/getRelatedTopics?uniqid=${uniqid}&limit=8`).then((res) => {
|
||||
const data = res.data;
|
||||
relatedTime.value = data.updated_at || "";
|
||||
relatedList.value = data.list || [];
|
||||
@@ -295,9 +443,10 @@ const appSectionIndex = createApp({
|
||||
let commentTotalCount = ref(0);
|
||||
|
||||
const getComment = () => {
|
||||
if (commentPage.value == 0 || isgetCommentSate) return;
|
||||
console.log("commentPage.value", commentPage.value);
|
||||
if (commentPage.value == 0 || isgetCommentSate || !token) return;
|
||||
isgetCommentSate = true;
|
||||
ajaxget(`/v2/api/forum/getCommentList?token=${token}&page=${commentPage.value}&limit=1500`)
|
||||
ajaxGet(`/v2/api/forum/getCommentList?token=${token}&page=${commentPage.value}&limit=20`)
|
||||
.then((res) => {
|
||||
if (res.code != 200) {
|
||||
creationAlertBox("error", res.message || "");
|
||||
@@ -309,26 +458,31 @@ const appSectionIndex = createApp({
|
||||
element.timestamp = strtimeago(element.created_at, 4);
|
||||
element["picture"] = [];
|
||||
element["isReplyBoxShow"] = 0;
|
||||
|
||||
if (element["content"]) element["content"] = restoreHtml(element["content"], element.attachments, "comment");
|
||||
|
||||
if (element.child.length > 0) {
|
||||
element.child.forEach((el) => {
|
||||
el["picture"] = [];
|
||||
el.timestamp = strtimeago(element.created_at, 4);
|
||||
el["isReplyBoxShow"] = 0;
|
||||
|
||||
if (el["content"]) el["content"] = restoreHtml(el["content"], el.attachments, "comment");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (commentPage.value > 1) {
|
||||
for (let index = 0; index < data.data.length; index++) {
|
||||
if (alreadyCommentIdList.includes(data.data[index].id)) {
|
||||
data.data.splice(index, 1);
|
||||
index--;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (commentPage.value > 1) {
|
||||
// for (let index = 0; index < data.data.length; index++) {
|
||||
// if (alreadyCommentIdList.includes(data.data[index].id)) {
|
||||
// data.data.splice(index, 1);
|
||||
// index--;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
commentList.value = commentList.value.concat(data.data);
|
||||
commentTotalCount.value = data.count;
|
||||
commentTotalCount.value = data.commentcount;
|
||||
commentPage.value = data.count > commentList.value.length ? commentPage.value + 1 : 0;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -339,8 +493,8 @@ const appSectionIndex = createApp({
|
||||
let picture = ref([]);
|
||||
|
||||
const openUserInfo = (index, i) => {
|
||||
if (i != undefined && commentList.value[index].child[i].user["uin"] > 0) commentList.value[index].child[i]["avatarState"] = true;
|
||||
if (i == undefined && index != undefined && commentList.value[index].user["uin"] > 0) commentList.value[index]["avatarState"] = true;
|
||||
if (i != undefined && (commentList.value[index].child[i].user["uin"] > 0 || commentList.value[index].child[i].user["uid"] > 0)) commentList.value[index].child[i]["avatarState"] = true;
|
||||
if (i == undefined && index != undefined && (commentList.value[index].user["uin"] > 0 || commentList.value[index].user["uid"] > 0)) commentList.value[index]["avatarState"] = true;
|
||||
};
|
||||
|
||||
const closeUserInfo = (index, i) => {
|
||||
@@ -348,6 +502,8 @@ const appSectionIndex = createApp({
|
||||
else if (index != undefined) commentList.value[index]["avatarState"] = false;
|
||||
};
|
||||
|
||||
let isReplyBoxShow = ref(true);
|
||||
|
||||
// 打开 回答-评论 的子评论
|
||||
const openAnswerCommentsChild = (index, i) => {
|
||||
if (realname.value == 0 && userInfoWin.value?.uin > 0) {
|
||||
@@ -364,6 +520,8 @@ const appSectionIndex = createApp({
|
||||
|
||||
if (i == null) commentList.value[index]["childState"] = true;
|
||||
else commentList.value[index].child[i]["childState"] = true;
|
||||
|
||||
isReplyBoxShow.value = false;
|
||||
};
|
||||
|
||||
// 关闭 回答-评论 的子评论
|
||||
@@ -378,21 +536,24 @@ const appSectionIndex = createApp({
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
isReplyBoxShow.value = true;
|
||||
};
|
||||
|
||||
let dialogSrc = ref("");
|
||||
const handleAnswerText = (e) => {
|
||||
if (e.target.tagName === "IMG") {
|
||||
var src = e.target.getAttribute("src");
|
||||
dialogSrc.value = src;
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
}
|
||||
};
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = `<div class="detail-image flexcenter"><img class="detail-img" src="${src}" /></div>`;
|
||||
div.className = "detail-image-mask flexcenter";
|
||||
div.addEventListener("click", () => {
|
||||
document.body.style.overflow = "auto";
|
||||
div.remove();
|
||||
});
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
if (event.key !== "Escape") return;
|
||||
dialogSrc.value = "";
|
||||
window.removeEventListener("keydown", handleKeydown); // 取消监听
|
||||
document.body.appendChild(div);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
};
|
||||
|
||||
// 回答-评论 点赞
|
||||
@@ -407,7 +568,7 @@ const appSectionIndex = createApp({
|
||||
return;
|
||||
}
|
||||
|
||||
ajax("https://api.gter.net/v2/api/forum/likeComment", {
|
||||
ajax("/v2/api/forum/likeComment", {
|
||||
token,
|
||||
}).then((res) => {
|
||||
if (res.code != 200) {
|
||||
@@ -471,8 +632,8 @@ const appSectionIndex = createApp({
|
||||
editEmojiState.value = false;
|
||||
};
|
||||
|
||||
const TAHomePage = (uin) => goHomePage(uin);
|
||||
const sendMessage = (uin) => goSendMessage(uin);
|
||||
const TAHomePage = (token) => goHomePage(token);
|
||||
const sendMessage = (token) => goSendMessage(token);
|
||||
|
||||
let emojiData = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]);
|
||||
|
||||
@@ -497,7 +658,7 @@ const appSectionIndex = createApp({
|
||||
if (!isLogin.value) goLogin();
|
||||
};
|
||||
|
||||
const maxPicture = 10;
|
||||
let maxPicture = ref(10);
|
||||
|
||||
const handleFileUpload = (event, index, i) => {
|
||||
closeEmoji();
|
||||
@@ -518,13 +679,11 @@ const appSectionIndex = createApp({
|
||||
else target = picture.value;
|
||||
}
|
||||
|
||||
if (target.length >= maxPicture) {
|
||||
creationAlertBox("error", `最多只能上传 ${maxPicture} 张图片`);
|
||||
if (target.length >= maxPicture.value) {
|
||||
creationAlertBox("error", `最多只能上传 ${maxPicture.value} 张图片`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("现有图片", target);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64 = e.target.result;
|
||||
@@ -580,7 +739,7 @@ const appSectionIndex = createApp({
|
||||
|
||||
const getUploadConfig = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ajax("https://api.gter.net/v1/config/upload?type=comment").then((res) => {
|
||||
ajaxGet("/v2/api/config/upload?type=comment").then((res) => {
|
||||
let data = res.data;
|
||||
uploadConfig = data;
|
||||
resolve();
|
||||
@@ -641,6 +800,8 @@ const appSectionIndex = createApp({
|
||||
images: image,
|
||||
};
|
||||
|
||||
console.log("userInfoWin", userInfoWin.value);
|
||||
|
||||
ajax("/v2/api/forum/postComment", {
|
||||
content,
|
||||
token,
|
||||
@@ -670,6 +831,7 @@ const appSectionIndex = createApp({
|
||||
attachments,
|
||||
picture: [],
|
||||
timestamp,
|
||||
user: { ...userInfoWin.value },
|
||||
};
|
||||
|
||||
commentList.value[index]["child"].push(targetData);
|
||||
@@ -686,6 +848,7 @@ const appSectionIndex = createApp({
|
||||
attachments,
|
||||
picture: [],
|
||||
timestamp,
|
||||
user: { ...userInfoWin.value },
|
||||
};
|
||||
commentList.value[index]["child"].unshift(targetData);
|
||||
commentList.value[index]["childnum"]++;
|
||||
@@ -701,6 +864,7 @@ const appSectionIndex = createApp({
|
||||
attachments,
|
||||
picture: [],
|
||||
timestamp,
|
||||
user: { ...userInfoWin.value },
|
||||
};
|
||||
commentList.value.unshift(targetData);
|
||||
inputTextarea.value = "";
|
||||
@@ -839,8 +1003,8 @@ const appSectionIndex = createApp({
|
||||
else target = picture.value;
|
||||
}
|
||||
|
||||
if (target.length >= maxPicture) {
|
||||
creationAlertBox("error", `最多只能上传 ${maxPicture} 张图片`);
|
||||
if (target.length >= maxPicture.value) {
|
||||
creationAlertBox("error", `最多只能上传 ${maxPicture.value} 张图片`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -870,6 +1034,39 @@ const appSectionIndex = createApp({
|
||||
}
|
||||
};
|
||||
|
||||
const alsoCommentsData = (index, i) => {
|
||||
if (!isLogin.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
const parentid = commentList.value[index]["id"];
|
||||
|
||||
ajax("/v2/api/forum/childrenList", {
|
||||
token,
|
||||
parentid,
|
||||
limit: 2000,
|
||||
page: 1,
|
||||
childlimit: 3,
|
||||
}).then((res) => {
|
||||
if (res.code != 200) {
|
||||
creationAlertBox("error", res.message || "操作成功");
|
||||
return;
|
||||
}
|
||||
let data = res.data;
|
||||
|
||||
data.data.forEach((element, index) => {
|
||||
element.timestamp = strtimeago(element.created_at, 4);
|
||||
element["isReplyBoxShow"] = 0;
|
||||
element["picture"] = [];
|
||||
});
|
||||
|
||||
let merged = [...commentList.value[index]["child"], ...data.data.filter((item2) => !commentList.value[index]["child"].find((item1) => item1.id == item2.id))];
|
||||
|
||||
commentList.value[index]["child"] = merged;
|
||||
});
|
||||
};
|
||||
|
||||
// 自动输入框增高
|
||||
const autoResize = (e) => {
|
||||
e.target.style.height = "auto"; // 重置高度
|
||||
@@ -916,7 +1113,108 @@ const appSectionIndex = createApp({
|
||||
});
|
||||
};
|
||||
|
||||
return { 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, dialogSrc, 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 };
|
||||
let show = ref(false);
|
||||
let ismanager = ref(false);
|
||||
const cutShow = () => {
|
||||
show.value = !show.value; // 修改为切换显示状态
|
||||
};
|
||||
|
||||
let reportState = ref(false);
|
||||
let reportToken = ref("");
|
||||
provide("reportState", reportState);
|
||||
|
||||
// 举报
|
||||
const report = (token) => {
|
||||
cutShow();
|
||||
reportState.value = true;
|
||||
reportToken.value = token;
|
||||
};
|
||||
|
||||
// 隐藏
|
||||
const hide = () => {
|
||||
const target = info.value;
|
||||
managerHide(token, target.hidden, "thread").then((value) => {
|
||||
target.hidden = value;
|
||||
info.value = target;
|
||||
cutShow();
|
||||
});
|
||||
};
|
||||
|
||||
// 推荐
|
||||
const recommend = () => {
|
||||
const target = info.value;
|
||||
managerRecommend(token, target.recommend).then((value) => {
|
||||
target.recommend = value;
|
||||
info.value = target;
|
||||
cutShow();
|
||||
});
|
||||
};
|
||||
|
||||
// 精华
|
||||
const essence = () => {
|
||||
const target = info.value;
|
||||
managerEssence(token, target.best).then((value) => {
|
||||
target.best = value;
|
||||
info.value = target;
|
||||
cutShow();
|
||||
});
|
||||
};
|
||||
|
||||
const copyLinkClick = () => {
|
||||
copyForumUid(location.href);
|
||||
};
|
||||
|
||||
const goPersonalHomepage = (token) => {
|
||||
if (!token) return;
|
||||
redirectToExternalWebsite(`/u/${token}`);
|
||||
};
|
||||
|
||||
let searchInput = ref("");
|
||||
let defaultSearchText = ref("屯特");
|
||||
const goSearch = () => {
|
||||
const searchText = searchInput.value || defaultSearchText.value;
|
||||
redirectToExternalWebsite("/search/" + searchText);
|
||||
};
|
||||
|
||||
const edit = () => {
|
||||
redirectToExternalWebsite(`/publish?uniqid=${info.value.uniqid}`);
|
||||
};
|
||||
|
||||
let pitchInputState = ref(false);
|
||||
|
||||
const sidebarFixed = ref(false);
|
||||
|
||||
const handleScroll = () => {
|
||||
matterHeight.value = -(matterRef.value.offsetHeight - window.innerHeight);
|
||||
sidebarHeight.value = -(sidebarRef.value.offsetHeight - window.innerHeight);
|
||||
if (matterHeight.value > 0) matterHeight.value = 12;
|
||||
if (sidebarHeight.value > 0) sidebarHeight.value = 12;
|
||||
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
|
||||
const clientHeight = window.innerHeight;
|
||||
|
||||
// 列表下 滑动到底部 获取新数据
|
||||
if (scrollTop + clientHeight >= scrollHeight - 200) getComment();
|
||||
};
|
||||
|
||||
const matterRef = ref(null);
|
||||
const sidebarRef = ref(null);
|
||||
|
||||
const deleteItem = () => {
|
||||
managerDelete(token)
|
||||
.then(() => redirectToExternalWebsite("/", "_self"))
|
||||
.finally(() => cutShow());
|
||||
};
|
||||
|
||||
let sidebarHeight = ref(0);
|
||||
let matterHeight = ref(0);
|
||||
|
||||
const share = () => {
|
||||
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 };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -929,5 +1227,7 @@ appSectionIndex.component("itemTenement", itemTenement);
|
||||
appSectionIndex.component("latestList", latestList);
|
||||
appSectionIndex.component("slideshowBox", slideshowBox);
|
||||
appSectionIndex.component("like", like);
|
||||
appSectionIndex.component("report", report);
|
||||
appSectionIndex.component("headTop", headTop);
|
||||
|
||||
appSectionIndex.mount("#details");
|
||||
|
||||
492
js/edit.js
492
js/edit.js
@@ -6,12 +6,18 @@ const editApp = createApp({
|
||||
setup() {
|
||||
let titleLength = ref(200);
|
||||
|
||||
let uniqid = ref("");
|
||||
onMounted(() => {
|
||||
const params = getUrlParams();
|
||||
uniqid.value = params.uniqid || "";
|
||||
|
||||
getUserInfoWin();
|
||||
|
||||
cUpload();
|
||||
init();
|
||||
|
||||
checkWConfig();
|
||||
|
||||
// 添加selectionchange事件监听,当鼠标选中区域内容时更新lastSelection
|
||||
document.addEventListener("selectionchange", handleSelectionChange);
|
||||
});
|
||||
@@ -21,19 +27,46 @@ const editApp = createApp({
|
||||
document.removeEventListener("selectionchange", handleSelectionChange);
|
||||
});
|
||||
|
||||
let isLogin = ref(true);
|
||||
let realname = ref(1); // 是否已经实名
|
||||
let userInfoWin = ref({
|
||||
authority: ["comment.edit", "comment.delete", "offercollege.hide", "offersummary.hide", "mj.hide", "topic:manager", "topic:hide"],
|
||||
avatar: "https://nas.gter.net:9008/avatar/97K4EWIMLrsbGTWXslC2WFVSEKWOikN42jDKLNjtax7HL4xtfMOJSdU9oWFhY2E~/middle?random=1761733169",
|
||||
groupid: 3,
|
||||
nickname: "肖荣豪",
|
||||
realname: 1,
|
||||
token: "01346a38444d71aaadb3adad52b52c39",
|
||||
uid: 500144,
|
||||
uin: 4238049,
|
||||
});
|
||||
let imageLength = 10;
|
||||
let videoLength = 5;
|
||||
|
||||
const checkWConfig = () => {
|
||||
const wConfig = JSON.parse(localStorage.getItem("wConfig")) || {};
|
||||
console.log("wConfig", wConfig);
|
||||
|
||||
if (wConfig.time) {
|
||||
const time = new Date(wConfig.time);
|
||||
const now = new Date();
|
||||
if (now - time > 24 * 60 * 60 * 1000) getWConfig();
|
||||
else {
|
||||
const config = wConfig.config || {};
|
||||
titleLength.value = config.max_topic_title_length;
|
||||
imageLength = config.topic_image_count || 0;
|
||||
videoLength = config.topic_video_count || 0;
|
||||
}
|
||||
} else {
|
||||
getWConfig();
|
||||
}
|
||||
};
|
||||
|
||||
const getWConfig = () => {
|
||||
ajaxGet("/v2/api/config/website").then((res) => {
|
||||
if (res.code == 200) {
|
||||
let data = res["data"] || {};
|
||||
const config = data.config || {};
|
||||
titleLength.value = config.max_topic_title_length;
|
||||
imageLength = config.topic_image_count || 0;
|
||||
videoLength = config.topic_video_count || 0;
|
||||
|
||||
data.time = new Date().toISOString();
|
||||
localStorage.setItem("wConfig", JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let isLogin = ref(false);
|
||||
let realname = ref(0); // 是否已经实名
|
||||
let userInfoWin = ref({});
|
||||
let permissions = ref([]);
|
||||
|
||||
const getUserInfoWin = () => {
|
||||
@@ -82,22 +115,21 @@ const editApp = createApp({
|
||||
let token = ref("");
|
||||
let infoImages = [];
|
||||
const init = () => {
|
||||
ajax("/v2/api/forum/postPublishInit")
|
||||
ajax("/v2/api/forum/postPublishInit", {
|
||||
uniqid: uniqid.value,
|
||||
})
|
||||
.then((res) => {
|
||||
const data = res.data;
|
||||
if (res.code != 200) {
|
||||
creationAlertBox(res.message || "操作失败");
|
||||
creationAlertBox("error", res.message || "操作失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const infoTarget = data.info || {};
|
||||
|
||||
infoImages = infoTarget.attachments?.images || [];
|
||||
|
||||
if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoImages);
|
||||
if (infoTarget.content) infoTarget.content = restoreHtml(infoTarget.content, infoTarget.attachments);
|
||||
|
||||
info.value = infoTarget;
|
||||
tagList.value = data.tagList;
|
||||
token.value = data.token;
|
||||
|
||||
nextTick(() => {
|
||||
@@ -109,7 +141,12 @@ const editApp = createApp({
|
||||
});
|
||||
};
|
||||
|
||||
const restoreHtml = (formattedText, imageList) => {
|
||||
const restoreHtml = (formattedText, attachments) => {
|
||||
const imageList = attachments?.images || [];
|
||||
|
||||
const filesList = attachments?.files || [];
|
||||
const videosList = attachments?.videos || [];
|
||||
|
||||
let html = formattedText;
|
||||
|
||||
// 1. 还原换行符为<br>标签
|
||||
@@ -130,22 +167,52 @@ const editApp = createApp({
|
||||
// 查找对应的图片信息
|
||||
const image = imageList.find((img) => img.aid == aid);
|
||||
if (image) {
|
||||
return `<img src="${image.url}" data-aid="${aid}">`;
|
||||
imageList.splice(imageList.indexOf(image), 1);
|
||||
return `<img src="${image.url}" data-aid="${aid}"><br/>`;
|
||||
}
|
||||
return match; // 未找到对应图片时保留原始标记
|
||||
});
|
||||
|
||||
html = html.replace(/\[attach\](\d+)\[\/attach\]/gi, (match, aid) => {
|
||||
// 查找对应的图片信息
|
||||
const image = imageList.find((img) => img.aid == aid);
|
||||
if (image) {
|
||||
imageList.splice(imageList.indexOf(image), 1);
|
||||
return `<img src="${image.url}" data-aid="${aid}"><br/>`;
|
||||
}
|
||||
|
||||
// 查找对应的视频信息
|
||||
const video = videosList.find((v) => v.aid == aid);
|
||||
if (video) {
|
||||
console.log("video", video);
|
||||
videosList.splice(videosList.indexOf(video), 1);
|
||||
return `<video contenteditable="false" src="${video.url}" width="400" height="400" preload="none" poster="${video.posterurl}" aid="${video.aid}" posterid="${video.posterid}" controls></video>`;
|
||||
}
|
||||
|
||||
return match; // 未找到对应图片时保留原始标记
|
||||
});
|
||||
|
||||
// 6. 还原填充标签
|
||||
html = html.replace(/(<span class="blue">[^<]+<\/span>)\s+/gi, '$1 <span class="fill"></span> ');
|
||||
|
||||
// 7. 清理多余的<br>标签
|
||||
html = html.replace(/<br><br>/g, "<br>");
|
||||
|
||||
imageList.forEach((element) => {
|
||||
html += `<img src="${element.url}" data-aid="${element.aid}"><br/>`;
|
||||
});
|
||||
|
||||
// video 不要预加载
|
||||
videosList.forEach((element) => {
|
||||
html += `<video contenteditable="false" src="${element.url}" width="400" height="400" preload="none" poster="${element.posterurl}" aid="${element.aid}" posterid="${element.posterid}" controls></video><br/>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => focusLastNode(), 1000);
|
||||
// document.addEventListener("keydown", handleUndoKeydown);
|
||||
});
|
||||
|
||||
const editorRef = ref(null);
|
||||
@@ -166,51 +233,89 @@ const editApp = createApp({
|
||||
const maxSize = 20 * 1024 * 1024; // 20MB
|
||||
|
||||
const insertImage = (event) => {
|
||||
let config = uConfigData;
|
||||
const images = extractImages(editorRef.value);
|
||||
|
||||
const count = imageLength - images.length || 0;
|
||||
|
||||
if (count == 0) {
|
||||
creationAlertBox("error", `最多只能上传 ${imageLength} 张图片`);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target.files[0];
|
||||
if (!target) return; // 处理未选择文件的情况
|
||||
|
||||
if (target.size > maxSize) {
|
||||
creationAlertBox("文件大小不能超过 20MB");
|
||||
creationAlertBox("error", "文件大小不能超过 20MB");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 不要删除,后面会用
|
||||
const formData = new FormData();
|
||||
formData.append(config.requestName, target); // 文件数据
|
||||
formData.append("name", target.name); // 文件名
|
||||
formData.append("type", "image"); // 文件名
|
||||
formData.append("data", config.params.data); // 文件名
|
||||
uploading(target, target.name, "image").then((data) => {
|
||||
const selection = window.getSelection();
|
||||
editorRef.value.focus();
|
||||
if (lastSelection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
}
|
||||
const html = `<img src="${data.url}" data-aid="${data.aid}"><br/>`;
|
||||
document.execCommand("insertHTML", false, html);
|
||||
judgeIsEmpty();
|
||||
});
|
||||
};
|
||||
|
||||
ajax(config.url, formData)
|
||||
.then((res) => {
|
||||
const data = res.data;
|
||||
try {
|
||||
const range = lastSelection;
|
||||
const img = document.createElement("img");
|
||||
const insertVideo = async (event) => {
|
||||
const videos = extractVideos(editorRef.value);
|
||||
|
||||
img.src = data.url;
|
||||
img.setAttribute("data-aid", data.aid);
|
||||
range.insertNode(img);
|
||||
const div = document.createElement("div");
|
||||
range.insertNode(div);
|
||||
judgeIsEmpty();
|
||||
} catch (error) {
|
||||
console.error("插入图片出错:", error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
const count = videoLength - videos.length || 0;
|
||||
|
||||
if (count == 0) {
|
||||
creationAlertBox("error", `最多只能上传 ${videoLength} 个视频`);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoFile = event.target.files[0];
|
||||
|
||||
if (!videoFile) return; // 处理未选择文件的情况
|
||||
|
||||
if (videoFile.size > maxSize) {
|
||||
creationAlertBox("error", "文件大小不能超过 20MB");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
console.log("videoFile", videoFile);
|
||||
|
||||
// 步骤1:提取视频第一帧(等待提取完成)
|
||||
const coverFile = await getVideoFirstFrame(videoFile);
|
||||
console.log("第一帧提取成功", coverFile);
|
||||
|
||||
// 步骤2:先上传视频文件(type 传 'video',按后端要求调整)
|
||||
const videoUploadRes = await uploading(videoFile, videoFile.name, "video");
|
||||
console.log("视频上传成功", videoUploadRes);
|
||||
|
||||
// 步骤3:再上传第一帧封面(type 传 'cover',按后端要求调整)
|
||||
const coverUploadRes = await uploading(coverFile, coverFile.name, "image");
|
||||
console.log("封面上传成功", coverUploadRes);
|
||||
|
||||
console.log("最终", videoUploadRes, videoUploadRes);
|
||||
|
||||
const selection = window.getSelection();
|
||||
editorRef.value.focus();
|
||||
if (lastSelection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
}
|
||||
const html = `<video width="400" height="400" controls preload="none" poster="${coverUploadRes.url}" src="${videoUploadRes.url}" contenteditable="false" posterid="${coverUploadRes.aid}" aid="${videoUploadRes.aid}"></video><br/>`;
|
||||
document.execCommand("insertHTML", false, html);
|
||||
judgeIsEmpty();
|
||||
};
|
||||
|
||||
let isEmpty = ref(true);
|
||||
|
||||
const onEditorInput = (event) => {
|
||||
console.log("onEditorInput");
|
||||
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection.rangeCount > 0) {
|
||||
@@ -221,7 +326,7 @@ const editApp = createApp({
|
||||
|
||||
judgeIsEmpty();
|
||||
|
||||
debouncedGetTagList();
|
||||
// debouncedGetTagList();
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
@@ -260,26 +365,28 @@ const editApp = createApp({
|
||||
};
|
||||
|
||||
const getTagList = () => {
|
||||
if (!isLogin.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
const content = editorRef.value.innerText;
|
||||
axios
|
||||
.post("https://api.gter.net/v2/api/forum/postPublishTags", {
|
||||
content,
|
||||
})
|
||||
.then((res) => {
|
||||
res = res.data;
|
||||
if (res.code != 200) return;
|
||||
let data = res.data || [];
|
||||
ajax("/v2/api/forum/postPublishTags", {
|
||||
content,
|
||||
}).then((res) => {
|
||||
res = res.data;
|
||||
if (res.code != 200) return;
|
||||
let data = res.data || [];
|
||||
|
||||
// 随机生成一下数据
|
||||
for (let i = 0; i < 5; i++) {
|
||||
data.push({
|
||||
title: getRandomChinese() + getRandomChinese(),
|
||||
tagId: generateRandomString(),
|
||||
});
|
||||
}
|
||||
// 随机生成一下数据
|
||||
for (let i = 0; i < 5; i++) {
|
||||
data.push({
|
||||
title: getRandomChinese() + getRandomChinese(),
|
||||
tagId: generateRandomString(),
|
||||
});
|
||||
}
|
||||
|
||||
tagList.value = data;
|
||||
});
|
||||
tagList.value = data;
|
||||
});
|
||||
};
|
||||
|
||||
const debouncedGetTagList = debounce(getTagList, 500);
|
||||
@@ -296,7 +403,7 @@ const editApp = createApp({
|
||||
// 判断是否为空
|
||||
const judgeIsEmpty = () => {
|
||||
const text = editorRef.value.innerText;
|
||||
isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img");
|
||||
isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img") && !editorRef.value.querySelector("video");
|
||||
};
|
||||
|
||||
// 处理选中文本变化的函数
|
||||
@@ -309,7 +416,6 @@ const editApp = createApp({
|
||||
const commonAncestor = range.commonAncestorContainer;
|
||||
if (editorRef.value.contains(commonAncestor)) {
|
||||
console.log("选中区域在编辑器内", range);
|
||||
|
||||
lastSelection = range;
|
||||
}
|
||||
}
|
||||
@@ -366,36 +472,6 @@ const editApp = createApp({
|
||||
|
||||
const cutAnonymity = () => (info.value.anonymous = info.value.anonymous ? 0 : 1);
|
||||
|
||||
const insertLabel = (id) => {
|
||||
const index = tagList.value.findIndex((item) => item.tagId == id);
|
||||
if (index == -1) return;
|
||||
const label = tagList.value[index].title;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.innerHTML = `<span class="blue">#${label}</span> <span class="fill"></span> `;
|
||||
lastSelection.insertNode(span);
|
||||
|
||||
// 移动光标到元素后面并确保光标位置被正确设置和获取
|
||||
const newRange = document.createRange();
|
||||
newRange.setStartAfter(span);
|
||||
newRange.setEndAfter(span);
|
||||
|
||||
// 更新选择范围
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
lastSelection = newRange;
|
||||
|
||||
// 手动触发selectionchange事件,确保其他组件知道光标位置变化
|
||||
const selectionChangeEvent = new Event("selectionchange", { bubbles: true });
|
||||
document.dispatchEvent(selectionChangeEvent);
|
||||
|
||||
judgeIsEmpty();
|
||||
|
||||
// 删除 tagList 中当前标签
|
||||
tagList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
let emojiState = ref(false);
|
||||
|
||||
const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]);
|
||||
@@ -405,22 +481,13 @@ const editApp = createApp({
|
||||
const closeEmoji = () => (emojiState.value = false);
|
||||
|
||||
const selectEmoji = (emoji) => {
|
||||
const textNode = document.createTextNode(emoji);
|
||||
lastSelection.insertNode(textNode);
|
||||
|
||||
// 移动光标到emoji后面并确保光标位置被正确设置和获取
|
||||
const newRange = document.createRange();
|
||||
newRange.setStartAfter(textNode);
|
||||
newRange.setEndAfter(textNode);
|
||||
|
||||
// 更新选择范围
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
lastSelection = newRange;
|
||||
|
||||
// 手动触发selectionchange事件,确保其他组件知道光标位置变化
|
||||
const selectionChangeEvent = new Event("selectionchange", { bubbles: true });
|
||||
document.dispatchEvent(selectionChangeEvent);
|
||||
editorRef.value.focus();
|
||||
if (lastSelection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
}
|
||||
document.execCommand("insertText", false, emoji);
|
||||
closeEmoji();
|
||||
judgeIsEmpty();
|
||||
};
|
||||
@@ -430,14 +497,21 @@ const editApp = createApp({
|
||||
const infoTarget = { ...info.value } || {};
|
||||
let content = editorRef.value.innerHTML;
|
||||
|
||||
const images = extractImages(content);
|
||||
const images = extractImages(editorRef.value);
|
||||
const videos = extractVideos(editorRef.value);
|
||||
|
||||
infoTarget.attachments = infoTarget.attachments || {};
|
||||
infoTarget.attachments.images = images;
|
||||
infoTarget.attachments.videos = videos;
|
||||
|
||||
info.value["attachments"] = info.value["attachments"] || {};
|
||||
info.value["attachments"]["images"] = images;
|
||||
console.log("转换前:", content);
|
||||
info.value["attachments"]["videos"] = videos;
|
||||
|
||||
console.log(content);
|
||||
content = formatContent(content);
|
||||
console.log("转换后:", content);
|
||||
console.log(content);
|
||||
|
||||
const data = {
|
||||
...infoTarget,
|
||||
content,
|
||||
@@ -456,8 +530,8 @@ const editApp = createApp({
|
||||
|
||||
creationAlertBox("success", res.message || "操作成功");
|
||||
const back = () => {
|
||||
if (status == 1) redirectToExternalWebsite("./details.html?uniqid=" + data.uniqid);
|
||||
else redirectToExternalWebsite("./index.html");
|
||||
if (status == 1) redirectToExternalWebsite("/details/" + data.uniqid, "_self");
|
||||
else redirectToExternalWebsite("/", "_self");
|
||||
};
|
||||
|
||||
setTimeout(() => back(), 1500);
|
||||
@@ -468,15 +542,12 @@ const editApp = createApp({
|
||||
// 1. 替换图片标签
|
||||
html = html.replace(/<img[^>]*data-aid="(\d+)"[^>]*>/gi, "[attachimg]$1[/attachimg]");
|
||||
|
||||
// 1.1 替换视频标签
|
||||
html = html.replace(/<video[^>]*aid="(\d+)"[^>]*>[\s\S]*?<\/video>/gi, "[attach]$1[/attach]");
|
||||
|
||||
// 2. 替换H2标签
|
||||
html = html.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "[b]$1[/b]");
|
||||
|
||||
// 3. 替换标签(保留与前后内容的连续性)
|
||||
html = html.replace(/<span\s+class="blue">#([^<]+)<\/span>/gi, "[tag]$1[/tag]");
|
||||
|
||||
// 4. 移除无关标签(如空的<span class="fill"></span>)
|
||||
html = html.replace(/<span\s+class="fill">[^<]*<\/span>/gi, "");
|
||||
|
||||
// 5. 处理块级标签换行(仅<div>等块级标签前后换行,保持行内内容连续)
|
||||
// 块级标签:div、p、h1-h6等,这里以div为例
|
||||
html = html.replace(/<\/div>\s*/gi, "</div>\n"); // 闭合div后换行
|
||||
@@ -485,8 +556,8 @@ const editApp = createApp({
|
||||
// 6. 处理<br>为换行
|
||||
html = html.replace(/<br\s*\/?>/gi, "\n");
|
||||
|
||||
// 7. 移除所有剩余HTML标签
|
||||
html = html.replace(/<[^>]+>/gi, "");
|
||||
// 7. 移除所有剩余HTML标签 a标签除外
|
||||
html = html.replace(/<(?!(a\b|\/a\b))[^>]+>/gi, "");
|
||||
|
||||
// 8. 清理连续换行(最多保留两个空行,避免过多空行)
|
||||
html = html.replace(/\n{3,}/g, "\n\n");
|
||||
@@ -496,29 +567,67 @@ const editApp = createApp({
|
||||
return html;
|
||||
};
|
||||
|
||||
const extractImages = (html) => {
|
||||
const extractImages = (dom) => {
|
||||
const images = [];
|
||||
// 正则匹配 img 标签,提取 src(url)和 data-aid
|
||||
const imgRegex = /<img[^>]*src="([^"]+)"[^>]*data-aid="(\d+)"[^>]*>/gi;
|
||||
let match;
|
||||
|
||||
// 循环匹配所有图片标签
|
||||
while ((match = imgRegex.exec(html)) !== null) {
|
||||
// 直接查找页面中所有带 data-aid 的 img 标签
|
||||
const imgElements = dom.querySelectorAll("img");
|
||||
|
||||
imgElements.forEach((imgEl) => {
|
||||
const url = imgEl.getAttribute("src")?.trim() || "";
|
||||
const aid = imgEl.dataset.aid?.trim() || ""; // 用 dataset 简化自定义属性读取
|
||||
|
||||
images.push({
|
||||
url: match[1], // 图片的 src 地址
|
||||
aid: Number(match[2]), // 图片的 data-aid 属性值
|
||||
url,
|
||||
aid: Number(aid),
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
return images;
|
||||
};
|
||||
|
||||
const extractVideos = (dom) => {
|
||||
// 1. 查找页面中所有 <video> 节点(返回 NodeList 集合)
|
||||
const videoElements = dom.querySelectorAll("video");
|
||||
const result = [];
|
||||
|
||||
// 2. 遍历每个 video 节点,直接获取属性
|
||||
videoElements.forEach((videoEl) => {
|
||||
const url = videoEl.getAttribute("src")?.trim() || ""; // 视频地址
|
||||
const posterurl = videoEl.getAttribute("poster")?.trim() || ""; // 封面图
|
||||
const aid = videoEl.getAttribute("aid")?.trim() || ""; // 视频 ID(自定义属性)
|
||||
const posterid = videoEl.getAttribute("posterid")?.trim() || ""; // 封面 ID(自定义属性)
|
||||
|
||||
result.push({
|
||||
aid: Number(aid),
|
||||
posterid: Number(posterid),
|
||||
url,
|
||||
posterurl,
|
||||
});
|
||||
});
|
||||
|
||||
console.log("提取完成的视频列表:", result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection.rangeCount > 0) {
|
||||
lastSelection = selection.getRangeAt(0);
|
||||
// console.log("更新选区");
|
||||
updatePTitleStatus();
|
||||
}
|
||||
};
|
||||
|
||||
let linkUrl = ref("");
|
||||
let linkText = ref("");
|
||||
|
||||
let linkState = ref(false);
|
||||
const openLink = () => {
|
||||
console.log("打开链接");
|
||||
const text = lastSelection ? lastSelection.toString().trim() : "";
|
||||
console.log("lastSelection", text);
|
||||
linkText.value = text;
|
||||
linkState.value = true;
|
||||
};
|
||||
|
||||
@@ -534,31 +643,98 @@ const editApp = createApp({
|
||||
creationAlertBox("error", "请输入链接文字和链接地址");
|
||||
return;
|
||||
}
|
||||
const a = document.createElement("a");
|
||||
a.href = linkUrl.value;
|
||||
a.target = "_blank";
|
||||
a.textContent = linkText.value;
|
||||
console.log("insertLink", lastSelection);
|
||||
|
||||
// 先删除选中的内容,再插入链接
|
||||
lastSelection.deleteContents();
|
||||
lastSelection.insertNode(a);
|
||||
|
||||
// 移动光标到元素后面并确保光标位置被正确设置和获取
|
||||
const newRange = document.createRange();
|
||||
newRange.setStartAfter(a);
|
||||
newRange.setEndAfter(a);
|
||||
|
||||
// 更新选择范围
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
lastSelection = newRange;
|
||||
|
||||
editorRef.value.focus();
|
||||
if (lastSelection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
}
|
||||
const html = `<a href="${linkUrl.value}" target="_blank" contenteditable="false">${linkText.value}</a>`;
|
||||
document.execCommand("insertHTML", false, html);
|
||||
closeLink();
|
||||
judgeIsEmpty();
|
||||
};
|
||||
|
||||
return { insertLink, linkUrl, linkText, linkState, openLink, closeLink, userInfoWin, titleLength, submit, insertLabel, emojiState, openEmoji, closeEmoji, selectEmoji, optionEmoji, isPTitle, onEditorInput, onEditorFocus, onEditorBlur, paragraphTitle, info, tagList, token, cutAnonymity, editorRef, insertImage, judgeIsEmpty, isEmpty };
|
||||
const uploading = (target, name, type) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let config = uConfigData;
|
||||
const formData = new FormData();
|
||||
formData.append(config.requestName, target); // 文件数据
|
||||
formData.append("name", name); // 文件名
|
||||
formData.append("type", type); // 文件名
|
||||
formData.append("data", config.params.data); // 文件名
|
||||
|
||||
ajax(config.url, formData)
|
||||
.then((res) => {
|
||||
const data = res.data;
|
||||
try {
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
console.error("插入图片出错:", error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 封装:提取视频第一帧(返回 Promise,resolve 第一帧 Blob)
|
||||
const getVideoFirstFrame = (videoFile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!(videoFile instanceof File) || !videoFile.type.startsWith("video/")) {
|
||||
reject(new Error("请传入合法的视频文件"));
|
||||
return;
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(videoFile);
|
||||
const video = document.createElement("video");
|
||||
video.src = objectUrl;
|
||||
video.preload = "auto";
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
|
||||
const cleanup = () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
video.src = "";
|
||||
};
|
||||
|
||||
video.addEventListener("error", () => {
|
||||
cleanup();
|
||||
reject(new Error("视频加载失败,请检查文件完整性"));
|
||||
}, { once: true });
|
||||
|
||||
video.addEventListener("loadeddata", () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const w = video.videoWidth || 320;
|
||||
const h = video.videoHeight || 240;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext("2d");
|
||||
try {
|
||||
ctx.drawImage(video, 0, 0, w, h);
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
canvas.toBlob((blob) => {
|
||||
cleanup();
|
||||
if (!blob) {
|
||||
reject(new Error("第一帧提取失败,Blob 生成异常"));
|
||||
return;
|
||||
}
|
||||
const frameFile = new File([blob], `video_cover_${Date.now()}.png`, { type: "image/png" });
|
||||
resolve(frameFile);
|
||||
}, "image/png", 0.9);
|
||||
}, { once: true });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const linkClick = () => {};
|
||||
|
||||
return { 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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user