叶静 1 сар өмнө
parent
commit
0040315997

+ 23 - 31
src/app/shop/admin/data/report/components/add-chart-dialog.vue

@@ -1,12 +1,6 @@
 <template>
 <template>
-  <el-dialog
-    v-model="visible"
-    title="新增图表"
-    width="50%"
-    class="add-chart-dialog"
-    @close="handleClose"
-  >
-    <div class="dialog-content">
+  <el-container>
+    <el-main>
       <el-form :model="formData" :rules="formRules" ref="formRef" label-width="100px">
       <el-form :model="formData" :rules="formRules" ref="formRef" label-width="100px">
         <!-- 基本信息 -->
         <!-- 基本信息 -->
         <div class="section">
         <div class="section">
@@ -206,20 +200,17 @@
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
-    </div>
-
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button @click="handleClose">取消</el-button>
-        <el-button @click="handlePreview" :loading="previewLoading">
-          预览图表
-        </el-button>
-        <el-button type="primary" @click="handleSave" :loading="saving">
-          保存图表
-        </el-button>
-      </div>
-    </template>
-  </el-dialog>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button @click="handleClose">取消</el-button>
+      <el-button @click="handlePreview" :loading="previewLoading">
+        预览图表
+      </el-button>
+      <el-button type="primary" @click="handleSave" :loading="saving">
+        保存图表
+      </el-button>
+    </el-footer>
+  </el-container>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -260,17 +251,13 @@ use([
 ]);
 ]);
 
 
 const props = defineProps({
 const props = defineProps({
-  visible: {
-    type: Boolean,
-    default: false
-  },
   chartData: {
   chartData: {
     type: Object,
     type: Object,
     default: () => ({})
     default: () => ({})
   }
   }
 });
 });
 
 
-const emit = defineEmits(['update:visible', 'save']);
+const emit = defineEmits(['save', 'close']);
 
 
 // 响应式数据
 // 响应式数据
 const formRef = ref();
 const formRef = ref();
@@ -382,6 +369,11 @@ const handleSave = async () => {
   }
   }
 };
 };
 
 
