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']
|
||||
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']
|
||||
|
||||
@@ -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
|
||||
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/")
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user