|
@@ -0,0 +1,4483 @@
|
|
|
+<template>
|
|
|
+ <div class="timing-strategy-page">
|
|
|
+ <!-- 页面标题 -->
|
|
|
+ <div class="page-header">
|
|
|
+ <h2 class="page-title">
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
+ 定时策略管理
|
|
|
+ </h2>
|
|
|
+ <p class="page-description">管理设备的定时控制策略,支持按周期和时间自动执行设备操作</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 查询条件卡片 -->
|
|
|
+ <el-card class="search-card" shadow="hover">
|
|
|
+ <el-form :model="searchForm" inline class="search-form">
|
|
|
+ <el-form-item label="策略名称">
|
|
|
+ <el-input
|
|
|
+ v-model="searchForm.searchText"
|
|
|
+ placeholder="请输入策略名称搜索"
|
|
|
+ clearable
|
|
|
+ prefix-icon="Search"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ class="search-input"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch" :icon="Search">
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="handleReset" :icon="Refresh">
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ <el-button type="success" @click="handleAdd" :icon="Plus">
|
|
|
+ 新增策略
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 数据表格卡片 -->
|
|
|
+ <el-card class="table-card" shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <div class="header-left">
|
|
|
+ <el-icon><List /></el-icon>
|
|
|
+ <span>策略列表</span>
|
|
|
+ <el-tag type="info" class="count-tag">共 {{ pagination.total }} 条</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ :data="tableData"
|
|
|
+ stripe
|
|
|
+ class="strategy-table"
|
|
|
+ >
|
|
|
+ <el-table-column prop="timing_name" label="策略名称" min-width="150">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="strategy-name">
|
|
|
+ <el-icon class="name-icon"><Timer /></el-icon>
|
|
|
+ {{ row.timing_name }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="weeks" label="重复日期" min-width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="weeks-container">
|
|
|
+ <el-tag
|
|
|
+ v-for="week in getWeekTags(row.weeks)"
|
|
|
+ :key="week.value"
|
|
|
+ :type="week.type"
|
|
|
+ size="small"
|
|
|
+ class="week-tag"
|
|
|
+ >
|
|
|
+ {{ week.label }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="timing_start_time" label="执行时间" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="time-display">
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
+ {{ row.timing_start_time }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 保持原来的设备指令列显示方式,但优化内容 -->
|
|
|
+ <el-table-column prop="timing_agreement" label="设备指令" min-width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="agreement-display">
|
|
|
+ <el-tag
|
|
|
+ v-for="(item, index) in getAgreementTags(row.timing_agreement)"
|
|
|
+ :key="index"
|
|
|
+ :type="item.type"
|
|
|
+ size="small"
|
|
|
+ class="agreement-tag"
|
|
|
+ :class="item.configType"
|
|
|
+ >
|
|
|
+ <span class="tag-icon" v-if="item.icon">
|
|
|
+ <el-icon>
|
|
|
+ <component :is="item.icon" />
|
|
|
+ </el-icon>
|
|
|
+ </span>
|
|
|
+ {{ item.label }}
|
|
|
+ </el-tag>
|
|
|
+
|
|
|
+ <!-- 如果没有任何配置显示提示 -->
|
|
|
+ <el-tag v-if="getAgreementTags(row.timing_agreement).length === 0" type="info" size="small" class="no-config-tag">
|
|
|
+ <el-icon><InfoFilled /></el-icon>
|
|
|
+ 未配置路数
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column prop="timing_state" label="状态" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch
|
|
|
+ v-model="row.timing_state"
|
|
|
+ :active-value="1"
|
|
|
+ :inactive-value="0"
|
|
|
+ active-color="#13ce66"
|
|
|
+ inactive-color="#ff4949"
|
|
|
+ @change="handleStatusChange(row)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <el-table-column label="操作" width="180" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="action-buttons">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :icon="Edit"
|
|
|
+ @click="handleEdit(row)"
|
|
|
+ link
|
|
|
+ class="action-btn control-btn"
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="info"
|
|
|
+ size="small"
|
|
|
+ :icon="View"
|
|
|
+ @click="handleDetail(row)"
|
|
|
+ link
|
|
|
+ >
|
|
|
+ 详情
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.currentPage"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ background
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 新增/编辑弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ width="900px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ class="strategy-dialog"
|
|
|
+ @close="handleDialogClose"
|
|
|
+ >
|
|
|
+ <el-form
|
|
|
+ :model="formData"
|
|
|
+ :rules="formRules"
|
|
|
+ ref="formRef"
|
|
|
+ label-width="120px"
|
|
|
+ class="strategy-form"
|
|
|
+ >
|
|
|
+ <!-- 基本信息 -->
|
|
|
+ <div class="form-section">
|
|
|
+ <h3 class="section-title">
|
|
|
+ <el-icon><InfoFilled /></el-icon>
|
|
|
+ 基本信息
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="策略名称" prop="timing_name">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.timing_name"
|
|
|
+ placeholder="请输入策略名称"
|
|
|
+ prefix-icon="Edit"
|
|
|
+ maxlength="50"
|
|
|
+ show-word-limit
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="执行时间" prop="timing_start_time">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="formData.timing_start_time"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm"
|
|
|
+ placeholder="选择时间"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 重复日期选择 -->
|
|
|
+ <el-form-item label="重复日期" prop="weeks">
|
|
|
+ <div class="weeks-selection">
|
|
|
+ <!-- 快捷选择按钮 -->
|
|
|
+ <div class="quick-select-buttons">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="selectAllWeekdays"
|
|
|
+ :type="isAllWeekdaysSelected ? 'primary' : ''"
|
|
|
+ >
|
|
|
+ 工作日
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="selectWeekend"
|
|
|
+ :type="isWeekendSelected ? 'primary' : ''"
|
|
|
+ >
|
|
|
+ 周末
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="selectAllWeek"
|
|
|
+ :type="isAllWeekSelected ? 'primary' : ''"
|
|
|
+ >
|
|
|
+ 全选
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @click="clearAllWeeks"
|
|
|
+ >
|
|
|
+ 清空
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 星期选择 -->
|
|
|
+ <div class="week-selection-container">
|
|
|
+ <div class="week-days-grid">
|
|
|
+ <div
|
|
|
+ v-for="day in weekDays"
|
|
|
+ :key="day.value"
|
|
|
+ :class="[
|
|
|
+ 'week-day-item',
|
|
|
+ { 'selected': formData.weeks.includes(day.value) },
|
|
|
+ { 'weekend': day.isWeekend }
|
|
|
+ ]"
|
|
|
+ @click="toggleWeekDay(day.value)"
|
|
|
+ >
|
|
|
+ <div class="day-content">
|
|
|
+ <div class="day-name">{{ day.name }}</div>
|
|
|
+ <div class="day-en">{{ day.en }}</div>
|
|
|
+ <div class="day-icon">
|
|
|
+ <el-icon v-if="formData.weeks.includes(day.value)">
|
|
|
+ <Check />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 特殊选项 -->
|
|
|
+ <div class="special-options">
|
|
|
+ <div class="special-option-group">
|
|
|
+ <h4 class="option-group-title">特殊设置</h4>
|
|
|
+ <div class="special-checkboxes">
|
|
|
+ <div
|
|
|
+ :class="[
|
|
|
+ 'special-checkbox',
|
|
|
+ { 'checked': formData.weeks.includes(256) }
|
|
|
+ ]"
|
|
|
+ @click="toggleSpecialOption(256)"
|
|
|
+ >
|
|
|
+ <el-icon class="checkbox-icon">
|
|
|
+ <Check v-if="formData.weeks.includes(256)" />
|
|
|
+ </el-icon>
|
|
|
+ <span class="checkbox-label">节假日执行</span>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ :class="[
|
|
|
+ 'special-checkbox',
|
|
|
+ { 'checked': formData.weeks.includes(512) }
|
|
|
+ ]"
|
|
|
+ @click="toggleSpecialOption(512)"
|
|
|
+ >
|
|
|
+ <el-icon class="checkbox-icon">
|
|
|
+ <Check v-if="formData.weeks.includes(512)" />
|
|
|
+ </el-icon>
|
|
|
+ <span class="checkbox-label">节假日不执行</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 选择结果预览 -->
|
|
|
+ <div class="selection-preview" v-if="formData.weeks.length > 0">
|
|
|
+ <div class="preview-title">已选择:</div>
|
|
|
+ <div class="preview-tags">
|
|
|
+ <el-tag
|
|
|
+ v-for="week in getSelectedWeekTags"
|
|
|
+ :key="week.value"
|
|
|
+ :type="week.type"
|
|
|
+ size="small"
|
|
|
+ closable
|
|
|
+ @close="removeWeekDay(week.value)"
|
|
|
+ class="preview-tag"
|
|
|
+ >
|
|
|
+ {{ week.label }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 设备选择 -->
|
|
|
+ <div class="form-section">
|
|
|
+ <h3 class="section-title">
|
|
|
+ <el-icon><Monitor /></el-icon>
|
|
|
+ 设备选择
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ <el-form-item label="选择方式">
|
|
|
+ <el-radio-group v-model="deviceSelectType" class="select-type-radio" @change="handleSelectTypeChange">
|
|
|
+ <el-radio label="region">
|
|
|
+ <el-icon><Location /></el-icon>
|
|
|
+ 按区域选择
|
|
|
+ </el-radio>
|
|
|
+ <el-radio label="group">
|
|
|
+ <el-icon><Collection /></el-icon>
|
|
|
+ 按分组选择
|
|
|
+ </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="所属位置" prop="region_ids">
|
|
|
+ <!-- 区域选择 - 使用树形选择器 -->
|
|
|
+ <el-tree-select
|
|
|
+ ref="regionTreeRef"
|
|
|
+ v-model="formData.region_ids"
|
|
|
+ :data="regionTreeData"
|
|
|
+ :props="regionTreeProps"
|
|
|
+ placeholder="请选择所属位置"
|
|
|
+ clearable
|
|
|
+ filterable
|
|
|
+ check-strictly
|
|
|
+ :load="loadRegionNode"
|
|
|
+ lazy
|
|
|
+ style="width: 100%"
|
|
|
+ @change="handleRegionSelectChange"
|
|
|
+ :default-expanded-keys="[]"
|
|
|
+ :show-checkbox="false"
|
|
|
+ node-key="id"
|
|
|
+ :render-after-expand="false"
|
|
|
+ :cache-data="false"
|
|
|
+ :key="`region-tree-${treeKey}`"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="设备分类" prop="category_id">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.category_id"
|
|
|
+ placeholder="请选择设备分类"
|
|
|
+ clearable
|
|
|
+ style="width: 100%"
|
|
|
+ @change="handleCategoryChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in categoryOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="设备型号" prop="device_type_id">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.device_type_id"
|
|
|
+ placeholder="请选择设备型号"
|
|
|
+ clearable
|
|
|
+ style="width: 100%"
|
|
|
+ @change="handleDeviceTypeChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in deviceTypeOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-form-item label="设备名称" prop="device_ids">
|
|
|
+ <div class="device-select-container">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.device_ids"
|
|
|
+ placeholder="请选择设备名称"
|
|
|
+ multiple
|
|
|
+ clearable
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ :max-collapse-tags="30"
|
|
|
+ style="width: 100%"
|
|
|
+ @change="handleDeviceChange"
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <div class="device-select-header">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="isAllDevicesSelected"
|
|
|
+ :indeterminate="isDeviceIndeterminate"
|
|
|
+ @change="handleSelectAllDevices"
|
|
|
+ >
|
|
|
+ 全选设备 ({{ deviceOptions.length }})
|
|
|
+ </el-checkbox>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-option
|
|
|
+ v-for="item in deviceOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <div class="device-count-info" v-if="formData.device_ids.length > 0">
|
|
|
+ 已选择 {{ formData.device_ids.length }} 个设备
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 修改路数配置部分的模板 -->
|
|
|
+ <div v-if="channelCount > 0" class="form-section">
|
|
|
+ <h3 class="section-title">
|
|
|
+ <el-icon><Connection /></el-icon>
|
|
|
+ 路数配置
|
|
|
+ <el-tag type="info" size="small">{{ channelCount }}路设备</el-tag>
|
|
|
+ <el-tag type="warning" size="small" style="margin-left: 8px;">可选配置</el-tag>
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ <!-- 路数操作按钮 -->
|
|
|
+ <!-- 修改路数操作按钮部分 -->
|
|
|
+ <div class="channel-operations">
|
|
|
+ <div class="operation-buttons">
|
|
|
+ <el-button-group>
|
|
|
+ <el-button size="small" @click="selectAllChannels" :icon="Check">
|
|
|
+ 全部开启
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" @click="unselectAllChannels" :icon="Close">
|
|
|
+ 全部关闭
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" @click="reverseAllChannels" :icon="Refresh">
|
|
|
+ 反选
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ <el-button size="small" @click="restoreChannelConfig" :icon="RefreshLeft" v-if="isEdit">
|
|
|
+ 恢复原配置
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" @click="clearAllChannels" :icon="Delete" type="danger">
|
|
|
+ 清空配置
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="channel-status-info">
|
|
|
+ <div class="status-line">
|
|
|
+ 开启: {{ getChannelStatusCount().on }} |
|
|
|
+ 关闭: {{ getChannelStatusCount().off }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="channel-grid">
|
|
|
+ <div
|
|
|
+ v-for="i in channelCount"
|
|
|
+ :key="i"
|
|
|
+ class="channel-item"
|
|
|
+ :class="{
|
|
|
+ 'has-name': hasChannelName(i),
|
|
|
+ 'has-status': hasChannelStatus(i),
|
|
|
+ 'configured': hasChannelName(i) || hasChannelStatus(i)
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div class="channel-header">
|
|
|
+ <div class="channel-info">
|
|
|
+ <span class="channel-number">{{ getChineseNumber(i) }}路</span>
|
|
|
+ </div>
|
|
|
+ <el-form-item
|
|
|
+ :prop="`channel_names.Tag${i}`"
|
|
|
+ :rules="channelNameRules"
|
|
|
+ class="channel-name-form-item"
|
|
|
+ >
|
|
|
+ <el-input
|
|
|
+ v-model="formData.channel_names[`Tag${i}`]"
|
|
|
+ size="small"
|
|
|
+ placeholder="可选:输入路数名称"
|
|
|
+ class="channel-name-input"
|
|
|
+ maxlength="20"
|
|
|
+ clearable
|
|
|
+ @input="handleChannelNameChange(i)"
|
|
|
+ @clear="handleChannelNameClear(i)"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="channel-control">
|
|
|
+ <div class="channel-buttons">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ :type="formData.timing_agreement[`Tag${i}`] === 0 ? 'danger' : ''"
|
|
|
+ :plain="formData.timing_agreement[`Tag${i}`] !== 0"
|
|
|
+ @click="toggleChannelStatus(i, 0)"
|
|
|
+ class="status-button"
|
|
|
+ >
|
|
|
+ <el-icon><SwitchButton /></el-icon>
|
|
|
+ 关闭
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ :type="formData.timing_agreement[`Tag${i}`] === 1 ? 'success' : ''"
|
|
|
+ :plain="formData.timing_agreement[`Tag${i}`] !== 1"
|
|
|
+ @click="toggleChannelStatus(i, 1)"
|
|
|
+ class="status-button"
|
|
|
+ >
|
|
|
+ <el-icon><SwitchButton /></el-icon>
|
|
|
+ 开启
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="dialogVisible = false" :icon="Close">
|
|
|
+ 取消
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" @click="handleSubmit" :icon="Check" :loading="submitLoading">
|
|
|
+ {{ submitLoading ? '保存中...' : '确定' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 详情弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="detailVisible"
|
|
|
+ title="策略详情"
|
|
|
+ width="900px"
|
|
|
+ class="detail-dialog"
|
|
|
+ >
|
|
|
+ <div class="detail-content" v-if="detailData">
|
|
|
+ <div class="detail-section">
|
|
|
+ <h4>基本信息</h4>
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="策略名称">
|
|
|
+ {{ detailData.timing_name }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="执行时间">
|
|
|
+ {{ detailData.timing_start_time }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="重复日期" :span="2">
|
|
|
+ <el-tag
|
|
|
+ v-for="week in getWeekTags(detailData.weeks)"
|
|
|
+ :key="week.value"
|
|
|
+ :type="week.type"
|
|
|
+ size="small"
|
|
|
+ class="week-tag"
|
|
|
+ >
|
|
|
+ {{ week.label }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="detailData.timing_state === 1 ? 'success' : 'danger'">
|
|
|
+ {{ detailData.timing_state === 1 ? '启用' : '禁用' }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="detail-section">
|
|
|
+ <h4>设备指令</h4>
|
|
|
+ <div class="agreement-detail">
|
|
|
+ <div
|
|
|
+ v-for="item in getAgreementTags(detailData.timing_agreement)"
|
|
|
+ :key="item.label"
|
|
|
+ class="agreement-item"
|
|
|
+ >
|
|
|
+ <span class="channel-name">{{ item.label }}</span>
|
|
|
+ <el-tag :type="item.type" size="small">{{ item.status }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="zmTime">
|
|
|
+import {
|
|
|
+ Clock, Search, Refresh, Plus, List, Timer, Edit, View, Delete,
|
|
|
+ InfoFilled, Monitor, Location, Collection, Connection, SwitchButton,
|
|
|
+ Close, Check, RefreshLeft
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
+import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const searchForm = reactive({
|
|
|
+ searchText: ''
|
|
|
+})
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
+
|
|
|
+const tableData = ref([])
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const detailVisible = ref(false)
|
|
|
+const detailData = ref(null)
|
|
|
+const dialogTitle = ref('新增策略')
|
|
|
+const isEdit = ref(false)
|
|
|
+const deviceSelectType = ref('region')
|
|
|
+const channelCount = ref(0)
|
|
|
+const submitLoading = ref(false)
|
|
|
+const regionTreeRef = ref(null)
|
|
|
+
|
|
|
+const requestCache = new Map() // 请求缓存
|
|
|
+const requestQueue = new Map() // 请求队列,防止重复请求
|
|
|
+const treeKey = ref(0) // 用于强制刷新树组件
|
|
|
+let debounceTimer = null
|
|
|
+
|
|
|
+// 请求缓存工具函数
|
|
|
+const getCacheKey = (url, params) => {
|
|
|
+ return `${url}?${new URLSearchParams(params).toString()}`
|
|
|
+}
|
|
|
+
|
|
|
+const getCachedRequest = (url, params) => {
|
|
|
+ const key = getCacheKey(url, params)
|
|
|
+ return requestCache.get(key)
|
|
|
+}
|
|
|
+
|
|
|
+const setCachedRequest = (url, params, data) => {
|
|
|
+ const key = getCacheKey(url, params)
|
|
|
+ requestCache.set(key, {
|
|
|
+ data,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ expiry: 5 * 60 * 1000 // 5分钟过期
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const isCacheValid = (cachedItem) => {
|
|
|
+ return cachedItem && (Date.now() - cachedItem.timestamp < cachedItem.expiry)
|
|
|
+}
|
|
|
+
|
|
|
+// 区域树形数据
|
|
|
+const regionTreeData = ref([])
|
|
|
+const regionTreeProps = {
|
|
|
+ value: 'id',
|
|
|
+ label: 'name',
|
|
|
+ children: 'children',
|
|
|
+ isLeaf: (data) => data.children_count === 0
|
|
|
+}
|
|
|
+
|
|
|
+// 存储原始配置用于恢复
|
|
|
+const originalChannelConfig = ref({
|
|
|
+ timing_agreement: {},
|
|
|
+ channel_names: {}
|
|
|
+})
|
|
|
+
|
|
|
+const formData = reactive({
|
|
|
+ timing_id: null,
|
|
|
+ timing_name: '',
|
|
|
+ weeks: [],
|
|
|
+ timing_start_time: '',
|
|
|
+ region_ids: '',
|
|
|
+ group_id: '',
|
|
|
+ category_id: '',
|
|
|
+ device_type_id: '',
|
|
|
+ device_ids: [],
|
|
|
+ timing_agreement: {},
|
|
|
+ channel_names: {},
|
|
|
+ region_type: null,
|
|
|
+ selectedRegionNode: null // 新增:保存完整的区域节点信息
|
|
|
+})
|
|
|
+
|
|
|
+const categoryOptions = ref([])
|
|
|
+const deviceTypeOptions = ref([])
|
|
|
+const deviceOptions = ref([])
|
|
|
+const formRef = ref()
|
|
|
+
|
|
|
+// 星期数据配置
|
|
|
+const weekDays = [
|
|
|
+ { value: 2, name: '周一', en: 'MON', isWeekend: false },
|
|
|
+ { value: 4, name: '周二', en: 'TUE', isWeekend: false },
|
|
|
+ { value: 8, name: '周三', en: 'WED', isWeekend: false },
|
|
|
+ { value: 16, name: '周四', en: 'THU', isWeekend: false },
|
|
|
+ { value: 32, name: '周五', en: 'FRI', isWeekend: false },
|
|
|
+ { value: 64, name: '周六', en: 'SAT', isWeekend: true },
|
|
|
+ { value: 128, name: '周日', en: 'SUN', isWeekend: true }
|
|
|
+]
|
|
|
+
|
|
|
+// 表单验证规则
|
|
|
+const formRules = {
|
|
|
+ timing_name: [
|
|
|
+ { required: true, message: '请输入策略名称', trigger: 'blur' },
|
|
|
+ { min: 2, max: 50, message: '策略名称长度在 2 到 50 个字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ weeks: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (!value || value.length === 0) {
|
|
|
+ callback(new Error('请选择重复日期'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ timing_start_time: [
|
|
|
+ { required: true, message: '请选择执行时间', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ region_ids: [
|
|
|
+ {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (deviceSelectType.value === 'region' && !value) {
|
|
|
+ callback(new Error('请选择所属位置'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ group_id: [
|
|
|
+ {
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (deviceSelectType.value === 'group' && !value) {
|
|
|
+ callback(new Error('请选择设备分组'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ category_id: [
|
|
|
+ { required: true, message: '请选择设备分类', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ device_type_id: [
|
|
|
+ { required: true, message: '请选择设备型号', trigger: 'change' }
|
|
|
+ ],
|
|
|
+ device_ids: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ validator: (rule, value, callback) => {
|
|
|
+ if (!value || value.length === 0) {
|
|
|
+ callback(new Error('请选择至少一个设备'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+// 路数名称验证规则
|
|
|
+const channelNameRules = [
|
|
|
+ { min: 1, max: 20, message: '路数名称长度在 1 到 20 个字符', trigger: 'blur' }
|
|
|
+]
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const weekMap = {
|
|
|
+ 2: { label: '周一', type: 'primary' },
|
|
|
+ 4: { label: '周二', type: 'success' },
|
|
|
+ 8: { label: '周三', type: 'info' },
|
|
|
+ 16: { label: '周四', type: 'warning' },
|
|
|
+ 32: { label: '周五', type: 'danger' },
|
|
|
+ 64: { label: '周六', type: '' },
|
|
|
+ 128: { label: '周日', type: '' },
|
|
|
+ 256: { label: '节假日执行', type: 'success' },
|
|
|
+ 512: { label: '节假日不执行', type: 'danger' }
|
|
|
+}
|
|
|
+
|
|
|
+const hasChannelName = (channelNum) => {
|
|
|
+ return formData.channel_names[`Tag${channelNum}`] &&
|
|
|
+ formData.channel_names[`Tag${channelNum}`].trim() !== ''
|
|
|
+}
|
|
|
+
|
|
|
+const hasChannelStatus = (channelNum) => {
|
|
|
+ return formData.timing_agreement[`Tag${channelNum}`] !== undefined &&
|
|
|
+ formData.timing_agreement[`Tag${channelNum}`] !== null
|
|
|
+}
|
|
|
+
|
|
|
+const getNamedChannelCount = () => {
|
|
|
+ let count = 0
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ if (hasChannelName(i)) {
|
|
|
+ count++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return count
|
|
|
+}
|
|
|
+
|
|
|
+const getChannelStatusType = (channelNum) => {
|
|
|
+ const status = formData.timing_agreement[`Tag${channelNum}`]
|
|
|
+ if (status === 1) return 'success'
|
|
|
+ if (status === 0) return 'danger'
|
|
|
+ return 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const getChannelStatusText = (channelNum) => {
|
|
|
+ const status = formData.timing_agreement[`Tag${channelNum}`]
|
|
|
+ if (status === 1) return '开启'
|
|
|
+ if (status === 0) return '关闭'
|
|
|
+ return '未设置'
|
|
|
+}
|
|
|
+
|
|
|
+const toggleChannelStatus = (channelNum, targetStatus) => {
|
|
|
+ const currentStatus = formData.timing_agreement[`Tag${channelNum}`]
|
|
|
+
|
|
|
+ if (currentStatus === targetStatus) {
|
|
|
+ // 如果当前状态与目标状态相同,则取消选择
|
|
|
+ formData.timing_agreement[`Tag${channelNum}`] = undefined
|
|
|
+ } else {
|
|
|
+ // 否则设置为目标状态
|
|
|
+ formData.timing_agreement[`Tag${channelNum}`] = targetStatus
|
|
|
+
|
|
|
+ // 只有在没有名称时才设置默认名称
|
|
|
+ if (!formData.channel_names[`Tag${channelNum}`] || formData.channel_names[`Tag${channelNum}`].trim() === '') {
|
|
|
+ formData.channel_names[`Tag${channelNum}`] = `${channelNum}路`
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleChannelNameChange = (channelNum) => {
|
|
|
+ // 当用户输入路数名称时,不自动设置状态
|
|
|
+ // 用户可以选择只设置名称而不设置开关状态
|
|
|
+}
|
|
|
+
|
|
|
+const handleChannelNameClear = (channelNum) => {
|
|
|
+ // 当用户清空路数名称时,只清空名称,不影响状态
|
|
|
+ formData.channel_names[`Tag${channelNum}`] = ''
|
|
|
+}
|
|
|
+
|
|
|
+// 修改批量操作方法,避免强制设置名称
|
|
|
+const selectAllChannels = () => {
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ formData.timing_agreement[`Tag${i}`] = 1
|
|
|
+ // 只有在没有名称时才设置默认名称
|
|
|
+ if (!formData.channel_names[`Tag${i}`] || formData.channel_names[`Tag${i}`].trim() === '') {
|
|
|
+ formData.channel_names[`Tag${i}`] = `${i}路`
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const unselectAllChannels = () => {
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ formData.timing_agreement[`Tag${i}`] = 0
|
|
|
+ // 只有在没有名称时才设置默认名称
|
|
|
+ if (!formData.channel_names[`Tag${i}`] || formData.channel_names[`Tag${i}`].trim() === '') {
|
|
|
+ formData.channel_names[`Tag${i}`] = `${i}路`
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const reverseAllChannels = () => {
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ const currentStatus = formData.timing_agreement[`Tag${i}`]
|
|
|
+ if (currentStatus === 1) {
|
|
|
+ formData.timing_agreement[`Tag${i}`] = 0
|
|
|
+ } else if (currentStatus === 0) {
|
|
|
+ formData.timing_agreement[`Tag${i}`] = 1
|
|
|
+ } else {
|
|
|
+ // 如果是未配置状态,设置为开启
|
|
|
+ formData.timing_agreement[`Tag${i}`] = 1
|
|
|
+ // 只有在没有名称时才设置默认名称
|
|
|
+ if (!formData.channel_names[`Tag${i}`] || formData.channel_names[`Tag${i}`].trim() === '') {
|
|
|
+ formData.channel_names[`Tag${i}`] = `${i}路`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 修改判断是否有配置的方法
|
|
|
+const hasChannelConfig = (channelNum) => {
|
|
|
+ return hasChannelName(channelNum) || hasChannelStatus(channelNum)
|
|
|
+}
|
|
|
+
|
|
|
+const clearChannelStatus = (channelNum) => {
|
|
|
+ formData.timing_agreement[`Tag${channelNum}`] = undefined
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const isChannelConfigured = (channelNum) => {
|
|
|
+ return formData.timing_agreement[`Tag${channelNum}`] !== undefined
|
|
|
+}
|
|
|
+
|
|
|
+const getChannelStatusCount = () => {
|
|
|
+ let on = 0, off = 0
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ if (formData.timing_agreement[`Tag${i}`] === 1) {
|
|
|
+ on++
|
|
|
+ } else if (formData.timing_agreement[`Tag${i}`] === 0) {
|
|
|
+ off++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { on, off }
|
|
|
+}
|
|
|
+
|
|
|
+const clearAllChannels = () => {
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ formData.timing_agreement[`Tag${i}`] = undefined
|
|
|
+ formData.channel_names[`Tag${i}`] = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 快捷选择相关计算属性
|
|
|
+const isAllWeekdaysSelected = computed(() => {
|
|
|
+ const weekdays = [2, 4, 8, 16, 32]
|
|
|
+ return weekdays.every(day => formData.weeks.includes(day))
|
|
|
+})
|
|
|
+
|
|
|
+const isWeekendSelected = computed(() => {
|
|
|
+ const weekend = [64, 128]
|
|
|
+ return weekend.every(day => formData.weeks.includes(day))
|
|
|
+})
|
|
|
+
|
|
|
+const isAllWeekSelected = computed(() => {
|
|
|
+ const allWeek = [2, 4, 8, 16, 32, 64, 128]
|
|
|
+ return allWeek.every(day => formData.weeks.includes(day))
|
|
|
+})
|
|
|
+
|
|
|
+const getSelectedWeekTags = computed(() => {
|
|
|
+ return formData.weeks.map(week => ({
|
|
|
+ value: week,
|
|
|
+ label: weekMap[week]?.label || week,
|
|
|
+ type: weekMap[week]?.type || ''
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+// 设备选择相关计算属性
|
|
|
+const isAllDevicesSelected = computed({
|
|
|
+ get() {
|
|
|
+ return deviceOptions.value.length > 0 && formData.device_ids.length === deviceOptions.value.length
|
|
|
+ },
|
|
|
+ set(value) {
|
|
|
+ if (value) {
|
|
|
+ formData.device_ids = deviceOptions.value.map(item => item.value)
|
|
|
+ } else {
|
|
|
+ formData.device_ids = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const isDeviceIndeterminate = computed(() => {
|
|
|
+ return formData.device_ids.length > 0 && formData.device_ids.length < deviceOptions.value.length
|
|
|
+})
|
|
|
+
|
|
|
+// 监听设备选择变化
|
|
|
+watch(() => formData.device_ids, (newVal) => {
|
|
|
+ if (newVal.length > 0 && channelCount.value > 0) {
|
|
|
+ // 确保路数配置存在,但不强制设置值
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ if (formData.timing_agreement[`Tag${i}`] === undefined) {
|
|
|
+ // 保持未配置状态
|
|
|
+ formData.timing_agreement[`Tag${i}`] = undefined
|
|
|
+ }
|
|
|
+ if (formData.channel_names[`Tag${i}`] === undefined) {
|
|
|
+ formData.channel_names[`Tag${i}`] = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+// 工具方法
|
|
|
+const getWeekTags = (weeks) => {
|
|
|
+ if (!weeks || !Array.isArray(weeks)) return []
|
|
|
+ return weeks.map(week => ({
|
|
|
+ value: week,
|
|
|
+ label: weekMap[week]?.label || week,
|
|
|
+ type: weekMap[week]?.type || ''
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+const getAgreementTags = (agreement, channelNames = null) => {
|
|
|
+ if (!agreement) return []
|
|
|
+ try {
|
|
|
+ const parsed = typeof agreement === 'string' ? JSON.parse(agreement) : agreement
|
|
|
+
|
|
|
+ const result = []
|
|
|
+ const processedTags = new Set()
|
|
|
+
|
|
|
+ // 收集所有的Tag和name信息
|
|
|
+ const tagStatus = {}
|
|
|
+ const nameInfo = {}
|
|
|
+
|
|
|
+ Object.keys(parsed).forEach(key => {
|
|
|
+ if (key.startsWith('Tag') || key.startsWith('tag')) {
|
|
|
+ const tagKey = key.startsWith('Tag') ? key : `Tag${key.replace('tag', '')}`
|
|
|
+ const num = tagKey.replace('Tag', '')
|
|
|
+ tagStatus[num] = parseInt(parsed[key])
|
|
|
+ } else if (key.startsWith('name')) {
|
|
|
+ const num = key.replace('name', '')
|
|
|
+ nameInfo[num] = parsed[key]
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理所有有配置的路数
|
|
|
+ const allNums = new Set([...Object.keys(tagStatus), ...Object.keys(nameInfo)])
|
|
|
+
|
|
|
+ allNums.forEach(num => {
|
|
|
+ if (processedTags.has(num)) return
|
|
|
+ processedTags.add(num)
|
|
|
+
|
|
|
+ const hasStatus = tagStatus.hasOwnProperty(num)
|
|
|
+ const hasName = nameInfo.hasOwnProperty(num)
|
|
|
+
|
|
|
+ if (hasStatus || hasName) {
|
|
|
+ let channelName = nameInfo[num] || `${num}路`
|
|
|
+ let label = ''
|
|
|
+ let type = 'info'
|
|
|
+ let configType = ''
|
|
|
+ let icon = null
|
|
|
+
|
|
|
+ if (hasStatus && hasName) {
|
|
|
+ // 完整配置:显示名称和状态
|
|
|
+ const status = tagStatus[num] === 1 ? '开启' : '关闭'
|
|
|
+ label = `${channelName}: ${status}`
|
|
|
+ type = tagStatus[num] === 1 ? 'success' : 'danger'
|
|
|
+ configType = 'full-config'
|
|
|
+ icon = tagStatus[num] === 1 ? 'Check' : 'Close'
|
|
|
+ } else if (hasStatus && !hasName) {
|
|
|
+ // 仅状态配置:显示默认名称和状态
|
|
|
+ const status = tagStatus[num] === 1 ? '开启' : '关闭'
|
|
|
+ label = `${num}路: ${status}`
|
|
|
+ type = tagStatus[num] === 1 ? 'success' : 'danger'
|
|
|
+ configType = 'status-only'
|
|
|
+ icon = tagStatus[num] === 1 ? 'Check' : 'Close'
|
|
|
+ } else if (hasName && !hasStatus) {
|
|
|
+ // 仅名称配置:只显示名称,标记为标识用
|
|
|
+ label = `${channelName} `
|
|
|
+ type = 'warning'
|
|
|
+ configType = 'name-only'
|
|
|
+ icon = 'PriceTag'
|
|
|
+ }
|
|
|
+
|
|
|
+ result.push({
|
|
|
+ label: label,
|
|
|
+ status: hasStatus ? (tagStatus[num] === 1 ? '开启' : '关闭') : '标识',
|
|
|
+ type: type,
|
|
|
+ configType: configType,
|
|
|
+ channelName: channelName,
|
|
|
+ tagNum: num,
|
|
|
+ hasStatus: hasStatus,
|
|
|
+ hasName: hasName,
|
|
|
+ icon: icon
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 按路数排序
|
|
|
+ result.sort((a, b) => parseInt(a.tagNum) - parseInt(b.tagNum))
|
|
|
+
|
|
|
+ return result
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析设备指令失败:', e)
|
|
|
+ return []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getChineseNumber = (num) => {
|
|
|
+ const numbers = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
|
|
+ return numbers[num] || num
|
|
|
+}
|
|
|
+
|
|
|
+const restoreChannelConfig = () => {
|
|
|
+ if (isEdit.value && originalChannelConfig.value.timing_agreement) {
|
|
|
+ formData.timing_agreement = { ...originalChannelConfig.value.timing_agreement }
|
|
|
+ formData.channel_names = { ...originalChannelConfig.value.channel_names }
|
|
|
+ ElMessage.success('已恢复原配置')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 星期选择相关方法
|
|
|
+const toggleWeekDay = (value) => {
|
|
|
+ const index = formData.weeks.indexOf(value)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ formData.weeks.push(value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleSpecialOption = (value) => {
|
|
|
+ const index = formData.weeks.indexOf(value)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ // 如果是节假日相关选项,需要互斥
|
|
|
+ if (value === 256) {
|
|
|
+ const notRunIndex = formData.weeks.indexOf(512)
|
|
|
+ if (notRunIndex > -1) {
|
|
|
+ formData.weeks.splice(notRunIndex, 1)
|
|
|
+ }
|
|
|
+ } else if (value === 512) {
|
|
|
+ const runIndex = formData.weeks.indexOf(256)
|
|
|
+ if (runIndex > -1) {
|
|
|
+ formData.weeks.splice(runIndex, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ formData.weeks.push(value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const removeWeekDay = (value) => {
|
|
|
+ const index = formData.weeks.indexOf(value)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const selectAllWeekdays = () => {
|
|
|
+ const weekdays = [2, 4, 8, 16, 32]
|
|
|
+ if (isAllWeekdaysSelected.value) {
|
|
|
+ weekdays.forEach(day => {
|
|
|
+ const index = formData.weeks.indexOf(day)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ weekdays.forEach(day => {
|
|
|
+ if (!formData.weeks.includes(day)) {
|
|
|
+ formData.weeks.push(day)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const selectWeekend = () => {
|
|
|
+ const weekend = [64, 128]
|
|
|
+ if (isWeekendSelected.value) {
|
|
|
+ weekend.forEach(day => {
|
|
|
+ const index = formData.weeks.indexOf(day)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ weekend.forEach(day => {
|
|
|
+ if (!formData.weeks.includes(day)) {
|
|
|
+ formData.weeks.push(day)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const selectAllWeek = () => {
|
|
|
+ const allWeek = [2, 4, 8, 16, 32, 64, 128]
|
|
|
+ if (isAllWeekSelected.value) {
|
|
|
+ allWeek.forEach(day => {
|
|
|
+ const index = formData.weeks.indexOf(day)
|
|
|
+ if (index > -1) {
|
|
|
+ formData.weeks.splice(index, 1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ allWeek.forEach(day => {
|
|
|
+ if (!formData.weeks.includes(day)) {
|
|
|
+ formData.weeks.push(day)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const clearAllWeeks = () => {
|
|
|
+ formData.weeks = []
|
|
|
+}
|
|
|
+
|
|
|
+// 设备选择相关方法
|
|
|
+const handleSelectAllDevices = (value) => {
|
|
|
+ if (value) {
|
|
|
+ formData.device_ids = deviceOptions.value.map(item => item.value)
|
|
|
+ } else {
|
|
|
+ formData.device_ids = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectTypeChange = () => {
|
|
|
+ // 切换选择方式时清空相关数据
|
|
|
+ formData.region_ids = ''
|
|
|
+ formData.group_id = ''
|
|
|
+ formData.category_id = ''
|
|
|
+ formData.device_type_id = ''
|
|
|
+ formData.device_ids = []
|
|
|
+ categoryOptions.value = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+}
|
|
|
+
|
|
|
+const handleLocationOrGroupChange = async (value, node) => {
|
|
|
+ // 清空相关数据
|
|
|
+ formData.category_id = ''
|
|
|
+ formData.device_type_id = ''
|
|
|
+ formData.device_ids = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+
|
|
|
+ if (value && node) {
|
|
|
+ // 从选中的节点获取完整的数据
|
|
|
+ formData.region_ids = node.id || node.value // 获取选中节点的id
|
|
|
+ formData.region_type = node.type // 获取选中节点的type
|
|
|
+
|
|
|
+ console.log('选中的区域数据:', {
|
|
|
+ id: formData.region_ids,
|
|
|
+ type: formData.region_type,
|
|
|
+ node: node
|
|
|
+ })
|
|
|
+
|
|
|
+ await fetchDeviceCategories()
|
|
|
+ } else {
|
|
|
+ // 如果没有选中值,清空相关字段
|
|
|
+ formData.region_ids = ''
|
|
|
+ formData.region_type = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理区域选择变化
|
|
|
+const handleRegionSelectChange = async (value) => {
|
|
|
+ // 清除之前的防抖定时器
|
|
|
+ if (debounceTimer) {
|
|
|
+ clearTimeout(debounceTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ debounceTimer = setTimeout(async () => {
|
|
|
+ console.log('区域选择变化:', value)
|
|
|
+
|
|
|
+ if (value) {
|
|
|
+ // 获取选中节点的完整数据
|
|
|
+ const selectedNode = regionTreeRef.value?.getCurrentNode?.() ||
|
|
|
+ regionTreeRef.value?.getNode?.(value)?.data
|
|
|
+
|
|
|
+ if (selectedNode) {
|
|
|
+ formData.region_ids = selectedNode.id
|
|
|
+ formData.region_type = selectedNode.type
|
|
|
+ formData.selectedRegionNode = selectedNode
|
|
|
+
|
|
|
+ console.log('区域选择变化:', {
|
|
|
+ value: value,
|
|
|
+ selectedNode: selectedNode
|
|
|
+ })
|
|
|
+
|
|
|
+ // 清空相关数据
|
|
|
+ resetDeviceSelections()
|
|
|
+
|
|
|
+ // 获取设备分类
|
|
|
+ await fetchDeviceCategories()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 清空选择
|
|
|
+ formData.region_ids = ''
|
|
|
+ formData.region_type = null
|
|
|
+ formData.selectedRegionNode = null
|
|
|
+ resetDeviceSelections()
|
|
|
+ }
|
|
|
+ }, 300) // 300ms 防抖
|
|
|
+}
|
|
|
+
|
|
|
+// 重置设备相关选择
|
|
|
+const resetDeviceSelections = () => {
|
|
|
+ formData.category_id = ''
|
|
|
+ formData.device_type_id = ''
|
|
|
+ formData.device_ids = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+}
|
|
|
+
|
|
|
+// 丰富节点信息,获取父级信息
|
|
|
+const enrichNodeWithParentInfo = async (node) => {
|
|
|
+ try {
|
|
|
+ const enrichedNode = { ...node }
|
|
|
+
|
|
|
+ // 如果是房间类型且没有parentName,尝试获取
|
|
|
+ if (node.type === 3 && !node.parentName && node.parentId) {
|
|
|
+ const parentNode = await getNodeById(node.parentId)
|
|
|
+ if (parentNode) {
|
|
|
+ enrichedNode.parentName = parentNode.name
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保有建筑信息
|
|
|
+ if (!enrichedNode.buildingName && enrichedNode.BuildingId) {
|
|
|
+ const buildingNode = await getNodeById(enrichedNode.BuildingId)
|
|
|
+ if (buildingNode) {
|
|
|
+ enrichedNode.buildingName = buildingNode.name
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return enrichedNode
|
|
|
+ } catch (error) {
|
|
|
+ console.error('丰富节点信息失败:', error)
|
|
|
+ return node
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 根据ID获取节点信息(可能需要调用API)
|
|
|
+const getNodeById = async (nodeId) => {
|
|
|
+ try {
|
|
|
+ // 首先在已加载的树数据中查找
|
|
|
+ const foundNode = findNodeInTree(regionTreeData.value, nodeId)
|
|
|
+ if (foundNode) {
|
|
|
+ return foundNode
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果在树中找不到,可能需要调用API获取
|
|
|
+ // 这里可以根据实际情况调用相应的API
|
|
|
+ return null
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取节点信息失败:', error)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 强制刷新树形选择器
|
|
|
+const refreshTreeSelect = async () => {
|
|
|
+ if (regionTreeRef.value) {
|
|
|
+ try {
|
|
|
+ // 清空树数据
|
|
|
+ regionTreeData.value = []
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ // 重新加载根节点
|
|
|
+ await loadRootRegions()
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ // 重新设置选中值
|
|
|
+ if (formData.region_ids) {
|
|
|
+ regionTreeRef.value.setCurrentKey(formData.region_ids)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('刷新树形选择器失败:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 区域树形加载方法
|
|
|
+const loadRegionNode = async (node, resolve) => {
|
|
|
+ try {
|
|
|
+ let params = {}
|
|
|
+ let url = `${__LOCAL_API__}/illuminating/getBuildingRegionRoomTree`
|
|
|
+
|
|
|
+ if (node.level === 0) {
|
|
|
+ params = {}
|
|
|
+ } else if (node.level === 1) {
|
|
|
+ params = { building_id: node.data.id }
|
|
|
+ } else {
|
|
|
+ params = {
|
|
|
+ building_id: node.data.BuildingId || getBuildingIdFromPath(node),
|
|
|
+ region_parent_id: node.data.id
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查缓存
|
|
|
+ const cacheKey = getCacheKey(url, params)
|
|
|
+ const cachedData = getCachedRequest(url, params)
|
|
|
+
|
|
|
+ if (isCacheValid(cachedData)) {
|
|
|
+ console.log('使用缓存数据:', cacheKey)
|
|
|
+ resolve(cachedData.data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否正在请求中
|
|
|
+ if (requestQueue.has(cacheKey)) {
|
|
|
+ console.log('等待现有请求:', cacheKey)
|
|
|
+ const existingRequest = requestQueue.get(cacheKey)
|
|
|
+ const result = await existingRequest
|
|
|
+ resolve(result)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新请求
|
|
|
+ const requestPromise = axios.get(url, { params }).then(response => {
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data || []
|
|
|
+ const treeData = data.map(item => ({
|
|
|
+ ...item,
|
|
|
+ id: item.id,
|
|
|
+ type: item.type,
|
|
|
+ BuildingId: item.BuildingId || params.building_id || item.id,
|
|
|
+ children: item.children_count > 0 ? [] : undefined,
|
|
|
+ RegionId: item.RegionId,
|
|
|
+ IsLastRegion: item.IsLastRegion,
|
|
|
+ RegionType: item.RegionType,
|
|
|
+ parentName: item.parentName,
|
|
|
+ parentId: item.parentId,
|
|
|
+ label: item.name,
|
|
|
+ value: item.id
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 缓存结果
|
|
|
+ setCachedRequest(url, params, treeData)
|
|
|
+ return treeData
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data.message || '获取区域数据失败')
|
|
|
+ }
|
|
|
+ }).finally(() => {
|
|
|
+ // 请求完成后从队列中移除
|
|
|
+ requestQueue.delete(cacheKey)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 将请求加入队列
|
|
|
+ requestQueue.set(cacheKey, requestPromise)
|
|
|
+
|
|
|
+ const result = await requestPromise
|
|
|
+ resolve(result)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取区域数据失败:', error)
|
|
|
+ ElMessage.error('获取区域数据失败,请检查网络连接')
|
|
|
+ resolve([])
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 辅助函数:从节点路径中获取建筑ID
|
|
|
+const getBuildingIdFromPath = (node) => {
|
|
|
+ let currentNode = node
|
|
|
+ while (currentNode && currentNode.level > 1) {
|
|
|
+ currentNode = currentNode.parent
|
|
|
+ }
|
|
|
+ return currentNode?.data?.BuildingId || currentNode?.data?.id
|
|
|
+}
|
|
|
+
|
|
|
+// 为编辑模式加载完整的区域路径
|
|
|
+const loadRegionPathForEdit = async (regionId, regionType, buildingId, parentId, regionName, fullPath) => {
|
|
|
+ try {
|
|
|
+ console.log('加载编辑区域路径:', { regionId, regionType, buildingId, parentId, regionName, fullPath })
|
|
|
+
|
|
|
+ // 增加树组件的key,强制重新渲染
|
|
|
+ treeKey.value++
|
|
|
+
|
|
|
+ // 等待组件重新渲染
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ // 1. 首先加载根节点(建筑列表)- 只在没有数据时加载
|
|
|
+ if (regionTreeData.value.length === 0) {
|
|
|
+ await loadRootRegions()
|
|
|
+ await nextTick()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 根据类型逐级展开节点
|
|
|
+ if (regionType >= 2 && buildingId) {
|
|
|
+ await expandBuildingNode(buildingId)
|
|
|
+ await nextTick()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (regionType === 3 && parentId) {
|
|
|
+ await expandRegionNode(parentId, buildingId)
|
|
|
+ await nextTick()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 设置选中值
|
|
|
+ formData.region_ids = regionId
|
|
|
+
|
|
|
+ // 4. 手动设置树的当前节点
|
|
|
+ if (regionTreeRef.value) {
|
|
|
+ try {
|
|
|
+ regionTreeRef.value.setCurrentKey(regionId)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('setCurrentKey失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('区域路径加载完成')
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载编辑区域路径失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载根节点区域
|
|
|
+const loadRootRegions = async () => {
|
|
|
+ try {
|
|
|
+ // 如果已经有数据,直接返回,避免重复加载
|
|
|
+ if (regionTreeData.value.length > 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getBuildingRegionRoomTree`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data || []
|
|
|
+
|
|
|
+ // 去重处理
|
|
|
+ const uniqueData = data.filter((item, index, self) =>
|
|
|
+ index === self.findIndex(t => t.id === item.id)
|
|
|
+ )
|
|
|
+
|
|
|
+ regionTreeData.value = uniqueData.map(item => ({
|
|
|
+ ...item,
|
|
|
+ id: item.id,
|
|
|
+ type: item.type,
|
|
|
+ BuildingId: item.id, // 建筑的BuildingId就是自己的id
|
|
|
+ children: item.children_count > 0 ? [] : undefined,
|
|
|
+ RegionId: item.RegionId,
|
|
|
+ IsLastRegion: item.IsLastRegion,
|
|
|
+ RegionType: item.RegionType,
|
|
|
+ parentName: item.parentName,
|
|
|
+ parentId: item.parentId || 0,
|
|
|
+ label: item.name,
|
|
|
+ value: item.id
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载根节点失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 展开建筑节点
|
|
|
+const expandBuildingNode = async (buildingId) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getBuildingRegionRoomTree`, {
|
|
|
+ params: { building_id: buildingId }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data || []
|
|
|
+
|
|
|
+ // 找到对应的建筑节点并更新其children
|
|
|
+ const updateBuildingChildren = (nodes) => {
|
|
|
+ for (let node of nodes) {
|
|
|
+ if (node.id === buildingId && node.type === 1) {
|
|
|
+ node.children = data.map(item => ({
|
|
|
+ ...item,
|
|
|
+ id: item.id,
|
|
|
+ type: item.type,
|
|
|
+ BuildingId: buildingId,
|
|
|
+ children: item.children_count > 0 ? [] : undefined,
|
|
|
+ RegionId: item.RegionId,
|
|
|
+ IsLastRegion: item.IsLastRegion,
|
|
|
+ RegionType: item.RegionType,
|
|
|
+ parentName: item.parentName,
|
|
|
+ parentId: item.parentId,
|
|
|
+ label: item.name,
|
|
|
+ value: item.id
|
|
|
+ }))
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ if (updateBuildingChildren(node.children)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ updateBuildingChildren(regionTreeData.value)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('展开建筑节点失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 展开区域节点
|
|
|
+const expandRegionNode = async (regionId, buildingId) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getBuildingRegionRoomTree`, {
|
|
|
+ params: {
|
|
|
+ building_id: buildingId,
|
|
|
+ region_parent_id: regionId
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data || []
|
|
|
+
|
|
|
+ // 找到对应的区域节点并更新其children
|
|
|
+ const updateRegionChildren = (nodes) => {
|
|
|
+ for (let node of nodes) {
|
|
|
+ if (node.id === regionId && node.type === 2) {
|
|
|
+ node.children = data.map(item => ({
|
|
|
+ ...item,
|
|
|
+ id: item.id,
|
|
|
+ type: item.type,
|
|
|
+ BuildingId: buildingId,
|
|
|
+ children: item.children_count > 0 ? [] : undefined,
|
|
|
+ RegionId: item.RegionId,
|
|
|
+ IsLastRegion: item.IsLastRegion,
|
|
|
+ RegionType: item.RegionType,
|
|
|
+ parentName: item.parentName,
|
|
|
+ parentId: item.parentId,
|
|
|
+ label: item.name,
|
|
|
+ value: item.id
|
|
|
+ }))
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ if (updateRegionChildren(node.children)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ updateRegionChildren(regionTreeData.value)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('展开区域节点失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 事件处理方法
|
|
|
+const handleSearch = () => {
|
|
|
+ pagination.currentPage = 1
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleReset = () => {
|
|
|
+ searchForm.searchText = ''
|
|
|
+ handleSearch()
|
|
|
+}
|
|
|
+
|
|
|
+const handleAdd = () => {
|
|
|
+ dialogTitle.value = '新增策略'
|
|
|
+ isEdit.value = false
|
|
|
+ resetForm()
|
|
|
+ dialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleEdit = async (row) => {
|
|
|
+ try {
|
|
|
+ dialogTitle.value = '编辑策略'
|
|
|
+ isEdit.value = true
|
|
|
+
|
|
|
+ // 先重置表单和清空树数据
|
|
|
+ resetForm()
|
|
|
+
|
|
|
+ // 先调用详情接口获取完整数据
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getTimingId`, {
|
|
|
+ params: { timing_id: row.timing_id }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data
|
|
|
+
|
|
|
+ // 解析region_ids,格式为 "2-5-1=09A/09A"
|
|
|
+ let regionId = ''
|
|
|
+ let regionType = 1
|
|
|
+ let buildingId = null
|
|
|
+ let parentId = 0
|
|
|
+ let selectedRegionNode = null
|
|
|
+ let regionName = ''
|
|
|
+ let fullPath = ''
|
|
|
+
|
|
|
+ if (data.region_ids) {
|
|
|
+ const parts = data.region_ids.split('=')
|
|
|
+ fullPath = parts[1] || ''
|
|
|
+ const idParts = parts[0].split('-')
|
|
|
+
|
|
|
+ if (idParts.length >= 2) {
|
|
|
+ regionType = parseInt(idParts[0])
|
|
|
+ regionId = parseInt(idParts[1])
|
|
|
+
|
|
|
+ if (idParts.length > 2) {
|
|
|
+ buildingId = parseInt(idParts[2])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从完整路径中解析当前节点名称
|
|
|
+ const pathParts = fullPath.split('/')
|
|
|
+ if (regionType === 1) {
|
|
|
+ regionName = pathParts[0] || ''
|
|
|
+ buildingId = regionId
|
|
|
+ } else if (regionType === 2) {
|
|
|
+ regionName = pathParts[pathParts.length - 1] || ''
|
|
|
+ } else if (regionType === 3) {
|
|
|
+ regionName = pathParts[pathParts.length - 1] || ''
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedRegionNode = {
|
|
|
+ id: regionId,
|
|
|
+ name: regionName,
|
|
|
+ type: regionType,
|
|
|
+ parentId: parentId,
|
|
|
+ RegionId: parentId || regionId,
|
|
|
+ BuildingId: buildingId,
|
|
|
+ buildingName: pathParts[0] || '',
|
|
|
+ parentName: pathParts.length > 2 ? pathParts[pathParts.length - 2] : '',
|
|
|
+ fullPath: fullPath
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置选择方式
|
|
|
+ deviceSelectType.value = data.group_id ? 'group' : 'region'
|
|
|
+
|
|
|
+ // 填充表单数据
|
|
|
+ Object.assign(formData, {
|
|
|
+ timing_id: data.timing_id,
|
|
|
+ timing_name: data.timing_name,
|
|
|
+ weeks: data.weeks || [],
|
|
|
+ timing_start_time: data.timing_start_time,
|
|
|
+ region_ids: regionId,
|
|
|
+ region_type: regionType,
|
|
|
+ selectedRegionNode: selectedRegionNode,
|
|
|
+ group_id: data.group_id || '',
|
|
|
+ category_id: data.category_id || '',
|
|
|
+ device_type_id: data.device_type_id || '',
|
|
|
+ device_ids: data.device_ids || []
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理路数配置
|
|
|
+ await handleTimingAgreementData(data)
|
|
|
+
|
|
|
+ // 先打开对话框
|
|
|
+ dialogVisible.value = true
|
|
|
+
|
|
|
+ // 等待DOM更新后再加载数据
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ // 然后异步加载区域树和其他数据
|
|
|
+ await Promise.all([
|
|
|
+ loadRegionPathForEdit(regionId, regionType, buildingId, parentId, regionName, fullPath),
|
|
|
+ loadEditData()
|
|
|
+ ])
|
|
|
+
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取策略详情失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载编辑数据失败:', error)
|
|
|
+ ElMessage.error('加载数据失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理设备指令数据
|
|
|
+const handleTimingAgreementData = async (data) => {
|
|
|
+ if (data.timing_agreement) {
|
|
|
+ try {
|
|
|
+ const agreement = typeof data.timing_agreement === 'string'
|
|
|
+ ? JSON.parse(data.timing_agreement)
|
|
|
+ : data.timing_agreement
|
|
|
+
|
|
|
+ console.log('原始设备配置:', agreement)
|
|
|
+
|
|
|
+ // 分离Tag配置和name配置
|
|
|
+ const tagAgreement = {}
|
|
|
+ const channelNames = {}
|
|
|
+ let maxTagNum = 0
|
|
|
+
|
|
|
+ // 先初始化所有路数为未配置状态
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ tagAgreement[`Tag${i}`] = undefined
|
|
|
+ channelNames[`Tag${i}`] = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ Object.keys(agreement).forEach(key => {
|
|
|
+ if (key.startsWith('Tag')) {
|
|
|
+ // 直接使用Tag1格式存储
|
|
|
+ tagAgreement[key] = parseInt(agreement[key])
|
|
|
+ const tagNum = parseInt(key.replace('Tag', ''))
|
|
|
+ maxTagNum = Math.max(maxTagNum, tagNum)
|
|
|
+ } else if (key.startsWith('name')) {
|
|
|
+ const tagNum = key.replace('name', '')
|
|
|
+ channelNames[`Tag${tagNum}`] = agreement[key]
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 如果没有从agreement中获取到name配置,尝试从设备数据中获取
|
|
|
+ if (Object.keys(channelNames).filter(key => channelNames[key]).length === 0 && data.devices && data.devices.length > 0) {
|
|
|
+ const firstDevice = data.devices[0]
|
|
|
+ if (firstDevice.devices_json_object) {
|
|
|
+ try {
|
|
|
+ const deviceData = JSON.parse(firstDevice.devices_json_object)
|
|
|
+ for (let i = 1; i <= maxTagNum; i++) {
|
|
|
+ if (deviceData[`name${i}`] && !channelNames[`Tag${i}`]) {
|
|
|
+ channelNames[`Tag${i}`] = deviceData[`name${i}`]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析设备数据失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ formData.timing_agreement = tagAgreement
|
|
|
+ formData.channel_names = channelNames
|
|
|
+ channelCount.value = Math.max(maxTagNum, channelCount.value)
|
|
|
+
|
|
|
+ // 保存原始配置用于恢复
|
|
|
+ originalChannelConfig.value = {
|
|
|
+ timing_agreement: { ...tagAgreement },
|
|
|
+ channel_names: { ...channelNames }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('解析后的配置:', {
|
|
|
+ tagAgreement,
|
|
|
+ channelNames,
|
|
|
+ maxTagNum
|
|
|
+ })
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析设备指令失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载编辑数据的辅助方法
|
|
|
+const loadEditData = async () => {
|
|
|
+ try {
|
|
|
+ // 1. 先加载设备分类
|
|
|
+ await fetchDeviceCategories()
|
|
|
+
|
|
|
+ // 2. 如果有分类ID,加载设备型号
|
|
|
+ if (formData.category_id) {
|
|
|
+ await fetchDeviceTypes()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 如果有设备型号ID,加载设备列表和路数信息
|
|
|
+ if (formData.device_type_id) {
|
|
|
+ await Promise.all([
|
|
|
+ fetchDevices(),
|
|
|
+ fetchChannelCount(formData.device_type_id)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载编辑数据失败:', error)
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDetail = (row) => {
|
|
|
+ detailData.value = row
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleStatusChange = async (row) => {
|
|
|
+ try {
|
|
|
+ await updateStrategyStatus(row.timing_id, row.timing_state)
|
|
|
+ ElMessage.success(`策略已${row.timing_state === 1 ? '启用' : '禁用'}`)
|
|
|
+ } catch (error) {
|
|
|
+ // 恢复原状态
|
|
|
+ row.timing_state = row.timing_state === 1 ? 0 : 1
|
|
|
+ ElMessage.error('状态更新失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (size) => {
|
|
|
+ pagination.pageSize = size
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleCurrentChange = (page) => {
|
|
|
+ pagination.currentPage = page
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const handleDialogClose = () => {
|
|
|
+ resetForm()
|
|
|
+}
|
|
|
+
|
|
|
+const handleCategoryChange = (categoryId) => {
|
|
|
+ formData.device_type_id = ''
|
|
|
+ formData.device_ids = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+
|
|
|
+ if (categoryId) {
|
|
|
+ fetchDeviceTypes()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDeviceTypeChange = (deviceTypeId) => {
|
|
|
+ formData.device_ids = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+
|
|
|
+ if (deviceTypeId) {
|
|
|
+ fetchDevices()
|
|
|
+ fetchChannelCount(deviceTypeId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDeviceChange = (deviceIds) => {
|
|
|
+ // 设备选择变化时的处理
|
|
|
+ if (deviceIds.length === 0) {
|
|
|
+ // 如果没有选择设备,清空路数配置
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ await formRef.value.validate()
|
|
|
+
|
|
|
+ submitLoading.value = true
|
|
|
+
|
|
|
+ // 构建region_ids格式
|
|
|
+ let regionIdsStr = ''
|
|
|
+ if (deviceSelectType.value === 'region' && formData.selectedRegionNode) {
|
|
|
+ regionIdsStr = buildRegionIdsString(formData.selectedRegionNode)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建完整的设备控制配置
|
|
|
+ const fullAgreement = {}
|
|
|
+ const fullCommitAgreement = {}
|
|
|
+
|
|
|
+ // 处理所有有配置的路数(包括只有名称的和只有状态的)
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ const tagKey = `Tag${i}`
|
|
|
+ const hasName = hasChannelName(i)
|
|
|
+ const hasStatus = hasChannelStatus(i)
|
|
|
+
|
|
|
+ if (hasName || hasStatus) {
|
|
|
+ // 如果有状态配置,添加状态
|
|
|
+ if (hasStatus) {
|
|
|
+ const lowerKey = tagKey.toLowerCase() // Tag1 -> tag1
|
|
|
+ const value = formData.timing_agreement[tagKey]
|
|
|
+ fullAgreement[lowerKey] = value.toString()
|
|
|
+ fullCommitAgreement[lowerKey] = value.toString()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有名称配置,添加名称
|
|
|
+ if (hasName) {
|
|
|
+ const nameKey = tagKey.replace('Tag', 'name') // Tag1 -> name1
|
|
|
+ const nameValue = formData.channel_names[tagKey]
|
|
|
+ fullAgreement[nameKey] = nameValue
|
|
|
+ fullCommitAgreement[nameKey] = nameValue
|
|
|
+ } else if (hasStatus) {
|
|
|
+ // 如果只有状态没有名称,使用默认名称
|
|
|
+ /*const nameKey = tagKey.replace('Tag', 'name')
|
|
|
+ const defaultName = `${i}路`
|
|
|
+ fullAgreement[nameKey] = defaultName
|
|
|
+ fullCommitAgreement[nameKey] = defaultName*/
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('完整的设备配置:', fullAgreement)
|
|
|
+
|
|
|
+ // 构建提交数据
|
|
|
+ const submitData = {
|
|
|
+ timing_name: formData.timing_name,
|
|
|
+ weeks: formData.weeks,
|
|
|
+ timing_start_time: formData.timing_start_time,
|
|
|
+ device_ids: formData.device_ids,
|
|
|
+ timing_agreement: JSON.stringify(fullAgreement),
|
|
|
+ commit_agreement: JSON.stringify(fullCommitAgreement),
|
|
|
+ timing_state: 1,
|
|
|
+ radio: 0
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据选择方式添加不同的参数
|
|
|
+ if (deviceSelectType.value === 'region') {
|
|
|
+ submitData.region_ids = regionIdsStr
|
|
|
+ } else {
|
|
|
+ submitData.group_id = formData.group_id
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是编辑模式,添加timing_id
|
|
|
+ if (isEdit.value) {
|
|
|
+ submitData.timing_id = formData.timing_id
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('提交的数据:', submitData)
|
|
|
+
|
|
|
+ if (isEdit.value) {
|
|
|
+ await updateStrategy(submitData)
|
|
|
+ ElMessage.success('编辑成功')
|
|
|
+ } else {
|
|
|
+ await createStrategy(submitData)
|
|
|
+ ElMessage.success('新增成功')
|
|
|
+ }
|
|
|
+
|
|
|
+ dialogVisible.value = false
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ if (error.message) {
|
|
|
+ ElMessage.error(error.message)
|
|
|
+ } else {
|
|
|
+ console.error('表单验证失败:', error)
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查找选中的区域节点
|
|
|
+const findSelectedRegionNode = (regionId) => {
|
|
|
+ // 从区域树中查找选中的节点
|
|
|
+ return findNodeInTree(regionTreeData.value, regionId)
|
|
|
+}
|
|
|
+
|
|
|
+// 构建region_ids字符串
|
|
|
+const buildRegionIdsString = (regionNode) => {
|
|
|
+ try {
|
|
|
+ console.log('构建region_ids,节点数据:', regionNode)
|
|
|
+
|
|
|
+ // 根据节点数据构建ID字符串
|
|
|
+ let idParts = []
|
|
|
+
|
|
|
+ // 添加type
|
|
|
+ if (regionNode.type !== undefined) {
|
|
|
+ idParts.push(regionNode.type)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加id
|
|
|
+ if (regionNode.id !== undefined) {
|
|
|
+ idParts.push(regionNode.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据类型添加第三个参数
|
|
|
+ if (regionNode.type === 2 && regionNode.BuildingId !== undefined) {
|
|
|
+ // 区域类型:type-id-buildingId
|
|
|
+ idParts.push(regionNode.BuildingId)
|
|
|
+ } else if (regionNode.type === 3 && regionNode.RegionId !== undefined) {
|
|
|
+ // 房间类型:type-id-regionId
|
|
|
+ idParts.push(regionNode.RegionId)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建完整的层级路径名称
|
|
|
+ const fullPath = buildFullPathName(regionNode)
|
|
|
+
|
|
|
+ // 组装最终的region_ids字符串
|
|
|
+ const idString = idParts.join('-')
|
|
|
+ const result = `${idString}=${fullPath}`
|
|
|
+
|
|
|
+ console.log('构建的region_ids:', result)
|
|
|
+
|
|
|
+ return result
|
|
|
+ } catch (error) {
|
|
|
+ console.error('构建region_ids失败:', error)
|
|
|
+ return `${regionNode.id}=${regionNode.name || ''}`
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 构建完整的层级路径名称(优化版)
|
|
|
+const buildFullPathName = (regionNode) => {
|
|
|
+ try {
|
|
|
+ const pathParts = []
|
|
|
+
|
|
|
+ // 根据节点类型构建路径
|
|
|
+ if (regionNode.type === 1) {
|
|
|
+ // 建筑类型:只显示建筑名称
|
|
|
+ pathParts.push(regionNode.name)
|
|
|
+ } else if (regionNode.type === 2) {
|
|
|
+ // 区域类型:建筑名称/区域名称
|
|
|
+ if (regionNode.buildingName) {
|
|
|
+ pathParts.push(regionNode.buildingName)
|
|
|
+ } else {
|
|
|
+ // 如果没有buildingName,尝试从树中获取
|
|
|
+ const buildingName = getBuildingNameById(regionNode.BuildingId)
|
|
|
+ if (buildingName) {
|
|
|
+ pathParts.push(buildingName)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ pathParts.push(regionNode.name)
|
|
|
+ } else if (regionNode.type === 3) {
|
|
|
+ // 房间类型:建筑名称/父区域名称/房间名称
|
|
|
+ if (regionNode.buildingName) {
|
|
|
+ pathParts.push(regionNode.buildingName)
|
|
|
+ } else {
|
|
|
+ const buildingName = getBuildingNameById(regionNode.BuildingId)
|
|
|
+ if (buildingName) {
|
|
|
+ pathParts.push(buildingName)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加父区域名称
|
|
|
+ if (regionNode.parentName) {
|
|
|
+ pathParts.push(regionNode.parentName)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加当前房间名称
|
|
|
+ pathParts.push(regionNode.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ return pathParts.join('/')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('构建路径名称失败:', error)
|
|
|
+ return regionNode.name || ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 根据建筑ID获取建筑名称
|
|
|
+const getBuildingNameById = (buildingId) => {
|
|
|
+ try {
|
|
|
+ // 从区域树数据中查找建筑名称
|
|
|
+ const building = findBuildingInTree(regionTreeData.value, buildingId)
|
|
|
+ return building ? building.name : null
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取建筑名称失败:', error)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 在树中查找建筑
|
|
|
+const findBuildingInTree = (treeData, buildingId) => {
|
|
|
+ for (const node of treeData) {
|
|
|
+ if (node.type === 1 && node.id === buildingId) {
|
|
|
+ return node
|
|
|
+ }
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ const found = findBuildingInTree(node.children, buildingId)
|
|
|
+ if (found) return found
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+// 递归查找树节点
|
|
|
+const findNodeInTree = (treeData, targetId) => {
|
|
|
+ for (const node of treeData) {
|
|
|
+ if (node.id === targetId) {
|
|
|
+ return node
|
|
|
+ }
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ const found = findNodeInTree(node.children, targetId)
|
|
|
+ if (found) return found
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null
|
|
|
+}
|
|
|
+
|
|
|
+const resetForm = () => {
|
|
|
+ // 清除防抖定时器
|
|
|
+ if (debounceTimer) {
|
|
|
+ clearTimeout(debounceTimer)
|
|
|
+ debounceTimer = null
|
|
|
+ }
|
|
|
+
|
|
|
+ Object.assign(formData, {
|
|
|
+ timing_id: null,
|
|
|
+ timing_name: '',
|
|
|
+ weeks: [],
|
|
|
+ timing_start_time: '',
|
|
|
+ region_ids: '',
|
|
|
+ group_id: '',
|
|
|
+ category_id: '',
|
|
|
+ device_type_id: '',
|
|
|
+ device_ids: [],
|
|
|
+ timing_agreement: {},
|
|
|
+ channel_names: {},
|
|
|
+ region_type: null,
|
|
|
+ selectedRegionNode: null
|
|
|
+ })
|
|
|
+
|
|
|
+ channelCount.value = 0
|
|
|
+ deviceSelectType.value = 'region'
|
|
|
+ categoryOptions.value = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+
|
|
|
+ // 清空树形数据缓存
|
|
|
+ regionTreeData.value = []
|
|
|
+
|
|
|
+ // 增加树组件的key,强制重新渲染
|
|
|
+ treeKey.value++
|
|
|
+
|
|
|
+ originalChannelConfig.value = {
|
|
|
+ timing_agreement: {},
|
|
|
+ channel_names: {}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除表单验证
|
|
|
+ if (formRef.value) {
|
|
|
+ formRef.value.clearValidate()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// API 调用方法 (获取定时策略列表)
|
|
|
+const fetchData = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ currentPage: pagination.currentPage,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ searchText: searchForm.searchText
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getDeviceOrgid`, {
|
|
|
+ params
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ tableData.value = response.data.data.Timing || []
|
|
|
+ pagination.total = response.data.data.count || 0
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取数据失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取列表数据失败:', error)
|
|
|
+ ElMessage.error('获取数据失败,请检查网络连接')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取设备分类
|
|
|
+const fetchDeviceCategories = async () => {
|
|
|
+ try {
|
|
|
+ // 必须有id和type才能查询
|
|
|
+ if (!formData.region_ids || !formData.region_type) {
|
|
|
+ console.log('缺少必要参数:', {
|
|
|
+ region_ids: formData.region_ids,
|
|
|
+ region_type: formData.region_type
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ const params = {
|
|
|
+ id: formData.region_ids,
|
|
|
+ type: formData.region_type
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('fetchDeviceCategories 请求参数:', params)
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/categoryPart`, {
|
|
|
+ params
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const categories = response.data.data.BaseDevicesCategorys || []
|
|
|
+ categoryOptions.value = categories.map(item => ({
|
|
|
+ label: item.category_name,
|
|
|
+ value: item.category_id
|
|
|
+ }))
|
|
|
+ console.log('获取到的设备分类:', categoryOptions.value)
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取设备分类失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取设备分类失败:', error)
|
|
|
+ ElMessage.error('获取设备分类失败,请检查网络连接')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取设备型号
|
|
|
+const fetchDeviceTypes = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ category_id: formData.category_id,
|
|
|
+ devices_enabled: -1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据选择方式添加不同的参数
|
|
|
+ if (deviceSelectType.value === 'region' && formData.region_ids) {
|
|
|
+ params.id = formData.region_ids
|
|
|
+ params.type = formData.region_type // 使用从区域选择中获取的真实type值
|
|
|
+ } else if (deviceSelectType.value === 'group' && formData.group_id) {
|
|
|
+ params.id = formData.group_id
|
|
|
+ params.type = 'group' // 或者根据实际情况设置group的type值
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('fetchDeviceTypes 请求参数:', params)
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getTypeFind`, {
|
|
|
+ params
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const deviceTypes = response.data.data.baseDeviceType || []
|
|
|
+ deviceTypeOptions.value = deviceTypes.map(item => ({
|
|
|
+ label: item.devices_type_name,
|
|
|
+ value: item.devices_type_id,
|
|
|
+ code: item.devices_type_code,
|
|
|
+ // 从dialect_agreements中获取路数信息
|
|
|
+ channelCount: getChannelCountFromDialects(item.dialect_agreements || [])
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取设备型号失败')
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取设备型号失败:', error)
|
|
|
+ ElMessage.error('获取设备型号失败,请检查网络连接')
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 从dialect_agreements中获取路数信息
|
|
|
+const getChannelCountFromDialects = (dialects) => {
|
|
|
+ const tagDialects = dialects.filter(dialect =>
|
|
|
+ dialect.dialect_key && dialect.dialect_key.startsWith('Tag')
|
|
|
+ )
|
|
|
+
|
|
|
+ if (tagDialects.length > 0) {
|
|
|
+ // 获取最大的Tag数字
|
|
|
+ const maxTag = Math.max(...tagDialects.map(dialect => {
|
|
|
+ const match = dialect.dialect_key.match(/Tag(\d+)/)
|
|
|
+ return match ? parseInt(match[1]) : 0
|
|
|
+ }))
|
|
|
+ return maxTag
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认返回8路
|
|
|
+ return 8
|
|
|
+}
|
|
|
+
|
|
|
+// 获取设备列表
|
|
|
+const fetchDevices = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ device_type_id: formData.device_type_id,
|
|
|
+ devices_enabled: -1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据选择方式添加不同的参数
|
|
|
+ if (deviceSelectType.value === 'region' && formData.region_ids) {
|
|
|
+ params.position_id = formData.region_ids
|
|
|
+ params.position_type = formData.region_type // 使用真实的type值,而不是固定的'region'
|
|
|
+ } else if (deviceSelectType.value === 'group' && formData.group_id) {
|
|
|
+ params.position_id = formData.group_id
|
|
|
+ params.position_type = 'group' // 或者根据实际情况设置
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('fetchDevices 请求参数:', params)
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/deviceAllMini`, {
|
|
|
+ params
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const devices = response.data.data.devices || []
|
|
|
+ deviceOptions.value = devices.map(item => ({
|
|
|
+ label: `${item.devices_name} (${item.full_region_name}/${item.room_name})`,
|
|
|
+ value: item.devices_id,
|
|
|
+ deviceInfo: item
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取设备列表失败')
|
|
|
+ deviceOptions.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取设备列表失败:', error)
|
|
|
+ ElMessage.error('获取设备列表失败,请检查网络连接')
|
|
|
+ deviceOptions.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取设备路数信息
|
|
|
+const fetchChannelCount = async (deviceTypeId) => {
|
|
|
+ try {
|
|
|
+ const deviceType = deviceTypeOptions.value.find(item => item.value === deviceTypeId)
|
|
|
+ if (deviceType && deviceType.channelCount) {
|
|
|
+ channelCount.value = deviceType.channelCount
|
|
|
+
|
|
|
+ // 初始化路数配置(设置为未配置状态)
|
|
|
+ if (!isEdit.value || Object.keys(formData.timing_agreement).length === 0) {
|
|
|
+ const agreement = {}
|
|
|
+ const names = {}
|
|
|
+ for (let i = 1; i <= deviceType.channelCount; i++) {
|
|
|
+ agreement[`Tag${i}`] = undefined // 设置为未配置状态
|
|
|
+ names[`Tag${i}`] = '' // 名称为空
|
|
|
+ }
|
|
|
+ formData.timing_agreement = agreement
|
|
|
+ formData.channel_names = names
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有找到路数信息,使用默认值
|
|
|
+ channelCount.value = 8
|
|
|
+ if (!isEdit.value || Object.keys(formData.timing_agreement).length === 0) {
|
|
|
+ const agreement = {}
|
|
|
+ const names = {}
|
|
|
+ for (let i = 1; i <= 8; i++) {
|
|
|
+ agreement[`Tag${i}`] = undefined // 设置为未配置状态
|
|
|
+ names[`Tag${i}`] = '' // 名称为空
|
|
|
+ }
|
|
|
+ formData.timing_agreement = agreement
|
|
|
+ formData.channel_names = names
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取路数信息失败:', error)
|
|
|
+ // 使用默认路数
|
|
|
+ channelCount.value = 8
|
|
|
+ if (!isEdit.value || Object.keys(formData.timing_agreement).length === 0) {
|
|
|
+ const agreement = {}
|
|
|
+ const names = {}
|
|
|
+ for (let i = 1; i <= 8; i++) {
|
|
|
+ agreement[`Tag${i}`] = undefined // 设置为未配置状态
|
|
|
+ names[`Tag${i}`] = '' // 名称为空
|
|
|
+ }
|
|
|
+ formData.timing_agreement = agreement
|
|
|
+ formData.channel_names = names
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建定时策略
|
|
|
+const createStrategy = async (data) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.post(`${__LOCAL_API__}/illuminating/timingSave`, data, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ return response.data
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data.message || '创建策略失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('创建策略失败:', error)
|
|
|
+ throw new Error(error.response?.data?.message || error.message || '创建策略失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 更新定时策略
|
|
|
+const updateStrategy = async (data) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.post(`${__LOCAL_API__}/illuminating/timingSave`, data, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ return response.data
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data.message || '更新策略失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新策略失败:', error)
|
|
|
+ throw new Error(error.response?.data?.message || error.message || '更新策略失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 更新策略状态
|
|
|
+const updateStrategyStatus = async (timingId, status) => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ timing_id: timingId,
|
|
|
+ timing_state: status
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/changeState`, {
|
|
|
+ params
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ return response.data
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data.message || '状态更新失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新状态失败:', error)
|
|
|
+ throw new Error(error.response?.data?.message || error.message || '状态更新失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除定时策略
|
|
|
+const deleteStrategy = async (timingId) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.delete(`${__LOCAL_API__}/illuminating/deleteTimingStrategy/${timingId}`)
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ return response.data
|
|
|
+ } else {
|
|
|
+ throw new Error(response.data.message || '删除策略失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除策略失败:', error)
|
|
|
+ throw new Error(error.response?.data?.message || error.message || '删除策略失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 批量操作方法
|
|
|
+const handleBatchDelete = async () => {
|
|
|
+ const selectedRows = tableData.value.filter(row => row.selected)
|
|
|
+
|
|
|
+ if (selectedRows.length === 0) {
|
|
|
+ ElMessage.warning('请选择要删除的策略')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要删除选中的 ${selectedRows.length} 个策略吗?`,
|
|
|
+ '批量删除',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const deletePromises = selectedRows.map(row => deleteStrategy(row.timing_id))
|
|
|
+ await Promise.all(deletePromises)
|
|
|
+
|
|
|
+ ElMessage.success('批量删除成功')
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ ElMessage.error('批量删除失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleBatchEnable = async (enable = true) => {
|
|
|
+ const selectedRows = tableData.value.filter(row => row.selected)
|
|
|
+
|
|
|
+ if (selectedRows.length === 0) {
|
|
|
+ ElMessage.warning('请选择要操作的策略')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const action = enable ? '启用' : '禁用'
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要${action}选中的 ${selectedRows.length} 个策略吗?`,
|
|
|
+ `批量${action}`,
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ const updatePromises = selectedRows.map(row =>
|
|
|
+ updateStrategyStatus(row.timing_id, enable ? 1 : 0)
|
|
|
+ )
|
|
|
+ await Promise.all(updatePromises)
|
|
|
+
|
|
|
+ ElMessage.success(`批量${action}成功`)
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ ElMessage.error(`批量${enable ? '启用' : '禁用'}失败`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 导出功能
|
|
|
+const handleExport = async () => {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/exportTimingStrategies`, {
|
|
|
+ params: {
|
|
|
+ searchText: searchForm.searchText
|
|
|
+ },
|
|
|
+ responseType: 'blob'
|
|
|
+ })
|
|
|
+
|
|
|
+ // 创建下载链接
|
|
|
+ const blob = new Blob([response.data], {
|
|
|
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
+ })
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = url
|
|
|
+ link.download = `定时策略_${new Date().toISOString().slice(0, 10)}.xlsx`
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
+
|
|
|
+ ElMessage.success('导出成功')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导出失败:', error)
|
|
|
+ ElMessage.error('导出失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 复制策略
|
|
|
+const handleCopyStrategy = async (row) => {
|
|
|
+ try {
|
|
|
+ dialogTitle.value = '复制策略'
|
|
|
+ isEdit.value = false
|
|
|
+
|
|
|
+ // 先调用详情接口获取完整数据
|
|
|
+ const response = await axios.get(`${__LOCAL_API__}/illuminating/getTimingId`, {
|
|
|
+ params: { timing_id: row.timing_id }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const data = response.data.data
|
|
|
+
|
|
|
+ // 解析region_ids
|
|
|
+ let regionId = ''
|
|
|
+ if (data.region_ids) {
|
|
|
+ const parts = data.region_ids.split('=')[0]
|
|
|
+ const idParts = parts.split('-')
|
|
|
+ regionId = idParts.length === 2 ? idParts[1] : idParts[0]
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置选择方式
|
|
|
+ deviceSelectType.value = data.group_id ? 'group' : 'region'
|
|
|
+
|
|
|
+ // 复制数据但不包含ID
|
|
|
+ Object.assign(formData, {
|
|
|
+ timing_id: null,
|
|
|
+ timing_name: `${data.timing_name}_副本`,
|
|
|
+ weeks: [...(data.weeks || [])],
|
|
|
+ timing_start_time: data.timing_start_time,
|
|
|
+ region_ids: regionId,
|
|
|
+ group_id: data.group_id || '',
|
|
|
+ category_id: data.category_id || '',
|
|
|
+ device_type_id: data.device_type_id || '',
|
|
|
+ device_ids: [...(data.device_ids || [])]
|
|
|
+ })
|
|
|
+
|
|
|
+ // 复制路数配置
|
|
|
+ if (data.timing_agreement) {
|
|
|
+ try {
|
|
|
+ const agreement = typeof data.timing_agreement === 'string'
|
|
|
+ ? JSON.parse(data.timing_agreement)
|
|
|
+ : data.timing_agreement
|
|
|
+
|
|
|
+ // 从设备数据中提取路数名称
|
|
|
+ const channelNames = {}
|
|
|
+ if (data.devices && data.devices.length > 0) {
|
|
|
+ const firstDevice = data.devices[0]
|
|
|
+ if (firstDevice.devices_json_object) {
|
|
|
+ try {
|
|
|
+ const deviceData = JSON.parse(firstDevice.devices_json_object)
|
|
|
+ for (let i = 1; i <= 12; i++) {
|
|
|
+ if (deviceData[`name${i}`]) {
|
|
|
+ channelNames[`Tag${i}`] = deviceData[`name${i}`]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析设备数据失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ formData.timing_agreement = { ...agreement }
|
|
|
+ formData.channel_names = { ...channelNames }
|
|
|
+
|
|
|
+ const tagCount = Object.keys(agreement).length
|
|
|
+ channelCount.value = tagCount
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析设备指令失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按顺序加载相关选项数据
|
|
|
+ await loadEditData()
|
|
|
+
|
|
|
+ dialogVisible.value = true
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.data.message || '获取策略详情失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载复制数据失败:', error)
|
|
|
+ ElMessage.error('复制策略失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 预览策略执行时间
|
|
|
+const previewExecutionTimes = (weeks, startTime) => {
|
|
|
+ if (!weeks || weeks.length === 0 || !startTime) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = new Date()
|
|
|
+ const times = []
|
|
|
+
|
|
|
+ // 获取未来7天的执行时间
|
|
|
+ for (let i = 0; i < 7; i++) {
|
|
|
+ const date = new Date(now)
|
|
|
+ date.setDate(date.getDate() + i)
|
|
|
+
|
|
|
+ const dayOfWeek = Math.pow(2, date.getDay() === 0 ? 7 : date.getDay()) // 转换为位值
|
|
|
+
|
|
|
+ if (weeks.includes(dayOfWeek)) {
|
|
|
+ const [hours, minutes] = startTime.split(':')
|
|
|
+ date.setHours(parseInt(hours), parseInt(minutes), 0, 0)
|
|
|
+
|
|
|
+ times.push({
|
|
|
+ date: date.toLocaleDateString(),
|
|
|
+ time: startTime,
|
|
|
+ dayName: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return times
|
|
|
+}
|
|
|
+
|
|
|
+// 验证策略冲突
|
|
|
+const validateStrategyConflict = async (formData) => {
|
|
|
+ try {
|
|
|
+ const response = await axios.post(`${__LOCAL_API__}/illuminating/validateTimingConflict`, {
|
|
|
+ weeks: formData.weeks,
|
|
|
+ timing_start_time: formData.timing_start_time,
|
|
|
+ device_ids: formData.device_ids,
|
|
|
+ timing_id: formData.timing_id // 编辑时传入,用于排除自身
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.data.code === 200) {
|
|
|
+ const conflicts = response.data.data.conflicts || []
|
|
|
+ if (conflicts.length > 0) {
|
|
|
+ const conflictMessages = conflicts.map(conflict =>
|
|
|
+ `策略"${conflict.timing_name}"在${conflict.conflict_time}存在冲突`
|
|
|
+ ).join('\n')
|
|
|
+
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `检测到以下时间冲突:\n${conflictMessages}\n\n是否继续保存?`,
|
|
|
+ '策略冲突警告',
|
|
|
+ {
|
|
|
+ confirmButtonText: '继续保存',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (error === 'cancel') {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ console.error('验证策略冲突失败:', error)
|
|
|
+ // 验证失败时允许继续,但给出提示
|
|
|
+ ElMessage.warning('无法验证策略冲突,请手动检查')
|
|
|
+ return true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理周数据转换
|
|
|
+const convertWeeksToArray = (weekValue) => {
|
|
|
+ if (Array.isArray(weekValue)) {
|
|
|
+ return weekValue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof weekValue === 'number') {
|
|
|
+ const weeks = []
|
|
|
+ const weekValues = [2, 4, 8, 16, 32, 64, 128, 256, 512]
|
|
|
+
|
|
|
+ weekValues.forEach(value => {
|
|
|
+ if (weekValue & value) {
|
|
|
+ weeks.push(value)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return weeks
|
|
|
+ }
|
|
|
+
|
|
|
+ return []
|
|
|
+}
|
|
|
+
|
|
|
+// 处理周数据转换为数字
|
|
|
+const convertWeeksToNumber = (weeksArray) => {
|
|
|
+ if (!Array.isArray(weeksArray)) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+
|
|
|
+ return weeksArray.reduce((sum, week) => sum + week, 0)
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化区域显示
|
|
|
+const formatRegionDisplay = (regionIds) => {
|
|
|
+ if (!regionIds) return ''
|
|
|
+
|
|
|
+ // 如果是 "1-1=南京芯片之城一期" 格式
|
|
|
+ if (regionIds.includes('=')) {
|
|
|
+ return regionIds.split('=')[1]
|
|
|
+ }
|
|
|
+
|
|
|
+ return regionIds
|
|
|
+}
|
|
|
+
|
|
|
+// 解析设备JSON对象获取路数名称
|
|
|
+const parseDeviceChannelNames = (devices) => {
|
|
|
+ const channelNames = {}
|
|
|
+
|
|
|
+ if (!devices || devices.length === 0) {
|
|
|
+ return channelNames
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从第一个设备获取路数名称配置
|
|
|
+ const firstDevice = devices[0]
|
|
|
+ if (firstDevice && firstDevice.devices_json_object) {
|
|
|
+ try {
|
|
|
+ const deviceData = JSON.parse(firstDevice.devices_json_object)
|
|
|
+
|
|
|
+ // 提取name1-name12的配置
|
|
|
+ for (let i = 1; i <= 12; i++) {
|
|
|
+ const nameKey = `name${i}`
|
|
|
+ if (deviceData[nameKey] && deviceData[nameKey].trim() !== '' && deviceData[nameKey] !== '备用') {
|
|
|
+ channelNames[`Tag${i}`] = deviceData[nameKey]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析设备JSON失败:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return channelNames
|
|
|
+}
|
|
|
+
|
|
|
+// 生成默认路数名称
|
|
|
+const generateDefaultChannelNames = (count) => {
|
|
|
+ const names = {}
|
|
|
+ for (let i = 1; i <= count; i++) {
|
|
|
+ names[`Tag${i}`] = `${getChineseNumber(i)}路`
|
|
|
+ }
|
|
|
+ return names
|
|
|
+}
|
|
|
+
|
|
|
+// 验证表单数据完整性
|
|
|
+const validateFormData = () => {
|
|
|
+ // 验证基本信息
|
|
|
+ if (!formData.timing_name.trim()) {
|
|
|
+ throw new Error('请输入策略名称')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.timing_start_time) {
|
|
|
+ throw new Error('请选择执行时间')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.weeks || formData.weeks.length === 0) {
|
|
|
+ throw new Error('请选择重复日期')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证设备选择
|
|
|
+ if (deviceSelectType.value === 'region' && !formData.region_ids) {
|
|
|
+ throw new Error('请选择所属位置')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (deviceSelectType.value === 'group' && !formData.group_id) {
|
|
|
+ throw new Error('请选择设备分组')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.category_id) {
|
|
|
+ throw new Error('请选择设备分类')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.device_type_id) {
|
|
|
+ throw new Error('请选择设备型号')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!formData.device_ids || formData.device_ids.length === 0) {
|
|
|
+ throw new Error('请选择至少一个设备')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 路数配置验证(可选)
|
|
|
+ if (channelCount.value > 0) {
|
|
|
+ // 验证已配置的路数名称长度
|
|
|
+ for (let i = 1; i <= channelCount.value; i++) {
|
|
|
+ const tagKey = `Tag${i}`
|
|
|
+ if (formData.timing_agreement[tagKey] !== undefined) {
|
|
|
+ const channelName = formData.channel_names[tagKey]
|
|
|
+ if (channelName && channelName.length > 20) {
|
|
|
+ throw new Error(`第${i}路的名称长度不能超过20个字符`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// 处理表格数据显示
|
|
|
+const processTableData = (data) => {
|
|
|
+ return data.map(item => ({
|
|
|
+ ...item,
|
|
|
+ // 处理周数据
|
|
|
+ weeks: convertWeeksToArray(item.timing_week || item.weeks),
|
|
|
+ // 格式化区域显示
|
|
|
+ region_display: formatRegionDisplay(item.region_ids),
|
|
|
+ // 处理设备指令显示
|
|
|
+ agreement_display: item.timing_agreement ?
|
|
|
+ (typeof item.timing_agreement === 'string' ? item.timing_agreement : JSON.stringify(item.timing_agreement)) : '',
|
|
|
+ }))
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化页面数据
|
|
|
+const initPageData = async () => {
|
|
|
+ try {
|
|
|
+ await fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化页面数据失败:', error)
|
|
|
+ ElMessage.error('初始化页面失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重置所有选择器
|
|
|
+const resetAllSelectors = () => {
|
|
|
+ formData.category_id = ''
|
|
|
+ formData.device_type_id = ''
|
|
|
+ formData.device_ids = []
|
|
|
+ categoryOptions.value = []
|
|
|
+ deviceTypeOptions.value = []
|
|
|
+ deviceOptions.value = []
|
|
|
+ channelCount.value = 0
|
|
|
+ formData.timing_agreement = {}
|
|
|
+ formData.channel_names = {}
|
|
|
+}
|
|
|
+
|
|
|
+// 处理区域选择变化
|
|
|
+const handleRegionChange = (value) => {
|
|
|
+ formData.region_ids = value
|
|
|
+ resetAllSelectors()
|
|
|
+
|
|
|
+ if (value) {
|
|
|
+ fetchDeviceCategories()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理分组选择变化
|
|
|
+const handleGroupChange = (value) => {
|
|
|
+ formData.group_id = value
|
|
|
+ resetAllSelectors()
|
|
|
+
|
|
|
+ if (value) {
|
|
|
+ fetchDeviceCategories()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取设备型号的路数配置
|
|
|
+const getDeviceTypeChannelConfig = async (deviceTypeId) => {
|
|
|
+ try {
|
|
|
+ const deviceType = deviceTypeOptions.value.find(item => item.value === deviceTypeId)
|
|
|
+ if (!deviceType) {
|
|
|
+ return { count: 8, names: generateDefaultChannelNames(8) }
|
|
|
+ }
|
|
|
+
|
|
|
+ const count = deviceType.channelCount || 8
|
|
|
+ const names = generateDefaultChannelNames(count)
|
|
|
+
|
|
|
+ return { count, names }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取设备型号路数配置失败:', error)
|
|
|
+ return { count: 8, names: generateDefaultChannelNames(8) }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 更新路数配置
|
|
|
+const updateChannelConfig = async (deviceTypeId) => {
|
|
|
+ try {
|
|
|
+ const { count, names } = await getDeviceTypeChannelConfig(deviceTypeId)
|
|
|
+
|
|
|
+ channelCount.value = count
|
|
|
+
|
|
|
+ // 如果是新增或者没有现有配置,初始化配置
|
|
|
+ if (!isEdit.value || Object.keys(formData.timing_agreement).length === 0) {
|
|
|
+ const agreement = {}
|
|
|
+ for (let i = 1; i <= count; i++) {
|
|
|
+ agreement[`Tag${i}`] = 0
|
|
|
+ }
|
|
|
+ formData.timing_agreement = agreement
|
|
|
+ formData.channel_names = { ...names }
|
|
|
+ } else {
|
|
|
+ // 编辑模式下,确保路数名称存在
|
|
|
+ for (let i = 1; i <= count; i++) {
|
|
|
+ if (!formData.channel_names[`Tag${i}`]) {
|
|
|
+ formData.channel_names[`Tag${i}`] = names[`Tag${i}`] || `${i}路`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新路数配置失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理编辑数据的特殊逻辑
|
|
|
+const handleEditDataSpecial = (data) => {
|
|
|
+ // 处理周数据
|
|
|
+ if (data.timing_week !== undefined) {
|
|
|
+ formData.weeks = convertWeeksToArray(data.timing_week)
|
|
|
+ } else if (data.weeks) {
|
|
|
+ formData.weeks = Array.isArray(data.weeks) ? data.weeks : convertWeeksToArray(data.weeks)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理区域ID
|
|
|
+ if (data.region_ids) {
|
|
|
+ const parts = data.region_ids.split('=')[0]
|
|
|
+ const idParts = parts.split('-')
|
|
|
+ formData.region_ids = idParts.length === 2 ? idParts[1] : idParts[0]
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理设备指令和路数名称
|
|
|
+ if (data.timing_agreement) {
|
|
|
+ try {
|
|
|
+ const agreement = typeof data.timing_agreement === 'string'
|
|
|
+ ? JSON.parse(data.timing_agreement)
|
|
|
+ : data.timing_agreement
|
|
|
+
|
|
|
+ formData.timing_agreement = { ...agreement }
|
|
|
+
|
|
|
+ // 从设备数据中提取路数名称
|
|
|
+ const deviceChannelNames = parseDeviceChannelNames(data.devices)
|
|
|
+
|
|
|
+ // 合并路数名称(优先使用设备中的名称)
|
|
|
+ const channelNames = { ...generateDefaultChannelNames(Object.keys(agreement).length) }
|
|
|
+ Object.assign(channelNames, deviceChannelNames)
|
|
|
+
|
|
|
+ formData.channel_names = channelNames
|
|
|
+
|
|
|
+ // 设置路数
|
|
|
+ channelCount.value = Object.keys(agreement).length
|
|
|
+
|
|
|
+ // 保存原始配置
|
|
|
+ originalChannelConfig.value = {
|
|
|
+ timing_agreement: { ...agreement },
|
|
|
+ channel_names: { ...channelNames }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理设备指令失败:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 生命周期
|
|
|
+onMounted(() => {
|
|
|
+ initPageData()
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+// 导出组件需要的方法和数据(如果需要的话)
|
|
|
+defineExpose({
|
|
|
+ fetchData,
|
|
|
+ resetForm,
|
|
|
+ handleAdd,
|
|
|
+ handleEdit
|
|
|
+})
|
|
|
+
|
|
|
+// 添加清理过期缓存的方法
|
|
|
+const clearExpiredCache = () => {
|
|
|
+ const now = Date.now()
|
|
|
+ for (const [key, value] of requestCache.entries()) {
|
|
|
+ if (now - value.timestamp > value.expiry) {
|
|
|
+ requestCache.delete(key)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 在组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ if (debounceTimer) {
|
|
|
+ clearTimeout(debounceTimer)
|
|
|
+ }
|
|
|
+ requestCache.clear()
|
|
|
+ requestQueue.clear()
|
|
|
+})
|
|
|
+
|
|
|
+// 定期清理过期缓存
|
|
|
+setInterval(clearExpiredCache, 1800000) // 每分钟清理一次
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.timing-strategy-page {
|
|
|
+ padding: 24px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: 90vh;
|
|
|
+}
|
|
|
+
|
|
|
+/* 页面标题 */
|
|
|
+.page-header {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0 0 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.page-description {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 卡片样式 */
|
|
|
+.search-card,
|
|
|
+.table-card {
|
|
|
+ margin-bottom: 24px;
|
|
|
+ border-radius: 12px;
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header .header-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.count-tag {
|
|
|
+ margin-left: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 搜索表单 */
|
|
|
+.search-form {
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form--inline .el-form-item {
|
|
|
+ display: inline-flex;
|
|
|
+ margin-right: 32px;
|
|
|
+ vertical-align: baseline;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input {
|
|
|
+ width: 280px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格样式 */
|
|
|
+.strategy-table {
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding: 12px 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-name {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.name-icon {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.weeks-container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.week-tag {
|
|
|
+ margin: 2px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.time-display {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-display {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-tag {
|
|
|
+ margin: 2px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分页 */
|
|
|
+.pagination-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗样式 */
|
|
|
+.strategy-dialog :deep(.el-dialog__header) {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ border-radius: 12px 12px 0 0;
|
|
|
+ padding: 20px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-dialog :deep(.el-dialog__title) {
|
|
|
+ color: white;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-dialog :deep(.el-dialog__body) {
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单样式 */
|
|
|
+.strategy-form {
|
|
|
+ max-height: 60vh;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-section {
|
|
|
+ margin-bottom: 32px;
|
|
|
+ padding: 20px;
|
|
|
+ background: #fafbfc;
|
|
|
+ border-radius: 8px;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin: 0 0 20px 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备选择样式 */
|
|
|
+.device-select-container {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.device-select-header {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+ background: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.device-count-info {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路数配置操作按钮 */
|
|
|
+.channel-operations {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 16px;
|
|
|
+ background: #f0f9ff;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #b3d8ff;
|
|
|
+}
|
|
|
+
|
|
|
+.operation-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-status-info {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 重复日期选择样式 */
|
|
|
+.weeks-selection {
|
|
|
+ width: 100%;
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-select-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-select-buttons .el-button {
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.quick-select-buttons .el-button:hover {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.week-selection-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.week-days-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(7, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item {
|
|
|
+ position: relative;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 16px 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ user-select: none;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item:hover::before {
|
|
|
+ opacity: 0.1;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item.selected {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-color: #667eea;
|
|
|
+ color: white;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item.weekend {
|
|
|
+ background: #fff5f5;
|
|
|
+ border-color: #fed7d7;
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item.weekend.selected {
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ border-color: #f093fb;
|
|
|
+}
|
|
|
+
|
|
|
+.day-content {
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.day-name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.day-en {
|
|
|
+ font-size: 11px;
|
|
|
+ opacity: 0.7;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.day-icon {
|
|
|
+ position: absolute;
|
|
|
+ top: -8px;
|
|
|
+ right: -8px;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ background: #67c23a;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 12px;
|
|
|
+ opacity: 0;
|
|
|
+ transform: scale(0);
|
|
|
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
|
+}
|
|
|
+
|
|
|
+.week-day-item.selected .day-icon {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1);
|
|
|
+}
|
|
|
+
|
|
|
+/* 特殊选项 */
|
|
|
+.special-options {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.special-option-group {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.option-group-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0 0 12px 0;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.special-checkboxes {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.special-checkbox {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 10px 16px;
|
|
|
+ background: white;
|
|
|
+ border: 2px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ user-select: none;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.special-checkbox:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.special-checkbox.checked {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.checkbox-icon {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 3px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: white;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.special-checkbox.checked .checkbox-icon {
|
|
|
+ background: #409eff;
|
|
|
+ border-color: #409eff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.checkbox-label {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 选择结果预览 */
|
|
|
+.selection-preview {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 16px;
|
|
|
+ background: #f0f9ff;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #b3d8ff;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #409eff;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-tags {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-tag {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-tag:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备选择样式 */
|
|
|
+.select-type-radio {
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.select-type-radio .el-radio {
|
|
|
+ margin-right: 0;
|
|
|
+ padding: 12px 20px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: white;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.select-type-radio .el-radio:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.select-type-radio .el-radio.is-checked {
|
|
|
+ border-color: #409eff;
|
|
|
+ background: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.select-type-radio .el-radio :deep(.el-radio__input) {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.select-type-radio .el-radio :deep(.el-radio__label) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路数配置样式 */
|
|
|
+.channel-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-item {
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 16px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-item:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.channel-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-number {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ min-width: 40px;
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路数名称表单项样式 */
|
|
|
+.channel-name-form-item {
|
|
|
+ flex: 1;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-name-form-item :deep(.el-form-item__content) {
|
|
|
+ margin-left: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-name-form-item :deep(.el-form-item__error) {
|
|
|
+ position: static;
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-name-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-control {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-radio {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-radio .el-radio {
|
|
|
+ margin: 0;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: white;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.radio-off.is-checked {
|
|
|
+ border-color: #f56c6c;
|
|
|
+ background: #fef0f0;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.radio-on.is-checked {
|
|
|
+ border-color: #67c23a;
|
|
|
+ background: #f0f9ff;
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-radio .el-radio :deep(.el-radio__input) {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-radio .el-radio :deep(.el-radio__label) {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 0;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 弹窗底部 */
|
|
|
+.dialog-footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 20px 24px;
|
|
|
+ background: #fafbfc;
|
|
|
+ border-radius: 0 0 12px 12px;
|
|
|
+ margin: 0 -24px -24px -24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 详情弹窗 */
|
|
|
+.detail-dialog :deep(.el-dialog__header) {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ border-radius: 12px 12px 0 0;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-content {
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section h4 {
|
|
|
+ margin: 0 0 16px 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 2px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-detail {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.agreement-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 6px;
|
|
|
+ border-left: 3px solid #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-name {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 树形选择器样式优化 */
|
|
|
+.el-tree-select :deep(.el-tree-node__content) {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select :deep(.el-tree-node__content):hover {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select :deep(.el-tree-node__expand-icon) {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select :deep(.el-tree-node__loading-icon) {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .channel-grid {
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-days-grid {
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .timing-strategy-page {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-title {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-input {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form .el-form-item {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-days-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-day-item {
|
|
|
+ padding: 12px 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .day-name {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .day-en {
|
|
|
+ font-size: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .quick-select-buttons {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .special-checkboxes {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .channel-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .agreement-detail {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-buttons {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .strategy-dialog {
|
|
|
+ width: 95% !important;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-dialog {
|
|
|
+ width: 95% !important;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .weeks-selection {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-tags {
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .channel-operations {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .operation-buttons {
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .select-type-radio {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 480px) {
|
|
|
+ .week-days-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-day-item {
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .quick-select-buttons {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ display: grid;
|
|
|
+ }
|
|
|
+
|
|
|
+ .channel-radio {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .special-checkboxes {
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .special-checkbox {
|
|
|
+ padding: 8px 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 滚动条样式 */
|
|
|
+.strategy-form::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-form::-webkit-scrollbar-track {
|
|
|
+ background: #f1f1f1;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-form::-webkit-scrollbar-thumb {
|
|
|
+ background: #c1c1c1;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-form::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #a8a8a8;
|
|
|
+}
|
|
|
+
|
|
|
+/* 动画效果 */
|
|
|
+.el-card {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table__row {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table__row:hover {
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 设备选择下拉框头部样式 */
|
|
|
+.device-select-header :deep(.el-checkbox) {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.device-select-header :deep(.el-checkbox__label) {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单验证错误样式优化 */
|
|
|
+.el-form-item.is-error :deep(.el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px #f56c6c inset;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form-item.is-error :deep(.el-select .el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px #f56c6c inset;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form-item.is-error :deep(.el-tree-select .el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px #f56c6c inset;
|
|
|
+}
|
|
|
+
|
|
|
+/* 提交按钮加载状态 */
|
|
|
+.el-button.is-loading {
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路数配置状态指示 */
|
|
|
+.channel-status-info {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.channel-status-info::before {
|
|
|
+ content: '';
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #67c23a;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化选择器样式 */
|
|
|
+.el-select-dropdown__item.selected {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化时间选择器样式 */
|
|
|
+.el-time-picker {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.el-time-picker :deep(.el-input__wrapper) {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-time-picker :deep(.el-input__wrapper):hover {
|
|
|
+ box-shadow: 0 0 0 1px #409eff inset;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化多选标签样式 */
|
|
|
+.el-select :deep(.el-tag) {
|
|
|
+ margin: 2px 4px 2px 0;
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ border-color: #b3d8ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-select :deep(.el-tag .el-tag__close) {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-select :deep(.el-tag .el-tag__close):hover {
|
|
|
+ background-color: #409eff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化开关样式 */
|
|
|
+.el-switch.is-checked :deep(.el-switch__core) {
|
|
|
+ background-color: #13ce66;
|
|
|
+}
|
|
|
+
|
|
|
+.el-switch :deep(.el-switch__core) {
|
|
|
+ background-color: #ff4949;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化描述列表样式 */
|
|
|
+.el-descriptions :deep(.el-descriptions__header) {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-descriptions :deep(.el-descriptions__title) {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.el-descriptions :deep(.el-descriptions__body) {
|
|
|
+ background-color: #fafbfc;
|
|
|
+}
|
|
|
+
|
|
|
+.el-descriptions :deep(.el-descriptions-item__label) {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.el-descriptions :deep(.el-descriptions-item__content) {
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 工具提示样式 */
|
|
|
+.el-tooltip__popper {
|
|
|
+ max-width: 300px;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+/* 过渡动画 */
|
|
|
+.week-day-item,
|
|
|
+.special-checkbox,
|
|
|
+.preview-tag {
|
|
|
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+}
|
|
|
+
|
|
|
+/* 焦点状态 */
|
|
|
+.week-day-item:focus,
|
|
|
+.special-checkbox:focus {
|
|
|
+ outline: 2px solid #409eff;
|
|
|
+ outline-offset: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 禁用状态 */
|
|
|
+.week-day-item.disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 减少动画模式支持 */
|
|
|
+@media (prefers-reduced-motion: reduce) {
|
|
|
+ .week-day-item,
|
|
|
+ .special-checkbox,
|
|
|
+ .preview-tag,
|
|
|
+ .day-icon {
|
|
|
+ transition: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 树形选择器下拉面板样式 */
|
|
|
+.el-tree-select-dropdown {
|
|
|
+ max-height: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree) {
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node) {
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node__content) {
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node__content):hover {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node__label) {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node.is-current .el-tree-node__label) {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载状态样式 */
|
|
|
+.el-tree-select-dropdown :deep(.el-tree-node__loading-icon) {
|
|
|
+ color: #409eff;
|
|
|
+ animation: rotating 2s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes rotating {
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 空状态样式 */
|
|
|
+.el-tree-select-dropdown :deep(.el-tree__empty-block) {
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化表格行高 */
|
|
|
+.strategy-table :deep(.el-table__row) {
|
|
|
+ height: 60px;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-table :deep(.el-table__cell) {
|
|
|
+ padding: 12px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化表格头部样式 */
|
|
|
+.strategy-table :deep(.el-table__header-wrapper) {
|
|
|
+ background: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.strategy-table :deep(.el-table__header th) {
|
|
|
+ background: #f8f9fa;
|
|
|
+ color: #495057;
|
|
|
+ font-weight: 600;
|
|
|
+ border-bottom: 2px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化分页样式 */
|
|
|
+.el-pagination {
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.el-pagination__total) {
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.btn-prev),
|
|
|
+.el-pagination :deep(.btn-next) {
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.btn-prev):hover,
|
|
|
+.el-pagination :deep(.btn-next):hover {
|
|
|
+ color: #409eff;
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.el-pager li) {
|
|
|
+ border-radius: 6px;
|
|
|
+ margin: 0 2px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.el-pager li:hover) {
|
|
|
+ color: #409eff;
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+.el-pagination :deep(.el-pager li.is-active) {
|
|
|
+ background-color: #409eff;
|
|
|
+ color: white;
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化消息提示样式 */
|
|
|
+.el-message {
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.el-message.el-message--success {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ border-color: #b3d8ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-message.el-message--error {
|
|
|
+ background-color: #fef0f0;
|
|
|
+ border-color: #fbc4c4;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化确认框样式 */
|
|
|
+.el-message-box {
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.el-message-box__header {
|
|
|
+ padding: 20px 24px 16px;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+.el-message-box__title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.el-message-box__content {
|
|
|
+ padding: 20px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-message-box__btns {
|
|
|
+ padding: 16px 24px 20px;
|
|
|
+ border-top: 1px solid #e9ecef;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化加载动画 */
|
|
|
+.el-loading-mask {
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+}
|
|
|
+
|
|
|
+.el-loading-spinner {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化表单项间距 */
|
|
|
+.el-form-item {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form-item:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化输入框样式 */
|
|
|
+.el-input__wrapper {
|
|
|
+ border-radius: 6px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-input__wrapper:hover {
|
|
|
+ box-shadow: 0 0 0 1px #c0c4cc inset;
|
|
|
+}
|
|
|
+
|
|
|
+.el-input__wrapper.is-focus {
|
|
|
+ box-shadow: 0 0 0 1px #409eff inset;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化选择器样式 */
|
|
|
+.el-select .el-input__wrapper {
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.el-select .el-input__wrapper:hover {
|
|
|
+ box-shadow: 0 0 0 1px #c0c4cc inset;
|
|
|
+}
|
|
|
+
|
|
|
+.el-select .el-input__wrapper.is-focus {
|
|
|
+ box-shadow: 0 0 0 1px #409eff inset;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化按钮样式 */
|
|
|
+.el-button {
|
|
|
+ border-radius: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button:hover {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--primary {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #3a8ee6 100%);
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--primary:hover {
|
|
|
+ background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--success {
|
|
|
+ background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
|
|
|
+ border-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button--success:hover {
|
|
|
+ background: linear-gradient(135deg, #85ce61 0%, #67c23a 100%);
|
|
|
+ box-shadow: 0 4px 12px rgba(103, 194, 58, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化标签样式 */
|
|
|
+.el-tag {
|
|
|
+ border-radius: 4px;
|
|
|
+ font-weight: 500;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--primary {
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ border-color: #b3d8ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--success {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ border-color: #b3e19d;
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--info {
|
|
|
+ background-color: #f4f4f5;
|
|
|
+ border-color: #d3d4d6;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--warning {
|
|
|
+ background-color: #fdf6ec;
|
|
|
+ border-color: #f5dab1;
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag--danger {
|
|
|
+ background-color: #fef0f0;
|
|
|
+ border-color: #fbc4c4;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+/* 打印样式 */
|
|
|
+@media print {
|
|
|
+ .timing-strategy-page {
|
|
|
+ background: white;
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-card,
|
|
|
+ .pagination-container,
|
|
|
+ .action-buttons {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-card {
|
|
|
+ box-shadow: none;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ }
|
|
|
+
|
|
|
+ .strategy-table {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 高对比度模式支持 */
|
|
|
+@media (prefers-contrast: high) {
|
|
|
+ .week-day-item {
|
|
|
+ border-width: 3px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-day-item.selected {
|
|
|
+ border-width: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .special-checkbox {
|
|
|
+ border-width: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ border-width: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 深色模式支持 */
|
|
|
+@media (prefers-color-scheme: dark) {
|
|
|
+ .timing-strategy-page {
|
|
|
+ background: #1a1a1a;
|
|
|
+ color: #e5e5e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-card,
|
|
|
+ .table-card {
|
|
|
+ background: #2d2d2d;
|
|
|
+ border-color: #404040;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-section {
|
|
|
+ background: #2a2a2a;
|
|
|
+ border-left-color: #409eff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-day-item {
|
|
|
+ background: #3a3a3a;
|
|
|
+ border-color: #505050;
|
|
|
+ color: #e5e5e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .week-day-item:hover {
|
|
|
+ background: #404040;
|
|
|
+ }
|
|
|
+
|
|
|
+ .channel-item {
|
|
|
+ background: #3a3a3a;
|
|
|
+ border-color: #505050;
|
|
|
+ }
|
|
|
+}
|
|
|
+.action-btn {
|
|
|
+ min-width: 60px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 12px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ border: 1px solid transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.action-btn {
|
|
|
+ width: 40%;
|
|
|
+ min-width: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn {
|
|
|
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn:hover:not(:disabled) {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.control-btn:disabled {
|
|
|
+ background: #c0c4cc;
|
|
|
+ color: #ffffff;
|
|
|
+ cursor: not-allowed;
|
|
|
+ transform: none;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+</style>
|