接入M002V4订单任务多卡前端

This commit is contained in:
andy
2026-07-19 13:48:07 +07:00
parent 13364e7edf
commit cae169cefd
19 changed files with 4084 additions and 343 deletions

View File

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