接入M002V4订单任务多卡前端
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="v4-field-renderer">
|
||||
<label
|
||||
v-for="field in fields"
|
||||
:key="fieldKey(field)"
|
||||
class="v4-field"
|
||||
:class="{ 'v4-field--readonly': !isEditable(field) }"
|
||||
>
|
||||
<span class="v4-field__label">
|
||||
{{ field.display_name }}
|
||||
<sup v-if="field.required">*</sup>
|
||||
<em v-if="!isEditable(field)">{{ t('taskV4.field.readonly') }}</em>
|
||||
</span>
|
||||
|
||||
<select
|
||||
v-if="isEditable(field) && isSelectField(field)"
|
||||
class="v4-field__control"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@change="updateField(field, $event)"
|
||||
>
|
||||
<option value="">{{ t('taskV4.field.selectPlaceholder') }}</option>
|
||||
<option
|
||||
v-for="option in fieldOptions(field)"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isDateField(field)"
|
||||
class="v4-field__control"
|
||||
type="date"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field) && isNumberField(field)"
|
||||
class="v4-field__control"
|
||||
type="number"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<textarea
|
||||
v-else-if="isEditable(field) && isTextAreaField(field)"
|
||||
class="v4-field__control v4-field__control--textarea"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
rows="3"
|
||||
@input="updateField(field, $event)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-else-if="isEditable(field)"
|
||||
class="v4-field__control"
|
||||
type="text"
|
||||
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
||||
:value="fieldValue(field)"
|
||||
@input="updateField(field, $event)"
|
||||
>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="v4-field__static"
|
||||
>
|
||||
{{ stringifyV4Value(fieldValue(field)) }}
|
||||
</span>
|
||||
|
||||
<small
|
||||
v-if="field.control_hint"
|
||||
class="v4-field__hint"
|
||||
>{{ field.control_hint }}</small>
|
||||
<small
|
||||
v-for="error in visibleFieldErrors(field)"
|
||||
:key="error"
|
||||
class="v4-field__error"
|
||||
>{{ error }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ReservationRecord, ReservationV4TaskCardFieldResult } from '@/types/reservation'
|
||||
import {
|
||||
isReservationV4EditableField,
|
||||
readV4FieldValue,
|
||||
reservationV4FieldKey,
|
||||
staticReservationV4Options,
|
||||
stringifyReservationV4SafeDisplayValue,
|
||||
} from '@/utils/reservationV4FieldRules'
|
||||
|
||||
const props = defineProps<{
|
||||
fields: ReservationV4TaskCardFieldResult[]
|
||||
modelValue: ReservationRecord
|
||||
readOnly: boolean
|
||||
validationErrors?: Record<string, string>
|
||||
editableKeys?: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ReservationRecord]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function fieldKey(field: ReservationV4TaskCardFieldResult): string {
|
||||
return reservationV4FieldKey(field)
|
||||
}
|
||||
|
||||
function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
const key = reservationV4FieldKey(field)
|
||||
return !props.readOnly &&
|
||||
(!props.editableKeys || props.editableKeys.includes(key)) &&
|
||||
isReservationV4EditableField(field)
|
||||
}
|
||||
|
||||
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'SELECT' && fieldOptions(field).length > 0
|
||||
}
|
||||
|
||||
function isDateField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'DATE'
|
||||
}
|
||||
|
||||
function isNumberField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'NUMBER'
|
||||
}
|
||||
|
||||
function isTextAreaField(field: ReservationV4TaskCardFieldResult): boolean {
|
||||
return normalizeControlType(field.control_type) === 'TEXTAREA'
|
||||
}
|
||||
|
||||
function fieldOptions(field: ReservationV4TaskCardFieldResult): Array<{ value: string; label: string }> {
|
||||
return staticReservationV4Options(field.options_source)
|
||||
}
|
||||
|
||||
function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
||||
const value = readV4FieldValue(field, props.modelValue)
|
||||
if (!isEditable(field)) {
|
||||
return stringifyV4Value(value)
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return stringifyV4Value(value)
|
||||
}
|
||||
|
||||
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
||||
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
[reservationV4FieldKey(field)]: target.value,
|
||||
})
|
||||
}
|
||||
|
||||
function visibleFieldErrors(field: ReservationV4TaskCardFieldResult): string[] {
|
||||
return [
|
||||
...(field.validation_errors ?? []),
|
||||
fieldError(field),
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
function fieldError(field: ReservationV4TaskCardFieldResult): string {
|
||||
return props.validationErrors?.[reservationV4FieldKey(field)] ?? ''
|
||||
}
|
||||
|
||||
function stringifyV4Value(value: unknown): string {
|
||||
return stringifyReservationV4SafeDisplayValue(
|
||||
value,
|
||||
t('taskV4.field.empty'),
|
||||
t('taskV4.field.hidden'),
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeControlType(value: string | null): string {
|
||||
return value?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v4-field-renderer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.v4-field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.v4-field__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.v4-field__label sup {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.v4-field__label em {
|
||||
border-radius: 999px;
|
||||
background: var(--th-color-slate-100);
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.v4-field__control,
|
||||
.v4-field__static {
|
||||
min-height: 38px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
font: inherit;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.v4-field__control--textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.v4-field__control[aria-invalid='true'] {
|
||||
border-color: var(--th-color-danger);
|
||||
box-shadow: 0 0 0 3px var(--th-color-danger-bg);
|
||||
}
|
||||
|
||||
.v4-field__static {
|
||||
display: block;
|
||||
min-height: 38px;
|
||||
background: var(--th-color-slate-50);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.v4-field__hint {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.v4-field__error {
|
||||
color: var(--th-color-danger);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.v4-field-renderer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user