refactor: 将IP与端口输入框单独抽象成独立文件方便调用
This commit is contained in:
parent
da7b3f4a4b
commit
ff7f7b5a76
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" v-if="label || icon">
|
||||||
|
<component :is="icon" class="w-4 h-4" v-if="icon" />
|
||||||
|
<span class="label-text" v-if="label">{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
:type="type"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:class="inputClasses"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="handleInput"
|
||||||
|
@blur="handleBlur"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<slot name="suffix"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="label" v-if="error">
|
||||||
|
<span class="label-text-alt text-error">{{ error }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string | number
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
error?: string
|
||||||
|
type?: 'text' | 'number' | 'email' | 'password'
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||||
|
variant?: 'default' | 'bordered' | 'ghost'
|
||||||
|
icon?: any
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
variant: 'bordered'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string | number]
|
||||||
|
'blur': [event: FocusEvent]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const inputClasses = computed(() => {
|
||||||
|
const baseClasses = ['input', 'flex-1']
|
||||||
|
|
||||||
|
// 添加变体样式
|
||||||
|
if (props.variant === 'bordered') baseClasses.push('input-bordered')
|
||||||
|
else if (props.variant === 'ghost') baseClasses.push('input-ghost')
|
||||||
|
|
||||||
|
// 添加尺寸样式
|
||||||
|
if (props.size === 'xs') baseClasses.push('input-xs')
|
||||||
|
else if (props.size === 'sm') baseClasses.push('input-sm')
|
||||||
|
else if (props.size === 'lg') baseClasses.push('input-lg')
|
||||||
|
|
||||||
|
// 添加错误样式
|
||||||
|
if (props.error) baseClasses.push('input-error')
|
||||||
|
|
||||||
|
// 添加状态样式
|
||||||
|
if (props.disabled) baseClasses.push('input-disabled')
|
||||||
|
|
||||||
|
return baseClasses
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
let value: string | number = target.value
|
||||||
|
|
||||||
|
// 如果是数字类型,转换为数字
|
||||||
|
if (props.type === 'number' && value !== '') {
|
||||||
|
value = Number(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = (event: FocusEvent) => {
|
||||||
|
emit('blur', event)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<BaseInputField
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
:placeholder="placeholder || '192.168.1.100'"
|
||||||
|
:error="validationError"
|
||||||
|
:icon="icon || Globe"
|
||||||
|
type="text"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@blur="validateOnBlur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { Globe } from 'lucide-vue-next'
|
||||||
|
import BaseInputField from './BaseInputField.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
icon?: any
|
||||||
|
required?: boolean
|
||||||
|
validateOnBlur?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
label: 'IP 地址',
|
||||||
|
validateOnBlur: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasBlurred = ref(false)
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
// IP地址验证模式
|
||||||
|
const ipSchema = z.string().ip({
|
||||||
|
version: 'v4',
|
||||||
|
message: '请输入有效的IPv4地址'
|
||||||
|
})
|
||||||
|
|
||||||
|
const validationError = computed(() => {
|
||||||
|
// 如果是必填且为空
|
||||||
|
if (props.required && !props.modelValue) {
|
||||||
|
return '请输入IP地址'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
|
||||||
|
if (props.modelValue && (!props.validateOnBlur || hasBlurred.value)) {
|
||||||
|
const result = ipSchema.safeParse(props.modelValue)
|
||||||
|
return result.success ? '' : result.error.errors[0]?.message || '无效的IP地址'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateOnBlur = () => {
|
||||||
|
if (props.validateOnBlur) {
|
||||||
|
hasBlurred.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<BaseInputField
|
||||||
|
v-model="value"
|
||||||
|
:label="label"
|
||||||
|
:placeholder="placeholder || '8080'"
|
||||||
|
:error="validationError"
|
||||||
|
:icon="icon || Network"
|
||||||
|
type="number"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@blur="validateOnBlur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { Network } from 'lucide-vue-next'
|
||||||
|
import BaseInputField from './BaseInputField.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: number
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
icon?: any
|
||||||
|
required?: boolean
|
||||||
|
validateOnBlur?: boolean
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
label: '端口',
|
||||||
|
validateOnBlur: true,
|
||||||
|
min: 1,
|
||||||
|
max: 65535
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasBlurred = ref(false)
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', Number(val))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 端口验证模式
|
||||||
|
const portSchema = computed(() =>
|
||||||
|
z.number()
|
||||||
|
.int('端口必须是整数')
|
||||||
|
.min(props.min, `端口必须大于等于${props.min}`)
|
||||||
|
.max(props.max, `端口必须小于等于${props.max}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const validationError = computed(() => {
|
||||||
|
// 如果是必填且为空
|
||||||
|
if (props.required && (!props.modelValue && props.modelValue !== 0)) {
|
||||||
|
return '请输入端口号'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
|
||||||
|
if ((props.modelValue || props.modelValue === 0) && (!props.validateOnBlur || hasBlurred.value)) {
|
||||||
|
const result = portSchema.value.safeParse(props.modelValue)
|
||||||
|
return result.success ? '' : result.error.errors[0]?.message || '无效的端口号'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateOnBlur = () => {
|
||||||
|
if (props.validateOnBlur) {
|
||||||
|
hasBlurred.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as BaseInputField } from './BaseInputField.vue'
|
||||||
|
export { default as IpInputField } from './IpInputField.vue'
|
||||||
|
export { default as PortInputField } from './PortInputField.vue'
|
|
@ -12,42 +12,18 @@
|
||||||
|
|
||||||
<div class="flex flex-row justify-around gap-4">
|
<div class="flex flex-row justify-around gap-4">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<label class="label">
|
<IpInputField
|
||||||
<Globe class="w-4 h-4" />
|
|
||||||
<span class="label-text">IP 地址</span>
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="192.168.1.100"
|
|
||||||
class="input input-bordered flex-1"
|
|
||||||
v-model="tempConfig.ip"
|
v-model="tempConfig.ip"
|
||||||
:class="{ 'input-error': ipError }"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label class="label" v-if="ipError">
|
|
||||||
<span class="label-text-alt text-error">{{ ipError }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<label class="label">
|
<PortInputField
|
||||||
<Network class="w-4 h-4" />
|
v-model="tempConfig.port"
|
||||||
<span class="label-text">端口</span>
|
required
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="8080"
|
|
||||||
class="input input-bordered flex-1"
|
|
||||||
v-model.number="tempConfig.port"
|
|
||||||
:class="{ 'input-error': portError }"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label class="label" v-if="portError">
|
|
||||||
<span class="label-text-alt text-error">{{ portError }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions justify-end mt-4">
|
||||||
|
@ -91,14 +67,12 @@ import { useStorage } from "@vueuse/core";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
Globe,
|
|
||||||
Network,
|
|
||||||
Save,
|
Save,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
CheckCircle,
|
|
||||||
Activity,
|
Activity,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { WaveformDisplay, generateTestData } from "@/components/Oscilloscope";
|
import { WaveformDisplay, generateTestData } from "@/components/Oscilloscope";
|
||||||
|
import { IpInputField, PortInputField } from "@/components/InputField";
|
||||||
|
|
||||||
// 配置类型定义
|
// 配置类型定义
|
||||||
const configSchema = z.object({
|
const configSchema = z.object({
|
||||||
|
@ -151,25 +125,11 @@ const tempConfig = reactive<OscilloscopeConfig>({
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
|
|
||||||
// 验证错误
|
// 检查配置是否有效 - 简化版本,因为验证现在在组件内部
|
||||||
const ipError = computed(() => {
|
|
||||||
if (!tempConfig.ip) return "";
|
|
||||||
const result = z.string().ip({ version: "v4" }).safeParse(tempConfig.ip);
|
|
||||||
return result.success
|
|
||||||
? ""
|
|
||||||
: result.error.errors[0]?.message || "无效的IP地址";
|
|
||||||
});
|
|
||||||
|
|
||||||
const portError = computed(() => {
|
|
||||||
if (!tempConfig.port && tempConfig.port !== 0) return "";
|
|
||||||
const result = z.number().int().min(1).max(65535).safeParse(tempConfig.port);
|
|
||||||
return result.success ? "" : result.error.errors[0]?.message || "无效的端口";
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查配置是否有效
|
|
||||||
const isValidConfig = computed(() => {
|
const isValidConfig = computed(() => {
|
||||||
const result = configSchema.safeParse(tempConfig);
|
return tempConfig.ip && tempConfig.port &&
|
||||||
return result.success;
|
tempConfig.port >= 1 && tempConfig.port <= 65535 &&
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}$/.test(tempConfig.ip);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查是否有更改
|
// 检查是否有更改
|
||||||
|
|
Loading…
Reference in New Issue