refactor: 将IP与端口输入框单独抽象成独立文件方便调用

This commit is contained in:
SikongJueluo 2025-07-07 20:24:34 +08:00
parent da7b3f4a4b
commit ff7f7b5a76
No known key found for this signature in database
5 changed files with 266 additions and 53 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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'

View File

@ -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);
}); });
// //