重构dom选择器

This commit is contained in:
fzk
2025-01-22 17:31:58 +08:00
parent d34556b326
commit f4c3d129f9
7 changed files with 462 additions and 276 deletions
-248
View File
@@ -1,248 +0,0 @@
import React, { useEffect, useRef } from "react"
import { createRoot } from "react-dom/client"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "@plasmohq/storage/dist/hook"
import ValidateContent from "~component/contents/validateContent"
import { addCss, saveHtml, saveMarkdown } from "~tools"
import { useEditMarkdown } from "~utils/editMarkdownHook"
import { useParseMarkdown } from "~utils/parseMarkdownHook"
import { Print } from "~utils/print"
import Turndown from "~utils/turndown"
import Tooltip from "../ui/tooltip"
const turndownService = Turndown()
export default function CustomDomSelector() {
const [aiType, setAiType] = useStorage("app-aiType")
const isReady = useRef(false)
const isSelect = useRef(false)
const downloadType = useRef("")
const [editContent, setEditContent] = useEditMarkdown()
const [parseContent, setParseContent] = useParseMarkdown()
const selectorRef = useRef<HTMLElement | null>(null)
const tooltipRef = useRef<HTMLElement | null>(null)
const modalRef = useRef<HTMLElement | null>(null)
const articleTitle = document
.querySelector<HTMLElement>("head title")
?.innerText.trim()
useEffect(() => {
if (aiType) {
addEventListeners()
addCss(`.codebox-current { outline: 2px solid #42b88350 !important; }`)
}
return () => {
removeEventListeners()
removeTooltip()
removeHighlight()
}
}, [aiType])
const createTooltip = () => {
const tooltip = document.createElement("div")
tooltip.classList.add("codebox-tooltip")
tooltip.style.position = "absolute"
tooltip.style.zIndex = "2147483641"
tooltip.style.backgroundColor = "#fff"
tooltip.style.border = "1px solid #eee"
tooltip.style.borderRadius = "5px"
tooltip.style.padding = "8px"
tooltip.style.boxShadow = "0 2px 8px rgba(0, 0, 0, 0.15)"
document.body.appendChild(tooltip)
const root = createRoot(tooltip)
root.render(
<Tooltip
onConfirm={handleConfirm}
onCancel={handleCancel}
onNavigate={navigateElement}
/>
)
tooltipRef.current = tooltip
}
const removeTooltip = () => {
if (tooltipRef.current) {
document.body.removeChild(tooltipRef.current)
tooltipRef.current = null
}
}
const addEventListeners = () => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("click", handleClick)
}
const removeEventListeners = () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("click", handleClick)
}
const handleMouseMove = (event: MouseEvent) => {
if (isReady.current && !isSelect.current) {
const target = event.target as HTMLElement
highlightElement(target)
}
}
const handleClick = (event: MouseEvent) => {
if (isReady.current) {
const target = event.target as HTMLElement
const tooltip = target.closest(".codebox-tooltip")
const modal = target.closest(".codebox-modal")
const submit = target.closest(".valid-submit")
if (!tooltip && !modal && !submit) {
removeTooltip()
createTooltip()
isSelect.current = true
highlightElement(target)
updateTooltipPosition(target)
event.stopPropagation()
event.preventDefault()
}
}
}
const highlightElement = (element: HTMLElement) => {
removeHighlight()
element.classList.add("codebox-current")
selectorRef.current = element
}
const removeHighlight = () => {
const currentList = document.querySelectorAll(".codebox-current")
currentList.forEach((el) => el.classList.remove("codebox-current"))
selectorRef.current = null
}
const updateTooltipPosition = (element: HTMLElement) => {
const rect = element.getBoundingClientRect()
const distanceTop = rect.top + window.scrollY
const distanceLeft = rect.left + window.scrollX
const top =
distanceTop < 50 ? distanceTop + rect.height + 5 : distanceTop - 40
tooltipRef.current.style.top = `${top}px`
tooltipRef.current.style.left = `${distanceLeft + 5}px`
window.scrollTo({ top: top - 150, behavior: "smooth" })
}
useMessage(async (req: any, res: any) => {
const name = req.name
setCustom(name.split("-")[1])
})
const setCustom = (type: string) => {
downloadType.current = type
isReady.current = true
isSelect.current = false
}
const handleOkModal = () => {
if (!selectorRef.current || !downloadType.current) return
const current = document.querySelector(".codebox-current")
current.classList.remove("codebox-current")
switch (downloadType.current) {
case "downloadHtml":
saveHtml(selectorRef.current, articleTitle)
break
case "downloadMarkdown":
const markdown = turndownService.turndown(selectorRef.current)
saveMarkdown(markdown, articleTitle)
break
case "editMarkdown":
setEditContent(selectorRef.current)
break
case "parseMarkdown":
setParseContent(selectorRef.current, aiType)
break
case "downloadPdf":
const elementToPrint = selectorRef.current
if (elementToPrint) {
Print.print(elementToPrint, { title: articleTitle })
.then(() => console.log("Printing complete"))
.catch((error) => console.error("Printing failed:", error))
}
break
}
resetState()
setTimeout(() => {
document.body.removeChild(modalRef.current)
modalRef.current = null
}, 1000)
}
const handleConfirm = () => {
const modal = document.createElement("div")
modal.classList.add("codebox-modal")
modal.style.position = "fixed"
modal.style.zIndex = "2147483648"
modal.style.backgroundColor = "rgba(0, 0, 0, 0.7)"
modal.style.width = "100vw"
modal.style.height = "100vh"
modal.style.top = "0"
modal.style.left = "0"
document.body.appendChild(modal)
const root = createRoot(modal)
root.render(
<ValidateContent
type={downloadType.current}
handleOk={handleOkModal}
handleCancel={handleCancelModal}
/>
)
modalRef.current = modal
}
const handleCancel = () => {
resetState()
}
const resetState = () => {
removeHighlight()
removeTooltip()
isReady.current = false
isSelect.current = false
}
const navigateElement = (direction: "parent" | "child" | "prev" | "next") => {
if (!selectorRef.current) return
let newElement: HTMLElement | null = null
switch (direction) {
case "parent":
newElement = selectorRef.current.parentElement
break
case "child":
newElement = selectorRef.current.firstElementChild as HTMLElement
break
case "prev":
newElement = selectorRef.current.previousElementSibling as HTMLElement
break
case "next":
newElement = selectorRef.current.nextElementSibling as HTMLElement
break
}
if (newElement) {
highlightElement(newElement)
// updateSelectorPosition(newElement)
updateTooltipPosition(newElement)
}
}
const handleCancelModal = () => {
document.body.removeChild(modalRef.current)
modalRef.current = null
}
return <div style={{ display: "none" }}></div>
}
-6
View File
@@ -8,7 +8,6 @@ interface QRCodeModalProps {
onClose: () => void
}
// 新增样式
const styles = {
overlay: {
position: "fixed" as const,
@@ -70,10 +69,6 @@ const styles = {
letterSpacing: "2px",
color: "#333"
},
countdown: {
color: "#666",
fontSize: "0.9rem"
},
inputGroup: {
display: "flex",
gap: "0.5rem"
@@ -130,7 +125,6 @@ export default function QRCodeModal({ onClose, onConfirm }: QRCodeModalProps) {
return (
<div style={styles.overlay}>
<div style={styles.content}>
{/* 原有关闭按钮和二维码内容... */}
<button style={styles.closeButton} onClick={onClose} aria-label="Close">
×
</button>
+204
View File
@@ -0,0 +1,204 @@
import React, { useEffect, useRef, useState } from "react"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "@plasmohq/storage/dist/hook"
import ValidModal from "~component/ui/validModal"
import { addCss, saveHtml, saveMarkdown } from "~tools"
import { useEditMarkdown } from "~utils/editMarkdownHook"
import { useParseMarkdown } from "~utils/parseMarkdownHook"
import { Print } from "~utils/print"
import Turndown from "~utils/turndown"
import Tooltip from "../ui/tooltip"
const turndownService = Turndown()
export default function CustomDomSelector() {
const [aiType, setAiType] = useStorage("app-aiType")
const [position, setPosition] = useState({ top: 0, left: 0 })
const [isReady, setIsReady] = useState(false)
const [isSelect, setIsSelect] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const downloadType = useRef("")
const [editContent, setEditContent] = useEditMarkdown()
const [parseContent, setParseContent] = useParseMarkdown()
const articleTitle = document
.querySelector<HTMLElement>("head title")
?.innerText.trim()
useEffect(() => {
if (aiType) {
addCss(`.codebox-current { outline: 2px solid #42b88350 !important; }`)
if (isReady) {
addEventListeners()
} else {
removeEventListeners()
}
}
return () => {
removeEventListeners()
}
}, [aiType, isReady, isSelect])
const addEventListeners = () => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("click", handleClick)
}
const removeEventListeners = () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("click", handleClick)
}
const handleMouseMove = (event: MouseEvent) => {
if (isReady && !isSelect) {
const target = event.target as HTMLElement
highlightElement(target)
}
}
const handleClick = (event: MouseEvent) => {
if (isReady) {
const target = event.target as HTMLElement
const tooltip = target.closest(".codebox-tooltip")
const modal = target.closest(".codebox-modal")
const csui = target.closest("#codebox-csui")
if (!tooltip && !modal && !csui) {
setIsSelect(true)
highlightElement(target)
updateTooltipPosition(target)
event.stopPropagation()
event.preventDefault()
}
}
}
const highlightElement = (element: HTMLElement) => {
removeHighlight()
element.classList.add("codebox-current")
}
const removeHighlight = () => {
const currentList = document.querySelectorAll(".codebox-current")
currentList.forEach((el) => el.classList.remove("codebox-current"))
}
const updateTooltipPosition = (element: HTMLElement) => {
const rect = element.getBoundingClientRect()
const distanceTop = rect.top + window.scrollY
const distanceLeft = rect.left + window.scrollX
const top =
distanceTop < 50 ? distanceTop + rect.height + 5 : distanceTop - 40
const left = distanceLeft + 5
setPosition({ top: top, left: left })
window.scrollTo({ top: top - 150, behavior: "smooth" })
}
useMessage(async (req: any, res: any) => {
const name = req.name
setCustom(name.split("-")[1])
})
const setCustom = (type: string) => {
downloadType.current = type
setIsReady(true)
setIsSelect(false)
}
const handleOkModal = () => {
const selector = document.querySelector<HTMLElement>(".codebox-current")
if (!selector || !downloadType.current) return
selector.classList.remove("codebox-current")
switch (downloadType.current) {
case "downloadHtml":
saveHtml(selector, articleTitle)
break
case "downloadMarkdown":
const markdown = turndownService.turndown(selector)
saveMarkdown(markdown, articleTitle)
break
case "editMarkdown":
setEditContent(selector)
break
case "parseMarkdown":
setParseContent(selector, aiType)
break
case "downloadPdf":
Print.print(selector, { title: articleTitle })
.then(() => console.log("Printing complete"))
.catch((error) => console.error("Printing failed:", error))
break
}
resetState()
setTimeout(() => {
setIsModalOpen(false)
}, 1000)
}
const handleConfirm = () => {
setIsModalOpen(true)
}
const handleCancel = () => {
resetState()
}
const resetState = () => {
removeHighlight()
setIsSelect(false)
setIsReady(false)
}
const navigateElement = (direction: "parent" | "child" | "prev" | "next") => {
const selector = document.querySelector(".codebox-current")
if (!selector) return
let newElement: HTMLElement | null = null
switch (direction) {
case "parent":
newElement = selector.parentElement
break
case "child":
newElement = selector.firstElementChild as HTMLElement
break
case "prev":
newElement = selector.previousElementSibling as HTMLElement
break
case "next":
newElement = selector.nextElementSibling as HTMLElement
break
}
if (newElement) {
highlightElement(newElement)
updateTooltipPosition(newElement)
}
}
const handleCancelModal = () => {
setIsModalOpen(false)
}
return (
<>
{isSelect && (
<Tooltip
left={position.left}
top={position.top}
onConfirm={handleConfirm}
onCancel={handleCancel}
onNavigate={navigateElement}
/>
)}
{isModalOpen && (
<ValidModal onClose={handleCancelModal} onConfirm={handleOkModal} />
)}
</>
)
}
+17 -3
View File
@@ -10,7 +10,16 @@ import React from "react"
const tooltipStyles = {
tooltip: {
backgroundColor: "#fff"
position: "absolute" as const,
zIndex: "2147483641",
backgroundColor: "#fff",
border: "1px solid #eee",
borderRadius: "5px",
padding: "8px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
width: "450px",
display: "flex",
justifyContent: "space-between"
},
button: {
backgroundColor: "#fff",
@@ -18,7 +27,8 @@ const tooltipStyles = {
color: "#000",
borderRadius: "4px",
padding: "2px 7px",
fontSize: "14px"
fontSize: "14px",
cursor: "pointer"
},
blueButton: {
backgroundColor: "#1677FF",
@@ -51,8 +61,12 @@ export default function Tooltip(props: any) {
props.onCancel()
}
const setTooltipStyle = () => {
return { ...tooltipStyles.tooltip, left: props.left, top: props.top }
}
return (
<div style={tooltipStyles.tooltip as React.CSSProperties}>
<div style={setTooltipStyle()} className="codebox-tooltip">
<button style={tooltipStyles.blueButton} onClick={handleConfirm}>
<CheckOutlined />
</button>
+238
View File
@@ -0,0 +1,238 @@
import dayjs from "dayjs"
import qrcodeUrl from "raw:~/public/wx/qrcode_wx.jpg"
import React, { useRef, useState } from "react"
import { sendToBackground } from "@plasmohq/messaging"
import { useStorage } from "@plasmohq/storage/dist/hook"
import { verifyTOTP } from "~utils/2FA"
const styles = {
overlay: {
position: "fixed" as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000
},
container: {
width: "350px",
padding: "2rem",
position: "absolute" as const,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
backgroundColor: "white",
borderRadius: "5px",
textAlign: "center" as const,
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)"
},
closeButton: {
position: "absolute" as const,
top: "10px",
right: "10px",
background: "none",
border: "none",
fontSize: "1.5rem",
cursor: "pointer",
padding: "0.5rem"
},
activatedText: {
fontSize: "16px",
color: "red"
},
noticeText: {
fontSize: "14px",
color: "red"
},
helpLink: {
cursor: "pointer"
},
qrcodeImage: {
margin: "auto"
},
inputContainer: {
marginTop: "10px",
display: "flex",
justifyContent: "center",
alignItems: "center"
},
label: {
display: "block",
marginRight: "7px"
},
captchaSection: {
marginTop: "1.5rem",
borderTop: "1px solid #eee",
paddingTop: "1.5rem"
},
captchaRow: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem"
},
captchaText: {
fontSize: "1.2rem",
letterSpacing: "2px",
color: "#333"
},
inputGroup: {
display: "flex",
gap: "0.5rem"
},
input: {
flex: 1,
padding: "0.8rem",
border: "1px solid #ddd",
borderRadius: "4px",
fontSize: "1rem",
outline: "none",
"&:focus": {
borderColor: "#007bff"
}
},
verifyButton: {
padding: "0.8rem 1.5rem",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
"&:hover": {
backgroundColor: "#0056b3"
}
},
errorText: {
color: "#ff4444",
fontSize: "0.9rem",
marginTop: "0.5rem"
}
}
export default function ValidModal(props) {
const [validTime, setValidTime] = useStorage("app-validTime", "1730390400")
const [inputCode, setInputCode] = useState<string>("")
const [isValid, setIsValid] = useState(true)
function handleSubmit() {
if (Number(validTime) > dayjs().unix()) {
props.handleOk()
} else if (
verifyTOTP(process.env.PLASMO_PUBLIC_CODEBOX_SECRET1, inputCode)
) {
let time = dayjs().add(20, "day").unix()
setValidTime(String(time))
setIsValid(true)
props.handleOk()
} else if (
verifyTOTP(process.env.PLASMO_PUBLIC_CODEBOX_SECRET2, inputCode)
) {
let time = dayjs().add(65, "day").unix()
setValidTime(String(time))
setIsValid(true)
props.handleOk()
} else if (process.env.PLASMO_PUBLIC_CODEBOX_SECRET3 == inputCode) {
let time = dayjs().add(7, "day").unix()
setValidTime(String(time))
setIsValid(true)
props.handleOk()
} else if (
verifyTOTP(process.env.PLASMO_PUBLIC_CODEBOX_SECRET4, inputCode)
) {
setIsValid(true)
props.handleOk()
} else {
setIsValid(false)
}
}
function handleCancel() {
props.onClose()
}
function help() {
sendToBackground({
name: "tab",
body: {
url: "feed.html"
}
})
}
const typeMap = {
downloadHtml: "下载HTML",
downloadMarkdown: "下载markdown",
editMarkdown: "编辑markdown",
parseMarkdown: "解析markdown",
downloadPdf: "下载PDF"
}
return (
<div style={styles.overlay} className="codebox-modal">
<div style={styles.container}>
<button
style={styles.closeButton}
onClick={handleCancel}
aria-label="Close">
×
</button>
{Number(validTime) > dayjs().unix() ? (
<p style={styles.activatedText}>
{typeMap[props.type]}
</p>
) : (
<>
<div style={styles.noticeText}>
<p>
codebox代码助手(
<a onClick={help} target="_blank" style={styles.helpLink}>
</a>
)
</p>
</div>
<div>
<img
style={styles.qrcodeImage}
src={qrcodeUrl}
alt="微信公众号"
/>
</div>
<div style={styles.captchaSection}>
<div style={styles.inputGroup}>
<input
type="text"
value={inputCode}
onChange={(e) => {
const val = e.target.value.replace(/[^\d\s]/g, "")
setInputCode(val)
setIsValid(null) // 清除验证状态
}}
placeholder="请输入验证码"
style={{
...styles.input,
borderColor: isValid === false ? "#ff4444" : "#ddd"
}}
maxLength={11} // 6数字+5空格
/>
<button style={styles.verifyButton} onClick={handleSubmit}>
</button>
</div>
{isValid === false && (
<div style={styles.errorText}></div>
)}
</div>
</>
)}
</div>
</div>
)
}
+2 -18
View File
@@ -1,5 +1,3 @@
import { StyleProvider } from "@ant-design/cssinjs"
import antdResetCssText from "data-text:antd/dist/reset.css"
import type {
PlasmoCSConfig,
PlasmoGetShadowHostId,
@@ -11,8 +9,7 @@ import { sendToBackground } from "@plasmohq/messaging"
import { useMessage } from "@plasmohq/messaging/hook"
import { useStorage } from "@plasmohq/storage/hook"
import CustomDomSelector from "~component/customDomSelector"
import { ThemeProvider } from "~theme"
import CustomDomSelector from "~component/ui/customDomSelector"
import { setIcon } from "~tools"
import { getSummary } from "~utils/coze"
import DrawImages from "~utils/drawImages"
@@ -21,13 +18,6 @@ const HOST_ID = "codebox-csui"
export const getShadowHostId: PlasmoGetShadowHostId = () => HOST_ID
export const getStyle: PlasmoGetStyle = () => {
const style = document.createElement("style")
style.textContent = antdResetCssText
return style
}
const articleTitle = document
.querySelector<HTMLElement>("head title")
?.innerText.trim()
@@ -101,11 +91,5 @@ export default function CustomOverlay() {
}
}
return (
<ThemeProvider>
<StyleProvider container={document.getElementById(HOST_ID).shadowRoot}>
<CustomDomSelector />
</StyleProvider>
</ThemeProvider>
)
return <CustomDomSelector />
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "code-box",
"displayName": "__MSG_extensionName__",
"version": "1.0.4",
"version": "1.0.5",
"description": "__MSG_extensionDescription__",
"author": "027xiguapi. <458813868@qq.com>",
"scripts": {