refactor(group): Refactor grouping to add logic and optimize validation rules.

- Remove direct database operations and use groupRepository instead.
- Use notifyStore to unify notification messages.
This commit is contained in:
2025-10-15 14:34:59 +08:00
parent 8988766d09
commit 29843bb5e5
11 changed files with 198 additions and 119 deletions

View File

@@ -1,5 +1,6 @@
<template> <template>
<v-app> <v-app>
<notification-provider />
<v-main> <v-main>
<v-card class="fill-height"> <v-card class="fill-height">
<v-layout class="fill-height"> <v-layout class="fill-height">
@@ -53,6 +54,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import notificationProvider from './components/notify/notificationProvider.vue'
const drawer = ref(true) const drawer = ref(true)
const router = useRouter() const router = useRouter()

1
src/components.d.ts vendored
View File

@@ -12,6 +12,7 @@ declare module 'vue' {
AddNode: typeof import('./components/nodeEdit/addNode.vue')['default'] AddNode: typeof import('./components/nodeEdit/addNode.vue')['default']
NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default'] NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default']
NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default'] NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default']
NotificationProvider: typeof import('./components/notify/notificationProvider.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

@@ -1,21 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted, computed} from 'vue' import {ref, computed} from 'vue'
import {invoke} from '@tauri-apps/api/core' import {invoke} from '@tauri-apps/api/core'
import Database from '@tauri-apps/plugin-sql' import {groupRepository} from "@/entities/group.ts";
import {notify} from "@/components/notify/notifyStore.ts";
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 groupName = ref('') const groupName = ref('')
const groupNameRule = [ const groupNameRule = [
(value: string): boolean | string => { (value: string): boolean | string => {
if (value?.length >= 3 && value?.length <= 15) return true if (value?.length >= 1 && value?.length <= 15) return true
return 'Group name must be between 3 and 15 characters.' return 'Group name must be between 1 and 15 characters.'
}, },
] ]
@@ -38,43 +31,31 @@ const groupArguments = ref<string | null>(null)
const isAdding = ref(false) const isAdding = ref(false)
const isAddDisabled = computed(() => { const isAddDisabled = computed(() => {
return !groupName.value || groupName.value.length < 3 || groupName.value.length > 15 || isAdding.value || !db_ready.value return !groupName.value || groupName.value.length < 3 || groupName.value.length > 15 || isAdding.value
}) })
async function addGroup() { async function addGroup() {
if (!isAddDisabled.value) { if (!isAddDisabled.value) {
add_group(groupName.value, groupSubscribeUrl.value, groupArguments.value) await add_group(groupName.value, groupSubscribeUrl.value, groupArguments.value)
const check_repeat_one = await db.value.select( const check_repeat_one = await groupRepository.findByName(groupName.value)
"SELECT * FROM `group` WHERE name = ?",
[groupName.value]
)
console.log(check_repeat_one) console.log(check_repeat_one)
if (check_repeat_one.length > 0) { if (check_repeat_one.length > 0) {
alert("Group already exists.") notify("Group already exists.", {
color: "error"
})
return return
} }
let add_result await groupRepository.insert(
const params = [groupName.value]; {
let sqlStmt = "INSERT INTO `group` (name"; created_at: null, id: null, updated_at: null,
name: groupName.value,
if (groupSubscribeUrl.value) { url: groupSubscribeUrl.value,
sqlStmt += ", url"; arguments: groupArguments.value
params.push(groupSubscribeUrl.value); }
} )
notify("Group added.")
if (groupArguments.value) {
sqlStmt += ", arguments";
params.push(groupArguments.value);
}
sqlStmt += ") VALUES (" + params.map(() => "?").join(", ") + ")";
add_result = await db.value.execute(sqlStmt, params);
if (add_result.rowsAffected > 0) {
alert("Group added.")
}
} }
} }
@@ -86,6 +67,7 @@ async function add_group(groupName: string, groupSubscribeUrl: string | null, gr
isAdding.value = false isAdding.value = false
} }
} }
</script> </script>
<template> <template>

View File

