ソースを参照

概览页面优化

wangwei 3 年 前
コミット
8c3b8cfa61
33 ファイル変更2180 行追加139 行削除
  1. 4 3
      src/api/sys/model/userModel.ts
  2. 3 3
      src/components/Cropper/src/CopperModal.vue
  3. 3 6
      src/components/Cropper/src/CropperAvatar.vue
  4. 35 0
      src/views/home/survey/components/AssociationIntroduction.vue
  5. 34 0
      src/views/home/survey/components/AssociationSurvey.vue
  6. 68 0
      src/views/home/survey/components/MemberSurvey.vue
  7. 59 0
      src/views/home/survey/components/RecentAnnouncement.vue
  8. 41 0
      src/views/home/survey/components/WorkbenchHeader.vue
  9. 0 0
      src/views/home/survey/components/data.ts
  10. 40 86
      src/views/home/survey/index.vue
  11. 78 5
      src/views/home/user/index.vue
  12. 43 0
      src/views/test/analysis/components/GrowCard.vue
  13. 69 0
      src/views/test/analysis/components/SalesProductPie.vue
  14. 46 0
      src/views/test/analysis/components/SiteAnalysis.vue
  15. 110 0
      src/views/test/analysis/components/VisitAnalysis.vue
  16. 62 0
      src/views/test/analysis/components/VisitAnalysisBar.vue
  17. 106 0
      src/views/test/analysis/components/VisitRadar.vue
  18. 88 0
      src/views/test/analysis/components/VisitSource.vue
  19. 16 0
      src/views/test/analysis/components/props.ts
  20. 43 0
      src/views/test/analysis/data.ts
  21. 39 0
      src/views/test/analysis/index.vue
  22. 432 0
      src/views/test/house/index.css
  23. 494 0
      src/views/test/house/index.less
  24. 89 0
      src/views/test/house/index.vue
  25. 0 36
      src/views/test/index.vue
  26. 22 0
      src/views/test/welcome/index.vue
  27. 0 0
      src/views/test/workbench/components/DynamicInfo.vue
  28. 0 0
      src/views/test/workbench/components/ProjectCard.vue
  29. 0 0
      src/views/test/workbench/components/QuickNav.vue
  30. 0 0
      src/views/test/workbench/components/SaleRadar.vue
  31. 0 0
      src/views/test/workbench/components/WorkbenchHeader.vue
  32. 156 0
      src/views/test/workbench/components/data.ts
  33. 0 0
      src/views/test/workbench/index.vue

+ 4 - 3
src/api/sys/model/userModel.ts

@@ -44,10 +44,11 @@ export interface EditUserParams {
   menus?: string[] | number[];
 }
 export interface EditMyInfoParams {
-  password: string;
+  password?: string;
   // 真实名字
-  nickname: string;
-  email: string;
+  nickname?: string;
+  email?: string;
+  avatar?: string;
 }
 
 export interface DeleteUserParams {

+ 3 - 3
src/components/Cropper/src/CopperModal.vue

@@ -25,10 +25,10 @@
           <Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
             <Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
               <a-button
-                size="middle"
-                iconSize="18"
-                preIcon="ant-design:upload-outlined"
                 type="primary"
+                size="default"
+                :iconSize="18"
+                preIcon="ant-design:upload-outlined"
               />
             </Tooltip>
           </Upload>

+ 3 - 6
src/components/Cropper/src/CropperAvatar.vue

@@ -42,7 +42,6 @@
   import CopperModal from './CopperModal.vue';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { useModal } from '/@/components/Modal';
-  import { useMessage } from '/@/hooks/web/useMessage';
   import { useI18n } from '/@/hooks/web/useI18n';
   import type { ButtonProps } from '/@/components/Button';
   import Icon from '/@/components/Icon';
@@ -65,7 +64,6 @@
       const sourceValue = ref(props.value || '');
       const { prefixCls } = useDesign('cropper-avatar');
       const [register, { openModal, closeModal }] = useModal();
-      const { createMessage } = useMessage();
       const { t } = useI18n();
 
       const getClass = computed(() => [prefixCls]);
@@ -91,10 +89,9 @@
         }
       );
 
-      function handleUploadSuccess({ source }) {
-        sourceValue.value = source;
-        emit('change', source);
-        createMessage.success(t('component.cropper.uploadSuccess'));
+      function handleUploadSuccess(data) {
+        sourceValue.value = data.source;
+        emit('change', data);
       }
 
       expose({ openModal: openModal.bind(null, true), closeModal });

+ 35 - 0
src/views/home/survey/components/AssociationIntroduction.vue

@@ -0,0 +1,35 @@
+<template>
+  <Card title="协会介绍" v-bind="$attrs">
+    <template #extra>
+      <a-button type="link" size="small">编辑</a-button>
+    </template>
+
+    <div :style="{ width, height }">
+      <ScrollContainer> 12122 </ScrollContainer>
+    </div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Card } from 'ant-design-vue';
+  import { groupItems } from './data';
+  import { ScrollContainer } from '/@/components/Container/index';
+
+  export default defineComponent({
+    components: { Card, ScrollContainer },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup() {
+      return { items: groupItems };
+    },
+  });
+</script>

+ 34 - 0
src/views/home/survey/components/AssociationSurvey.vue

@@ -0,0 +1,34 @@
+<template>
+  <Card title="协会概况" v-bind="$attrs">
+    <template #extra>
+      <a-button type="link" size="small">编辑</a-button>
+    </template>
+
+    <div :style="{ width, height }">
+      <ScrollContainer>sad </ScrollContainer>
+    </div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { ScrollContainer } from '/@/components/Container/index';
+  import { Card } from 'ant-design-vue';
+
+  export default defineComponent({
+    components: { Card, ScrollContainer },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup() {
+      return {};
+    },
+  });
+</script>

+ 68 - 0
src/views/home/survey/components/MemberSurvey.vue

@@ -0,0 +1,68 @@
+<template>
+  <Card title="会员概况">
+    <template #extra>
+      <a-button type="link" size="small">前往查看</a-button>
+    </template>
+    <div :style="{ width, height }">
+      <ScrollContainer>
+        <div v-for="item in data" :key="item.id">
+          <div class="flex item-wrap">
+            <div class="item-title">{{ item.title }}</div>
+            <div class="item-value">{{ item.value }}</div>
+          </div>
+        </div>
+      </ScrollContainer>
+    </div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { ScrollContainer } from '/@/components/Container/index';
+  import { Card } from 'ant-design-vue';
+
+  interface DataType {
+    id: number;
+    title: string;
+    value: string;
+  }
+  export default defineComponent({
+    components: { Card, ScrollContainer },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+      data: {
+        type: Array as PropType<DataType[]>,
+        default: () => [
+          { id: 1, title: '会员总数', value: '120' },
+          { id: 2, title: '个人总数', value: '120' },
+          { id: 3, title: '单位总数', value: '120' },
+          { id: 4, title: '本年会费缴纳', value: '70%' },
+          { id: 5, title: '本年会议参会率', value: '60%' },
+          { id: 6, title: '本年活动参会率', value: '50%' },
+        ],
+      },
+    },
+    setup() {
+      return {};
+    },
+  });
+</script>
+<style scoped>
+  .item-wrap {
+    margin: 5px;
+    font-size: 18px;
+  }
+  .item-title {
+    width: 50%;
+    margin-right: 10px;
+  }
+  /* .item-value {
+  } */
+</style>

+ 59 - 0
src/views/home/survey/components/RecentAnnouncement.vue

@@ -0,0 +1,59 @@
+<template>
+  <Card title="近期公告" v-bind="$attrs">
+    <template #extra>
+      <a-button type="link" size="small">查看更多</a-button>
+    </template>
+    <div :style="{ width, height }">
+      <ScrollContainer>
+        <List item-layout="horizontal" :data-source="items">
+          <template #renderItem="{ item }">
+            <ListItem>
+              <ListItemMeta>
+                <template #description>
+                  {{ item.date }}
+                </template>
+                <template #title> {{ item.name }} <span v-html="item.desc"> </span> </template>
+                <template #avatar>
+                  <Icon :icon="item.avatar" :size="30" />
+                </template>
+              </ListItemMeta>
+            </ListItem>
+          </template>
+        </List>
+      </ScrollContainer>
+    </div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Card, List } from 'ant-design-vue';
+  import { dynamicInfoItems } from './data';
+  import headerImg from '/@/assets/images/header.jpg';
+  import { Icon } from '/@/components/Icon';
+  import { ScrollContainer } from '/@/components/Container/index';
+
+  export default defineComponent({
+    components: {
+      Card,
+      List,
+      ListItem: List.Item,
+      ListItemMeta: List.Item.Meta,
+      Icon,
+      ScrollContainer,
+    },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup() {
+      return { items: dynamicInfoItems, headerImg };
+    },
+  });
+</script>

