|
|
@@ -1,641 +1,2027 @@
|
|
|
<template>
|
|
|
- <div class="app-container">
|
|
|
- <el-row :gutter="20">
|
|
|
- <!-- 左侧树形区域 -->
|
|
|
- <el-col :span="5" :xs="24">
|
|
|
- <div class="head-container">
|
|
|
- <el-input
|
|
|
- v-model="areaName"
|
|
|
- placeholder="请输入区域名称"
|
|
|
- clearable
|
|
|
+ <div class="custom-report-container">
|
|
|
+ <!-- 页面标题区域 -->
|
|
|
+ <div class="page-header">
|
|
|
+ <div class="header-left">
|
|
|
+ <h2 class="page-title">
|
|
|
+ <i class="el-icon-data-analysis"></i>
|
|
|
+ 自定义报表
|
|
|
+ </h2>
|
|
|
+ <span class="page-subtitle">支持产能、用能、储能数据的灵活查询与分析</span>
|
|
|
+ </div>
|
|
|
+ <div class="header-right">
|
|
|
+ <el-button-group>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
size="small"
|
|
|
- prefix-icon="el-icon-search"
|
|
|
- style="margin-bottom: 20px"
|
|
|
- @input="filterTree"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div class="head-container tree-container">
|
|
|
- <el-tree
|
|
|
- ref="tree"
|
|
|
- :data="areaOptions"
|
|
|
- :props="defaultProps"
|
|
|
- :expand-on-click-node="false"
|
|
|
- :filter-node-method="filterNode"
|
|
|
- node-key="id"
|
|
|
- :default-expanded-keys="defaultExpandedKeys"
|
|
|
- highlight-current
|
|
|
- @node-click="handleNodeClick"
|
|
|
+ icon="el-icon-folder-opened"
|
|
|
+ @click="showTemplateDialog = true"
|
|
|
>
|
|
|
- <span class="custom-tree-node" slot-scope="{ node, data }">
|
|
|
- <span class="tree-label">
|
|
|
- <i :class="getTreeIcon(data)" class="tree-icon"></i>
|
|
|
- {{ node.label }}
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- </el-tree>
|
|
|
+ 模板管理
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ plain
|
|
|
+ size="small"
|
|
|
+ icon="el-icon-document-add"
|
|
|
+ :disabled="!canSaveTemplate"
|
|
|
+ @click="handleSaveTemplate"
|
|
|
+ >
|
|
|
+ 保存模板
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <!-- 左侧配置区域 -->
|
|
|
+ <el-col :span="6" :xs="24">
|
|
|
+ <div class="config-panel">
|
|
|
+ <!-- 数据源选择 -->
|
|
|
+ <div class="panel-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <i class="el-icon-connection"></i>
|
|
|
+ <span>数据源</span>
|
|
|
+ </div>
|
|
|
+ <div class="datasource-cards">
|
|
|
+ <div
|
|
|
+ v-for="ds in datasourceList"
|
|
|
+ :key="ds.dsCode"
|
|
|
+ :class="['ds-card', { active: selectedDsCode === ds.dsCode }]"
|
|
|
+ @click="handleSelectDatasource(ds)"
|
|
|
+ >
|
|
|
+ <div class="ds-card-icon">
|
|
|
+ <i :class="getDatasourceIcon(ds.category)"></i>
|
|
|
+ </div>
|
|
|
+ <div class="ds-card-content">
|
|
|
+ <div class="ds-card-name">{{ ds.dsName }}</div>
|
|
|
+ <div class="ds-card-desc">{{ ds.dsDesc }}</div>
|
|
|
+ </div>
|
|
|
+ <div v-if="selectedDsCode === ds.dsCode" class="ds-card-check">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 字段选择 -->
|
|
|
+ <div class="panel-section" v-if="selectedDsCode">
|
|
|
+ <div class="section-title">
|
|
|
+ <i class="el-icon-menu"></i>
|
|
|
+ <span>选择字段</span>
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ @click="handleSelectAllFields"
|
|
|
+ >
|
|
|
+ {{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="field-groups">
|
|
|
+ <div
|
|
|
+ v-for="(fields, groupName) in groupedFields"
|
|
|
+ :key="groupName"
|
|
|
+ class="field-group"
|
|
|
+ >
|
|
|
+ <div class="group-header" @click="toggleFieldGroup(groupName)">
|
|
|
+ <i :class="expandedGroups.includes(groupName) ? 'el-icon-arrow-down' : 'el-icon-arrow-right'"></i>
|
|
|
+ <span>{{ groupName }}</span>
|
|
|
+ <el-badge :value="getSelectedCountInGroup(groupName)" type="primary" class="group-badge" />
|
|
|
+ </div>
|
|
|
+ <el-collapse-transition>
|
|
|
+ <div v-show="expandedGroups.includes(groupName)" class="group-fields">
|
|
|
+ <el-checkbox
|
|
|
+ v-for="field in fields"
|
|
|
+ :key="field.fieldCode"
|
|
|
+ v-model="selectedFieldCodes"
|
|
|
+ :label="field.fieldCode"
|
|
|
+ class="field-checkbox"
|
|
|
+ >
|
|
|
+ <span class="field-label">
|
|
|
+ {{ field.fieldName }}
|
|
|
+ <el-tag v-if="field.unit" size="mini" type="info">{{ field.unit }}</el-tag>
|
|
|
+ </span>
|
|
|
+ </el-checkbox>
|
|
|
+ </div>
|
|
|
+ </el-collapse-transition>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</el-col>
|
|
|
|
|
|
<!-- 右侧内容区域 -->
|
|
|
- <el-col :span="19" :xs="24">
|
|
|
- <div class="content-wrapper">
|
|
|
- <!-- 标题区域 -->
|
|
|
- <div class="content-header">
|
|
|
- <div class="header-left">
|
|
|
- <h3 class="page-title">
|
|
|
- <i class="el-icon-document"></i>
|
|
|
- 自定义报表【{{ selectedLabel }}】
|
|
|
- </h3>
|
|
|
+ <el-col :span="18" :xs="24">
|
|
|
+ <!-- 查询条件区域 -->
|
|
|
+ <div class="query-panel" v-if="selectedDsCode">
|
|
|
+ <div class="panel-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <i class="el-icon-search"></i>
|
|
|
+ <span>查询条件</span>
|
|
|
+ <el-button type="text" size="mini" icon="el-icon-plus" @click="addCondition">
|
|
|
+ 添加条件
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 查询条件区域 -->
|
|
|
- <div class="search-container">
|
|
|
- <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
|
|
|
- <el-form-item label="报表类型">
|
|
|
- <el-select v-model="queryParams.reportType" placeholder="请选择类型" @change="handleReportTypeChange">
|
|
|
- <el-option label="产能" value="prod" />
|
|
|
- <el-option label="用能" value="consume" />
|
|
|
+ <div class="conditions-area">
|
|
|
+ <div
|
|
|
+ v-for="(condition, index) in queryConditions"
|
|
|
+ :key="index"
|
|
|
+ class="condition-row"
|
|
|
+ >
|
|
|
+ <el-select
|
|
|
+ v-model="condition.field"
|
|
|
+ placeholder="选择字段"
|
|
|
+ size="small"
|
|
|
+ filterable
|
|
|
+ class="condition-field"
|
|
|
+ @change="handleConditionFieldChange(condition)"
|
|
|
+ >
|
|
|
+ <el-option-group
|
|
|
+ v-for="(fields, groupName) in groupedFields"
|
|
|
+ :key="groupName"
|
|
|
+ :label="groupName"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="field in fields.filter(f => f.isFilterable === 1)"
|
|
|
+ :key="field.fieldCode"
|
|
|
+ :label="field.fieldName"
|
|
|
+ :value="field.fieldCode"
|
|
|
+ />
|
|
|
+ </el-option-group>
|
|
|
</el-select>
|
|
|
- </el-form-item>
|
|
|
|
|
|
- <el-form-item label="指标项">
|
|
|
- <el-select v-model="queryParams.metricField" placeholder="请选择指标">
|
|
|
- <el-option v-for="item in metricOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="条件">
|
|
|
- <el-select v-model="queryParams.conditionType" placeholder="请选择条件">
|
|
|
- <el-option label="大于" value="gt" />
|
|
|
- <el-option label="小于" value="lt" />
|
|
|
- <el-option label="等于" value="eq" />
|
|
|
- <el-option label="不等于" value="ne" />
|
|
|
+ <el-select
|
|
|
+ v-model="condition.operator"
|
|
|
+ placeholder="条件"
|
|
|
+ size="small"
|
|
|
+ class="condition-operator"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="op in getAvailableOperators(condition.field)"
|
|
|
+ :key="op.value"
|
|
|
+ :label="op.label"
|
|
|
+ :value="op.value"
|
|
|
+ />
|
|
|
</el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="指标值">
|
|
|
- <el-input v-model.number="queryParams.metricValue" type="number" placeholder="请输入值" />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="开始时间">
|
|
|
- <el-date-picker
|
|
|
- v-model="queryParams.startRecTime"
|
|
|
- type="datetime"
|
|
|
- value-format="yyyy-MM-dd HH:00:00"
|
|
|
- placeholder="选择开始时间"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="结束时间">
|
|
|
- <el-date-picker
|
|
|
- v-model="queryParams.endRecTime"
|
|
|
- type="datetime"
|
|
|
- value-format="yyyy-MM-dd HH:00:00"
|
|
|
- placeholder="选择结束时间"
|
|
|
+
|
|
|
+ <!-- 根据字段类型和操作符显示不同的输入组件 -->
|
|
|
+ <template v-if="condition.operator !== 'isNull' && condition.operator !== 'isNotNull'">
|
|
|
+ <!-- 日期类型 -->
|
|
|
+ <template v-if="getFieldType(condition.field) === 'date'">
|
|
|
+ <el-date-picker
|
|
|
+ v-if="condition.operator === 'between'"
|
|
|
+ v-model="condition.dateRange"
|
|
|
+ type="daterange"
|
|
|
+ size="small"
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
+ range-separator="-"
|
|
|
+ start-placeholder="开始"
|
|
|
+ end-placeholder="结束"
|
|
|
+ class="condition-value-wide"
|
|
|
+ @change="handleDateRangeChange(condition)"
|
|
|
+ />
|
|
|
+ <el-date-picker
|
|
|
+ v-else
|
|
|
+ v-model="condition.value"
|
|
|
+ type="date"
|
|
|
+ size="small"
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
+ placeholder="选择日期"
|
|
|
+ class="condition-value"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 日期时间类型 -->
|
|
|
+ <template v-else-if="getFieldType(condition.field) === 'datetime'">
|
|
|
+ <el-date-picker
|
|
|
+ v-if="condition.operator === 'between'"
|
|
|
+ v-model="condition.dateRange"
|
|
|
+ type="datetimerange"
|
|
|
+ size="small"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ range-separator="-"
|
|
|
+ start-placeholder="开始"
|
|
|
+ end-placeholder="结束"
|
|
|
+ class="condition-value-wide"
|
|
|
+ @change="handleDateRangeChange(condition)"
|
|
|
+ />
|
|
|
+ <el-date-picker
|
|
|
+ v-else
|
|
|
+ v-model="condition.value"
|
|
|
+ type="datetime"
|
|
|
+ size="small"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ placeholder="选择时间"
|
|
|
+ class="condition-value"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 数值类型 -->
|
|
|
+ <template v-else-if="getFieldType(condition.field) === 'number'">
|
|
|
+ <template v-if="condition.operator === 'between'">
|
|
|
+ <el-input-number
|
|
|
+ v-model="condition.value"
|
|
|
+ size="small"
|
|
|
+ :controls="false"
|
|
|
+ placeholder="最小值"
|
|
|
+ class="condition-value-half"
|
|
|
+ />
|
|
|
+ <span class="condition-separator">~</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="condition.value2"
|
|
|
+ size="small"
|
|
|
+ :controls="false"
|
|
|
+ placeholder="最大值"
|
|
|
+ class="condition-value-half"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <el-input-number
|
|
|
+ v-else
|
|
|
+ v-model="condition.value"
|
|
|
+ size="small"
|
|
|
+ :controls="false"
|
|
|
+ placeholder="输入数值"
|
|
|
+ class="condition-value"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 字符串类型 -->
|
|
|
+ <template v-else>
|
|
|
+ <el-input
|
|
|
+ v-model="condition.value"
|
|
|
+ size="small"
|
|
|
+ placeholder="输入值"
|
|
|
+ class="condition-value"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ class="condition-delete"
|
|
|
+ @click="removeCondition(index)"
|
|
|
/>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
|
|
|
- <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
- <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 统计卡片 -->
|
|
|
- <div class="stats-cards" v-if="resultList.length > 0">
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="6" :xs="24">
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
|
- <i class="el-icon-data-analysis"></i>
|
|
|
- </div>
|
|
|
- <div class="stat-content">
|
|
|
- <div class="stat-value">{{ total }}</div>
|
|
|
- <div class="stat-label">符合条件记录</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6" :xs="24">
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
|
|
- <i class="el-icon-document-checked"></i>
|
|
|
- </div>
|
|
|
- <div class="stat-content">
|
|
|
- <div class="stat-value">{{ queryParams.reportType === 'prod' ? '产能' : '用能' }}</div>
|
|
|
- <div class="stat-label">报表类型</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6" :xs="24">
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
|
|
- <i class="el-icon-s-data"></i>
|
|
|
- </div>
|
|
|
- <div class="stat-content">
|
|
|
- <div class="stat-value">{{ getMetricLabel() }}</div>
|
|
|
- <div class="stat-label">当前指标</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6" :xs="24">
|
|
|
- <div class="stat-card">
|
|
|
- <div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
|
|
- <i class="el-icon-s-operation"></i>
|
|
|
- </div>
|
|
|
- <div class="stat-content">
|
|
|
- <div class="stat-value">{{ getConditionLabel() }} {{ queryParams.metricValue || 0 }}</div>
|
|
|
- <div class="stat-label">筛选条件</div>
|
|
|
+ <div v-if="queryConditions.length === 0" class="no-conditions">
|
|
|
+ <i class="el-icon-info"></i>
|
|
|
+ <span>暂无筛选条件,点击"添加条件"开始配置</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 高级选项 -->
|
|
|
+ <el-collapse v-model="advancedOptionsExpanded" class="advanced-options">
|
|
|
+ <el-collapse-item name="advanced">
|
|
|
+ <template slot="title">
|
|
|
+ <i class="el-icon-setting"></i>
|
|
|
+ <span style="margin-left: 8px;">高级选项</span>
|
|
|
+ </template>
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :span="8">
|
|
|
+ <div class="option-item">
|
|
|
+ <label>分组统计</label>
|
|
|
+ <el-select
|
|
|
+ v-model="groupByFields"
|
|
|
+ multiple
|
|
|
+ collapse-tags
|
|
|
+ size="small"
|
|
|
+ placeholder="选择分组字段"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="field in availableGroupByFields"
|
|
|
+ :key="field.fieldCode"
|
|
|
+ :label="field.fieldName"
|
|
|
+ :value="field.fieldCode"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <div class="option-item">
|
|
|
+ <label>排序方式</label>
|
|
|
+ <div style="display: flex; gap: 8px;">
|
|
|
+ <el-select
|
|
|
+ v-model="orderByField"
|
|
|
+ size="small"
|
|
|
+ placeholder="排序字段"
|
|
|
+ style="flex: 1"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="field in sortableFields"
|
|
|
+ :key="field.fieldCode"
|
|
|
+ :label="field.fieldName"
|
|
|
+ :value="field.fieldCode"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <el-select
|
|
|
+ v-model="orderDirection"
|
|
|
+ size="small"
|
|
|
+ style="width: 80px"
|
|
|
+ >
|
|
|
+ <el-option label="升序" value="ASC" />
|
|
|
+ <el-option label="降序" value="DESC" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <div class="option-item">
|
|
|
+ <label>数据去重</label>
|
|
|
+ <el-switch v-model="distinctFlag" />
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 聚合函数配置 -->
|
|
|
+ <div v-if="groupByFields.length > 0" class="aggregate-config">
|
|
|
+ <label>聚合配置</label>
|
|
|
+ <div class="aggregate-fields">
|
|
|
+ <div
|
|
|
+ v-for="field in aggregatableFields"
|
|
|
+ :key="field.fieldCode"
|
|
|
+ class="aggregate-item"
|
|
|
+ >
|
|
|
+ <el-checkbox
|
|
|
+ v-model="aggregateFieldsConfig[field.fieldCode]"
|
|
|
+ :label="field.fieldCode"
|
|
|
+ >
|
|
|
+ {{ field.fieldName }}
|
|
|
+ </el-checkbox>
|
|
|
+ <el-select
|
|
|
+ v-if="aggregateFieldsConfig[field.fieldCode]"
|
|
|
+ v-model="aggregateFuncsConfig[field.fieldCode]"
|
|
|
+ size="mini"
|
|
|
+ style="width: 80px; margin-left: 8px;"
|
|
|
+ >
|
|
|
+ <el-option label="求和" value="SUM" />
|
|
|
+ <el-option label="平均" value="AVG" />
|
|
|
+ <el-option label="最大" value="MAX" />
|
|
|
+ <el-option label="最小" value="MIN" />
|
|
|
+ <el-option label="计数" value="COUNT" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div class="query-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-search"
|
|
|
+ :loading="loading"
|
|
|
+ @click="handleQuery"
|
|
|
+ >
|
|
|
+ 查询
|
|
|
+ </el-button>
|
|
|
+ <el-button icon="el-icon-refresh" @click="handleReset">
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="warning"
|
|
|
+ plain
|
|
|
+ icon="el-icon-download"
|
|
|
+ :disabled="!hasQueryResult"
|
|
|
+ @click="handleExport"
|
|
|
+ >
|
|
|
+ 导出Excel
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- 数据表格 -->
|
|
|
- <div class="table-container">
|
|
|
- <el-table v-loading="loading" :data="resultList" stripe>
|
|
|
- <el-table-column
|
|
|
- v-for="col in resultTableCols"
|
|
|
- :key="col.prop"
|
|
|
- :label="col.label"
|
|
|
- :prop="col.prop"
|
|
|
- align="center"
|
|
|
+ <!-- 数据展示区域 -->
|
|
|
+ <div class="result-panel" v-if="selectedDsCode">
|
|
|
+ <!-- 汇总信息 -->
|
|
|
+ <div v-if="summary && Object.keys(summary).length > 0" class="summary-bar">
|
|
|
+ <div class="summary-title">
|
|
|
+ <i class="el-icon-data-line"></i>
|
|
|
+ <span>数据汇总</span>
|
|
|
+ </div>
|
|
|
+ <div class="summary-items">
|
|
|
+ <div
|
|
|
+ v-for="(value, key) in summary"
|
|
|
+ :key="key"
|
|
|
+ class="summary-item"
|
|
|
>
|
|
|
- <template slot-scope="scope">
|
|
|
- <span v-if="col.prop.includes('Quantity') || col.prop.includes('Cost') || col.prop.includes('Earn')" class="data-value">
|
|
|
- {{ formatNumber(scope.row[col.prop]) }}
|
|
|
- </span>
|
|
|
- <span v-else>{{ scope.row[col.prop] }}</span>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <span class="summary-label">{{ getSummaryLabel(key) }}</span>
|
|
|
+ <span class="summary-value">{{ formatSummaryValue(key, value) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <pagination
|
|
|
- v-show="total > 0"
|
|
|
+ <!-- ===== 修复:首次查询前显示初始状态,而不是空表格 ===== -->
|
|
|
+ <!-- 初始状态:未执行过查询 -->
|
|
|
+ <div v-if="!hasQueried && !loading" class="initial-state">
|
|
|
+ <div class="initial-icon">
|
|
|
+ <i class="el-icon-search"></i>
|
|
|
+ </div>
|
|
|
+ <h4>请配置查询条件</h4>
|
|
|
+ <p>选择需要的字段,设置筛选条件后点击"查询"按钮获取数据</p>
|
|
|
+ <div class="initial-tips">
|
|
|
+ <div class="tip-item">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ <span>已选择 <strong>{{ selectedFieldCodes.length }}</strong> 个字段</span>
|
|
|
+ </div>
|
|
|
+ <div class="tip-item">
|
|
|
+ <i class="el-icon-filter"></i>
|
|
|
+ <span>已配置 <strong>{{ queryConditions.length }}</strong> 个筛选条件</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据表格:执行过查询后显示 -->
|
|
|
+ <el-table
|
|
|
+ v-else
|
|
|
+ ref="dataTable"
|
|
|
+ v-loading="loading"
|
|
|
+ :data="tableData"
|
|
|
+ :max-height="tableMaxHeight"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ highlight-current-row
|
|
|
+ class="data-table"
|
|
|
+ @sort-change="handleSortChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="index" label="序号" width="60" align="center" fixed="left" />
|
|
|
+
|
|
|
+ <el-table-column
|
|
|
+ v-for="col in tableColumns"
|
|
|
+ :key="col.prop"
|
|
|
+ :prop="col.prop"
|
|
|
+ :label="col.label + (col.unit ? '(' + col.unit + ')' : '')"
|
|
|
+ :width="col.width"
|
|
|
+ :min-width="col.minWidth || 120"
|
|
|
+ :sortable="col.sortable ? 'custom' : false"
|
|
|
+ :align="col.type === 'number' ? 'right' : 'center'"
|
|
|
+ show-overflow-tooltip
|
|
|
+ >
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span v-if="col.type === 'number'">
|
|
|
+ {{ formatNumber(scope.row[col.prop], col.decimals) }}
|
|
|
+ </span>
|
|
|
+ <span v-else-if="col.type === 'date' || col.type === 'datetime'">
|
|
|
+ {{ formatDateTime(scope.row[col.prop], col.format) }}
|
|
|
+ </span>
|
|
|
+ <span v-else>{{ scope.row[col.prop] }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+
|
|
|
+ <!-- 空数据 -->
|
|
|
+ <template slot="empty">
|
|
|
+ <div class="empty-data">
|
|
|
+ <i class="el-icon-document-delete"></i>
|
|
|
+ <p>暂无数据</p>
|
|
|
+ <span>未查询到符合条件的记录,请调整筛选条件后重试</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination-wrapper" v-if="total > 0">
|
|
|
+ <el-pagination
|
|
|
+ background
|
|
|
+ :current-page.sync="pageNum"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :page-size.sync="pageSize"
|
|
|
:total="total"
|
|
|
- :page.sync="queryParams.pageNum"
|
|
|
- :limit.sync="queryParams.pageSize"
|
|
|
- @pagination="handleQuery"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleQuery"
|
|
|
+ @current-change="handleQuery"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 未选择数据源时的引导 -->
|
|
|
+ <div v-if="!selectedDsCode" class="empty-guide">
|
|
|
+ <div class="guide-icon">
|
|
|
+ <i class="el-icon-data-board"></i>
|
|
|
+ </div>
|
|
|
+ <h3>开始创建自定义报表</h3>
|
|
|
+ <p>请先在左侧选择一个数据源,然后配置字段和查询条件</p>
|
|
|
+ <div class="guide-steps">
|
|
|
+ <div class="step-item">
|
|
|
+ <span class="step-num">1</span>
|
|
|
+ <span class="step-text">选择数据源</span>
|
|
|
+ </div>
|
|
|
+ <div class="step-arrow">
|
|
|
+ <i class="el-icon-arrow-right"></i>
|
|
|
+ </div>
|
|
|
+ <div class="step-item">
|
|
|
+ <span class="step-num">2</span>
|
|
|
+ <span class="step-text">勾选字段</span>
|
|
|
+ </div>
|
|
|
+ <div class="step-arrow">
|
|
|
+ <i class="el-icon-arrow-right"></i>
|
|
|
+ </div>
|
|
|
+ <div class="step-item">
|
|
|
+ <span class="step-num">3</span>
|
|
|
+ <span class="step-text">配置条件</span>
|
|
|
+ </div>
|
|
|
+ <div class="step-arrow">
|
|
|
+ <i class="el-icon-arrow-right"></i>
|
|
|
+ </div>
|
|
|
+ <div class="step-item">
|
|
|
+ <span class="step-num">4</span>
|
|
|
+ <span class="step-text">查询导出</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
+
|
|
|
+ <!-- 模板管理弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ title="报表模板管理"
|
|
|
+ :visible.sync="showTemplateDialog"
|
|
|
+ width="800px"
|
|
|
+ class="template-dialog"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <div class="template-tabs">
|
|
|
+ <el-tabs v-model="templateTabActive">
|
|
|
+ <el-tab-pane label="我的模板" name="my">
|
|
|
+ <div class="template-list">
|
|
|
+ <div
|
|
|
+ v-for="tpl in myTemplates"
|
|
|
+ :key="tpl.templateCode"
|
|
|
+ class="template-item"
|
|
|
+ >
|
|
|
+ <div class="template-info">
|
|
|
+ <div class="template-name">
|
|
|
+ <i :class="getDatasourceIcon(getDatasourceCategory(tpl.dsCode))"></i>
|
|
|
+ {{ tpl.templateName }}
|
|
|
+ </div>
|
|
|
+ <div class="template-meta">
|
|
|
+ <span>数据源:{{ tpl.dsName }}</span>
|
|
|
+ <span>使用次数:{{ tpl.useCount || 0 }}</span>
|
|
|
+ <span>创建时间:{{ tpl.createTime }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="template-desc">{{ tpl.templateDesc }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="template-actions">
|
|
|
+ <el-button type="text" icon="el-icon-view" @click="handleLoadTemplate(tpl)">
|
|
|
+ 加载
|
|
|
+ </el-button>
|
|
|
+ <el-button type="text" icon="el-icon-edit" @click="handleEditTemplate(tpl)">
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button type="text" icon="el-icon-delete" class="danger" @click="handleDeleteTemplate(tpl)">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="myTemplates.length === 0" class="no-template">
|
|
|
+ <i class="el-icon-folder-opened"></i>
|
|
|
+ <p>暂无自定义模板</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="系统模板" name="system">
|
|
|
+ <div class="template-list">
|
|
|
+ <div
|
|
|
+ v-for="tpl in systemTemplates"
|
|
|
+ :key="tpl.templateCode"
|
|
|
+ class="template-item"
|
|
|
+ >
|
|
|
+ <div class="template-info">
|
|
|
+ <div class="template-name">
|
|
|
+ <i :class="getDatasourceIcon(getDatasourceCategory(tpl.dsCode))"></i>
|
|
|
+ {{ tpl.templateName }}
|
|
|
+ <el-tag size="mini" type="warning">系统</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="template-meta">
|
|
|
+ <span>数据源:{{ tpl.dsName }}</span>
|
|
|
+ <span>使用次数:{{ tpl.useCount || 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="template-desc">{{ tpl.templateDesc }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="template-actions">
|
|
|
+ <el-button type="text" icon="el-icon-view" @click="handleLoadTemplate(tpl)">
|
|
|
+ 加载
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="systemTemplates.length === 0" class="no-template">
|
|
|
+ <i class="el-icon-folder-opened"></i>
|
|
|
+ <p>暂无系统模板</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 保存模板弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ :title="editingTemplate ? '编辑模板' : '保存为模板'"
|
|
|
+ :visible.sync="showSaveTemplateDialog"
|
|
|
+ width="500px"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <el-form ref="templateForm" :model="templateForm" :rules="templateRules" label-width="100px">
|
|
|
+ <el-form-item label="模板名称" prop="templateName">
|
|
|
+ <el-input v-model="templateForm.templateName" placeholder="请输入模板名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="模板描述" prop="templateDesc">
|
|
|
+ <el-input
|
|
|
+ v-model="templateForm.templateDesc"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入模板描述"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="是否公开">
|
|
|
+ <el-switch v-model="templateForm.isPublic" />
|
|
|
+ <span class="form-tip">公开后其他用户可使用此模板</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="showSaveTemplateDialog = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitSaveTemplate">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { areaTreeSelect } from '@/api/basecfg/area'
|
|
|
-import { getAreaElecHourMeter } from '@/api/device/energyConsumption'
|
|
|
-import { listPvSupplyH } from '@/api/mgr/pgSupplyH'
|
|
|
+import {
|
|
|
+ listDatasources,
|
|
|
+ getDatasourceDetail,
|
|
|
+ executeQuery,
|
|
|
+ getSummary,
|
|
|
+ exportReport,
|
|
|
+ listTemplates,
|
|
|
+ getTemplate,
|
|
|
+ saveTemplate,
|
|
|
+ updateTemplate,
|
|
|
+ deleteTemplate,
|
|
|
+ downloadFile
|
|
|
+} from '@/api/mgr/customReport'
|
|
|
|
|
|
export default {
|
|
|
name: 'CustomReport',
|
|
|
data() {
|
|
|
return {
|
|
|
- loading: false,
|
|
|
- areaName: '',
|
|
|
- areaOptions: [],
|
|
|
- defaultExpandedKeys: [],
|
|
|
- selectedLabel: '全部',
|
|
|
+ // 数据源相关
|
|
|
+ datasourceList: [],
|
|
|
+ selectedDsCode: null,
|
|
|
+ currentDatasource: null,
|
|
|
+ allFields: [],
|
|
|
+
|
|
|
+ // 字段选择
|
|
|
+ selectedFieldCodes: [],
|
|
|
+ expandedGroups: [],
|
|
|
+
|
|
|
+ // 查询条件
|
|
|
+ queryConditions: [],
|
|
|
+
|
|
|
+ // 高级选项
|
|
|
+ advancedOptionsExpanded: [],
|
|
|
+ groupByFields: [],
|
|
|
+ orderByField: '',
|
|
|
+ orderDirection: 'DESC',
|
|
|
+ distinctFlag: false,
|
|
|
+ aggregateFieldsConfig: {},
|
|
|
+ aggregateFuncsConfig: {},
|
|
|
+
|
|
|
+ // 分页
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
total: 0,
|
|
|
- resultList: [],
|
|
|
- resultTableCols: [],
|
|
|
- queryParams: {
|
|
|
- pageNum: 1,
|
|
|
- pageSize: 10,
|
|
|
- reportType: 'prod',
|
|
|
- metricField: '',
|
|
|
- conditionType: 'gt',
|
|
|
- metricValue: null,
|
|
|
- areaCode: '-1',
|
|
|
- startRecTime: this.getFirstDayOfMonth(),
|
|
|
- endRecTime: this.getTodayEndTime(),
|
|
|
+
|
|
|
+ // 数据
|
|
|
+ tableData: [],
|
|
|
+ tableColumns: [],
|
|
|
+ summary: {},
|
|
|
+ loading: false,
|
|
|
+ hasQueried: false, // ===== 新增:标记是否执行过查询 =====
|
|
|
+
|
|
|
+ // 模板管理
|
|
|
+ showTemplateDialog: false,
|
|
|
+ templateTabActive: 'my',
|
|
|
+ myTemplates: [],
|
|
|
+ systemTemplates: [],
|
|
|
+
|
|
|
+ // 保存模板
|
|
|
+ showSaveTemplateDialog: false,
|
|
|
+ editingTemplate: null,
|
|
|
+ templateForm: {
|
|
|
+ templateName: '',
|
|
|
+ templateDesc: '',
|
|
|
+ isPublic: false
|
|
|
},
|
|
|
- metricOptions: [],
|
|
|
- metricOptionsMap: {
|
|
|
- prod: [
|
|
|
- { label: '总发电量', value: 'genElecQuantity' },
|
|
|
- { label: '自用电量', value: 'useElecQuantity' },
|
|
|
- { label: '上网电量', value: 'upElecQuantity' },
|
|
|
- { label: '上网收益', value: 'upElecEarn' },
|
|
|
- ],
|
|
|
- consume: [
|
|
|
- { label: '用电量', value: 'elecQuantity' },
|
|
|
- { label: '用电花费', value: 'useElecCost' },
|
|
|
+ templateRules: {
|
|
|
+ templateName: [
|
|
|
+ { required: true, message: '请输入模板名称', trigger: 'blur' },
|
|
|
+ { min: 2, max: 50, message: '长度在2到50个字符', trigger: 'blur' }
|
|
|
]
|
|
|
},
|
|
|
- defaultProps: {
|
|
|
- children: 'children',
|
|
|
- label: 'label'
|
|
|
- }
|
|
|
+
|
|
|
+ // 表格最大高度
|
|
|
+ tableMaxHeight: 500
|
|
|
}
|
|
|
},
|
|
|
- created() {
|
|
|
- this.metricOptions = this.metricOptionsMap[this.queryParams.reportType]
|
|
|
- this.getAreaList()
|
|
|
- },
|
|
|
- methods: {
|
|
|
- getAreaList() {
|
|
|
- areaTreeSelect(0, 1).then(response => {
|
|
|
- this.areaOptions = [{
|
|
|
- id: '-1',
|
|
|
- label: '全部',
|
|
|
- children: response.data || []
|
|
|
- }]
|
|
|
-
|
|
|
- // 设置默认展开第一级
|
|
|
- this.defaultExpandedKeys = ['-1']
|
|
|
|
|
|
- // 默认选中全部
|
|
|
- this.$nextTick(() => {
|
|
|
- if (this.$refs.tree) {
|
|
|
- this.$refs.tree.setCurrentKey('-1')
|
|
|
- }
|
|
|
- })
|
|
|
+ computed: {
|
|
|
+ // 按分组整理字段
|
|
|
+ groupedFields() {
|
|
|
+ const groups = {}
|
|
|
+ this.allFields.forEach(field => {
|
|
|
+ const groupName = field.groupName || '其他'
|
|
|
+ if (!groups[groupName]) {
|
|
|
+ groups[groupName] = []
|
|
|
+ }
|
|
|
+ groups[groupName].push(field)
|
|
|
})
|
|
|
+ return groups
|
|
|
},
|
|
|
|
|
|
- // 获取树节点图标
|
|
|
- getTreeIcon(data) {
|
|
|
- if (data.id === '-1') {
|
|
|
- return 'el-icon-s-home'
|
|
|
- }
|
|
|
- return 'el-icon-office-building'
|
|
|
+ // 是否全选字段
|
|
|
+ isAllFieldsSelected() {
|
|
|
+ return this.allFields.length > 0 &&
|
|
|
+ this.selectedFieldCodes.length === this.allFields.length
|
|
|
},
|
|
|
|
|
|
- // 过滤树
|
|
|
- filterTree() {
|
|
|
- this.$refs.tree.filter(this.areaName)
|
|
|
+ // 可用于分组的字段
|
|
|
+ availableGroupByFields() {
|
|
|
+ return this.allFields.filter(f =>
|
|
|
+ f.fieldType === 'string' || f.fieldType === 'date'
|
|
|
+ )
|
|
|
},
|
|
|
|
|
|
- handleNodeClick(data) {
|
|
|
- this.queryParams.areaCode = data.id
|
|
|
- this.selectedLabel = data.label
|
|
|
+ // 可排序字段
|
|
|
+ sortableFields() {
|
|
|
+ return this.allFields.filter(f => f.isSortable === 1)
|
|
|
},
|
|
|
|
|
|
- handleReportTypeChange(type) {
|
|
|
- this.metricOptions = this.metricOptionsMap[type]
|
|
|
- this.queryParams.metricField = ''
|
|
|
+ // 可聚合字段
|
|
|
+ aggregatableFields() {
|
|
|
+ return this.allFields.filter(f => f.isAggregatable === 1)
|
|
|
},
|
|
|
|
|
|
- handleQuery() {
|
|
|
- const { reportType, ...params } = this.queryParams
|
|
|
- this.loading = true
|
|
|
- const api = reportType === 'prod' ? listPvSupplyH : getAreaElecHourMeter
|
|
|
-
|
|
|
- api(params).then(res => {
|
|
|
- const list = res.rows || []
|
|
|
- const metric = this.queryParams.metricField
|
|
|
- const value = this.queryParams.metricValue
|
|
|
- const op = this.queryParams.conditionType
|
|
|
-
|
|
|
- this.resultList = list.filter(item => {
|
|
|
- const v = item[metric]
|
|
|
- if (v === undefined || v === null) return false
|
|
|
- switch (op) {
|
|
|
- case 'gt': return v > value
|
|
|
- case 'lt': return v < value
|
|
|
- case 'eq': return v == value
|
|
|
- case 'ne': return v != value
|
|
|
- default: return true
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- this.resultTableCols = this.generateColumns(reportType)
|
|
|
- this.total = this.resultList.length
|
|
|
- this.loading = false
|
|
|
- })
|
|
|
+ // 是否可以保存模板
|
|
|
+ canSaveTemplate() {
|
|
|
+ return this.selectedDsCode && this.selectedFieldCodes.length > 0
|
|
|
},
|
|
|
|
|
|
- resetQuery() {
|
|
|
- this.queryParams = {
|
|
|
- ...this.queryParams,
|
|
|
- metricField: '',
|
|
|
- conditionType: 'gt',
|
|
|
- metricValue: null,
|
|
|
- startRecTime: this.getFirstDayOfMonth(),
|
|
|
- endRecTime: this.getTodayEndTime(),
|
|
|
+ // 是否有查询结果
|
|
|
+ hasQueryResult() {
|
|
|
+ return this.tableData.length > 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ watch: {
|
|
|
+ showTemplateDialog(val) {
|
|
|
+ if (val) {
|
|
|
+ this.loadTemplates()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ created() {
|
|
|
+ this.loadDatasources()
|
|
|
+ this.calculateTableHeight()
|
|
|
+ },
|
|
|
+
|
|
|
+ mounted() {
|
|
|
+ window.addEventListener('resize', this.calculateTableHeight)
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('resize', this.calculateTableHeight)
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ // 加载数据源列表
|
|
|
+ async loadDatasources() {
|
|
|
+ try {
|
|
|
+ const response = await listDatasources()
|
|
|
+ this.datasourceList = response.data || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('加载数据源失败')
|
|
|
}
|
|
|
- this.resultList = []
|
|
|
- this.total = 0
|
|
|
},
|
|
|
|
|
|
- generateColumns(type) {
|
|
|
- return type === 'prod'
|
|
|
- ? [
|
|
|
- { label: '日期', prop: 'date' },
|
|
|
- { label: '时间', prop: 'time' },
|
|
|
- { label: '总发电量(kW·h)', prop: 'genElecQuantity' },
|
|
|
- { label: '自用电量(kW·h)', prop: 'useElecQuantity' },
|
|
|
- { label: '上网电量(kW·h)', prop: 'upElecQuantity' },
|
|
|
- { label: '上网收益(元)', prop: 'upElecEarn' },
|
|
|
- ]
|
|
|
- : [
|
|
|
- { label: '对象名称', prop: 'deviceName' },
|
|
|
- { label: '日期', prop: 'date' },
|
|
|
- { label: '时间', prop: 'time' },
|
|
|
- { label: '用电量(kW·h)', prop: 'elecQuantity' },
|
|
|
- { label: '用电花费(元)', prop: 'useElecCost' },
|
|
|
- ]
|
|
|
+ // 获取数据源图标
|
|
|
+ getDatasourceIcon(category) {
|
|
|
+ const iconMap = {
|
|
|
+ 'prod': 'el-icon-sunny',
|
|
|
+ 'elec': 'el-icon-lightning',
|
|
|
+ 'water': 'el-icon-cold-drink',
|
|
|
+ 'store': 'el-icon-coin'
|
|
|
+ }
|
|
|
+ return iconMap[category] || 'el-icon-s-data'
|
|
|
},
|
|
|
|
|
|
- handleExport() {
|
|
|
- const { reportType } = this.queryParams
|
|
|
- const url = reportType === 'prod'
|
|
|
- ? 'ems/prod/pv/hour/export'
|
|
|
- : 'ems/elecMeterH/exportAreaMeter'
|
|
|
- this.download(url, this.queryParams, `自定义报表_${new Date().getTime()}.xlsx`)
|
|
|
+ // 选择数据源
|
|
|
+ async handleSelectDatasource(ds) {
|
|
|
+ if (this.selectedDsCode === ds.dsCode) return
|
|
|
+
|
|
|
+ this.selectedDsCode = ds.dsCode
|
|
|
+ this.resetQueryState()
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await getDatasourceDetail(ds.dsCode)
|
|
|
+ this.currentDatasource = response.data
|
|
|
+ this.allFields = response.data.fields || []
|
|
|
+
|
|
|
+ // 默认展开所有分组
|
|
|
+ this.expandedGroups = Object.keys(this.groupedFields)
|
|
|
+
|
|
|
+ // 默认选中标记为默认的字段
|
|
|
+ this.selectedFieldCodes = this.allFields
|
|
|
+ .filter(f => f.isDefault === 1)
|
|
|
+ .map(f => f.fieldCode)
|
|
|
+
|
|
|
+ // 如果没有默认字段,选中前5个
|
|
|
+ if (this.selectedFieldCodes.length === 0) {
|
|
|
+ this.selectedFieldCodes = this.allFields.slice(0, 5).map(f => f.fieldCode)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('加载数据源详情失败')
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
- getMetricLabel() {
|
|
|
- const metric = this.metricOptions.find(m => m.value === this.queryParams.metricField)
|
|
|
- return metric ? metric.label : '未选择'
|
|
|
+ // 重置查询状态
|
|
|
+ resetQueryState() {
|
|
|
+ this.selectedFieldCodes = []
|
|
|
+ this.queryConditions = []
|
|
|
+ this.groupByFields = []
|
|
|
+ this.orderByField = ''
|
|
|
+ this.orderDirection = 'DESC'
|
|
|
+ this.distinctFlag = false
|
|
|
+ this.aggregateFieldsConfig = {}
|
|
|
+ this.aggregateFuncsConfig = {}
|
|
|
+ this.tableData = []
|
|
|
+ this.tableColumns = []
|
|
|
+ this.summary = {}
|
|
|
+ this.total = 0
|
|
|
+ this.pageNum = 1
|
|
|
+ this.hasQueried = false // ===== 重置查询状态 =====
|
|
|
},
|
|
|
|
|
|
- getConditionLabel() {
|
|
|
- const conditions = {
|
|
|
- 'gt': '>',
|
|
|
- 'lt': '<',
|
|
|
- 'eq': '=',
|
|
|
- 'ne': '≠'
|
|
|
+ // 切换字段分组展开
|
|
|
+ toggleFieldGroup(groupName) {
|
|
|
+ const index = this.expandedGroups.indexOf(groupName)
|
|
|
+ if (index > -1) {
|
|
|
+ this.expandedGroups.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ this.expandedGroups.push(groupName)
|
|
|
}
|
|
|
- return conditions[this.queryParams.conditionType] || ''
|
|
|
},
|
|
|
|
|
|
- formatNumber(value) {
|
|
|
- if (value === null || value === undefined) return '0.00'
|
|
|
- return parseFloat(value).toFixed(2)
|
|
|
+ // 获取分组中已选字段数
|
|
|
+ getSelectedCountInGroup(groupName) {
|
|
|
+ const fields = this.groupedFields[groupName] || []
|
|
|
+ return fields.filter(f => this.selectedFieldCodes.includes(f.fieldCode)).length
|
|
|
},
|
|
|
|
|
|
- getFirstDayOfMonth() {
|
|
|
- const date = new Date()
|
|
|
- date.setDate(1)
|
|
|
- date.setHours(0, 0, 0, 0)
|
|
|
- return this.formatDateTime(date)
|
|
|
+ // 全选/取消全选字段
|
|
|
+ handleSelectAllFields() {
|
|
|
+ if (this.isAllFieldsSelected) {
|
|
|
+ this.selectedFieldCodes = []
|
|
|
+ } else {
|
|
|
+ this.selectedFieldCodes = this.allFields.map(f => f.fieldCode)
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
- getTodayEndTime() {
|
|
|
- const date = new Date()
|
|
|
- date.setHours(23, 59, 59, 999)
|
|
|
- return this.formatDateTime(date)
|
|
|
+ // 添加查询条件
|
|
|
+ addCondition() {
|
|
|
+ this.queryConditions.push({
|
|
|
+ field: '',
|
|
|
+ operator: 'eq',
|
|
|
+ value: null,
|
|
|
+ value2: null,
|
|
|
+ dateRange: [],
|
|
|
+ optional: true
|
|
|
+ })
|
|
|
},
|
|
|
|
|
|
- formatDateTime(date) {
|
|
|
- if (!date) return ''
|
|
|
- if (typeof date === 'string') date = new Date(date)
|
|
|
- const y = date.getFullYear()
|
|
|
- const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
- const d = String(date.getDate()).padStart(2, '0')
|
|
|
- const h = String(date.getHours()).padStart(2, '0')
|
|
|
- return `${y}-${m}-${d} ${h}:00:00`
|
|
|
+ // 移除查询条件
|
|
|
+ removeCondition(index) {
|
|
|
+ this.queryConditions.splice(index, 1)
|
|
|
},
|
|
|
|
|
|
- filterNode(value, data) {
|
|
|
- if (!value) return true
|
|
|
- return data.label.indexOf(value) !== -1
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</script>
|
|
|
+ // 条件字段变化
|
|
|
+ handleConditionFieldChange(condition) {
|
|
|
+ condition.value = null
|
|
|
+ condition.value2 = null
|
|
|
+ condition.dateRange = []
|
|
|
+ condition.operator = 'eq'
|
|
|
+ },
|
|
|
|
|
|
-<style lang="scss" scoped>
|
|
|
-.app-container {
|
|
|
- padding: 20px;
|
|
|
- background: #f5f7fa;
|
|
|
- min-height: calc(100vh - 84px);
|
|
|
+ // 获取字段类型
|
|
|
+ getFieldType(fieldCode) {
|
|
|
+ const field = this.allFields.find(f => f.fieldCode === fieldCode)
|
|
|
+ return field ? field.fieldType : 'string'
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取可用的操作符
|
|
|
+ getAvailableOperators(fieldCode) {
|
|
|
+ const fieldType = this.getFieldType(fieldCode)
|
|
|
+
|
|
|
+ const commonOps = [
|
|
|
+ { value: 'eq', label: '等于' },
|
|
|
+ { value: 'ne', label: '不等于' }
|
|
|
+ ]
|
|
|
+
|
|
|
+ if (fieldType === 'number') {
|
|
|
+ return [
|
|
|
+ ...commonOps,
|
|
|
+ { value: 'gt', label: '大于' },
|
|
|
+ { value: 'gte', label: '大于等于' },
|
|
|
+ { value: 'lt', label: '小于' },
|
|
|
+ { value: 'lte', label: '小于等于' },
|
|
|
+ { value: 'between', label: '范围' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
|
|
|
- .head-container {
|
|
|
- background: #fff;
|
|
|
- padding: 15px;
|
|
|
- border-radius: 8px;
|
|
|
- margin-bottom: 15px;
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ if (fieldType === 'date' || fieldType === 'datetime') {
|
|
|
+ return [
|
|
|
+ { value: 'gte', label: '开始于' },
|
|
|
+ { value: 'lte', label: '结束于' },
|
|
|
+ { value: 'between', label: '时间范围' },
|
|
|
+ { value: 'eq', label: '等于' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
|
|
|
- &.tree-container {
|
|
|
- max-height: calc(100vh - 280px);
|
|
|
- overflow-y: auto;
|
|
|
+ return [
|
|
|
+ ...commonOps,
|
|
|
+ { value: 'like', label: '包含' },
|
|
|
+ { value: 'in', label: '在列表中' },
|
|
|
+ { value: 'isNull', label: '为空' },
|
|
|
+ { value: 'isNotNull', label: '不为空' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
|
|
|
- &::-webkit-scrollbar {
|
|
|
- width: 6px;
|
|
|
+ // 日期范围变化处理
|
|
|
+ handleDateRangeChange(condition) {
|
|
|
+ if (condition.dateRange && condition.dateRange.length === 2) {
|
|
|
+ condition.value = condition.dateRange[0]
|
|
|
+ condition.value2 = condition.dateRange[1]
|
|
|
+ } else {
|
|
|
+ condition.value = null
|
|
|
+ condition.value2 = null
|
|
|
}
|
|
|
+ },
|
|
|
|
|
|
- &::-webkit-scrollbar-track {
|
|
|
- background: #f1f1f1;
|
|
|
- border-radius: 3px;
|
|
|
+ // 构建查询配置
|
|
|
+ buildQueryConfig() {
|
|
|
+ const config = {
|
|
|
+ dsCode: this.selectedDsCode,
|
|
|
+ selectedFields: this.selectedFieldCodes,
|
|
|
+ conditions: this.queryConditions
|
|
|
+ .filter(c => c.field && (c.value !== null || c.operator === 'isNull' || c.operator === 'isNotNull'))
|
|
|
+ .map(c => ({
|
|
|
+ field: c.field,
|
|
|
+ operator: c.operator,
|
|
|
+ value: c.value,
|
|
|
+ value2: c.value2,
|
|
|
+ optional: c.optional
|
|
|
+ })),
|
|
|
+ distinct: this.distinctFlag,
|
|
|
+ pageNum: this.pageNum,
|
|
|
+ pageSize: this.pageSize
|
|
|
}
|
|
|
|
|
|
- &::-webkit-scrollbar-thumb {
|
|
|
- background: #c1c1c1;
|
|
|
- border-radius: 3px;
|
|
|
+ // 分组
|
|
|
+ if (this.groupByFields.length > 0) {
|
|
|
+ config.groupBy = this.groupByFields
|
|
|
+
|
|
|
+ // 聚合函数
|
|
|
+ const aggregateFields = {}
|
|
|
+ Object.keys(this.aggregateFieldsConfig).forEach(key => {
|
|
|
+ if (this.aggregateFieldsConfig[key]) {
|
|
|
+ aggregateFields[key] = this.aggregateFuncsConfig[key] || 'SUM'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (Object.keys(aggregateFields).length > 0) {
|
|
|
+ config.aggregateFields = aggregateFields
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- &::-webkit-scrollbar-thumb:hover {
|
|
|
- background: #a8a8a8;
|
|
|
+ // 排序
|
|
|
+ if (this.orderByField) {
|
|
|
+ config.orderBy = [{
|
|
|
+ field: this.orderByField,
|
|
|
+ direction: this.orderDirection
|
|
|
+ }]
|
|
|
}
|
|
|
|
|
|
- ::v-deep .el-tree {
|
|
|
- background: transparent;
|
|
|
+ return config
|
|
|
+ },
|
|
|
|
|
|
- .el-tree-node__content {
|
|
|
- height: 40px;
|
|
|
- padding: 0 8px;
|
|
|
- transition: all 0.3s;
|
|
|
+ // 执行查询
|
|
|
+ async handleQuery() {
|
|
|
+ if (this.selectedFieldCodes.length === 0) {
|
|
|
+ this.$modal.msgWarning('请至少选择一个字段')
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- &:hover {
|
|
|
- background-color: #f5f7fa;
|
|
|
+ this.loading = true
|
|
|
+ this.hasQueried = true // ===== 标记已执行查询 =====
|
|
|
+
|
|
|
+ try {
|
|
|
+ const queryConfig = this.buildQueryConfig()
|
|
|
+ const response = await executeQuery(queryConfig)
|
|
|
+
|
|
|
+ if (response.data) {
|
|
|
+ this.tableData = response.data.rows || []
|
|
|
+ this.tableColumns = response.data.columns || []
|
|
|
+ this.total = response.data.total || 0
|
|
|
+
|
|
|
+ // 获取汇总数据
|
|
|
+ if (this.groupByFields.length > 0) {
|
|
|
+ const summaryResponse = await getSummary(queryConfig)
|
|
|
+ this.summary = summaryResponse.data || {}
|
|
|
+ } else {
|
|
|
+ this.summary = {}
|
|
|
}
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('查询失败:' + (error.message || '未知错误'))
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- .el-tree-node.is-current > .el-tree-node__content {
|
|
|
- background-color: #ecf5ff;
|
|
|
- color: #409eff;
|
|
|
- }
|
|
|
+ // 重置查询
|
|
|
+ handleReset() {
|
|
|
+ this.queryConditions = []
|
|
|
+ this.groupByFields = []
|
|
|
+ this.orderByField = ''
|
|
|
+ this.orderDirection = 'DESC'
|
|
|
+ this.distinctFlag = false
|
|
|
+ this.aggregateFieldsConfig = {}
|
|
|
+ this.aggregateFuncsConfig = {}
|
|
|
+ this.tableData = []
|
|
|
+ this.tableColumns = []
|
|
|
+ this.summary = {}
|
|
|
+ this.total = 0
|
|
|
+ this.pageNum = 1
|
|
|
+ this.hasQueried = false // ===== 重置查询状态 =====
|
|
|
+ },
|
|
|
|
|
|
- .custom-tree-node {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- font-size: 14px;
|
|
|
-
|
|
|
- .tree-label {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
-
|
|
|
- .tree-icon {
|
|
|
- margin-right: 8px;
|
|
|
- font-size: 16px;
|
|
|
- color: #909399;
|
|
|
- transition: color 0.3s;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // 表格排序变化
|
|
|
+ handleSortChange({ prop, order }) {
|
|
|
+ if (order) {
|
|
|
+ this.orderByField = prop
|
|
|
+ this.orderDirection = order === 'ascending' ? 'ASC' : 'DESC'
|
|
|
+ } else {
|
|
|
+ this.orderByField = ''
|
|
|
+ }
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
|
|
|
- .el-tree-node.is-current .tree-icon {
|
|
|
- color: #409eff;
|
|
|
- }
|
|
|
+ // 导出Excel
|
|
|
+ async handleExport() {
|
|
|
+ if (!this.hasQueryResult) {
|
|
|
+ this.$modal.msgWarning('请先查询数据')
|
|
|
+ return
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- .content-wrapper {
|
|
|
- background: #fff;
|
|
|
- border-radius: 8px;
|
|
|
- padding: 20px;
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
- min-height: calc(100vh - 160px);
|
|
|
-
|
|
|
- .content-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 20px;
|
|
|
- padding-bottom: 15px;
|
|
|
- border-bottom: 2px solid #f0f2f5;
|
|
|
-
|
|
|
- .page-title {
|
|
|
- margin: 0;
|
|
|
- font-size: 20px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
-
|
|
|
- i {
|
|
|
- margin-right: 8px;
|
|
|
- color: #409eff;
|
|
|
+ try {
|
|
|
+ this.$modal.loading('正在导出数据,请稍候...')
|
|
|
+ const queryConfig = {
|
|
|
+ ...this.buildQueryConfig(),
|
|
|
+ exportAll: true
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- .search-container {
|
|
|
- background: #f9fbfd;
|
|
|
- border-radius: 8px;
|
|
|
- padding: 15px;
|
|
|
- margin-bottom: 20px;
|
|
|
+ const response = await exportReport(queryConfig)
|
|
|
+ const dsName = this.currentDatasource?.dsName || '报表'
|
|
|
+ const filename = `${dsName}_${this.getCurrentTimeStr()}.xlsx`
|
|
|
+ downloadFile(response, filename)
|
|
|
|
|
|
- ::v-deep .el-form {
|
|
|
- .el-form-item {
|
|
|
- margin-bottom: 10px;
|
|
|
- margin-right: 15px;
|
|
|
+ this.$modal.closeLoading()
|
|
|
+ this.$modal.msgSuccess('导出成功')
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.closeLoading()
|
|
|
+ this.$modal.msgError('导出失败:' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
- .el-form-item__label {
|
|
|
- font-weight: 500;
|
|
|
- color: #606266;
|
|
|
- }
|
|
|
- }
|
|
|
+ // 获取当前时间字符串
|
|
|
+ getCurrentTimeStr() {
|
|
|
+ const now = new Date()
|
|
|
+ return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`
|
|
|
+ },
|
|
|
|
|
|
- .el-button {
|
|
|
- padding: 7px 15px;
|
|
|
- }
|
|
|
+ // 加载模板列表
|
|
|
+ async loadTemplates() {
|
|
|
+ try {
|
|
|
+ const response = await listTemplates({ pageNum: 1, pageSize: 100 })
|
|
|
+ const templates = response.rows || []
|
|
|
+
|
|
|
+ this.systemTemplates = templates.filter(t => t.isSystem === 1)
|
|
|
+ this.myTemplates = templates.filter(t => t.isSystem !== 1)
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('加载模板失败')
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取数据源分类
|
|
|
+ getDatasourceCategory(dsCode) {
|
|
|
+ const ds = this.datasourceList.find(d => d.dsCode === dsCode)
|
|
|
+ return ds ? ds.category : ''
|
|
|
+ },
|
|
|
|
|
|
- .stats-cards {
|
|
|
- margin-bottom: 20px;
|
|
|
-
|
|
|
- .stat-card {
|
|
|
- background: #fff;
|
|
|
- border-radius: 8px;
|
|
|
- padding: 20px;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- transition: transform 0.3s, box-shadow 0.3s;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
|
+ // 加载模板
|
|
|
+ async handleLoadTemplate(tpl) {
|
|
|
+ try {
|
|
|
+ const response = await getTemplate(tpl.templateCode)
|
|
|
+ const template = response.data
|
|
|
+
|
|
|
+ // 切换数据源
|
|
|
+ const ds = this.datasourceList.find(d => d.dsCode === template.dsCode)
|
|
|
+ if (ds) {
|
|
|
+ await this.handleSelectDatasource(ds)
|
|
|
}
|
|
|
|
|
|
- .stat-icon {
|
|
|
- width: 60px;
|
|
|
- height: 60px;
|
|
|
- border-radius: 12px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-right: 15px;
|
|
|
-
|
|
|
- i {
|
|
|
- font-size: 28px;
|
|
|
- color: #fff;
|
|
|
+ // 应用模板配置
|
|
|
+ if (template.queryConfig) {
|
|
|
+ const config = template.queryConfig
|
|
|
+
|
|
|
+ // 字段选择
|
|
|
+ if (config.selectedFields) {
|
|
|
+ this.selectedFieldCodes = config.selectedFields
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- .stat-content {
|
|
|
- flex: 1;
|
|
|
+ // 查询条件
|
|
|
+ if (config.conditions) {
|
|
|
+ this.queryConditions = config.conditions.map(c => ({
|
|
|
+ ...c,
|
|
|
+ dateRange: c.value && c.value2 ? [c.value, c.value2] : []
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分组
|
|
|
+ if (config.groupBy) {
|
|
|
+ this.groupByFields = config.groupBy
|
|
|
+ }
|
|
|
|
|
|
- .stat-value {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
- margin-bottom: 5px;
|
|
|
+ // 排序
|
|
|
+ if (config.orderBy && config.orderBy.length > 0) {
|
|
|
+ this.orderByField = config.orderBy[0].field
|
|
|
+ this.orderDirection = config.orderBy[0].direction || 'DESC'
|
|
|
}
|
|
|
|
|
|
- .stat-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #909399;
|
|
|
+ // 聚合
|
|
|
+ if (config.aggregateFields) {
|
|
|
+ Object.keys(config.aggregateFields).forEach(key => {
|
|
|
+ this.aggregateFieldsConfig[key] = true
|
|
|
+ this.aggregateFuncsConfig[key] = config.aggregateFields[key]
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ this.showTemplateDialog = false
|
|
|
+ this.$modal.msgSuccess('模板加载成功')
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('加载模板失败')
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
|
|
|
- .table-container {
|
|
|
- ::v-deep .el-table {
|
|
|
- .el-table__header {
|
|
|
- th {
|
|
|
- background-color: #f5f7fa;
|
|
|
- color: #606266;
|
|
|
- font-weight: 600;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
+ // 保存模板
|
|
|
+ handleSaveTemplate() {
|
|
|
+ this.editingTemplate = null
|
|
|
+ this.templateForm = {
|
|
|
+ templateName: '',
|
|
|
+ templateDesc: '',
|
|
|
+ isPublic: false
|
|
|
+ }
|
|
|
+ this.showSaveTemplateDialog = true
|
|
|
+ },
|
|
|
+
|
|
|
+ // 编辑模板
|
|
|
+ handleEditTemplate(tpl) {
|
|
|
+ this.editingTemplate = tpl
|
|
|
+ this.templateForm = {
|
|
|
+ templateCode: tpl.templateCode,
|
|
|
+ templateName: tpl.templateName,
|
|
|
+ templateDesc: tpl.templateDesc,
|
|
|
+ isPublic: tpl.isPublic === 1
|
|
|
+ }
|
|
|
+ this.showSaveTemplateDialog = true
|
|
|
+ this.showTemplateDialog = false
|
|
|
+ },
|
|
|
+
|
|
|
+ // 删除模板
|
|
|
+ handleDeleteTemplate(tpl) {
|
|
|
+ this.$confirm(`确认删除模板"${tpl.templateName}"?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ try {
|
|
|
+ await deleteTemplate(tpl.templateCode)
|
|
|
+ this.$modal.msgSuccess('删除成功')
|
|
|
+ this.loadTemplates()
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('删除失败')
|
|
|
}
|
|
|
+ }).catch(() => {})
|
|
|
+ },
|
|
|
|
|
|
- .el-table__body {
|
|
|
- .data-value {
|
|
|
- font-weight: 600;
|
|
|
- color: #409eff;
|
|
|
- font-size: 14px;
|
|
|
+ // 提交保存模板
|
|
|
+ submitSaveTemplate() {
|
|
|
+ this.$refs.templateForm.validate(async (valid) => {
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ const configJson = JSON.stringify(this.buildQueryConfig())
|
|
|
+
|
|
|
+ const templateData = {
|
|
|
+ ...this.templateForm,
|
|
|
+ dsCode: this.selectedDsCode,
|
|
|
+ configJson,
|
|
|
+ isPublic: this.templateForm.isPublic ? 1 : 0
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (this.editingTemplate) {
|
|
|
+ await updateTemplate(templateData)
|
|
|
+ this.$modal.msgSuccess('模板更新成功')
|
|
|
+ } else {
|
|
|
+ await saveTemplate(templateData)
|
|
|
+ this.$modal.msgSuccess('模板保存成功')
|
|
|
}
|
|
|
+ this.showSaveTemplateDialog = false
|
|
|
+ } catch (error) {
|
|
|
+ this.$modal.msgError('保存失败:' + (error.message || '未知错误'))
|
|
|
}
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化数值
|
|
|
+ formatNumber(value, decimals = 2) {
|
|
|
+ if (value === null || value === undefined || isNaN(value)) {
|
|
|
+ return '-'
|
|
|
+ }
|
|
|
+ return Number(value).toFixed(decimals)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化日期时间
|
|
|
+ formatDateTime(value, format) {
|
|
|
+ if (!value) return '-'
|
|
|
+ return value
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取汇总标签
|
|
|
+ getSummaryLabel(key) {
|
|
|
+ const field = this.allFields.find(f => f.fieldCode === key || f.fieldAlias === key)
|
|
|
+ return field ? field.fieldName : key
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化汇总值
|
|
|
+ formatSummaryValue(key, value) {
|
|
|
+ const field = this.allFields.find(f => f.fieldCode === key || f.fieldAlias === key)
|
|
|
+ if (field && field.fieldType === 'number') {
|
|
|
+ const formatted = this.formatNumber(value, field.decimals || 2)
|
|
|
+ return field.unit ? `${formatted} ${field.unit}` : formatted
|
|
|
}
|
|
|
+ return value
|
|
|
+ },
|
|
|
+
|
|
|
+ // 计算表格高度
|
|
|
+ calculateTableHeight() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const windowHeight = window.innerHeight
|
|
|
+ this.tableMaxHeight = windowHeight - 450
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.custom-report-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+}
|
|
|
+
|
|
|
+/* 页面头部 */
|
|
|
+.page-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 16px 20px;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.header-left {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
|
|
|
-// 响应式布局
|
|
|
+.page-title {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title i {
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-subtitle {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.header-right .el-button {
|
|
|
+ border-color: rgba(255, 255, 255, 0.5);
|
|
|
+ color: #fff;
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.header-right .el-button:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border-color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+/* 配置面板 */
|
|
|
+.config-panel {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-section {
|
|
|
+ padding: 16px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-section:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title i {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title .el-button {
|
|
|
+ margin-left: auto;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 数据源卡片 */
|
|
|
+.datasource-cards {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px;
|
|
|
+ border: 2px solid #e4e7ed;
|
|
|
+ border-radius: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card:hover {
|
|
|
+ border-color: #667eea;
|
|
|
+ background: #f8f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card.active {
|
|
|
+ border-color: #667eea;
|
|
|
+ background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card-icon {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 20px;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card-content {
|
|
|
+ flex: 1;
|
|
|
+ margin-left: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card-name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.ds-card-check {
|
|
|
+ position: absolute;
|
|
|
+ top: -8px;
|
|
|
+ right: -8px;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #67c23a;
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+/* 字段分组 */
|
|
|
+.field-groups {
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.field-groups::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-groups::-webkit-scrollbar-thumb {
|
|
|
+ background: #c1c1c1;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-group {
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.group-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+ transition: background 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.group-header:hover {
|
|
|
+ background: #e8ebf0;
|
|
|
+}
|
|
|
+
|
|
|
+.group-header i {
|
|
|
+ font-size: 12px;
|
|
|
+ transition: transform 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.group-badge {
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.group-fields {
|
|
|
+ padding: 8px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-checkbox {
|
|
|
+ display: block;
|
|
|
+ margin: 6px 0;
|
|
|
+ margin-left: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label .el-tag {
|
|
|
+ transform: scale(0.85);
|
|
|
+}
|
|
|
+
|
|
|
+/* 查询面板 */
|
|
|
+.query-panel {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 条件区域 */
|
|
|
+.conditions-area {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-field {
|
|
|
+ width: 180px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-operator {
|
|
|
+ width: 110px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-value {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 150px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-value-wide {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-value-half {
|
|
|
+ width: 100px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-separator {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-delete {
|
|
|
+ color: #f56c6c;
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.condition-delete:hover {
|
|
|
+ color: #f44336;
|
|
|
+}
|
|
|
+
|
|
|
+.no-conditions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 30px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 高级选项 */
|
|
|
+.advanced-options {
|
|
|
+ margin-top: 16px;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.advanced-options /deep/ .el-collapse-item__header {
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 0 12px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.advanced-options /deep/ .el-collapse-item__content {
|
|
|
+ padding: 16px 0 0;
|
|
|
+}
|
|
|
+
|
|
|
+.option-item {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.option-item label {
|
|
|
+ display: block;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.aggregate-config {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding-top: 16px;
|
|
|
+ border-top: 1px dashed #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.aggregate-config > label {
|
|
|
+ display: block;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.aggregate-fields {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.aggregate-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 操作按钮 */
|
|
|
+.query-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ padding-top: 16px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 结果面板 */
|
|
|
+.result-panel {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 汇总栏 */
|
|
|
+.summary-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
+ border-bottom: 1px solid #e0f2fe;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0284c7;
|
|
|
+ margin-right: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-items {
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-value {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0284c7;
|
|
|
+}
|
|
|
+
|
|
|
+/* ===== 新增:初始状态样式 ===== */
|
|
|
+.initial-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 60px 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.initial-icon {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.initial-icon i {
|
|
|
+ font-size: 36px;
|
|
|
+ color: #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.initial-state h4 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.initial-state p {
|
|
|
+ margin: 0 0 24px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.initial-tips {
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-item i {
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-item strong {
|
|
|
+ color: #667eea;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+/* 数据表格 */
|
|
|
+.data-table {
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.data-table /deep/ .el-table__header th {
|
|
|
+ background: #f8f9fa;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 空数据 - 修复样式,确保最小宽度 */
|
|
|
+.empty-data {
|
|
|
+ padding: 60px 20px;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ min-width: 300px; /* ===== 确保最小宽度 ===== */
|
|
|
+}
|
|
|
+
|
|
|
+.empty-data i {
|
|
|
+ font-size: 60px;
|
|
|
+ color: #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-data p {
|
|
|
+ margin: 16px 0 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #606266;
|
|
|
+ white-space: nowrap; /* ===== 防止文字换行 ===== */
|
|
|
+}
|
|
|
+
|
|
|
+.empty-data span {
|
|
|
+ font-size: 13px;
|
|
|
+ white-space: nowrap; /* ===== 防止文字换行 ===== */
|
|
|
+}
|
|
|
+
|
|
|
+/* 分页 */
|
|
|
+.pagination-wrapper {
|
|
|
+ padding: 16px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ background: #fafafa;
|
|
|
+}
|
|
|
+
|
|
|
+/* 引导页面 */
|
|
|
+.empty-guide {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 80px 20px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.guide-icon {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.guide-icon i {
|
|
|
+ font-size: 48px;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-guide h3 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 20px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-guide p {
|
|
|
+ margin: 0 0 32px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.guide-steps {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.step-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 10px 16px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.step-num {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #667eea;
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.step-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.step-arrow {
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+/* 模板弹窗 */
|
|
|
+.template-dialog /deep/ .el-dialog__body {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.template-tabs {
|
|
|
+ padding: 0 20px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.template-list {
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.template-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 16px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.template-item:hover {
|
|
|
+ border-color: #667eea;
|
|
|
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.template-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.template-name {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.template-name i {
|
|
|
+ color: #667eea;
|
|
|
+}
|
|
|
+
|
|
|
+.template-meta {
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.template-desc {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.template-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.template-actions .danger {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.no-template {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 40px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.no-template i {
|
|
|
+ font-size: 48px;
|
|
|
+ color: #dcdfe6;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单提示 */
|
|
|
+.form-tip {
|
|
|
+ margin-left: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式 */
|
|
|
@media (max-width: 768px) {
|
|
|
- .app-container {
|
|
|
- padding: 10px;
|
|
|
+ .page-header {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
|
|
|
- .el-col {
|
|
|
- margin-bottom: 20px;
|
|
|
- }
|
|
|
+ .condition-row {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
|
|
|
- .content-wrapper {
|
|
|
- .content-header {
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-start;
|
|
|
- }
|
|
|
+ .condition-field,
|
|
|
+ .condition-operator,
|
|
|
+ .condition-value {
|
|
|
+ width: 100%;
|
|
|
+ min-width: 100%;
|
|
|
+ }
|
|
|
|
|
|
- .search-container {
|
|
|
- ::v-deep .el-form {
|
|
|
- .el-form-item {
|
|
|
- display: block;
|
|
|
- margin-bottom: 10px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ .guide-steps {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
|
|
|
- .stats-cards {
|
|
|
- .el-col {
|
|
|
- margin-bottom: 10px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ .step-arrow {
|
|
|
+ transform: rotate(90deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ .initial-tips {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|