feat(nodeEdit): Add node configurator and schema field components
- Introduced the NodeConfigurator component for node configuration - Added the SchemaField component for schema field rendering - Changed node parameters to an object configuration model - Added error handling and notification mechanisms - Modified the CoreTypes definition method to avoid Proxy issues
This commit is contained in:
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -13,11 +13,13 @@ declare module 'vue' {
|
|||||||
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||||
MainConsole: typeof import('./components/index/mainConsole.vue')['default']
|
MainConsole: typeof import('./components/index/mainConsole.vue')['default']
|
||||||
MainDrawer: typeof import('./components/index/mainDrawer.vue')['default']
|
MainDrawer: typeof import('./components/index/mainDrawer.vue')['default']
|
||||||
|
NodeConfigurator: typeof import('./components/nodeEdit/nodeConfigurator.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']
|
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']
|
||||||
|
SchemaField: typeof import('./components/nodeEdit/SchemaField.vue')['default']
|
||||||
Spary: typeof import('./components/index/spary.vue')['default']
|
Spary: typeof import('./components/index/spary.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
132
src/components/nodeEdit/SchemaField.vue
Normal file
132
src/components/nodeEdit/SchemaField.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, toRaw} from 'vue';
|
||||||
|
import { z, ZodObject, ZodString, ZodNumber, ZodBoolean, ZodEnum, ZodOptional } from 'zod';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SchemaField'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
schema: z.ZodTypeAny;
|
||||||
|
modelValue: any;
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 使用toRaw获取原始schema对象,避免Proxy问题
|
||||||
|
const rawSchema = toRaw(props.schema);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const isOptional = computed(() => rawSchema.optional());
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeName = computed(() => {
|
||||||
|
if (rawSchema instanceof ZodObject) return 'ZodObject';
|
||||||
|
if (rawSchema instanceof ZodString) return 'ZodString';
|
||||||
|
if (rawSchema instanceof ZodNumber) return 'ZodNumber';
|
||||||
|
if (rawSchema instanceof ZodBoolean) return 'ZodBoolean';
|
||||||
|
if (rawSchema instanceof ZodEnum) return 'ZodEnum';
|
||||||
|
if (rawSchema instanceof ZodOptional) return 'ZodOptional';
|
||||||
|
// Fallback for unknown types or types not handled explicitly
|
||||||
|
return 'Unknown';
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectValue = computed(() => value.value as Record<string, any> | null);
|
||||||
|
|
||||||
|
const objectSchema = computed(() => {
|
||||||
|
const schema = rawSchema;
|
||||||
|
if (schema instanceof ZodOptional) {
|
||||||
|
const unwrapped = schema.unwrap();
|
||||||
|
if (unwrapped instanceof ZodObject) {
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schema instanceof ZodObject) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schema-field">
|
||||||
|
<template v-if="typeName === 'ZodString'">
|
||||||
|
<v-text-field
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodNumber'">
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="value"
|
||||||
|
type="number"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodBoolean'">
|
||||||
|
<v-switch
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
color="primary"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-switch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodEnum'">
|
||||||
|
<v-select
|
||||||
|
v-model="value"
|
||||||
|
:items="(rawSchema as ZodEnum<any>).options"
|
||||||
|
:label="label"
|
||||||
|
:hint="isOptional ? 'Optional' : ''"
|
||||||
|
persistent-hint
|
||||||
|
></v-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="objectSchema">
|
||||||
|
<v-card variant="outlined" class="pa-4 mb-4">
|
||||||
|
<div class="text-subtitle-1 font-weight-medium">{{ label }}</div>
|
||||||
|
<div v-for="(field, key) in objectSchema.shape" :key="key">
|
||||||
|
<SchemaField
|
||||||
|
:schema="field"
|
||||||
|
:label="key.toString()"
|
||||||
|
:model-value="objectValue ? objectValue[key] : undefined"
|
||||||
|
@update:modelValue="newValue => {
|
||||||
|
const newObjectValue = { ...value.value };
|
||||||
|
newObjectValue[key] = newValue;
|
||||||
|
value.value = newObjectValue;
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="typeName === 'ZodOptional'">
|
||||||
|
<SchemaField
|
||||||
|
:schema="(rawSchema as ZodOptional<any>).unwrap()"
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<p class="text-caption">Unsupported type: {{ typeName }} for {{label}}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.schema-field {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,7 @@ import {nodeRepository} from "@/entities/node.ts";
|
|||||||
import {notify} from "@/components/notify/notifyStore.ts";
|
import {notify} from "@/components/notify/notifyStore.ts";
|
||||||
import {ConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
import {ConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
||||||
import {CoreTypes} from "@/utils/core/CoreDef.ts";
|
import {CoreTypes} from "@/utils/core/CoreDef.ts";
|
||||||
|
import NodeConfigurator from "@/components/nodeEdit/nodeConfigurator.vue";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ const props = defineProps<{ groupId: string }>()
|
|||||||
const allGroups = ref<Group[]>([])
|
const allGroups = ref<Group[]>([])
|
||||||
const selectedGroupId = ref<number | null>(null)
|
const selectedGroupId = ref<number | null>(null)
|
||||||
const configurationSchema= ref<ConfigurationSchema | null>(null)
|
const configurationSchema= ref<ConfigurationSchema | null>(null)
|
||||||
|
const nodeConfig = ref<object | null>(null)
|
||||||
|
|
||||||
const loadGroups = async () => {
|
const loadGroups = async () => {
|
||||||
allGroups.value = await groupRepository.findAll()
|
allGroups.value = await groupRepository.findAll()
|
||||||
@@ -24,33 +26,47 @@ const loadGroups = async () => {
|
|||||||
onMounted(loadGroups)
|
onMounted(loadGroups)
|
||||||
|
|
||||||
const nodeAlias = ref('')
|
const nodeAlias = ref('')
|
||||||
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
|
return !nodeAlias.value || nodeAlias.value.length < 3 || nodeAlias.value.length > 15 || isAdding.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onConfigurationSchemaChange(value: any) {
|
||||||
|
console.log('Configuration schema changed:', value);
|
||||||
|
// 确保设置的值是正确的对象结构
|
||||||
|
configurationSchema.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
async function addNode() {
|
async function addNode() {
|
||||||
if (!isAddDisabled.value && selectedGroupId.value !== null) {
|
if (!isAddDisabled.value && selectedGroupId.value !== null) {
|
||||||
isAdding.value = true
|
isAdding.value = true
|
||||||
|
try {
|
||||||
await nodeRepository.insert({
|
await nodeRepository.insert({
|
||||||
created_at: null,
|
created_at: null,
|
||||||
id: null,
|
id: null,
|
||||||
updated_at: null,
|
updated_at: null,
|
||||||
alias: nodeAlias.value,
|
alias: nodeAlias.value,
|
||||||
arguments: nodeArguments.value,
|
arguments: nodeConfig.value || {},
|
||||||
group_id: selectedGroupId.value
|
group_id: selectedGroupId.value
|
||||||
})
|
})
|
||||||
notify(t('addNode.nodeAddedSuccess'))
|
notify(t('addNode.nodeAddedSuccess'))
|
||||||
|
// Reset form after successful submission
|
||||||
|
nodeAlias.value = ''
|
||||||
|
nodeConfig.value = null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding node:', error)
|
||||||
|
notify(t('addNode.nodeAddedError'))
|
||||||
|
} finally {
|
||||||
isAdding.value = false
|
isAdding.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</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="addNode">
|
||||||
<v-select
|
<v-select
|
||||||
v-model="selectedGroupId"
|
v-model="selectedGroupId"
|
||||||
:label="$t('addNode.group')"
|
:label="$t('addNode.group')"
|
||||||
@@ -70,16 +86,19 @@ async function addNode() {
|
|||||||
:label="$t('addNode.nodeType')"
|
:label="$t('addNode.nodeType')"
|
||||||
:items="CoreTypes"
|
:items="CoreTypes"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
variant="solo">
|
return-object
|
||||||
|
variant="solo"
|
||||||
|
@update:modelValue="onConfigurationSchemaChange">
|
||||||
</v-select>
|
</v-select>
|
||||||
|
|
||||||
|
<node-configurator v-if="configurationSchema" :configuration-schema="configurationSchema" v-model="nodeConfig"/>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
type="submit"
|
type="submit"
|
||||||
block
|
block
|
||||||
:disabled="isAddDisabled"
|
:disabled="isAddDisabled"
|
||||||
:loading="isAdding"
|
:loading="isAdding">
|
||||||
@click="addNode">
|
|
||||||
{{ $t('addNode.addNodeButton') }}
|
{{ $t('addNode.addNodeButton') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|||||||
@@ -1,14 +1,58 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
import { watch, ref, computed, toRaw} from 'vue';
|
||||||
|
import { ConfigurationSchema } from "@/utils/core/configurator/schema/schema.ts";
|
||||||
|
import SchemaField from './SchemaField.vue';
|
||||||
|
|
||||||
const props = defineProps<{ configurationSchema: ConfigurationSchema }>()
|
const props = defineProps<{
|
||||||
|
configurationSchema: ConfigurationSchema,
|
||||||
|
modelValue: object | null, // Allow null for initial state
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
// Use a ref instead of a computed property with setter to avoid proxy issues
|
||||||
|
const localConfig = ref<Record<string, any>>(props.modelValue || {});
|
||||||
|
|
||||||
|
// Watch for changes in the configurationSchema prop to reset internalConfig
|
||||||
|
watch(() => props.configurationSchema, () => {
|
||||||
|
// Reset localConfig to an empty object when schema changes
|
||||||
|
localConfig.value = {};
|
||||||
|
emit('update:modelValue', {});
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Watch for changes in modelValue prop to update localConfig
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
localConfig.value = newVal || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in localConfig to emit updates
|
||||||
|
watch(localConfig, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal);
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 使用toRaw来避免响应式代理问题
|
||||||
|
const mainSchema = computed(() => {
|
||||||
|
const rawSchema = toRaw(props.configurationSchema);
|
||||||
|
return rawSchema.schema;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<v-card class="pa-4" variant="tonal">
|
||||||
|
<div v-for="(field, key) in mainSchema.shape" :key="key.toString()">
|
||||||
|
<SchemaField
|
||||||
|
:schema="field"
|
||||||
|
:label="key.toString()"
|
||||||
|
:model-value="localConfig[key.toString()]"
|
||||||
|
@update:modelValue="newValue => {
|
||||||
|
const newConfig = { ...localConfig.value };
|
||||||
|
newConfig[key.toString()] = newValue;
|
||||||
|
localConfig.value = newConfig;
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"arguments": "Arguments",
|
"arguments": "Arguments",
|
||||||
"addGroupButton": "Add",
|
"addGroupButton": "Add",
|
||||||
"groupExists": "Group already exists.",
|
"groupExists": "Group already exists.",
|
||||||
"groupAdded": "Group added."
|
"groupAdded": "Group added.",
|
||||||
|
"nodeType": "Node Type"
|
||||||
},
|
},
|
||||||
"addNode": {
|
"addNode": {
|
||||||
"title": "Add Node",
|
"title": "Add Node",
|
||||||
@@ -40,7 +41,8 @@
|
|||||||
"nodeAlias": "Node alias",
|
"nodeAlias": "Node alias",
|
||||||
"arguments": "Arguments",
|
"arguments": "Arguments",
|
||||||
"addNodeButton": "Add",
|
"addNodeButton": "Add",
|
||||||
"nodeAddedSuccess": "Node added successfully"
|
"nodeAddedSuccess": "Node added successfully",
|
||||||
|
"nodeAddedError": "Error adding node"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@
|
|||||||
"nodeAlias": "节点别名",
|
"nodeAlias": "节点别名",
|
||||||
"arguments": "参数",
|
"arguments": "参数",
|
||||||
"addNodeButton": "添加",
|
"addNodeButton": "添加",
|
||||||
"nodeAddedSuccess": "节点添加成功"
|
"nodeAddedSuccess": "节点添加成功",
|
||||||
|
"nodeAddedError": "添加节点时出错",
|
||||||
|
"nodeType": "节点类型"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {XraySchema} from "@/utils/core/configurator/schema/xray.schema.ts";
|
import {XraySchema} from "@/utils/core/configurator/schema/xray.schema.ts";
|
||||||
import {ConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
import {createConfigurationSchema} from "@/utils/core/configurator/schema/schema.ts";
|
||||||
import {V2flySchema} from "@/utils/core/configurator/schema/v2fly.schema.ts";
|
import {V2flySchema} from "@/utils/core/configurator/schema/v2fly.schema.ts";
|
||||||
|
|
||||||
export const CoreTypes = [
|
export const CoreTypes = [
|
||||||
new ConfigurationSchema("xray", XraySchema),
|
createConfigurationSchema("xray", XraySchema),
|
||||||
new ConfigurationSchema("v2fly", V2flySchema),
|
createConfigurationSchema("v2fly", V2flySchema),
|
||||||
]
|
]
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import {ZodObject} from "zod";
|
import {ZodObject} from "zod";
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
export class ConfigurationSchema {
|
// Use a simple plain object type to avoid Proxy issues
|
||||||
constructor(
|
export type ConfigurationSchema = {
|
||||||
public readonly name: string,
|
name: string;
|
||||||
public readonly schema: ZodObject<any>
|
schema: ZodObject<any>;
|
||||||
) {
|
};
|
||||||
}
|
|
||||||
|
export function createConfigurationSchema(name: string, schema: ZodObject<any>): ConfigurationSchema {
|
||||||
|
// Use markRaw to mark the schema object to prevent Vue from converting it to reactive, thus avoiding Proxy issues
|
||||||
|
return { name, schema: markRaw(schema) };
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user