+ 41 - 0
src/views/home/survey/components/WorkbenchHeader.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="lg:flex">
+    <Avatar :src="imgUrlPrefix + userinfo.avatar" :size="72" class="!mx-auto !block" />
+    <div class="md:ml-6 flex flex-col justify-center md:mt-0 mt-2">
+      <h1 class="md:text-lg text-md">早安, {{ userinfo.nickname }}, 开始您一天的工作吧!</h1>
+      <span class="text-secondary"> 今日晴,20℃ - 32℃! </span>
+    </div>
+    <div class="flex flex-1 justify-end md:mt-0 mt-4">
+      <div class="flex flex-col justify-center text-right">
+        <span class="text-secondary"> 待办 </span>
+        <span class="text-2xl">2/10</span>
+      </div>
+
+      <div class="flex flex-col justify-center text-right md:mx-16 mx-12">
+        <span class="text-secondary"> 项目 </span>
+        <span class="text-2xl">8</span>
+      </div>
+      <div class="flex flex-col justify-center text-right md:mr-10 mr-4">
+        <span class="text-secondary"> 团队 </span>
+        <span class="text-2xl">300</span>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { computed, defineComponent } from 'vue';
+
+  import { Avatar } from 'ant-design-vue';
+  import { useUserStore } from '/@/store/modules/user';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import headerImg from '/@/assets/images/header.jpg';
+  export default defineComponent({
+    components: { Avatar },
+    setup() {
+      const { imgUrlPrefix } = useGlobSetting();
+      const userStore = useUserStore();
+      const userinfo = computed(() => userStore.getUserInfo);
+      return { imgUrlPrefix, userinfo, headerImg };
+    },
+  });
+</script>

+ 0 - 0
src/views/dashboard/workbench/components/data.ts → src/views/home/survey/components/data.ts


+ 40 - 86
src/views/home/survey/index.vue

@@ -1,99 +1,53 @@
 <template>
-  <div class="wrap" style="padding: 20px">
-    <p class="head-title">协会概况</p>
-    <div class="header-content">
-      <div class="edit-text">
-        <textarea name="" id=""></textarea>
+  <PageWrapper>
+    <template #headerContent> <WorkbenchHeader /> </template>
+    <div class="lg:flex">
+      <div class="lg:w-4/10 w-full !mr-4 enter-y">
+        <AssociationSurvey :loading="loading" class="!my-2 enter-y" />
+      </div>
+      <div class="lg:w-6/10 w-full enter-y">
+        <AssociationIntroduction :loading="loading" class="!my-2 enter-y" />
       </div>
-      <div class="association-intro"> 协会介绍 </div>
     </div>
-    <div class="content">
-      <div class="left-content">
-        <div class="head-title">会员概况</div>
+    <div class="lg:flex">
+      <div class="lg:w-3/10 w-full !mr-4 enter-y">
+        <MemberSurvey :loading="loading" class="!my-2 enter-y" />
       </div>
-      <div class="right-content">
-        <div class="head-title">近期公告</div>
+      <div class="lg:w-7/10 w-full enter-y">
+        <RecentAnnouncement :loading="loading" class="!my-2 enter-y" />
       </div>
     </div>
-  </div>
+  </PageWrapper>
 </template>
 <script lang="ts">
-  import { defineComponent } from 'vue';
+  import { defineComponent, ref } from 'vue';
+
+  import { PageWrapper } from '/@/components/Page';
+  import WorkbenchHeader from './components/WorkbenchHeader.vue';
+  import AssociationIntroduction from './components/AssociationIntroduction.vue';
+  import AssociationSurvey from './components/AssociationSurvey.vue';
+  import RecentAnnouncement from './components/RecentAnnouncement.vue';
+  import MemberSurvey from './components/MemberSurvey.vue';
+
   export default defineComponent({
-    name: 'Survey',
-    components: {},
+    components: {
+      PageWrapper,
+      WorkbenchHeader,
+      AssociationIntroduction,
+      AssociationSurvey,
+      RecentAnnouncement,
+      MemberSurvey,
+    },
     setup() {
-      return {};
+      const loading = ref(true);
+
+      setTimeout(() => {
+        loading.value = false;
+      }, 1500);
+
+      return {
+        loading,
+      };
     },
   });
 </script>
-<style scoped>
-  .head-title {
-    font-size: 20px;
-  }
-
-  .header-content {
-    display: flex;
-  }
-
-  .edit-text {
-    max-width: 350px;
-  }
-
-  .edit-text textarea {
-    width: 100%;
-    height: 300px;
-    padding: 10px;
-    outline: none;
-    resize: none;
-  }
-
-  .association-intro {
-    padding: 10px;
-    margin-left: 50px;
-    flex: 1;
-    border: 1px solid gray;
-  }
-
-  .content {
-    display: flex;
-    margin: 20px 0;
-  }
-
-  .left-content {
-    width: 250px;
-    height: 320px;
-    margin-right: 20px;
-    text-align: center;
-    border: 1px solid gray;
-  }
-
-  .right-content {
-    flex: 1;
-    padding: 15px;
-    border: 1px solid gray;
-  }
-  @media (max-width: 639px) {
-    .header-content,
-    .content {
-      display: block;
-      width: 90%;
-      margin: 0 auto;
-    }
-
-    .association-intro {
-      height: 320px;
-      padding: 10px;
-      margin: 10px auto;
-      margin-left: 0;
-      border: 1px solid gray;
-    }
-
-    .left-content,
-    .right-content {
-      width: 100%;
-      height: 320px;
-      margin: 20px 0;
-    }
-  }
-</style>

+ 78 - 5
src/views/home/user/index.vue

@@ -2,7 +2,20 @@
   <div class="wrap">
     <div class="title">个人资料</div>
     <div class="userinfo">
-      <CropperAvatar :uploadApi="uploadApi" :value="avatar" :showBtn="false" />
+      <CropperAvatar
+        id="pc-show"
+        :uploadApi="uploadApi"
+        :value="avatar"
+        :showBtn="false"
+        @change="handleChange"
+      />
+      <div id="phone-show">
+        <a-upload :showUploadList="false" :multiple="false" :before-upload="beforeUpload">
+          <div class="user-avatar">
+            <img :src="avatar" alt="" />
+          </div>
+        </a-upload>
+      </div>
       <div class="username">{{ user.username }}</div>
       <div class="email">{{ user.email }}</div>
     </div>
@@ -45,8 +58,8 @@
   import { useUserStore } from '/@/store/modules/user';
   import { useGlobSetting } from '/@/hooks/setting';
   import { Upload } from 'ant-design-vue';
-  import { uploadApi } from '/@/api/sys/upload';
   import { CropperAvatar } from '/@/components/Cropper';
