feat: 后端添加键值对存储支持。前端添加一个相应的插件
This commit is contained in:
1 parent
6f42c634ff
commit
d3e19d4855
9 files changed
+415
-7
No files matched your search
Vendored
+3
@@ -15,13 +15,16 @@ declare module 'vue' {
|
|||||||
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
|
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
|
||||||
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
|
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
|
||||||
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
|
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
|
||||||
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NGrid: typeof import('naive-ui')['NGrid']
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
|||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@ func main() {
|
|||||||
// 添加获取所有可用地址的API
|
// 添加获取所有可用地址的API
|
||||||
router.GET("/addresses", api.AddressesHandler(port))
|
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/'
|
// Redirect from '/' to '/web/'
|
||||||
router.GET("/", func(c *gin.Context) {
|
router.GET("/", func(c *gin.Context) {
|
||||||
c.Redirect(http.StatusMovedPermanently, "/web/")
|
c.Redirect(http.StatusMovedPermanently, "/web/")
|
||||||
|
|||||||
@@ -173,12 +173,12 @@ export default class PluginCenter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usePlugin(
|
// usePlugin(
|
||||||
key: string,
|
// key: string,
|
||||||
VDirectories?: VirtualDirectory[],
|
// VDirectories?: VirtualDirectory[],
|
||||||
VFiles?: VirtualFile[],
|
// VFiles?: VirtualFile[],
|
||||||
...args: any[]
|
// ...args: any[]
|
||||||
): void
|
// ): void
|
||||||
usePlugin(
|
usePlugin(
|
||||||
key: string,
|
key: string,
|
||||||
VDirectories?: VirtualDirectory[],
|
VDirectories?: VirtualDirectory[],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const initPlugin = async (globalAlertTip: alertTipType) => {
|
|||||||
})
|
})
|
||||||
const pluginCenter = usePluginCenter()
|
const pluginCenter = usePluginCenter()
|
||||||
for (let pluginConfig of Object.values(modulesFiles)) {
|
for (let pluginConfig of Object.values(modulesFiles)) {
|
||||||
|
try {
|
||||||
const _config = pluginConfig as {
|
const _config = pluginConfig as {
|
||||||
default: registerComponentOption | Plugin
|
default: registerComponentOption | Plugin
|
||||||
type: PluginExportType
|
type: PluginExportType
|
||||||
@@ -22,6 +23,9 @@ const initPlugin = async (globalAlertTip: alertTipType) => {
|
|||||||
} else if (type === PluginExportType.default) {
|
} else if (type === PluginExportType.default) {
|
||||||
pluginCenter.register(key as string, _config.default as Plugin)
|
pluginCenter.register(key as string, _config.default as Plugin)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`插件 ${pluginConfig} 初始化失败`, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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
@@ -40,7 +40,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
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',
|
target: 'http://127.0.0.1:17705',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path
|
rewrite: (path) => path
|
||||||
|
|||||||
Reference in new issue
Block a user