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:
2025-10-14 17:22:34 +08:00
parent 7a20655557
commit edb026e6ed
8 changed files with 297 additions and 28 deletions

View File

@@ -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
View File

@@ -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']

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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