+  import { uploadApi } from '/@/api/sys/upload';
 
   export default defineComponent({
     name: 'User',
@@ -54,7 +67,6 @@
     setup() {
       const userStore = useUserStore();
       const { imgUrlPrefix } = useGlobSetting();
-      console.log(`userStore`, userStore);
       const { createMessage } = useMessage();
       const { success, error } = createMessage;
       const userinfo = reactive({
@@ -73,7 +85,6 @@
       init(); // 初始化获取信息
       function init() {
         getMyInfo().then((res) => {
-          console.log(`res`, res);
           const data = res.row;
           userinfo.username = data.username;
           userinfo.nickname = data.nickname;
@@ -87,6 +98,34 @@
           userStore.setUserInfo(data);
         });
       }
+      function beforeUpload(file) {
+        uploadApi({ file })
+          .then((res: any) => {
+            editMyInfo({ avatar: res.data.result.url })
+              .then(() => {
+                success('修改成功!');
+                init();
+              })
+              .catch((err) => {
+                error(err);
+              });
+          })
+          .catch((err) => {
+            error('头像修改失败');
+            console.log(`err`, err);
+          });
+        return false;
+      }
+      function handleChange(e) {
+        editMyInfo({ avatar: e.data.result.url })
+          .then(() => {
+            success('修改成功!');
+            init();
+          })
+          .catch((err) => {
+            error(err);
+          });
+      }
       function reset() {
         userinfo.username = user.username;
         userinfo.nickname = user.nickname;
@@ -114,6 +153,8 @@
       return {
         reset,
         uploadApi,
+        handleChange,
+        beforeUpload,
         submit,
         user,
         ...toRefs(userinfo),
@@ -137,6 +178,31 @@
     text-align: center;
   }
 
+  #pc-show {
+    display: block;
+  }
+
+  #phone-show {
+    display: none;
+  }
+
+  .user-avatar {
+    width: 152px;
+    height: 152px;
+    padding: 4px;
+    margin: 0 auto;
+    border: 2px solid #d2d6de;
+    border-radius: 50%;
+    box-sizing: border-box;
+  }
+
+  .user-avatar img {
+    width: 140px;
+    height: 140px;
+    margin: 0 auto;
+    border-radius: 50%;
+  }
+
   .username {
     padding-top: 2px;
     font-size: 25px;
@@ -182,7 +248,14 @@
   .btn-warp button {
     margin: 0 2px;
   }
-  @media (max-width: 400px) {
+  @media (max-width: 430px) {
+    #pc-show {
+      display: none;
+    }
+
+    #phone-show {
+      display: block;
+    }
     .form-item input {
       width: 95%;
     }

+ 43 - 0
src/views/test/analysis/components/GrowCard.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="md:flex">
+    <template v-for="(item, index) in growCardList" :key="item.title">
+      <Card
+        size="small"
+        :loading="$attrs.loading"
+        :title="item.title"
+        class="md:w-1/4 w-full !md:mt-0 !mt-4"
+        :class="[index + 1 < 4 && '!md:mr-4']"
+        :canExpan="false"
+      >
+        <template #extra>
+          <Tag :color="item.color">{{ item.action }}</Tag>
+        </template>
+
+        <div class="py-4 px-4 flex justify-between">
+          <CountTo prefix="$" :startVal="1" :endVal="item.value" class="text-2xl" />
+          <Icon :icon="item.icon" :size="40" />
+        </div>
+
+        <div class="p-2 px-4 flex justify-between">
+          <span>总{{ item.title }}</span>
+          <CountTo prefix="$" :startVal="1" :endVal="item.total" />
+        </div>
+      </Card>
+    </template>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  import { CountTo } from '/@/components/CountTo/index';
+  import { Icon } from '/@/components/Icon';
+  import { Tag, Card } from 'ant-design-vue';
+
+  import { growCardList } from '../data';
+  export default defineComponent({
+    components: { CountTo, Tag, Card, Icon },
+    setup() {
+      return { growCardList };
+    },
+  });
+</script>

+ 69 - 0
src/views/test/analysis/components/SalesProductPie.vue

@@ -0,0 +1,69 @@
+<template>
+  <Card title="成交占比" :loading="loading">
+    <div ref="chartRef" :style="{ width, height }"></div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent, Ref, ref, watch } from 'vue';
+
+  import { Card } from 'ant-design-vue';
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  export default defineComponent({
+    components: { Card },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup(props) {
+      const chartRef = ref<HTMLDivElement | null>(null);
+      const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+      watch(
+        () => props.loading,
+        () => {
+          if (props.loading) {
+            return;
+          }
+          setOptions({
+            tooltip: {
+              trigger: 'item',
+            },
+
+            series: [
+              {
+                name: '访问来源',
+                type: 'pie',
+                radius: '80%',
+                center: ['50%', '50%'],
+                color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
+                data: [
+                  { value: 500, name: '电子产品' },
+                  { value: 310, name: '服装' },
+                  { value: 274, name: '化妆品' },
+                  { value: 400, name: '家居' },
+                ].sort(function (a, b) {
+                  return a.value - b.value;
+                }),
+                roseType: 'radius',
+                animationType: 'scale',
+                animationEasing: 'exponentialInOut',
+                animationDelay: function () {
+                  return Math.random() * 400;
+                },
+              },
+            ],
+          });
+        },
+        { immediate: true }
+      );
+      return { chartRef };
+    },
+  });
+</script>

+ 46 - 0
src/views/test/analysis/components/SiteAnalysis.vue

@@ -0,0 +1,46 @@
+<template>
+  <Card
+    :tab-list="tabListTitle"
+    v-bind="$attrs"
+    :active-tab-key="activeKey"
+    @tabChange="onTabChange"
+  >
+    <p v-if="activeKey === 'tab1'">
+      <VisitAnalysis />
+    </p>
+    <p v-if="activeKey === 'tab2'">
+      <VisitAnalysisBar />
+    </p>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+
+  import { Card } from 'ant-design-vue';
+
+  import VisitAnalysis from './VisitAnalysis.vue';
+  import VisitAnalysisBar from './VisitAnalysisBar.vue';
+
+  export default defineComponent({
+    components: { Card, VisitAnalysis, VisitAnalysisBar },
+    setup() {
+      const activeKey = ref('tab1');
+
+      const tabListTitle = [
+        {
+          key: 'tab1',
+          tab: '流量趋势',
+        },
+        {
+          key: 'tab2',
+          tab: '访问量',
+        },
+      ];
+
+      function onTabChange(key) {
+        activeKey.value = key;
+      }
+      return { tabListTitle, activeKey, onTabChange };
+    },
+  });
+</script>

+ 110 - 0
src/views/test/analysis/components/VisitAnalysis.vue

@@ -0,0 +1,110 @@
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { defineComponent, onMounted, ref, Ref } from 'vue';
+
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  import { basicProps } from './props';
+  export default defineComponent({
+    props: basicProps,
+    setup() {
+      const chartRef = ref<HTMLDivElement | null>(null);
+      const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+      onMounted(() => {
+        setOptions({
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              lineStyle: {
+                width: 1,
+                color: '#019680',
+              },
+            },
+          },
+          xAxis: {
+            type: 'category',
+            boundaryGap: false,
+            data: [
+              '6:00',
+              '7:00',
+              '8:00',
+              '9: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',
+            ],
+            splitLine: {
+              show: true,
+              lineStyle: {
+                width: 1,
+                type: 'solid',
+                color: 'rgba(226,226,226,0.5)',
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+          },
+          yAxis: [
+            {
+              type: 'value',
+              max: 80000,
+              splitNumber: 4,
+              axisTick: {
+                show: false,
+              },
+              splitArea: {
+                show: true,
+                areaStyle: {
+                  color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
+                },
+              },
+            },
+          ],
+          grid: { left: '1%', right: '1%', top: '2  %', bottom: 0, containLabel: true },
+          series: [
+            {
+              smooth: true,
+              data: [
+                111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444,
+                22222, 11111, 4000, 2000, 500, 333, 222, 111,
+              ],
+              type: 'line',
+              areaStyle: {},
+              itemStyle: {
+                color: '#5ab1ef',
+              },
+            },
+            {
+              smooth: true,
+              data: [
+                33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201,
+                390, 198, 60, 30, 22, 11,
+              ],
+              type: 'line',
+              areaStyle: {},
+              itemStyle: {
+                color: '#019680',
+              },
+            },
+          ],
+        });
+      });
+      return { chartRef };
+    },
+  });
+</script>

+ 62 - 0
src/views/test/analysis/components/VisitAnalysisBar.vue

@@ -0,0 +1,62 @@
+<template>
+  <div ref="chartRef" :style="{ height, width }"></div>
+</template>
+<script lang="ts">
+  import { defineComponent, onMounted, ref, Ref } from 'vue';
+
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  import { basicProps } from './props';
+  export default defineComponent({
+    props: basicProps,
+    setup() {
+      const chartRef = ref<HTMLDivElement | null>(null);
+      const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+
+      onMounted(() => {
+        setOptions({
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              lineStyle: {
+                width: 1,
+                color: '#019680',
+              },
+            },
+          },
+          grid: { left: '1%', right: '1%', top: '2  %', bottom: 0, containLabel: true },
+          xAxis: {
+            type: 'category',
+            data: [
+              '1月',
+              '2月',
+              '3月',
+              '4月',
+              '5月',
+              '6月',
+              '7月',
+              '8月',
+              '9月',
+              '10月',
+              '11月',
+              '12月',
+            ],
+          },
+          yAxis: {
+            type: 'value',
+            max: 8000,
+            splitNumber: 4,
+          },
+          series: [
+            {
+              data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800],
+              type: 'bar',
+              barMaxWidth: 80,
+            },
+          ],
+        });
+      });
+      return { chartRef };
+    },
+  });
+</script>

+ 106 - 0
src/views/test/analysis/components/VisitRadar.vue

@@ -0,0 +1,106 @@
+<template>
+  <Card title="转化率" :loading="loading">
+    <div ref="chartRef" :style="{ width, height }"></div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent, Ref, ref, watch } from 'vue';
+
+  import { Card } from 'ant-design-vue';
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  export default defineComponent({
+    components: { Card },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup(props) {
+      const chartRef = ref<HTMLDivElement | null>(null);
+      const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+      watch(
+        () => props.loading,
+        () => {
+          if (props.loading) {
+            return;
+          }
+          setOptions({
+            legend: {
+              bottom: 0,
+              data: ['访问', '购买'],
+            },
+            tooltip: {},
+            radar: {
+              radius: '60%',
+              splitNumber: 8,
+              indicator: [
+                {
+                  text: '电脑',
+                  max: 100,
+                },
+                {
+                  text: '充电器',
+                  max: 100,
+                },
+                {
+                  text: '耳机',
+                  max: 100,
+                },
+                {
+                  text: '手机',
+                  max: 100,
+                },
+                {
+                  text: 'Ipad',
+                  max: 100,
+                },
+                {
+                  text: '耳机',
+                  max: 100,
+                },
+              ],
+            },
+            series: [
+              {
+                type: 'radar',
+                symbolSize: 0,
+                areaStyle: {
+                  shadowBlur: 0,
+                  shadowColor: 'rgba(0,0,0,.2)',
+                  shadowOffsetX: 0,
+                  shadowOffsetY: 10,
+                  opacity: 1,
+                },
+                data: [
+                  {
+                    value: [90, 50, 86, 40, 50, 20],
+                    name: '访问',
+                    itemStyle: {
+                      color: '#b6a2de',
+                    },
+                  },
+                  {
+                    value: [70, 75, 70, 76, 20, 85],
+                    name: '购买',
+                    itemStyle: {
+                      color: '#5ab1ef',
+                    },
+                  },
+                ],
+              },
+            ],
+          });
+        },
+        { immediate: true }
+      );
+      return { chartRef };
+    },
+  });
+</script>

