CarouselConfig.vue 8.55 KB
<template>
  <el-dialog
    title="轮播策略配置"
    :visible.sync="visible"
    width="650px"
    append-to-body
    @close="resetForm"
  >
    <el-form ref="form" :model="form" label-width="120px" :rules="rules">

      <el-form-item label="轮播范围" prop="sourceType">
        <el-radio-group v-model="form.sourceType">
          <el-radio label="all_online">所有在线设备 (自动同步)</el-radio>
          <el-radio label="custom">手动选择设备</el-radio>
        </el-radio-group>

        <div v-show="form.sourceType === 'custom'" class="device-select-box">
          <el-input
            placeholder="搜索设备名称"
            v-model="filterText"
            size="mini"
            prefix-icon="el-icon-search"
            clearable
            style="margin-bottom: 5px;"
            @input="handleFilterInput">
          </el-input>

          <vue-easy-tree
            ref="deviceTree"
            :data="deviceTreeData"
            :props="treeProps"
            show-checkbox
            node-key="code"
            :filter-node-method="filterNode"
            height="250px"
            style="height: 250px; overflow-y: auto; border: 1px solid #dcdfe6; border-radius: 4px; padding: 5px;"
          ></vue-easy-tree>
        </div>

        <div class="tip-text">
          <i class="el-icon-info"></i>
          {{ form.sourceType === 'all_online'
          ? '将自动从左侧设备列表中筛选状态为"在线"的设备进行循环播放。'
          : '请勾选上方需要轮播的设备或通道。搜索时,之前勾选的设备会被保留。'
          }}
          <div style="margin-top: 5px; font-weight: bold; color: #E6A23C;">⚠️ 为保证播放流畅,轮播间隔建议设置为45秒以上</div>
        </div>
      </el-form-item>

      <el-form-item label="分屏布局" prop="layout">
        <el-select v-model="form.layout" placeholder="请选择布局" style="width: 100%">
          <el-option label="四分屏 (2x2)" value="4"></el-option>
          <el-option label="九分屏 (3x3)" value="9"></el-option>
          <el-option label="十六分屏 (4x4)" value="16"></el-option>
          <el-option label="二十五分屏 (5x5)" value="25"></el-option>
          <el-option label="三十六分屏 (6x6)" value="36"></el-option>
          <el-option label="1+9 异形屏" value="1+9"></el-option>
          <el-option label="1+11 异形屏" value="1+11"></el-option>
        </el-select>
      </el-form-item>

      <el-form-item label="轮播间隔" prop="interval">
        <el-input-number v-model="form.interval" :min="30" :step="5" step-strictly controls-position="right"></el-input-number>
        <span class="unit-text">秒</span>
      </el-form-item>

      <el-form-item label="执行模式" prop="runMode">
        <el-radio-group v-model="form.runMode">
          <el-radio label="manual">手动控制</el-radio>
          <el-radio label="schedule">定时计划</el-radio>
        </el-radio-group>
      </el-form-item>

      <transition name="el-zoom-in-top">
        <div v-if="form.runMode === 'schedule'" class="schedule-box">
          <el-form-item label="生效时段" prop="timeRange" label-width="80px" style="margin-bottom: 0">
            <el-time-picker
              is-range
              v-model="form.timeRange"
              range-separator="至"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
              value-format="HH:mm:ss"
              style="width: 100%"
            >
            </el-time-picker>
          </el-form-item>
        </div>
      </transition>

    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="visible = false">关 闭</el-button>
      <el-button type="primary" @click="handleSave">确认并启动</el-button>
    </div>
  </el-dialog>
</template>

<script>

