瀏覽代碼

feat(points): 将积分管理组件迁移至后端模块

runphp 2 月之前
父節點
當前提交
7782a36e01
共有 2 個文件被更改,包括 886 次插入0 次删除
  1. 809 0
      resource/admin/PointsLog.vue
  2. 77 0
      resource/admin/index.vue

+ 809 - 0
resource/admin/PointsLog.vue

@@ -0,0 +1,809 @@
+<template>
+  <div class="points-management">
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>积分明细管理</span>
+          <div class="header-actions">
+<!--            <el-button type="primary" @click="showAdjustDialog">手动调整积分</el-button>
+            <el-button @click="exportData">导出数据</el-button>-->
+          </div>
+        </div>
+      </template>
+
+      <!-- 搜索栏 -->
+      <div class="search-bar">
+        <el-form :model="searchForm" inline>
+          <el-form-item label="用户">
+            <el-input
+              v-model="selectedUserDisplay"
+              placeholder="点击选择用户"
+              clearable
+              style="width: 150px;"
+              @click="showUserDialog"
+              @clear="handleUserClear"
+            >
+<!--              <template #suffix>
+                <el-icon style="cursor: pointer;">
+                  <Search />
+                </el-icon>
+              </template>-->
+            </el-input>
+          </el-form-item>
+          <el-form-item label="积分类型">
+            <el-select
+              v-model="searchForm.type"
+              placeholder="请选择类型"
+              clearable
+              style="width: 150px;"
+            >
+              <el-option
+                v-for="item in pointsTypes"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>
+<!--          <el-form-item label="业务类型">
+            <el-select
+              v-model="searchForm.biz_type"
+              placeholder="请选择业务类型"
+              clearable
+              style="width: 150px;"
+            >
+              <el-option
+                v-for="item in bizTypes"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </el-form-item>-->
+          <el-form-item label="时间范围">
+            <el-date-picker
+              v-model="searchForm.dateRange"
+              type="datetimerange"
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              style="width: 300px;"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
+            <el-button @click="handleReset">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 统计卡片 -->
+      <div class="stats-cards">
+        <el-row :gutter="20" style="margin-bottom: 20px;">
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-item">
+                <div class="stats-label">积分发放</div>
+                <div class="stats-value increase">+{{ statsData.positive_total || 0 }}</div>
+              </div>
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card class="stats-card">
+              <div class="stats-item">
+                <div class="stats-label">积分消耗</div>
+                <div class="stats-value decrease">{{ statsData.negative_total || 0 }}</div>
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </div>
+
+      <!-- 积分记录表格 -->
+      <el-table
+        :data="pointsList"
+        border
+        style="width: 100%"
+        v-loading="loading"
+        row-key="id"
+      >
+        <el-table-column prop="id" label="记录ID" width="80" />
+        <el-table-column prop="user_id" label="用户ID" width="100" />
+        <el-table-column prop="user.nickname" label="用户名" width="100" />
+        <el-table-column label="积分变动" width="150">
+          <template #default="{ row }">
+            <span
+              :class="{
+                'point-increase': row.point > 0,
+                'point-decrease': row.point < 0,
+                'point-freeze': row.freeze_point !== 0
+              }"
+            >
+              {{ row.point > 0 ? '+' : '' }}{{ row.point }}
+              <span v-if="row.freeze_point !== 0" class="freeze-point">
+                (冻结: {{ row.freeze_point > 0 ? '+' : '' }}{{ row.freeze_point }})
+              </span>
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="变动后余额" width="150">
+          <template #default="{ row }">
+            <div>
+              <div>积分: {{ row.after_point }}</div>
+              <div v-if="row.after_freeze_point !== 0" class="freeze-balance">
+                冻结: {{ row.after_freeze_point }}
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="type" label="变动类型" width="120">
+          <template #default="{ row }">
+            <el-tag
+              :type="getTypeTagType(row.type)"
+              size="small"
+            >
+              {{ getTypeName(row.type) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+<!--        <el-table-column prop="biz_type" label="业务类型" width="120">
+          <template #default="{ row }">
+            <el-tag
+              type="info"
+              size="small"
+            >
+              {{ getBizTypeName(row.biz_type) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="biz_id" label="业务ID" width="100" />-->
+        <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="create_time" label="创建时间" width="180" />
+<!--        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              type="text"
+              size="small"
+              @click="showDetailDialog(row)"
+            >
+              查看详情
+            </el-button>
+          </template>
+        </el-table-column>-->
+      </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        :current-page="pagination.page"
+        :page-size="pagination.limit"
+        :total="pagination.total"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        style="margin-top: 20px; text-align: right;"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </el-card>
+
+    <!-- 手动调整积分对话框 -->
+    <el-dialog
+      v-model="adjustDialogVisible"
+      title="手动调整积分"
+      width="500px"
+    >
+      <el-form
+        ref="adjustFormRef"
+        :model="adjustForm"
+        :rules="adjustRules"
+        label-width="100px"
+      >
+        <el-form-item label="用户ID" prop="user_id">
+          <el-input
+            v-model="adjustForm.user_id"
+            placeholder="请输入用户ID"
+          />
+        </el-form-item>
+        <el-form-item label="积分变动" prop="point">
+          <el-input-number
+            v-model="adjustForm.point"
+            :min="-999999"
+            :max="999999"
+            placeholder="正数为增加,负数为减少"
+            style="width: 100%;"
+          />
+        </el-form-item>
+        <el-form-item label="冻结积分" prop="freeze_point">
+          <el-input-number
+            v-model="adjustForm.freeze_point"
+            :min="-999999"
+            :max="999999"
+            placeholder="冻结积分变动量"
+            style="width: 100%;"
+          />
+        </el-form-item>
+        <el-form-item label="业务类型" prop="biz_type">
+          <el-select
+            v-model="adjustForm.biz_type"
+            placeholder="请选择业务类型"
+            style="width: 100%;"
+          >
+            <el-option
+              v-for="item in bizTypes"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="业务ID" prop="biz_id">
+          <el-input
+            v-model="adjustForm.biz_id"
+            placeholder="关联的业务ID(选填)"
+          />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input
+            v-model="adjustForm.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入调整原因"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="adjustDialogVisible = false">取消</el-button>
+          <el-button
+            type="primary"
+            @click="handleAdjustPoints"
+            :loading="adjustLoading"
+          >
+            确定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 详情对话框 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      title="积分记录详情"
+      width="600px"
+    >
+      <el-descriptions :column="2" border v-if="currentDetail">
+        <el-descriptions-item label="记录ID">{{ currentDetail.id }}</el-descriptions-item>
+        <el-descriptions-item label="用户ID">{{ currentDetail.user_id }}</el-descriptions-item>
+        <el-descriptions-item label="积分变动">
+          <span
+            :class="{
+              'point-increase': currentDetail.point > 0,
+              'point-decrease': currentDetail.point < 0
+            }"
+          >
+            {{ currentDetail.point > 0 ? '+' : '' }}{{ currentDetail.point }}
+          </span>
+        </el-descriptions-item>
+        <el-descriptions-item label="冻结积分变动">
+          <span v-if="currentDetail.freeze_point !== 0">
+            {{ currentDetail.freeze_point > 0 ? '+' : '' }}{{ currentDetail.freeze_point }}
+          </span>
+          <span v-else>无</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="变动后积分余额">{{ currentDetail.after_point }}</el-descriptions-item>
+        <el-descriptions-item label="变动后冻结余额">{{ currentDetail.after_freeze_point }}</el-descriptions-item>
+        <el-descriptions-item label="变动类型">
+          <el-tag :type="getTypeTagType(currentDetail.type)" size="small">
+            {{ getTypeName(currentDetail.type) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="业务类型">
+          <el-tag type="info" size="small">
+            {{ getBizTypeName(currentDetail.biz_type) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="业务ID">{{ currentDetail.biz_id || '无' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ currentDetail.create_time }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ currentDetail.remark || '无' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 用户选择对话框 -->
+    <el-dialog
+      v-model="userDialogVisible"
+      title="选择用户"
+      width="80%"
+    >
+      <div style="margin-bottom: 20px;">
+        <el-input
+          v-model="userSearchKeyword"
+          placeholder="请输入用户ID、昵称或手机号进行搜索"
+          clearable
+          style="width: 300px;"
+          @keyup.enter="handleUserSearch"
+        >
+          <template #append>
+            <el-button @click="handleUserSearch">搜索</el-button>
+          </template>
+        </el-input>
+        <el-button @click="resetUserSearch" style="margin-left: 10px;">重置</el-button>
+      </div>
+
+      <el-table :data="userList" @row-click="selectUser" v-loading="userLoading" max-height="400px">
+        <el-table-column prop="id" label="ID" min-width="80"></el-table-column>
+        <el-table-column label="头像" min-width="80">
+          <template #default="{ row }">
+            <el-avatar :size="40" :src="row.avatar" :alt="row.nickname">
+              <template #default>
+                <el-icon><User /></el-icon>
+              </template>
+            </el-avatar>
+          </template>
+        </el-table-column>
+        <el-table-column prop="nickname" label="昵称" min-width="80"></el-table-column>
+        <el-table-column prop="mobile" label="手机号" min-width="120">
+          <template #default="{ row }">
+            {{ row.mobile || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="point" label="当前积分" min-width="100">
+          <template #default="{ row }">
+            <span class="point-value">{{ row.point || 0 }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="update_time" label="更新时间" min-width="200" />
+        <el-table-column label="操作" width="80" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" @click="selectUser(row)">选择</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <el-pagination
+        :current-page="userPagination.page"
+        :page-size="userPagination.limit"
+        :total="userPagination.total"
+        :page-sizes="[10, 20, 50]"
+        layout="total, sizes, prev, pager, next"
+        style="margin-top: 20px; text-align: right;"
+        @size-change="handleUserSizeChange"
+        @current-change="handleUserCurrentChange"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'PointsLog',
+  props: {
+    axiosInstance: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      // 搜索表单
+      searchForm: {
+        user_id: '',
+        type: '',
+        biz_type: '',
+        dateRange: []
+      },
+      // 分页
+      pagination: {
+        page: 1,
+        limit: 20,
+        total: 0
+      },
+      // 数据
+      pointsList: [],
+      loading: false,
+      statsData: {},
+      // 枚举数据
+      pointsTypes: [
+        { value: 1, label: '新增' },
+        { value: 2, label: '减少' },
+        { value: 3, label: '对冲' }
+      ],
+      bizTypes: [
+        { value: 1, label: '订单' },
+        { value: 2, label: '签到' },
+        { value: 3, label: '推荐奖励' },
+        { value: 4, label: '手动调整' },
+        { value: 5, label: '商城兑换' }
+      ],
+      // 手动调整对话框
+      adjustDialogVisible: false,
+      adjustLoading: false,
+      adjustFormRef: null,
+      adjustForm: {
+        user_id: '',
+        point: 0,
+        freeze_point: 0,
+        biz_type: 4,
+        biz_id: '',
+        remark: ''
+      },
+      adjustRules: {
+        user_id: [
+          { required: true, message: '请输入用户ID', trigger: 'blur' },
+          { pattern: /^\d+$/, message: '用户ID必须是数字', trigger: 'blur' }
+        ],
+        point: [
+          { required: true, message: '请输入积分变动量', trigger: 'blur' }
+        ],
+        biz_type: [
+          { required: true, message: '请选择业务类型', trigger: 'change' }
+        ],
+        remark: [
+          { required: true, message: '请输入调整原因', trigger: 'blur' }
+        ]
+      },
+      // 详情对话框
+      detailDialogVisible: false,
+      currentDetail: null,
+      // 用户选择对话框
+      userDialogVisible: false,
+      userSearchKeyword: '',
+      userList: [],
+      userLoading: false,
+      userPagination: {
+        page: 1,
+        limit: 20,
+        total: 0
+      }
+    }
+  },
+  computed: {
+    // 添加计算属性用于显示用户昵称
+    selectedUserDisplay: {
+      get() {
+        if (!this.searchForm.user_id) return ''
+        const selectedUser = this.userList.find(user => user.id.toString() === this.searchForm.user_id)
+        return selectedUser ? selectedUser.nickname : this.searchForm.user_id
+      },
+      set(value) {
+        // 不需要设置逻辑,因为显示和实际值分离
+      }
+    }
+  },
+  mounted() {
+    this.getUserPointLogs()
+  },
+  methods: {
+    getTypeName(type) {
+      const item = this.pointsTypes.find(item => item.value === type)
+      return item ? item.label : '未知'
+    },
+
+    getTypeTagType(type) {
+      const typeMap = {
+        1: 'success', // 新增
+        2: 'danger',  // 减少
+        3: 'warning'  // 对冲
+      }
+      return typeMap[type] || 'info'
+    },
+
+    getBizTypeName(bizType) {
+      const item = this.bizTypes.find(item => item.value === bizType)
+      return item ? item.label : '未知'
+    },
+
+    // 获取积分记录列表
+    async getUserPointLogs() {
+      try {
+        this.loading = true
+        const params = {
+          page: this.pagination.page,
+          limit: this.pagination.limit,
+          ...this.searchForm
+        }
+        
+        // 处理时间范围
+        if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
+          params.create_time = this.searchForm.dateRange
+        }
+        delete params.dateRange
+
+        const response = await this.axiosInstance.get('/points/user_point_log', { params })
+        this.pointsList = response.page.data || []
+        this.pagination.total = response.page.total || 0
+        this.statsData = response.data || {}
+      } catch (error) {
+        console.error('获取积分记录失败:', error)
+        this.$message.error('获取积分记录失败')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    // 搜索
+    handleSearch() {
+      this.pagination.page = 1
+      this.getUserPointLogs()
+    },
+
+    // 重置
+    handleReset() {
+      Object.assign(this.searchForm, {
+        user_id: '',
+        type: '',
+        biz_type: '',
+        dateRange: []
+      })
+      this.pagination.page = 1
+      this.getUserPointLogs()
+    },
+
+    // 分页变化
+    handleSizeChange(newSize) {
+      this.pagination.limit = newSize
+      this.pagination.page = 1
+      this.getUserPointLogs()
+    },
+
+    handleCurrentChange(newPage) {
+      this.pagination.page = newPage
+      this.getUserPointLogs()
+    },
+
+    // 新增: 用户清除处理方法
+    handleUserClear() {
+      this.searchForm.user_id = ''
+    },
+
+    // 显示调整对话框
+    showAdjustDialog() {
+      Object.assign(this.adjustForm, {
+        user_id: '',
+        point: 0,
+        freeze_point: 0,
+        biz_type: 4,
+        biz_id: '',
+        remark: ''
+      })
+      this.adjustDialogVisible = true
+    },
+
+    // 手动调整积分
+    async handleAdjustPoints() {
+      try {
+        // Note: In a real implementation, you'd need to properly validate the form
+        // This is a simplified version
+        this.adjustLoading = true
+        
+        const data = { ...this.adjustForm }
+        if (!data.biz_id) {
+          delete data.biz_id
+        }
+        
+        await this.axiosInstance.post('/points/adjust_user_points', data)
+        this.$message.success('积分调整成功')
+        this.adjustDialogVisible = false
+        this.getUserPointLogs()
+      } catch (error) {
+        console.error('积分调整失败:', error)
+        if (error.message) {
+          this.$message.error(error.message)
+        }
+      } finally {
+        this.adjustLoading = false
+      }
+    },
+
+    // 显示详情
+    showDetailDialog(row) {
+      this.currentDetail = row
+      this.detailDialogVisible = true
+    },
+
+    // 显示用户选择对话框
+    showUserDialog() {
+      this.userDialogVisible = true
+      this.getUserList()
+    },
+
+    // 获取用户列表
+    async getUserList() {
+      try {
+        this.userLoading = true
+        const params = {
+          page: this.userPagination.page,
+          limit: this.userPagination.limit,
+          keyword: this.userSearchKeyword
+        }
+        
+        const response = await this.axiosInstance.get('/points/user_point', { params })
+        this.userList = response.page.data || []
+        this.userPagination.total = response.page.total || 0
+      } catch (error) {
+        console.error('获取用户列表失败:', error)
+        this.$message.error('获取用户列表失败')
+      } finally {
+        this.userLoading = false
+      }
+    },
+
+    // 搜索用户
+    handleUserSearch() {
+      this.userPagination.page = 1
+      this.getUserList()
+    },
+
+    // 重置用户搜索
+    resetUserSearch() {
+      this.userSearchKeyword = ''
+      this.userPagination.page = 1
+      this.getUserList()
+    },
+
+    // 选择用户
+    selectUser(user) {
+      this.searchForm.user_id = user.id.toString()
+      this.userDialogVisible = false
+      this.$message.success(`已选择用户: ${user.nickname} (ID: ${user.id})`)
+    },
+
+    // 用户列表分页处理
+    handleUserSizeChange(newSize) {
+      this.userPagination.limit = newSize
+      this.userPagination.page = 1
+      this.getUserList()
+    },
+
+    handleUserCurrentChange(newPage) {
+      this.userPagination.page = newPage
+      this.getUserList()
+    },
+
+    // 导出数据
+    async exportData() {
+      try {
+        const params = { ...this.searchForm }
+        
+        // 处理时间范围
+        if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
+          params.start_time = this.searchForm.dateRange[0]
+          params.end_time = this.searchForm.dateRange[1]
+        }
+        delete params.dateRange
+
+        const response = await this.axiosInstance.get('/points/export_logs', { params })
+        
+        if (response.data.download_url) {
+          // 创建下载链接
+          const link = document.createElement('a')
+          link.href = response.data.download_url
+          link.download = response.data.filename || 'points_logs.xlsx'
+          document.body.appendChild(link)
+          link.click()
+          document.body.removeChild(link)
+          this.$message.success('导出成功')
+        }
+      } catch (error) {
+        console.error('导出失败:', error)
+        this.$message.error('导出失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.points-management {
+  padding: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-weight: 600;
+  font-size: 16px;
+}
+
+.header-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.search-bar {
+  background: #f5f7fa;
+  padding: 15px;
+  border-radius: 4px;
+  margin-bottom: 20px;
+}
+
+.stats-cards .stats-card {
+  text-align: center;
+}
+
+.stats-cards .stats-card .stats-item .stats-label {
+  color: #909399;
+  font-size: 14px;
+  margin-bottom: 8px;
+}
+
+.stats-cards .stats-card .stats-item .stats-value {
+  font-size: 24px;
+  font-weight: bold;
+}
+
+.stats-cards .stats-card .stats-item .stats-value.increase {
+  color: #67c23a;
+}
+
+.stats-cards .stats-card .stats-item .stats-value.decrease {
+  color: #f56c6c;
+}
+
+.point-increase {
+  color: #67c23a;
+  font-weight: bold;
+}
+
+.point-decrease {
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+.point-freeze {
+  color: #e6a23c;
+}
+
+.freeze-point {
+  color: #e6a23c;
+  font-size: 12px;
+  margin-left: 5px;
+}
+
+.freeze-balance {
+  color: #e6a23c;
+  font-size: 12px;
+}
+
+:deep(.el-form--inline .el-form-item) {
+  margin-right: 15px;
+  margin-bottom: 10px;
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 8px 0;
+}
+
+:deep(.el-dialog .el-form-item) {
+  margin-bottom: 20px;
+}
+
+:deep(.el-dialog .el-form-item__label) {
+  font-weight: 500;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+.point-value {
+  color: #67c23a;
+  font-weight: 500;
+}
+
+:deep(.el-table .el-table__row) {
+  cursor: pointer;
+}
+
+:deep(.el-table .el-table__row:hover) {
+  background-color: #f5f7fa;
+}
+
+:deep(.el-input.is-readonly .el-input__inner) {
+  cursor: pointer;
+}
+</style>

+ 77 - 0
resource/admin/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="module-container">
+    <el-container>
+      <el-header height="60px">
+        <el-menu
+          :default-active="activeMenu"
+          mode="horizontal"
+          @select="handleMenuSelect"
+        >
+          <el-menu-item index="pointsLog">
+            <span>积分明细</span>
+          </el-menu-item>
+        </el-menu>
+      </el-header>
+      
+      <el-main>
+        <PointsLogComponent 
+          :axios-instance="axiosInstance" 
+        />
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<script>
+import PointsLogComponent from './PointsLog.vue'
+
+export default {
+  name: 'PointsAdmin',
+  components: {
+    PointsLogComponent
+  },
+  props: {
+    axiosInstance: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      activeMenu: 'pointsLog'
+    }
+  },
+  methods: {
+    handleMenuSelect(key) {
+      this.activeMenu = key
+    }
+  }
+}
+</script>
+
+<style scoped>
+.module-container {
+  padding: 0;
+  height: calc(100vh - 120px);
+}
+
+.el-menu {
+  height: 60px;
+  font-size: 16px;
+}
+
+.el-header {
+  padding: 0;
+  background-color: #fff;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.el-main {
+  padding: 30px;
+  background: #fff;
+  margin: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  flex: 1;
+}
+</style>