소스 검색

优化首页数据获取、解封日志表筛选

wangzhiqiang 3 달 전
부모
커밋
111bbae01d

+ 2 - 2
.env.development

@@ -1,11 +1,11 @@
 ENV = 'development'
 
 # 线上测试
-VITE_BASE_URL = 'http://advise.ytmdm.com/yt-gateway'
+# VITE_BASE_URL = 'http://advise.ytmdm.com/yt-gateway'
 # VITE_BASE_URL = 'https://test.book.ytpm.net/yt-gateway'
 # VITE_BASE_URL = 'http://119.45.71.139:25001'
 
 # 本地 
-# VITE_BASE_URL = 'http://192.168.1.9:25001'
+VITE_BASE_URL = 'http://192.168.1.128:25001'
 # VITE_BASE_URL = 'http://192.168.1.159:25001'
 

+ 13 - 12
src/utils/index.js

@@ -36,18 +36,6 @@ export function camelToSnake(str) {
     return str.replace(/([A-Z])/g, '_$1').toLowerCase();
 }
 
-/**
- * 保留小数位
- * @param {*} value 价格
- * @param {*} place 小数位默认2
- * @returns 
- */
-export function roundPrice(value, place = 2) {
-    if (value === 0) {
-        return 0.00
-    }
-    return Number(Number(value).toFixed(place));
-}
 
 /**
  * 将 base64 转为 Blob
@@ -64,4 +52,17 @@ export function base64ToBlob(base64) {
         u8arr[n] = bstr.charCodeAt(n)
     }
     return new Blob([u8arr], { type: mime })
+}
+
+/**
+ * 保留小数位
+ * @param {*} value 输入值
+ * @param {number} place 保留小数位,默认2位
+ * @param {boolean} type 返回类似 true-number,false-string,默认false
+ * @returns {string | number} 默认返回string 0.00
+ */
+export function roundPrice(value, place = 2, type = false) {
+    const num = Number(value)
+    if (isNaN(num)) return (0).toFixed(place)
+    return type ? Number(num.toFixed(place)) : num.toFixed(place)
 }

+ 1 - 1
src/utils/system/request.js

@@ -7,7 +7,7 @@ const baseURL = import.meta.env.VITE_BASE_URL
 
 const service = axios.create({
   baseURL: baseURL,
-  timeout: 20000
+  timeout: 30 * 1000
 })
 
 // 请求前的统一处理

+ 91 - 24
src/views/main/dashboard/components/charts/index.vue

@@ -1,8 +1,9 @@
 <template>
   <div>
-    <lineChartModel :tableData="tableData" :lineListData="lineListData" :totalProfit="totalProfit" title="收益" />
-    <lineChartModel :tableData="tableData" :lineListData="lineListData" :totalProfit="totalProfit" :type="'ecpm'"
-      title="ECPM趋势" />
+    <lineChartModel :tableData="tableData" :lineListData="seriesData" :totalProfit="totalProfit"
+      :legendData="legendData" title="收益" />
+    <lineChartModel :tableData="tableData" :lineListData="seriesEcpmData" :totalProfit="totalProfit"
+      :legendData="legendData" :type="'ecpm'" title="ECPM趋势" />
 
     <userDataChart />
   </div>
@@ -13,6 +14,7 @@ import { ref, onBeforeMount } from 'vue'
 import { getIndexProfit, getIndexHourReport } from '@/api/dashboard.js'
 import lineChartModel from './lineChartModel.vue'
 import userDataChart from './userDataChart.vue'
