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",
|
||||
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,
|
||||
}
|
||||
},
|
||||
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()
|
||||
.plugin(
|
||||
|
||||
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
@@ -9,6 +9,9 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
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']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
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">
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter()
|
||||
|
||||
import NodeList from "@/components/nodeEdit/nodeList.vue";
|
||||
</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>
|
||||
<nodes-float-button/>
|
||||
<node-list/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fab-fixed {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
</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 {
|
||||
'/': RouteRecordInfo<'/', '/', 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>>,
|
||||
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
@@ -43,6 +44,10 @@ declare module 'vue-router/auto-routes' {
|
||||
routes: '/addGroup'
|
||||
views: never
|
||||
}
|
||||
'src/pages/addNode/[groupId].vue': {
|
||||
routes: '/addNode/[groupId]'
|
||||
views: never
|
||||
}
|
||||
'src/pages/nodes.vue': {
|
||||
routes: '/nodes'
|
||||
views: never
|
||||
|
||||
Reference in New Issue
Block a user