diff --git a/packages/mobile/src/components/PostDetail.vue b/packages/mobile/src/components/PostDetail.vue index fbfc842..49006f0 100644 --- a/packages/mobile/src/components/PostDetail.vue +++ b/packages/mobile/src/components/PostDetail.vue @@ -77,6 +77,8 @@ +
+
高赞回复 @@ -305,10 +307,7 @@ export default { return [] }, topReply() { - return this.post.replyList - .filter(v => v.thankCount >= this.config.topReplyLoveMinCount) - .sort((a, b) => b.thankCount - a.thankCount) - .slice(0, this.config.topReplyCount) + return this.post.topReplyList }, replyList() { // console.log('this.post.nestedReplies', this.post.nestedReplies) @@ -358,6 +357,7 @@ export default { } }, 'post.headerTemplate'(n, o) { + nextTick(this.mountTopicTipBox) let mountEl = document.querySelector('.main-wrapper .post-wrapper .html-wrapper .header') if (mountEl) { this.showOpTag = true @@ -394,6 +394,7 @@ export default { }, mounted() { this.debounceScroll = debounce(this.scroll, 300, false) + this.mountTopicTipBox() if (this.isLogin) { const observer = new IntersectionObserver( ([e]) => e.target.toggleAttribute('stuck', e.intersectionRatio < 1), @@ -542,6 +543,16 @@ export default { collapseTopReplyList() { $(this.$refs.topReply).slideToggle('fast') }, + mountTopicTipBox() { + if (!this.isPost) return + + const mountEl = this.$refs.topicTipBox + const tipBox = document.querySelector('#topic-tip-box') + if (!mountEl || !tipBox) return + + mountEl.append(tipBox) + tipBox.style.display = '' + }, goBottom() { this.isSticky = false setTimeout(() => { @@ -675,6 +686,16 @@ export default { } } + .native-topic-tip { + width: 100%; + + :deep(#topic-tip-box) { + display: block; + width: 100%; + box-sizing: border-box; + } + } + .loading-wrapper { height: 20rem; display: flex; diff --git a/packages/mobile/src/main.ts b/packages/mobile/src/main.ts index d7bfcef..7bf23ac 100644 --- a/packages/mobile/src/main.ts +++ b/packages/mobile/src/main.ts @@ -1,5 +1,3 @@ -import {createApp} from 'vue'; -import App from './pages/App.vue'; import {GM_notification} from "gmApi" import './global.d.ts' import {PageType, Post, Reply} from "@v2next/core/types" @@ -7,23 +5,50 @@ import {DefaultUser, functions, getDefaultConfig, getDefaultPost} from "@v2next/ let isMobile = !document.querySelector('#Rightbar'); +function isReplyCell(cell: Element) { + return /^r_\d+$/.test(cell.id) +} + function findReplyBoxMobile(boxs: any) { - // 根据内容的元素属性特征定位回复区域 + let fallbackBox + + // 根据回复 cell 的稳定 id 特征定位回复区域 for (let i = 1; i < boxs.length; i++) { const box = boxs[i]; const cells = box.querySelectorAll ? box.querySelectorAll('.cell') : []; - - if (cells && cells.length > 0) { + + if (!cells || !cells.length || box.id === 'topic-tip-box' || box.id === 'reply-box') { + continue + } + + if (Array.from(cells).some((cell: any) => isReplyCell(cell))) { + return box; + } + + if (!fallbackBox) { const firstCell = cells[0]; - if (firstCell.querySelector?.('.snow')) { - return box; - } + if (firstCell.querySelector?.('.snow')) { + fallbackBox = box; + } else if (box.querySelector?.('.page_normal')) { + fallbackBox = box; + } } } - + // 回退逻辑 console.warn('[V2Next] 无法智能检测回复区域,使用默认位置'); - return boxs[1]; + return fallbackBox; +} + +function applyNestedReplyCounts(post: Post) { + const replyByFloor = new Map(post.replyList.map(reply => [Number(reply.floor), reply])) + + post.nestedReplies.forEach((nestedReply) => { + const reply = replyByFloor.get(Number(nestedReply.floor)) + if (reply) { + reply.replyCount = nestedReply.replyCount + } + }) } let $section = document.createElement('section') @@ -227,7 +252,7 @@ function run() { box = findReplyBoxMobile(boxs) } - let cells: any = box.querySelectorAll('.cell') + let cells: any = box?.querySelectorAll('.cell') if (cells && cells.length) { // post.fr = boxs[1].querySelector('.inner')!.innerHTML @@ -239,19 +264,17 @@ function run() { let repliesMap: any[] = [] //如果第二条有id,就说明是第二条是回复。只有一页回复 - if (cells[1].id) { + if (cells[1] && isReplyCell(cells[1])) { repliesMap.push({i: pageNo, replyList: this.parsePageReplies(cells.slice(1))}) let replyList = functions.getAllReply(repliesMap) - post.replyList = replyList - post.replyCount = replyList.length - post.allReplyUsers = Array.from(new Set(replyList.map((v: any) => v.username))) - post.nestedReplies = functions.createNestedList(replyList) - post.nestedRedundReplies = functions.createNestedRedundantList(replyList) - post.nestedReplies.map((v) => { - post.replyList[Number(v.floor) - 1].replyCount = v.replyCount - }) + functions.createList(post, replyList) + applyNestedReplyCounts(post) return post } else { + if (!cells[1]) { + return post + } + let promiseList: any = [] // console.log(this.current.repliesMap) return new Promise((resolve, reject) => { @@ -270,20 +293,15 @@ function run() { results.filter((result) => result.status === "fulfilled").map(v => repliesMap.push(v.value)) let replyList = functions.getAllReply(repliesMap) - post.replyList = replyList - post.replyCount = replyList.length - post.allReplyUsers = Array.from(new Set(replyList.map((v: any) => v.username))) - post.nestedReplies = functions.createNestedList(replyList) - post.nestedRedundReplies = functions.createNestedRedundantList(replyList) - post.nestedReplies.map((v) => { - post.replyList[Number(v.floor) - 1].replyCount = v.replyCount - }) + functions.createList(post, replyList) + applyNestedReplyCounts(post) resolve(post) } ) }) } } + return post }, //请求帖子其他页的回复 fetchPostOtherPageReplies(href: string, pageNo: number) { @@ -298,7 +316,11 @@ function run() { box = findReplyBoxMobile(boxs) } }) - let cells: any = box!.querySelectorAll('.cell') + let cells: any = box?.querySelectorAll('.cell') + if (!cells || !cells.length) { + resolve({i: pageNo, replyList: []}) + return + } cells = Array.from(cells) resolve({i: pageNo, replyList: this.parsePageReplies(cells.slice(2, cells.length - 1))}) }).catch((r: any) => { @@ -1007,7 +1029,12 @@ if (isMobile) { })(); run() - let vueApp = createApp(App) - vueApp.config.unwrapInjectedRef = true - vueApp.mount($section); -} \ No newline at end of file + Promise.all([ + import('vue'), + import('./pages/App.vue'), + ]).then(([{createApp}, {default: App}]) => { + let vueApp = createApp(App) + vueApp.config.unwrapInjectedRef = true + vueApp.mount($section); + }) +} diff --git a/packages/mobile/src/pages/App.vue b/packages/mobile/src/pages/App.vue index 65aebdd..6f7f344 100644 --- a/packages/mobile/src/pages/App.vue +++ b/packages/mobile/src/pages/App.vue @@ -388,17 +388,10 @@ export default { regenerateReplyList() { // console.log('重新生成列表') if (this.current.replyList.length) { - this.current.replyCount = this.current.replyList.length - let res = functions.createNestedList(this.current.replyList) - if (res) { - this.current.nestedReplies = res - } - let dup_res = functions.createNestedRedundantList(this.current.replyList) - if (dup_res) { - this.current.nestedRedundReplies = dup_res - } + functions.createList(this.current, this.current.replyList) } else { this.current.replyCount = 0 + this.current.topReplyList = [] this.current.nestedReplies = [] this.current.nestedRedundReplies = [] } @@ -679,4 +672,3 @@ export default { gap: 1rem; } - diff --git a/packages/mobile/vite.config.ts b/packages/mobile/vite.config.ts index 9f42e69..379997a 100644 --- a/packages/mobile/vite.config.ts +++ b/packages/mobile/vite.config.ts @@ -51,6 +51,18 @@ export default defineConfig({ }, build: { fileName:'V2Next-Mobile.user.js', + cssSideEffects: (css: string) => ` + (() => { + if (document.querySelector('#Rightbar')) return; + if (typeof GM_addStyle === 'function') { + GM_addStyle(${JSON.stringify(css)}); + return; + } + const style = document.createElement('style'); + style.textContent = ${JSON.stringify(css)}; + document.head.append(style); + })(); + `, externalGlobals: { vue: cdn.jsdelivr('Vue', 'dist/vue.global.prod.js'), }, diff --git a/packages/pc/src/App.vue b/packages/pc/src/App.vue index 5a55baa..abafe2f 100644 --- a/packages/pc/src/App.vue +++ b/packages/pc/src/App.vue @@ -540,6 +540,10 @@ export default { showPost() { this.show = true $(`#Wrapper #Main .box:lt(3)`).each(function () { + if (this.id === 'topic-tip-box' || this.querySelector('#topic-tip-box')) { + $(this).show() + return + } $(this).hide() }) }, @@ -1066,4 +1070,3 @@ export default { } } - diff --git a/packages/pc/src/components/PostDetail.vue b/packages/pc/src/components/PostDetail.vue index f7e17be..c73e523 100644 --- a/packages/pc/src/components/PostDetail.vue +++ b/packages/pc/src/components/PostDetail.vue @@ -81,6 +81,7 @@ :api-url="'topic/'+post.id"/>
+
@@ -512,6 +513,7 @@ export default { this.$refs?.detail?.scrollTo({top: 0}) }) } + this.moveTopicTipBox() }, 'post.headerTemplate'(n, o) { let mountEl = document.querySelector('.main-wrapper .post-wrapper .html-wrapper .header') @@ -527,6 +529,7 @@ export default { this.currentFloor = '' nextTick(() => { this.$refs?.main?.focus() + this.moveTopicTipBox() }) } else { this.isSticky = false @@ -537,6 +540,7 @@ export default { }, mounted() { nextTick(() => { + this.moveTopicTipBox() setTimeout(() => { this.postDetailWidth = this.$refs.mainWrapper?.getBoundingClientRect().width || 0 }, 500) @@ -582,6 +586,14 @@ export default { eventBus.off(CMD.SHOW_CALL) }, methods: { + moveTopicTipBox() { + nextTick(() => { + let topicTipBox = document.querySelector('#topic-tip-box') + if (!topicTipBox || !this.$refs.topicTipMount) return + this.$refs.topicTipMount.appendChild(topicTipBox) + topicTipBox.style.display = '' + }) + }, addTag() { eventBus.emit(CMD.ADD_TAG, this.post.member.username) }, @@ -979,6 +991,17 @@ export default { } } + .topic-tip-mount { + width: 100%; + margin-bottom: 2rem; + + :deep(#topic-tip-box) { + width: 100%; + margin: 0; + box-sizing: border-box; + } + } + .loading-wrapper { height: 20rem; display: flex; diff --git a/packages/pc/src/main.ts b/packages/pc/src/main.ts index 176eeba..4efbafc 100644 --- a/packages/pc/src/main.ts +++ b/packages/pc/src/main.ts @@ -1,5 +1,3 @@ -import {createApp} from 'vue'; -import App from './App.vue'; import './global.d.ts' import { GM_registerMenuCommand, @@ -64,6 +62,13 @@ function run() { window.postList = [] window.isDeadline = dayjs().isAfter(dayjs('2024-11-26')) // window.isDeadline = true + function hasV2pConflict() { + if (window.user.username) { + return $(window.atob('LnYycC1ob3Zlci1idG4=')).length !== 0 + } + return $(window.atob('LnYycC1mb290ZXI=')).length !== 0 + } + window.parse = { //解析主题内容 async parsePostContent(post: Post, body: JQuery, htmlText: string) { @@ -418,13 +423,7 @@ function run() { //检测 if (window.pageType === PageType.Home) { const a = () => { - let d - if (window.user.username) { - d = $(window.atob('LnYycC1ob3Zlci1idG4=')).length - } else { - d = $(window.atob('LnYycC1mb290ZXI=')).length - } - if (d !== 0) { + if (hasV2pConflict()) { window.stopMe = true localStorage.setItem('d', '1') functions.cbChecker({type: 'syncData'}) @@ -996,12 +995,14 @@ function run() { window.parse.parsePagePostList(list, box) break case PageType.Post: - let d: string = localStorage.getItem('d') - if (d) { + if (hasV2pConflict()) { window.stopMe = true + localStorage.setItem('d', '1') functions.cbChecker({type: 'syncData'}) return } + localStorage.setItem('d', '') + box = document.querySelector('#Wrapper #Main .box') // @ts-ignore box.after($section) @@ -1178,9 +1179,14 @@ function run() { if (!isMobile) { console.log('V2EX PC端') run() - let vueApp = createApp(App) - vueApp.config.unwrapInjectedRef = true - vueApp.mount($section); + Promise.all([ + import('vue'), + import('./App.vue'), + ]).then(([{createApp}, {default: App}]) => { + let vueApp = createApp(App) + vueApp.config.unwrapInjectedRef = true + vueApp.mount($section); + }) // functions.loadAndRunScript("https://update.greasyfork.org/scripts/448472/v2%E6%96%B0%E5%B8%96%E6%8C%82%E4%BB%B6.user.js") functions.loadAndRunScript("https://update.greasyfork.org/scripts/448472/1074290/v2%E6%96%B0%E5%B8%96%E6%8C%82%E4%BB%B6.user.js") diff --git a/packages/pc/vite.config.ts b/packages/pc/vite.config.ts index 8116b95..760c742 100644 --- a/packages/pc/vite.config.ts +++ b/packages/pc/vite.config.ts @@ -55,6 +55,18 @@ export default defineConfig({ }, build: { fileName: 'V2Next.user.js', + cssSideEffects: (css: string) => ` + (() => { + if (!document.querySelector('#Rightbar')) return; + if (typeof GM_addStyle === 'function') { + GM_addStyle(${JSON.stringify(css)}); + return; + } + const style = document.createElement('style'); + style.textContent = ${JSON.stringify(css)}; + document.head.append(style); + })(); + `, externalGlobals: { vue: cdn.jsdelivr('Vue', 'dist/vue.global.prod.min.js'), dayjs: cdn.jsdelivr('dayjs', 'dayjs.min.js'),