x-control-panel/web-ui/vue-app/src/components/PropertyWidget.vue

308 lines
8.2 KiB
Vue

<template>
<div class="property-widget">
<!-- Boolean type - checkbox -->
<v-checkbox
v-if="descriptor?.propertyType === 'Boolean'"
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
@update:model-value="$emit('update:modelValue', $event)"
/>
<!-- Array type - multiple widgets -->
<div v-else-if="descriptor?.array">
<div
v-for="(item, index) in modelValue || []"
:key="String(index)"
class="d-flex align-center mb-2"
>
<PropertyWidget
:model-value="item"
:descriptor="{ ...descriptor, array: false }"
:disabled="disabled"
class="flex-grow-1"
@update:model-value="updateArrayItem(Number(index), $event)"
/>
<div class="d-flex flex-column ml-2">
<v-btn
icon="mdi-chevron-up"
size="x-small"
variant="text"
:disabled="disabled || index === 0"
@click="moveUp(Number(index))"
/>
<v-btn
icon="mdi-chevron-down"
size="x-small"
variant="text"
:disabled="disabled || index === (modelValue?.length || 0) - 1"
@click="moveDown(Number(index))"
/>
</div>
<v-btn
icon="mdi-close"
size="x-small"
variant="text"
color="error"
:disabled="disabled"
class="ml-1"
@click="removeItem(Number(index))"
/>
</div>
<v-btn
prepend-icon="mdi-plus"
size="small"
variant="outlined"
:disabled="disabled"
@click="addItem"
>
Добавить
</v-btn>
</div>
<!-- Text type -->
<v-textarea
v-if="descriptor?.propertyType === 'Text' && stringConstraint?.multiline"
:model-value="modelValue"
:disabled="disabled"
:rules="textValidationRules"
density="compact"
variant="outlined"
:maxlength="stringConstraint?.maxLength"
rows="3"
auto-grow
@update:model-value="$emit('update:modelValue', $event)"
/>
<v-text-field
v-else-if="descriptor?.propertyType === 'Text'"
:model-value="modelValue"
:disabled="disabled"
:rules="textValidationRules"
density="compact"
variant="outlined"
:maxlength="stringConstraint?.maxLength"
@update:model-value="$emit('update:modelValue', $event)"
/>
<!-- Number type -->
<div v-else-if="descriptor?.propertyType === 'Number'">
<!-- Range slider for numbers with step > 0 -->
<v-slider
v-if="useSlider"
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
:min="numberConstraint?.min"
:max="numberConstraint?.max"
:step="numberConstraint?.step"
@update:model-value="$emit('update:modelValue', $event)"
>
<template #append>
<v-text-field
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
style="width: 100px"
type="number"
:min="numberConstraint?.min"
:max="numberConstraint?.max"
:step="numberConstraint?.step"
@update:model-value="$emit('update:modelValue', $event)"
/>
</template>
</v-slider>
<!-- Text field for other numbers -->
<v-text-field
v-else
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
type="number"
:min="numberConstraint?.min"
:max="numberConstraint?.max"
:step="numberConstraint?.step || (numberConstraint?.integer ? 1 : 0.1)"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
<!-- ValueList type - dropdown -->
<v-select
v-else-if="descriptor?.propertyType === 'ValueList'"
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
:items="valueListConstraint?.values || []"
item-title="label"
item-value="value"
@update:model-value="$emit('update:modelValue', $event)"
/>
<!-- JSON type - textarea -->
<v-textarea
v-else-if="descriptor?.propertyType === 'JSON'"
:model-value="
typeof modelValue === 'object' ? JSON.stringify(modelValue, null, 2) : modelValue
"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
rows="5"
@update:model-value="updateJsonValue"
/>
<!-- Fallback for unknown types -->
<v-text-field
v-else
:model-value="modelValue"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
placeholder="Unknown type"
@update:model-value="$emit('update:modelValue', $event)"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type {
PropertyDescriptor,
NumberConstraint,
ValueListConstraint,
StringConstraint
} from '@/generated/RpcClient'
interface Props {
modelValue: any
descriptor?: PropertyDescriptor
disabled?: boolean
}
interface Emits {
(e: 'update:modelValue', value: any): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const stringConstraint = computed((): StringConstraint | undefined => {
return props.descriptor?.constraints?.find(
(c) => c.type === 'StringConstraint'
) as StringConstraint
})
const numberConstraint = computed((): NumberConstraint | undefined => {
return props.descriptor?.constraints?.find(
(c) => c.type === 'NumberConstraint'
) as NumberConstraint
})
const valueListConstraint = computed((): ValueListConstraint | undefined => {
return props.descriptor?.constraints?.find(
(c) => c.type === 'ValueListConstraint'
) as ValueListConstraint
})
const useSlider = computed((): boolean => {
const constraint = numberConstraint.value
return constraint ? constraint.step > 0 : false
})
const textValidationRules = computed(() => {
const rules: any[] = []
if (stringConstraint.value?.regexp) {
const regex = new RegExp(stringConstraint.value.regexp)
rules.push((value: string) => {
if (!value) return true // Allow empty values
return regex.test(value) || 'Значение не соответствует требуемому формату'
})
}
return rules
})
const updateJsonValue = (newValue: string) => {
try {
if (!newValue.trim()) {
emit('update:modelValue', null)
return
}
const parsed = JSON.parse(newValue)
emit('update:modelValue', parsed)
} catch (e) {
// Keep invalid JSON as string for now
emit('update:modelValue', newValue)
}
}
const updateArrayItem = (index: number, newValue: any) => {
const newArray = [...(props.modelValue || [])]
newArray[index] = newValue
emit('update:modelValue', newArray)
}
const addItem = () => {
const newArray = [...(props.modelValue || [])]
// Determine default value based on property type
let defaultValue: any = ''
if (props.descriptor?.propertyType === 'Boolean') {
defaultValue = false
} else if (props.descriptor?.propertyType === 'Number') {
defaultValue = 0
} else if (props.descriptor?.propertyType === 'JSON') {
defaultValue = {}
}
newArray.push(defaultValue)
emit('update:modelValue', newArray)
}
const removeItem = (index: number) => {
const newArray = [...(props.modelValue || [])]
newArray.splice(index, 1)
emit('update:modelValue', newArray)
}
const moveUp = (index: number) => {
if (index === 0) return
const newArray = [...(props.modelValue || [])]
const temp = newArray[index]
newArray[index] = newArray[index - 1]
newArray[index - 1] = temp
emit('update:modelValue', newArray)
}
const moveDown = (index: number) => {
const array = props.modelValue || []
if (index === array.length - 1) return
const newArray = [...array]
const temp = newArray[index]
newArray[index] = newArray[index + 1]
newArray[index + 1] = temp
emit('update:modelValue', newArray)
}
</script>
<style scoped>
.property-widget {
width: 100%;
}
</style>