@@ -1,90 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref} from 'vue' import {ref, onMounted, computed} from 'vue'
import {invoke} from '@tauri-apps/api/core'
import Database from '@tauri-apps/plugin-sql'
import {Group, groupRepository} from "@/entities/group.ts"; import {Group, groupRepository} from "@/entities/group.ts";
import {nodeRepository} from "@/entities/node.ts";
import {notify} from "@/components/notify/notifyStore.ts";
defineProps<{ const props = defineProps<{ groupId: string }>()
groupId: string
}>()
const db = ref<any>(null)
const db_ready = ref(false)
const allGroups = ref<Group[]>([]) const allGroups = ref<Group[]>([])
const selectedGroupId = ref<number | null>(null)
const loadGroups = async () => { const loadGroups = async () => {
allGroups.value = await groupRepository.findAll() allGroups.value = await groupRepository.findAll()
const group = allGroups.value.find(g => g.id === Number(props.groupId))
selectedGroupId.value = group ? group.id : null
} }
onMounted(async () => { onMounted(loadGroups)
db.value = await Database.load('sqlite:spary.db')
db_ready.value = true
})
const nodeAlias = ref('') const nodeAlias = ref('')
const nodeArguments = ref<string | null>(null) const nodeArguments = ref<string | null>(null)
const isAdding = ref(false) const isAdding = ref(false)
const isAddDisabled = computed(() => { const isAddDisabled = computed(() => {
return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value || !db_ready.value return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value
}) })
async function addNode() { async function addNode() {
if (!isAddDisabled.value) { if (!isAddDisabled.value && selectedGroupId.value !== null) {
await add_node(nodeAlias.value, nodeArguments.value) isAdding.value = true
await nodeRepository.insert({
const check_repeat_one = await db.value.select( created_at: null,
"SELECT * FROM `node` WHERE alias = ?", id: null,
[nodeAlias.value] updated_at: null,
) alias: nodeAlias.value,
console.log(check_repeat_one) arguments: nodeArguments.value,
if (check_repeat_one.length > 0) { group_id: selectedGroupId.value
alert("Node already exists.") })
return notify("Node added successfully")
}
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 isAdding.value = false
} }
} }
loadGroups()
</script> </script>
<template> <template>
<v-container> <v-container>
<v-sheet class="mx-auto" width="80vw"> <v-sheet class="mx-auto" width="80vw">
<v-form fast-fail @submit.prevent> <v-form fast-fail @submit.prevent>
<v-text-field
v-model="nodeAlias"
label="Node alias"
></v-text-field>
<v-select <v-select
v-model="selectedGroupId"
label="Group" label="Group"
:items="allGroups" :items="allGroups"
item-title="name" item-title="name"
@@ -92,6 +54,11 @@ loadGroups()
variant="solo" variant="solo"
></v-select> ></v-select>
<v-text-field
v-model="nodeAlias"
label="Node alias"
></v-text-field>
<v-textarea <v-textarea
v-model="nodeArguments" v-model="nodeArguments"
label="Arguments" label="Arguments"
@@ -108,4 +75,4 @@ loadGroups()
</v-form> </v-form>
</v-sheet> </v-sheet>
</v-container> </v-container>
</template> </template>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import {ref} from 'vue'
import Database from "@tauri-apps/plugin-sql";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {groupRepository} from "@/entities/group.ts";
import {nodeRepository} from "@/entities/node.ts";
const router = useRouter() const router = useRouter()
@@ -17,19 +18,13 @@ interface Group {
id: number id: number
} }
const db = ref<any>(null)
const panels = ref([]) const panels = ref([])
const groups = ref<Group[]>([]) const groups = ref<Group[]>([])
const loadData = async () => { const loadData = async () => {
db.value = await Database.load('sqlite:spary.db')
const groupsInDb = await db.value.select( const groupsInDb = await groupRepository.findAll()
'SELECT * FROM `group`' const nodesInDb = await nodeRepository.findAll()
)
const nodesInDb = await db.value.select(
'SELECT * FROM `node`'
)
for (let i = 0; i < groupsInDb.length; i++) { for (let i = 0; i < groupsInDb.length; i++) {
const group: any = groupsInDb[i] const group: any = groupsInDb[i]

View File

@@ -0,0 +1,37 @@
<template>
<div>
<v-snackbar
v-for="(item, index) in notifyQueue"
:key="item.id"
v-model="visible[item.id]"
:timeout="item.timeout"
:color="item.color"
:variant="item.variant"
top
right
:style="{
zIndex: 9999,
marginTop: `${index * 70 + 16}px` // 每条通知向下偏移
}"
>
{{ item.message }}
</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { reactive, watch } from 'vue'
import { notifyQueue } from './notifyStore'
const visible: Record<number, boolean> = reactive({})
watch(
() => notifyQueue.slice(),
(queue) => {
queue.forEach(item => {
if (visible[item.id] === undefined) visible[item.id] = true
})
},
{ immediate: true }
)
</script>

View File

@@ -0,0 +1,39 @@
import { reactive } from 'vue'
export interface NotifyOptions {
color?: string
timeout?: number
variant?: string
}
// 通知对象
export interface NotifyItem {
id: number
message: string
color: string
timeout: number
variant: string
}
// 队列状态
export const notifyQueue = reactive<NotifyItem[]>([])
// 全局可调用 notify
let nextId = 1
export function notify(message: string, options?: NotifyOptions) {
const id = nextId++
const item: NotifyItem = {
id,
message,
color: options?.color ?? 'success',
timeout: options?.timeout ?? 3000,
variant: options?.variant ?? 'outlined'
}
notifyQueue.push(item)
// 自动移除
setTimeout(() => {
const index = notifyQueue.findIndex(i => i.id === id)
if (index !== -1) notifyQueue.splice(index, 1)
}, item.timeout)
}

View File

@@ -1,9 +1,9 @@
import {z} from "zod"; import {z} from "zod";
import {getDatabase} from "@/utils/db.ts"; import {getDatabase} from "@/utils/db.ts";
import {DBDefaultDateTime} from "@/utils/common.ts"; import {DBDefaultDateTime} from "@/utils/zodCommon.ts";
export const GroupSchema = z.object({ export const GroupSchema = z.object({
id: z.number(), id: z.number().nullable(),
name: z.string(), name: z.string(),
url: z.string().nullable().optional(), url: z.string().nullable().optional(),
arguments: z arguments: z

55
src/entities/node.ts Normal file
View File

@@ -0,0 +1,55 @@
import {z} from "zod";
import {getDatabase} from "@/utils/db.ts";
import {DBDefaultDateTime} from "@/utils/zodCommon.ts";
export const NodeSchema = z.object({
id: z.number().nullable(),
alias: z.string(),
arguments: z
.union([z.string(), z.record(z.any(), z.any())]) // 兼容 SQLite JSON 字段可能返回 string 或 object
.transform(val => {
if (typeof val === "string") {
try {
return JSON.parse(val);
} catch {
return {};
}
}
return val ?? {};
}),
created_at: DBDefaultDateTime,
updated_at: DBDefaultDateTime,
group_id: z.number(),
});
export type Node = z.infer<typeof NodeSchema>;
export class NodeRepository {
async findAll(): Promise<Node[]> {
const db = await getDatabase();
const rows = await db.select(`SELECT *
FROM "node"
ORDER BY id DESC`);
return NodeSchema.array().parse(rows);
}
async insert(node: Node): Promise<void> {
const db = await getDatabase();
await db.execute(`INSERT INTO "node" (alias, arguments, group_id)
VALUES (?, ?, ?)`, [
node.alias,
JSON.stringify(node.arguments),
node.group_id
]);
}
async findByAlias(alias: string): Promise<Node[]> {
const db = await getDatabase();
const rows = await db.select(`SELECT *
FROM "node"
WHERE alias = ?`, [alias]);
return NodeSchema.array().parse(rows);
}
}
export const nodeRepository = new NodeRepository();

View File

@@ -1,9 +0,0 @@
// 自定义日期时间格式 yyyy-MM-dd HH:mm:ss
import {z} from "zod";
export const DBDefaultDateTime = z
.string()
.refine(
val => /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(val),
{ message: "Invalid datetime format, expected yyyy-MM-dd HH:mm:ss" }
);

10
src/utils/zodCommon.ts Normal file
View File

@@ -0,0 +1,10 @@
// 自定义日期时间格式 yyyy-MM-dd HH:mm:ss
import {z} from "zod";
export const DBDefaultDateTime = z
.string()
.nullable()
.refine(
val => val && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(val),
{message: "Invalid datetime format, expected yyyy-MM-dd HH:mm:ss"}
);