Merge branch 'main' of https://nj.miaospring.top:3001/539943419/miaoBlog
This commit is contained in:
commit
12f9726814
27 files changed
+2222
-55
No files matched your search
+3
-1
@@ -25,4 +25,6 @@ logs
|
|||||||
|
|
||||||
.pnpm-lock.yaml
|
.pnpm-lock.yaml
|
||||||
|
|
||||||
userData
|
userData
|
||||||
|
|
||||||
|
test/*
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
v20.18.1
|
v20.15.0
|
||||||
@@ -18,7 +18,7 @@ import "/style/default.scss";
|
|||||||
useHead({
|
useHead({
|
||||||
title: '杪 Blog',
|
title: '杪 Blog',
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: '杪儿的博客' }
|
{ name: 'description', content: '杪儿的博客', lang: 'zh-CN' }
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="md-render-wrapper markdown-body">
|
<div class="md-render-wrapper markdown-body">
|
||||||
<div v-html="renderedData" :key="_key"></div>
|
<!-- <div v-html="renderedData" :key="_key"></div> -->
|
||||||
|
<VNodeArr></VNodeArr>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useMarkdownit from '~/hooks/useMarkdownit';
|
import useMarkdownit from '~/hooks/useMarkdownit';
|
||||||
|
import Html2VNode from '~/utils/html2VNode';
|
||||||
|
import codeMirror from './codeMirror.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: string
|
data: string
|
||||||
@@ -17,17 +20,57 @@ const _key = ref(Math.random())
|
|||||||
|
|
||||||
const markdownIt = await useMarkdownit()
|
const markdownIt = await useMarkdownit()
|
||||||
|
|
||||||
const renderedData = computed(() => {
|
const VNodeArr = computed(() => {
|
||||||
return markdownIt.render(props.data)
|
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, () => {
|
// watch(() => renderedData.value, () => {
|
||||||
_key.value = Math.random()
|
// _key.value = Math.random()
|
||||||
})
|
// })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.md-render-wrapper {
|
.md-render-wrapper {
|
||||||
code {
|
code {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -39,5 +82,14 @@ watch(() => renderedData.value, () => {
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
overflow: hidden;
|
||||||
|
border-style: inset;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+41
-15
@@ -15,9 +15,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
const cwRef = ref<HTMLDivElement>()
|
const cwRef = ref<HTMLDivElement>()
|
||||||
const cRef = ref<HTMLDivElement>()
|
const cRef = ref<HTMLDivElement>()
|
||||||
const cwStyle = ref<{ [key: string]: string | number }>({
|
const cwStyle = ref<{ [key: string]: string | number }>({})
|
||||||
maxHeight: '99999vh',
|
|
||||||
})
|
|
||||||
const cStyle = ref<{ [key: string]: string | number }>({})
|
const cStyle = ref<{ [key: string]: string | number }>({})
|
||||||
|
|
||||||
let originHeight = -1
|
let originHeight = -1
|
||||||
@@ -33,19 +31,28 @@ const clearTimer = () => {
|
|||||||
|
|
||||||
const handleHide = () => {
|
const handleHide = () => {
|
||||||
clearTimer()
|
clearTimer()
|
||||||
|
calcOriginHeight();
|
||||||
cwStyle.value = {
|
cwStyle.value = {
|
||||||
maxHeight: 0,
|
maxHeight: `${originHeight}px`,
|
||||||
opacity: 0,
|
opacity: 1,
|
||||||
transition: "all 0.2s ease-in-out, opacity 0.1s ease-out",
|
transition: "all 0.2s ease-in-out, opacity 0.1s ease-out",
|
||||||
pointerEvents: 'none',
|
|
||||||
}
|
}
|
||||||
timer.push(setTimeout(() => {
|
timer.push(setTimeout(() => {
|
||||||
cwStyle.value = {
|
cwStyle.value = {
|
||||||
maxHeight: 0,
|
maxHeight: 0,
|
||||||
display: 'none',
|
|
||||||
opacity: 0,
|
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 = () => {
|
const handleShow = () => {
|
||||||
@@ -53,13 +60,31 @@ const handleShow = () => {
|
|||||||
cwStyle.value = {
|
cwStyle.value = {
|
||||||
maxHeight: 0,
|
maxHeight: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
}
|
}
|
||||||
timer.push(setTimeout(() => {
|
timer.push(setTimeout(() => {
|
||||||
|
calcOriginHeight();
|
||||||
cwStyle.value = {
|
cwStyle.value = {
|
||||||
maxHeight: `${originHeight}px`,
|
maxHeight: 0,
|
||||||
opacity: 1,
|
opacity: 0,
|
||||||
transition: "all 0.2s ease-in-out, opacity 0.3s ease-in"
|
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(() => {
|
onMounted(() => {
|
||||||
originHeight = cwRef.value!.clientHeight
|
calcOriginHeight()
|
||||||
cwStyle.value = {
|
|
||||||
maxHeight: `${originHeight}px`,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -1,6 +1,8 @@
|
|||||||
// import AutoImport from "unplugin-auto-import/vite";
|
// import AutoImport from "unplugin-auto-import/vite";
|
||||||
// import Components from "unplugin-vue-components/vite";
|
// import Components from "unplugin-vue-components/vite";
|
||||||
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import mkcert from "vite-plugin-mkcert";
|
||||||
|
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -16,8 +18,10 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
server: {},
|
server: {
|
||||||
plugins: [],
|
https: true,
|
||||||
|
},
|
||||||
|
plugins: [mkcert()],
|
||||||
},
|
},
|
||||||
modules: ["@pinia/nuxt", '@element-plus/nuxt'],
|
modules: ["@pinia/nuxt", '@element-plus/nuxt'],
|
||||||
compatibilityDate: "2025-01-12",
|
compatibilityDate: "2025-01-12",
|
||||||
|
|||||||
+17
-1
@@ -10,29 +10,45 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
"@codemirror/state": "^6.5.1",
|
||||||
"@pinia/nuxt": "^0.9.0",
|
"@pinia/nuxt": "^0.9.0",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "11.3.0",
|
"better-sqlite3": "11.3.0",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"nuxt": "^3.15.1",
|
"nuxt": "^3.15.1",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^2.3.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"unplugin-auto-import": "^19.0.0",
|
"unplugin-auto-import": "^19.0.0",
|
||||||
"unplugin-vue-components": "^28.0.0",
|
"unplugin-vue-components": "^28.0.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@element-plus/nuxt": "^1.1.1",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"element-plus": "^2.9.3",
|
"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"
|
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
|
||||||
}
|
}
|
||||||
+25
-1
@@ -91,8 +91,12 @@ const PageContent = reactive<PageContent[]>([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MiaoBlog',
|
name: 'MiaoBlog',
|
||||||
description: '施工中,不过不要抱有太大期待',
|
description: '施工中,不过最好不要抱有太大期待,毕竟还是以实现功能为目标去做的',
|
||||||
links: [
|
links: [
|
||||||
|
{
|
||||||
|
name: "gitea",
|
||||||
|
url: "https://nj.miaospring.top:3001/539943419/miaoBlog.git"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "github",
|
name: "github",
|
||||||
url: "https://github.com/Zioywishing/miaoBlog"
|
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: '学习资料',
|
subTitle: '学习资料',
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -4,11 +4,17 @@
|
|||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<main class="fuck-sq-main">
|
<main class="fuck-sq-main">
|
||||||
<el-row style="width: 100%;" :gutter="20">
|
<el-row style="width: 100%;" :gutter="20">
|
||||||
<el-col :span="24">
|
<el-col :span="16">
|
||||||
<el-input v-model="url">
|
<el-input v-model="url">
|
||||||
<template #prepend>链接</template>
|
<template #prepend>链接</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-col>
|
</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>
|
||||||
<el-row style="width: 100%;" :gutter="20">
|
<el-row style="width: 100%;" :gutter="20">
|
||||||
<el-col :span="13" class="col-flex-end">
|
<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-input-number style="width: 130px;" v-model="likes" :min="1" :max="300" />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="4" class="col-flex-end">
|
<el-col :span="4" class="col-flex-end">
|
||||||
<el-button type="primary" style="width: 100%"
|
<el-button type="primary" style="width: 100%" :disabled="status === 'busy' || id === undefined"
|
||||||
:disabled="status === 'busy' || id === undefined" @click="handleStart">开始</el-button>
|
@click="handleStart">开始</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="7" class="col-flex-end">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="message-container-wrapper">
|
<div class="message-container-wrapper">
|
||||||
@@ -35,15 +42,25 @@
|
|||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<div class="qr-scanner" v-if="displayQRScanner" @click="displayQRScanner = false">
|
||||||
|
<miaoQRScanner class="qr-scanner-scanner" @on-result="handleScannerRes" @click.stop></miaoQRScanner>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import jsQR from 'jsqr'
|
||||||
|
|
||||||
|
|
||||||
|
// const router = useRouter()
|
||||||
|
const testCRef = ref<HTMLCanvasElement>()
|
||||||
|
|
||||||
const url = ref('')
|
const url = ref('')
|
||||||
const likes = ref(10)
|
const likes = ref(10)
|
||||||
const status = ref<'waiting' | 'busy'>('waiting')
|
const status = ref<'waiting' | 'busy'>('waiting')
|
||||||
|
|
||||||
|
const displayQRScanner = ref(false)
|
||||||
|
|
||||||
const messages = reactive<{
|
const messages = reactive<{
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
message: string,
|
message: string,
|
||||||
@@ -58,10 +75,14 @@ const log = (...message: string[]) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = computed(() => {
|
const matchId = (url: string) => {
|
||||||
const idMatch0 = url.value.match(/id%3D(\d+)/);
|
const idMatch0 = url.match(/id%3D(\d+)/);
|
||||||
const idMatch1 = url.value.match(/id=(\d+)/);
|
const idMatch1 = url.match(/id=(\d+)/);
|
||||||
return idMatch0 ? idMatch0[1] : idMatch1 ? idMatch1[1] : undefined
|
return idMatch0 ? idMatch0[1] : idMatch1 ? idMatch1[1] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = computed(() => {
|
||||||
|
return matchId(url.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const _fd = (
|
const _fd = (
|
||||||
@@ -77,12 +98,12 @@ const handleStart = () => {
|
|||||||
};
|
};
|
||||||
sse.onmessage = function (event) {
|
sse.onmessage = function (event) {
|
||||||
const data = JSON.parse(event.data.split('data: ')[1])
|
const data = JSON.parse(event.data.split('data: ')[1])
|
||||||
if(data.likeCountBefore || data.likeCountAfter) {
|
if (data.likeCountBefore || data.likeCountAfter) {
|
||||||
log(`当前赞数: ${data?.likeCountBefore ?? data?.likeCountAfter}`)
|
log(`当前赞数: ${data?.likeCountBefore ?? data?.likeCountAfter}`)
|
||||||
} else if(data.process) {
|
} else if (data.process) {
|
||||||
log(`已点赞 ${data.process} 次,已完成 ${data.percent}%`)
|
log(`已点赞 ${data.process} 次,已完成 ${data.percent}%`)
|
||||||
}
|
}
|
||||||
if(data.likeCountAfter) {
|
if (data.likeCountAfter) {
|
||||||
sse.close()
|
sse.close()
|
||||||
log('任务完成')
|
log('任务完成')
|
||||||
status.value = 'waiting'
|
status.value = 'waiting'
|
||||||
@@ -101,6 +122,46 @@ const handleClear = () => {
|
|||||||
messages.splice(0, messages.length)
|
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(() => {
|
watchEffect(() => {
|
||||||
if (id.value) {
|
if (id.value) {
|
||||||
log(`识别到id: ${id.value}`)
|
log(`识别到id: ${id.value}`)
|
||||||
@@ -111,7 +172,7 @@ watchEffect(() => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.fuck-sq-wrapper {
|
.fuck-sq-wrapper {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -150,16 +211,36 @@ watchEffect(() => {
|
|||||||
.message-container {
|
.message-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
height: calc( 100vh - 350px );
|
height: calc(100vh - 350px);
|
||||||
max-height: calc( 100vh - 350px );
|
max-height: calc(100vh - 350px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px 0 ;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container-message {
|
.message-container-message {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
margin: 0 10px;
|
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>
|
</style>
|
||||||
@@ -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>
|
||||||
Generated
+760
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,4 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import testData from "~/public/testData/test.md"
|
|
||||||
import DB from 'better-sqlite3';
|
import DB from 'better-sqlite3';
|
||||||
import { postItem } from "~/types/post";
|
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' };
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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' };
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,12 +7,10 @@ const pathNeedAuth = serverConfig.auth.api
|
|||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const path = getRequestURL(event).pathname
|
const path = getRequestURL(event).pathname
|
||||||
// console.log(path)
|
|
||||||
if (pathNeedAuth.has(path)) {
|
if (pathNeedAuth.has(path)) {
|
||||||
const key = getKey()
|
const key = getKey()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const authHeader = event.headers.get('authorization');
|
const authHeader = event.headers.get('authorization');
|
||||||
// console.log(authHeader)
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
||||||
}
|
}
|
||||||
@@ -20,7 +18,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, key) as { userId: number, password: string, username: string, exp: number }
|
const decoded = jwt.verify(token, key) as { userId: number, password: string, username: string, exp: number }
|
||||||
if (decoded.exp > Date.now()) {
|
if (decoded.exp > Date.now()) {
|
||||||
// console.log('token expired')
|
|
||||||
throw createError({ statusCode: 401, statusMessage: 'Token invalid or expired', data: { err: 'token expired', token } })
|
throw createError({ statusCode: 401, statusMessage: 'Token invalid or expired', data: { err: 'token expired', token } })
|
||||||
}
|
}
|
||||||
// console.log(decoded)
|
// console.log(decoded)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import mkfileIfNotExist from "./mkfileIfNotExist"
|
import mkfileIfNotExist from "./mkfileIfNotExist"
|
||||||
import db from 'better-sqlite3'
|
import DB from 'better-sqlite3'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
let checked = false
|
let checked = false
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ function checkUserData() {
|
|||||||
const dirList = [
|
const dirList = [
|
||||||
'./userData/',
|
'./userData/',
|
||||||
'./userData/db/',
|
'./userData/db/',
|
||||||
|
'./userData/imgBed/',
|
||||||
]
|
]
|
||||||
dirList.forEach(mkdirIfNotExist)
|
dirList.forEach(mkdirIfNotExist)
|
||||||
|
|
||||||
@@ -24,9 +26,14 @@ function checkUserData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDB() {
|
function createDB() {
|
||||||
const userDB = new db('./userData/db/user.db');
|
createUserDB();
|
||||||
const postsDB = new db('./userData/db/posts.db');
|
createPostsDB();
|
||||||
|
createImgBedDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const createUserDB = () => {
|
||||||
|
const userDB = new DB('./userData/db/user.db');
|
||||||
userDB.exec(`
|
userDB.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -36,7 +43,11 @@ function createDB() {
|
|||||||
lastLoginTime INTEGER NOT NULL
|
lastLoginTime INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
userDB.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPostsDB = () => {
|
||||||
|
const postsDB = new DB('./userData/db/posts.db');
|
||||||
postsDB.exec(`
|
postsDB.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS posts (
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -49,14 +60,12 @@ function createDB() {
|
|||||||
FOREIGN KEY(tag_id) REFERENCES tags(id)
|
FOREIGN KEY(tag_id) REFERENCES tags(id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
postsDB.exec(`
|
postsDB.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS postContent (
|
CREATE TABLE IF NOT EXISTS postContent (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
content TEXT NOT NULL
|
content TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
postsDB.exec(`
|
postsDB.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS tags (
|
CREATE TABLE IF NOT EXISTS tags (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -77,9 +86,56 @@ function createDB() {
|
|||||||
tag15 TEXT
|
tag15 TEXT
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
postsDB.close();
|
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
|
export default checkUserData
|
||||||
@@ -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()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// })()
|
||||||
@@ -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('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', '\'')
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in new issue
Block a user