+ 88 - 0
src/views/test/analysis/components/VisitSource.vue

@@ -0,0 +1,88 @@
+<template>
+  <Card title="访问来源" :loading="loading">
+    <div ref="chartRef" :style="{ width, height }"></div>
+  </Card>
+</template>
+<script lang="ts">
+  import { defineComponent, Ref, ref, watch } from 'vue';
+
+  import { Card } from 'ant-design-vue';
+  import { useECharts } from '/@/hooks/web/useECharts';
+
+  export default defineComponent({
+    components: { Card },
+    props: {
+      loading: Boolean,
+      width: {
+        type: String as PropType<string>,
+        default: '100%',
+      },
+      height: {
+        type: String as PropType<string>,
+        default: '300px',
+      },
+    },
+    setup(props) {
+      const chartRef = ref<HTMLDivElement | null>(null);
+      const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
+      watch(
+        () => props.loading,
+        () => {
+          if (props.loading) {
+            return;
+          }
+          setOptions({
+            tooltip: {
+              trigger: 'item',
+            },
+            legend: {
+              bottom: '1%',
+              left: 'center',
+            },
+            series: [
+              {
+                color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
+                name: '访问来源',
+                type: 'pie',
+                radius: ['40%', '70%'],
+                avoidLabelOverlap: false,
+                itemStyle: {
+                  borderRadius: 10,
+                  borderColor: '#fff',
+                  borderWidth: 2,
+                },
+                label: {
+                  show: false,
+                  position: 'center',
+                },
+                emphasis: {
+                  label: {
+                    show: true,
+                    fontSize: '12',
+                    fontWeight: 'bold',
+                  },
+                },
+                labelLine: {
+                  show: false,
+                },
+                data: [
+                  { value: 1048, name: '搜索引擎' },
+                  { value: 735, name: '直接访问' },
+                  { value: 580, name: '邮件营销' },
+                  { value: 484, name: '联盟广告' },
+                ],
+                animationType: 'scale',
+                animationEasing: 'exponentialInOut',
+                animationDelay: function () {
+                  return Math.random() * 100;
+                },
+              },
+            ],
+          });
+        },
+        { immediate: true }
+      );
+      return { chartRef };
+    },
+  });
+</script>

+ 16 - 0
src/views/test/analysis/components/props.ts

@@ -0,0 +1,16 @@
+import { PropType } from 'vue';
+
+export interface BasicProps {
+  width: string;
+  height: string;
+}
+export const basicProps = {
+  width: {
+    type: String as PropType<string>,
+    default: '100%',
+  },
+  height: {
+    type: String as PropType<string>,
+    default: '280px',
+  },
+};

+ 43 - 0
src/views/test/analysis/data.ts

@@ -0,0 +1,43 @@
+export interface GrowCardItem {
+  icon: string;
+  title: string;
+  value: number;
+  total: number;
+  color: string;
+  action: string;
+}
+
+export const growCardList: GrowCardItem[] = [
+  {
+    title: '访问数',
+    icon: 'visit-count|svg',
+    value: 2000,
+    total: 120000,
+    color: 'green',
+    action: '月',
+  },
+  {
+    title: '成交额',
+    icon: 'total-sales|svg',
+    value: 20000,
+    total: 500000,
+    color: 'blue',
+    action: '月',
+  },
+  {
+    title: '下载数',
+    icon: 'download-count|svg',
+    value: 8000,
+    total: 120000,
+    color: 'orange',
+    action: '周',
+  },
+  {
+    title: '成交数',
+    icon: 'transaction|svg',
+    value: 5000,
+    total: 50000,
+    color: 'purple',
+    action: '年',
+  },
+];

+ 39 - 0
src/views/test/analysis/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="p-4">
+    <GrowCard :loading="loading" class="enter-y" />
+    <SiteAnalysis class="!my-4 enter-y" :loading="loading" />
+
+    <div class="md:flex enter-y">
+      <VisitRadar class="md:w-1/3 w-full" :loading="loading" />
+
+      <VisitSource class="md:w-1/3 !md:mx-4 !md:my-0 !my-4 w-full" :loading="loading" />
+      <SalesProductPie class="md:w-1/3 w-full" :loading="loading" />
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import GrowCard from './components/GrowCard.vue';
+  import SiteAnalysis from './components/SiteAnalysis.vue';
+  import VisitSource from './components/VisitSource.vue';
+  import VisitRadar from './components/VisitRadar.vue';
+  import SalesProductPie from './components/SalesProductPie.vue';
+
+  export default defineComponent({
+    components: {
+      GrowCard,
+      SiteAnalysis,
+      VisitRadar,
+      VisitSource,
+      SalesProductPie,
+    },
+    setup() {
+      const loading = ref(true);
+
+      setTimeout(() => {
+        loading.value = false;
+      }, 1500);
+      return { loading };
+    },
+  });
+</script>

+ 432 - 0
src/views/test/house/index.css