+import { roundPrice } from '@/utils'
 
 // 表格初始化数据
 const tableData = ref([
@@ -22,20 +24,46 @@ const tableData = ref([
   { platform: "快手", today: "0.00", yesterday: "0.00", month: "0.00", ecpmToday: "0.00", ecpmYesterday: "0.00", ecpmMonth: "0.00" },
   { platform: "Sigmob", today: "0.00", yesterday: "0.00", month: "0.00", ecpmToday: "0.00", ecpmYesterday: "0.00", ecpmMonth: "0.00" },
 ])
-const totalProfit = ref({})
+
+const colorData = [
+  '#add7f6', '#f7a8b8', '#ffcc5c', '#88d8b0', '#9966cc',
+  '#cf9ef1', '#57e7ec', '#fdb301', '#f59a8f', '#fca4bb',
+  '#2b908f', '#90ed7d', '#e75fc3', '#5085f2', '#8d7fec'
+]
+
+const totalProfit = ref({}) //表格数据汇总
+const seriesData = ref([])
+const seriesEcpmData = ref([])
+const legendData = ref([]) //名称
 const getIndexProfitData = async () => {
   const res = await getIndexProfit()
-  tableData.value = res.data
-
-  const summary = res.data.reduce((acc, cur) => {
-    acc.today += parseFloat(cur.today)
-    acc.yesterday += parseFloat(cur.yesterday)
-    acc.month += parseFloat(cur.month)
-    acc.ecpmToday += parseFloat(cur.ecpmToday)
-    acc.ecpmYesterday += parseFloat(cur.ecpmYesterday)
-    acc.ecpmMonth += parseFloat(cur.ecpmMonth)
+
+  // 清空现有数据(保留响应式引用)
+  tableData.value.splice(0)
+  seriesData.value.splice(0)
+  seriesEcpmData.value.splice(0)
+  legendData.value.splice(0)
+
+  res.data.forEach((item, index) => {
+    const { tableRow, chartSeries, chartSeriesEcpm, name } = mapAppRevenueItem(item, index)
+    tableData.value.push(tableRow)
+    seriesData.value.push(chartSeries)
+    seriesEcpmData.value.push(chartSeriesEcpm)
+    legendData.value.push(name)
+  })
+
+  const summary = tableData.value.reduce((acc, cur) => {
+    acc.today += parseFloat(cur.today) || 0
+    acc.yesterday += parseFloat(cur.yesterday) || 0
+    acc.month += parseFloat(cur.month) || 0
+    acc.ecpmToday += parseFloat(cur.ecpmToday) || 0
+    acc.ecpmYesterday += parseFloat(cur.ecpmYesterday) || 0
+    acc.ecpmMonth += parseFloat(cur.ecpmMonth) || 0
     return acc
-  }, { today: 0, yesterday: 0, month: 0, ecpmToday: 0, ecpmYesterday: 0, ecpmMonth: 0 })
+  }, {
+    today: 0, yesterday: 0, month: 0,
+    ecpmToday: 0, ecpmYesterday: 0, ecpmMonth: 0
+  })
 
   // 格式化为两位小数字符串
   totalProfit.value = {
@@ -48,21 +76,60 @@ const getIndexProfitData = async () => {
   }
 }
 
-const lineListData = ref({})
-const getIndexHourReportData = async () => {
-  const res = await getIndexHourReport()
+const mapAppRevenueItem = (item, index) => {
+  return {
+    tableRow: {
+      platform: item.netowrkName,
+      today: roundPrice(item.todayRevenue),
+      yesterday: roundPrice(item.yesterdayRevenue),
+      month: roundPrice(item.monthRevenue),
+      ecpmToday: roundPrice(item.todayEcpm),
+      ecpmYesterday: roundPrice(item.yesterdayEcpm),
+      ecpmMonth: roundPrice(item.monthEcpm),
+    },
+    chartSeries: {
+      type: 'line',
+      name: item.netowrkName,
+      symbolSize: 6,
+      z: 1,
+      label: { show: false },
+      itemStyle: { color: colorData[index % colorData.length] },
+      lineStyle: { color: colorData[index % colorData.length], width: 2 },
+      data: fillHourRevenueMap(item.todayHourRevenueMap)
+    },
+    chartSeriesEcpm: {
+      type: 'line',
+      name: item.netowrkName,
+      symbolSize: 6,
+      z: 1,
+      label: { show: false },
+      itemStyle: { color: colorData[index % colorData.length] },
+      lineStyle: { color: colorData[index % colorData.length], width: 2 },
+      data: fillHourRevenueMap(item.todayHourEcpmMap)
+    },
+    name: item.netowrkName
+  }
+}
+
+// 提取24小时数据
+const fillHourRevenueMap = (map) => {
+  const hourMap = {}
+
+  for (const [datetime, value] of Object.entries(map || {})) {
+    const hour = new Date(datetime).getHours()
+    hourMap[hour] = parseFloat(value) || 0
+  }
+
+  const fullMap = []
+  for (let i = 0; i < 24; i++) {
+    fullMap.push(hourMap[i] ?? 0)
+  }
 
-  // 整理 lineListData
-  lineListData.value = res.data.reduce((acc, item) => {
-    const { channelName, estimatedRevenueList, estimatedRevenueEcpmList } = item;
-    acc[channelName] = { estimatedRevenueList, estimatedRevenueEcpmList };
-    return acc;
-  }, {});
+  return fullMap
 }
 
 onBeforeMount(async () => {
   await getIndexProfitData()
-  await getIndexHourReportData()
 })
 
 </script>

+ 22 - 70
src/views/main/dashboard/components/charts/lineChartModel.vue

@@ -59,8 +59,13 @@ const props = defineProps({
     },
     // 折线图数据
     lineListData: {
-        type: Object,
-        default: {}
+        type: Array,
+        default: []
+    },
+    // legend名称
+    legendData: {
+        type: Array,
+        default: []
     },
     // 汇总数据
     totalProfit: {
@@ -78,11 +83,10 @@ const props = defineProps({
     }
 });
 
-const xAxisList = [
-    '00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00', '07:00',
-    '08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00',
-    '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00', '23:00'
-];
+const xAxisList = Array.from({ length: 24 }, (_, i) => {
+    const hour = i.toString().padStart(2, '0')
+    return `${hour}:00`
+})
 
 const options = ref({
     /* title: {
@@ -121,7 +125,7 @@ const options = ref({
         }
     },
     legend: {
-        data: ['穿山甲', '优量汇', '快手', 'Sigmob', '百度'],
+        data: [],
         top: 'top',
         textStyle: {
             color: '#808080'
@@ -175,69 +179,17 @@ const options = ref({
 watch(() => props.lineListData, (newVal) => {
     if (Object.keys(newVal).length === 0) return; // 空数据不更新
 
-    options.value.series = [
-        {
-            type: 'line',
-            name: '穿山甲',
-            symbolSize: 6,
-            z: 1,
-            label: { show: false },
-            itemStyle: { color: '#add7f6' },
-            lineStyle: { color: '#add7f6', width: 2 },
-            data: props.type === 'earn' ?
-                newVal['穿山甲']?.estimatedRevenueList ?? [] :
-                newVal['穿山甲']?.estimatedRevenueEcpmList ?? []
-        },
-        {
-            type: 'line',
-            name: '优量汇',
-            symbolSize: 6,
-            z: 1,
-            label: { show: false },
-            itemStyle: { color: '#f7a8b8' },
-            lineStyle: { color: '#f7a8b8', width: 2 },
-            data: props.type === 'earn' ?
-                newVal['优量汇']?.estimatedRevenueList ?? [] :
-                newVal['优量汇']?.estimatedRevenueEcpmList ?? []
-        },
-        {
-            type: 'line',
-            name: '快手',
-            symbolSize: 6,
-            z: 1,
-            label: { show: false },
-            itemStyle: { color: '#ffcc5c' },
-            lineStyle: { color: '#ffcc5c', width: 2 },
-            data: props.type === 'earn' ?
-                newVal['快手联盟']?.estimatedRevenueList ?? [] :
-                newVal['快手联盟']?.estimatedRevenueEcpmList ?? []
-        },
-        {
-            type: 'line',
-            name: 'Sigmob',
-            symbolSize: 6,
-            z: 1,
-            label: { show: false },
-            itemStyle: { color: '#88d8b0' },
-            lineStyle: { color: '#88d8b0', width: 2 },
-            data: props.type === 'earn' ?
-                newVal['Sigmob']?.estimatedRevenueList ?? [] :
-                newVal['Sigmob']?.estimatedRevenueEcpmList ?? []
-        },
-        {
-            type: 'line',
-            name: '百度',
-            symbolSize: 6,
-            z: 1,
-            label: { show: false },
-            itemStyle: { color: '#9966cc' },
-            lineStyle: { color: '#9966cc', width: 2 },
-            data: props.type === 'earn' ?
-                newVal['百度']?.estimatedRevenueList ?? [] :
-                newVal['百度']?.estimatedRevenueEcpmList ?? []
+    // 更新 ECharts 配置
+    options.value.legend = {
+        data: props.legendData,
+        top: 'top',
+        textStyle: {
+            color: '#808080'
         }
-    ]
-}, { immediate: true })
+    }
+
+    options.value.series = props.lineListData
+}, { immediate: true, deep: true })
 
 </script>
 

+ 202 - 218
src/views/main/userModule/relieveLogsList.vue

@@ -14,7 +14,7 @@
         <el-table-column prop="userId" label="用户ID" width="160" />
         <el-table-column prop="userStatus" label="用户状态" width="90">
           <template #default="scope">
-            {{ getDictionaryName('user_status',scope.row.userStatus) }}
+            {{ getDictionaryName('user_status', scope.row.userStatus) }}
           </template>
         </el-table-column>
         <el-table-column prop="platformId" label="平台ID" width="150" />
@@ -70,239 +70,223 @@
 </template>
 
 <script setup>
-  import { onBeforeMount, ref, reactive } from "vue";
-  import From from "@/components/from/index.vue";
-  import Table from "@/components/table/index.vue";
-  import Layer from '@/components/layer/index.vue'
-  import Card from './components/card/index.vue'
-  import { ElMessage } from 'element-plus'
-  import { riskDeblockingList, riskChangeUserStatus } from '@/api/riskModule.js'
-  import { convertUTCToBeijing, camelToSnake } from '@/utils/index.js'
-  import { useGetDictList } from '@/hooks/useGetDictList.js'
-  import { useStore } from 'vuex'
-
-  const store = useStore()
-  const { dictData, loadDictData, getOptions, getDictionaryName } = useGetDictList();
-  const form = ref(null);
-  const tableData = ref([]);
-
-  // 分页参数, 供table使用
-  const page = reactive({
-    pageNum: 1,
-    pageSize: 20,
-    total: 0,
-  });
-
-  const formSearch = ref({
-    appId: null,// 应用ID
-    channelOrigin: null,// 渠道来源
-    deblockingReason: null,// 解封原因
-    deblockingTimeBegin: null,// 解封时间开始
-    deblockingTimeEnd: null,// 解封时间结束
-    loginTimeBegin: null,// 登录时间开始
-    loginTimeEnd: null,// 登录时间截止
-    operator: null,// 用户昵称
-
-    limit: 20,// 当前页数量(查询量)
-    page: 1,// 当前页码
-  });
-
-
-  const dynamicFormItems = ref([])
-
-  onBeforeMount(() => {
-    settingData()
-    getList();
-  });
-
-  // 获取缓存数据设置筛选数据
-  const settingData = () => {
-    loadDictData().then(async () => {
-      dynamicFormItems.value = [
-        {
-          label: '用户ID',
-          prop: 'userId',
-          type: 'input',
-          needEnterEvent: true
-        },
-        {
-          label: '应用ID',
-          prop: 'appId',
-          type: 'input',
-          needEnterEvent: true
-        },
-        {
-          label: '渠道来源',
-          prop: 'channelOrigin',
-          type: 'select',
-          options: getOptions('channel_origin'),
-        },
-        {
-          label: '解封原因',
-          prop: 'deblockingReason',
-          type: 'input',
-          needEnterEvent: true
-        },
-        { label: '解封时间', prop: 'deblockingTime', type: 'daterange' },
-        { label: '登录时间', prop: 'loginTime', type: 'daterange' },
-      ]
-    })
-  }
+import { onBeforeMount, ref, reactive } from "vue";
+import From from "@/components/from/index.vue";
+import Table from "@/components/table/index.vue";
+import Layer from '@/components/layer/index.vue'
+import Card from './components/card/index.vue'
+import { ElMessage } from 'element-plus'
+import { riskDeblockingList, riskChangeUserStatus } from '@/api/riskModule.js'
+import { convertUTCToBeijing, camelToSnake } from '@/utils/index.js'
+import { useGetDictList } from '@/hooks/useGetDictList.js'
+import { useStore } from 'vuex'
+
+const store = useStore()
+const { dictData, loadDictData, getOptions, getDictionaryName } = useGetDictList();
+const form = ref(null);
+const tableData = ref([]);
+
+// 分页参数, 供table使用
+const page = reactive({
+  pageNum: 1,
+  pageSize: 20,
+  total: 0,
+});
+
+const formSearch = ref({
+  userId: undefined,// 用户ID
+  appId: undefined,// 应用ID
+  channelOrigin: undefined,// 渠道来源
+  deblockingReason: undefined,// 解封原因
+  deblockingTimeBegin: undefined,// 解封时间开始
+  deblockingTimeEnd: undefined,// 解封时间结束
+
+  limit: 20,// 当前页数量(查询量)
+  page: 1,// 当前页码
+});
+
+
+const dynamicFormItems = ref([])
+
+onBeforeMount(() => {
+  settingData()
+  getList();
+});
+
+// 获取缓存数据设置筛选数据
+const settingData = () => {
+  loadDictData().then(async () => {
+    dynamicFormItems.value = [
+      {
+        label: '用户ID',
+        prop: 'userId',
+        type: 'input',
+        needEnterEvent: true
+      },
+      {
+        label: '应用ID',
+        prop: 'appId',
+        type: 'input',
+        needEnterEvent: true
+      },
+      {
+        label: '渠道来源',
+        prop: 'channelOrigin',
+        type: 'select',
+        options: getOptions('channel_origin'),
+      },
+      {
+        label: '解封原因',
+        prop: 'deblockingReason',
+        type: 'input',
+        needEnterEvent: true
+      },
+      { label: '解封时间', prop: 'deblockingTime', type: 'daterange' },
+    ]
+  })
+}
 
-  // 分页数据
-  const getList = async () => {
-    let res = await riskDeblockingList({ ...formSearch.value });
-    tableData.value = res.data;
-    page.total = res.pageMeta.total;
-  };
+// 分页数据
+const getList = async () => {
+  let res = await riskDeblockingList({ ...formSearch.value });
+  tableData.value = res.data;
+  page.total = res.pageMeta.total;
+};
 
-  const changeTableData = (type) => {
-    formSearch.value.page = type ? 1 : page.pageNum;
-    formSearch.value.limit = page.pageSize;
-    // 分页切换
-    getList();
+const changeTableData = (type) => {
+  formSearch.value.page = type ? 1 : page.pageNum;
+  formSearch.value.limit = page.pageSize;
+  // 分页切换
+  getList();
 };
 
-  // 搜索
-  const handleFormSubmitted = (formData) => {
-    formSearch.value.page = 1;
-    formSearch.value.limit = 20;
-    formSearch.value.appId = formData.appId;
-    formSearch.value.channelId = formData.channelId;
-    formSearch.value.channelType = formData.channelType;
-    formSearch.value.channelOrigin = formData.channelOrigin;
-    formSearch.value.deblockingReason = formData.deblockingReason
-    formSearch.value.operator = formData.operator
-
-    // 解封时间
-    if (formData.deblockingTime) {
-      formSearch.value.deblockingTimeBegin = formData.deblockingTime[0];
-      formSearch.value.deblockingTimeEnd = formData.deblockingTime[1];
-    }else{
-      formSearch.value.deblockingTimeBegin = null
-      formSearch.value.deblockingTimeEnd = null
-    }
+// 搜索
+const handleFormSubmitted = (formData) => {
+  formSearch.value.page = 1;
+  formSearch.value.limit = 20;
+  formSearch.value.userId = formData.userId;
+  formSearch.value.appId = formData.appId;
+  formSearch.value.channelOrigin = formData.channelOrigin;
+  formSearch.value.deblockingReason = formData.deblockingReason
+
+  // 解封时间
+  if (formData.deblockingTime) {
+    formSearch.value.deblockingTimeBegin = formData.deblockingTime[0];
+    formSearch.value.deblockingTimeEnd = formData.deblockingTime[1];
+  } else {
+    formSearch.value.deblockingTimeBegin = undefined
+    formSearch.value.deblockingTimeEnd = undefined
+  }
 
-    // 登录时间
-    if (formData.loginTime) {
-      formSearch.value.loginTimeBegin = formData.loginTime[0];
-      formSearch.value.loginTimeEnd = formData.loginTime[1];
-    }else {
-      formSearch.value.loginTimeBegin = null
-      formSearch.value.loginTimeEnd = null
-    }
+  getList();
+};
 
-    getList();
-  };
+// 表单重置
+const handleFormReset = () => {
+  formSearch.value = {
+    userId: undefined,// 用户ID
+    appId: undefined,// 应用ID
+    channelOrigin: undefined,// 渠道来源
+    deblockingReason: undefined,// 解封原因
+    deblockingTimeBegin: undefined,// 解封时间开始
+    deblockingTimeEnd: undefined,// 解封时间结束
 
-  // 表单重置
-  const handleFormReset = () => {
-    formSearch.value = {
-      appId: null,// 应用ID
-      channelOrigin: null,// 渠道来源
-      deblockingReason: null,// 解封原因
-      deblockingTimeBegin: null,// 解封时间开始
-      deblockingTimeEnd: null,// 解封时间结束
-      loginTimeBegin: null,// 登录时间开始
-      loginTimeEnd: null,// 登录时间截止
-      operator: null,// 用户昵称
-
-      limit: 20,// 当前页数量(查询量)
-      page: 1,// 当前页码
-    };
-    getList();
+    limit: 20,// 当前页数量(查询量)
+    page: 1,// 当前页码
   };
+  getList();
+};
 
-  // 选择监听器
-  const handleSelectionChange = (val) => {
-    // context.emit("selection-change", val)
-  }
-
-  // 弹窗
-  const layer = ref({
-    show: false,
-    title: "更改用户状态",
-    showButton: true,
-    width: '300px'
-  });
-
-  const formEdit = ref({
-    bannedType: null,//封禁类型 1-渠道 2-平台
-    operator: '',//操作人
-    operatorName: '',//操作人名称
-    reason: '',//原因
-    userId: '', //用户ID
-    userStatus: null, //用户状态
+// 选择监听器
+const handleSelectionChange = (val) => {
+  // context.emit("selection-change", val)
+}
+
+// 弹窗
+const layer = ref({
+  show: false,
+  title: "更改用户状态",
+  showButton: true,
+  width: '300px'
+});
+
+const formEdit = ref({
+  bannedType: null,//封禁类型 1-渠道 2-平台
+  operator: '',//操作人
+  operatorName: '',//操作人名称
+  reason: '',//原因
+  userId: '', //用户ID
+  userStatus: null, //用户状态
+})
+
+const edit = (row) => {
+  ruleForm.value?.resetFields()
+  layer.value.show = true
+
+  formEdit.value.bannedType = row.channelType
+  formEdit.value.operator = store.state.user.info.loginName
+  formEdit.value.operatorName = store.state.user.info.nickName
+  formEdit.value.userId = row.userId
+}
+
+const ruleForm = ref(null);
+
+const rules = reactive({
+  userStatus: [
+    {
+      required: true,
+      message: "请选择用户状态",
+      trigger: "change",
+    },
+  ],
+  reason: [
+    {
+      required: true,
+      message: "请输入更改原因",
+      trigger: ["blur"],
+    },
+  ],
+});
+
+const submit = async (formEl) => {
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      // 提交内容
+      riskChangeUserStatus(formEdit.value).then((res) => {
+        ElMessage.success('更改用户状态成功')
+        getList();
+      })
+    } else {
+      console.log("error submit!", fields);
+    }
   })
-
-  const edit = (row) => {
-    ruleForm.value?.resetFields()
-    layer.value.show = true
-
-    formEdit.value.bannedType = row.channelType
-    formEdit.value.operator = store.state.user.info.loginName
-    formEdit.value.operatorName = store.state.user.info.nickName
-    formEdit.value.userId = row.userId
-  }
-
-  const ruleForm = ref(null);
-
-  const rules = reactive({
-    userStatus: [
-      {
-        required: true,
-        message: "请选择用户状态",
-        trigger: "change",
-      },
-    ],
-    reason: [
-      {
-        required: true,
-        message: "请输入更改原因",
-        trigger: ["blur"],
-      },
-    ],
-  });
-
-  const submit = async (formEl) => {
-    await formEl.validate(async (valid, fields) => {
-      if (valid) {
-        // 提交内容
-        riskChangeUserStatus(formEdit.value).then((res) => {
-          ElMessage.success('更改用户状态成功')
-          getList();
-        })
-      } else {
-        console.log("error submit!", fields);
-      }
-    })
-    layer.value.show = false
-  }
+  layer.value.show = false
+}
 </script>
 
 <style scoped lang="scss">
-  .layout-container {
-
-    .card {
-      .title {
-        margin-bottom: 10px;
-        font-weight: 600;
-      }
-
-      display: flex;
-      flex-direction: column;
-      align-items: start;
-      width: calc(100% - 60px);
-      margin: 30px 30px 0;
+.layout-container {
+
+  .card {
+    .title {
+      margin-bottom: 10px;
+      font-weight: 600;
     }
 
-    .button {
-      display: flex;
-      flex-direction: column;
+    display: flex;
+    flex-direction: column;
+    align-items: start;
+    width: calc(100% - 60px);
+    margin: 30px 30px 0;
+  }
+
+  .button {
+    display: flex;
+    flex-direction: column;
 
-      .button-item {
-        margin: 4px;
-      }
+    .button-item {
+      margin: 4px;
     }
   }
+}
 </style>