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:
@@ -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
1
src/components.d.ts
vendored
@@ -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']
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
notify("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>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
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
|
isAdding.value = true
|
||||||
try {
|
await nodeRepository.insert({
|
||||||
await invoke("add_node", {nodeAlias, nodeArguments})
|
created_at: null,
|
||||||
} finally {
|
id: null,
|
||||||
|
updated_at: null,
|
||||||
|
alias: nodeAlias.value,
|
||||||
|
arguments: nodeArguments.value,
|
||||||
|
group_id: selectedGroupId.value
|
||||||
|
})
|
||||||
|
notify("Node added successfully")
|
||||||
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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
37
src/components/notify/notificationProvider.vue
Normal file
37
src/components/notify/notificationProvider.vue
Normal 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>
|
||||||
39
src/components/notify/notifyStore.ts
Normal file
39
src/components/notify/notifyStore.ts
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
55
src/entities/node.ts
Normal 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();
|
||||||
@@ -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
10
src/utils/zodCommon.ts
Normal 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"}
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user