import VueEasyTree from "@wchbrad/vue-easy-tree/index";
export default {
  name: "CarouselConfig",
  components: {
    VueEasyTree
  },
  props: {
    deviceTreeData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    const validateTimeRange = (rule, value, callback) => {
      if (!value || value.length !== 2) return callback(new Error('请选择生效时段'));
      const toSeconds = (str) => {
        const [h, m, s] = str.split(':').map(Number);
        return h * 3600 + m * 60 + s;
      };
      const start = toSeconds(value[0]);
      const end = toSeconds(value[1]);
      let duration = end - start;
      if (duration < 0) duration += 24 * 3600;
      if (duration < this.form.interval) {
        return callback(new Error(`时段跨度(${duration}s) 不能小于 轮播间隔(${this.form.interval}s)`));
      }
      callback();
    };

    return {
      visible: false,
      filterText: '',
      filterTimer: null, // 【新增】用于防抖
      form: {
        sourceType: 'all_online',
        layout: '16',
        interval: 60,
        runMode: 'manual',
        timeRange: ['08:00:00', '18:00:00'],
        selectedDevices: []
      },
      treeProps: {
        label: 'name',
        children: 'children'
      },
      rules: {
        interval: [{ required: true, message: '间隔不能为空' }, { type: 'number', min: 30, message: '最少30秒' }],
        timeRange: [{ required: true, validator: validateTimeRange, trigger: 'change' }]
      }
    };
  },
  // 【移除】移除了 watch filterText,改为在 input 事件中手动触发,控制更精准
  methods: {
    // 【新增】防抖输入处理 (与 DeviceList 逻辑一致)
    handleFilterInput() {
      if (this.filterTimer) clearTimeout(this.filterTimer);
      this.filterTimer = setTimeout(() => {
        if (this.$refs.deviceTree) {
          this.$refs.deviceTree.filter(this.filterText);
        }
      }, 300);
    },

    // 【修改】递归过滤逻辑 (与 DeviceList 逻辑一致)
    // 逻辑:如果节点名称匹配 OR 父级匹配,则显示
    // 这确保了搜索时树形结构不会完全被打散,且容易定位
    filterNode(value, data, node) {
      if (!value) return true;
      return (data.name && data.name.toUpperCase().indexOf(value.toUpperCase()) !== -1) ||
        (node.parent && node.parent.data && this.filterNode(value, node.parent.data, node.parent));
    },

    open(currentConfig) {
      this.visible = true;
      if (currentConfig) {
        this.form = { ...currentConfig };
        // 回显选中状态
        if (this.form.sourceType === 'custom' && this.form.selectedDevices) {
          this.$nextTick(() => {
            if (this.$refs.deviceTree) {
              const keys = this.form.selectedDevices.map(d => d.code);
              // setCheckedKeys 会将 key 对应的节点勾选,
              // 无论该节点当前是否因为过滤而被隐藏,状态都会被正确设置
              this.$refs.deviceTree.setCheckedKeys(keys);

              // 打开弹窗时清空上一次的搜索
              this.filterText = '';
              this.$refs.deviceTree.filter('');
            }
          })
        }
      }
    },

    async handleSave() {
      this.$refs.form.validate(async valid => {
        if (valid) {
          const config = { ...this.form };

          if (config.sourceType === 'custom') {
            if (!this.$refs.deviceTree) {
              this.$message.error("组件未就绪");
              return;
            }

            // 【关键点】getCheckedNodes(false, false)
            // 第一个参数 leafOnly: false (我们需要父节点和子节点都拿到,或者根据你的业务只需要子节点)
            // 第二个参数 includeHalfChecked: false
            // ElementUI 的这个方法会返回所有被勾选的节点,哪怕它现在因为 filterText 而被隐藏了
            // 所以"搜索后之前的选择不消失"是原生支持的,只要不重置数据源。
            const checkedNodes = this.$refs.deviceTree.getCheckedNodes();

            if (checkedNodes.length === 0) {
              this.$message.warning("请至少选择一个设备或通道!");
              return;
            }
            config.selectedNodes = checkedNodes;
          }

          await this.$nextTick();
          this.$emit('save', config);
          this.visible = false;
        }
      });
    },

    resetForm() {
      this.filterText = '';
      if (this.filterTimer) clearTimeout(this.filterTimer);
      if (this.$refs.form) {
        this.$refs.form.clearValidate();
      }
    }
  }
};
</script>

<style scoped>
.device-select-box {
  margin-top: 10px;
}
.unit-text {
  margin-left: 10px;
}
.tip-text {
  font-size: 12px;
  color: #909399;
  line-height: 1.5;
  margin-top: 5px;
}
</style>