Browse Source

feat(Workplace): Add wrokplace demo

feat(Component): Add Highlight component

feat(hooks): Add useTimeAgo hook
kailong321200875 3 years ago
parent
commit
c53fa562e5

BIN
src/assets/imgs/avatar.jpg


+ 3 - 0
src/components/Highlight/index.ts

@@ -0,0 +1,3 @@
+import Highlight from './src/Highlight.vue'
+
+export { Highlight }

+ 65 - 0
src/components/Highlight/src/Highlight.vue

@@ -0,0 +1,65 @@
+<script lang="tsx">
+import { defineComponent, PropType, computed, h, unref } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+export default defineComponent({
+  name: 'Highlight',
+  props: {
+    tag: propTypes.string.def('span'),
+    keys: {
+      type: Array as PropType<string[]>,
+      default: () => []
+    },
+    color: propTypes.string.def('var(--el-color-primary)')
+  },
+  emits: ['click'],
+  setup(props, { emit, slots }) {
+    const keyNodes = computed(() => {
+      return props.keys.map((key) => {
+        return h(
+          'span',
+          {
+            onClick: () => {
+              emit('click', key)
+            },
+            style: {
+              color: props.color,
+              cursor: 'pointer'
+            }
+          },
+          key
+        )
+      })
+    })
+
+    const parseText = (text: string) => {
+      props.keys.forEach((key, index) => {
+        const regexp = new RegExp(key, 'g')
+        text = text.replace(regexp, `{{${index}}}`)
+      })
+      return text.split(/{{|}}/)
+    }
+
+    const renderText = () => {
+      if (!slots?.default) return null
+      const node = slots?.default()[0].children
+
+      if (!node) {
+        return slots?.default()[0]
+      }
+
+      const textArray = parseText(node as string)
+      const regexp = /^[0-9]*$/
+      const nodes = textArray.map((t) => {
+        if (regexp.test(t)) {
+          return unref(keyNodes)[Math.floor(Number(t))] || t
+        }
+        return t
+      })
+      return h(props.tag, nodes)
+    }
+
+    return () => renderText()
+  }
+})
+</script>

+ 5 - 1
src/components/UserInfo/src/UserInfo.vue

@@ -38,7 +38,11 @@ const loginOut = () => {
 <template>
   <ElDropdown :class="prefixCls" trigger="click">
     <div class="flex items-center">
-      <img src="@/assets/imgs/avatar.png" alt="" class="w-[calc(var(--tags-view-height)-10px)]" />
+      <img
+        src="@/assets/imgs/avatar.jpg"
+        alt=""
+        class="w-[calc(var(--tags-view-height)-10px)] rounded-[50%]"
+      />
       <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">Archer</span>
     </div>
     <template #dropdown>

+ 48 - 0
src/hooks/web/useTimeAgo.ts

@@ -0,0 +1,48 @@
+import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core'
+import { computed, unref } from 'vue'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+
+const TIME_AGO_MESSAGE_MAP: {
+  'zh-CN': UseTimeAgoMessages
+  en: UseTimeAgoMessages
+} = {
+  'zh-CN': {
+    justNow: '刚刚',
+    past: (n) => (n.match(/\d/) ? `${n}前` : n),
+    future: (n) => (n.match(/\d/) ? `${n}后` : n),
+    month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`),
+    year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`),
+    day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`),
+    week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`),
+    hour: (n) => `${n} 小时`,
+    minute: (n) => `${n} 分钟`,
+    second: (n) => `${n} 秒`
+  },
+  en: {
+    justNow: '刚刚',
+    past: (n) => (n.match(/\d/) ? `${n} ago` : n),
+    future: (n) => (n.match(/\d/) ? `in ${n}` : n),
+    month: (n, past) =>
+      n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
+    year: (n, past) =>
+      n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
+    day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`),
+    week: (n, past) =>
+      n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`,
+    hour: (n) => `${n} hour${n > 1 ? 's' : ''}`,
+    minute: (n) => `${n} minute${n > 1 ? 's' : ''}`,
+    second: (n) => `${n} second${n > 1 ? 's' : ''}`
+  }
+}
+
+export const useTimeAgo = (time: Date | number | string) => {
+  const localeStore = useLocaleStoreWithOut()
+
+  const currentLocale = computed(() => localeStore.getCurrentLocale)
+
+  const timeAgo = useTimeAgoCore(time, {
+    messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang]
+  })
+
+  return timeAgo
+}

+ 24 - 1
src/locales/en.ts

