feat(node): Implements node management functionality
- Adds a new node table structure - Configures routing to support dynamic groupId parameter passing
This commit is contained in:
@@ -14,7 +14,13 @@ pub fn run() {
|
|||||||
description: "create_initial_tables",
|
description: "create_initial_tables",
|
||||||
sql: "CREATE TABLE `group`(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(60) NOT NULL,url TEXT NULL, arguments JSON NOT NULL default '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)",
|
sql: "CREATE TABLE `group`(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(60) NOT NULL,url TEXT NULL, arguments JSON NOT NULL default '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)",
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
}
|
},
|
||||||
|
Migration {
|
||||||
|
version: 2,
|
||||||
|
description: "add_node_table",
|
||||||
|
sql: "CREATE TABLE node(id INTEGER PRIMARY KEY AUTOINCREMENT, alias VARCHAR(60) NOT NULL,arguments JSON NOT NULL default '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, group_id INTEGER NOT NULL)",
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(
|
.plugin(
|
||||||
|
|||||||
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
@@ -9,6 +9,9 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AddGroup: typeof import('./components/nodeEdit/addGroup.vue')['default']
|
AddGroup: typeof import('./components/nodeEdit/addGroup.vue')['default']
|
||||||
|
AddNode: typeof import('./components/nodeEdit/addNode.vue')['default']
|
||||||
|
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
|
||||||
|
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Spary: typeof import('./components/index/spary.vue')['default']
|
Spary: typeof import('./components/index/spary.vue')['default']
|
||||||
|
|||||||
95
src/components/nodeEdit/addNode.vue
Normal file
95
src/components/nodeEdit/addNode.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted, computed} from 'vue'
|
||||||
|
import {invoke} from '@tauri-apps/api/core'
|
||||||
|
import Database from '@tauri-apps/plugin-sql'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
groupId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const db = ref<any>(null)
|
||||||
|
const db_ready = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
db.value = await Database.load('sqlite:spary.db')
|
||||||
|
db_ready.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeAlias = ref('')
|
||||||
|
|
||||||
|
const nodeArguments = ref<string | null>(null)
|
||||||
|
const isAdding = ref(false)
|
||||||
|
|
||||||
|
const isAddDisabled = computed(() => {
|
||||||
|
return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value || !db_ready.value
|
||||||
|
})
|
||||||
|
|
||||||
|
async function addNode() {
|
||||||
|
if (!isAddDisabled.value) {
|
||||||
|
await add_node(nodeAlias.value, nodeArguments.value)
|
||||||
|
|
||||||
|
const check_repeat_one = await db.value.select(
|
||||||
|
"SELECT * FROM `node` WHERE alias = ?",
|
||||||
|
[nodeAlias.value]
|
||||||
|
)
|
||||||
|
console.log(check_repeat_one)
|
||||||
|
if (check_repeat_one.length > 0) {
|
||||||
|
alert("Node already exists.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let add_result
|
||||||
|
const params = [nodeAlias.value];
|
||||||
|
let sqlStmt = "INSERT INTO `node` (alias";
|
||||||
|
|
||||||
|
if (nodeArguments.value) {
|
||||||
|
sqlStmt += ", arguments";
|
||||||
|
params.push(nodeArguments.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlStmt += ") VALUES (" + params.map(() => "?").join(", ") + ")";
|
||||||
|
add_result = await db.value.execute(sqlStmt, params);
|
||||||
|
|
||||||
|
if (add_result.rowsAffected > 0) {
|
||||||
|
alert("Node added.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add_node(nodeAlias: string, nodeArguments: string | null) {
|
||||||
|
isAdding.value = true
|
||||||
|
try {
|
||||||
|
await invoke("add_node", {nodeAlias, nodeArguments})
|
||||||
|
} finally {
|
||||||
|
isAdding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-sheet class="mx-auto" width="80vw">
|
||||||
|
<v-form fast-fail @submit.prevent>
|
||||||
|
<v-text-field
|
||||||
|
v-model="nodeAlias"
|
||||||
|
label="Node alias"
|
||||||
|
></v-text-field>
|
||||||
|
<v-select></v-select>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="nodeArguments"
|
||||||
|
label="Arguments"
|
||||||
|
></v-textarea>
|
||||||
|
<v-btn
|
||||||
|
class="mt-2"
|
||||||
|
type="submit"
|
||||||
|
block
|
||||||
|
:disabled="isAddDisabled"
|
||||||
|
:loading="isAdding"
|
||||||
|
@click="addNode">
|
||||||
|
Add
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-sheet>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
134
src/components/nodeEdit/nodeList.vue
Normal file
134
src/components/nodeEdit/nodeList.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
import Database from "@tauri-apps/plugin-sql";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
|
interface GroupItem {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
traffic: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Group {
|
||||||
|
title: string
|
||||||
|
items: GroupItem[]
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = ref<any>(null)
|
||||||
|
const panels = ref([])
|
||||||
|
const loading = ref(true) // 是否在加载中
|
||||||
|
const groups = ref<Group[]>([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
db.value = await Database.load('sqlite:spary.db')
|
||||||
|
|
||||||
|
const groupsInDb = await db.value.select(
|
||||||
|
'SELECT * FROM `group`'
|
||||||
|
)
|
||||||
|
const nodesInDb = await db.value.select(
|
||||||
|
'SELECT * FROM `node`'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let i = 0; i < groupsInDb.length; i++) {
|
||||||
|
const group: any = groupsInDb[i]
|
||||||
|
const nodes = nodesInDb.filter((node: any) => node.group_id === group.id)
|
||||||
|
groups.value.push({
|
||||||
|
title: group.name,
|
||||||
|
items: nodes.map((node: any) => ({
|
||||||
|
name: node.name,
|
||||||
|
url: node.url,
|
||||||
|
traffic: "0MB"
|
||||||
|
})),
|
||||||
|
id: group.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 模拟异步加载(例如从 API 获取)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
function addItem(group: Group) {
|
||||||
|
console.log(group.id)
|
||||||
|
router.push(`/addNode/${group.id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<!-- 加载中时显示骨架屏 -->
|
||||||
|
<template v-if="loading">
|
||||||
|
<v-skeleton-loader type="heading" class="mb-4"/>
|
||||||
|
|
||||||
|
<v-row dense>
|
||||||
|
<v-col
|
||||||
|
v-for="i in 8"
|
||||||
|
:key="i"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="3"
|
||||||
|
>
|
||||||
|
<v-skeleton-loader type="card" class="ma-2"/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 加载完成后显示实际内容 -->
|
||||||
|
<template v-else>
|
||||||
|
<v-expansion-panels v-model="panels" multiple>
|
||||||
|
<v-expansion-panel
|
||||||
|
v-for="(group, i) in groups"
|
||||||
|
:key="i"
|
||||||
|
elevation="2"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<div class="d-flex align-center justify-space-between w-100">
|
||||||
|
<span class="text-h6">{{ group.title }}</span>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click.stop="addItem(group)"
|
||||||
|
>
|
||||||
|
add
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col
|
||||||
|
v-for="(item, j) in group.items"
|
||||||
|
:key="j"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="3"
|
||||||
|
>
|
||||||
|
<v-card elevation="3" class="ma-2">
|
||||||
|
<v-card-title class="text-subtitle-1">
|
||||||
|
{{ item.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div>{{ item.url }}</div>
|
||||||
|
<div>usage: {{ item.traffic }}</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</template>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
36
src/components/nodeEdit/nodesFloatButton.vue
Normal file
36
src/components/nodeEdit/nodesFloatButton.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fab-fixed">
|
||||||
|
<v-speed-dial location="left top" transition="slide-y-transition">
|
||||||
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
<v-fab v-bind="activatorProps" size="large" icon="mdi-plus"></v-fab>
|
||||||
|
</template>
|
||||||
|
<v-btn key="1" prepend-icon="mdi-group"
|
||||||
|
@click="router.push('/addGroup')">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="success"></v-icon>
|
||||||
|
</template>
|
||||||
|
add group
|
||||||
|
</v-btn>
|
||||||
|
<v-btn key="2" prepend-icon="mdi-airplane-marker">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="success"></v-icon>
|
||||||
|
</template>
|
||||||
|
add node
|
||||||
|
</v-btn>
|
||||||
|
</v-speed-dial>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fab-fixed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/pages/addNode/[groupId].vue
Normal file
13
src/pages/addNode/[groupId].vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useRoute} from 'vue-router/auto'
|
||||||
|
|
||||||
|
const route = useRoute('/addNode/[groupId]')
|
||||||
|
// 🔥 类型会自动推断出 route.params.groupId 是 string
|
||||||
|
|
||||||
|
const preSelectGroupId = route.params.groupId
|
||||||
|
console.log(`preSelectGroupId:${preSelectGroupId}`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<add-node :group-id="preSelectGroupId"/>
|
||||||
|
</template>
|
||||||
@@ -1,36 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useRouter} from "vue-router";
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
|
import NodeList from "@/components/nodeEdit/nodeList.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fab-fixed">
|
<nodes-float-button/>
|
||||||
<v-speed-dial location="left top" transition="slide-y-transition">
|
<node-list/>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
|
||||||
<v-fab v-bind="activatorProps" size="large" icon="mdi-plus"></v-fab>
|
|
||||||
</template>
|
|
||||||
<v-btn key="1" prepend-icon="mdi-group"
|
|
||||||
@click="router.push('/addGroup')">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon color="success"></v-icon>
|
|
||||||
</template>
|
|
||||||
add group
|
|
||||||
</v-btn>
|
|
||||||
<v-btn key="2" prepend-icon="mdi-airplane-marker">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon color="success"></v-icon>
|
|
||||||
</template>
|
|
||||||
add node
|
|
||||||
</v-btn>
|
|
||||||
</v-speed-dial>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fab-fixed {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
5
src/typed-router.d.ts
vendored
5
src/typed-router.d.ts
vendored
@@ -20,6 +20,7 @@ declare module 'vue-router/auto-routes' {
|
|||||||
export interface RouteNamedMap {
|
export interface RouteNamedMap {
|
||||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||||
'/addGroup': RouteRecordInfo<'/addGroup', '/addGroup', Record<never, never>, Record<never, never>>,
|
'/addGroup': RouteRecordInfo<'/addGroup', '/addGroup', Record<never, never>, Record<never, never>>,
|
||||||
|
'/addNode/[groupId]': RouteRecordInfo<'/addNode/[groupId]', '/addNode/:groupId', { groupId: ParamValue<true> }, { groupId: ParamValue<false> }>,
|
||||||
'/nodes': RouteRecordInfo<'/nodes', '/nodes', Record<never, never>, Record<never, never>>,
|
'/nodes': RouteRecordInfo<'/nodes', '/nodes', Record<never, never>, Record<never, never>>,
|
||||||
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
|
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,10 @@ declare module 'vue-router/auto-routes' {
|
|||||||
routes: '/addGroup'
|
routes: '/addGroup'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
|
'src/pages/addNode/[groupId].vue': {
|
||||||
|
routes: '/addNode/[groupId]'
|
||||||
|
views: never
|
||||||
|
}
|
||||||
'src/pages/nodes.vue': {
|
'src/pages/nodes.vue': {
|
||||||
routes: '/nodes'
|
routes: '/nodes'
|
||||||
views: never
|
views: never
|
||||||
|
|||||||
Reference in New Issue
Block a user