+const handleDialogClose = () => {
+  resetForm();
+  emit('close');
+};
+
 const generatePreview = async () => {
 const generatePreview = async () => {
   // 模拟数据生成
   // 模拟数据生成
   const mockData = generateMockData();
   const mockData = generateMockData();
@@ -469,11 +461,11 @@ const generateMockData = () => {
 };
 };
 
 
 // 监听器
 // 监听器
-watch(() => props.visible, (newVal) => {
-  if (newVal && props.chartData) {
-    Object.assign(formData, props.chartData);
+watch(() => props.chartData, (newVal) => {
+  if (newVal) {
+    Object.assign(formData, newVal);
   }
   }
-});
+}, { immediate: true });
 
 
 watch(() => [formData.type, formData.config.primaryColor, formData.config.backgroundColor], () => {
 watch(() => [formData.type, formData.config.primaryColor, formData.config.backgroundColor], () => {
   if (formData.type) {
   if (formData.type) {

+ 24 - 32
src/app/shop/admin/data/report/components/add-dashboard-dialog.vue

@@ -1,12 +1,6 @@
 <template>
 <template>
-  <el-dialog
-    v-model="visible"
-    title="新增看板"
-    width="60%"
-    class="add-dashboard-dialog"
-    @close="handleClose"
-  >
-    <div class="dialog-content">
+  <el-container>
+    <el-main>
       <!-- 看板基本信息 -->
       <!-- 看板基本信息 -->
       <div class="section">
       <div class="section">
         <h4 class="section-title">基本信息</h4>
         <h4 class="section-title">基本信息</h4>
@@ -130,24 +124,21 @@
           </el-form-item>
           </el-form-item>
         </el-form>
         </el-form>
       </div>
       </div>
-    </div>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" @click="handleSave" :loading="saving">
+        保存看板
+      </el-button>
+    </el-footer>
+  </el-container>
 
 
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button @click="handleClose">取消</el-button>
-        <el-button type="primary" @click="handleSave" :loading="saving">
-          保存看板
-        </el-button>
-      </div>
-    </template>
-
-    <!-- 图表编辑对话框 -->
-    <AddChartDialog 
-      v-model:visible="chartDialogVisible"
-      :chart-data="currentChart"
-      @save="handleChartSave"
-    />
-  </el-dialog>
+  <!-- 图表编辑对话框 -->
+  <AddChartDialog 
+    v-model:visible="chartDialogVisible"
+    :chart-data="currentChart"
+    @save="handleChartSave"
+  />
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -164,14 +155,9 @@ import {
 } from '@element-plus/icons-vue';
 } from '@element-plus/icons-vue';
 import AddChartDialog from './add-chart-dialog.vue';
 import AddChartDialog from './add-chart-dialog.vue';
 
 
-const props = defineProps({
-  visible: {
-    type: Boolean,
-    default: false
-  }
-});
+const props = defineProps({});
 
 
-const emit = defineEmits(['update:visible', 'save']);
+const emit = defineEmits(['save', 'close']);
 
 
 // 响应式数据
 // 响应式数据
 const formRef = ref();
 const formRef = ref();
@@ -311,6 +297,12 @@ const handleSave = async () => {
     saving.value = false;
     saving.value = false;
   }
   }
 };
 };
+
+const handleChartDialogClose = () => {
+  resetForm();
+  chartDialogVisible.value = false;
+  emit('close');
+};
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 104 - 0
src/app/shop/admin/data/report/components/add-report-dialog.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form :model="formData" :rules="formRules" ref="formRef" label-width="80px">
+        <el-form-item label="报表名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入报表名称" />
+        </el-form-item>
+        <el-form-item label="报表描述" prop="description">
+          <el-input v-model="formData.description" type="textarea" placeholder="请输入报表描述" />
+        </el-form-item>
+        <el-form-item label="报表类型" prop="type">
+          <el-select v-model="formData.type" placeholder="请选择报表类型">
+            <el-option label="销售报表" value="sales" />
+            <el-option label="用户报表" value="user" />
+            <el-option label="订单报表" value="order" />
+            <el-option label="财务报表" value="finance" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="type === 'add'" type="primary" @click="handleSave" :loading="saving">
+        保存
+      </el-button>
+      <el-button v-if="type === 'edit'" type="primary" @click="handleSave" :loading="saving">
+        更新
+      </el-button>
+    </el-footer>
+  </el-container>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: 'add'
+  },
+  reportData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['save'])
+
+const formRef = ref()
+const saving = ref(false)
+
+const formData = reactive({
+  name: props.reportData?.name || '',
+  description: props.reportData?.description || '',
+  type: props.reportData?.type || ''
+})
+
+const formRules = {
+  name: [
+    { required: true, message: '请输入报表名称', trigger: 'blur' }
+  ],
+  type: [
+    { required: true, message: '请选择报表类型', trigger: 'change' }
+  ]
+}
+
+// 保存报表
+const handleSave = async () => {
+  if (!formRef.value) return
+  
+  try {
+    await formRef.value.validate()
+    saving.value = true
+    
+    // 模拟保存
+    await new Promise(resolve => setTimeout(resolve, 1000))
+    
+    emit('save', { ...formData })
+    ElMessage.success(props.type === 'add' ? '报表创建成功!' : '报表更新成功!')
+  } catch (error) {
+    console.error('保存失败:', error)
+  } finally {
+    saving.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields()
+  }
+}
+
+// 暴露方法给 useModal
+defineExpose({
+  handleSave,
+  resetForm,
+  saving
+})
+</script>
+
+<style scoped>
+/* 使用 el-container 布局,不需要额外的 padding */
+</style>

+ 76 - 154
src/app/shop/admin/data/report/components/dashboard-tabs.vue

@@ -12,7 +12,7 @@
         >
         >
         </el-tab-pane>
         </el-tab-pane>
         <template #addIcon>
         <template #addIcon>
-          <el-button type="primary" size="small" @click="showAddDialog = true">
+          <el-button type="primary" size="small" @click="addReport">
             <el-icon><Plus /></el-icon>
             <el-icon><Plus /></el-icon>
             新增报表
             新增报表
           </el-button>
           </el-button>
@@ -134,59 +134,15 @@
       </div>
       </div>
     </div>
     </div>
 
 
-    <!-- 新增报表对话框 -->
-    <el-dialog v-model="showAddDialog" title="新增报表" width="500px" class="report-dialog sa-dialog">
-      <el-form :model="newReportForm" :rules="newReportRules" ref="newReportFormRef" label-width="80px">
-        <el-form-item label="报表名称" prop="name">
-          <el-input v-model="newReportForm.name" placeholder="请输入报表名称" />
-        </el-form-item>
-        <el-form-item label="报表描述" prop="description">
-          <el-input v-model="newReportForm.description" type="textarea" placeholder="请输入报表描述" />
-        </el-form-item>
-        <el-form-item label="报表类型" prop="type">
-          <el-select v-model="newReportForm.type" placeholder="请选择报表类型">
-            <el-option label="销售报表" value="sales" />
-            <el-option label="用户报表" value="user" />
-            <el-option label="订单报表" value="order" />
-            <el-option label="财务报表" value="finance" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="showAddDialog = false">取消</el-button>
-        <el-button type="primary" @click="addReport" :loading="addingReport">确认</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 编辑报表对话框 -->
-    <el-dialog v-model="showEditDialog" title="编辑报表" width="500px">
-      <el-form :model="editReportForm" :rules="newReportRules" ref="editReportFormRef" label-width="80px">
-        <el-form-item label="报表名称" prop="name">
-          <el-input v-model="editReportForm.name" placeholder="请输入报表名称" />
-        </el-form-item>
-        <el-form-item label="报表描述" prop="description">
-          <el-input v-model="editReportForm.description" type="textarea" placeholder="请输入报表描述" />
-        </el-form-item>
-        <el-form-item label="报表类型" prop="type">
-          <el-select v-model="editReportForm.type" placeholder="请选择报表类型">
-            <el-option label="销售报表" value="sales" />
-            <el-option label="用户报表" value="user" />
-            <el-option label="订单报表" value="order" />
-            <el-option label="财务报表" value="finance" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="showEditDialog = false">取消</el-button>
-        <el-button type="primary" @click="updateReport" :loading="updatingReport">确认</el-button>
-      </template>
-    </el-dialog>
+    <!-- 对话框已改为 useModal 调用 -->
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { ref, reactive, computed, onMounted, nextTick } from 'vue'
 import { ref, reactive, computed, onMounted, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { useModal } from '@/sheep/hooks'
+import AddReportDialog from './add-report-dialog.vue'
 import {
 import {
   Plus,
   Plus,
   Refresh,
   Refresh,
@@ -207,12 +163,6 @@ import { api } from '../../data.service.js'
 
 
 // 响应式数据
 // 响应式数据
 const activeTab = ref('1')
 const activeTab = ref('1')
-const showAddDialog = ref(false)
-const showEditDialog = ref(false)
-const addingReport = ref(false)
-const updatingReport = ref(false)
-const newReportFormRef = ref()
-const editReportFormRef = ref()
 
 
 // 标签页数据
 // 标签页数据
 const tabs = ref([
 const tabs = ref([
@@ -268,25 +218,7 @@ const tabs = ref([
   }
   }
 ])
 ])
 
 
-// 表单数据
-const newReportForm = reactive({
-  name: '',
-  description: '',
-  type: ''
-})
-
-const editReportForm = reactive({
-  id: '',
-  name: '',
-  description: '',
-  type: ''
-})
 
 
-// 表单验证规则
-const newReportRules = {
-  name: [{ required: true, message: '请输入报表名称', trigger: 'blur' }],
-  type: [{ required: true, message: '请选择报表类型', trigger: 'change' }]
-}
 
 
 // 方法
 // 方法
 const handleTabChange = (tabName) => {
 const handleTabChange = (tabName) => {
@@ -308,13 +240,7 @@ const refreshReport = async (tabId) => {
   }
   }
 }
 }
 
 
-const editReport = (tab) => {
-  editReportForm.id = tab.id
-  editReportForm.name = tab.name
-  editReportForm.description = tab.description
-  editReportForm.type = tab.type
-  showEditDialog.value = true
-}
+
 
 
 const deleteReport = async (tabId) => {
 const deleteReport = async (tabId) => {
   if (tabs.value.length <= 1) {
   if (tabs.value.length <= 1) {
@@ -356,82 +282,9 @@ const exportReport = async (tabId) => {
   }
   }
 }
 }
 
 
-const addReport = async () => {
-  try {
-    await newReportFormRef.value.validate()
-    addingReport.value = true
-    
-    // 调用API创建报表
-    const response = await api.operating.spectaculars.add({
-      name: newReportForm.name,
-      description: newReportForm.description,
-      type: newReportForm.type
-    })
-    
-    // 添加到本地数组
-    const newTab = {
-      id: response.data.id || Date.now().toString(),
-      name: newReportForm.name,
-      description: newReportForm.description,
-      type: newReportForm.type,
-      loading: false,
-      chartType: 'line',
-      chartTitle: '数据趋势',
-      searchKeyword: '',
-      stats: [],
-      chartData: [],
-      chartOption: {},
-      tableData: [],
-      tableColumns: []
-    }
-    
-    tabs.value.push(newTab)
-    activeTab.value = newTab.id
-    
-    // 重置表单
-    Object.assign(newReportForm, { name: '', description: '', type: '' })
-    showAddDialog.value = false
-    
-    ElMessage.success('创建成功')
-    
-    // 加载新报表数据
-    await loadReportData(newTab.id)
-  } catch (error) {
-    ElMessage.error('创建失败')
-  } finally {
-    addingReport.value = false
-  }
-}
 
 
-const updateReport = async () => {
-  try {
-    await editReportFormRef.value.validate()
-    updatingReport.value = true
-    
-    // 调用API更新报表
-    await api.operating.spectaculars.update({
-      id: editReportForm.id,
-      name: editReportForm.name,
-      description: editReportForm.description,
-      type: editReportForm.type
-    })
-    
-    // 更新本地数据
-    const tab = tabs.value.find(t => t.id === editReportForm.id)
-    if (tab) {
-      tab.name = editReportForm.name
-      tab.description = editReportForm.description
-      tab.type = editReportForm.type
-    }
-    
-    showEditDialog.value = false
-    ElMessage.success('更新成功')
-  } catch (error) {
-    ElMessage.error('更新失败')
-  } finally {
-    updatingReport.value = false
-  }
-}
+
+
 
 
 const updateChart = (tabId) => {
 const updateChart = (tabId) => {
   const tab = tabs.value.find(t => t.id === tabId)
   const tab = tabs.value.find(t => t.id === tabId)
@@ -635,6 +488,75 @@ const generateChartOption = (tab) => {
   }
   }
 }
 }
 
 
+// 新增报表
+const addReport = () => {
+  useModal(
+    AddReportDialog,
+    { title: '新增报表', type: 'add' },
+    {
+      confirm: (reportData) => {
+        // 添加新标签页
+        const newId = String(tabs.value.length + 1)
+        tabs.value.push({
+          id: newId,
+          name: reportData.name,
+          description: reportData.description,
+          type: reportData.type,
+          loading: false,
+          chartType: 'line',
+          chartTitle: reportData.name + '趋势',
+          searchKeyword: '',
+          stats: [],
+          chartData: [],
+          tableData: [],
+          filteredTableData: []
+        })
+        activeTab.value = newId
+        loadReportData(newId)
+        ElMessage.success('报表创建成功!')
+      },
+    },
+  )
+}
+
+// 编辑报表
+const editReport = (tab) => {
+  useModal(
+    AddReportDialog,
+    { 
+      title: '编辑报表', 
+      type: 'edit',
+      reportData: {
+        name: tab.name,
+        description: tab.description,
+        type: tab.type
+      }
+    },
+    {
+      confirm: (reportData) => {
+        // 更新标签页数据
+        tab.name = reportData.name
+        tab.description = reportData.description
+        tab.type = reportData.type
+        tab.chartTitle = reportData.name + '趋势'
+        loadReportData(tab.id)
+        ElMessage.success('报表更新成功!')
+      },
+    },
+  )
+}
+
+// 暴露方法
+defineExpose({
+  refreshAllTabs: () => {
+    tabs.value.forEach(tab => {
+      loadReportData(tab.id)
+    })
+  },
+  addReport,
+  editReport
+})
+
 // 初始化
 // 初始化
 onMounted(() => {
 onMounted(() => {
   loadReportData(activeTab.value)
   loadReportData(activeTab.value)

+ 5 - 16
src/app/shop/admin/data/report/components/data-display-dialog.vue

@@ -1,12 +1,6 @@
 <template>
 <template>
-  <el-dialog
-    v-model="visible"
-    :title="dialogTitle"
-    width="80%"
-    class="data-display-dialog"
-    @close="handleClose"
-  >
-    <div class="dialog-content">
+  <el-container>
+    <el-main>
       <!-- 数据概览 -->
       <!-- 数据概览 -->
       <div class="data-overview">
       <div class="data-overview">
         <div class="overview-card">
         <div class="overview-card">
@@ -120,8 +114,8 @@
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
-    </div>
-  </el-dialog>
+    </el-main>
+  </el-container>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -166,17 +160,13 @@ use([
 ]);
 ]);
 
 
 const props = defineProps({
 const props = defineProps({
-  visible: {
-    type: Boolean,
-    default: false
-  },
   dataType: {
   dataType: {
     type: String,
     type: String,
     required: true
     required: true
   }
   }
 });
 });
 
 
-const emit = defineEmits(['update:visible', 'close']);
+const emit = defineEmits(['close']);
 
 
 // 响应式数据
 // 响应式数据
 const timeRange = ref('month');
 const timeRange = ref('month');
@@ -244,7 +234,6 @@ const filteredTableData = computed(() => {
 
 
 // 方法
 // 方法
 const handleClose = () => {
 const handleClose = () => {
-  emit('update:visible', false);
   emit('close');
   emit('close');
 };
 };
 
 

+ 188 - 0
src/app/shop/admin/data/report/components/dimension-edit-dialog.vue

@@ -0,0 +1,188 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="维度名称" prop="name">
+              <el-input v-model="formData.name" placeholder="请输入维度名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="维度编码" prop="code">
+              <el-input v-model="formData.code" placeholder="请输入维度编码" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="维度类型" prop="type">
+              <el-select v-model="formData.type" placeholder="请选择维度类型" style="width: 100%;">
+                <el-option label="时间维度" value="time" />
+                <el-option label="地区维度" value="region" />
+                <el-option label="产品维度" value="product" />
+                <el-option label="用户维度" value="user" />
+                <el-option label="渠道维度" value="channel" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="数据类型" prop="dataType">
+              <el-select v-model="formData.dataType" placeholder="请选择数据类型" style="width: 100%;">
+                <el-option label="字符串" value="string" />
+                <el-option label="数字" value="number" />
+                <el-option label="日期" value="date" />
+                <el-option label="布尔值" value="boolean" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="单位" prop="unit">
+              <el-input v-model="formData.unit" placeholder="请输入单位" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态" prop="status">
+              <el-radio-group v-model="formData.status">
+                <el-radio :label="1">启用</el-radio>
+                <el-radio :label="0">禁用</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入维度描述" />
+        </el-form-item>
+
+        <el-form-item label="配置项" prop="config">
+          <el-input v-model="formData.config" type="textarea" :rows="4" placeholder="请输入JSON格式的配置项" />
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="type === 'add'" type="primary" @click="handleSave" :loading="submitting">
+        保存
+      </el-button>
+      <el-button v-if="type === 'edit'" type="primary" @click="handleSave" :loading="submitting">
+        更新
+      </el-button>
+    </el-footer>
+  </el-container>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: 'add'
+  },
+  dimensionData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['save'])
+
+const formRef = ref()
+const submitting = ref(false)
+
+const formData = reactive({
+  name: '',
+  code: '',
+  type: '',
+  dataType: '',
+  unit: '',
+  status: 1,
+  description: '',
+  config: ''
+})
+
+const formRules = {
+  name: [
+    { required: true, message: '请输入维度名称', trigger: 'blur' }
+  ],
+  code: [
+    { required: true, message: '请输入维度编码', trigger: 'blur' }
+  ],
+  type: [
+    { required: true, message: '请选择维度类型', trigger: 'change' }
+  ],
+  dataType: [
+    { required: true, message: '请选择数据类型', trigger: 'change' }
+  ]
+}
+
+// 监听 props 变化,初始化表单数据
+watch(() => props.dimensionData, (newData) => {
+  if (newData && Object.keys(newData).length > 0) {
+    Object.assign(formData, {
+      name: newData.name || '',
+      code: newData.code || '',
+      type: newData.type || '',
+      dataType: newData.dataType || '',
+      unit: newData.unit || '',
+      status: newData.status ?? 1,
+      description: newData.description || '',
+      config: newData.config || ''
+    })
+  }
+}, { immediate: true })
+
+// 保存维度
+const handleSave = async () => {
+  if (!formRef.value) return
+  
+  try {
+    await formRef.value.validate()
+    
+    // 验证配置项格式
+    if (formData.config) {
+      try {
+        JSON.parse(formData.config)
+      } catch (error) {
+        ElMessage.error('配置项格式错误,请输入有效的JSON格式')
+        return
+      }
+    }
+    
+    submitting.value = true
+    
+    // 模拟保存
+    await new Promise(resolve => setTimeout(resolve, 1000))
+    
+    emit('save', { ...formData })
+    ElMessage.success(props.type === 'add' ? '维度创建成功!' : '维度更新成功!')
+  } catch (error) {
+    console.error('保存失败:', error)
+  } finally {
+    submitting.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields()
+  }
+}
+
+// 暴露方法给 useModal
+defineExpose({
+  handleSave,
+  resetForm,
+  submitting
+})
+</script>
+
+<style scoped>
+/* 使用 el-container 布局,不需要额外的 padding */
+</style>

+ 37 - 214
src/app/shop/admin/data/report/components/dimension-management.vue

@@ -129,116 +129,7 @@
       </div>
       </div>
     </el-card>
     </el-card>
 
 
-    <!-- 添加/编辑对话框 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" :close-on-click-modal="false"
-      @close="handleDialogClose" class="report-dialog sa-dialog">
-      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="维度名称" prop="name">
-              <el-input v-model="formData.name" placeholder="请输入维度名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="维度编码" prop="code">
-              <el-input v-model="formData.code" placeholder="请输入维度编码" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="维度类型" prop="type">
-              <el-select v-model="formData.type" placeholder="请选择维度类型" style="width: 100%;">
-                <el-option label="时间维度" value="time" />
-                <el-option label="地区维度" value="region" />
-                <el-option label="产品维度" value="product" />
-                <el-option label="用户维度" value="user" />
-                <el-option label="渠道维度" value="channel" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="数据类型" prop="dataType">
-              <el-select v-model="formData.dataType" placeholder="请选择数据类型" style="width: 100%;">
-                <el-option label="字符串" value="string" />
-                <el-option label="数字" value="number" />
-                <el-option label="日期" value="date" />
-                <el-option label="布尔值" value="boolean" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
-          <el-col :span="12">
-            <el-form-item label="单位" prop="unit">
-              <el-input v-model="formData.unit" placeholder="请输入单位" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态" prop="status">
-              <el-radio-group v-model="formData.status">
-                <el-radio :label="1">启用</el-radio>
-                <el-radio :label="0">禁用</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
-        </el-row>
-
-        <el-form-item label="描述" prop="description">
-          <el-input v-model="formData.description" type="textarea" :rows="3"
-            placeholder="请输入描述" />
-        </el-form-item>
-
-        <el-form-item label="配置项" prop="config">
-          <el-input v-model="formData.config" type="textarea" :rows="4" placeholder="请输入配置项(JSON格式)" />
-        </el-form-item>
-      </el-form>
-
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="handleSubmit" :loading="submitting">
-            {{ isEdit ? '更新' : '创建' }}
-          </el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 查看详情对话框 -->
-    <el-dialog v-model="viewDialogVisible" title="维度详情" width="500px">
-      <el-descriptions :column="2" border>
-        <el-descriptions-item label="维度名称">{{ viewData.name }}</el-descriptions-item>
-        <el-descriptions-item label="维度编码">{{ viewData.code }}</el-descriptions-item>
-        <el-descriptions-item label="维度类型">
-          <el-tag :type="getDimensionTypeTag(viewData.type)" size="small">
-            {{ getDimensionTypeLabel(viewData.type) }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="数据类型">
-          <el-tag :type="getDataTypeTag(viewData.dataType)" size="small">
-            {{ getDataTypeLabel(viewData.dataType) }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="单位">{{ viewData.unit || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="状态">
-          <el-tag :type="viewData.status === 1 ? 'success' : 'danger'" size="small">
-            {{ viewData.status === 1 ? '启用' : '禁用' }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="创建时间" :span="2">
-          {{ formatDate(viewData.createTime) }}
-        </el-descriptions-item>
-        <el-descriptions-item label="描述" :span="2">
-          {{ viewData.description || '-' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="配置项" :span="2">
-          <pre v-if="viewData.config" class="config-display">{{ formatConfig(viewData.config) }}</pre>
-          <span v-else>-</span>
-        </el-descriptions-item>
-      </el-descriptions>
-    </el-dialog>
+    <!-- 对话框现在通过 useModal 处理 -->
   </div>
   </div>
 </template>
 </template>
 
 
@@ -250,14 +141,12 @@ import {
   Check, Close
   Check, Close
 } from '@element-plus/icons-vue'
 } from '@element-plus/icons-vue'
 import { api } from '../../data.service.js'
 import { api } from '../../data.service.js'
+import { useModal } from '@/composables/useModal'
+import DimensionEditDialog from './dimension-edit-dialog.vue'
+import DimensionViewDialog from './dimension-view-dialog.vue'
 
 
 // 响应式数据
 // 响应式数据
 const loading = ref(false)
 const loading = ref(false)
-const submitting = ref(false)
-const dialogVisible = ref(false)
-const viewDialogVisible = ref(false)
-const isEdit = ref(false)
-const formRef = ref()
 const selectedRows = ref([])
 const selectedRows = ref([])
 
 
 // 搜索表单
 // 搜索表单
@@ -275,98 +164,9 @@ const pagination = reactive({
 // 表格数据
 // 表格数据
 const tableData = ref([])
 const tableData = ref([])
 
 
-// 表单数据
-const formData = reactive({
-  id: null,
-  name: '',
-  code: '',
-  type: '',
-  dataType: '',
-  unit: '',
-  description: '',
-  config: '',
-  status: 1
-})
-
-// 查看数据
-const viewData = reactive({})
-
-// 表单验证规则
-const formRules = computed(() => ({
-  name: [
-    { required: true, message: '请输入维度名称', trigger: 'blur' },
-    { min: 2, max: 50, message: '维度名称长度应在2-50个字符之间', trigger: 'blur' }
-  ],
-  code: [
-    { required: true, message: '请输入维度编码', trigger: 'blur' },
-    { pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/, message: '维度编码格式不正确,应以字母开头,只能包含字母、数字和下划线', trigger: 'blur' }
-  ],
-  type: [
-    { required: true, message: '请选择维度类型', trigger: 'change' }
-  ],
-  dataType: [
-    { required: true, message: '请选择数据类型', trigger: 'change' }
-  ]
-}))
-
-// 计算属性
-const dialogTitle = computed(() => isEdit.value ? '编辑维度' : '新增维度')
-
-// 工具方法
-const getDimensionTypeLabel = (type) => {
-  const typeMap = {
-    time: '时间维度',
-    region: '地区维度',
-    product: '产品维度',
-    user: '用户维度',
-    channel: '渠道维度'
-  }
-  return typeMap[type] || type
-}
+// 不再需要 formData、viewData 和相关验证规则,这些已移至独立组件中
 
 
-const getDimensionTypeTag = (type) => {
-  const tagMap = {
-    time: 'primary',
-    region: 'success',
-    product: 'warning',
-    user: 'info',
-    channel: 'danger'
-  }
-  return tagMap[type] || ''
-}
-
-const getDataTypeLabel = (dataType) => {
-  const typeMap = {
-    string: '字符串',
-    number: '数字',
-    date: '日期',
-    boolean: '布尔值'
-  }
-  return typeMap[dataType] || dataType
-}
-
-const getDataTypeTag = (dataType) => {
-  const tagMap = {
-    string: 'primary',
-    number: 'success',
-    date: 'warning',
-    boolean: 'info'
-  }
-  return tagMap[dataType] || ''
-}
-
-const formatDate = (dateStr) => {
-  if (!dateStr) return '-'
-  return new Date(dateStr).toLocaleString('zh-CN')
-}
-
-const formatConfig = (config) => {
-  try {
-    return JSON.stringify(JSON.parse(config), null, 2)
-  } catch {
-    return config
-  }
-}
+// 工具方法已移至独立组件中
 
 
 // 数据加载
 // 数据加载
 const loadData = async () => {
 const loadData = async () => {
@@ -422,20 +222,43 @@ const generateMockData = () => {
 
 
 // 事件处理
 // 事件处理
 const handleAdd = () => {
 const handleAdd = () => {
-  isEdit.value = false
-  resetForm()
-  dialogVisible.value = true
+  useModal(DimensionEditDialog, {
+    title: '新增维度',
+    width: '600px',
+    props: {
+      type: 'add',
+      dimensionData: {}
+    },
+    confirm: async (modalRef) => {
+      await modalRef.handleSave()
+      loadData()
+    }
+  })
 }
 }
 
 
 const handleEdit = (row) => {
 const handleEdit = (row) => {
-  isEdit.value = true
-  Object.assign(formData, row)
-  dialogVisible.value = true
+  useModal(DimensionEditDialog, {
+    title: '编辑维度',
+    width: '600px',
+    props: {
+      type: 'edit',
+      dimensionData: row
+    },
+    confirm: async (modalRef) => {
+      await modalRef.handleSave()
+      loadData()
+    }
+  })
 }
 }
 
 
 const handleView = (row) => {
 const handleView = (row) => {
-  Object.assign(viewData, row)
-  viewDialogVisible.value = true
+  useModal(DimensionViewDialog, {
+    title: '维度详情',
+    width: '500px',
+    props: {
+      dimensionData: row
+    }
+  })
 }
 }
 
 
 const handleDelete = async (row) => {
 const handleDelete = async (row) => {

+ 128 - 0
src/app/shop/admin/data/report/components/dimension-view-dialog.vue

@@ -0,0 +1,128 @@
+<template>
+  <el-container>
+    <el-main>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="维度名称">{{ viewData.name }}</el-descriptions-item>
+        <el-descriptions-item label="维度编码">{{ viewData.code }}</el-descriptions-item>
+        <el-descriptions-item label="维度类型">
+          <el-tag :type="getDimensionTypeTag(viewData.type)" size="small">
+            {{ getDimensionTypeLabel(viewData.type) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="数据类型">
+          <el-tag :type="getDataTypeTag(viewData.dataType)" size="small">
+            {{ getDataTypeLabel(viewData.dataType) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="单位">{{ viewData.unit || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="viewData.status === 1 ? 'success' : 'danger'" size="small">
+            {{ viewData.status === 1 ? '启用' : '禁用' }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间" :span="2">
+          {{ formatDate(viewData.createTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="描述" :span="2">
+          {{ viewData.description || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="配置项" :span="2">
+          <pre v-if="viewData.config" class="config-display">{{ formatConfig(viewData.config) }}</pre>
+          <span v-else>-</span>
+        </el-descriptions-item>
+      </el-descriptions>
+    </el-main>
+  </el-container>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  dimensionData: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const viewData = computed(() => props.dimensionData || {})
+
+// 获取维度类型标签
+const getDimensionTypeTag = (type) => {
+  const typeMap = {
+    time: 'primary',
+    region: 'success',
+    product: 'warning',
+    user: 'info',
+    channel: 'danger'
+  }
+  return typeMap[type] || 'info'
+}
+
+// 获取维度类型标签文本
+const getDimensionTypeLabel = (type) => {
+  const typeMap = {
+    time: '时间维度',
+    region: '地区维度',
+    product: '产品维度',
+    user: '用户维度',
+    channel: '渠道维度'
+  }
+  return typeMap[type] || type
+}
+
+// 获取数据类型标签
+const getDataTypeTag = (dataType) => {
+  const typeMap = {
+    string: 'primary',
+    number: 'success',
+    date: 'warning',
+    boolean: 'info'
+  }
+  return typeMap[dataType] || 'info'
+}
+
+// 获取数据类型标签文本
+const getDataTypeLabel = (dataType) => {
+  const typeMap = {
+    string: '字符串',
+    number: '数字',
+    date: '日期',
+    boolean: '布尔值'
+  }
+  return typeMap[dataType] || dataType
+}
+
+// 格式化日期
+const formatDate = (date) => {
+  if (!date) return '-'
+  return new Date(date).toLocaleString('zh-CN')
+}
+
+// 格式化配置项
+const formatConfig = (config) => {
+  if (!config) return ''
+  try {
+    return JSON.stringify(JSON.parse(config), null, 2)
+  } catch (error) {
+    return config
+  }
+}
+</script>
+
+<style scoped>
+/* 使用 el-container 布局,不需要额外的 padding */
+
+.config-display {
+  background-color: #f5f7fa;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  padding: 12px;
+  margin: 0;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #606266;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+</style>

+ 16 - 29
src/app/shop/admin/data/report/components/report-detail-dialog.vue

@@ -1,21 +1,15 @@
 <template>
 <template>
-  <el-dialog
-    v-model="visible"
-    :title="reportData.name || '报表详情'"
-    width="90%"
-    class="report-detail-dialog"
-    @close="handleClose"
-  >
-    <div class="dialog-content">
+  <el-container>
+    <el-main>
       <!-- 报表头部信息 -->
       <!-- 报表头部信息 -->
       <div class="report-header">
       <div class="report-header">
         <div class="header-left">
         <div class="header-left">
           <div class="report-title">
           <div class="report-title">
-            <h3>{{ reportData.name || '未命名报表' }}</h3>
+            <h3>{{ reportData?.name || '未命名报表' }}</h3>
             <div class="report-meta">
             <div class="report-meta">
-              <el-tag :type="getStatusType(reportData.status)">{{ getStatusText(reportData.status) }}</el-tag>
-              <span class="meta-item">创建时间:{{ formatDate(reportData.createTime) }}</span>
-              <span class="meta-item">更新时间:{{ formatDate(reportData.updateTime) }}</span>
+              <el-tag :type="getStatusType(reportData?.status)">{{ getStatusText(reportData?.status) }}</el-tag>
+              <span class="meta-item">创建时间:{{ formatDate(reportData?.createTime) }}</span>
+              <span class="meta-item">更新时间:{{ formatDate(reportData?.updateTime) }}</span>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -226,17 +220,14 @@
           </div>
           </div>
         </el-card>
         </el-card>
       </div>
       </div>
-    </div>
-
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button @click="handleClose">关闭</el-button>
-        <el-button type="primary" @click="saveReport">
-          保存报表
-        </el-button>
-      </div>
-    </template>
-  </el-dialog>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button @click="handleClose">关闭</el-button>
+      <el-button type="primary" @click="saveReport">
+        保存报表
+      </el-button>
+    </el-footer>
+  </el-container>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -290,17 +281,13 @@ use([
 ]);
 ]);
 
 
 const props = defineProps({
 const props = defineProps({
-  visible: {
-    type: Boolean,
-    default: false
-  },
   reportData: {
   reportData: {
     type: Object,
     type: Object,
     default: () => ({})
     default: () => ({})
   }
   }
 });
 });
 
 
-const emit = defineEmits(['update:visible', 'save']);
+const emit = defineEmits(['save', 'close']);
 
 
 // 响应式数据
 // 响应式数据
 const refreshing = ref(false);
 const refreshing = ref(false);
@@ -362,7 +349,7 @@ const chartOption = ref({});
 
 
 // 方法
 // 方法
 const handleClose = () => {
 const handleClose = () => {
-  emit('update:visible', false);
+  emit('close');
 };
 };
 
 
 const refreshReport = async () => {
 const refreshReport = async () => {

+ 12 - 13
src/app/shop/admin/data/report/index.vue

@@ -157,16 +157,7 @@
       </div>
       </div>
     </div>
     </div>
 
 
-    <!-- 维度管理对话框 -->
-    <el-dialog
-      v-model="dimensionDialogVisible"
-      title="维度管理"
-      width="90%"
-      :close-on-click-modal="false"
-      top="5vh"
-    >
-      <DimensionManagement />
-    </el-dialog>
+    <!-- 维度管理对话框已改为 useModal 调用 -->
     
     
     <!-- 数据展示对话框 -->
     <!-- 数据展示对话框 -->
     <DataDisplayDialog 
     <DataDisplayDialog 
@@ -194,6 +185,7 @@ import { onMounted, reactive, ref } from 'vue';
 import { api } from '../data.service';
 import { api } from '../data.service';
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import { Bell, Setting, ArrowRight } from '@element-plus/icons-vue';
 import { Bell, Setting, ArrowRight } from '@element-plus/icons-vue';
+import { useModal } from '@/sheep/hooks';
 import DashboardTabs from './components/dashboard-tabs.vue';
 import DashboardTabs from './components/dashboard-tabs.vue';
 import DimensionManagement from './components/dimension-management.vue';
 import DimensionManagement from './components/dimension-management.vue';
 import DataDisplayDialog from './components/data-display-dialog.vue';
 import DataDisplayDialog from './components/data-display-dialog.vue';
@@ -210,8 +202,7 @@ const components = {
 
 
 // 当前活动标签
 // 当前活动标签
 const activeTab = ref('overview');
 const activeTab = ref('overview');
-// 维度管理对话框
-const dimensionDialogVisible = ref(false);
+// 维度管理对话框已改为 useModal
 // 数据展示对话框
 // 数据展示对话框
 const dataDisplayVisible = ref(false);
 const dataDisplayVisible = ref(false);
 const currentDataType = ref('');
 const currentDataType = ref('');
@@ -294,7 +285,15 @@ const refreshData = () => {
 
 
 // 显示维度管理
 // 显示维度管理
 const showDimensionManagement = () => {
 const showDimensionManagement = () => {
-  dimensionDialogVisible.value = true;
+  useModal(
+    DimensionManagement,
+    { title: '维度管理', width: '90%' },
+    {
+      confirm: () => {
+        refreshData();
+      },
+    },
+  );
 };
 };
 
 
 onMounted(() => {
 onMounted(() => {

+ 132 - 0
src/composables/useModal.js

@@ -0,0 +1,132 @@
+import { ref, reactive } from 'vue'
+import { ElDialog } from 'element-plus'
+import { createVNode, render } from 'vue'
+
+/**
+ * useModal - 模态框管理 Hook
+ * 用于替代 el-dialog,提供更灵活的模态框管理
+ */
+export function useModal() {
+  const visible = ref(false)
+  const loading = ref(false)
+  
+  const open = () => {
+    visible.value = true
+  }
+  
+  const close = () => {
+    visible.value = false
+  }
+  
+  const toggle = () => {
+    visible.value = !visible.value
+  }
+  
+  const setLoading = (state) => {
+    loading.value = state
+  }
+  
+  return {
+    visible,
+    loading,
+    open,
+    close,
+    toggle,
+    setLoading
+  }
+}
+
+/**
+ * 创建模态框实例
+ * @param {Object} component - Vue 组件
+ * @param {Object} props - 组件属性
+ * @param {Object} options - 模态框选项
+ */
+export function createModal(component, props = {}, options = {}) {
+  const {
+    title = '',
+    width = '50%',
+    destroyOnClose = true,
+    closeOnClickModal = true,
+    closeOnPressEscape = true,
+    showClose = true,
+    center = false,
+    modal = true,
+    lockScroll = true,
+    customClass = '',
+    zIndex = 2000,
+    appendToBody = true
+  } = options
+  
+  const modalState = reactive({
+    visible: false,
+    loading: false
+  })
+  
+  const modalProps = {
+    modelValue: modalState.visible,
+    title,
+    width,
+    destroyOnClose,
+    closeOnClickModal,
+    closeOnPressEscape,
+    showClose,
+    center,
+    modal,
+    lockScroll,
+    customClass,
+    zIndex,
+    appendToBody,
+    'onUpdate:modelValue': (value) => {
+      modalState.visible = value
+    },
+    onClose: () => {
+      modalState.visible = false
+      if (options.onClose) {
+        options.onClose()
+      }
+    }
+  }
+  
+  const componentProps = {
+    ...props,
+    onSave: (data) => {
+      if (options.onSave) {
+        options.onSave(data)
+      }
+      modalState.visible = false
+    },
+    onCancel: () => {
+      if (options.onCancel) {
+        options.onCancel()
+      }
+      modalState.visible = false
+    },
+    onClose: () => {
+      modalState.visible = false
+    }
+  }
+  
+  const open = () => {
+    modalState.visible = true
+  }
+  
+  const close = () => {
+    modalState.visible = false
+  }
+  
+  const setLoading = (state) => {
+    modalState.loading = state
+  }
+  
+  return {
+    modalState,
+    modalProps,
+    componentProps,
+    open,
+    close,
+    setLoading
+  }
+}
+
+export default useModal