@@ -0,0 +1,432 @@
+.house-wrap {
+  position: relative;
+  width: 600px;
+  height: 600px;
+  transform: scale(0.5);
+}
+.house-wrap .house {
+  position: absolute;
+  position: relative;
+  top: 50%;
+  left: 50%;
+  display: flex;
+  width: 400px;
+  height: 300px;
+  transform: translateX(-50%) translateY(-13%);
+  justify-content: center;
+  perspective: 200px;
+}
+.house-wrap .floor {
+  position: absolute;
+  bottom: 0;
+  display: flex;
+  width: 95%;
+  height: 30px;
+  background-color: #e1f6fd;
+  border: 4px solid #314b70;
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+  box-shadow: inset 4px 4px 0 #fffdff;
+  justify-content: center;
+}
+.house-wrap .floor::before,
+.house-wrap .floor::after {
+  position: absolute;
+  bottom: 0;
+  width: 32%;
+  height: 60%;
+  background-image: linear-gradient(to bottom, #e0f5fc 50%, #aac4d0 50%);
+  border-top: 4px solid #314b70;
+  border-right: 4px solid #314b70;
+  border-left: 4px solid #314b70;
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+  content: '';
+  box-shadow: 4px 0 0 #aac4d0;
+}
+.house-wrap .floor::after {
+  top: 0;
+  width: 25%;
+  height: 40%;
+  border-top: none;
+  border-top-right-radius: 0;
+  border-top-left-radius: 0;
+}
+.house-wrap .wall {
+  position: absolute;
+  bottom: 30px;
+  display: flex;
+  width: 91%;
+  height: 175px;
+  overflow: hidden;
+  background: #c3e0e7;
+  border-right: 4px solid #314b70;
+  border-left: 4px solid #314b70;
+  justify-content: space-between;
+  align-items: flex-end;
+}
+.house-wrap .window {
+  position: relative;
+  width: 34%;
+  height: 125px;
+  background: #aac4d0;
+  border-top: 4px solid #314b70;
+  border-right: 4px solid #314b70;
+  border-bottom: none;
+  border-left: none;
+  border-top-right-radius: 8px;
+  box-shadow: inset 0 4px 2px #e0f5fc;
+}
+.house-wrap .window::before {
+  position: absolute;
+  top: 6%;
+  left: 0;
+  width: 94%;
+  height: 88%;
+  background-image: linear-gradient(to top, #f3f6fa 47%, #9ab2d3 47%, #9ab2d3 50%, #f3f6fa 50%);
+  border-top: 4px solid #314b70;
+  border-right: 4px solid #314b70;
+  border-bottom: 4px solid #314b70;
+  border-left: none;
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+  content: '';
+}
+.house-wrap .window::after {
+  position: absolute;
+  top: 19%;
+  left: 20%;
+  width: 30px;
+  height: 40px;
+  background-color: #f9aabe;
+  border: 4px solid #9ab2d3;
+  content: '';
+}
+.house-wrap .window:nth-of-type(3) {
+  border-top: none;
+  border-right: 4px solid #314b70;
+  border-bottom: 4px solid #314b70;
+  border-left: none;
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 8px;
+  transform: rotateZ(180deg);
+  box-shadow: inset 0 -4px 2px #e0f5fc;
+}
+.house-wrap .window:nth-of-type(3)::after {
+  content: none;
+}
+.house-wrap .door {
+  display: flex;
+  width: 20%;
+  height: 130px;
+  padding-left: 8px;
+  background-color: #ffc26b;
+  border: 4px solid #314b70;
+  border-bottom: none;
+  border-top-right-radius: 10px;
+  border-top-left-radius: 10px;
+  box-shadow: inset 3px 3px #ffe0ad, inset -10px -8px #ffad61, 4px 0 #aac4d0;
+  flex-direction: column;
+  justify-content: space-evenly;
+  align-items: flex-start;
+}
+.house-wrap .door__square {
+  width: 85%;
+  height: 47px;
+  border: 4px solid #314b70;
+  border-radius: 4px;
+  box-shadow: inset 3px 3px #ffe0ad;
+}
+.house-wrap .door__line {
+  width: 25%;
+  height: 4px;
+  background: #314b70;
+  border-radius: 4px;
+}
+.house-wrap .top {
+  position: absolute;
+  width: 82%;
+  height: 30px;
+  background-color: #aac4d0;
+  border: 4px solid #314b70;
+  border-top-right-radius: 4px;
+  border-top-left-radius: 4px;
+  box-shadow: inset 4px 4px 0 #e1f6fd;
+}
+.house-wrap .circle {
+  position: absolute;
+  top: -10%;
+  display: flex;
+  width: 115px;
+  height: 115px;
+  background-color: #e0f5fc;
+  border: 4px solid #314b70;
+  border-radius: 50%;
+  content: '';
+  box-shadow: inset 4px 4px 0 #fffdff, inset 4px -4px 0 #fffdff, inset -4px 4px 0 #fffdff, inset -4px -4px 0 #fffdff;
+  justify-content: center;
+  align-items: center;
+}
+.house-wrap .circle::before,
+.house-wrap .circle::after {
+  position: absolute;
+  top: 35%;
+  width: 70%;
+  height: 4px;
+  background-color: #314b70;
+  content: '';
+}
+.house-wrap .circle::after {
+  top: 20%;
+  width: 35%;
+}
+.house-wrap .plastic {
+  position: absolute;
+  top: 30%;
+  z-index: 100;
+  width: 100%;
+  height: 30px;
+  overflow: hidden;
+}
+.house-wrap .plastic__g {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  overflow: hidden;
+  transform: translateY(-22px);
+}
+.house-wrap .plastic__item {
+  width: 43px;
+  height: 43px;
+  margin-bottom: 4px;
+  border: 3px solid #314b70;
+  border-radius: 50%;
+  box-shadow: 0 4px 0 #aac4d0;
+}
+.house-wrap .plastic__item:nth-child(odd) {
+  background: #0792d9;
+  box-shadow: 0 4px 0 #aac4d0, inset 4px 4px 0 #66c8fa;
+}
+.house-wrap .plastic__item:nth-child(even) {
+  background: #fffdff;
+}
+.house-wrap .plastic__item:nth-of-type(1),
+.house-wrap .plastic__item:nth-last-of-type(1) {
+  width: 45px;
+  height: 45px;
+  box-shadow: none;
+  box-shadow: inset 4px 4px 0 #66c8fa;
+}
+.house-wrap .plastic__item:nth-of-type(5) {
+  width: 45px;
+  height: 45px;
+}
+.house-wrap .line {
+  position: absolute;
+  top: 15px;
+  display: flex;
+  width: 90%;
+  height: 85px;
+  background-color: #e1f6fd;
+  border-right: 4px solid #314b70;
+  border-bottom: 4px solid #314b70;
+  border-left: 4px solid #314b70;
+  border-radius: 4px;
+  transform: rotateX(25deg);
+  transform-style: preserve-3d;
+}
+.house-wrap .line__item {
+  height: 100%;
+  flex-grow: 1;
+  border-right: 4px solid #314b70;
+}
+.house-wrap .line__item:nth-child(odd) {
+  background: #00affa;
+  box-shadow: inset 4px 4px 0 #66c8fa;
+}
+.house-wrap .line__item:nth-child(even) {
+  background: #fffdff;
+}
+.house-wrap .line__item:nth-last-of-type(1) {
+  border-right: none;
+}
+.house-wrap .line__item:nth-child(4),
+.house-wrap .line__item:nth-child(5),
+.house-wrap .line__item:nth-child(6) {
+  border-top: 6px solid #314b70;
+}
+.house-wrap .tree {
+  position: absolute;
+  bottom: 19%;
+  left: 10%;
+  display: flex;
+  width: 100px;
+  height: 165px;
+  background-color: #00d398;
+  border: 4px solid #314b70;
+  border-radius: 50px;
+  box-shadow: inset 4px 0 0 #77e4c6, inset -4px 0 0 #00a073;
+  animation: tree 1s linear alternate infinite;
+  justify-content: center;
+  transform-origin: 0% 100%;
+}
+.house-wrap .tree__item {
+  position: absolute;
+  bottom: -80px;
+  width: 4px;
+  height: 140px;
+  background: #314b70;
+}
+.house-wrap .tree__item:nth-of-type(2) {
+  bottom: 80px;
+  height: 40px;
+  border-radius: 20px;
+  box-shadow: 0 0 0 8px #77e4c6;
+}
+.house-wrap .tree__item:nth-of-type(2)::before {
+  position: absolute;
+  bottom: -45px;
+  left: -30px;
+  width: 20px;
+  height: 35px;
+  background-color: #77e4c6;
+  border-radius: 15px;
+  content: '';
+}
+.house-wrap .tree__item:nth-of-type(3) {
+  bottom: 20px;
+  left: 36%;
+  width: 4px;
+  height: 30px;
+  background-color: #314b70;
+  transform: rotateZ(-45deg);
+}
+.house-wrap .dot {
+  position: absolute;
+  bottom: 38px;
+  width: 100%;
+  height: 4px;
+  background-image: linear-gradient(to right, #314b70 10%, transparent 10%, transparent 11%, #314b70 11%, #314b70 85%, transparent 85%, transparent 86%, #314b70 86%);
+}
+.house-wrap .bush__item {
+  position: absolute;
+  bottom: 40px;
+  left: 18%;
+  width: 80px;
+  height: 60px;
+  background-color: #00d398;
+  border: 1px solid red;
+  border: 4px solid #314b70;
+  border-bottom: none;
+  border-top-right-radius: 100px;
+  border-top-left-radius: 50px;
+  box-shadow: inset 4px 0 0 #77e4c6, inset -4px 0 0 #00a073;
+  animation: bush 2s alternate infinite;
+  transform-origin: bottom center;
+}
+.house-wrap .bush__item:nth-of-type(2) {
+  left: 13%;
+  width: 50px;
+  height: 40px;
+  border-top-right-radius: 10px;
+  border-top-left-radius: 50px;
+  animation: tree 2s alternate reverse infinite 0.5s;
+}
+.house-wrap .bush__item::before {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  width: 20px;
+  height: 20px;
+  background: #77e4c6;
+  border-radius: 50%;
+  content: '';
+}
+.house-wrap .cloud {
+  position: absolute;
+  top: 200px;
+  left: 60px;
+  display: flex;
+  justify-content: center;
+  width: 85px;
+  height: 20px;
+  border-bottom: 4px solid #e1e8f2;
+  animation: cloud 4s infinite alternate;
+}
+.house-wrap .cloud:nth-of-type(2) {
+  top: 150px;
+  left: 50%;
+  animation: cloud 4s infinite reverse alternate 0.5s;
+}
+.house-wrap .cloud:nth-of-type(3) {
+  top: 250px;
+  left: 80%;
+  animation: cloud 4s ease infinite alternate 0.75s;
+}
+.house-wrap .cloud__item {
+  position: relative;
+  border-top: 20px solid #e1e8f2;
+  border-right: 20px solid transparent;
+  border-bottom: 20px solid transparent;
+  border-left: 20px solid #e1e8f2;
+  border-radius: 50%;
+  transform: rotateZ(45deg);
+}
+.house-wrap .cloud__item:nth-of-type(2) {
+  margin-top: 5px;
+  margin-left: -7px;
+  border-top: 15px solid #e1e8f2;
+  border-right: 15px solid transparent;
+  border-bottom: 15px solid transparent;
+  border-left: 15px solid #e1e8f2;
+}
+.house-wrap .bird {
+  position: absolute;
+  right: 10%;
+  bottom: 40%;
+  z-index: -1;
+  width: 20px;
+  height: 20px;
+  border-top: 4px solid #becde2;
+  border-left: 4px solid #becde2;
+  transform: rotateZ(-135deg);
+  animation: bird 1s ease alternate infinite;
+}
+.house-wrap .bird:nth-of-type(2) {
+  right: 20%;
+  bottom: 30%;
+  width: 15px;
+  height: 15px;
+}
+@keyframes bird {
+  0% {
+    transform: scaleY(0.7) rotateZ(-135deg) translateX(0) translateY(0) skew(-10deg, -10deg);
+  }
+  100% {
+    transform: scaleY(1) rotateZ(-135deg) translateX(50%) translateY(50%) skew(-10deg, -10deg);
+  }
+}
+@keyframes tree {
+  0% {
+    transform: scaleY(1);
+  }
+  100% {
+    transform: scaleY(0.975);
+  }
+}
+@keyframes bush {
+  0% {
+    transform: skewX(-2deg);
+  }
+  100% {
+    transform: skewX(5deg);
+  }
+}
+@keyframes cloud {
+  0% {
+    transform: translateX(-10%);
+  }
+  100% {
+    transform: translateX(20%);
+  }
+}

+ 494 - 0
src/views/test/house/index.less

@@ -0,0 +1,494 @@
+.house-wrap {
+  position: relative;
+  width: 600px;
+  height: 600px;
+  transform: scale(0.5);
+
+  .house {
+    position: absolute;
+    position: relative;
+    top: 50%;
+    left: 50%;
+    display: flex;
+    width: 400px;
+    height: 300px;
+    transform: translateX(-50%) translateY(-13%);
+    justify-content: center;
+    perspective: 200px;
+  }
+
+  .floor {
+    position: absolute;
+    bottom: 0;
+    display: flex;
+    width: 95%;
+    height: 30px;
+    background-color: #e1f6fd;
+    border: 4px solid #314b70;
+    border-top-right-radius: 4px;
+    border-top-left-radius: 4px;
+    box-shadow: inset 4px 4px 0 #fffdff;
+    justify-content: center;
+  }
+
+  .floor::before,
+  .floor::after {
+    position: absolute;
+    bottom: 0;
+    width: 32%;
+    height: 60%;
+    background-image: linear-gradient(to bottom, #e0f5fc 50%, #aac4d0 50%);
+    border-top: 4px solid #314b70;
+    border-right: 4px solid #314b70;
+    border-left: 4px solid #314b70;
+    border-top-right-radius: 4px;
+    border-top-left-radius: 4px;
+    content: '';
+    box-shadow: 4px 0 0 #aac4d0;
+  }
+
+  .floor::after {
+    top: 0;
+    width: 25%;
+    height: 40%;
+    border-top: none;
+    border-top-right-radius: 0;
+    border-top-left-radius: 0;
+  }
+
+  .wall {
+    position: absolute;
+    bottom: 30px;
+    display: flex;
+    width: 91%;
+    height: 175px;
+    overflow: hidden;
+    background: #c3e0e7;
+    border-right: 4px solid #314b70;
+    border-left: 4px solid #314b70;
+    justify-content: space-between;
+    align-items: flex-end;
+  }
+
+  .window {
+    position: relative;
+    width: 34%;
+    height: 125px;
+    background: #aac4d0;
+    border-top: 4px solid #314b70;
+    border-right: 4px solid #314b70;
+    border-bottom: none;
+    border-left: none;
+    border-top-right-radius: 8px;
+    box-shadow: inset 0 4px 2px #e0f5fc;
+  }
+
+  .window::before {
+    position: absolute;
+    top: 6%;
+    left: 0;
+    width: 94%;
+    height: 88%;
+    background-image: linear-gradient(to top, #f3f6fa 47%, #9ab2d3 47%, #9ab2d3 50%, #f3f6fa 50%);
+    border-top: 4px solid #314b70;
+    border-right: 4px solid #314b70;
+    border-bottom: 4px solid #314b70;
+    border-left: none;
+    border-top-right-radius: 4px;
+    border-bottom-right-radius: 4px;
+    content: '';
+  }
+
+  .window::after {
+    position: absolute;
+    top: 19%;
+    left: 20%;
+    width: 30px;
+    height: 40px;
+    background-color: #f9aabe;
+    border: 4px solid #9ab2d3;
+    content: '';
+  }
+
+  .window:nth-of-type(3) {
+    border-top: none;
+    border-right: 4px solid #314b70;
+    border-bottom: 4px solid #314b70;
+    border-left: none;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 8px;
+    transform: rotateZ(180deg);
+    box-shadow: inset 0 -4px 2px #e0f5fc;
+  }
+
+  .window:nth-of-type(3)::after {
+    content: none;
+  }
+
+  .door {
+    display: flex;
+    width: 20%;
+    height: 130px;
+    padding-left: 8px;
+    background-color: #ffc26b;
+    border: 4px solid #314b70;
+    border-bottom: none;
+    border-top-right-radius: 10px;
+    border-top-left-radius: 10px;
+    box-shadow: inset 3px 3px #ffe0ad, inset -10px -8px #ffad61, 4px 0 #aac4d0;
+    flex-direction: column;
+    justify-content: space-evenly;
+    align-items: flex-start;
+  }
+
+  .door__square {
+    width: 85%;
+    height: 47px;
+    border: 4px solid #314b70;
+    border-radius: 4px;
+    box-shadow: inset 3px 3px #ffe0ad;
+  }
+
+  .door__line {
+    width: 25%;
+    height: 4px;
+    background: #314b70;
+    border-radius: 4px;
+  }
+
+  .top {
+    position: absolute;
+    width: 82%;
+    height: 30px;
+    background-color: #aac4d0;
+    border: 4px solid #314b70;
+    border-top-right-radius: 4px;
+    border-top-left-radius: 4px;
+    box-shadow: inset 4px 4px 0 #e1f6fd;
+  }
+
+  .circle {
+    position: absolute;
+    top: -10%;
+    display: flex;
+    width: 115px;
+    height: 115px;
+    background-color: #e0f5fc;
+    border: 4px solid #314b70;
+    border-radius: 50%;
+    content: '';
+    box-shadow: inset 4px 4px 0 #fffdff, inset 4px -4px 0 #fffdff, inset -4px 4px 0 #fffdff,
+      inset -4px -4px 0 #fffdff;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .circle::before,
+  .circle::after {
+    position: absolute;
+    top: 35%;
+    width: 70%;
+    height: 4px;
+    background-color: #314b70;
+    content: '';
+  }
+
+  .circle::after {
+    top: 20%;
+    width: 35%;
+  }
+
+  .plastic {
+    position: absolute;
+    top: 30%;
+    z-index: 100;
+    width: 100%;
+    height: 30px;
+    overflow: hidden;
+  }
+
+  .plastic__g {
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    overflow: hidden;
+    transform: translateY(-22px);
+  }
+
+  .plastic__item {
+    width: 43px;
+    height: 43px;
+    margin-bottom: 4px;
+    border: 3px solid #314b70;
+    border-radius: 50%;
+    box-shadow: 0 4px 0 #aac4d0;
+  }
+
+  .plastic__item:nth-child(odd) {
+    background: #0792d9;
+    box-shadow: 0 4px 0 #aac4d0, inset 4px 4px 0 #66c8fa;
+  }
+
+  .plastic__item:nth-child(even) {
+    background: #fffdff;
+  }
+
+  .plastic__item:nth-of-type(1),
+  .plastic__item:nth-last-of-type(1) {
+    width: 45px;
+    height: 45px;
+    box-shadow: none;
+    box-shadow: inset 4px 4px 0 #66c8fa;
+  }
+
+  .plastic__item:nth-of-type(5) {
+    width: 45px;
+    height: 45px;
+  }
+
+  .line {
+    position: absolute;
+    top: 15px;
+    display: flex;
+    width: 90%;
+    height: 85px;
+    background-color: #e1f6fd;
+    border-right: 4px solid #314b70;
+    border-bottom: 4px solid #314b70;
+    border-left: 4px solid #314b70;
+    border-radius: 4px;
+    transform: rotateX(25deg);
+    transform-style: preserve-3d;
+  }
+
+  .line__item {
+    height: 100%;
+    flex-grow: 1;
+    border-right: 4px solid #314b70;
+  }
+
+  .line__item:nth-child(odd) {
+    background: #00affa;
+    box-shadow: inset 4px 4px 0 #66c8fa;
+  }
+
+  .line__item:nth-child(even) {
+    background: #fffdff;
+  }
+
+  .line__item:nth-last-of-type(1) {
+    border-right: none;
+  }
+
+  .line__item:nth-child(4),
+  .line__item:nth-child(5),
+  .line__item:nth-child(6) {
+    border-top: 6px solid #314b70;
+  }
+
+  .tree {
+    position: absolute;
+    bottom: 19%;
+    left: 10%;
+    display: flex;
+    width: 100px;
+    height: 165px;
+    background-color: #00d398;
+    border: 4px solid #314b70;
+    border-radius: 50px;
+    box-shadow: inset 4px 0 0 #77e4c6, inset -4px 0 0 #00a073;
+    animation: tree 1s linear alternate infinite;
+    justify-content: center;
+    transform-origin: 0% 100%;
+  }
+
+  .tree__item {
+    position: absolute;
+    bottom: -80px;
+    width: 4px;
+    height: 140px;
+    background: #314b70;
+  }
+
+  .tree__item:nth-of-type(2) {
+    bottom: 80px;
+    height: 40px;
+    border-radius: 20px;
+    box-shadow: 0 0 0 8px #77e4c6;
+  }
+
+  .tree__item:nth-of-type(2)::before {
+    position: absolute;
+    bottom: -45px;
+    left: -30px;
+    width: 20px;
+    height: 35px;
+    background-color: #77e4c6;
+    border-radius: 15px;
+    content: '';
+  }
+
+  .tree__item:nth-of-type(3) {
+    bottom: 20px;
+    left: 36%;
+    width: 4px;
+    height: 30px;
+    background-color: #314b70;
+    transform: rotateZ(-45deg);
+  }
+
+  .dot {
+    position: absolute;
+    bottom: 38px;
+    width: 100%;
+    height: 4px;
+    background-image: linear-gradient(
+      to right,
+      #314b70 10%,
+      transparent 10%,
+      transparent 11%,
+      #314b70 11%,
+      #314b70 85%,
+      transparent 85%,
+      transparent 86%,
+      #314b70 86%
+    );
+  }
+
+  .bush__item {
+    position: absolute;
+    bottom: 40px;
+    left: 18%;
+    width: 80px;
+    height: 60px;
+    background-color: #00d398;
+    border: 1px solid red;
+    border: 4px solid #314b70;
+    border-bottom: none;
+    border-top-right-radius: 100px;
+    border-top-left-radius: 50px;
+    box-shadow: inset 4px 0 0 #77e4c6, inset -4px 0 0 #00a073;
+    animation: bush 2s alternate infinite;
+    transform-origin: bottom center;
+  }
+
+  .bush__item:nth-of-type(2) {
+    left: 13%;
+    width: 50px;
+    height: 40px;
+    border-top-right-radius: 10px;
+    border-top-left-radius: 50px;
+    animation: tree 2s alternate reverse infinite 0.5s;
+  }
+
+  .bush__item::before {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    width: 20px;
+    height: 20px;
+    background: #77e4c6;
+    border-radius: 50%;
+    content: '';
+  }
+
+  .cloud {
+    position: absolute;
+    top: 200px;
+    left: 60px;
+    display: flex;
+    justify-content: center;
+    width: 85px;
+    height: 20px;
+    border-bottom: 4px solid #e1e8f2;
+    animation: cloud 4s infinite alternate;
+  }
+
+  .cloud:nth-of-type(2) {
+    top: 150px;
+    left: 50%;
+    animation: cloud 4s infinite reverse alternate 0.5s;
+  }
+
+  .cloud:nth-of-type(3) {
+    top: 250px;
+    left: 80%;
+    animation: cloud 4s ease infinite alternate 0.75s;
+  }
+
+  .cloud__item {
+    position: relative;
+    border-top: 20px solid #e1e8f2;
+    border-right: 20px solid transparent;
+    border-bottom: 20px solid transparent;
+    border-left: 20px solid #e1e8f2;
+    border-radius: 50%;
+    transform: rotateZ(45deg);
+  }
+
+  .cloud__item:nth-of-type(2) {
+    margin-top: 5px;
+    margin-left: -7px;
+    border-top: 15px solid #e1e8f2;
+    border-right: 15px solid transparent;
+    border-bottom: 15px solid transparent;
+    border-left: 15px solid #e1e8f2;
+  }
+
+  .bird {
+    position: absolute;
+    right: 10%;
+    bottom: 40%;
+    z-index: -1;
+    width: 20px;
+    height: 20px;
+    border-top: 4px solid #becde2;
+    border-left: 4px solid #becde2;
+    transform: rotateZ(-135deg);
+    animation: bird 1s ease alternate infinite;
+  }
+
+  .bird:nth-of-type(2) {
+    right: 20%;
+    bottom: 30%;
+    width: 15px;
+    height: 15px;
+  }
+
+  @keyframes bird {
+    0% {
+      transform: scaleY(0.7) rotateZ(-135deg) translateX(0) translateY(0) skew(-10deg, -10deg);
+    }
+
+    100% {
+      transform: scaleY(1) rotateZ(-135deg) translateX(50%) translateY(50%) skew(-10deg, -10deg);
+    }
+  }
+  @keyframes tree {
+    0% {
+      transform: scaleY(1);
+    }
+
+    100% {
+      transform: scaleY(0.975);
+    }
+  }
+  @keyframes bush {
+    0% {
+      transform: skewX(-2deg);
+    }
+
+    100% {
+      transform: skewX(5deg);
+    }
+  }
+  @keyframes cloud {
+    0% {
+      transform: translateX(-10%);
+    }
+
+    100% {
+      transform: translateX(20%);
+    }
+  }
+}

+ 89 - 0
src/views/test/house/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="house-wrap">
+    <div class="house">
+      <div class="floor"></div>
+      <div class="wall">
+        <div class="window"></div>
+        <div class="door">
+          <div class="door__square"></div>
+          <div class="door__line"></div>
+          <div class="door__square"></div>
+        </div>
+        <div class="window"></div>
+      </div>
+      <div class="top"></div>
+      <div class="circle"></div>
+      <div class="plastic">
+        <div class="plastic__g">
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+          <div class="plastic__item"></div>
+        </div>
+      </div>
+      <div class="line">
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+        <div class="line__item"></div>
+      </div>
+    </div>
+    <div class="clouds">
+      <div class="cloud">
+        <div class="cloud__item"></div>
+        <div class="cloud__item"></div>
+      </div>
+      <div class="cloud">
+        <div class="cloud__item"></div>
+        <div class="cloud__item"></div>
+      </div>
+      <div class="cloud">
+        <div class="cloud__item"></div>
+        <div class="cloud__item"></div>
+      </div>
+      <div class="bird"></div>
+    </div>
+    <div class="birds">
+      <div class="bird"></div>
+      <div class="bird"></div>
+    </div>
+    <div class="tree">
+      <div class="tree__item"></div>
+      <div class="tree__item"></div>
+      <div class="tree__item"></div>
+    </div>
+    <div class="bush">
+      <div class="bush__item"></div>
+      <div class="bush__item"></div>
+    </div>
+    <div class="dot"></div>
+  </div>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import { defineComponent } from 'vue';
+  export default defineComponent({
+    name: 'House',
+    props: {
+      size: {
+        type: Number as PropType<number>,
+        default: 600,
+      },
+    },
+    setup() {
+      return {};
+    },
+  });
+</script>
+<style lang="less" scoped>
+  @import './index.less';
+</style>

+ 0 - 36
src/views/test/index.vue

@@ -1,36 +0,0 @@
-<template>
-  <PageWrapper title="后台权限示例" contentBackground contentClass="p-4">
-    <div class="mt-2">
-      当前权限模式:
-      <a-button type="link">
-        {{ permissionMode === PermissionModeEnum.BACK ? '后台权限模式' : '前端角色权限模式' }}
-      </a-button>
-      <a-button class="ml-4" @click="togglePermissionMode" type="primary"> 切换权限模式 </a-button>
-      <Divider />
-    </div>
-  </PageWrapper>
-</template>
-<script lang="ts">
-  import { defineComponent, computed } from 'vue';
-  import { useAppStore } from '/@/store/modules/app';
-  import { PermissionModeEnum } from '/@/enums/appEnum';
-  import { Divider } from 'ant-design-vue';
-  import { usePermission } from '/@/hooks/web/usePermission';
-  import { PageWrapper } from '/@/components/Page';
-
-  export default defineComponent({
-    name: 'CurrentPermissionMode',
-    components: { Divider, PageWrapper },
-    setup() {
-      const appStore = useAppStore();
-      const permissionMode = computed(() => appStore.getProjectConfig.permissionMode);
-      const { togglePermissionMode } = usePermission();
-
-      return {
-        permissionMode,
-        PermissionModeEnum,
-        togglePermissionMode,
-      };
-    },
-  });
-</script>

+ 22 - 0
src/views/test/welcome/index.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="welcome">
+    <House />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import House from '../house/index.vue';
+  export default defineComponent({
+    name: 'Welcome',
+    components: { House },
+  });
+</script>
+<style lang="less" scoped>
+  .welcome {
+    display: flex;
+    width: 100%;
+    height: 100%;
+    justify-content: center;
+    align-items: center;
+  }
+</style>

+ 0 - 0
src/views/dashboard/workbench/components/DynamicInfo.vue → src/views/test/workbench/components/DynamicInfo.vue


+ 0 - 0
src/views/dashboard/workbench/components/ProjectCard.vue → src/views/test/workbench/components/ProjectCard.vue


+ 0 - 0
src/views/dashboard/workbench/components/QuickNav.vue → src/views/test/workbench/components/QuickNav.vue


+ 0 - 0
src/views/dashboard/workbench/components/SaleRadar.vue → src/views/test/workbench/components/SaleRadar.vue


+ 0 - 0
src/views/dashboard/workbench/components/WorkbenchHeader.vue → src/views/test/workbench/components/WorkbenchHeader.vue


+ 156 - 0
src/views/test/workbench/components/data.ts

@@ -0,0 +1,156 @@
+interface GroupItem {
+  title: string;
+  icon: string;
+  color: string;
+  desc: string;
+  date: string;
+  group: string;
+}
+
+interface NavItem {
+  title: string;
+  icon: string;
+  color: string;
+}
+
+interface DynamicInfoItem {
+  avatar: string;
+  name: string;
+  date: string;
+  desc: string;
+}
+
+export const navItems: NavItem[] = [
+  {
+    title: '首页',
+    icon: 'ion:home-outline',
+    color: '#1fdaca',
+  },
+  {
+    title: '仪表盘',
+    icon: 'ion:grid-outline',
+    color: '#bf0c2c',
+  },
+  {
+    title: '组件',
+    icon: 'ion:layers-outline',
+    color: '#e18525',
+  },
+  {
+    title: '系统管理',
+    icon: 'ion:settings-outline',
+    color: '#3fb27f',
+  },
+  {
+    title: '权限管理',
+    icon: 'ion:key-outline',
+    color: '#4daf1bc9',
+  },
+  {
+    title: '图表',
+    icon: 'ion:bar-chart-outline',
+    color: '#00d8ff',
+  },
+];
+
+export const dynamicInfoItems: DynamicInfoItem[] = [
+  {
+    avatar: 'dynamic-avatar-1|svg',
+    name: '威廉',
+    date: '刚刚',
+    desc: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
+  },
+  {
+    avatar: 'dynamic-avatar-2|svg',
+    name: '艾文',
+    date: '1个小时前',
+    desc: `关注了 <a>威廉</a> `,
+  },
+  {
+    avatar: 'dynamic-avatar-3|svg',
+    name: '克里斯',
+    date: '1天前',
+    desc: `发布了 <a>个人动态</a> `,
+  },
+  {
+    avatar: 'dynamic-avatar-4|svg',
+    name: 'Vben',
+    date: '2天前',
+    desc: `发表文章 <a>如何编写一个Vite插件</a> `,
+  },
+  {
+    avatar: 'dynamic-avatar-5|svg',
+    name: '皮特',
+    date: '3天前',
+    desc: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
+  },
+  {
+    avatar: 'dynamic-avatar-6|svg',
+    name: '杰克',
+    date: '1周前',
+    desc: `关闭了问题 <a>如何运行项目</a> `,
+  },
+  {
+    avatar: 'dynamic-avatar-1|svg',
+    name: '威廉',
+    date: '1周前',
+    desc: `发布了 <a>个人动态</a> `,
+  },
+  {
+    avatar: 'dynamic-avatar-1|svg',
+    name: '威廉',
+    date: '2021-04-01 20:00',
+    desc: `推送了代码到 <a>Github</a>`,
+  },
+];
+
+export const groupItems: GroupItem[] = [
+  {
+    title: 'Github',
+    icon: 'carbon:logo-github',
+    color: '',
+    desc: '不要等待机会,而要创造机会。',
+    group: '开源组',
+    date: '2021-04-01',
+  },
+  {
+    title: 'Vue',
+    icon: 'ion:logo-vue',
+    color: '#3fb27f',
+    desc: '现在的你决定将来的你。',
+    group: '算法组',
+    date: '2021-04-01',
+  },
+  {
+    title: 'Html5',
+    icon: 'ion:logo-html5',
+    color: '#e18525',
+    desc: '没有什么才能比努力更重要。',
+    group: '上班摸鱼',
+    date: '2021-04-01',
+  },
+  {
+    title: 'Angular',
+    icon: 'ion:logo-angular',
+    color: '#bf0c2c',
+    desc: '热情和欲望可以突破一切难关。',
+    group: 'UI',
+    date: '2021-04-01',
+  },
+  {
+    title: 'React',
+    icon: 'bx:bxl-react',
+    color: '#00d8ff',
+    desc: '健康的身体是实目标的基石。',
+    group: '技术牛',
+    date: '2021-04-01',
+  },
+  {
+    title: 'Js',
+    icon: 'ion:logo-javascript',
+    color: '#4daf1bc9',
+    desc: '路是走出来的,而不是空想出来的。',
+    group: '架构组',
+    date: '2021-04-01',
+  },
+];

+ 0 - 0
src/views/dashboard/workbench/index.vue → src/views/test/workbench/index.vue