|
|
@@ -1,47 +1,253 @@
|
|
|
<template>
|
|
|
<div class="section-card">
|
|
|
+ <!-- 头部 -->
|
|
|
<div class="section-header cursor-pointer" @click="toggleCollapse">
|
|
|
<h4 class="section-title flex items-center justify-between">
|
|
|
<span class="flex items-center">
|
|
|
<i class="el-icon-link mr-2"></i>
|
|
|
- 靶标-装备分配关系
|
|
|
+ 时序计划
|
|
|
</span>
|
|
|
<i
|
|
|
class="el-icon-arrow-right transition-transform duration-300"
|
|
|
:class="{ 'rotate-90': !isCollapsed, '-rotate-90': isCollapsed }"
|
|
|
- ></i>
|
|
|
+ />
|
|
|
</h4>
|
|
|
</div>
|
|
|
- <div class="target-equipment-map p-4" v-show="!isCollapsed">
|
|
|
+
|
|
|
+ <!-- 主体:检索 + 表格 -->
|
|
|
+ <div class="body p-4" v-show="!isCollapsed" ref="bodyRef">
|
|
|
+ <!-- 检索条 -->
|
|
|
+ <div class="toolbar" ref="toolbarRef">
|
|
|
+ <div class="filters">
|
|
|
+ <el-select
|
|
|
+ v-model="filterDomain"
|
|
|
+ clearable
|
|
|
+ placeholder="打击域"
|
|
|
+ size="small"
|
|
|
+ class="mr8"
|
|
|
+ @change="onDomainChange"
|
|
|
+ >
|
|
|
+ <el-option label="对陆" value="对陆" />
|
|
|
+ <el-option label="对海" value="对海" />
|
|
|
+ </el-select>
|
|
|
+
|
|
|
+ <el-input
|
|
|
+ v-model="filterKeyword"
|
|
|
+ clearable
|
|
|
+ placeholder="关键事件"
|
|
|
+ size="small"
|
|
|
+ class="mr8 w220"
|
|
|
+ @change="onKeywordChange"
|
|
|
+ >
|
|
|
+ <i slot="prefix" class="el-icon-search" />
|
|
|
+ </el-input>
|
|
|
+
|
|
|
+ <el-button size="small" @click="resetFilters">重置</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="ops">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ @click="openAdd"
|
|
|
+ >新增</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 表格 -->
|
|
|
<el-table
|
|
|
- :data="mapData"
|
|
|
+ :data="filteredData"
|
|
|
+ :height="tableHeight"
|
|
|
border
|
|
|
size="small"
|
|
|
+ :empty-text="emptyText"
|
|
|
:header-cell-style="{background: 'rgba(30, 58, 138, 0.3)', color: '#bae6fd', borderColor: 'rgba(14, 165, 233, 0.2)'}"
|
|
|
:row-style="{background: 'rgba(15, 23, 42, 0.5)', color: '#e0f2fe', borderColor: 'rgba(14, 165, 233, 0.1)'}"
|
|
|
>
|
|
|
- <el-table-column prop="targetName" label="靶标名称"></el-table-column>
|
|
|
- <el-table-column prop="targetType" label="靶标类型"></el-table-column>
|
|
|
- <el-table-column prop="threatLevel" label="威胁等级">
|
|
|
- <template slot-scope="scope">
|
|
|
- <div class="threat-bars flex gap-1 justify-center">
|
|
|
- <div
|
|
|
- class="threat-bar"
|
|
|
- v-for="n in 5"
|
|
|
- :key="n"
|
|
|
- :class="{ 'active': n <= scope.row.threatLevel }"
|
|
|
- ></div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="equipmentName" label="负责装备" width="180"></el-table-column>
|
|
|
- <el-table-column prop="equipmentStatus" label="装备状态" width="100">
|
|
|
+ <el-table-column type="index" label="序号" align="center" width="80"/>
|
|
|
+ <el-table-column prop="strikeDomain" label="打击域" align="center" min-width="100"/>
|
|
|
+ <el-table-column prop="timing" label="时序" align="center" min-width="160"/>
|
|
|
+ <el-table-column prop="keyEvent" label="关键事件" align="center" min-width="220" show-overflow-tooltip/>
|
|
|
+ <el-table-column label="操作" width="180" align="center" fixed="right">
|
|
|
<template slot-scope="scope">
|
|
|
- <span :class="statusLabelClass(scope.row.equipmentStatus)">{{ equipmentStatusText(scope.row.equipmentStatus) }}</span>
|
|
|
+ <el-tooltip content="查看" placement="top" :open-delay="200">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-view"
|
|
|
+ class="p-1 text-blue-400 action-btn"
|
|
|
+ @click="openForm('view', scope.row)"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="编辑" placement="top" :open-delay="200">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ class="p-1 text-blue-400 action-btn"
|
|
|
+ @click="openForm('edit', scope.row)"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="删除" placement="top" :open-delay="200">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ class="p-1 text-blue-400 action-btn"
|
|
|
+ @click="confirmDelete(scope.row)"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
+
|
|
|
+ <!-- 完全无数据空态 -->
|
|
|
+ <el-empty
|
|
|
+ v-if="!hasAnyData"
|
|
|
+ description="暂无时序计划"
|
|
|
+ :image-size="80"
|
|
|
+ class="abs-empty"
|
|
|
+ />
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 新增抽屉:多选关键事件 -->
|
|
|
+ <el-drawer
|
|
|
+ title="新增时序(多选)"
|
|
|
+ :visible.sync="dlgAddVisible"
|
|
|
+ size="60%"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :append-to-body="true"
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <div class="drawer-body">
|
|
|
+ <div class="drawer-toolbar">
|
|
|
+ <el-input
|
|
|
+ v-model.trim="addSearch"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ placeholder="搜索关键事件/时序/打击域"
|
|
|
+ class="w260 mr8"
|
|
|
+ >
|
|
|
+ <i slot="prefix" class="el-icon-search" />
|
|
|
+ </el-input>
|
|
|
+ <el-select v-model="addDomain" size="small" clearable placeholder="按打击域筛选" class="mr8">
|
|
|
+ <el-option label="对陆" value="对陆" />
|
|
|
+ <el-option label="对海" value="对海" />
|
|
|
+ </el-select>
|
|
|
+ <el-button size="small" @click="resetAddFilters">重置</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ ref="addTableRef"
|
|
|
+ :data="addFiltered"
|
|
|
+ height="420"
|
|
|
+ border
|
|
|
+ size="small"
|
|
|
+ @selection-change="onAddSelectionChange"
|
|
|
+ :header-cell-style="{background: 'rgba(30, 58, 138, 0.3)', color: '#bae6fd', borderColor: 'rgba(14, 165, 233, 0.2)'}"
|
|
|
+ :row-style="{background: 'rgba(15, 23, 42, 0.5)', color: '#e0f2fe', borderColor: 'rgba(14, 165, 233, 0.1)'}"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="50" />
|
|
|
+ <el-table-column prop="strikeDomain" label="打击域" width="80" align="center" />
|
|
|
+ <el-table-column prop="timing" label="时序" width="130" align="center" />
|
|
|
+ <el-table-column prop="keyEvent" label="关键事件" min-width="280" show-overflow-tooltip />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <span class="hint">已选择:{{ addSelection.length }} 条</span>
|
|
|
+ <div>
|
|
|
+ <el-button @click="dlgAddVisible=false">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="submitAddBatch">确 定</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 统一表单弹窗(查看 / 编辑共用) -->
|
|
|
+ <el-dialog
|
|
|
+ :title="isViewMode ? '查看时序' : '编辑时序'"
|
|
|
+ :visible.sync="dlgFormVisible"
|
|
|
+ width="520px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :append-to-body="true"
|
|
|
+ destroy-on-close
|
|
|
+ >
|
|
|
+ <el-form
|
|
|
+ :model="form"
|
|
|
+ :rules="rules"
|
|
|
+ ref="formRef"
|
|
|
+ label-width="96px"
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ <el-form-item label="打击域" prop="strikeDomain">
|
|
|
+ <el-select v-model="form.strikeDomain" :disabled="isViewMode" placeholder="选择打击域">
|
|
|
+ <el-option label="对陆" value="对陆" />
|
|
|
+ <el-option label="对海" value="对海" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 时序:单位秒,不做格式化。构造为 T、T+10、或 T-10~T+20 -->
|
|
|
+ <el-form-item label="时序" prop="timing">
|
|
|
+ <div class="timing-input-group">
|
|
|
+ <!-- 起始 -->
|
|
|
+ <div class="timing-input-item">
|
|
|
+ <span>T</span>
|
|
|
+ <el-select v-model="timingStart.operator" :disabled="isViewMode" size="mini" style="width: 60px;">
|
|
|
+ <el-option label="+" value="+" />
|
|
|
+ <el-option label="-" value="-" />
|
|
|
+ </el-select>
|
|
|
+ <el-input-number
|
|
|
+ v-model="timingStart.value"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ :min="0"
|
|
|
+ :max="999999"
|
|
|
+ controls-position="right"
|
|
|
+ size="mini"
|
|
|
+ style="width: 100px;"
|
|
|
+ placeholder="秒"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <span class="tilde">~</span>
|
|
|
+
|
|
|
+ <!-- 结束 -->
|
|
|
+ <div class="timing-input-item">
|
|
|
+ <span>T</span>
|
|
|
+ <el-select v-model="timingEnd.operator" :disabled="isViewMode" size="mini" style="width: 60px;">
|
|
|
+ <el-option label="+" value="+" />
|
|
|
+ <el-option label="-" value="-" />
|
|
|
+ </el-select>
|
|
|
+ <el-input-number
|
|
|
+ v-model="timingEnd.value"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ :min="0"
|
|
|
+ :max="999999"
|
|
|
+ controls-position="right"
|
|
|
+ size="mini"
|
|
|
+ style="width: 100px;"
|
|
|
+ placeholder="秒"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预览(实时展示最终会提交/展示的时序字符串) -->
|
|
|
+ <div class="timing-preview">结果:{{ timingPreview }}</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="关键事件" prop="keyEvent">
|
|
|
+ <el-input
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ v-model.trim="form.keyEvent"
|
|
|
+ :disabled="isViewMode"
|
|
|
+ placeholder="填写关键事件描述"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <span slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="dlgFormVisible=false">{{ isViewMode ? '关 闭' : '取 消' }}</el-button>
|
|
|
+ <el-button v-if="!isViewMode" type="primary" @click="submitForm">保 存</el-button>
|
|
|
+ </span>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -49,39 +255,306 @@
|
|
|
export default {
|
|
|
name: 'TargetEquipmentMap',
|
|
|
props: {
|
|
|
- mapData: {
|
|
|
+ // 表格数据由父组件持有
|
|
|
+ mapData: { type: Array, default: () => [] },
|
|
|
+ // 关键事件总表(新增抽屉多选)
|
|
|
+ // timing 建议也用本规则:T / T+10 / T-10~T+20
|
|
|
+ eventCatalog: {
|
|
|
type: Array,
|
|
|
- required: true,
|
|
|
- default: () => []
|
|
|
+ default: () => ([
|
|
|
+ { id: 'E1', strikeDomain: '对陆', timing: 'T+30', keyEvent: '测量系统上电' },
|
|
|
+ { id: 'E2', strikeDomain: '对陆', timing: 'T+60', keyEvent: '目标进入监视区' },
|
|
|
+ { id: 'E3', strikeDomain: '对海', timing: 'T+90', keyEvent: '干扰装备启动' },
|
|
|
+ { id: 'E4', strikeDomain: '对海', timing: 'T+120', keyEvent: '目标锁定' },
|
|
|
+ { id: 'E5', strikeDomain: '对陆', timing: 'T+150', keyEvent: '导弹准备' },
|
|
|
+ { id: 'E6', strikeDomain: '对海', timing: 'T+180', keyEvent: '导弹发射' },
|
|
|
+ { id: 'E7', strikeDomain: '对陆', timing: 'T+210', keyEvent: '数据记录开始' },
|
|
|
+ { id: 'E8', strikeDomain: '对海', timing: 'T+240', keyEvent: '拦截评估' }
|
|
|
+ ])
|
|
|
}
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
- isCollapsed: false
|
|
|
+ isCollapsed: false,
|
|
|
+
|
|
|
+ // 检索条件
|
|
|
+ filterDomain: '',
|
|
|
+ filterKeyword: '',
|
|
|
+
|
|
|
+ // 表格高度
|
|
|
+ tableHeight: 320,
|
|
|
+
|
|
|
+ // 新增抽屉
|
|
|
+ dlgAddVisible: false,
|
|
|
+ addSearch: '',
|
|
|
+ addDomain: '',
|
|
|
+ addSelection: [],
|
|
|
+
|
|
|
+ // 统一弹窗
|
|
|
+ dlgFormVisible: false,
|
|
|
+ dlgMode: 'view', // 'view' | 'edit'
|
|
|
+ form: this.initForm(),
|
|
|
+
|
|
|
+ // 时序控件:value=null 表示未填
|
|
|
+ timingStart: {operator: '+', value: null},
|
|
|
+ timingEnd: {operator: '+', value: null},
|
|
|
+
|
|
|
+ // 校验
|
|
|
+ rules: {
|
|
|
+ strikeDomain: [{required: true, message: '请选择打击域', trigger: 'change'}],
|
|
|
+ keyEvent: [{required: true, message: '请输入关键事件', trigger: 'blur'}],
|
|
|
+ // timing 允许:T、T+10、T-10、T-10~T+20
|
|
|
+ timing: [{
|
|
|
+ validator: (rule, v, cb) => {
|
|
|
+ const ok = /^T(?:[+-]\d+)?(?:~T[+-]\d+)?$/.test(String(v || ''));
|
|
|
+ ok ? cb() : cb(new Error('时序格式不合法(示例:T、T+10、T-10~T+20)'));
|
|
|
+ },
|
|
|
+ trigger: 'change'
|
|
|
+ }]
|
|
|
+ },
|
|
|
+
|
|
|
+ // 尺寸观察
|
|
|
+ _ro: null
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ isViewMode() {
|
|
|
+ return this.dlgMode === 'view';
|
|
|
+ },
|
|
|
+ hasAnyData() {
|
|
|
+ return Array.isArray(this.mapData) && this.mapData.length > 0;
|
|
|
+ },
|
|
|
+ filteredData() {
|
|
|
+ if (!this.hasAnyData) return [];
|
|
|
+ const kw = (this.filterKeyword || '').trim();
|
|
|
+ return this.mapData.filter(row => {
|
|
|
+ const okDomain = this.filterDomain ? String(row.strikeDomain) === this.filterDomain : true;
|
|
|
+ const okKey = kw ? String(row.keyEvent || '').includes(kw) : true;
|
|
|
+ return okDomain && okKey;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ emptyText() {
|
|
|
+ if (!this.hasAnyData) return '暂无数据';
|
|
|
+ if (this.filterDomain || (this.filterKeyword || '').trim()) return '无匹配数据';
|
|
|
+ return '暂无数据';
|
|
|
+ },
|
|
|
+ /** 抽屉内:筛选关键事件总表 */
|
|
|
+ addFiltered() {
|
|
|
+ let list = Array.isArray(this.eventCatalog) ? this.eventCatalog.slice() : [];
|
|
|
+ const kw = (this.addSearch || '').trim();
|
|
|
+ if (this.addDomain) list = list.filter(x => String(x.strikeDomain) === this.addDomain);
|
|
|
+ if (kw) {
|
|
|
+ list = list.filter(x =>
|
|
|
+ String(x.keyEvent || '').includes(kw) ||
|
|
|
+ String(x.timing || '').includes(kw) ||
|
|
|
+ String(x.strikeDomain || '').includes(kw)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ },
|
|
|
+ /** 预览当前时序字符串(不格式化秒) */
|
|
|
+ timingPreview() {
|
|
|
+ return this.buildTimingString(
|
|
|
+ this.timingStart.operator, this.timingStart.value,
|
|
|
+ this.timingEnd.operator, this.timingEnd.value
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ isCollapsed(nv) {
|
|
|
+ if (!nv) this.$nextTick(this.calcTableHeight);
|
|
|
}
|
|
|
},
|
|
|
+ mounted() {
|
|
|
+ console.log("eventCatalog",this.eventCatalog)
|
|
|
+ this.$nextTick(this.calcTableHeight);
|
|
|
+ if (window.ResizeObserver) {
|
|
|
+ this._ro = new ResizeObserver(() => this.calcTableHeight());
|
|
|
+ const body = this.$refs.bodyRef;
|
|
|
+ if (body) this._ro.observe(body);
|
|
|
+ } else {
|
|
|
+ window.addEventListener('resize', this.calcTableHeight);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this._ro) {
|
|
|
+ this._ro.disconnect();
|
|
|
+ this._ro = null;
|
|
|
+ }
|
|
|
+ window.removeEventListener('resize', this.calcTableHeight);
|
|
|
+ },
|
|
|
methods: {
|
|
|
+ /* ========== 基础 ========== */
|
|
|
+ initForm() {
|
|
|
+ return {id: '', strikeDomain: '', timing: 'T', keyEvent: ''};
|
|
|
+ },
|
|
|
toggleCollapse() {
|
|
|
this.isCollapsed = !this.isCollapsed;
|
|
|
},
|
|
|
- equipmentStatusText(status) {
|
|
|
- const statusMap = {
|
|
|
- standby: '待命',
|
|
|
- ready: '就绪',
|
|
|
- maintenance: '维护中'
|
|
|
- };
|
|
|
- return statusMap[status] || '未知';
|
|
|
- },
|
|
|
- statusLabelClass(status) {
|
|
|
- const classMap = {
|
|
|
- standby: 'text-yellow-400',
|
|
|
- ready: 'text-green-400',
|
|
|
- maintenance: 'text-gray-400'
|
|
|
- };
|
|
|
- return classMap[status] || 'text-gray-300';
|
|
|
+
|
|
|
+ /* ========== 表格检索 ========== */
|
|
|
+ onDomainChange(val) {
|
|
|
+ this.filterDomain = val || '';
|
|
|
+ this.$emit('search-change', {strikeDomain: this.filterDomain, keyEvent: (this.filterKeyword || '').trim()});
|
|
|
+ this.$nextTick(this.calcTableHeight);
|
|
|
+ },
|
|
|
+ onKeywordChange(val) {
|
|
|
+ this.filterKeyword = (val || '').trim();
|
|
|
+ this.$emit('search-change', {strikeDomain: this.filterDomain, keyEvent: this.filterKeyword});
|
|
|
+ this.$nextTick(this.calcTableHeight);
|
|
|
+ },
|
|
|
+ resetFilters() {
|
|
|
+ this.filterDomain = '';
|
|
|
+ this.filterKeyword = '';
|
|
|
+ this.$emit('search-change', {strikeDomain: '', keyEvent: ''});
|
|
|
+ this.$nextTick(this.calcTableHeight);
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 表格高度计算 ========== */
|
|
|
+ calcTableHeight() {
|
|
|
+ const bodyEl = this.$refs.bodyRef;
|
|
|
+ if (!bodyEl || this.isCollapsed) return;
|
|
|
+ const bodyH = bodyEl.clientHeight || 0;
|
|
|
+ const toolbarH = this.$refs.toolbarRef ? this.$refs.toolbarRef.offsetHeight : 0;
|
|
|
+ const usable = Math.max(0, bodyH - toolbarH);
|
|
|
+ const h = Math.floor(usable * 0.6);
|
|
|
+ this.tableHeight = Math.max(h, 240);
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 新增(抽屉) ========== */
|
|
|
+ openAdd() {
|
|
|
+ this.addSearch = '';
|
|
|
+ this.addDomain = '';
|
|
|
+ this.addSelection = [];
|
|
|
+ this.dlgAddVisible = true;
|
|
|
+ console.log(this.addFiltered,"sss===")
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.addTableRef) this.$refs.addTableRef.clearSelection();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onAddSelectionChange(rows) {
|
|
|
+ this.addSelection = Array.isArray(rows) ? rows : [];
|
|
|
+ },
|
|
|
+ resetAddFilters() {
|
|
|
+ this.addSearch = '';
|
|
|
+ this.addDomain = '';
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.addTableRef) this.$refs.addTableRef.clearSelection();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ submitAddBatch() {
|
|
|
+ if (!this.addSelection.length) return this.$message.warning('请至少选择一条关键事件');
|
|
|
+ const items = this.addSelection.map(x => ({
|
|
|
+ id: this.genId(),
|
|
|
+ strikeDomain: x.strikeDomain,
|
|
|
+ timing: x.timing, // 直接使用目录中的 timing(T / T±n / T±n~T±m)
|
|
|
+ keyEvent: x.keyEvent
|
|
|
+ }));
|
|
|
+ this.$emit('create-batch', items);
|
|
|
+ items.forEach(it => this.$emit('create', it)); // 兼容:逐条
|
|
|
+ this.dlgAddVisible = false;
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 查看 / 编辑(统一弹窗) ========== */
|
|
|
+ openForm(mode, row) {
|
|
|
+ this.dlgMode = mode === 'edit' ? 'edit' : 'view';
|
|
|
+ this.form = {...(row || this.initForm())};
|
|
|
+ if (!this.form.id) this.form.id = this.genId();
|
|
|
+
|
|
|
+ // 解析 timing -> 控件
|
|
|
+ this.applyTimingToControls(this.form.timing);
|
|
|
+ this.dlgFormVisible = true;
|
|
|
+
|
|
|
+ this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate());
|
|
|
+ },
|
|
|
+ submitForm() {
|
|
|
+ // 仅编辑模式提交
|
|
|
+ if (this.isViewMode) return (this.dlgFormVisible = false);
|
|
|
+
|
|
|
+ // 用控件值构造 timing 字符串
|
|
|
+ this.form.timing = this.buildTimingString(
|
|
|
+ this.timingStart.operator, this.timingStart.value,
|
|
|
+ this.timingEnd.operator, this.timingEnd.value
|
|
|
+ );
|
|
|
+
|
|
|
+ this.$refs.formRef.validate(valid => {
|
|
|
+ if (!valid) return;
|
|
|
+ const payload = {...this.form};
|
|
|
+ this.$emit('update', payload);
|
|
|
+ this.dlgFormVisible = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 时序字符串 <-> 控件 ========== */
|
|
|
+ /**
|
|
|
+ * 构造时序字符串(不格式化秒)
|
|
|
+ * 规则:
|
|
|
+ * - 两端都未填 -> "T"
|
|
|
+ * - 单端:T±n;若 n 为 0 -> "T"
|
|
|
+ * - 双端:T±a~T±b;若 a=0 且 b=0 -> "T"
|
|
|
+ * - 若一端为 0,另一端非 0 -> 返回单端形式(只保留非 0 的那端)
|
|
|
+ */
|
|
|
+ buildTimingString(op1, val1, op2, val2) {
|
|
|
+ const has1 = Number.isFinite(val1);
|
|
|
+ const has2 = Number.isFinite(val2);
|
|
|
+ const n1 = has1 ? Number(val1) : null;
|
|
|
+ const n2 = has2 ? Number(val2) : null;
|
|
|
+
|
|
|
+ if (!has1 && !has2) return 'T';
|
|
|
+
|
|
|
+ // 单端
|
|
|
+ if (has1 && !has2) return n1 === 0 ? 'T' : `T${op1}${n1}`;
|
|
|
+ if (!has1 && has2) return n2 === 0 ? 'T' : `T${op2}${n2}`;
|
|
|
+
|
|
|
+ // 双端
|
|
|
+ if (n1 === 0 && n2 === 0) return 'T';
|
|
|
+ if (n1 === 0 && n2 !== 0) return `T${op2}${n2}`;
|
|
|
+ if (n1 !== 0 && n2 === 0) return `T${op1}${n1}`;
|
|
|
+ return `T${op1}${n1}~T${op2}${n2}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 解析字符串到控件,支持:T / T±n / T±a~T±b */
|
|
|
+ applyTimingToControls(str) {
|
|
|
+ const s = String(str || '').trim();
|
|
|
+
|
|
|
+ // 默认值
|
|
|
+ this.timingStart = {operator: '+', value: null};
|
|
|
+ this.timingEnd = {operator: '+', value: null};
|
|
|
+
|
|
|
+ if (!s || s === 'T') return;
|
|
|
+
|
|
|
+ // 范围:T±a~T±b
|
|
|
+ let m = /^T([+-])(\d+)~T([+-])(\d+)$/.exec(s);
|
|
|
+ if (m) {
|
|
|
+ this.timingStart.operator = m[1];
|
|
|
+ this.timingStart.value = Number(m[2]);
|
|
|
+ this.timingEnd.operator = m[3];
|
|
|
+ this.timingEnd.value = Number(m[4]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单端:T±n
|
|
|
+ m = /^T([+-])(\d+)$/.exec(s);
|
|
|
+ if (m) {
|
|
|
+ this.timingStart.operator = m[1];
|
|
|
+ this.timingStart.value = Number(m[2]);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 删除 ========== */
|
|
|
+ confirmDelete(row) {
|
|
|
+ this.$confirm('确定删除该条时序吗?', '提示', {type: 'warning'})
|
|
|
+ .then(() => this.$emit('delete', row))
|
|
|
+ .catch(() => {
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ========== 工具 ========== */
|
|
|
+ genId() {
|
|
|
+ return 'S' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -120,18 +593,97 @@ export default {
|
|
|
color: #94a3b8;
|
|
|
}
|
|
|
|
|
|
-.threat-bars {
|
|
|
+.body {
|
|
|
+ position: relative;
|
|
|
+ min-height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 检索条 */
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.filters {
|
|
|
+ display: flex;
|
|
|
align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.mr8 {
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.w220 {
|
|
|
+ width: 220px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 空态覆盖整个主体 */
|
|
|
+.abs-empty {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 抽屉 */
|
|
|
+.drawer-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.w260 {
|
|
|
+ width: 260px;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #9aa0a6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 统一表单:时序控件 */
|
|
|
+.timing-input-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.timing-input-item {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.tilde {
|
|
|
+ opacity: 0.7;
|
|
|
}
|
|
|
|
|
|
-.threat-bar {
|
|
|
- width: 6px;
|
|
|
- height: 6px;
|
|
|
- background-color: rgba(148, 163, 184, 0.3);
|
|
|
- border-radius: 2px;
|
|
|
+.timing-preview {
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #9aa0a6;
|
|
|
}
|
|
|
|
|
|
-.threat-bar.active {
|
|
|
- background-color: #f97316;
|
|
|
+/* 操作按钮 */
|
|
|
+.action-btn:hover {
|
|
|
+ filter: brightness(1.15);
|
|
|
}
|
|
|
</style>
|