<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useRouter } from 'vue-router'
|
import { getUserInfo } from '@/api/auth'
|
import { changeBatteryGroupFieldStatus, createBatteryGroupField, deleteBatteryGroupField, fieldTypeCode, getBatteryGroupFields, updateBatteryGroupField, type BatteryGroupField, type BatteryGroupFieldStatus, type BatteryGroupFieldType } from '@/api/batteryGroupFields'
|
|
interface Filters {
|
id: string
|
name: string
|
status: string
|
}
|
|
const router = useRouter()
|
const fields = ref<BatteryGroupField[]>([])
|
const creators = ref<string[]>([])
|
const statuses: BatteryGroupFieldStatus[] = ['启用', '禁用']
|
const fieldTypes: BatteryGroupFieldType[] = ['文本', '数字', '时间', '备注']
|
const filters = reactive<Filters>({ id: '', name: '', status: '' })
|
const appliedFilters = ref<Filters>({ id: '', name: '', status: '' })
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
const dialogVisible = ref(false)
|
const editingId = ref<number | null>(null)
|
const loading = ref(false)
|
const saving = ref(false)
|
const fieldForm = reactive<{ name: string, type: BatteryGroupFieldType, creator: string, status: BatteryGroupFieldStatus }>({ name: '', type: '文本', creator: '', status: '启用' })
|
|
const dialogTitle = computed(() => editingId.value ? '编辑电池组字段' : '新增电池组字段')
|
const filteredFields = computed(() => {
|
const query = appliedFilters.value
|
return fields.value
|
.filter(item => !query.id || String(item.id).includes(query.id.trim()))
|
.filter(item => !query.name || item.name.toLowerCase().includes(query.name.trim().toLowerCase()))
|
.filter(item => !query.status || query.status === '全部' || item.status === query.status)
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
})
|
const pagedFields = computed(() => {
|
const start = (currentPage.value - 1) * pageSize.value
|
return filteredFields.value.slice(start, start + pageSize.value)
|
})
|
|
function applySearch() {
|
appliedFilters.value = { ...filters }
|
currentPage.value = 1
|
ElMessage.success(`已找到 ${filteredFields.value.length} 个字段`)
|
}
|
|
function resetSearch() {
|
Object.assign(filters, { id: '', name: '', status: '' })
|
appliedFilters.value = { id: '', name: '', status: '' }
|
currentPage.value = 1
|
}
|
|
function openCreate() {
|
editingId.value = null
|
Object.assign(fieldForm, { name: '', type: '文本', creator: creators.value[0] || '', status: '启用' })
|
dialogVisible.value = true
|
}
|
|
function openEdit(item: BatteryGroupField) {
|
editingId.value = item.id
|
Object.assign(fieldForm, { name: item.name, type: item.type, creator: item.creator, status: item.status })
|
dialogVisible.value = true
|
}
|
|
async function saveField() {
|
const name = fieldForm.name.trim()
|
if (!name) {
|
ElMessage.warning('请输入字段名称')
|
return
|
}
|
if (!fieldForm.creator) {
|
ElMessage.warning('请选择添加人')
|
return
|
}
|
const duplicated = fields.value.some(item => item.id !== editingId.value && item.name.trim().toLowerCase() === name.toLowerCase())
|
if (duplicated) {
|
ElMessage.warning('字段名称已存在')
|
return
|
}
|
saving.value = true
|
try {
|
const payload = { fieldName: name, fieldType: fieldTypeCode[fieldForm.type], status: fieldForm.status === '启用' ? '0' as const : '1' as const }
|
if (editingId.value) await updateBatteryGroupField(editingId.value, payload)
|
else await createBatteryGroupField(payload)
|
dialogVisible.value = false
|
ElMessage.success(editingId.value ? '字段已更新' : '字段已添加')
|
await loadFields()
|
} catch (error) {
|
ElMessage.error(error instanceof Error ? error.message : '保存字段失败')
|
} finally {
|
saving.value = false
|
}
|
}
|
|
async function toggleStatus(item: BatteryGroupField) {
|
const nextStatus: BatteryGroupFieldStatus = item.status === '启用' ? '禁用' : '启用'
|
try {
|
await ElMessageBox.confirm(`确认${nextStatus}字段“${item.name}”?`, `${nextStatus}字段`, { confirmButtonText: nextStatus, cancelButtonText: '取消', type: 'warning' })
|
await changeBatteryGroupFieldStatus(item.id, nextStatus === '启用' ? '0' : '1')
|
item.status = nextStatus
|
ElMessage.success(`字段已${nextStatus}`)
|
} catch (error) {
|
if (error instanceof Error) ElMessage.error(error.message || '更新字段状态失败')
|
}
|
}
|
|
async function deleteField(item: BatteryGroupField) {
|
try {
|
await ElMessageBox.confirm(`删除字段“${item.name}”后无法恢复,是否继续?`, '确认删除字段', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' })
|
await deleteBatteryGroupField(item.id)
|
fields.value = fields.value.filter(field => field.id !== item.id)
|
currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredFields.value.length / pageSize.value)))
|
ElMessage.success('字段已删除')
|
} catch (error) {
|
if (error instanceof Error) ElMessage.error(error.message || '删除字段失败')
|
}
|
}
|
|
async function loadFields() {
|
loading.value = true
|
try {
|
fields.value = await getBatteryGroupFields()
|
} catch (error) {
|
fields.value = []
|
ElMessage.error(error instanceof Error ? error.message : '加载字段失败')
|
} finally {
|
loading.value = false
|
}
|
}
|
|
async function loadPage() {
|
try {
|
const userInfo = await getUserInfo()
|
const currentUser = userInfo.user.userName?.trim() || ''
|
creators.value = currentUser ? [currentUser] : []
|
fieldForm.creator = currentUser
|
} catch (error) {
|
creators.value = []
|
ElMessage.error(error instanceof Error ? error.message : '加载当前登录用户失败')
|
}
|
await loadFields()
|
}
|
|
onMounted(loadPage)
|
</script>
|
|
<template>
|
<div class="group-field-page">
|
<div class="page-tools"><button class="back-button" type="button" @click="router.push('/battery-group')"><el-icon><ArrowLeft /></el-icon>返回电池组管理</button><span>BATTERY GROUP FIELDS</span></div>
|
|
<section class="filter-panel" aria-label="电池组字段查询条件">
|
<header class="section-heading"><div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div><small>共 {{ fields.length }} 个字段</small></header>
|
<div class="filter-grid">
|
<label><span>字段ID</span><el-input v-model="filters.id" clearable placeholder="输入字段ID" @keyup.enter="applySearch" /></label>
|
<label><span>字段名称</span><el-input v-model="filters.name" clearable placeholder="输入字段名称" @keyup.enter="applySearch" /></label>
|
<label><span>状态</span><el-select v-model="filters.status" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label>
|
<div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div>
|
</div>
|
</section>
|
|
<section v-loading="loading" class="result-panel">
|
<div class="result-toolbar"><div><i></i><strong>电池组字段列表</strong><span>{{ filteredFields.length }} RESULTS · 按添加时间倒序</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新增字段</button></div>
|
<div class="table-wrap">
|
<table>
|
<thead><tr><th>字段ID</th><th>字段名称</th><th>字段类型</th><th>添加人</th><th>添加时间</th><th>状态</th><th class="action-column">操作</th></tr></thead>
|
<tbody>
|
<tr v-for="item in pagedFields" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td><strong>{{ item.name }}</strong></td><td><span class="type-tag">{{ item.type }}</span></td><td>{{ item.creator }}</td><td>{{ item.createdAt }}</td><td><span class="status-tag" :class="item.status === '启用' ? 'enabled' : 'disabled'"><i></i>{{ item.status }}</span></td><td class="action-column"><div class="row-actions"><button type="button" @click="openEdit(item)">编辑</button><button type="button" :class="item.status === '启用' ? 'disable-action' : 'enable-action'" @click="toggleStatus(item)">{{ item.status === '启用' ? '禁用' : '启用' }}</button><button class="delete-action" type="button" @click="deleteField(item)">删除</button></div></td></tr>
|
<tr v-if="!pagedFields.length"><td colspan="7" class="empty-cell">暂无符合条件的电池组字段</td></tr>
|
</tbody>
|
</table>
|
</div>
|
<footer class="pagination-bar"><span>显示 {{ pagedFields.length }} 条,共 {{ filteredFields.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="filteredFields.length" layout="sizes, prev, pager, next" background /></footer>
|
</section>
|
|
<el-dialog v-model="dialogVisible" class="field-dialog" :title="dialogTitle" width="520px" align-center destroy-on-close>
|
<div class="field-form">
|
<label><span><b>*</b>字段名称</span><el-input v-model="fieldForm.name" maxlength="30" show-word-limit placeholder="输入字段名称" @keyup.enter="saveField" /></label>
|
<label><span><b>*</b>字段类型</span><el-select v-model="fieldForm.type" placeholder="选择字段类型"><el-option v-for="item in fieldTypes" :key="item" :label="item" :value="item" /></el-select></label>
|
<label><span><b>*</b>添加人</span><el-select v-model="fieldForm.creator" disabled placeholder="当前登录用户"><el-option v-for="item in creators" :key="item" :label="item" :value="item" /></el-select></label>
|
<label><span>状态</span><el-radio-group v-model="fieldForm.status"><el-radio-button value="启用">启用</el-radio-button><el-radio-button value="禁用">禁用</el-radio-button></el-radio-group></label>
|
</div>
|
<template #footer><button class="secondary-button" type="button" @click="dialogVisible = false">取消</button><button class="primary-button" type="button" :disabled="saving" @click="saveField"><el-icon><Check /></el-icon>保存</button></template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped>
|
.group-field-page { width: 100%; }
|
.page-tools { height: 34px; display: flex; align-items: center; justify-content: space-between; }
|
.page-tools > span { color: #7f8d99; font-size: 10px; font-weight: 600; }
|
.back-button { display: inline-flex; align-items: center; gap: 6px; border: 0; color: #52606d; background: transparent; font-size: 13px; cursor: pointer; }
|
.back-button:hover { color: var(--blue); }
|
.primary-button, .secondary-button { height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 1px solid transparent; border-radius: 3px; font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
|
.primary-button { color: #fff; background: var(--blue); border-color: var(--blue); }
|
.primary-button:hover { background: #176fcb; border-color: #176fcb; }
|
.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
|
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
|
.filter-panel, .result-panel { background: #fff; border: 1px solid #d8e1e9; }
|
.filter-panel { margin-bottom: 16px; }
|
.section-heading, .result-toolbar { display: flex; align-items: center; justify-content: space-between; background: var(--surface-soft); border-bottom: 1px solid var(--line); }
|
.section-heading { height: 44px; padding: 0 16px; }
|
.section-heading > div, .result-toolbar > div { display: flex; align-items: center; gap: 8px; }
|
.section-heading > div > i, .result-toolbar > div:first-child > i { width: 3px; height: 16px; background: #2b78a3; }
|
.section-heading strong, .result-toolbar strong { color: var(--ink); font-size: 15px; font-weight: 600; }
|
.section-heading span, .result-toolbar span { color: #81909d; font-size: 10px; }
|
.section-heading small { color: #647380; font-size: 11px; }
|
.filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 18px; padding: 16px; }
|
.filter-grid label { min-width: 0; display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: 8px; }
|
.filter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }
|
.filter-grid :deep(.el-select) { width: 100%; }
|
.filter-grid :deep(.el-input__wrapper), .filter-grid :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
|
.filter-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
.result-toolbar { height: 50px; padding: 0 16px; }
|
.table-wrap { overflow-x: auto; }
|
table { width: 100%; min-width: 900px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; }
|
th, td { height: 50px; padding: 0 12px; border-right: 1px solid #dde7f0; border-bottom: 1px solid #dde7f0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
th { height: 40px; color: #52606d; background: #eaf4ff; border-color: #c7e0ff; font-size: 12px; font-weight: 600; }
|
th:nth-child(1) { width: 100px; } th:nth-child(2) { width: 190px; } th:nth-child(3) { width: 110px; } th:nth-child(4) { width: 110px; } th:nth-child(5) { width: 180px; } th:nth-child(6) { width: 90px; } th:last-child { width: 190px; }
|
th:last-child, td:last-child { border-right: 0; text-align: center; }
|
.action-column { position: sticky; right: 0; z-index: 2; background: #fff; overflow: visible; box-shadow: inset 1px 0 0 #b4c7d9, -8px 0 14px rgba(23, 49, 74, .12); }
|
th.action-column { z-index: 3; background: #eaf4ff; }
|
tbody tr:hover, tbody tr:hover td.action-column { background: #f7fbff; }
|
tbody tr:last-child td { border-bottom: 0; }
|
.id-cell { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; }
|
td strong { color: #26333f; font-weight: 600; }
|
.type-tag { display: inline-flex; padding: 4px 8px; color: #37688e; background: #edf5fb; border: 1px solid #c9deed; font-size: 11px; line-height: 1; }
|
.status-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 7px; border: 1px solid; font-size: 11px; line-height: 1; }
|
.status-tag i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
.status-tag.enabled { color: #23865e; background: #edf9f3; border-color: #b8dfcd; }
|
.status-tag.disabled { color: #a64a4a; background: #fff1f1; border-color: #e9bcbc; }
|
.row-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
.row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
|
.row-actions .disable-action, .row-actions .delete-action { color: #ad4a4a; }
|
.row-actions .enable-action { color: #23865e; }
|
.empty-cell { height: 160px; color: #8795a3; text-align: center; }
|
.pagination-bar { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 14px 8px 16px; border-top: 1px solid var(--line); }
|
.pagination-bar > span { color: #7b8794; font-size: 11px; }
|
.group-field-page :deep(.field-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
|
.group-field-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
|
.group-field-page :deep(.el-dialog__body) { padding: 22px 28px; }
|
.group-field-page :deep(.el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--line); }
|
.field-form { display: grid; gap: 16px; }
|
.field-form label { display: grid; grid-template-columns: 80px minmax(0, 1fr); align-items: center; gap: 12px; }
|
.field-form label > span { color: #52606d; font-size: 12px; font-weight: 500; }
|
.field-form label > span b { color: #b34f4f; }
|
.field-form :deep(.el-select) { width: 100%; }
|
.field-form :deep(.el-input__wrapper), .field-form :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
|
@media (max-width: 980px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media (max-width: 680px) { .filter-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px 12px; } .filter-actions { justify-content: stretch; } .filter-actions button { flex: 1; } .section-heading, .result-toolbar { padding-left: 12px; } .section-heading span, .result-toolbar span { display: none; } .pagination-bar { align-items: flex-start; flex-direction: column; overflow-x: auto; } .field-form label { grid-template-columns: minmax(0, 1fr); gap: 7px; } }
|
</style>
|