This commit is contained in:
539943419 committed 2025-01-27 23:13:08 +08:00
commit 12f9726814
27 files changed
+2222 -55

No files matched your search

+3 -1
View File
@@ -25,4 +25,6 @@ logs
.pnpm-lock.yaml
userData
userData
test/*
+1 -1
View File
@@ -1 +1 @@
v20.18.1
v20.15.0
+1 -1
View File
@@ -18,7 +18,7 @@ import "/style/default.scss";
useHead({
title: '杪 Blog',
meta: [
{ name: 'description', content: '杪儿的博客' }
{ name: 'description', content: '杪儿的博客', lang: 'zh-CN' }
],
})
+158
View File
@@ -0,0 +1,158 @@
<template>
<div class="cm-rander-wrapper">
<div class="cm-header">
<div class="cm-header-title">
<span>CodeMirror</span>
</div>
<div class="cm-header-controls">
<div class="cm-switch-show-hide" @click="show = !show">
{{ show ? '收起' : '展开' }}
</div>
</div>
</div>
<miao-collapse :show="show" style="overflow: hidden;">
<div ref="cmRootRef"></div>
<el-skeleton v-if="isLoading === true" :rows="rows" style="display: flex;flex-direction: column;" />
</miao-collapse>
</div>
</template>
<script setup lang="ts">
import { EditorView, basicSetup } from "codemirror"
import { EditorState } from "@codemirror/state"
// import chevronDown from "./icons/chevronDown.vue";
// import {javascript} from "@codemirror/lang-javascript"
const props = defineProps<{
data: string,
type?: string,
}>()
const show = ref(true)
const cmRootRef = ref<HTMLElement>()
const isLoading = ref(true)
const rows = computed(() => {
if (!props.data) {
return 5
}
const lines = props.data.split('\n')
console.log({
L: lines.length,
})
return lines.length - 2 > 0 ? lines.length - 2 : 1
})
const useLangExtensions = async () => {
const ext = props.type
switch (ext) {
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
const { javascript } = await import('@codemirror/lang-javascript')
return [
javascript({
jsx: ext.substring(2, 3) === 'x',
typescript: ext.substring(0, 2) === 'js'
})
]
break
case 'css':
case 'scss':
const { sass } = await import('@codemirror/lang-sass')
return [sass()]
break
case 'html':
const { html } = await import('@codemirror/lang-html')
return [html()]
break
case 'py':
const { python } = await import('@codemirror/lang-python')
return [python()]
break
case 'go':
const { go } = await import('@codemirror/lang-go')
return [go()]
break
case 'json':
const { json } = await import('@codemirror/lang-json')
return [json()]
break
case 'md':
const { markdown } = await import('@codemirror/lang-markdown')
return [markdown()]
break
case 'yaml':
const { yaml } = await import('@codemirror/lang-yaml')
return [yaml()]
break
case 'vue':
const { vue } = await import('@codemirror/lang-vue')
return [vue()]
break
case 'xml':
const { xml } = await import('@codemirror/lang-xml')
return [xml()]
break
default:
return []
}
}
let cm: EditorView
onMounted(async () => {
const langExtensions = await useLangExtensions()
cm = new EditorView({
parent: cmRootRef.value,
doc: props.data,
extensions: [
basicSetup,
EditorState.readOnly.of(true),
...langExtensions
],
})
isLoading.value = false
})
onUpdated(() => {
cm.dispatch({
changes: {
from: 0,
to: cm.state.doc.length,
insert: props.data,
},
})
})
onBeforeUnmount(() => {
cm.destroy()
})
</script>
<style lang="scss">
.cm-rander-wrapper {
.cm-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
height: 25px;
background-color: #000;
color: #fff;
border-radius: 10px 10px 0 0;
user-select: none;
.cm-header-controls {
.cm-switch-show-hide {
cursor: pointer;
color: #fff;
fill: #fff;
}
}
}
}
</style>
+1
View File
@@ -0,0 +1 @@
<template><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><rect x="128" y="128" width="336" height="336" rx="57" ry="57" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"></rect><path d="M383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></path></svg></template>
+1
View File
@@ -0,0 +1 @@
<template><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><path d="M320 367.79h76c55 0 100-29.21 100-83.6s-53-81.47-96-83.6c-8.89-85.06-71-136.8-144-136.8c-69 0-113.44 45.79-128 91.2c-60 5.7-112 43.88-112 106.4s54 106.4 120 106.4h56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M320 255.79l-64-64l-64 64"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 448.21V207.79"></path></svg></template>
+59 -7
View File
@@ -2,12 +2,15 @@
<template>
<div class="md-render-wrapper markdown-body">
<div v-html="renderedData" :key="_key"></div>
<!-- <div v-html="renderedData" :key="_key"></div> -->
<VNodeArr></VNodeArr>
</div>
</template>
<script setup lang="ts">
import useMarkdownit from '~/hooks/useMarkdownit';
import Html2VNode from '~/utils/html2VNode';
import codeMirror from './codeMirror.vue';
const props = defineProps<{
data: string
@@ -17,17 +20,57 @@ const _key = ref(Math.random())
const markdownIt = await useMarkdownit()
const renderedData = computed(() => {
return markdownIt.render(props.data)
const VNodeArr = computed(() => {
if (!props.data) {
return () => []
}
const renderedData = markdownIt.render(props.data)
const h2v = new Html2VNode()
h2v.use(
{
filter(tagName) {
return tagName === 'code'
},
rander({
item,
// tagName,
tagAttrs,
// middlewareMap
}) {
// console.log({
// tagName,
// tagAttrs,
// })
const codeType = tagAttrs?.class?.slice(1, -1)?.split('-')?.[1] ?? ''
// console.log({
// codeType,
// })
return h(
codeMirror,
{
// @ts-ignore
data: item.children[0].children.trim(),
type: codeType,
},
)
},
}
)
const res = h2v.rander(renderedData)
return {
setup: () => {
return () => res
},
}
})
watch(() => renderedData.value, () => {
_key.value = Math.random()
})
// watch(() => renderedData.value, () => {
// _key.value = Math.random()
// })
</script>
<style>
<style lang="scss">
.md-render-wrapper {
code {
box-sizing: border-box;
@@ -39,5 +82,14 @@ watch(() => renderedData.value, () => {
padding-left: 10px;
border-radius: 5px;
}
hr {
display: block;
margin: 0;
unicode-bidi: isolate;
overflow: hidden;
border-style: inset;
border-width: 0;
}
}
</style>
+41 -15
View File
@@ -15,9 +15,7 @@ const props = defineProps({
})
const cwRef = ref<HTMLDivElement>()
const cRef = ref<HTMLDivElement>()
const cwStyle = ref<{ [key: string]: string | number }>({
maxHeight: '99999vh',
})
const cwStyle = ref<{ [key: string]: string | number }>({})
const cStyle = ref<{ [key: string]: string | number }>({})
let originHeight = -1
@@ -33,19 +31,28 @@ const clearTimer = () => {
const handleHide = () => {
clearTimer()
calcOriginHeight();
cwStyle.value = {
maxHeight: 0,
opacity: 0,
maxHeight: `${originHeight}px`,
opacity: 1,
transition: "all 0.2s ease-in-out, opacity 0.1s ease-out",
pointerEvents: 'none',
}
timer.push(setTimeout(() => {
cwStyle.value = {
maxHeight: 0,
display: 'none',
opacity: 0,
transition: "all 0.2s ease-in-out, opacity 0.1s ease-out",
pointerEvents: 'none',
}
}, 300))
timer.push(setTimeout(() => {
cwStyle.value = {
maxHeight: 0,
display: 'none',
opacity: 0,
pointerEvents: 'none',
}
}, 300))
}))
}
const handleShow = () => {
@@ -53,13 +60,31 @@ const handleShow = () => {
cwStyle.value = {
maxHeight: 0,
opacity: 0,
pointerEvents: 'none',
}
timer.push(setTimeout(() => {
calcOriginHeight();
cwStyle.value = {
maxHeight: `${originHeight}px`,
opacity: 1,
transition: "all 0.2s ease-in-out, opacity 0.3s ease-in"
maxHeight: 0,
opacity: 0,
transition: "all .2s ease-in-out, opacity .15s ease-in .07s",
pointerEvents: 'none',
}
timer.push(setTimeout(() => {
cwStyle.value = {
maxHeight: `${originHeight}px`,
opacity: 1,
transition: "all .2s ease-in-out, opacity .15s ease-in .07s"
}
timer.push(setTimeout(() => {
cwStyle.value = {
opacity: 1,
}
nextTick(() => {
calcOriginHeight()
})
}, 300))
}))
}))
}
@@ -73,11 +98,12 @@ watch(() => props.show, (newVal) => {
}
})
const calcOriginHeight = () => {
originHeight = cRef.value!.clientHeight
}
onMounted(() => {
originHeight = cwRef.value!.clientHeight
cwStyle.value = {
maxHeight: `${originHeight}px`,
}
calcOriginHeight()
})
</script>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div class="qr-scanner-wrapper">
<video ref="scannerDisplayRef"></video>
<!-- <canvas ref="testCtx" v-show="false"></canvas> -->
</div>
</template>
<script setup lang="ts">
import jsQR from 'jsqr'
const emit = defineEmits<{
onResult: [value: string]
}>()
const scannerDisplayRef = ref<HTMLVideoElement>()
// const testCtx = ref<HTMLCanvasElement>()
const result = reactive<string[]>([])
let stream: MediaStream | null = null
let timer: any | undefined = undefined
const scannerResult = new Set()
const getQRData = (videoFrame: ImageBitmap ): string | undefined => {
const canvas = document.createElement('canvas')
// const canvas = testCtx.value!
canvas.width = videoFrame.width
canvas.height = videoFrame.height
// console.log(canvas.width, canvas.height)
const ctx = canvas.getContext('2d')
ctx?.drawImage(videoFrame, 0, 0, canvas.width, canvas.height)
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height)
if (!imageData) return undefined
const code = jsQR(imageData?.data, imageData?.width, imageData?.height)
return code?.data
}
const handleVideoTrack = (track: MediaStreamTrack) => {
timer && clearInterval(timer);
let errCount = 0
// @ts-ignore
const imageCapture = new ImageCapture(track)
timer = setInterval(async () => {
try {
const videoFrame = await imageCapture.grabFrame()
const data = getQRData(videoFrame)
if (data && !scannerResult.has(data)) {
console.log(data)
scannerResult.add(data)
result.push(data)
emit('onResult', data)
}
videoFrame.close()
} catch (e) {
errCount++
if (errCount > 10) {
clearInterval(timer)
}
}
}, 500)
}
onMounted(async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
})
} catch (e) {
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
})
}
if (stream) {
scannerDisplayRef.value!.srcObject = stream
scannerDisplayRef.value!.play()
const videoTrack = stream.getVideoTracks()[0]
handleVideoTrack(videoTrack)
}
})
onBeforeUnmount(() => {
stream?.getTracks().forEach(track => track.stop())
clearInterval(timer)
})
</script>
<style scoped>
.qr-scanner-wrapper {
display: flex;
justify-content: center;
align-items: center;
video {
height: inherit;
width: inherit;
max-height: inherit;
max-width: inherit;
object-fit: contain;
}
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div>
<canvas ref="canvasRefRGB"></canvas>
</div>
</template>
<script setup lang="ts">
import QRCode from 'qrcode';
import { onMounted, ref, watchEffect } from 'vue';
const props = defineProps<{
value: string
}>()
const width = ref(600)
const canvasRefRGB = ref<HTMLCanvasElement>()
const generateQRCodeCanvasData = (data: string) => {
const canvas = document.createElement('canvas')
canvas.width = width.value
canvas.height = width.value
const ctx = canvas.getContext('2d')!
QRCode.toCanvas(canvas, [{ data: new TextEncoder().encode(data), mode: 'byte' }], {
width: width.value,
})
const imageData = ctx.getImageData(0, 0, width.value, width.value)
return imageData
}
const paddingData = (data: string) => {
const b64Data = btoa(data)
if (b64Data.length % 3 === 0) {
return b64Data + '012'
} else if (b64Data.length % 3 === 1) {
return b64Data + '01'
} else {
return b64Data + '0'
}
}
onMounted(() => {
watchEffect(() => {
const padedData = paddingData(props.value)
const data = {
r: padedData.slice(0, padedData.length / 3),
g: padedData.slice(padedData.length / 3, padedData.length / 3 * 2),
b: padedData.slice(padedData.length / 3 * 2, padedData.length)
}
// console.log({
// data,
// ECData: {
// r: new TextEncoder().encode(data.r),
// g: new TextEncoder().encode(data.g),
// b: new TextEncoder().encode(data.b)
// }
// })
const imgData = {
r: generateQRCodeCanvasData(data.r),
g: generateQRCodeCanvasData(data.g),
b: generateQRCodeCanvasData(data.b)
}
const rgbImgData = new ImageData(width.value, width.value)
for (let i = 0; i < rgbImgData.data.length; i += 4) {
rgbImgData.data[i] = imgData.r.data[i]
rgbImgData.data[i + 1] = imgData.g.data[i + 1]
rgbImgData.data[i + 2] = imgData.b.data[i + 2]
rgbImgData.data[i + 3] = 2 << 8 - 1
}
const canvas = canvasRefRGB.value!
canvas.width = width.value
canvas.height = width.value
const ctx = canvas.getContext('2d')!
ctx.putImageData(rgbImgData, 0, 0)
})
})
</script>
<style scoped></style>
+6 -2
View File
@@ -1,6 +1,8 @@
// import AutoImport from "unplugin-auto-import/vite";
// import Components from "unplugin-vue-components/vite";
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import mkcert from "vite-plugin-mkcert";
// https://nuxt.com/docs/api/configuration/nuxt-config
// @ts-ignore
@@ -16,8 +18,10 @@ export default defineNuxtConfig({
}
},
vite: {
server: {},
plugins: [],
server: {
https: true,
},
plugins: [mkcert()],
},
modules: ["@pinia/nuxt", '@element-plus/nuxt'],
compatibilityDate: "2025-01-12",
+17 -1
View File
@@ -10,29 +10,45 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/state": "^6.5.1",
"@pinia/nuxt": "^0.9.0",
"@types/node": "^22.10.5",
"bcryptjs": "^2.4.3",
"better-sqlite3": "11.3.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.13",
"jsonwebtoken": "^9.0.2",
"jsqr": "^1.4.0",
"markdown-it": "^14.1.0",
"node-fetch": "^3.3.2",
"nuxt": "^3.15.1",
"pinia": "^2.3.0",
"qrcode": "^1.5.4",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@element-plus/nuxt": "^1.1.1",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.12",
"@types/jsonwebtoken": "^9.0.7",
"@types/markdown-it": "^14.1.2",
"@types/qrcode": "^1.5.5",
"element-plus": "^2.9.3",
"sass": "^1.83.1"
"sass": "^1.83.1",
"vite-plugin-mkcert": "^1.17.6"
},
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
}
+25 -1
View File
@@ -91,8 +91,12 @@ const PageContent = reactive<PageContent[]>([
},
{
name: 'MiaoBlog',
description: '施工中,不过不要抱有太大期待',
description: '施工中,不过最好不要抱有太大期待,毕竟还是以实现功能为目标去做的',
links: [
{
name: "gitea",
url: "https://nj.miaospring.top:3001/539943419/miaoBlog.git"
},
{
name: "github",
url: "https://github.com/Zioywishing/miaoBlog"
@@ -134,6 +138,26 @@ const PageContent = reactive<PageContent[]>([
}
]
},
{
name: '喵喵图床',
description: '其实就是一个很简陋的图床,上传,下载,没了',
nuxtLinks: [
{
name: 'link',
to: `/tools/imgBed`
}
]
},
{
name: 'QRGB',
description: '把三个二维码叠在一起,真是创创又新新啊',
nuxtLinks: [
{
name: 'link',
to: `/tools/QRGB`
}
]
},
]
}, {
subTitle: '学习资料',
+15
View File
@@ -0,0 +1,15 @@
<template>
<div>
<nuxt-link to="/tools/QRGB/code">code</nuxt-link>
<nuxt-link to="/tools/QRGB/scanner">scanner</nuxt-link>
<nuxt-page></nuxt-page>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
+21
View File
@@ -0,0 +1,21 @@
<template>
<div>
<miaoQRGBCode :value="testData"/>
<div>{{ testData }}</div>
<input type="text" v-model="testData">
<!-- <miaoQRCode :value="testData"/> -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import miaoQRGBCode from '~/components/tools/miaoQRGBCode.vue';
// import miaoQRCode from '../components/miaoQRCode.vue';
const testData = ref(new Array(3).fill(0).map(_ => `${Math.random()}`).join(''))
</script>
<style scoped>
</style>
+149
View File
@@ -0,0 +1,149 @@
<template>
<div class="qr-scanner-wrapper">
<video ref="scannerDisplayRef"></video>
<canvas ref="testCtx" style="transform: scale(.6);" v-show="true"></canvas>
<canvas ref="testCtxR" style="transform: scale(.6);" v-show="true"></canvas>
<canvas ref="testCtxG" style="transform: scale(.6);" v-show="true"></canvas>
<canvas ref="testCtxB" style="transform: scale(.6);" v-show="true"></canvas>
<div>{{ res }}</div>
</div>
</template>
<script setup lang="ts">
import jsQR from 'jsqr'
import { ref, onMounted, onBeforeUnmount } from 'vue';
const emit = defineEmits<{
onResult: [value: string]
}>()
const scannerDisplayRef = ref<HTMLVideoElement>()
const testCtx = ref<HTMLCanvasElement>()
const testCtxR = ref<HTMLCanvasElement>()
const testCtxG = ref<HTMLCanvasElement>()
const testCtxB = ref<HTMLCanvasElement>()
// const result = reactive<string[]>([])
const res = ref<string>('')
let stream: MediaStream | null = null
let timer: any | undefined = undefined
const unpaddingData = (data: string) => {
if (data.endsWith('012')) {
return atob(data.slice(0, data.length - 3))
} else if (data.endsWith('01')) {
return atob(data.slice(0, data.length - 2))
} else {
return atob(data.slice(0, data.length - 1))
}
}
// const scannerResult = new Set()
const getQRDataFromVideoFrame = (videoFrame: VideoFrame): string | undefined => {
const canvas = testCtx.value!
canvas.width = videoFrame.codedWidth
canvas.height = videoFrame.codedHeight
// if (!videoFrame.codedWidth) return undefined
const ctx = canvas.getContext('2d')
ctx?.drawImage(videoFrame, 0, 0, canvas.width, canvas.height)
videoFrame.close()
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height)
if (!imageData) return undefined
const imageDataRGB = {
r: new ImageData(canvas.width, canvas.height),
g: new ImageData(canvas.width, canvas.height),
b: new ImageData(canvas.width, canvas.height),
}
for (let i = 0; i < imageData.data.length; i += 4) {
imageDataRGB.r.data[i] = imageData.data[i]
imageDataRGB.r.data[i + 1] = imageData.data[i]
imageDataRGB.r.data[i + 2] = imageData.data[i]
imageDataRGB.r.data[i + 3] = imageData.data[i + 3]
imageDataRGB.g.data[i] = imageData.data[i + 1]
imageDataRGB.g.data[i + 1] = imageData.data[i + 1]
imageDataRGB.g.data[i + 2] = imageData.data[i + 1]
imageDataRGB.g.data[i + 3] = imageData.data[i + 3]
imageDataRGB.b.data[i] = imageData.data[i + 2]
imageDataRGB.b.data[i + 1] = imageData.data[i + 2]
imageDataRGB.b.data[i + 2] = imageData.data[i + 2]
imageDataRGB.b.data[i + 3] = imageData.data[i + 3]
}
testCtxR.value!.width = canvas.width
testCtxR.value!.height = canvas.height
testCtxG.value!.width = canvas.width
testCtxG.value!.height = canvas.height
testCtxB.value!.width = canvas.width
testCtxB.value!.height = canvas.height
testCtxR.value!.getContext('2d')?.putImageData(imageDataRGB.r, 0, 0)
testCtxG.value!.getContext('2d')?.putImageData(imageDataRGB.g, 0, 0)
testCtxB.value!.getContext('2d')?.putImageData(imageDataRGB.b, 0, 0)
// const code = jsQR(imageData?.data, imageData?.width, imageData?.height)
const codeR = jsQR(imageDataRGB.r.data, imageDataRGB.r.width, imageDataRGB.r.height)
const codeG = jsQR(imageDataRGB.g.data, imageDataRGB.g.width, imageDataRGB.g.height)
const codeB = jsQR(imageDataRGB.b.data, imageDataRGB.b.width, imageDataRGB.b.height)
if(!codeR || !codeG || !codeB) return undefined
const data = unpaddingData(codeR?.data + codeG?.data + codeB?.data)
return data
}
const handleTrackProcessor = async (track: MediaStreamTrack) => {
// @ts-ignore
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frameFromCamera = result.value as VideoFrame;
const data = getQRDataFromVideoFrame(frameFromCamera)
if (data) {
res.value = data
console.log(data)
emit('onResult', data)
}
}
}
onMounted(async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
})
} catch (e) {
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
})
}
if (stream) {
scannerDisplayRef.value!.srcObject = stream
scannerDisplayRef.value!.play()
const videoTrack = stream.getVideoTracks()[0]
// handleVideoTrack(videoTrack)
handleTrackProcessor(videoTrack)
}
})
onBeforeUnmount(() => {
stream?.getTracks().forEach(track => track.stop())
clearInterval(timer)
})
</script>
<style scoped>
.qr-scanner-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
video {
height: inherit;
width: inherit;
max-height: inherit;
max-width: inherit;
object-fit: contain;
}
}
</style>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="qr-scanner-wrapper">
<miaoQRScanner></miaoQRScanner>
</div>
</template>
<script setup lang="ts">
import miaoQRScanner from '~/components/miaoQRScanner.vue';
</script>
<style scoped>
.qr-scanner-wrapper {
width: 100%;
height: calc(100vh - 60px);
display: flex;
justify-content: center;
align-items: center;
}
</style>
+96 -15
View File
@@ -4,11 +4,17 @@
<el-divider></el-divider>
<main class="fuck-sq-main">
<el-row style="width: 100%;" :gutter="20">
<el-col :span="24">
<el-col :span="16">
<el-input v-model="url">
<template #prepend>链接</template>
</el-input>
</el-col>
<el-col :span="4">
<el-button type="primary" style="width: 100%" @click="handleSelectImage">图片</el-button>
</el-col>
<el-col :span="4">
<el-button type="primary" style="width: 100%" @click="handleOpenScanner">扫码</el-button>
</el-col>
</el-row>
<el-row style="width: 100%;" :gutter="20">
<el-col :span="13" class="col-flex-end">
@@ -16,11 +22,12 @@
<el-input-number style="width: 130px;" v-model="likes" :min="1" :max="300" />
</el-col>
<el-col :span="4" class="col-flex-end">
<el-button type="primary" style="width: 100%"
:disabled="status === 'busy' || id === undefined" @click="handleStart">开始</el-button>
<el-button type="primary" style="width: 100%" :disabled="status === 'busy' || id === undefined"
@click="handleStart">开始</el-button>
</el-col>
<el-col :span="7" class="col-flex-end">
<el-button type="primary" style="width: 100%" :disabled="messages.length === 0" @click="handleClear">清空输出</el-button>
<el-button type="primary" style="width: 100%" :disabled="messages.length === 0"
@click="handleClear">清空输出</el-button>
</el-col>
</el-row>
<div class="message-container-wrapper">
@@ -35,15 +42,25 @@
</el-scrollbar>
</div>
</main>
<div class="qr-scanner" v-if="displayQRScanner" @click="displayQRScanner = false">
<miaoQRScanner class="qr-scanner-scanner" @on-result="handleScannerRes" @click.stop></miaoQRScanner>
</div>
</div>
</template>
<script setup lang="ts">
import jsQR from 'jsqr'
// const router = useRouter()
const testCRef = ref<HTMLCanvasElement>()
const url = ref('')
const likes = ref(10)
const status = ref<'waiting' | 'busy'>('waiting')
const displayQRScanner = ref(false)
const messages = reactive<{
timestamp: number,
message: string,
@@ -58,10 +75,14 @@ const log = (...message: string[]) => {
})
}
const id = computed(() => {
const idMatch0 = url.value.match(/id%3D(\d+)/);
const idMatch1 = url.value.match(/id=(\d+)/);
const matchId = (url: string) => {
const idMatch0 = url.match(/id%3D(\d+)/);
const idMatch1 = url.match(/id=(\d+)/);
return idMatch0 ? idMatch0[1] : idMatch1 ? idMatch1[1] : undefined
}
const id = computed(() => {
return matchId(url.value)
})
const _fd = (
@@ -77,12 +98,12 @@ const handleStart = () => {
};
sse.onmessage = function (event) {
const data = JSON.parse(event.data.split('data: ')[1])
if(data.likeCountBefore || data.likeCountAfter) {
if (data.likeCountBefore || data.likeCountAfter) {
log(`当前赞数: ${data?.likeCountBefore ?? data?.likeCountAfter}`)
} else if(data.process) {
} else if (data.process) {
log(`已点赞 ${data.process} 次,已完成 ${data.percent}%`)
}
if(data.likeCountAfter) {
if (data.likeCountAfter) {
sse.close()
log('任务完成')
status.value = 'waiting'
@@ -101,6 +122,46 @@ const handleClear = () => {
messages.splice(0, messages.length)
}
const handleOpenScanner = () => {
// router.push('/tools/QRScanner')
displayQRScanner.value = true
}
const handleScannerRes = (res: string) => {
const id = matchId(res)
if (id) {
url.value = res
}
displayQRScanner.value = false
}
const handleSelectImage = async () => {
const fper = new FilePicker()
const files = await fper.pick({
accept: 'image/*',
multiple: false
})
const file = files[0]
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
const img = await createImageBitmap(file)
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, img.width, img.height)
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height)
if (!imageData) return
const code = jsQR(imageData?.data, imageData?.width, imageData?.height)
if(code?.data) {
const _url = code.data
const id = matchId(_url)
if(id) {
url.value = _url
} else {
log('图片未识别到id')
}
}
}
watchEffect(() => {
if (id.value) {
log(`识别到id: ${id.value}`)
@@ -111,7 +172,7 @@ watchEffect(() => {
</script>
<style scoped>
<style lang="scss" scoped>
.fuck-sq-wrapper {
width: 95%;
height: 100%;
@@ -150,16 +211,36 @@ watchEffect(() => {
.message-container {
width: 100%;
max-width: 800px;
height: calc( 100vh - 350px );
max-height: calc( 100vh - 350px );
height: calc(100vh - 350px);
max-height: calc(100vh - 350px);
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 10px 0 ;
padding: 10px 0;
}
.message-container-message {
.message-container-message {
width: 95%;
margin: 0 10px;
}
.qr-scanner {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
&-scanner {
max-width: 80vw;
max-height: 80vh;
// height: 80%;
object-fit: contain;
}
}
</style>
+196
View File
@@ -0,0 +1,196 @@
<template>
<div class="imgBed-wrapper">
<div class="imgBed-wrapper-upload">
<el-upload class="imgBed-wrapper-upload-el" :auto-upload="false" v-model="fileList"
action="/api/tools/imgBed/upload" ref="uploadRef" :headers="headers" drag :data="data"
@success="handleSuccess">
<template #trigger>
<div class="imgBed-upload">
<upload class="imgBed-upload-icon"></upload>
<el-text style="margin: 0;">拖拽 图片 至此<br />或单击此处选择文件</el-text>
</div>
</template>
</el-upload>
<el-row>
<el-col :span="17">
<el-input v-model="expire" placeholder="有效期(小时)" style="width: 100%;">
<template #prepend>过期时间(小时)</template>
</el-input>
</el-col>
<el-col :span="7" class="flex-end">
<el-button type="primary" @click="handleUpload">
点击上传图片
</el-button>
</el-col>
</el-row>
<el-table :data="tableData" style="width: 100%">
<el-table-column show-overflow-tooltip prop="name" label="文件名" />
<el-table-column prop="expire" label="过期时间" width="180" />
<el-table-column fixed="right" prop="share" label="操作" width="180">
<template #default="scoped">
<el-button link type="primary" size="small" @click="handleDownload(scoped.row)">
下载
</el-button>
<el-button link type="primary" size="small" @click="handleShare(scoped.row)">分享</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="showShareDialog" title="分享链接">
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
<img :src="shareDialogData.img" alt="shareQRCodeBase64" />
<div style="max-width: 80%;">
<span>{{ shareDialogData.url }}</span>
<span>
<copy style="width: 15px; margin-left: 4px; transform: translateY(3px); cursor: pointer;" @click="handleCopyUrl(shareDialogData.url)"></copy>
</span>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import type { UploadInstance } from 'element-plus';
import upload from '~/components/icons/upload.vue';
import copy from '~/components/icons/copy.vue';
// @ts-ignore
import QRCode from 'qrcode'
const fileList = ref([])
const headers = reactive({
Authorization: ``
})
const uploadRef = ref<UploadInstance>()
const tableData = reactive<{
name: string;
expire: string;
hash: string;
}[]>([])
const showShareDialog = ref(false)
const shareDialogData = ref({
img: '',
url: '',
})
const expire = ref(1)
const data = computed(() => {
return {
expire: expire.value * 3600 * 1000
}
})
const handleUpload = () => {
uploadRef.value!.submit()
}
const handleSuccess = (res: {
code: number;
fileName: string;
expire: number;
hash: string;
fileSize: number;
}) => {
if (res.code === 200) {
const data = {
name: res.fileName,
expire: formatDate(new Date(Number(res.expire)), 'yyyy-MM-dd HH:mm:ss'),
expireDate: Number(res.expire),
hash: res.hash,
}
tableData.push(data)
}
}
const handleDownload = (row: {
name: string;
hash: string;
}) => {
const url = `/api/tools/imgBed/download/${row.hash}`
const a = document.createElement('a')
a.download = row.name
a.href = url
a.click()
}
const handleShare = async (row: {
name: string;
hash: string;
}) => {
const url = `${location.origin}/api/tools/imgBed/download/${row.hash}`
const data = await QRCode.toDataURL(url)
shareDialogData.value = {
img: data,
url: url,
}
showShareDialog.value = true
}
const handleCopyUrl = async (url: string) => {
await navigator.clipboard.writeText(url)
ElMessage.success('复制成功')
}
onMounted(() => {
const record = localStorage.getItem('miaoTool-imgBed-uploadRecord')
if (record) {
let _record = JSON.parse(record)
// console.log({ _record, record })
_record = _record.filter((item: { expireDate: number }) => {
return item.expireDate && item.expireDate > Date.now()
})
tableData.push(..._record)
}
watchEffect(() => {
localStorage.setItem('miaoTool-imgBed-uploadRecord', JSON.stringify(tableData))
})
})
</script>
<style lang="scss" scoped>
.imgBed-wrapper {
// box-sizing: border-box;
position: relative;
margin: 0 20px;
padding: 20px 0 0 0;
width: 100%;
// height: calc(100vh - 60px);
// max-height: calc(100vh - 60px);
max-width: 800px;
display: flex;
flex-direction: column;
align-items: center;
&-upload {
width: 95%;
&-el {
margin-bottom: 30px;
}
}
.imgBed-upload {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 5px;
&-icon {
display: block;
width: 45px;
color: #595959;
}
}
}
.flex-end {
display: flex;
justify-content: flex-end;
}
</style>
+760
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1,5 +1,4 @@
// @ts-ignore
import testData from "~/public/testData/test.md"
import DB from 'better-sqlite3';
import { postItem } from "~/types/post";
@@ -0,0 +1,39 @@
// @ts-ignore
import DB from 'better-sqlite3';
import { defineEventHandler, getRouterParam } from 'h3';
import fs from 'fs';
import { sendStream } from 'h3';
export default defineEventHandler(async (event) => {
try {
const hash = getRouterParam(event, 'hash');
if (!hash) {
return { code: 400, msg: 'Invalid hash parameter' };
}
const db = new DB('./userData/db/imgBed.db');
// 从数据库中查询文件信息
const fileInfo = db.prepare(`
SELECT name, expire FROM imgBed WHERE hash =?
`).get(hash) as { name: string, expire: number };
db.close();
if (!fileInfo) {
return { code: 404, msg: 'File not found' };
}
const fileName = fileInfo.name;
const filePath = `./userData/imgBed/${hash}`;
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return { code: 404, msg: 'File not found on disk' };
}
const fileStream = fs.createReadStream(filePath);
setResponseHeader(event, 'Content-Disposition', `attachment; filename="${fileName}"`);
// 发送文件流
return sendStream(event, fileStream);
} catch (error) {
console.error(error);
return { code: 500, msg: 'Internal Server Error' };
}
});
+39
View File
@@ -0,0 +1,39 @@
// @ts-ignore
import DB from 'better-sqlite3';
import { defineEventHandler, readBody } from 'h3';
import crypto from 'crypto';
import fs from 'fs';
export default defineEventHandler(async (event) => {
try {
const data = await readFormData(event)
const file = data.get('file') as File
const expire = data.get('expire') as string
console.log({
file: file.name,
expire,
})
const fileBuffer = Buffer.from(await file.arrayBuffer());
// 计算文件的 MD5 哈希值
const hash = crypto.createHash('md5').update(fileBuffer).digest('hex');
const fileName = file.name;
const filePath = `./userData/imgBed/${hash}`;
// 将文件写入磁盘
await fs.promises.writeFile(filePath, fileBuffer);
// 获取文件大小
const fileSize = fileBuffer.length;
const db = new DB('./userData/db/imgBed.db');
// 插入数据到数据库
db.prepare(`
INSERT INTO imgBed (name, hash, size, expire)
VALUES (?,?,?,?)
`).run([fileName, hash, fileSize, expire]);
db.close();
return { code: 200, hash, fileName, fileSize, expire: Number(expire) + Date.now() };
} catch (error) {
console.error(error);
return { code: 500, msg: 'Internal Server Error' };
}
});
-3
View File
@@ -7,12 +7,10 @@ const pathNeedAuth = serverConfig.auth.api
export default defineEventHandler(async (event) => {
const path = getRequestURL(event).pathname
// console.log(path)
if (pathNeedAuth.has(path)) {
const key = getKey()
// @ts-ignore
const authHeader = event.headers.get('authorization');
// console.log(authHeader)
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
@@ -20,7 +18,6 @@ export default defineEventHandler(async (event) => {
try {
const decoded = jwt.verify(token, key) as { userId: number, password: string, username: string, exp: number }
if (decoded.exp > Date.now()) {
// console.log('token expired')
throw createError({ statusCode: 401, statusMessage: 'Token invalid or expired', data: { err: 'token expired', token } })
}
// console.log(decoded)
+63 -7
View File
@@ -1,5 +1,6 @@
import mkfileIfNotExist from "./mkfileIfNotExist"
import db from 'better-sqlite3'
import DB from 'better-sqlite3'
import fs from 'fs'
let checked = false
@@ -14,6 +15,7 @@ function checkUserData() {
const dirList = [
'./userData/',
'./userData/db/',
'./userData/imgBed/',
]
dirList.forEach(mkdirIfNotExist)
@@ -24,9 +26,14 @@ function checkUserData() {
}
function createDB() {
const userDB = new db('./userData/db/user.db');
const postsDB = new db('./userData/db/posts.db');
createUserDB();
createPostsDB();
createImgBedDB();
}
const createUserDB = () => {
const userDB = new DB('./userData/db/user.db');
userDB.exec(`
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -36,7 +43,11 @@ function createDB() {
lastLoginTime INTEGER NOT NULL
);
`);
userDB.close();
}
const createPostsDB = () => {
const postsDB = new DB('./userData/db/posts.db');
postsDB.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -49,14 +60,12 @@ function createDB() {
FOREIGN KEY(tag_id) REFERENCES tags(id)
);
`);
postsDB.exec(`
CREATE TABLE IF NOT EXISTS postContent (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL
);
`);
postsDB.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -77,9 +86,56 @@ function createDB() {
tag15 TEXT
);
`);
postsDB.close();
userDB.close();
}
const createImgBedDB = () => {
const imgBedDB = new DB('./userData/db/imgBed.db');
imgBedDB.exec(`
CREATE TABLE IF NOT EXISTS imgBed (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hash TEXT NOT NULL,
size INTEGER NOT NULL,
expire INTEGER NOT NULL
);
`);
imgBedDB.close();
setInterval(cleanExpiredFiles_imgBed, 1000 * 30 * 60 );
// cleanExpiredFiles_imgBed()
}
const cleanExpiredFiles_imgBed = async () => {
try {
const db = new DB('./userData/db/imgBed.db');
const currentTime = Date.now();
// 从数据库中查询过期文件的哈希
const expiredFiles = db.prepare(`
SELECT hash FROM imgBed WHERE expire <?
`).all(currentTime) as {
hash: string;
}[];
db.close();
for (const file of expiredFiles) {
const hash = file.hash;
const filePath = `./userData/imgBed/${hash}`;
// 检查文件是否存在
if (fs.existsSync(filePath)) {
// 删除文件
fs.unlinkSync(filePath);
}
}
// 从数据库中删除过期文件的记录
const db2 = new DB('./userData/db/imgBed.db');
db2.prepare(`
DELETE FROM imgBed WHERE expire <?
`).run(currentTime);
db2.close();
} catch (error) {
console.error('Error cleaning expired files:', error);
}
};
export default checkUserData
+105
View File
@@ -0,0 +1,105 @@
export class FilePicker {
private _resolve: ((files: File[]) => void) | undefined
constructor() {
if (this._resolve) {
this._resolve([])
this._resolve = undefined
}
}
public pick(option?: { accept?: string; multiple?: boolean }) {
if (this._resolve) {
this._resolve([])
this._resolve = undefined
}
return new Promise<File[]>((resolve) => {
const inpEle = document.createElement('input')
inpEle.id = `__file_${Math.ceil(Math.random() * 100000000)}`
inpEle.type = 'file'
inpEle.style.display = 'none'
this._resolve = resolve
// 文件类型限制
// inpEle.accept="image/*"
option?.accept && (inpEle.accept = option?.accept)
// 多选限制
option?.multiple && (inpEle.multiple = option?.multiple)
// inpEle.addEventListener("change", event => fn.call(inpEle, event, inpEle.files), { once: true });
inpEle.addEventListener(
'change',
() => {
if (!inpEle.files) {
return
}
const files: File[] = []
let i = 0
while (i < inpEle.files?.length) {
const _file = inpEle?.files?.item(i)
if (_file!== null) {
files.push(_file)
}
i += 1
}
resolve(files)
this._resolve = undefined
},
{ once: true }
)
inpEle.addEventListener('abort', () => {
this._resolve && this._resolve([])
this._resolve = undefined
})
inpEle.addEventListener('error', () => {
this._resolve && this._resolve([])
this._resolve = undefined
})
inpEle.click()
})
}
}
// (() => {
// let _resolve: ((files: File[]) => void) | undefined
// return (option?: { accept?: 'string'; multiple: boolean }) => {
// if (_resolve) {
// _resolve([])
// _resolve = undefined
// }
// return new Promise<File[]>((resolve) => {
// const inpEle = document.createElement('input')
// inpEle.id = `__file_${Math.ceil(Math.random() * 100000000)}`
// inpEle.type = 'file'
// inpEle.style.display = 'none'
// _resolve = resolve
// // 文件类型限制
// // inpEle.accept="image/*"
// option?.accept && (inpEle.accept = option?.accept)
// // 多选限制
// option?.multiple && (inpEle.multiple = option?.multiple)
// // inpEle.addEventListener("change", event => fn.call(inpEle, event, inpEle.files), { once: true });
// inpEle.addEventListener(
// 'change',
// () => {
// if (!inpEle.files) {
// return
// }
// const files: File[] = []
// let i = 0
// while (i < inpEle.files?.length) {
// const _file = inpEle?.files?.item(i)
// if (_file !== null) {
// files.push(_file)
// }
// i += 1
// }
// resolve(files)
// _resolve = undefined
// },
// { once: true }
// )
// inpEle.click()
// })
// }
// })()
+224
View File
@@ -0,0 +1,224 @@
import type { VNode } from "vue";
// 未完成
export default class Html2VNode {
private middlewareList: middlewareType[] = []
constructor() {
this.use(this.defaultMiddleware)
}
public use(middleware: middlewareType) {
this.middlewareList.push(middleware)
}
public rander(htmlString: string) {
const html2ObjResult = this.html2Obj(htmlString);
const middlewareMap = this.buildMiddlewareMap(html2ObjResult);
const wrinklesResult = this.wrinkles(html2ObjResult)
const vnodes = this.rander2VNode(wrinklesResult, middlewareMap)
return vnodes;
}
private buildMiddlewareMap(objs: (TagType | miaoVNodeType)[]) {
const tagNameSet = new Set<string>()
const middlewareMap = new Map<string, middlewareType[]>()
for (const item of objs) {
const { tagName = '' } = this.parseTag(item.tag);
tagNameSet.add(tagName)
}
for (const tagName of tagNameSet) {
for (const middleware of this.middlewareList) {
if (middleware.filter(tagName)) {
if(!middlewareMap.has(tagName)) {
middlewareMap.set(tagName, [middleware])
} else {
middlewareMap.get(tagName)!.unshift(middleware)
}
}
}
}
return middlewareMap;
}
private rander2VNode = (
wrinklesResult: (miaoVNodeType | string)[],
middlewareMap: Map<string, middlewareType[]>
): (VNode | string)[] => {
return wrinklesResult.map((item: miaoVNodeType | string): VNode | string => {
if (typeof item === 'string') {
return item;
}
const { tagName = '', tagAttrs } = this.parseTag(item.tag);
const middlewares = middlewareMap.get(tagName)
for(const middleware of middlewares!) {
const _res = middleware.rander({
item,
tagName,
tagAttrs,
middlewareMap
})
if(_res) {
return _res
}
}
return ''
})
}
private defaultMiddleware: middlewareType = {
filter: (_: string) => {
return true;
},
rander: ({
item, tagName, tagAttrs, middlewareMap
}) => {
if (!tagName) {
return item.children as string;
}
let child: string | (string | VNode)[] = item.children as string
if (typeof item.children !== 'string') {
child = this.rander2VNode(item.children, middlewareMap)
}
return h(tagName ?? 'span', tagAttrs, child)
}
}
private html2Obj = (htmlString: string) => {
const html2ObjResult: (TagType | miaoVNodeType)[] = []
let currhtml = htmlString;
while (true) {
const nextTag = this.getNextHtmlTag(currhtml);
if (!nextTag) {
if(currhtml) {
html2ObjResult.push({
tag: '',
children: currhtml,
})
}
break;
}
const middleChild = currhtml.slice(0, nextTag.Index);
if (middleChild) {
html2ObjResult.push({
tag: '',
children: middleChild,
})
}
currhtml = currhtml.slice(nextTag.Index + nextTag.tag.length);
html2ObjResult.push(nextTag)
}
html2ObjResult.forEach(item => {
// @ts-ignore
if(item.children &&typeof item.children==='string') {
// @ts-ignore
item.children = this.parseString(item.children)
}
})
return html2ObjResult;
}
private parseString = (target: string) => {
return target.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', '\'')
}
private check = (html2ObjResult: (TagType | miaoVNodeType)[]) => {
const stack = []
for (const item of html2ObjResult) {
if (!item.tag) {
continue;
}
// @ts-ignore
if (item?.isClose && item.isClose == true) {
const last = stack.pop();
if (last !== this.parseTag(item.tag).tagName) {
return false;
}
} else {
stack.push(this.parseTag(item.tag).tagName)
}
}
return true;
}
// 想必是flat的反义词吧
private wrinkles = (target: (TagType | miaoVNodeType)[]) => {
const res: miaoVNodeType[] = []
const resStack = [res]
for (const item of target) {
if (!item.tag) {
// @ts-ignore
resStack[resStack.length - 1].push(item)
// @ts-ignore
} else if (item.isClose) {
resStack.pop()
} else {
const _childRes: any = []
resStack[resStack.length - 1].push({
tag: item.tag,
// tag: this.getTagName(item.tag),
children: _childRes,
})
resStack.push(_childRes)
}
}
return res;
}
private getNextHtmlTag = (htmlString: string): {
tag: string,
Index: number,
isClose: boolean
} | null => {
let startIndex = 0;
let tagRegex = /<("[^"]*"|'[^']*'|[^'">])*>/;
let tagMatch = tagRegex.exec(htmlString.slice(startIndex));
if (tagMatch) {
const isClose = tagMatch[0].startsWith('</');
return {
tag: tagMatch[0],
Index: tagMatch.index,
isClose
};
}
return null;
}
private parseTag = (tag: string) => {
const tagStr = tag.replace(/<\s*\/*\s*|\s*>/g, '');
const tagArr = tagStr.split(' ').filter(item => item.trim());
const tagName = tagArr.shift();
const tagAttrs = tagArr.map(v => v.trim().split('=')).reduce((pre, cur) => {
pre[cur[0]] = cur[1];
return pre;
}, {} as { [key: string]: string })
return { tagName, tagAttrs };
}
}
type miaoVNodeType = {
tag: string,
children: string | miaoVNodeType[],
}
type TagType = {
tag: string;
Index: number;
isClose: boolean;
}
// 要优化也是以后优化了
export type middlewareType = {
filter: (tagName: string) => boolean,
rander: (option: {
item: miaoVNodeType,
tagName: string,
tagAttrs: { [key: string]: string },
middlewareMap: Map<string, middlewareType[]>
}
) => VNode | string
}