Добавил редактор property. Дополнил генерацию spec.

This commit is contained in:
kirillius 2026-02-01 00:22:50 +03:00
parent 3d6b3d8059
commit db88b2cf28
14 changed files with 768 additions and 48 deletions

View File

@ -118,14 +118,13 @@ public class SpecGenerator {
continue;
}
var classAnnotation = type.getAnnotation(GenerateApiSpec.class);
if (classAnnotation == null || !type.isInterface()) {
if (classAnnotation == null) {
continue;
}
for (var aClass : classAnnotation.directInheritors()) {
if (!types.contains(aClass)) {
types.add(aClass);
typesQueue.add(aClass);
typesQueue.add(registerType(aClass));
}
}
@ -135,8 +134,8 @@ public class SpecGenerator {
var parents = typeDescriptor.putArray("parents");
for (var aClass : type.getInterfaces()) {
if (!types.contains(aClass)) {
types.add(aClass);
typesQueue.add(aClass);
typesQueue.add(registerType(aClass));
}
if (!aClass.isAnnotationPresent(GenerateApiSpec.class)) {
continue;
@ -145,6 +144,29 @@ public class SpecGenerator {
}
var fields = typeDescriptor.putArray("fields");
if (!type.isInterface()) {
for (var field : type.getDeclaredFields()) {
var annotation = field.getAnnotation(GenerateApiSpec.class);
if (annotation == null) {
continue;
}
var returnType = annotation.type();
if (returnType == void.class) {
returnType = field.getType();
}
if (!types.contains(returnType)) {
typesQueue.put(registerType(returnType));
}
var fieldDescriptor = fields.addObject();
fieldDescriptor.put("name", annotation.alias().isEmpty() ? field.getName() : annotation.alias());
fieldDescriptor.put("type", getTypeName(returnType));
}
}
for (var method : type.getMethods()) {
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);
if (methodAnnotation == null) {
@ -157,8 +179,7 @@ public class SpecGenerator {
}
if (!types.contains(returnType)) {
types.add(returnType);
typesQueue.put(returnType);
typesQueue.put(registerType(returnType));
}
var methodName = method.getName();
@ -195,18 +216,20 @@ public class SpecGenerator {
}
}
private void registerType(Class<?> type) {
private Class<?> registerType(Class<?> type) {
if (type.isArray()) {
types.add(type.getComponentType());
return type.getComponentType();
} else {
types.add(type);
return type;
}
}
private String getTypeName(Class<?> type) {
if (type == null) return "unknown";
if (type == boolean.class) return "boolean";
if (type == int.class || type == long.class) return "number";
if (type == int.class || type == long.class || type == double.class || type == float.class) return "number";
if (type == String.class) return "string";
if (type == void.class || type == Void.class) return "void";

View File

@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface GenerateApiSpec {
String alias() default "";

View File

@ -2,12 +2,13 @@ package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import ru.kirillius.XCP.Properties.Constraint;
import ru.kirillius.XCP.Properties.Constraints;
import ru.kirillius.XCP.Properties.PropertyType;
@GenerateApiSpec
public interface PropertyDescriptor extends PersistenceEntity {
@GenerateApiSpec
@GenerateApiSpec(type = Constraint[].class)
Constraints getConstraints();
void setConstraints(Constraints constraints);

View File

@ -10,7 +10,8 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
})
public interface Constraint {
@JsonProperty(value = "type")
default String type() {
@GenerateApiSpec
default String getType() {
return getClass().getSimpleName();
}
}

View File

@ -12,11 +12,15 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
@NoArgsConstructor
public final class NumberConstraint implements Constraint {
@JsonProperty
@GenerateApiSpec
private double min;
@JsonProperty
@GenerateApiSpec
private double max;
@JsonProperty
@GenerateApiSpec
private double step;
@JsonProperty
@GenerateApiSpec
private boolean integer;
}

View File

@ -12,5 +12,14 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec;
@NoArgsConstructor
public final class StringConstraint implements Constraint {
@JsonProperty
@GenerateApiSpec
private int maxLength;
@JsonProperty
@GenerateApiSpec
private boolean multiline;
@JsonProperty
@GenerateApiSpec
private String regexp;
}

View File

@ -3,6 +3,7 @@ package ru.kirillius.XCP.Properties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import tools.jackson.databind.node.ArrayNode;
import java.util.Collection;
@ -14,5 +15,6 @@ import java.util.Collection;
@NoArgsConstructor
public final class ValueListConstraint implements Constraint {
@JsonProperty
@GenerateApiSpec(type = ArrayNode.class)
private Collection<String> values;
}

View File

@ -9,5 +9,22 @@
"uuid": "00000000-0000-0000-0000-000000000000",
"passwordHash": "$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ"
}
},
{
"type": "PropertyDescriptor",
"entity": {
"constraints": [
{
"type": "StringConstraint",
"maxLength": 15,
"multiline": false,
"regexp": "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
}
],
"array": false,
"propertyType": "Text",
"name": "host.ip",
"uuid": "00bd0f8a-e9d8-4789-83e5-d2f2eb94f876"
}
}
]

View File

@ -63,7 +63,8 @@ public final class RepositoryServiceImpl implements RepositoryService {
InputRepositoryImpl.class,
OutputRepositoryImpl.class,
TagRepositoryImpl.class,
UserRepositoryImpl.class
UserRepositoryImpl.class,
PropertyDescriptorRepositoryImpl.class
);
configuration = new Configuration();
configuration.configure();

View File

@ -9,6 +9,7 @@ import ru.kirillius.XCP.Commons.Context;
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet;
import ru.kirillius.XCP.RPC.Services.Auth;
import ru.kirillius.XCP.RPC.Services.Profile;
import ru.kirillius.XCP.RPC.Services.Properties;
import ru.kirillius.XCP.RPC.Services.Users;
import ru.kirillius.XCP.Services.ServiceLoadPriority;
import ru.kirillius.XCP.Services.WebService;
@ -30,7 +31,8 @@ public class WebServiceImpl implements WebService {
jsonRpc.registerRpcService(
Users.class,
Auth.class,
Profile.class
Profile.class,
Properties.class
);
var config = context.getConfig();
server = new Server(new InetSocketAddress(config.getHost(), config.getHttpPort()));

View File

@ -30,6 +30,7 @@ interface ApiModule {
interface ApiType {
name: string
type: 'class' | 'enum'
parents?: string[]
fields?: Array<{
name: string
type: string
@ -77,8 +78,12 @@ ${values.map((value) => ` ${value} = "${value}"`).join(',\n')}
}`
} else if (apiType.type === 'class') {
const fields = apiType.fields || []
const fieldDefinitions = fields.map((field) => ` ${field.name}: ${field.type};`).join('\n')
return `export interface ${apiType.name} {
const fieldDefinitions = fields
.map((field) => ` ${field.name}: ${field.type === 'array' ? '[]' : field.type};`)
.join('\n')
const parents = apiType.parents || []
const extendsClause = parents.length > 0 ? ` extends ${parents.join(', ')}` : ''
return `export interface ${apiType.name}${extendsClause} {
${fieldDefinitions}
}`
}

View File

@ -0,0 +1,369 @@
<template>
<v-card elevation="2" class="property-editor">
<v-card-title class="text-h6"> Свойства </v-card-title>
<v-card-text>
<v-form ref="propertyForm" v-model="isFormValid">
<v-table density="compact">
<thead>
<tr>
<th style="width: 35%">Свойство</th>
<th style="width: 55%">Значение</th>
<th style="width: 10%"></th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in modelValue" :key="key">
<td class="align-middle">
<div class="font-weight-medium">{{ key }}</div>
</td>
<td>
<!-- Edit mode - show widgets -->
<template v-if="!disabled">
<PropertyWidget
v-if="getDescriptor(key)"
v-model="modelValue[key]"
:descriptor="getDescriptor(key)"
:disabled="disabled"
@update:model-value="updateValue(key, $event)"
/>
<!-- Fallback for unknown properties -->
<v-text-field
v-else
v-model="modelValue[key]"
:disabled="disabled"
hide-details
density="compact"
variant="outlined"
placeholder="Введите значение"
@update:model-value="updateValue(key, $event)"
/>
</template>
<!-- View mode - show display values -->
<template v-else>
<div v-if="getDescriptor(key)">
<span v-if="getDescriptor(key)?.propertyType === 'Boolean'">
<v-icon
:icon="modelValue[key] ? 'mdi-check' : 'mdi-close'"
:color="modelValue[key] ? 'success' : 'error'"
/>
</span>
<span v-else-if="getDescriptor(key)?.propertyType === 'JSON'">
<div class="json-display" style="max-height: 100px; overflow-y: auto">
{{ formatJsonDisplay(modelValue[key]) }}
</div>
</span>
<span v-else>
{{ formatValueDisplay(key) }}
</span>
</div>
<div v-else>
{{ formatValueDisplay(key) }}
</div>
</template>
</td>
<td class="text-center">
<v-btn
v-if="!disabled"
icon="mdi-close"
size="x-small"
variant="text"
color="error"
@click="removeProperty(key)"
/>
</td>
</tr>
<!-- Add new property row -->
<tr v-if="!disabled">
<td class="align-middle">
<v-combobox
v-model="propertyInput"
:items="availableForCombobox"
:error-messages="propertyInputError"
item-title="displayName"
item-value="name"
placeholder="Имя нового свойства"
hide-details
density="compact"
variant="outlined"
@update:model-value="onPropertyInputChange"
/>
</td>
<td class="align-middle">
<v-btn
color="primary"
:disabled="!getInputValue().trim() || !!propertyInputError"
size="small"
@click="addPropertyFromInput"
>
Добавить
</v-btn>
</td>
<td class="text-center align-middle">
<!-- Empty cell for actions column -->
</td>
</tr>
</tbody>
</v-table>
<v-alert v-if="isLoading" type="info" variant="tonal" class="mt-4">
Загрузка дескрипторов свойств...
</v-alert>
<v-alert v-if="error" type="error" variant="tonal" class="mt-4">
{{ error }}
</v-alert>
</v-form>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
import type {
PropertyDescriptor,
NumberConstraint,
ValueListConstraint,
StringConstraint
} from '@/generated/RpcClient'
import PropertyWidget from '@/components/PropertyWidget.vue'
interface Props {
modelValue: Record<string, any>
disabled?: boolean
}
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const propertyForm = ref()
const isFormValid = ref(false)
const isLoading = ref(false)
const error = ref<string | null>(null)
const descriptors = ref<PropertyDescriptor[]>([])
const propertyInput = ref<string>('')
const propertyInputError = ref<string>('')
const hasPropertyInputError = computed(() => {
return !!propertyInputError.value
})
const availableForCombobox = computed(() => {
const existingKeys = Object.keys(props.modelValue || {})
return availableDescriptors.value.filter((d) => !existingKeys.includes(d.name))
})
const buttonDisabled = computed(() => {
const hasText = propertyInputText.value && propertyInputText.value.trim().length > 0
const hasError = !!propertyInputError.value && propertyInputError.value.trim().length > 0
console.log('Debug button:', {
text: propertyInputText.value,
hasText: hasText,
error: propertyInputError.value,
hasError: hasError,
disabled: !hasText || hasError
})
return !hasText || hasError
})
const getDescriptor = (key: string): PropertyDescriptor | undefined => {
return descriptors.value.find((d) => d.name === key)
}
const availableDescriptors = computed(() => {
const existingKeys = Object.keys(props.modelValue || {})
return descriptors.value
.filter((d) => !existingKeys.includes(d.name))
.map((d) => ({
...d,
displayName: d.name
}))
.sort((a, b) => a.name.localeCompare(b.name))
})
const getInputValue = (): string => {
if (typeof propertyInput.value === 'object' && propertyInput.value?.name) {
return propertyInput.value.name
}
return String(propertyInput.value || '')
}
const onPropertyInputChange = (newValue: any) => {
propertyInput.value = newValue
validateInput(getInputValue())
}
const validateInput = (input: string) => {
const existingKeys = Object.keys(props.modelValue || {})
const trimmedName = input.trim()
if (!trimmedName) {
propertyInputError.value = ''
return
}
if (existingKeys.includes(trimmedName)) {
propertyInputError.value = 'Свойство с таким именем уже существует'
} else {
propertyInputError.value = ''
}
}
// Watch input for validation
watch(
() => getInputValue(),
(newValue) => {
validateInput(newValue)
}
)
const updateValue = (key: string, newValue: any) => {
const updatedValues = { ...props.modelValue }
updatedValues[key] = newValue
emit('update:modelValue', updatedValues)
}
const addPropertyFromInput = () => {
const propertyName = getInputValue().trim()
if (!propertyName || propertyInputError.value) return
const existingKeys = Object.keys(props.modelValue || {})
if (existingKeys.includes(propertyName)) return
const descriptor = getDescriptor(propertyName)
let defaultValue: any = ''
if (descriptor) {
// Set default values based on property type
switch (descriptor.propertyType) {
case 'Boolean':
defaultValue = false
break
case 'Number':
const numberConstraint = descriptor.constraints?.find((c) => c.type === 'NumberConstraint')
if (numberConstraint) {
defaultValue = numberConstraint.min || 0
} else {
defaultValue = 0
}
break
case 'ValueList':
const valueListConstraint = descriptor.constraints?.find(
(c) => c.type === 'ValueListConstraint'
)
if (valueListConstraint && valueListConstraint.values?.length > 0) {
defaultValue = valueListConstraint.values[0]
} else {
defaultValue = ''
}
break
case 'JSON':
defaultValue = {}
break
case 'Text':
default:
defaultValue = ''
break
}
// Handle arrays
if (descriptor.array) {
defaultValue = [defaultValue]
}
} else {
// Custom property - empty string
defaultValue = ''
}
const updatedValues = { ...props.modelValue }
updatedValues[propertyName] = defaultValue
emit('update:modelValue', updatedValues)
// Reset input
propertyInput.value = ''
}
const removeProperty = (key: string) => {
const updatedValues = { ...props.modelValue }
delete updatedValues[key]
emit('update:modelValue', updatedValues)
}
const formatValueDisplay = (key: string): string => {
const value = props.modelValue[key]
const descriptor = getDescriptor(key)
if (descriptor?.propertyType === 'ValueList') {
const valueListConstraint = descriptor.constraints?.find(
(c) => c.type === 'ValueListConstraint'
)
const option = valueListConstraint?.values?.find((v: any) => v.value === value)
return option?.label || value
}
if (Array.isArray(value)) {
return `[${value.length} элементов]`
}
if (typeof value === 'object' && value !== null) {
return '[Объект]'
}
return String(value)
}
const formatJsonDisplay = (value: any): string => {
if (typeof value === 'object' && value !== null) {
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
return String(value)
}
const loadDescriptors = async () => {
// Import app store dynamically to avoid circular dependency
const { useAppStore } = await import('@/stores/app')
const appStore = useAppStore()
if (!appStore.api) {
error.value = 'API недоступен'
return
}
isLoading.value = true
error.value = null
try {
descriptors.value = await appStore.api.Properties.getAll()
} catch (err) {
console.error('Error loading property descriptors:', err)
error.value = 'Не удалось загрузить дескрипторы свойств'
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadDescriptors()
})
</script>
<style scoped>
.property-editor {
border-radius: 12px;
}
.v-table td {
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,307 @@
<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>

View File

@ -128,34 +128,10 @@
</v-card-text>
</v-card>
<!-- Карта с дополнительной информацией -->
<!-- Редактор свойств пользователя -->
<v-card elevation="2" class="mt-4">
<v-card-text>
<v-list density="compact">
<v-list-item>
<template #prepend>
<v-icon>mdi-fingerprint</v-icon>
</template>
<v-list-item-title>ID пользователя</v-list-item-title>
<v-list-item-subtitle>{{ currentUser?.id }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-identifier</v-icon>
</template>
<v-list-item-title>UUID</v-list-item-title>
<v-list-item-subtitle>{{ currentUser?.uuid }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-shield-account</v-icon>
</template>
<v-list-item-title>Роль</v-list-item-title>
<v-list-item-subtitle>{{ userRole }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<property-editor v-model="userValues" :disabled="!isEditing" />
</v-card-text>
</v-card>
</v-col>
@ -168,6 +144,7 @@
import { ref, computed, onMounted, watch } from 'vue'
import { useAppStore } from '@/stores/app'
import { useNotificationStore } from '@/stores/notification'
import PropertyEditor from '@/components/PropertyEditor.vue'
const appStore = useAppStore()
const { showSuccess, showError } = useNotificationStore()
@ -199,6 +176,9 @@ const profileData = ref({
confirmPassword: ''
})
// Свойства пользователя для редактора
const userValues = ref<Record<string, any>>({})
const originalData = ref({
login: '',
name: '',
@ -227,7 +207,8 @@ const passwordRules = [
]
const confirmPasswordRules = [
(v: string) => !profileData.newPassword || v === profileData.newPassword || 'Пароли не совпадают'
(v: string) =>
!profileData.value.newPassword || v === profileData.value.newPassword || 'Пароли не совпадают'
]
const getUserDisplayName = () => {
@ -239,10 +220,7 @@ const getUserDisplayName = () => {
}
const startEditing = () => {
originalData.value = {
login: profileData.value.login,
name: profileData.value.name
}
originalData.value = { ...profileData.value }
isEditing.value = true
}
@ -272,7 +250,7 @@ const saveProfile = async () => {
const success = await appStore.api.Profile.save(
profileData.value.login,
profileData.value.name,
currentUser.value?.values || {},
userValues.value,
password,
profileData.value.currentPassword
)
@ -312,6 +290,7 @@ const loadProfileData = () => {
confirmPassword: ''
}
originalData.value = { ...profileData.value }
userValues.value = user.values || {}
}
}