Quellcode durchsuchen

自定义报表

learshaw vor 2 Monaten
Ursprung
Commit
d150db2598

+ 314 - 0
ems-ui-cloud/src/api/mgr/customReport.js

@@ -0,0 +1,314 @@
+/**
+ * 自定义报表API
+ * @author ruoyi
+ * @date 2026-01-30
+ */
+import request from '@/utils/request'
+
+// ==================== 数据源管理 ====================
+
+/**
+ * 查询数据源列表
+ * @param {String} category - 分类筛选(可选)
+ */
+export function listDatasources(category) {
+  return request({
+    url: '/ems/report/custom/datasource/list',
+    method: 'get',
+    params: { category }
+  })
+}
+
+/**
+ * 获取数据源详情(包含字段、条件、关联配置)
+ * @param {String} dsCode - 数据源编码
+ */
+export function getDatasourceDetail(dsCode) {
+  return request({
+    url: `/ems/report/custom/datasource/${dsCode}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 获取数据源字段列表
+ * @param {String} dsCode - 数据源编码
+ */
+export function getDatasourceFields(dsCode) {
+  return request({
+    url: `/ems/report/custom/datasource/${dsCode}/fields`,
+    method: 'get'
+  })
+}
+
+/**
+ * 获取字段条件列表
+ * @param {String} dsCode - 数据源编码
+ * @param {String} fieldCode - 字段编码
+ */
+export function getFieldConditions(dsCode, fieldCode) {
+  return request({
+    url: `/ems/report/custom/datasource/${dsCode}/field/${fieldCode}/conditions`,
+    method: 'get'
+  })
+}
+
+// ==================== 模板管理 ====================
+
+/**
+ * 查询模板列表
+ * @param {Object} query - 查询参数
+ */
+export function listTemplates(query) {
+  return request({
+    url: '/ems/report/custom/template/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 获取模板详情
+ * @param {String} templateCode - 模板编码
+ */
+export function getTemplate(templateCode) {
+  return request({
+    url: `/ems/report/custom/template/${templateCode}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 保存模板
+ * @param {Object} data - 模板配置
+ */
+export function saveTemplate(data) {
+  return request({
+    url: '/ems/report/custom/template',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 更新模板
+ * @param {Object} data - 模板配置
+ */
+export function updateTemplate(data) {
+  return request({
+    url: '/ems/report/custom/template',
+    method: 'put',
+    data
+  })
+}
+
+/**
+ * 删除模板
+ * @param {String} templateCode - 模板编码
+ */
+export function deleteTemplate(templateCode) {
+  return request({
+    url: `/ems/report/custom/template/${templateCode}`,
+    method: 'delete'
+  })
+}
+
+// ==================== 动态查询执行 ====================
+
+/**
+ * 执行报表查询
+ * @param {Object} queryConfig - 查询配置
+ *
+ * queryConfig结构:
+ * {
+ *   dsCode: '数据源编码',
+ *   templateCode: '模板编码(可选)',
+ *   selectedFields: ['field1', 'field2'], // 选中的字段
+ *   conditions: [
+ *     { field: 'fieldCode', operator: 'eq', value: 'xxx' },
+ *     { field: 'fieldCode', operator: 'between', value: 'start', value2: 'end' }
+ *   ],
+ *   groupBy: ['field1', 'field2'], // 分组字段(可选)
+ *   orderBy: [
+ *     { field: 'fieldCode', direction: 'DESC' }
+ *   ],
+ *   aggregateFields: {
+ *     'fieldCode': 'SUM' // 聚合函数
+ *   },
+ *   pageNum: 1,
+ *   pageSize: 20
+ * }
+ */
+export function executeQuery(queryConfig) {
+  return request({
+    url: '/ems/report/custom/query',
+    method: 'post',
+    data: queryConfig
+  })
+}
+
+/**
+ * 基于模板执行查询
+ * @param {String} templateCode - 模板编码
+ * @param {Object} params - 参数值(用于模板变量替换)
+ */
+export function executeQueryByTemplate(templateCode, params) {
+  return request({
+    url: `/ems/report/custom/query/template/${templateCode}`,
+    method: 'post',
+    data: params || {}
+  })
+}
+
+/**
+ * 获取报表汇总数据
+ * @param {Object} queryConfig - 查询配置
+ */
+export function getSummary(queryConfig) {
+  return request({
+    url: '/ems/report/custom/summary',
+    method: 'post',
+    data: queryConfig
+  })
+}
+
+/**
+ * 导出报表数据
+ * @param {Object} queryConfig - 查询配置
+ */
+export function exportReport(queryConfig) {
+  return request({
+    url: '/ems/report/custom/export',
+    method: 'post',
+    data: queryConfig,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 工具方法 ====================
+
+/**
+ * 获取操作符显示名称
+ * @param {String} operator - 操作符
+ */
+export function getOperatorLabel(operator) {
+  const operatorMap = {
+    'eq': '等于 =',
+    'ne': '不等于 ≠',
+    'gt': '大于 >',
+    'gte': '大于等于 ≥',
+    'lt': '小于 <',
+    'lte': '小于等于 ≤',
+    'like': '包含',
+    'notLike': '不包含',
+    'in': '在列表中',
+    'notIn': '不在列表中',
+    'between': '范围',
+    'isNull': '为空',
+    'isNotNull': '不为空'
+  }
+  return operatorMap[operator] || operator
+}
+
+/**
+ * 获取字段类型显示名称
+ * @param {String} type - 字段类型
+ */
+export function getFieldTypeLabel(type) {
+  const typeMap = {
+    'string': '文本',
+    'number': '数值',
+    'date': '日期',
+    'datetime': '日期时间',
+    'time': '时间'
+  }
+  return typeMap[type] || type
+}
+
+/**
+ * 获取聚合函数显示名称
+ * @param {String} func - 聚合函数
+ */
+export function getAggregateFuncLabel(func) {
+  const funcMap = {
+    'SUM': '求和',
+    'AVG': '平均值',
+    'MAX': '最大值',
+    'MIN': '最小值',
+    'COUNT': '计数'
+  }
+  return funcMap[func] || func
+}
+
+/**
+ * 格式化数值
+ * @param {Number} value - 数值
+ * @param {Number} decimals - 小数位数
+ * @param {String} unit - 单位
+ */
+export function formatNumber(value, decimals = 2, unit = '') {
+  if (value === null || value === undefined) return '0.00'
+  const num = parseFloat(value).toFixed(decimals)
+  return unit ? `${num} ${unit}` : num
+}
+
+/**
+ * 构建默认查询配置
+ * @param {String} dsCode - 数据源编码
+ * @param {Array} fields - 字段列表
+ */
+export function buildDefaultQueryConfig(dsCode, fields) {
+  const defaultFields = fields
+    .filter(f => f.isDefault === 1)
+    .map(f => f.fieldCode)
+
+  return {
+    dsCode,
+    selectedFields: defaultFields.length > 0 ? defaultFields : fields.slice(0, 5).map(f => f.fieldCode),
+    conditions: [],
+    groupBy: null,
+    orderBy: [],
+    aggregateFields: null,
+    distinct: false,
+    pageNum: 1,
+    pageSize: 20
+  }
+}
+
+/**
+ * 下载导出文件
+ * @param {Blob} blob - 文件Blob
+ * @param {String} fileName - 文件名
+ */
+export function downloadFile(blob, fileName) {
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.download = fileName || '报表导出.xlsx'
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
+
+export default {
+  listDatasources,
+  getDatasourceDetail,
+  getDatasourceFields,
+  getFieldConditions,
+  listTemplates,
+  getTemplate,
+  saveTemplate,
+  updateTemplate,
+  deleteTemplate,
+  executeQuery,
+  executeQueryByTemplate,
+  getSummary,
+  exportReport,
+  getOperatorLabel,
+  getFieldTypeLabel,
+  getAggregateFuncLabel,
+  formatNumber,
+  buildDefaultQueryConfig,
+  downloadFile
+}

+ 1896 - 510
ems-ui-cloud/src/views/analysis/report/statement-self.vue

@@ -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>