<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useRouter } from 'vue-router'
|
import { getTestTypeList } from '@/api/testTypes'
|
import { getEnabledUserOptions, type UserOption } from '@/api/users'
|
import { useTests } from '@/stores/tests'
|
import type { TestTypeRecord } from '@/stores/testTypes'
|
|
interface BatteryRow {
|
key: number
|
selected: boolean
|
batteryNo: string
|
cabinet: string
|
cabinetChannel: string
|
collector: string
|
temperature1: string
|
temperature2: string
|
temperature3: string
|
positive1: string
|
positive2: string
|
negative1: string
|
negative2: string
|
customChannels: Record<string, string>
|
}
|
|
interface CustomChannelColumn {
|
key: string
|
label: string
|
}
|
|
interface BatteryGroupOption {
|
name: string
|
batteries: string[]
|
}
|
|
interface ThresholdRow {
|
key: number
|
selected: boolean
|
type: string
|
min: string
|
max: string
|
}
|
|
const router = useRouter()
|
const { createTest } = useTests()
|
const submitting = ref(false)
|
const batteryDialogVisible = ref(false)
|
const selectedBatteryGroup = ref('')
|
const selectedBatteryNos = ref<string[]>([])
|
let batteryKey = 3
|
let thresholdKey = 3
|
let customChannelKey = 1
|
|
const form = reactive({
|
name: '',
|
principalId: null as number | null,
|
testTypeId: null as number | null,
|
purpose: '',
|
remark: '',
|
syncMode: '默认同步测试柜',
|
})
|
|
const owners = ref<UserOption[]>([])
|
const testTypes = ref<TestTypeRecord[]>([])
|
const ownersLoading = ref(false)
|
const testTypesLoading = ref(false)
|
const cabinetOptions = ['A-01 检测柜', 'A-02 检测柜', 'B-01 检测柜']
|
const collectorOptions = ['COL-A01', 'COL-A02', 'COL-B01']
|
const channelOptions = Array.from({ length: 16 }, (_, index) => `CH-${String(index + 1).padStart(2, '0')}`)
|
const sensorChannels = Array.from({ length: 12 }, (_, index) => `S-${String(index + 1).padStart(2, '0')}`)
|
const thresholdTypes = ['温度', '电压', '电流', '容量', '测试柜返回项']
|
const fixedSensorFields = ['temperature1', 'temperature2', 'temperature3', 'positive1', 'positive2', 'negative1', 'negative2'] as const
|
type FixedSensorField = typeof fixedSensorFields[number]
|
const reservedChannelLabels = ['主温度通道', '温度通道 2', '温度通道 3', '正电位主通道', '正电位通道 2', '负电位主通道', '负电位通道 2']
|
const customChannelColumns = ref<CustomChannelColumn[]>([])
|
const batteryGroupOptions: BatteryGroupOption[] = [
|
{ name: 'SP26 循环测试组', batteries: ['SP26-1', 'SP26-2', 'SP26-3', 'SP26-4', 'SP26-5'] },
|
{ name: 'SP27 容量验证组', batteries: ['SP27-1', 'SP27-2', 'SP27-3', 'SP27-4'] },
|
{ name: 'LT25 低温性能组', batteries: ['LT25-01', 'LT25-02', 'LT25-03'] },
|
{ name: 'HP26 倍率性能组', batteries: ['HP26-01', 'HP26-02', 'HP26-03', 'HP26-04'] },
|
]
|
|
function createBatteryRow(batteryNo = ''): BatteryRow {
|
return {
|
key: batteryKey++, selected: false, batteryNo, cabinet: '', cabinetChannel: '', collector: '',
|
temperature1: '', temperature2: '', temperature3: '', positive1: '', positive2: '', negative1: '', negative2: '',
|
customChannels: Object.fromEntries(customChannelColumns.value.map(column => [column.key, ''])),
|
}
|
}
|
|
function createThresholdRow(type = ''): ThresholdRow {
|
return { key: thresholdKey++, selected: false, type, min: '', max: '' }
|
}
|
|
const batteries = ref<BatteryRow[]>([createBatteryRow('SP26-1'), createBatteryRow('SP26-3')])
|
const thresholds = ref<ThresholdRow[]>([createThresholdRow('温度'), createThresholdRow('电压')])
|
const selectedOwnerName = computed(() => owners.value.find(item => item.id === form.principalId)?.name || '')
|
const selectedTestTypeName = computed(() => testTypes.value.find(item => item.id === form.testTypeId)?.name || '')
|
const groupBatteryNos = computed(() => batteryGroupOptions.find(group => group.name === selectedBatteryGroup.value)?.batteries || [])
|
const availableGroupBatteryNos = computed(() => groupBatteryNos.value.filter(batteryNo => !isBatteryAdded(batteryNo)))
|
|
const allBatteriesSelected = computed({
|
get: () => batteries.value.length > 0 && batteries.value.every(item => item.selected),
|
set: (checked: boolean) => batteries.value.forEach(item => { item.selected = checked }),
|
})
|
|
const allThresholdsSelected = computed({
|
get: () => thresholds.value.length > 0 && thresholds.value.every(item => item.selected),
|
set: (checked: boolean) => thresholds.value.forEach(item => { item.selected = checked }),
|
})
|
|
const allGroupBatteriesSelected = computed({
|
get: () => availableGroupBatteryNos.value.length > 0
|
&& availableGroupBatteryNos.value.every(batteryNo => selectedBatteryNos.value.includes(batteryNo)),
|
set: (checked: boolean) => {
|
selectedBatteryNos.value = checked ? [...availableGroupBatteryNos.value] : []
|
},
|
})
|
|
function isBatteryAdded(batteryNo: string) {
|
return batteries.value.some(item => item.batteryNo === batteryNo)
|
}
|
|
function openBatteryDialog() {
|
selectedBatteryGroup.value = ''
|
selectedBatteryNos.value = []
|
batteryDialogVisible.value = true
|
}
|
|
function handleBatteryGroupChange() {
|
selectedBatteryNos.value = []
|
}
|
|
function confirmAddBatteries() {
|
if (!selectedBatteryGroup.value) {
|
ElMessage.warning('请先选择电池组')
|
return
|
}
|
if (!selectedBatteryNos.value.length) {
|
ElMessage.warning('请至少选择一块电池')
|
return
|
}
|
const newBatteryNos = selectedBatteryNos.value.filter(batteryNo => !isBatteryAdded(batteryNo))
|
batteries.value.push(...newBatteryNos.map(batteryNo => createBatteryRow(batteryNo)))
|
batteryDialogVisible.value = false
|
ElMessage.success(`已添加 ${newBatteryNos.length} 块电池`)
|
}
|
|
function removeBatteries() {
|
if (!batteries.value.some(item => item.selected)) {
|
ElMessage.warning('请先选择需要删除的电池')
|
return
|
}
|
batteries.value = batteries.value.filter(item => !item.selected)
|
}
|
|
function getSensorSelections(item: BatteryRow) {
|
return [
|
...fixedSensorFields.map(field => ({ id: field, value: item[field] })),
|
...customChannelColumns.value.map(column => ({ id: `custom:${column.key}`, value: item.customChannels[column.key] || '' })),
|
]
|
}
|
|
function clearSensorSelection(item: BatteryRow, fieldId: string) {
|
if (fieldId.startsWith('custom:')) {
|
item.customChannels[fieldId.slice(7)] = ''
|
return
|
}
|
item[fieldId as FixedSensorField] = ''
|
}
|
|
function isCabinetChannelUsed(item: BatteryRow, channel: string) {
|
if (!item.cabinet) return false
|
return batteries.value.some(other => other.key !== item.key && other.cabinet === item.cabinet && other.cabinetChannel === channel)
|
}
|
|
function isSensorChannelUsed(item: BatteryRow, fieldId: string, channel: string) {
|
if (!item.collector) return false
|
return batteries.value.some(other => {
|
if (other.key !== item.key && other.collector !== item.collector) return false
|
return getSensorSelections(other).some(selection => {
|
if (other.key === item.key && selection.id === fieldId) return false
|
return selection.value === channel
|
})
|
})
|
}
|
|
function handleCabinetChange(item: BatteryRow) {
|
if (!item.cabinetChannel || !isCabinetChannelUsed(item, item.cabinetChannel)) return
|
item.cabinetChannel = ''
|
ElMessage.warning('该测试柜通道已被其他电池占用,请重新选择')
|
}
|
|
function handleCollectorChange(item: BatteryRow) {
|
if (!item.collector) return
|
const occupiedChannels = new Set(
|
batteries.value
|
.filter(other => other.key !== item.key && other.collector === item.collector)
|
.flatMap(other => getSensorSelections(other).map(selection => selection.value).filter(Boolean)),
|
)
|
const currentChannels = new Set<string>()
|
let clearedCount = 0
|
getSensorSelections(item).forEach(selection => {
|
if (!selection.value) return
|
if (occupiedChannels.has(selection.value) || currentChannels.has(selection.value)) {
|
clearSensorSelection(item, selection.id)
|
clearedCount += 1
|
return
|
}
|
currentChannels.add(selection.value)
|
})
|
if (clearedCount) ElMessage.warning(`已清空 ${clearedCount} 个重复通道,请重新选择`)
|
}
|
|
async function addChannelField() {
|
try {
|
const result = await ElMessageBox.prompt('输入字段名称后,将在表格最后新增一个可选择采集器通道的列。', '添加采集器字段', {
|
confirmButtonText: '添加',
|
cancelButtonText: '取消',
|
inputPlaceholder: '例如:壳体温度通道',
|
inputValidator: (value: string) => {
|
const label = value.trim()
|
if (!label) return '请输入字段名称'
|
if (label.length > 16) return '字段名称不能超过 16 个字'
|
const normalizedLabel = label.toLocaleLowerCase()
|
const labelExists = [...reservedChannelLabels, ...customChannelColumns.value.map(column => column.label)]
|
.some(item => item.toLocaleLowerCase() === normalizedLabel)
|
return labelExists ? '字段名称已存在' : true
|
},
|
})
|
const label = result.value.trim()
|
const key = `custom-channel-${customChannelKey++}`
|
customChannelColumns.value.push({ key, label })
|
batteries.value.forEach(item => { item.customChannels[key] = '' })
|
ElMessage.success(`已添加“${label}”通道列`)
|
} catch {
|
// 用户取消输入时保持当前配置。
|
}
|
}
|
|
async function loadOwners() {
|
ownersLoading.value = true
|
try {
|
owners.value = await getEnabledUserOptions()
|
} catch (error) {
|
owners.value = []
|
ElMessage.error(error instanceof Error ? error.message : '测试负责人加载失败')
|
} finally {
|
ownersLoading.value = false
|
}
|
}
|
|
async function loadTestTypes() {
|
testTypesLoading.value = true
|
try {
|
const result = await getTestTypeList({ pageNum: 1, pageSize: 1000, status: '0' })
|
testTypes.value = result.rows
|
} catch (error) {
|
testTypes.value = []
|
ElMessage.error(error instanceof Error ? error.message : '测试类型加载失败')
|
} finally {
|
testTypesLoading.value = false
|
}
|
}
|
|
function addThreshold() {
|
thresholds.value.push(createThresholdRow())
|
}
|
|
function removeThresholds() {
|
if (!thresholds.value.some(item => item.selected)) {
|
ElMessage.warning('请先选择需要删除的阈值')
|
return
|
}
|
thresholds.value = thresholds.value.filter(item => !item.selected)
|
}
|
|
function validate() {
|
if (!form.name.trim()) return '请输入测试名称'
|
if (!form.principalId) return '请选择测试负责人'
|
if (!form.testTypeId) return '请选择测试类型'
|
if (!form.purpose.trim()) return '请输入测试目的'
|
if (!batteries.value.length) return '至少需要配置一块电池'
|
|
const requiredFields: Array<[keyof BatteryRow, string]> = [
|
['batteryNo', '电池号'], ['cabinet', '测试柜'], ['cabinetChannel', '测试柜通道'], ['collector', '采集器'],
|
['temperature1', '主温度通道'], ['positive1', '正电位主通道'], ['negative1', '负电位主通道'],
|
]
|
for (let index = 0; index < batteries.value.length; index += 1) {
|
for (const [field, label] of requiredFields) {
|
if (!String(batteries.value[index][field]).trim()) return `第 ${index + 1} 行请选择${label}`
|
}
|
}
|
|
const cabinetChannels = batteries.value.map(item => `${item.cabinet}/${item.cabinetChannel}`)
|
if (new Set(cabinetChannels).size !== cabinetChannels.length) return '同一测试柜的测试柜通道不可重复'
|
|
const collectorChannels = batteries.value.flatMap(item => [
|
...fixedSensorFields.map(field => item[field]),
|
...customChannelColumns.value.map(column => item.customChannels[column.key]),
|
].filter(Boolean).map(channel => `${item.collector}/${channel}`))
|
if (new Set(collectorChannels).size !== collectorChannels.length) return '同一采集器的采集通道不可重复'
|
|
for (let index = 0; index < thresholds.value.length; index += 1) {
|
const item = thresholds.value[index]
|
if (!item.type || item.min === '' || item.max === '') return `请完整填写第 ${index + 1} 行预警阈值`
|
if (Number(item.min) >= Number(item.max)) return `第 ${index + 1} 行阈值最小值必须小于最大值`
|
}
|
return ''
|
}
|
|
async function submit() {
|
const error = validate()
|
if (error) {
|
ElMessage.warning(error)
|
return
|
}
|
submitting.value = true
|
await new Promise(resolve => window.setTimeout(resolve, 350))
|
createTest({
|
name: form.name.trim(),
|
type: selectedTestTypeName.value,
|
batteryNo: batteries.value.map(item => item.batteryNo).join('、'),
|
batteryGroup: selectedBatteryGroup.value || '自定义电池组',
|
batteryCount: batteries.value.length,
|
purpose: form.purpose.trim(),
|
owner: selectedOwnerName.value,
|
remark: form.remark.trim() || '等待接入检测柜',
|
})
|
ElMessage.success('测试已创建并加入测试列表')
|
submitting.value = false
|
await router.push('/test-list')
|
}
|
|
onMounted(() => {
|
loadOwners()
|
loadTestTypes()
|
})
|
</script>
|
|
<template>
|
<div class="create-page">
|
<div class="page-tools">
|
<button class="back-button" type="button" @click="router.push('/test-list')"><el-icon><ArrowLeft /></el-icon>返回测试列表</button>
|
<span>NEW TEST CONFIGURATION</span>
|
</div>
|
|
<section class="form-panel">
|
<header class="panel-heading">
|
<div><i></i><strong>测试参数</strong><span>TEST PARAMETERS</span></div>
|
<small><b>*</b> 为必填项</small>
|
</header>
|
<div class="parameter-grid">
|
<label><span><b>*</b>测试名称</span><el-input v-model="form.name" maxlength="40" show-word-limit placeholder="输入测试名称" /></label>
|
<label><span><b>*</b>测试负责人</span><el-select v-model="form.principalId" :loading="ownersLoading" filterable clearable placeholder="请选择负责人"><el-option v-for="item in owners" :key="item.id" :label="item.name" :value="item.id"><span>{{ item.name }}</span><small v-if="item.userName && item.userName !== item.name">({{ item.userName }})</small></el-option></el-select></label>
|
<label><span><b>*</b>测试类型</span><el-select v-model="form.testTypeId" :loading="testTypesLoading" filterable clearable placeholder="请选择测试类型"><el-option v-for="item in testTypes" :key="item.id" :label="item.name" :value="item.id" /></el-select></label>
|
<label class="textarea-field"><span><b>*</b>测试目的</span><el-input v-model="form.purpose" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入测试目的" /></label>
|
<label class="textarea-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入备注(选填)" /></label>
|
</div>
|
</section>
|
|
<section class="form-panel">
|
<header class="panel-heading channel-heading">
|
<div><i></i><strong>电池组和通道设置</strong><span>BATTERY & CHANNELS</span></div>
|
<label class="sync-setting"><span>同步方式</span><el-select v-model="form.syncMode"><el-option label="默认同步测试柜" value="默认同步测试柜" /></el-select></label>
|
</header>
|
<div class="section-actions">
|
<div><button class="primary-button" type="button" @click="openBatteryDialog"><el-icon><Plus /></el-icon>添加电池</button><button class="secondary-button" type="button" @click="addChannelField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button danger" type="button" @click="removeBatteries"><el-icon><Delete /></el-icon>删除所选</button></div>
|
<span>至少配置一块电池 · 同一采集器通道不可重复</span>
|
</div>
|
<div class="config-table-wrap">
|
<table class="config-table battery-table" :style="{ minWidth: `${1700 + customChannelColumns.length * 150}px` }">
|
<thead><tr>
|
<th><el-checkbox v-model="allBatteriesSelected" aria-label="选择全部电池" /></th><th>电池号 *</th><th>测试柜 *</th><th>测试柜通道 *</th><th>采集器 *</th>
|
<th>主温度通道 *</th><th>温度通道 2</th><th>温度通道 3</th><th>正电位主通道 *</th><th>正电位通道 2</th><th>负电位主通道 *</th><th>负电位通道 2</th>
|
<th v-for="column in customChannelColumns" :key="column.key" class="custom-channel-header">{{ column.label }}</th>
|
</tr></thead>
|
<tbody>
|
<tr v-for="item in batteries" :key="item.key">
|
<td><el-checkbox v-model="item.selected" :aria-label="`选择电池 ${item.batteryNo || item.key}`" /></td>
|
<td><el-input v-model="item.batteryNo" placeholder="电池号" /></td>
|
<td><el-select v-model="item.cabinet" placeholder="选择" @change="handleCabinetChange(item)"><el-option v-for="option in cabinetOptions" :key="option" :label="option" :value="option" /></el-select></td>
|
<td><el-select v-model="item.cabinetChannel" clearable :disabled="!item.cabinet" :placeholder="item.cabinet ? '选择' : '请先选择测试柜'"><el-option v-for="option in channelOptions" :key="option" :label="isCabinetChannelUsed(item, option) ? `${option}(已占用)` : option" :value="option" :disabled="isCabinetChannelUsed(item, option)" /></el-select></td>
|
<td><el-select v-model="item.collector" placeholder="选择" @change="handleCollectorChange(item)"><el-option v-for="option in collectorOptions" :key="option" :label="option" :value="option" /></el-select></td>
|
<td v-for="field in fixedSensorFields" :key="field">
|
<el-select v-model="item[field]" clearable :disabled="!item.collector" :placeholder="item.collector ? '选择' : '请先选择采集器'"><el-option v-for="option in sensorChannels" :key="option" :label="isSensorChannelUsed(item, field, option) ? `${option}(已占用)` : option" :value="option" :disabled="isSensorChannelUsed(item, field, option)" /></el-select>
|
</td>
|
<td v-for="column in customChannelColumns" :key="column.key">
|
<el-select v-model="item.customChannels[column.key]" clearable :disabled="!item.collector" :placeholder="item.collector ? '选择' : '请先选择采集器'"><el-option v-for="option in sensorChannels" :key="option" :label="isSensorChannelUsed(item, `custom:${column.key}`, option) ? `${option}(已占用)` : option" :value="option" :disabled="isSensorChannelUsed(item, `custom:${column.key}`, option)" /></el-select>
|
</td>
|
</tr>
|
<tr v-if="!batteries.length"><td :colspan="12 + customChannelColumns.length" class="empty-row">尚未配置电池,请点击“添加电池”</td></tr>
|
</tbody>
|
</table>
|
</div>
|
</section>
|
|
<section class="form-panel threshold-panel">
|
<header class="panel-heading"><div><i></i><strong>预警阈值</strong><span>ALERT THRESHOLDS</span></div><small>可配置温度、电压、电流、容量及测试柜返回项</small></header>
|
<div class="section-actions">
|
<div><button class="primary-button" type="button" @click="addThreshold"><el-icon><Plus /></el-icon>添加阈值</button><button class="secondary-button danger" type="button" @click="removeThresholds"><el-icon><Delete /></el-icon>删除所选</button></div>
|
</div>
|
<div class="config-table-wrap threshold-wrap">
|
<table class="config-table threshold-table">
|
<thead><tr><th><el-checkbox v-model="allThresholdsSelected" aria-label="选择全部阈值" /></th><th>阈值类型</th><th>最小值</th><th>最大值</th></tr></thead>
|
<tbody>
|
<tr v-for="item in thresholds" :key="item.key">
|
<td><el-checkbox v-model="item.selected" :aria-label="`选择阈值 ${item.key}`" /></td>
|
<td><el-select v-model="item.type" placeholder="选择类型"><el-option v-for="option in thresholdTypes" :key="option" :label="option" :value="option" /></el-select></td>
|
<td><el-input v-model="item.min" type="number" placeholder="输入最小值" /></td>
|
<td><el-input v-model="item.max" type="number" placeholder="输入最大值" /></td>
|
</tr>
|
<tr v-if="!thresholds.length"><td colspan="4" class="empty-row">暂未设置预警阈值</td></tr>
|
</tbody>
|
</table>
|
</div>
|
</section>
|
|
<footer class="submit-bar">
|
<button class="secondary-button" type="button" @click="router.push('/test-list')">取消</button>
|
<button class="primary-button submit-button" type="button" :disabled="submitting" @click="submit"><el-icon><Check /></el-icon>{{ submitting ? '提交中' : '提交测试' }}</button>
|
</footer>
|
|
<el-dialog v-model="batteryDialogVisible" class="battery-dialog" width="560px" align-center destroy-on-close>
|
<template #header>
|
<div class="dialog-title"><strong>添加电池组</strong><span>ADD BATTERY GROUP</span></div>
|
</template>
|
<div class="battery-dialog-body">
|
<label class="battery-group-search">
|
<span>电池组</span>
|
<el-select v-model="selectedBatteryGroup" filterable clearable placeholder="输入电池组名称搜索" @change="handleBatteryGroupChange">
|
<el-option v-for="group in batteryGroupOptions" :key="group.name" :label="group.name" :value="group.name" />
|
</el-select>
|
</label>
|
|
<div class="battery-picker-table">
|
<table>
|
<thead><tr><th><el-checkbox v-model="allGroupBatteriesSelected" :disabled="!availableGroupBatteryNos.length" aria-label="选择全部可用电池" /></th><th>电池组</th><th>电池名</th><th>状态</th></tr></thead>
|
<tbody>
|
<tr v-for="batteryNo in groupBatteryNos" :key="batteryNo" :class="{ added: isBatteryAdded(batteryNo) }">
|
<td><el-checkbox v-model="selectedBatteryNos" :value="batteryNo" :disabled="isBatteryAdded(batteryNo)" :aria-label="`选择电池 ${batteryNo}`" /></td>
|
<td>{{ selectedBatteryGroup }}</td>
|
<td>{{ batteryNo }}</td>
|
<td><span :class="['battery-state', { used: isBatteryAdded(batteryNo) }]">{{ isBatteryAdded(batteryNo) ? '已添加' : '可添加' }}</span></td>
|
</tr>
|
<tr v-if="!selectedBatteryGroup"><td colspan="4" class="picker-empty">请选择或搜索电池组</td></tr>
|
</tbody>
|
</table>
|
</div>
|
</div>
|
<template #footer>
|
<div class="dialog-footer"><button class="secondary-button" type="button" @click="batteryDialogVisible = false">取消</button><button class="primary-button" type="button" @click="confirmAddBatteries"><el-icon><Check /></el-icon>提交</button></div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped>
|
.create-page { width: 100%; display: flex; flex-direction: column; gap: 16px; }
|
.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); }
|
.form-panel { background: #fff; border: 1px solid #d8e1e9; }
|
.panel-heading { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 16px; background: var(--surface-soft); border-bottom: 1px solid var(--line); }
|
.panel-heading > div { display: flex; align-items: center; gap: 8px; }
|
.panel-heading > div > i { width: 3px; height: 16px; background: #2b78a3; }
|
.panel-heading strong { color: var(--ink); font-size: 15px; font-weight: 600; }
|
.panel-heading div span, .panel-heading small { color: #81909d; font-size: 10px; }
|
.panel-heading small b, .parameter-grid label > span b { color: #b34f4f; }
|
.parameter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; padding: 18px 16px; }
|
.parameter-grid label { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
|
.parameter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }
|
.textarea-field { grid-column: span 1; }
|
.parameter-grid :deep(.el-select), .sync-setting :deep(.el-select) { width: 100%; }
|
.create-page :deep(.el-input__wrapper), .create-page :deep(.el-select__wrapper), .create-page :deep(.el-textarea__inner) { border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
|
.create-page :deep(.el-input__wrapper), .create-page :deep(.el-select__wrapper) { min-height: 34px; }
|
.create-page :deep(.el-textarea__inner) { resize: vertical; }
|
.channel-heading { min-height: 54px; }
|
.sync-setting { display: flex; align-items: center; gap: 10px; }
|
.sync-setting > span { color: #52606d; font-size: 12px; white-space: nowrap; }
|
.sync-setting :deep(.el-select) { width: 190px; }
|
.section-actions { min-height: 50px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 14px; }
|
.section-actions > div { display: flex; gap: 8px; }
|
.section-actions > span { color: #7b8794; font-size: 11px; }
|
.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; }
|
.primary-button:disabled { opacity: .55; cursor: wait; }
|
.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
|
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
|
.secondary-button.danger { color: #a84848; }
|
.config-table-wrap { overflow-x: auto; border-top: 1px solid var(--line); }
|
.config-table { width: 100%; min-width: 1700px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 12px; }
|
.config-table th, .config-table td { height: 48px; padding: 6px; border-right: 1px solid #cbd9e5; border-bottom: 1px solid #d8e1e9; text-align: center; }
|
.config-table th { height: 40px; color: #fff; background: #255884; font-size: 11px; font-weight: 600; }
|
.config-table th:first-child { width: 46px; }
|
.config-table th:nth-child(2) { width: 130px; }
|
.config-table th.custom-channel-header { width: 150px; }
|
.config-table th:last-child, .config-table td:last-child { border-right: 0; }
|
.config-table tbody tr:last-child td { border-bottom: 0; }
|
.config-table :deep(.el-select), .config-table :deep(.el-input) { width: 100%; }
|
.config-table :deep(.el-input__wrapper), .config-table :deep(.el-select__wrapper) { min-height: 32px; padding: 0 8px; background: #fff; }
|
.threshold-panel { padding-bottom: 14px; }
|
.threshold-wrap { width: min(620px, 100%); margin: 0 14px; border: 1px solid var(--line); }
|
.threshold-table { min-width: 600px; }
|
.threshold-table th:nth-child(2) { width: 210px; }
|
.empty-row { height: 88px !important; color: #8795a3; }
|
.submit-bar { position: sticky; bottom: 0; z-index: 20; min-height: 62px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 10px 16px; background: rgba(234, 240, 246, .96); border-top: 1px solid #cbd9e5; }
|
.submit-button { min-width: 110px; }
|
.create-page :deep(.battery-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
|
.create-page :deep(.battery-dialog .el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
|
.create-page :deep(.battery-dialog .el-dialog__headerbtn) { top: 7px; right: 8px; width: 42px; height: 42px; font-size: 20px; }
|
.create-page :deep(.battery-dialog .el-dialog__body) { padding: 20px; }
|
.create-page :deep(.battery-dialog .el-dialog__footer) { padding: 14px 20px 18px; border-top: 1px solid var(--line); }
|
.dialog-title { display: flex; align-items: baseline; gap: 9px; }
|
.dialog-title strong { color: var(--ink); font-size: 16px; font-weight: 600; }
|
.dialog-title span { color: #8795a3; font-size: 10px; }
|
.battery-dialog-body { display: flex; flex-direction: column; gap: 16px; }
|
.battery-group-search { display: grid; grid-template-columns: 64px minmax(0, 1fr); align-items: center; gap: 10px; }
|
.battery-group-search > span { color: #52606d; font-size: 13px; font-weight: 500; }
|
.battery-group-search :deep(.el-select) { width: 100%; }
|
.battery-picker-table { min-height: 190px; overflow-x: auto; border: 1px solid var(--line); }
|
.battery-picker-table table { width: 100%; min-width: 480px; border-collapse: collapse; table-layout: fixed; color: #52606d; font-size: 12px; }
|
.battery-picker-table th, .battery-picker-table td { height: 40px; padding: 6px 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); text-align: left; }
|
.battery-picker-table th { height: 38px; color: #fff; background: #255884; font-weight: 600; }
|
.battery-picker-table th:first-child, .battery-picker-table td:first-child { width: 48px; text-align: center; }
|
.battery-picker-table td:first-child :deep(.el-checkbox) { height: 18px; margin-right: 0; }
|
.battery-picker-table td:first-child :deep(.el-checkbox__label) { display: none; }
|
.battery-picker-table th:nth-child(2) { width: 190px; }
|
.battery-picker-table th:last-child, .battery-picker-table td:last-child { width: 76px; border-right: 0; text-align: center; }
|
.battery-picker-table tbody tr:last-child td { border-bottom: 0; }
|
.battery-picker-table tbody tr.added { color: #99a5af; background: #f5f7f9; }
|
.battery-state { color: #23815a; }
|
.battery-state.used { color: #8a97a3; }
|
.picker-empty { height: 150px !important; color: #8996a2; text-align: center !important; }
|
.dialog-footer { display: flex; justify-content: flex-end; gap: 8px; }
|
|
@media (max-width: 980px) {
|
.parameter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.channel-heading { align-items: flex-start; flex-direction: column; padding-top: 12px; padding-bottom: 12px; }
|
}
|
|
@media (max-width: 680px) {
|
.create-page { gap: 12px; }
|
.page-tools > span { display: none; }
|
.panel-heading { padding: 0 12px; }
|
.panel-heading div span, .panel-heading > small { display: none; }
|
.parameter-grid { grid-template-columns: 1fr; gap: 14px; padding: 14px 12px; }
|
.section-actions { align-items: flex-start; flex-direction: column; padding: 10px 12px; }
|
.section-actions > span { line-height: 1.5; }
|
.sync-setting { width: 100%; }
|
.sync-setting :deep(.el-select) { flex: 1; width: auto; }
|
.threshold-wrap { width: calc(100% - 24px); margin: 0 12px; }
|
.submit-bar { margin: 0 -14px -14px; padding-right: 14px; }
|
.create-page :deep(.battery-dialog .el-dialog__body) { padding: 16px 12px; }
|
.battery-group-search { grid-template-columns: 1fr; gap: 7px; }
|
}
|
</style>
|