feat: 后端添加键值对存储支持。前端添加一个相应的插件

This commit is contained in:
539943419 committed 2025-04-01 20:19:32 +08:00
1 parent 6f42c634ff
commit d3e19d4855
9 files changed
+415 -7

No files matched your search

+3
View File
@@ -15,13 +15,16 @@ declare module 'vue' {
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NQrCode: typeof import('naive-ui')['NQrCode']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
+142
View File
@@ -0,0 +1,142 @@
package api
import (
"crypto/md5"
"encoding/hex"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
// 内存存储
var memoryStorage = make(map[string][]byte)
// MapSetHandler 处理 /map/set/*key 请求
func MapSetHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
// 读取请求体数据
data, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Failed to read request body"})
return
}
if diskParam == "1" {
// 保存到文件系统
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 确保缓存目录存在
if err := os.MkdirAll(cachePath, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to create cache directory"})
return
}
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 写入文件
filePath := filepath.Join(cachePath, filename)
if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to write file"})
return
}
} else {
// 保存到内存
memoryStorage[key] = data
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
// MapGetHandler 处理 /map/get/*key 请求
func MapGetHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
var data []byte
var err error
if diskParam == "1" {
// 从文件系统获取
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 读取文件
filePath := filepath.Join(cachePath, filename)
data, err = ioutil.ReadFile(filePath)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"message": "failed", "error": "Key not found"})
return
}
} else {
// 从内存获取
var exists bool
data, exists = memoryStorage[key]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"message": "failed", "error": "Key not found"})
return
}
}
c.Data(http.StatusOK, "application/octet-stream", data)
}
// MapDeleteHandler 处理 /map/delete/*key 请求
func MapDeleteHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
if diskParam == "1" {
// 从文件系统删除
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 删除文件
filePath := filepath.Join(cachePath, filename)
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to delete file"})
return
}
} else {
// 从内存删除
delete(memoryStorage, key)
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
+5
View File
@@ -42,6 +42,11 @@ func main() {
// 添加获取所有可用地址的API
router.GET("/addresses", api.AddressesHandler(port))
// 添加map相关API
router.POST("/map/set/*key", api.MapSetHandler)
router.GET("/map/get/*key", api.MapGetHandler)
router.POST("/map/delete/*key", api.MapDeleteHandler)
// Redirect from '/' to '/web/'
router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/web/")
+6 -6
View File
@@ -173,12 +173,12 @@ export default class PluginCenter {
}
}
usePlugin(
key: string,
VDirectories?: VirtualDirectory[],
VFiles?: VirtualFile[],
...args: any[]
): void
// usePlugin(
// key: string,
// VDirectories?: VirtualDirectory[],
// VFiles?: VirtualFile[],
// ...args: any[]
// ): void
usePlugin(
key: string,
VDirectories?: VirtualDirectory[],
+4
View File
@@ -9,6 +9,7 @@ const initPlugin = async (globalAlertTip: alertTipType) => {
})
const pluginCenter = usePluginCenter()
for (let pluginConfig of Object.values(modulesFiles)) {
try {
const _config = pluginConfig as {
default: registerComponentOption | Plugin
type: PluginExportType
@@ -22,6 +23,9 @@ const initPlugin = async (globalAlertTip: alertTipType) => {
} else if (type === PluginExportType.default) {
pluginCenter.register(key as string, _config.default as Plugin)
}
} catch (error) {
console.error(`插件 ${pluginConfig} 初始化失败`, error)
}
}
}
+211
View File
@@ -0,0 +1,211 @@
<template>
<miao-alert-tip-provider ref="miaoMessageRef">
<div class="clipboard-container">
<n-card class="clipboard-card" title="剪贴板" size="medium">
<div class="key-section">
<n-input v-model:value="key" type="text" placeholder="请输入Key值" class="key-input" />
<div class="button-group">
<n-button @click="getData" type="primary" size="small">查询</n-button>
<n-button @click="setData" type="info" size="small">设置</n-button>
<n-button @click="deleteData" type="error" size="small">删除</n-button>
</div>
</div>
<div class="data-section">
<n-input v-model:value="content" type="textarea" placeholder="数据内容"
:autosize="{ minRows: 5, maxRows: 15 }" class="data-input" />
</div>
<div class="flex justify-end items-center gap-3">
<n-button @click="clearInput" type="error" size="small">清空输入</n-button>
<div class="flex justify-center items-center">
<n-checkbox v-model:checked="useDisk">保存到磁盘</n-checkbox>
</div>
</div>
<div class="status-section" v-if="statusMessage">
<n-alert :type="statusType" class="status-alert">
{{ statusMessage }}
</n-alert>
</div>
</n-card>
</div>
</miao-alert-tip-provider>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import {
NCard,
NInput,
NButton,
NCheckbox,
NAlert
} from 'naive-ui'
import miaoAlertTipProvider from '@/components/miaoAlertTipProvider.vue'
import { alertTipType } from '@/types/type'
const key = ref('')
const content = ref('')
const useDisk = ref(false)
const statusMessage = ref('')
const statusType = ref<'success' | 'error' | 'info' | 'warning'>('info')
const miaoMessageRef = ref<InstanceType<typeof miaoAlertTipProvider>>()
const globalAlertTip = inject<alertTipType>('globalAlertTip')
// 计算磁盘参数
const diskParam = computed(() => useDisk.value ? '1' : '0')
// 显示状态消息
const showStatus = (msg: string, type: 'success' | 'error' | 'info' | 'warning' = 'info') => {
statusMessage.value = msg
statusType.value = type
setTimeout(() => {
statusMessage.value = ''
}, 3000)
}
// 显示消息提示
const showMessage = (content: string, type: 'success' | 'error' | 'info' = 'info') => {
if (miaoMessageRef.value?.alertTip) {
miaoMessageRef.value.alertTip(content, { type, timeout: 3000 })
} else if (globalAlertTip) {
globalAlertTip(content, { type, timeout: 3000 })
}
}
// 获取数据
const getData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/get/${key.value}?disk=${diskParam.value}`)
if (!response.ok) {
if (response.status === 404) {
showStatus('未找到对应的数据', 'warning')
showMessage('未找到对应的数据', 'error')
} else {
showStatus(`获取失败:${response.status}`, 'error')
showMessage(`获取失败:${response.status}`, 'error')
}
return
}
const data = await response.text()
content.value = data
showStatus('获取成功', 'success')
showMessage('获取成功', 'success')
} catch (error) {
showStatus(`获取失败:${error}`, 'error')
showMessage(`获取失败:${error}`, 'error')
}
}
// 设置数据
const setData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/set/${key.value}?disk=${diskParam.value}`, {
method: 'POST',
body: content.value
})
if (!response.ok) {
showStatus(`设置失败:${response.status}`, 'error')
showMessage(`设置失败:${response.status}`, 'error')
return
}
showStatus('设置成功', 'success')
showMessage('设置成功', 'success')
} catch (error) {
showStatus(`设置失败:${error}`, 'error')
showMessage(`设置失败:${error}`, 'error')
}
}
// 删除数据
const deleteData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/delete/${key.value}?disk=${diskParam.value}`, {
method: 'POST'
})
if (!response.ok) {
showStatus(`删除失败:${response.status}`, 'error')
showMessage(`删除失败:${response.status}`, 'error')
return
}
content.value = ''
showStatus('删除成功', 'success')
showMessage('删除成功', 'success')
} catch (error) {
showStatus(`删除失败:${error}`, 'error')
showMessage(`删除失败:${error}`, 'error')
}
}
// 清空输入
const clearInput = () => {
key.value = ''
content.value = ''
}
</script>
<style scoped lang="scss">
.clipboard-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
}
.clipboard-card {
width: 100%;
max-width: 600px;
}
.key-section {
display: flex;
margin-bottom: 16px;
gap: 10px;
}
.key-input {
flex: 1;
}
.button-group {
display: flex;
gap: 8px;
}
.data-section {
margin-bottom: 16px;
}
// .disk-option {
// // margin-bottom: 16px;
// }
.status-section {
margin-top: 16px;
}
.status-alert {
width: 100%;
}
</style>
+18
View File
@@ -0,0 +1,18 @@
import { registerComponentOption } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
import { CopyOutline } from '@vicons/ionicons5'
const pluginConfig: registerComponentOption = {
key: 'miaoKVMap',
name: '键值存储',
icon: CopyOutline,
getComponent: async () => (await import('./KVMapPlugin.vue')).default,
filter: (_, __) => _.length === 0 && __.length === 0,
disable: false,
single: true,
priority: 0
}
export default pluginConfig
export const type = PluginExportType.component
+25
View File
@@ -0,0 +1,25 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
import { CopyOutline } from '@vicons/ionicons5'
const pluginConfig: Plugin = {
name: '键值存储',
icon: shallowRef(CopyOutline),
filter: (_, __, views) => {
return views.find('miaoKVMap').length === 0
},
disable: false,
group: [PluginGroup.mainMenu],
func(_, __, hooks) {
const { getPluginCenter } = hooks
const PluginCenter = getPluginCenter()
PluginCenter.usePlugin('miaoKVMap', [], [])
},
priority: 0
}
export default pluginConfig
export const type = PluginExportType.default
// export const key = 'miaoClipboard_mainMenu'
+1 -1
View File
@@ -40,7 +40,7 @@ export default defineConfig({
},
server: {
proxy: {
'^/(get|upload|delete|query|rename|cut|copy|mkdir|addresses)': {
'^/(get|upload|delete|query|rename|cut|copy|mkdir|addresses|map)': {
target: 'http://127.0.0.1:17705',
changeOrigin: true,
rewrite: (path) => path