新增分段管理

This commit is contained in:
18086616529
2026-06-23 17:22:40 +08:00
parent a6086f1f8b
commit 88c688c0df
19 changed files with 1882 additions and 1 deletions
@@ -568,6 +568,11 @@ export const reflectionApi = {
? Promise.resolve<CarStyleTypesPayload>({ globalDefault: defaultCarStyleDto(), types: [] })
: get<CarStyleTypesPayload>('/car-style/types'),
/** 车型编码字段元数据(site/track/plan/car 四类字段及默认值)。 */
getCarTypeCoderFields: () => MOCK
? Promise.resolve<CarTypeCoderFieldsRow[]>([])
: get<CarTypeCoderFieldsRow[]>('/car-types/coder-fields'),
getCarStyle: (typeFullName: string) => MOCK
? Promise.resolve(defaultCarStyleDto())
: get<CarStyleDto>(`/car-style/${encodeURIComponent(typeFullName)}`),
@@ -741,6 +746,31 @@ export interface CarStyleTypesPayload {
types: CarStyleTypeRow[]
}
export interface CoderFieldDef {
name: string
typeName: string
defaultValue: unknown
}
export interface CoderFieldGroup {
typeName: string
shortName: string
assemblyName: string
baseTypeName?: string
fields: CoderFieldDef[]
}
export interface CarTypeCoderFieldsRow {
typeName: string
shortName: string
label: string
assemblyName: string
siteFields: CoderFieldGroup
trackFields: CoderFieldGroup
planFields: CoderFieldGroup
carFields: CoderFieldGroup
}
export interface AlarmColorEntry {
key: string
colorArgb: number
@@ -0,0 +1,31 @@
import http from './http'
import type { SimpleFieldBatchPayload, SimpleFieldPayload, SimpleFieldRecord } from '@/types/simpleField'
function q(params?: Record<string, unknown>) {
return { params }
}
export async function listSimpleFields(fieldType?: string, carType?: string, keyword?: string) {
const { data } = await http.get<SimpleFieldRecord[]>('/simple-fields', q({
fieldType,
carType,
q: keyword
}))
return data
}
export async function saveSimpleField(payload: SimpleFieldPayload) {
const { data } = payload.id
? await http.put<SimpleFieldRecord>(`/simple-fields/${payload.id}`, payload)
: await http.post<SimpleFieldRecord>('/simple-fields', payload)
return data
}
export async function deleteSimpleField(id: string) {
await http.delete(`/simple-fields/${id}`)
}
export async function saveSimpleFieldsBatch(payload: SimpleFieldBatchPayload) {
const { data } = await http.post<{ count: number }>('/simple-fields/batch', payload)
return data
}
@@ -0,0 +1,54 @@
<template>
<el-card class="dtp-card" shadow="never">
<template #header>
<div class="dtp-header">
<span class="dtp-title">{{ title }}</span>
<div class="dtp-actions">
<el-input v-if="searchable" v-model="kw" :placeholder="searchPlaceholder" clearable size="small" style="width: 220px" />
<slot name="actions" />
</div>
</div>
</template>
<el-table :data="filtered" stripe size="small" :max-height="maxHeight" border>
<el-table-column v-for="c in columns" :key="c.prop" :prop="c.prop" :label="c.label" :width="c.width" :min-width="c.minWidth">
<template #default="scope">
<slot :name="`col-${c.prop}`" :row="scope.row">
{{ scope.row[c.prop] }}
</slot>
</template>
</el-table-column>
<slot name="extra-columns" />
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Column { prop: string; label: string; width?: number | string; minWidth?: number | string }
const props = defineProps<{
title: string
data: Array<Record<string, unknown>>
columns: Column[]
searchable?: boolean
searchPlaceholder?: string
maxHeight?: number | string
}>()
const kw = ref('')
const filtered = computed(() => {
if (!props.searchable || !kw.value) return props.data
const q = kw.value.trim().toLowerCase()
return props.data.filter((row) =>
Object.values(row).some((v) => String(v ?? '').toLowerCase().includes(q))
)
})
</script>
<style scoped>
.dtp-header { display: flex; justify-content: space-between; align-items: center; }
.dtp-title { font-weight: 600; }
.dtp-actions { display: flex; gap: 8px; align-items: center; }
</style>
@@ -154,7 +154,8 @@ const ADMIN_MENU: MenuItem[] = [
{ path: '/admin/cars', label: '车辆管理', key: 'admin-cars' },
{ path: '/admin/processes', label: '进程管理', key: 'admin-processes' },
{ path: '/admin/scripts', label: '脚本管理', key: 'admin-scripts' },
{ path: '/admin/task-templates', label: '任务编排', key: 'admin-task-templates' }
{ path: '/admin/task-templates', label: '任务编排', key: 'admin-task-templates' },
{ path: '/admin/simple-fields', label: '字段管理', key: 'admin-simple-fields' }
]
},
{
@@ -20,6 +20,7 @@ const PAGES: PageDef[] = [
{ key: 'admin-processes', label: '进程管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-scripts', label: '脚本管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-task-templates', label: '任务编排', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-simple-fields', label: '字段管理', group: '设计与编排', scope: 'Platform' },
{ key: 'admin-config-strategy', label: '调度策略', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-vehicle-hub', label: '车辆运维', group: '平台配置中心', scope: 'Platform' },
{ key: 'admin-config-facility', label: '设备与库位', group: '平台配置中心', scope: 'Platform' },
@@ -37,6 +37,7 @@ const routes: RouteRecordRaw[] = [
{ path: 'processes', name: 'admin-processes', component: () => import('@/views/admin/ProcessPanelView.vue'), meta: { title: '进程管理' } },
{ path: 'scripts', name: 'admin-scripts', component: () => import('@/views/admin/ScriptPanelView.vue'), meta: { title: '脚本管理' } },
{ path: 'task-templates', name: 'admin-task-templates', component: () => import('@/views/admin/TaskTemplateView.vue'), meta: { title: '任务编排' } },
{ path: 'simple-fields', name: 'admin-simple-fields', component: () => import('@/views/admin/SimpleFieldManagementView.vue'), meta: { title: '字段管理' } },
{ path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } },
// ── 平台配置中心:聚合页(每个聚合页一个 page key = route.name,对齐后端 PageCatalog)。
// 原十余个独立配置页按业务收敛为下列 6 个入口,子页改为聚合页内的 tab。 ──
@@ -0,0 +1,89 @@
export type SimpleFieldCategory = 'siteFields' | 'trackFields' | 'planFields' | 'carFields'
export const SIMPLE_FIELD_CATEGORIES: { key: SimpleFieldCategory; label: string }[] = [
{ key: 'siteFields', label: '站点字段 (siteFields)' },
{ key: 'trackFields', label: '路径字段 (trackFields)' },
{ key: 'planFields', label: '计划字段 (planFields)' },
{ key: 'carFields', label: '车辆字段 (carFields)' }
]
export interface SimpleFieldRecord {
id: string
carType: string
fieldType: string
key: string
value: string
dataType: string
chinese: string | null
english: string | null
other: string
isDefault: boolean
createTime: string
updateTime: string
}
export interface SimpleFieldPayload {
id?: string
carType: string
fieldType: string
key: string
value?: string
dataType?: string
chinese?: string | null
english?: string | null
other?: string
isDefault: boolean
}
export interface CoderFieldDef {
name: string
typeName: string
defaultValue: unknown
}
export interface CoderFieldGroup {
typeName: string
shortName: string
assemblyName: string
baseTypeName?: string
fields: CoderFieldDef[]
}
export interface CarTypeCoderFields {
typeName: string
shortName: string
label: string
assemblyName: string
siteFields: CoderFieldGroup
trackFields: CoderFieldGroup
planFields: CoderFieldGroup
carFields: CoderFieldGroup
}
export interface SimpleFieldBatchPayload {
replaceAll: boolean
items: SimpleFieldPayload[]
}
export interface FieldRow {
rowKey: string
id?: string
carType: string
fieldType: SimpleFieldCategory
key: string
dataType: string
value: string
chinese: string | null
english: string | null
other: string
isDefault: boolean
}
export const ALL_CAR_TYPES = '__all__'
/** car_type 唯一标识:assemblyName.shortName */
export function buildCarType(assemblyName: string, shortName: string): string {
const asm = assemblyName?.trim() ?? ''
const sn = shortName?.trim() ?? ''
return asm && sn ? `${asm}.${sn}` : sn || asm
}
@@ -0,0 +1,682 @@
<template>
<div class="simple-field-page">
<el-card v-loading="initializing" shadow="never" class="page-card" element-loading-text="正在加载字段数据…">
<template #header>
<div class="page-header">
<div>
<h2>字段管理</h2>
<p>默认 SimpleLite 拉取全部车型字段刷新从数据库读取保存将全部字段写入数据库</p>
</div>
<div class="header-actions">
<el-button :loading="initializing || loadingDefaults" @click="loadDefaults">默认</el-button>
<el-button type="primary" :loading="saving" :disabled="initializing || !fieldRows.length" @click="saveAll">保存</el-button>
<el-button :icon="Refresh" :loading="initializing || loading" @click="loadFromDb()">刷新</el-button>
</div>
</div>
</template>
<div class="filters">
<div class="car-type-filter">
<span class="filter-label">车辆类型</span>
<el-select
v-model="filterCarType"
filterable
placeholder="请选择车型"
class="car-type-select bordered-select"
popper-class="car-type-option-popper"
>
<el-option
v-for="c in carTypes"
:key="c.typeName"
:label="carTypeSelectLabel(c)"
:value="buildCarType(c.assemblyName, c.shortName)"
>
<span class="car-type-option">
<span class="car-type-option-cn">{{ carTypeCn(c) }}</span>
<span class="car-type-option-en">({{ buildCarType(c.assemblyName, c.shortName) }})</span>
</span>
</el-option>
</el-select>
</div>
<div class="keyword-filter">
<span class="filter-label">搜索</span>
<el-input
v-model="keyword"
placeholder="当前车型下搜索属性 / key / 中文 / 英文 / 其他语言"
clearable
class="keyword-input"
/>
</div>
<el-button type="primary" :disabled="initializing" @click="openDialog()">新增字段</el-button>
</div>
<el-tabs v-model="activeCategory" class="field-tabs">
<el-tab-pane
v-for="cat in SIMPLE_FIELD_CATEGORIES"
:key="cat.key"
:name="cat.key"
:label="cat.label"
>
<el-table
v-loading="initializing || loading || loadingDefaults"
:data="filteredRows"
:row-class-name="searchRowClassName"
border
size="small"
height="520"
>
<el-table-column prop="carType" label="车型 (car_type)" min-width="200" />
<el-table-column prop="key" label="属性" min-width="140" sortable />
<el-table-column prop="dataType" label="数据类型" min-width="130" sortable />
<el-table-column prop="value" label="默认值" min-width="100" />
<el-table-column prop="chinese" label="中文名" min-width="100" />
<el-table-column prop="english" label="英文名" min-width="100" />
<el-table-column prop="other" label="其他语言" min-width="100" />
<el-table-column prop="isDefault" label="默认" width="72" sortable :sort-method="sortByIsDefault">
<template #default="{ row }">
<el-tag :type="row.isDefault ? 'info' : 'success'" size="small">{{ row.isDefault ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openDialog(row)">编辑</el-button>
<el-button size="small" type="danger" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close>
<el-form label-width="96px">
<el-form-item label="车型" required>
<el-select
v-model="form.carType"
filterable
:disabled="!!editingRowKey"
class="bordered-select"
popper-class="car-type-option-popper"
style="width: 100%"
>
<el-option
v-for="c in carTypes"
:key="c.typeName"
:label="carTypeSelectLabel(c)"
:value="buildCarType(c.assemblyName, c.shortName)"
>
<span class="car-type-option">
<span class="car-type-option-cn">{{ carTypeCn(c) }}</span>
<span class="car-type-option-en">({{ buildCarType(c.assemblyName, c.shortName) }})</span>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="字段类型" required>
<el-select
v-model="form.fieldType"
:disabled="!!editingRowKey"
class="bordered-select"
style="width: 100%"
>
<el-option
v-for="cat in SIMPLE_FIELD_CATEGORIES"
:key="cat.key"
:label="cat.label"
:value="cat.key"
/>
</el-select>
</el-form-item>
<el-form-item label="属性" required>
<el-input v-model="form.key" :disabled="!!editingRowKey" placeholder="属性名" />
</el-form-item>
<el-form-item label="数据类型" required>
<el-select
v-model="form.dataType"
filterable
allow-create
:disabled="!!editingRowKey"
class="bordered-select"
style="width: 100%"
>
<el-option v-for="t in DATA_TYPE_OPTIONS" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="默认值" required>
<el-input v-model="form.value" placeholder="默认值(字符串存储)" />
</el-form-item>
<el-form-item label="中文名">
<el-input v-model="form.chinese" />
</el-form-item>
<el-form-item label="英文名">
<el-input v-model="form.english" />
</el-form-item>
<el-form-item label="其他语言">
<el-input v-model="form.other" placeholder="日语、德语等其他语言名称" />
</el-form-item>
<el-form-item v-if="editingRowKey" label="是否内置">
<el-switch v-model="form.isDefault" class="builtin-switch" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="applyDialog">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { reflectionApi, type CarTypeCoderFieldsRow, type ReflectionCreatableType } from '@/api/reflection'
import * as simpleFieldApi from '@/api/simpleField'
import {
SIMPLE_FIELD_CATEGORIES,
buildCarType,
type FieldRow,
type SimpleFieldCategory,
type SimpleFieldRecord
} from '@/types/simpleField'
const DATA_TYPE_OPTIONS = [
'System.Boolean',
'System.Int32',
'System.Single',
'System.Double',
'System.String'
]
const initializing = ref(true)
const loading = ref(false)
const loadingDefaults = ref(false)
const saving = ref(false)
const carTypes = ref<ReflectionCreatableType[]>([])
const fieldRows = ref<FieldRow[]>([])
const filterCarType = ref('')
const activeCategory = ref<SimpleFieldCategory>('siteFields')
const keyword = ref('')
const highlightRowKey = ref('')
const dialogVisible = ref(false)
const editingRowKey = ref('')
const form = reactive({
carType: '',
fieldType: 'siteFields' as SimpleFieldCategory,
key: '',
value: '',
dataType: 'System.String',
chinese: '',
english: '',
other: '',
isDefault: false
})
const dialogTitle = computed(() => (editingRowKey.value ? '编辑字段' : '新增字段'))
const categoryRows = computed(() =>
rowsForCurrentCar().filter((r) => r.fieldType === activeCategory.value)
)
function rowsForCurrentCar() {
if (!filterCarType.value) return fieldRows.value
return fieldRows.value.filter((r) => r.carType === filterCarType.value)
}
function rowMatchesKeyword(row: FieldRow, kw: string) {
return (
row.key.toLowerCase().includes(kw) ||
(row.chinese ?? '').toLowerCase().includes(kw) ||
(row.english ?? '').toLowerCase().includes(kw) ||
row.other.toLowerCase().includes(kw) ||
row.value.toLowerCase().includes(kw) ||
row.dataType.toLowerCase().includes(kw)
)
}
function findFirstSearchMatch(kw: string): FieldRow | undefined {
const rows = rowsForCurrentCar()
for (const cat of SIMPLE_FIELD_CATEGORIES) {
const match = rows.find((r) => r.fieldType === cat.key && rowMatchesKeyword(r, kw))
if (match) return match
}
return undefined
}
function searchRowClassName({ row }: { row: FieldRow }) {
return row.rowKey === highlightRowKey.value ? 'search-hit-row' : ''
}
function sortByIsDefault(a: FieldRow, b: FieldRow) {
return Number(a.isDefault) - Number(b.isDefault)
}
function scrollToHighlightedRow() {
nextTick(() => {
document.querySelector('.field-tabs .search-hit-row')?.scrollIntoView({ block: 'nearest' })
})
}
function navigateSearchToFirstMatch() {
const kw = keyword.value.trim().toLowerCase()
highlightRowKey.value = ''
if (!kw) return
const first = findFirstSearchMatch(kw)
if (!first) return
activeCategory.value = first.fieldType
highlightRowKey.value = first.rowKey
scrollToHighlightedRow()
}
watch([keyword, filterCarType], () => {
navigateSearchToFirstMatch()
})
const filteredRows = computed(() => {
const rows = categoryRows.value
const kw = keyword.value.trim().toLowerCase()
if (!kw) return rows
return rows.filter((r) => rowMatchesKeyword(r, kw))
})
function formatValue(v: unknown): string {
if (v === null || v === undefined) return ''
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
}
function makeRowKey(carType: string, fieldType: string, key: string) {
return `${carType}:${fieldType}:${key}`
}
function recordToRow(r: SimpleFieldRecord): FieldRow {
return {
rowKey: makeRowKey(r.carType, r.fieldType, r.key),
id: r.id,
carType: r.carType,
fieldType: r.fieldType as SimpleFieldCategory,
key: r.key,
dataType: r.dataType,
value: r.value,
chinese: r.chinese,
english: r.english,
other: r.other,
isDefault: r.isDefault
}
}
function carTypeToRows(car: CarTypeCoderFieldsRow): FieldRow[] {
const carType = buildCarType(car.assemblyName, car.shortName)
const rows: FieldRow[] = []
for (const cat of SIMPLE_FIELD_CATEGORIES) {
const group = car[cat.key]
for (const f of group?.fields ?? []) {
rows.push({
rowKey: makeRowKey(carType, cat.key, f.name),
carType,
fieldType: cat.key,
key: f.name,
dataType: f.typeName,
value: formatValue(f.defaultValue),
chinese: '',
english: '',
other: '',
isDefault: true
})
}
}
return rows
}
function allCarsToRows(cars: CarTypeCoderFieldsRow[]): FieldRow[] {
return cars.flatMap(carTypeToRows)
}
/** 从已加载的数据库记录推导车型下拉(不请求 SimpleLite)。 */
function syncCarTypesFromFieldRows() {
const labelByKey = new Map(
carTypes.value.map((c) => [buildCarType(c.assemblyName, c.shortName), c.label])
)
const seen = new Set<string>()
const options: ReflectionCreatableType[] = []
for (const r of fieldRows.value) {
if (seen.has(r.carType)) continue
seen.add(r.carType)
const carType = r.carType
const dot = carType.lastIndexOf('.')
const assemblyName = dot >= 0 ? carType.slice(0, dot) : ''
const shortName = dot >= 0 ? carType.slice(dot + 1) : carType
options.push({
typeName: carType,
shortName,
label: labelByKey.get(carType) || shortName,
assemblyName
})
}
carTypes.value = options
}
/** 从 SimpleLite 补全车型中文名(轻量接口,不拉字段定义)。 */
async function enrichCarTypeLabels() {
try {
const types = await reflectionApi.listCreatableTypes('car')
const labelMap = new Map(
types.map((t) => [buildCarType(t.assemblyName, t.shortName), t.label || t.shortName])
)
carTypes.value = carTypes.value.map((c) => {
const key = buildCarType(c.assemblyName, c.shortName)
const label = labelMap.get(key)
return label ? { ...c, label } : c
})
} catch {
/* SimpleLite 未连接时保留现有显示 */
}
}
function ensureDefaultCarType() {
if (filterCarType.value) return
if (carTypes.value.length) {
const first = carTypes.value[0]
filterCarType.value = buildCarType(first.assemblyName, first.shortName)
} else if (fieldRows.value.length) {
filterCarType.value = fieldRows.value[0].carType
}
}
function carTypeCn(c: ReflectionCreatableType) {
if (c.label && c.label !== c.shortName) return c.label
return c.label || c.shortName
}
function carTypeSelectLabel(c: ReflectionCreatableType, fullCarType = true) {
const en = fullCarType ? buildCarType(c.assemblyName, c.shortName) : c.shortName
return `${carTypeCn(c)} (${en})`
}
async function loadFromDb(silent = false) {
loading.value = true
try {
const records = await simpleFieldApi.listSimpleFields()
fieldRows.value = records.map(recordToRow)
syncCarTypesFromFieldRows()
await enrichCarTypeLabels()
ensureDefaultCarType()
if (!silent) ElMessage.success(`已从数据库加载 ${fieldRows.value.length} 条字段`)
} catch (e) {
fieldRows.value = []
carTypes.value = []
ElMessage.error(e instanceof Error ? e.message : '从数据库加载失败')
} finally {
loading.value = false
}
}
async function loadDefaults() {
loadingDefaults.value = true
try {
const cars = await reflectionApi.getCarTypeCoderFields()
if (!cars.length) {
ElMessage.warning('未获取到车型列表,请确认 SimpleLite 已启动')
return
}
carTypes.value = cars.map((c) => ({
typeName: c.typeName,
shortName: c.shortName,
label: c.label,
assemblyName: c.assemblyName
}))
ensureDefaultCarType()
fieldRows.value = allCarsToRows(cars)
ElMessage.success(`已从 SimpleLite 加载 ${cars.length} 种车型、共 ${fieldRows.value.length} 条默认字段(请点击保存写入数据库)`)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '加载默认字段失败')
} finally {
loadingDefaults.value = false
}
}
async function saveAll() {
if (!fieldRows.value.length) return
saving.value = true
try {
const { count } = await simpleFieldApi.saveSimpleFieldsBatch({
replaceAll: true,
items: fieldRows.value.map((r) => ({
carType: r.carType,
fieldType: r.fieldType,
key: r.key,
value: r.value,
dataType: r.dataType,
chinese: r.chinese ?? '',
english: r.english ?? '',
other: r.other,
isDefault: r.isDefault
}))
})
ElMessage.success(`已保存 ${count} 条字段到数据库`)
await loadFromDb(true)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '保存失败')
} finally {
saving.value = false
}
}
function resetForm() {
editingRowKey.value = ''
form.carType = filterCarType.value
|| (carTypes.value[0] ? buildCarType(carTypes.value[0].assemblyName, carTypes.value[0].shortName) : '')
form.fieldType = activeCategory.value
form.key = ''
form.value = ''
form.dataType = 'System.String'
form.chinese = ''
form.english = ''
form.other = ''
form.isDefault = false
}
function openDialog(row?: FieldRow) {
resetForm()
if (row) {
editingRowKey.value = row.rowKey
form.carType = row.carType
form.fieldType = row.fieldType
form.key = row.key
form.value = row.value
form.dataType = row.dataType
form.chinese = row.chinese ?? ''
form.english = row.english ?? ''
form.other = row.other
form.isDefault = row.isDefault
}
dialogVisible.value = true
}
function applyDialog() {
if (!form.carType.trim()) {
ElMessage.warning('请选择车型')
return
}
if (!form.key.trim()) {
ElMessage.warning('请填写属性')
return
}
if (!form.dataType.trim()) {
ElMessage.warning('请选择数据类型')
return
}
if (!form.value.trim()) {
ElMessage.warning('请填写默认值')
return
}
const rowKey = makeRowKey(form.carType.trim(), form.fieldType, form.key.trim())
const payload: FieldRow = {
rowKey,
carType: form.carType.trim(),
fieldType: form.fieldType,
key: form.key.trim(),
dataType: form.dataType,
value: form.value,
chinese: form.chinese,
english: form.english,
other: form.other,
isDefault: editingRowKey.value ? form.isDefault : false,
}
if (editingRowKey.value) {
const idx = fieldRows.value.findIndex((r) => r.rowKey === editingRowKey.value)
if (idx >= 0) {
fieldRows.value[idx] = {
...fieldRows.value[idx],
value: form.value,
chinese: form.chinese,
english: form.english,
other: form.other,
isDefault: form.isDefault
}
}
} else {
if (fieldRows.value.some((r) => r.rowKey === rowKey)) {
ElMessage.warning('该车型下该字段已存在')
return
}
fieldRows.value.push(payload)
}
activeCategory.value = form.fieldType
dialogVisible.value = false
}
async function removeRow(row: FieldRow) {
await ElMessageBox.confirm(`确定删除字段「${row.carType} / ${row.key}」?`, '确认', { type: 'warning' })
fieldRows.value = fieldRows.value.filter((r) => r.rowKey !== row.rowKey)
}
onMounted(async () => {
initializing.value = true
try {
await loadFromDb(true)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '初始化失败')
} finally {
initializing.value = false
}
})
</script>
<style scoped>
.simple-field-page {
padding: 16px;
height: 100%;
overflow: auto;
}
.page-card { min-height: calc(100vh - 88px); }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.page-header h2 { margin: 0 0 4px; font-size: 18px; }
.page-header p { margin: 0; color: var(--el-text-color-secondary); font-size: 13px; }
.header-actions { display: flex; gap: 8px; flex-shrink: 0; }
.filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.car-type-filter,
.keyword-filter {
display: inline-flex;
align-items: center;
gap: 10px;
}
.filter-label {
flex-shrink: 0;
display: inline-flex;
align-items: center;
height: 32px;
margin: 0;
font-size: 14px;
line-height: 1;
color: var(--el-text-color-regular);
white-space: nowrap;
}
.car-type-select {
width: 480px;
}
.car-type-select :deep(.el-select__selected-item),
.car-type-select :deep(.el-select__placeholder) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.keyword-input {
width: 400px;
}
.bordered-select :deep(.el-select__wrapper) {
min-height: 32px;
background-color: #fff !important;
border: 1px solid #dcdfe6 !important;
border-radius: var(--el-border-radius-base, 4px);
box-shadow: none !important;
}
.bordered-select :deep(.el-select__wrapper:hover) {
border-color: #c0c4cc !important;
box-shadow: none !important;
}
.bordered-select :deep(.el-select__wrapper.is-focused),
.bordered-select :deep(.el-select__wrapper.is-hovering.is-focused) {
border-color: var(--el-color-primary, #7c3aed) !important;
box-shadow: 0 0 0 1px var(--el-color-primary, #7c3aed) inset !important;
}
.bordered-select :deep(.el-select__selected-item),
.bordered-select :deep(.el-select__placeholder) {
line-height: 30px;
}
.field-tabs { margin-top: 4px; }
.field-tabs :deep(.search-hit-row > td.el-table__cell) {
background-color: #f5f3ff !important;
}
.builtin-switch :deep(.el-switch__core) {
border: 1px solid #c0c4cc;
background-color: #dcdfe6 !important;
}
.builtin-switch.is-checked :deep(.el-switch__core) {
background-color: var(--el-color-primary, #7c3aed) !important;
border-color: var(--el-color-primary, #7c3aed) !important;
}
.builtin-switch :deep(.el-switch__action) {
background-color: #fff;
}
</style>
<style>
.car-type-option-popper {
min-width: 520px !important;
}
.car-type-option-popper .car-type-option {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
min-width: 0;
}
.car-type-option-popper .car-type-option-cn {
flex: 0 0 auto;
max-width: 11em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.car-type-option-popper .car-type-option-en {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--el-text-color-secondary);
}
</style>