@@ -71,7 +71,8 @@ export default {
     menu12: 'Menu1-2',
     menu2: 'Menu2',
     dashboard: 'Dashboard',
-    analysis: 'Analysis'
+    analysis: 'Analysis',
+    workplace: 'Workplace'
   },
   analysis: {
     newUser: 'New user',
@@ -109,6 +110,28 @@ export default {
     saturday: 'Saturday',
     sunday: 'Sunday'
   },
+  workplace: {
+    goodMorning: 'Good morning',
+    happyDay: 'Wish you happy every day!',
+    toady: `It's sunny today`,
+    project: 'Project',
+    access: 'Project access',
+    toDo: 'To do',
+    introduction: 'A serious introduction',
+    more: 'More',
+    shortcutOperation: 'Shortcut operation',
+    operation: 'Operation',
+    index: 'Index',
+    personal: 'Personal',
+    team: 'Team',
+    quote: 'Quote',
+    contribution: 'Contribution',
+    hot: 'Hot',
+    yield: 'Yield',
+    dynamic: 'Dynamic',
+    push: 'push',
+    pushCode: 'Archer push code to GitHub'
+  },
   formDemo: {
     input: 'Input',
     inputNumber: 'InputNumber',

+ 24 - 1
src/locales/zh-CN.ts

@@ -71,7 +71,8 @@ export default {
     menu12: '菜单1-2',
     menu2: '菜单2',
     dashboard: '首页',
-    analysis: '分析页'
+    analysis: '分析页',
+    workplace: '工作台'
   },
   analysis: {
     newUser: '新增用户',
@@ -109,6 +110,28 @@ export default {
     saturday: '周六',
     sunday: '周日'
   },
+  workplace: {
+    goodMorning: '早安',
+    happyDay: '祝你开心每一天!',
+    toady: '今日晴',
+    project: '项目数',
+    access: '项目访问',
+    toDo: '待办',
+    introduction: '一个正经的简介',
+    more: '更多',
+    shortcutOperation: '快捷操作',
+    operation: '操作',
+    index: '指数',
+    personal: '个人',
+    team: '团队',
+    quote: '引用',
+    contribution: '贡献',
+    hot: '热度',
+    yield: '产量',
+    dynamic: '动态',
+    push: '推送',
+    pushCode: 'Archer 推送 代码到 Github'
+  },
   formDemo: {
     input: '输入框',
     inputNumber: '数字输入框',

+ 10 - 2
src/plugins/echarts/index.ts

@@ -1,6 +1,13 @@
 import * as echarts from 'echarts/core'
 
-import { BarChart, LineChart, PieChart, MapChart, PictorialBarChart } from 'echarts/charts'
+import {
+  BarChart,
+  LineChart,
+  PieChart,
+  MapChart,
+  PictorialBarChart,
+  RadarChart
+} from 'echarts/charts'
 
 import {
   TitleComponent,
@@ -27,7 +34,8 @@ echarts.use([
   PieChart,
   MapChart,
   CanvasRenderer,
-  PictorialBarChart
+  PictorialBarChart,
+  RadarChart
 ])
 
 export default echarts

+ 10 - 2
src/router/index.ts

@@ -54,8 +54,16 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         name: 'Analysis',
         meta: {
           title: t('router.analysis'),
-          noCache: true,
-          affix: true
+          noCache: true
+        }
+      },
+      {
+        path: 'workplace',
+        component: () => import('@/views/Dashboard/Workplace.vue'),
+        name: 'Workplace',
+        meta: {
+          title: t('router.workplace'),
+          noCache: true
         }
       }
     ]

+ 32 - 0
src/utils/index.ts

@@ -63,3 +63,35 @@ export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
 export const trim = (str: string) => {
   return str.replace(/(^\s*)|(\s*$)/g, '')
 }
+
+/**
+ * @param {Date | number | string} time 需要转换的时间
+ * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
+ */
+export function formatTime(time: Date | number | string, fmt: string) {
+  if (!time) return ''
+  else {
+    const date = new Date(time)
+    const o = {
+      'M+': date.getMonth() + 1,
+      'd+': date.getDate(),
+      'H+': date.getHours(),
+      'm+': date.getMinutes(),
+      's+': date.getSeconds(),
+      'q+': Math.floor((date.getMonth() + 3) / 3),
+      S: date.getMilliseconds()
+    }
+    if (/(y+)/.test(fmt)) {
+      fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+    }
+    for (const k in o) {
+      if (new RegExp('(' + k + ')').test(fmt)) {
+        fmt = fmt.replace(
+          RegExp.$1,
+          RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
+        )
+      }
+    }
+    return fmt
+  }
+}

+ 1 - 1
src/views/Dashboard/Analysis.vue

@@ -32,7 +32,7 @@ setTimeout(() => {
     <ElCol :span="24">
       <ElCard shadow="hover" class="mb-20px">
         <ElSkeleton :loading="loading" animated :rows="4">
-          <Echart :options="lineOptions" :height="400" />
+          <Echart :options="lineOptions" :height="350" />
         </ElSkeleton>
       </ElCard>
     </ElCol>

+ 299 - 0
src/views/Dashboard/Workplace.vue

@@ -0,0 +1,299 @@
+<script setup lang="ts">
+import { useTimeAgo } from '@/hooks/web/useTimeAgo'
+import { ElRow, ElCol, ElSkeleton, ElCard, ElDivider, ElLink } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ref } from 'vue'
+import { CountTo } from '@/components/CountTo'
+import { formatTime } from '@/utils'
+import { Echart } from '@/components/Echart'
+import { radarOption } from './echarts-data'
+import { Highlight } from '@/components/Highlight'
+
+interface Project {
+  name: string
+  icon: string
+  message: string
+  personal: string
+  time: Date | number | string
+}
+
+interface Dynamic {
+  keys: string[]
+  time: Date | number | string
+}
+
+interface Team {
+  name: string
+  icon: string
+}
+
+const { t } = useI18n()
+
+const projects: Project[] = [
+  {
+    name: 'Github',
+    icon: 'akar-icons:github-fill',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  },
+  {
+    name: 'Vue',
+    icon: 'logos:vue',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  },
+  {
+    name: 'Angular',
+    icon: 'logos:angular-icon',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  },
+  {
+    name: 'React',
+    icon: 'logos:react',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  },
+  {
+    name: 'Webpack',
+    icon: 'logos:webpack',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  },
+  {
+    name: 'Vite',
+    icon: 'vscode-icons:file-type-vite',
+    message: t('workplace.introduction'),
+    personal: 'Archer',
+    time: new Date()
+  }
+]
+
+const dynamics: Dynamic[] = [
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  },
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  },
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  },
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  },
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  },
+  {
+    keys: [t('workplace.push'), 'Github'],
+    time: new Date()
+  }
+]
+
+const team: Team[] = [
+  {
+    name: 'Github',
+    icon: 'akar-icons:github-fill'
+  },
+  {
+    name: 'Vue',
+    icon: 'logos:vue'
+  },
+  {
+    name: 'Angular',
+    icon: 'logos:angular-icon'
+  },
+  {
+    name: 'React',
+    icon: 'logos:react'
+  },
+  {
+    name: 'Webpack',
+    icon: 'logos:webpack'
+  },
+  {
+    name: 'Vite',
+    icon: 'vscode-icons:file-type-vite'
+  }
+]
+
+const loading = ref(true)
+
+setTimeout(() => {
+  loading.value = false
+}, 1000)
+</script>
+
+<template>
+  <div>
+    <ElCard shadow="never">
+      <ElSkeleton :loading="loading" animated>
+        <ElRow :gutter="20" justify="space-between">
+          <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="flex items-center">
+              <img
+                src="@/assets/imgs/avatar.jpg"
+                alt=""
+                class="w-70px h-70px rounded-[50%] mr-20px"
+              />
+              <div>
+                <div class="text-20px text-700">
+                  {{ t('workplace.goodMorning') }},Archer,{{ t('workplace.happyDay') }}
+                </div>
+                <div class="mt-10px text-14px text-gray-500">
+                  {{ t('workplace.toady') }},20℃ - 32℃!
+                </div>
+              </div>
+            </div>
+          </ElCol>
+          <ElCol :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="flex h-70px items-center justify-end <sm:mt-20px">
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.project') }}</div>
+                <CountTo class="text-20px" :start-val="0" :end-val="40" :duration="2600" />
+              </div>
+              <ElDivider direction="vertical" />
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.toDo') }}</div>
+                <CountTo class="text-20px" :start-val="0" :end-val="10" :duration="2600" />
+              </div>
+              <ElDivider direction="vertical" border-style="dashed" />
+              <div class="px-8px text-right">
+                <div class="text-14px text-gray-400 mb-20px">{{ t('workplace.access') }}</div>
+                <CountTo class="text-20px" :start-val="0" :end-val="2340" :duration="2600" />
+              </div>
+            </div>
+          </ElCol>
+        </ElRow>
+      </ElSkeleton>
+    </ElCard>
+  </div>
+
+  <ElRow class="mt-20px" :gutter="20" justify="space-between">
+    <ElCol :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-20px">
+      <ElSkeleton :loading="loading" animated>
+        <ElCard shadow="never">
+          <template #header>
+            <div class="flex justify-between">
+              <span>{{ t('workplace.project') }}</span>
+              <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
+            </div>
+          </template>
+          <ElRow>
+            <ElCol
+              v-for="(item, index) in projects"
+              :key="`card-${index}`"
+              :xl="8"
+              :lg="8"
+              :md="12"
+              :sm="24"
+              :xs="24"
+            >
+              <ElCard shadow="hover">
+                <div class="flex items-center">
+                  <Icon :icon="item.icon" :size="25" class="mr-10px" />
+                  <span class="text-16px">{{ item.name }}</span>
+                </div>
+                <div class="mt-15px text-14px text-gray-400">{{ item.message }}</div>
+                <div class="mt-20px text-12px text-gray-400 flex justify-between">
+                  <span>{{ item.personal }}</span>
+                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
+                </div>
+              </ElCard>
+            </ElCol>
+          </ElRow>
+        </ElCard>
+      </ElSkeleton>
+
+      <ElSkeleton :loading="loading" animated>
+        <ElCard shadow="never" class="mt-20px">
+          <template #header>
+            <div class="flex justify-between">
+              <span>{{ t('workplace.dynamic') }}</span>
+              <ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
+            </div>
+          </template>
+          <div v-for="(item, index) in dynamics" :key="`dynamics-${index}`">
+            <div class="flex items-center">
+              <img
+                src="@/assets/imgs/avatar.jpg"
+                alt=""
+                class="w-35px h-35px rounded-[50%] mr-20px"
+              />
+              <div>
+                <div class="text-14px">
+                  <Highlight :keys="item.keys"> {{ t('workplace.pushCode') }} </Highlight>
+                </div>
+                <div class="mt-15px text-12px text-gray-400">
+                  {{ useTimeAgo(item.time) }}
+                </div>
+              </div>
+            </div>
+            <ElDivider />
+          </div>
+        </ElCard>
+      </ElSkeleton>
+    </ElCol>
+    <ElCol :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-20px">
+      <ElSkeleton :loading="loading" animated>
+        <ElCard shadow="never">
+          <template #header>
+            <span>{{ t('workplace.shortcutOperation') }}</span>
+          </template>
+          <ElCol
+            v-for="item in 9"
+            :key="`card-${item}`"
+            :xl="12"
+            :lg="12"
+            :md="12"
+            :sm="24"
+            :xs="24"
+            class="mb-10px"
+          >
+            <ElLink type="default" :underline="false">
+              {{ t('workplace.operation') }}{{ item }}
+            </ElLink>
+          </ElCol>
+        </ElCard>
+      </ElSkeleton>
+
+      <ElSkeleton :loading="loading" animated>
+        <ElCard shadow="never" class="mt-20px">
+          <template #header>
+            <span>xx{{ t('workplace.index') }}</span>
+          </template>
+          <Echart :options="radarOption" :height="400" />
+        </ElCard>
+      </ElSkeleton>
+
+      <ElSkeleton :loading="loading" animated>
+        <ElCard shadow="never" class="mt-20px">
+          <template #header>
+            <span>{{ t('workplace.team') }}</span>
+          </template>
+          <ElRow>
+            <ElCol v-for="item in team" :key="`team-${item.name}`" :span="12" class="mb-20px">
+              <div class="flex items-center">
+                <Icon :icon="item.icon" class="mr-10px" />
+                <ElLink type="default" :underline="false">
+                  {{ item.name }}
+                </ElLink>
+              </div>
+            </ElCol>
+          </ElRow>
+        </ElCard>
+      </ElSkeleton>
+    </ElCol>
+  </ElRow>
+</template>

+ 31 - 0
src/views/Dashboard/echarts-data.ts

@@ -151,3 +151,34 @@ export const barOptions: EChartsOption = {
     }
   ]
 }
+
+export const radarOption: EChartsOption = {
+  legend: {
+    data: [t('workplace.personal'), t('workplace.team')]
+  },
+  radar: {
+    // shape: 'circle',
+    indicator: [
+      { name: t('workplace.quote'), max: 65 },
+      { name: t('workplace.contribution'), max: 160 },
+      { name: t('workplace.hot'), max: 300 },
+      { name: t('workplace.yield'), max: 130 }
+    ]
+  },
+  series: [
+    {
+      name: `xxx${t('workplace.index')}`,
+      type: 'radar',
+      data: [
+        {
+          value: [42, 30, 20, 35],
+          name: t('workplace.personal')
+        },
+        {
+          value: [50, 140, 28, 35],
+          name: t('workplace.team')
+        }
+      ]
+    }
+  ]
+}