Merge branch 'main' of https://nj.miaospring.top:3001/539943419/miaoBlog
This commit is contained in:
commit
12f9726814
27 files changed
+2207
-40
No files matched your search
@@ -26,3 +26,5 @@ logs
|
||||
.pnpm-lock.yaml
|
||||
|
||||
userData
|
||||
|
||||
test/*
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v20.18.1
|
||||
v20.15.0
|
||||
@@ -18,7 +18,7 @@ import "/style/default.scss";
|
||||
useHead({
|
||||
title: '杪 Blog',
|
||||
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>
|
||||
<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>
|
||||
@@ -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,6 +31,13 @@ const clearTimer = () => {
|
||||
|
||||
const handleHide = () => {
|
||||
clearTimer()
|
||||
calcOriginHeight();
|
||||
cwStyle.value = {
|
||||
maxHeight: `${originHeight}px`,
|
||||
opacity: 1,
|
||||
transition: "all 0.2s ease-in-out, opacity 0.1s ease-out",
|
||||
}
|
||||
timer.push(setTimeout(() => {
|
||||
cwStyle.value = {
|
||||
maxHeight: 0,
|
||||
opacity: 0,
|
||||
@@ -44,8 +49,10 @@ const handleHide = () => {
|
||||
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: 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 0.2s ease-in-out, opacity 0.3s ease-in"
|
||||
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) => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
originHeight = cwRef.value!.clientHeight
|
||||
cwStyle.value = {
|
||||
maxHeight: `${originHeight}px`,
|
||||
const calcOriginHeight = () => {
|
||||
originHeight = cRef.value!.clientHeight
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calcOriginHeight()
|
||||
})
|
||||
</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 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
@@ -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
@@ -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: '学习资料',
|
||||
|
||||
@@ -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>
|
||||
+89
-8
@@ -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 = (
|
||||
@@ -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%;
|
||||
@@ -162,4 +223,24 @@ watchEffect(() => {
|
||||
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>
|
||||
@@ -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
|
||||
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' };
|
||||
}
|
||||
});
|
||||
@@ -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) => {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -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