From 27935cb9b3335a6622ad0713d99197afe397d5e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E8=96=AA=E5=90=8D?= <942005050@qq.com>
Date: Tue, 6 Feb 2024 08:46:57 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E5=86=99=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/views/error/403.vue                       |   8 +
 src/views/error/404.vue                       |   7 +
 src/views/error/500.vue                       |   7 +
 src/views/home/Index copy.vue                 | 381 ++++++++++
 src/views/home/Index2.vue                     | 319 +++++++++
 src/views/home/Indexbark.vue                  | 653 ++++++++++++++++++
 src/views/home/echarts-data.ts                | 181 +++++
 src/views/home/index.vue                      | 227 ++++++
 src/views/home/types.ts                       |  55 ++
 src/views/login/components/LoginForm.vue      | 287 ++++++++
 src/views/login/components/LoginFormTitle.vue |  26 +
 src/views/login/components/MobileForm.vue     | 225 ++++++
 src/views/login/components/QrCodeForm.vue     |  30 +
 src/views/login/components/RegisterForm.vue   | 142 ++++
 src/views/login/components/SSOLogin.vue       | 199 ++++++
 src/views/login/components/index.ts           |   8 +
 src/views/login/components/useLogin.ts        |  42 ++
 src/views/login/login.vue                     | 111 +++
 src/views/profile/components/BasicInfo.vue    |  92 +++
 src/views/profile/components/ProfileUser.vue  |  99 +++
 src/views/profile/components/ResetPwd.vue     |  73 ++
 src/views/profile/components/UserAvatar.vue   |  39 ++
 src/views/profile/components/UserSocial.vue   |  94 +++
 src/views/profile/components/index.ts         |   7 +
 src/views/profile/index.vue                   |  64 ++
 src/views/redirect/redirect.vue               |  28 +
 26 files changed, 3404 insertions(+)
 create mode 100644 src/views/error/403.vue
 create mode 100644 src/views/error/404.vue
 create mode 100644 src/views/error/500.vue
 create mode 100644 src/views/home/Index copy.vue
 create mode 100644 src/views/home/Index2.vue
 create mode 100644 src/views/home/Indexbark.vue
 create mode 100644 src/views/home/echarts-data.ts
 create mode 100644 src/views/home/index.vue
 create mode 100644 src/views/home/types.ts
 create mode 100644 src/views/login/components/LoginForm.vue
 create mode 100644 src/views/login/components/LoginFormTitle.vue
 create mode 100644 src/views/login/components/MobileForm.vue
 create mode 100644 src/views/login/components/QrCodeForm.vue
 create mode 100644 src/views/login/components/RegisterForm.vue
 create mode 100644 src/views/login/components/SSOLogin.vue
 create mode 100644 src/views/login/components/index.ts
 create mode 100644 src/views/login/components/useLogin.ts
 create mode 100644 src/views/login/login.vue
 create mode 100644 src/views/profile/components/BasicInfo.vue
 create mode 100644 src/views/profile/components/ProfileUser.vue
 create mode 100644 src/views/profile/components/ResetPwd.vue
 create mode 100644 src/views/profile/components/UserAvatar.vue
 create mode 100644 src/views/profile/components/UserSocial.vue
 create mode 100644 src/views/profile/components/index.ts
 create mode 100644 src/views/profile/index.vue
 create mode 100644 src/views/redirect/redirect.vue

diff --git a/src/views/error/403.vue b/src/views/error/403.vue
new file mode 100644
index 0000000..a3ec487
--- /dev/null
+++ b/src/views/error/403.vue
@@ -0,0 +1,8 @@
+<template>
+  <Error type="403" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error403' })
+
+const { push } = useRouter()
+</script>
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..f6a08de
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,7 @@
+<template>
+  <Error @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error404' })
+const { push } = useRouter()
+</script>
diff --git a/src/views/error/500.vue b/src/views/error/500.vue
new file mode 100644
index 0000000..998487d
--- /dev/null
+++ b/src/views/error/500.vue
@@ -0,0 +1,7 @@
+<template>
+  <Error type="500" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error500' })
+const { push } = useRouter()
+</script>
diff --git a/src/views/home/Index copy.vue b/src/views/home/Index copy.vue
new file mode 100644
index 0000000..121ec6a
--- /dev/null
+++ b/src/views/home/Index copy.vue	
@@ -0,0 +1,381 @@
+<template>
+  <div>
+    <el-card shadow="never">
+      <el-skeleton :loading="loading" animated>
+        <el-row :gutter="20" justify="space-between">
+          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="flex items-center">
+              <img :src="avatar" alt="" class="mr-20px h-70px w-70px rounded-[50%]" />
+              <div>
+                <div class="text-20px">
+                  {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
+                </div>
+                <div class="mt-10px text-14px text-gray-500">
+                  {{ t('workplace.toady') }},20℃ - 32℃!
+                </div>
+              </div>
+            </div>
+          </el-col>
+          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
+              <div class="px-8px text-right">
+                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.project') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.project"
+                  :duration="2600"
+                />
+              </div>
+              <el-divider direction="vertical" />
+              <div class="px-8px text-right">
+                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.todo"
+                  :duration="2600"
+                />
+              </div>
+              <el-divider direction="vertical" border-style="dashed" />
+              <div class="px-8px text-right">
+                <div class="mb-20px text-14px text-gray-400">{{ t('workplace.access') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.access"
+                  :duration="2600"
+                />
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-skeleton>
+    </el-card>
+  </div>
+
+  <el-row class="mt-5px" :gutter="20" justify="space-between">
+    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-10px">
+      <el-card shadow="never">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.project') }}</span>
+            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <el-row>
+            <el-col
+              v-for="(item, index) in projects"
+              :key="`card-${index}`"
+              :xl="8"
+              :lg="8"
+              :md="8"
+              :sm="24"
+              :xs="24"
+            >
+              <el-card 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">{{ t(item.message) }}</div>
+                <div class="mt-20px flex justify-between text-12px text-gray-400">
+                  <span>{{ item.personal }}</span>
+                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+
+      <el-card shadow="never" class="mt-5px">
+        <el-skeleton :loading="loading" animated>
+          <el-row :gutter="20" justify="space-between">
+            <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
+              <el-card shadow="hover" class="mb-10px">
+                <el-skeleton :loading="loading" animated>
+                  <Echart :options="pieOptionsData" :height="280" />
+                </el-skeleton>
+              </el-card>
+            </el-col>
+            <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
+              <el-card shadow="hover" class="mb-10px">
+                <el-skeleton :loading="loading" animated>
+                  <Echart :options="barOptionsData" :height="280" />
+                </el-skeleton>
+              </el-card>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-10px">
+      <el-card shadow="never">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.shortcutOperation') }}</span>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <el-row>
+            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-10px">
+              <div class="flex items-center">
+                <Icon :icon="item.icon" class="mr-10px" />
+                <el-link type="default" :underline="false" @click="setWatermark(item.name)">
+                  {{ item.name }}
+                </el-link>
+              </div>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+      <el-card shadow="never" class="mt-10px">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.notice') }}</span>
+            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
+            <div class="flex items-center">
+              <img :src="avatar" alt="" class="mr-20px h-35px w-35px rounded-[50%]" />
+              <div>
+                <div class="text-14px">
+                  <Highlight :keys="item.keys.map((v) => t(v))">
+                    {{ item.type }} : {{ item.title }}
+                  </Highlight>
+                </div>
+                <div class="mt-15px text-12px text-gray-400">
+                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
+                </div>
+              </div>
+            </div>
+            <el-divider />
+          </div>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+import { formatTime } from '@/utils'
+
+import { useUserStore } from '@/store/modules/user'
+import { useWatermark } from '@/hooks/web/useWatermark'
+import avatarImg from '@/assets/imgs/avatar.gif'
+import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
+import { pieOptions, barOptions } from './echarts-data'
+
+defineOptions({ name: 'Home' })
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const { setWatermark } = useWatermark()
+const loading = ref(true)
+const avatar = userStore.getUser.avatar ? userStore.getUser.avatar : avatarImg
+const username = userStore.getUser.nickname
+const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+// 获取统计数
+let totalSate = reactive<WorkplaceTotal>({
+  project: 0,
+  access: 0,
+  todo: 0
+})
+
+const getCount = async () => {
+  const data = {
+    project: 40,
+    access: 2340,
+    todo: 10
+  }
+  totalSate = Object.assign(totalSate, data)
+}
+
+// 获取项目数
+let projects = reactive<Project[]>([])
+const getProject = async () => {
+  const data = [
+    {
+      name: 'Github',
+      icon: 'akar-icons:github-fill',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Vue',
+      icon: 'logos:vue',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Angular',
+      icon: 'logos:angular-icon',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'React',
+      icon: 'logos:react',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Webpack',
+      icon: 'logos:webpack',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Vite',
+      icon: 'vscode-icons:file-type-vite',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    }
+  ]
+  projects = Object.assign(projects, data)
+}
+
+// 获取通知公告
+let notice = reactive<Notice[]>([])
+const getNotice = async () => {
+  const data = [
+    {
+      title: '系统升级版本',
+      type: '通知',
+      keys: ['通知', '升级'],
+      date: new Date()
+    },
+    {
+      title: '系统凌晨维护',
+      type: '公告',
+      keys: ['公告', '维护'],
+      date: new Date()
+    },
+    {
+      title: '系统升级版本',
+      type: '通知',
+      keys: ['通知', '升级'],
+      date: new Date()
+    },
+    {
+      title: '系统凌晨维护',
+      type: '公告',
+      keys: ['公告', '维护'],
+      date: new Date()
+    }
+  ]
+  notice = Object.assign(notice, data)
+}
+
+// 获取快捷入口
+let shortcut = reactive<Shortcut[]>([])
+
+const getShortcut = async () => {
+  const data = [
+    {
+      name: 'Github',
+      icon: 'akar-icons:github-fill',
+      url: 'github.io'
+    },
+    {
+      name: 'Vue',
+      icon: 'logos:vue',
+      url: 'vuejs.org'
+    },
+    {
+      name: 'Vite',
+      icon: 'vscode-icons:file-type-vite',
+      url: 'https://vitejs.dev/'
+    },
+    {
+      name: 'Angular',
+      icon: 'logos:angular-icon',
+      url: 'github.io'
+    },
+    {
+      name: 'React',
+      icon: 'logos:react',
+      url: 'github.io'
+    },
+    {
+      name: 'Webpack',
+      icon: 'logos:webpack',
+      url: 'github.io'
+    }
+  ]
+  shortcut = Object.assign(shortcut, data)
+}
+
+// 用户来源
+const getUserAccessSource = async () => {
+  const data = [
+    { value: 335, name: 'analysis.directAccess' },
+    { value: 310, name: 'analysis.mailMarketing' },
+    { value: 234, name: 'analysis.allianceAdvertising' },
+    { value: 135, name: 'analysis.videoAdvertising' },
+    { value: 1548, name: 'analysis.searchEngines' }
+  ]
+  set(
+    pieOptionsData,
+    'legend.data',
+    data.map((v) => t(v.name))
+  )
+  pieOptionsData!.series![0].data = data.map((v) => {
+    return {
+      name: t(v.name),
+      value: v.value
+    }
+  })
+}
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+
+// 周活跃量
+const getWeeklyUserActivity = async () => {
+  const data = [
+    { value: 13253, name: 'analysis.monday' },
+    { value: 34235, name: 'analysis.tuesday' },
+    { value: 26321, name: 'analysis.wednesday' },
+    { value: 12340, name: 'analysis.thursday' },
+    { value: 24643, name: 'analysis.friday' },
+    { value: 1322, name: 'analysis.saturday' },
+    { value: 1324, name: 'analysis.sunday' }
+  ]
+  set(
+    barOptionsData,
+    'xAxis.data',
+    data.map((v) => t(v.name))
+  )
+  set(barOptionsData, 'series', [
+    {
+      name: t('analysis.activeQuantity'),
+      data: data.map((v) => v.value),
+      type: 'bar'
+    }
+  ])
+}
+
+const getAllApi = async () => {
+  await Promise.all([
+    getCount(),
+    getProject(),
+    getNotice(),
+    getShortcut(),
+    getUserAccessSource(),
+    getWeeklyUserActivity()
+  ])
+  loading.value = false
+}
+
+getAllApi()
+</script>
diff --git a/src/views/home/Index2.vue b/src/views/home/Index2.vue
new file mode 100644
index 0000000..c9429ab
--- /dev/null
+++ b/src/views/home/Index2.vue
@@ -0,0 +1,319 @@
+<template>
+  <el-row :class="prefixCls" :gutter="20" justify="space-between">
+    <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" :rows="2" animated>
+          <template #default>
+            <div :class="`${prefixCls}__item flex justify-between`">
+              <div>
+                <div
+                  :class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
+                >
+                  <Icon :size="40" icon="svg-icon:peoples" />
+                </div>
+              </div>
+              <div class="flex flex-col justify-between">
+                <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+                  >{{ t('analysis.newUser') }}
+                </div>
+                <CountTo
+                  :duration="2600"
+                  :end-val="102400"
+                  :start-val="0"
+                  class="text-right text-20px font-700"
+                />
+              </div>
+            </div>
+          </template>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+
+    <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" :rows="2" animated>
+          <template #default>
+            <div :class="`${prefixCls}__item flex justify-between`">
+              <div>
+                <div
+                  :class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
+                >
+                  <Icon :size="40" icon="svg-icon:message" />
+                </div>
+              </div>
+              <div class="flex flex-col justify-between">
+                <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+                  >{{ t('analysis.unreadInformation') }}
+                </div>
+                <CountTo
+                  :duration="2600"
+                  :end-val="81212"
+                  :start-val="0"
+                  class="text-right text-20px font-700"
+                />
+              </div>
+            </div>
+          </template>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+
+    <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" :rows="2" animated>
+          <template #default>
+            <div :class="`${prefixCls}__item flex justify-between`">
+              <div>
+                <div
+                  :class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
+                >
+                  <Icon :size="40" icon="svg-icon:money" />
+                </div>
+              </div>
+              <div class="flex flex-col justify-between">
+                <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+                  >{{ t('analysis.transactionAmount') }}
+                </div>
+                <CountTo
+                  :duration="2600"
+                  :end-val="9280"
+                  :start-val="0"
+                  class="text-right text-20px font-700"
+                />
+              </div>
+            </div>
+          </template>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+
+    <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" :rows="2" animated>
+          <template #default>
+            <div :class="`${prefixCls}__item flex justify-between`">
+              <div>
+                <div
+                  :class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
+                >
+                  <Icon :size="40" icon="svg-icon:shopping" />
+                </div>
+              </div>
+              <div class="flex flex-col justify-between">
+                <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+                  >{{ t('analysis.totalShopping') }}
+                </div>
+                <CountTo
+                  :duration="2600"
+                  :end-val="13600"
+                  :start-val="0"
+                  class="text-right text-20px font-700"
+                />
+              </div>
+            </div>
+          </template>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+  </el-row>
+  <el-row :gutter="20" justify="space-between">
+    <el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" animated>
+          <Echart :height="300" :options="pieOptionsData" />
+        </el-skeleton>
+      </el-card>
+    </el-col>
+    <el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" animated>
+          <Echart :height="300" :options="barOptionsData" />
+        </el-skeleton>
+      </el-card>
+    </el-col>
+    <el-col :span="24">
+      <el-card class="mb-20px" shadow="hover">
+        <el-skeleton :loading="loading" :rows="4" animated>
+          <Echart :height="350" :options="lineOptionsData" />
+        </el-skeleton>
+      </el-card>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import type { AnalysisTotalTypes } from './types'
+import { barOptions, lineOptions, pieOptions } from './echarts-data'
+
+defineOptions({ name: 'Home2' })
+
+const { t } = useI18n()
+const loading = ref(true)
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('panel')
+const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+
+let totalState = reactive<AnalysisTotalTypes>({
+  users: 0,
+  messages: 0,
+  moneys: 0,
+  shoppings: 0
+})
+
+const getCount = async () => {
+  const data = {
+    users: 102400,
+    messages: 81212,
+    moneys: 9280,
+    shoppings: 13600
+  }
+  totalState = Object.assign(totalState, data)
+}
+
+// 用户来源
+const getUserAccessSource = async () => {
+  const data = [
+    { value: 335, name: 'analysis.directAccess' },
+    { value: 310, name: 'analysis.mailMarketing' },
+    { value: 234, name: 'analysis.allianceAdvertising' },
+    { value: 135, name: 'analysis.videoAdvertising' },
+    { value: 1548, name: 'analysis.searchEngines' }
+  ]
+  set(
+    pieOptionsData,
+    'legend.data',
+    data.map((v) => t(v.name))
+  )
+  set(pieOptionsData, 'series.data', data)
+}
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+
+// 周活跃量
+const getWeeklyUserActivity = async () => {
+  const data = [
+    { value: 13253, name: 'analysis.monday' },
+    { value: 34235, name: 'analysis.tuesday' },
+    { value: 26321, name: 'analysis.wednesday' },
+    { value: 12340, name: 'analysis.thursday' },
+    { value: 24643, name: 'analysis.friday' },
+    { value: 1322, name: 'analysis.saturday' },
+    { value: 1324, name: 'analysis.sunday' }
+  ]
+  set(
+    barOptionsData,
+    'xAxis.data',
+    data.map((v) => t(v.name))
+  )
+  set(barOptionsData, 'series', [
+    {
+      name: t('analysis.activeQuantity'),
+      data: data.map((v) => v.value),
+      type: 'bar'
+    }
+  ])
+}
+
+const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
+
+// 每月销售总额
+const getMonthlySales = async () => {
+  const data = [
+    { estimate: 100, actual: 120, name: 'analysis.january' },
+    { estimate: 120, actual: 82, name: 'analysis.february' },
+    { estimate: 161, actual: 91, name: 'analysis.march' },
+    { estimate: 134, actual: 154, name: 'analysis.april' },
+    { estimate: 105, actual: 162, name: 'analysis.may' },
+    { estimate: 160, actual: 140, name: 'analysis.june' },
+    { estimate: 165, actual: 145, name: 'analysis.july' },
+    { estimate: 114, actual: 250, name: 'analysis.august' },
+    { estimate: 163, actual: 134, name: 'analysis.september' },
+    { estimate: 185, actual: 56, name: 'analysis.october' },
+    { estimate: 118, actual: 99, name: 'analysis.november' },
+    { estimate: 123, actual: 123, name: 'analysis.december' }
+  ]
+  set(
+    lineOptionsData,
+    'xAxis.data',
+    data.map((v) => t(v.name))
+  )
+  set(lineOptionsData, 'series', [
+    {
+      name: t('analysis.estimate'),
+      smooth: true,
+      type: 'line',
+      data: data.map((v) => v.estimate),
+      animationDuration: 2800,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: t('analysis.actual'),
+      smooth: true,
+      type: 'line',
+      itemStyle: {},
+      data: data.map((v) => v.actual),
+      animationDuration: 2800,
+      animationEasing: 'quadraticOut'
+    }
+  ])
+}
+
+const getAllApi = async () => {
+  await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
+  loading.value = false
+}
+
+getAllApi()
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-panel;
+
+.#{$prefix-cls} {
+  &__item {
+    &--peoples {
+      color: #40c9c6;
+    }
+
+    &--message {
+      color: #36a3f7;
+    }
+
+    &--money {
+      color: #f4516c;
+    }
+
+    &--shopping {
+      color: #34bfa3;
+    }
+
+    &:hover {
+      :deep(.#{$namespace}-icon) {
+        color: #fff !important;
+      }
+
+      .#{$prefix-cls}__item--icon {
+        transition: all 0.38s ease-out;
+      }
+
+      .#{$prefix-cls}__item--peoples {
+        background: #40c9c6;
+      }
+
+      .#{$prefix-cls}__item--message {
+        background: #36a3f7;
+      }
+
+      .#{$prefix-cls}__item--money {
+        background: #f4516c;
+      }
+
+      .#{$prefix-cls}__item--shopping {
+        background: #34bfa3;
+      }
+    }
+  }
+}
+</style>
diff --git a/src/views/home/Indexbark.vue b/src/views/home/Indexbark.vue
new file mode 100644
index 0000000..2627b64
--- /dev/null
+++ b/src/views/home/Indexbark.vue
@@ -0,0 +1,653 @@
+<template>
+  <div>
+    <ContentWrap class="search">
+      <el-form
+        :inline="true"
+        :model="queryParams"
+        class="demo-form-inline"
+        style="margin-bottom: -17px"
+      >
+        <el-form-item label="" label-width="0px" style="width: calc(20% - 32px)">
+          <el-select
+            v-model="queryParams.project"
+            placeholder="请选择项目"
+            clearable
+            style="width: 100%"
+          >
+            <el-option label="Zone one" value="shanghai" />
+            <el-option label="Zone two" value="beijing" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="" label-width="0px" style="width: calc(20% - 32px)">
+          <el-input
+            v-model="queryParams.code"
+            placeholder="请输入物料编号"
+            clearable
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="" label-width="0px" style="width: calc(20% - 32px)">
+          <el-select
+            v-model="queryParams.type"
+            placeholder="请选择检测类型"
+            clearable
+            style="width: 100%"
+          >
+            <el-option label="Zone one" value="shanghai" />
+            <el-option label="Zone two" value="beijing" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="" label-width="0px" style="width: calc(20% - 32px)">
+          <el-input
+            v-model="queryParams.content"
+            placeholder="请输入检测内容"
+            clearable
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="" label-width="0px" style="width: calc(20% - 32px)">
+          <el-date-picker
+            v-model="queryParams.date"
+            type="date"
+            placeholder="请选择日期"
+            clearable
+          />
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <ContentWrap class="search">
+      <div class="title">制程能力指数(CPK)</div>
+      <div class="cpk">
+        <div class="cpk-item">
+          <div class="cpk-item-1">
+            <div class="cpk-item-label">CP</div>
+            <div class="cpk-item-value">0.25</div>
+          </div>
+          <div class="cpk-item-1">
+            <div class="cpk-item-label">CPK</div>
+            <div class="cpk-item-value">0.11</div>
+          </div>
+        </div>
+        <div class="cpk-item">
+          <div class="cpk-item-1">
+            <div class="cpk-item-label">CP</div>
+            <div class="cpk-item-value">0.25</div>
+          </div>
+          <div class="cpk-item-1">
+            <div class="cpk-item-label">CPK</div>
+            <div class="cpk-item-value">0.11</div>
+          </div>
+        </div>
+        <div class="cpk-item1">
+          <div class="cpk-item-2">
+            <div class="cpk-item-label"><span>零件号</span> <span>1662525525</span></div>
+            <div class="cpk-item-label"><span>检测内容</span> <span>25+0.6 G10</span></div>
+          </div>
+          <div class="cpk-item-2">
+            <div class="cpk-item-label"><span>公差下限</span> <span>24.4</span></div>
+            <div class="cpk-item-label"><span>公差上限</span> <span>25.6</span></div>
+          </div>
+          <div class="cpk-item-2">
+            <div class="cpk-item-label"><span>X中值</span> <span>24.4</span></div>
+            <div class="cpk-item-label"><span>R中值</span> <span>1.88</span></div>
+          </div>
+          <div class="cpk-item-2">
+            <div class="cpk-item-label"><span>UCLx</span> <span>25.66666</span></div>
+            <div class="cpk-item-label"><span>UCLx</span> <span>23.55555</span></div>
+          </div>
+          <div class="cpk-item-2">
+            <div class="cpk-item-label"><span>LCLrc</span> <span>25.66666</span></div>
+            <div class="cpk-item-label"><span>LCLrc</span> <span>0</span></div>
+          </div>
+        </div>
+      </div>
+      <table border="1" width="100%" align="center" border-collapse="collapse">
+        <tr class="td-bg">
+          <td colspan="2">样本</td>
+          <td>1</td>
+          <td>2</td>
+          <td>3</td>
+          <td>4</td>
+          <td>5</td>
+          <td>6</td>
+          <td>7</td>
+          <td>8</td>
+          <td>9</td>
+          <td>10</td>
+          <td>12</td>
+          <td>13</td>
+          <td>14</td>
+          <td>15</td>
+          <td>16</td>
+          <td>17</td>
+          <td>18</td>
+          <td>19</td>
+          <td>20</td>
+          <td>21</td>
+          <td>22</td>
+          <td>23</td>
+          <td>24</td>
+          <td>25</td>
+        </tr>
+        <tr>
+          <td rowspan="8" class="td-bg">检测记录</td>
+          <td class="td-bg">1</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">2</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">3</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">4</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">5</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">合计值</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">平均值</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+        <tr>
+          <td class="td-bg">极差值</td>
+          <td>24</td>
+          <td>25</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+          <td>24</td>
+        </tr>
+      </table>
+    </ContentWrap>
+    <ContentWrap class="search">
+      <div class="tabs">
+        <div class="title" :class="tabIndex == 0 ? 'active' : ''" @click="tabIndex = 0">控制图</div>
+        <div class="title" :class="tabIndex == 1 ? 'active' : ''" @click="tabIndex = 1"
+          >CPK分析图</div
+        >
+        <div class="title" :class="tabIndex == 2 ? 'active' : ''" @click="tabIndex = 2"
+          >样本运行图</div
+        >
+        <div class="title" :class="tabIndex == 3 ? 'active' : ''" @click="tabIndex = 3"
+          >均值运行图</div
+        >
+      </div>
+      <div class="charts" v-show="tabIndex == 0">
+        <div id="myEcharts1" :style="{ width: '50%', height: '300px' }"></div>
+        <div id="myEcharts2" :style="{ width: '50%', height: '300px' }"></div>
+      </div>
+      <div class="charts" v-show="tabIndex == 1">
+        <div id="myEcharts3" :style="{ width: '80vw', height: '300px' }"></div>
+      </div>
+      <div class="charts" v-show="tabIndex == 2">
+        <div id="myEcharts4" :style="{ width: '80vw', height: '300px' }"></div>
+      </div>
+      <div class="charts" v-show="tabIndex == 3">
+        <div id="myEcharts5" :style="{ width: '80vw', height: '300px' }"></div>
+      </div>
+    </ContentWrap>
+  </div>
+</template>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+let echart = echarts
+const queryParams = reactive({
+  project: '',
+  code: '',
+  type: '',
+  content: '',
+  date: ''
+})
+const tabIndex = ref(0)
+// 设置延期未交付数据
+function setChart1() {
+  let chart = echart.init(document.getElementById('myEcharts1'), 'light')
+  // 把配置和数据放这里
+  chart.setOption({
+    title: {
+      text: 'RANGE CHART'
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['Highest', 'Lowest'],
+      top: '0',
+      right: '3%'
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      top: '15%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: 'Highest',
+        data: [150, 230, 224, 218, 135, 147, 260],
+        type: 'line',
+        markLine: {
+          data: [{ type: 'average', name: 'Avg' }]
+        }
+      },
+      {
+        name: 'Lowest',
+        data: [344, 23, 56, 88, 95, 54, 45],
+        type: 'line'
+      }
+    ]
+  })
+
+  window.onresize = function () {
+    //自适应大小
+    chart.resize()
+  }
+}
+function setChart2() {
+  let chart = echart.init(document.getElementById('myEcharts2'), 'light')
+  // 把配置和数据放这里
+  chart.setOption({
+    title: {
+      text: 'RANGE CHART'
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['Highest', 'Lowest'],
+      top: '0',
+      right: '3%'
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      top: '15%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: 'Highest',
+        data: [150, 230, 224, 218, 135, 147, 260],
+        type: 'line'
+      },
+      {
+        name: 'Lowest',
+        data: [344, 23, 56, 88, 95, 54, 45],
+        type: 'line'
+      }
+    ]
+  })
+  window.onresize = function () {
+    //自适应大小
+    chart.resize()
+  }
+}
+function setChart3() {
+  let chart = echart.init(document.getElementById('myEcharts3'), 'light')
+  // 把配置和数据放这里
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'cross',
+        crossStyle: {
+          color: '#999'
+        }
+      }
+    },
+    legend: {
+      data: ['Evaporation', 'Precipitation', 'Temperature']
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      top: '15%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+        axisPointer: {
+          type: 'shadow'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        name: 'Precipitation',
+        min: 0,
+        max: 250,
+        interval: 50,
+        axisLabel: {
+          formatter: '{value} ml'
+        }
+      },
+      {
+        type: 'value',
+        name: 'Temperature',
+        min: 0,
+        max: 25,
+        interval: 5,
+        axisLabel: {
+          formatter: '{value} °C'
+        }
+      }
+    ],
+    series: [
+      {
+        name: '原数据频率',
+        type: 'bar',
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + ' ml'
+          }
+        },
+        data: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3]
+      },
+      {
+        name: '正态分布',
+        type: 'line',
+        yAxisIndex: 1,
+        tooltip: {
+          valueFormatter: function (value) {
+            return value + ' °C'
+          }
+        },
+        data: [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2]
+      }
+    ]
+  })
+  window.onresize = function () {
+    //自适应大小
+    chart.resize()
+  }
+}
+function setChart4() {
+  let chart = echart.init(document.getElementById('myEcharts4'), 'light')
+  // 把配置和数据放这里
+  chart.setOption({
+    title: {
+      text: '样本运行图'
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['Highest', 'Lowest'],
+      top: '0',
+      right: '3%'
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      top: '15%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: 'Highest',
+        data: [150, 230, 224, 218, 135, 147, 260],
+        type: 'line'
+      },
+      {
+        name: 'Lowest',
+        data: [344, 23, 56, 88, 95, 54, 45],
+        type: 'line'
+      }
+    ]
+  })
+  window.onresize = function () {
+    //自适应大小
+    chart.resize()
+  }
+}
+function setChart5() {
+  let chart = echart.init(document.getElementById('myEcharts5'), 'light')
+  // 把配置和数据放这里
+  chart.setOption({
+    title: {
+      text: '均值运行图'
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['Highest', 'Lowest'],
+      top: '0',
+      right: '3%'
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      top: '15%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        name: 'Highest',
+        data: [150, 230, 224, 218, 135, 147, 260],
+        type: 'line'
+      },
+      {
+        name: 'Lowest',
+        data: [344, 23, 56, 88, 95, 54, 45],
+        type: 'line'
+      }
+    ]
+  })
+  window.onresize = function () {
+    //自适应大小
+    chart.resize()
+  }
+}
+onMounted(async () => {
+  setChart1()
+  setChart2()
+  setChart3()
+  setChart4()
+  setChart5()
+})
+</script>
+<style lang="scss" scoped>
+.search {
+  background: white;
+}
+::v-deep.el-form-item--default .el-form-item__label {
+  padding: 0px 20px !important;
+  background: rgb(53, 158, 256) !important;
+  color: white !important;
+}
+.title {
+  margin-bottom: 20px;
+  margin-right: 20px;
+}
+.cpk {
+  display: flex;
+  align-items: center;
+  .cpk-item {
+    display: flex;
+    align-items: center;
+    width: 10%;
+    margin-right: 10px;
+    background: rgb(235, 243, 255);
+    border: 1px solid rgb(44, 135, 255);
+    height: 60px;
+    justify-content: center;
+    .cpk-item-1 {
+      text-align: center;
+      margin: 0px 10px;
+      .cpk-item-label {
+        color: #a8a8a8;
+        font-size: 12px;
+      }
+      .cpk-item-value {
+        color: #000000;
+        font-size: 16px;
+      }
+    }
+  }
+  .cpk-item1 {
+    background: rgb(245, 245, 245);
+    flex: 1;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    height: 60px;
+    .cpk-item-2 {
+      .cpk-item-label {
+        color: #a8a8a8;
+        font-size: 12px;
+        span {
+          &:nth-child(1) {
+            width: 60px;
+            display: inline-block;
+            text-align: right;
+            margin-right: 6px;
+          }
+          &:nth-child(2) {
+            margin-right: 6px;
+            color: #000;
+          }
+        }
+      }
+    }
+  }
+}
+table {
+  width: 100%;
+  /*居中*/
+  margin: 20px auto 0px;
+  /*边框*/
+  /* border: 1px solid black; */
+
+  border-collapse: collapse;
+  /*设置背景颜色*/
+  /* background-color: #bfa; */
+}
+
+/*
+       * 设置边框
+       */
+td,
+th {
+  border: 1px solid rgb(200, 200, 200);
+  text-align: center;
+  font-size: 14px;
+}
+
+/*
+     * 设置隔行变色
+     */
+// tbody > tr:nth-child(even) {
+//   background-color: #bfa;
+// }
+
+/*
+     * 鼠标移入到tr以后,改变颜色
+     */
+// tr:hover {
+//   background-color: #ff0;
+// }
+.td-bg {
+  background: rgb(231, 244, 248);
+}
+.tabs {
+  display: flex;
+  align-items: center;
+  > div {
+    height: 30px;
+    cursor: pointer;
+  }
+  .active {
+    color: rgb(44, 135, 255);
+    font-weight: bold;
+    border-bottom: 2px solid rgb(44, 135, 255);
+  }
+}
+.charts {
+  display: flex;
+  align-content: center;
+  justify-content: center;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/home/echarts-data.ts b/src/views/home/echarts-data.ts
new file mode 100644
index 0000000..0f39f4e
--- /dev/null
+++ b/src/views/home/echarts-data.ts
@@ -0,0 +1,181 @@
+import { EChartsOption } from 'echarts'
+
+const { t } = useI18n()
+
+export const lineOptions: EChartsOption = {
+  title: {
+    text: t('analysis.monthlySales'),
+    left: 'center'
+  },
+  xAxis: {
+    data: [
+    ],
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  },
+  legend: {
+    data: [],
+    top: 50
+  },
+  series: [
+    {
+      name: "",
+      smooth: true,
+      type: 'line',
+      data: [],
+      animationDuration: 2800,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: t('analysis.actual'),
+      smooth: true,
+      type: 'line',
+      itemStyle: {},
+      data: [],
+      animationDuration: 2800,
+      animationEasing: 'quadraticOut'
+    }
+  ]
+}
+
+export const pieOptions: EChartsOption = {
+  title: {
+    text: "",
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'item',
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'left',
+    data: [
+    ]
+  },
+  series: [
+    {
+      name: "",
+      type: 'pie',
+      radius: '55%',
+      center: ['50%', '60%'],
+      data: [
+      ]
+    }
+  ]
+}
+
+export const barOptions: EChartsOption = {
+  title: {
+    text: "",
+    left: 'center'
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: 50,
+    right: 20,
+    bottom: 20
+  },
+  xAxis: {
+    type: 'category',
+    data: [
+    ],
+    axisTick: {
+      alignWithLabel: true
+    }
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: "",
+      data: [],
+      type: 'bar'
+    }
+  ]
+}
+
+export const radarOption: EChartsOption = {
+  legend: {
+    data: []
+  },
+  radar: {
+    // shape: 'circle',
+    indicator: [
+    
+    ]
+  },
+  series: [
+    {
+      name: "",
+      type: 'radar',
+      data: [
+
+      ]
+    }
+  ]
+}
+
+export const wordOptions = {
+  series: [
+    {
+      type: 'wordCloud',
+      gridSize: 2,
+      sizeRange: [12, 50],
+      rotationRange: [-90, 90],
+      shape: 'pentagon',
+      width: 600,
+      height: 400,
+      drawOutOfBound: true,
+      textStyle: {
+        color: function () {
+          return (
+            'rgb(' +
+            [
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160),
+              Math.round(Math.random() * 160)
+            ].join(',') +
+            ')'
+          )
+        }
+      },
+      emphasis: {
+        textStyle: {
+          shadowBlur: 10,
+          shadowColor: '#333'
+        }
+      },
+      data: [
+   
+      ]
+    }
+  ]
+}
diff --git a/src/views/home/index.vue b/src/views/home/index.vue
new file mode 100644
index 0000000..ad22b79
--- /dev/null
+++ b/src/views/home/index.vue
@@ -0,0 +1,227 @@
+<template>
+  <el-row :gutter="14" justify="space-between">
+    <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24" class="mb-14px">
+      <el-card shadow="never">
+        <Echart :options="barOptionsData" :height="280" />
+      </el-card>
+    </el-col>
+    <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24" class="mb-14px">
+      <el-card shadow="never">
+        <Echart :options="lineOptionsData" :height="280" />
+      </el-card>
+    </el-col>
+    <el-col :xl="24" :lg="24" :md="24" :sm="24" :xs="24">
+      <el-card shadow="never">
+        <div class="title font-size-18px font-bold mb-10px">项目物料检测类型次数清单</div>
+        <el-table :data="tableData" style="width: 100%">
+          <el-table-column label="序号" width="80" type="index" align="center" />
+          <el-table-column prop="projectName" label="项目" align="center" />
+          <el-table-column prop="itemCode" label="物料代码" align="center" />
+          <el-table-column prop="itemName" label="物料名称" align="center" />
+          <el-table-column prop="itemTypeName" label="检查类型" align="center" />
+          <el-table-column prop="cishu" label="次数" align="center" />
+        </el-table>
+      </el-card>
+    </el-col>
+    <el-col :xl="24" :lg="24" :md="24" :sm="24" :xs="24" class="mt-14px">
+      <el-card shadow="never">
+        <div class="title font-size-18px font-bold mb-10px">项目物料检测类型时间清单</div>
+        <el-table :data="tableData1" style="width: 100%" height="450px">
+          <el-table-column label="序号" width="80" type="index" align="center" />
+          <el-table-column prop="itemCode" label="物料代码" align="center" />
+          <el-table-column prop="itemName" label="物料名称" align="center" />
+          <el-table-column prop="itemTypeName" label="检测类型" align="center" />
+          <el-table-column prop="times" label="导入时间" align="center" />
+          <el-table-column prop="createName" label="操作人" align="center" />
+        </el-table>
+      </el-card>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+import { lineOptions, barOptions } from './echarts-data'
+import * as rescordAPI from '@/api/home/shouye'
+
+defineOptions({ name: 'Home' })
+const { t } = useI18n()
+const tableData = ref([])
+const tableData1 = ref([])
+const echartsData = ref()
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+const getWeeklyUserActivity = async () => {
+  const xdata = echartsData.value.days.map((item) => item)
+  const data = echartsData.value.CHROMATIC_ABERRATION
+  const data1 = echartsData.value.DIMENSION //尺寸
+  const data2 = echartsData.value.GLOSS //光泽桔皮
+  const data3 = echartsData.value.PROPERTY //性能
+  // const data = [
+  //   { value: 13253, name: 'analysis.monday' },
+  //   { value: 34235, name: 'analysis.tuesday' },
+  //   { value: 26321, name: 'analysis.wednesday' },
+  //   { value: 12340, name: 'analysis.thursday' },
+  //   { value: 24643, name: 'analysis.friday' },
+  //   { value: 1322, name: 'analysis.saturday' },
+  //   { value: 1324, name: 'analysis.sunday' }
+  // ]
+  // const data1 = [
+  //   { value: 23444, name: 'analysis.monday' },
+  //   { value: 12344, name: 'analysis.tuesday' },
+  //   { value: 34455, name: 'analysis.wednesday' },
+  //   { value: 3434, name: 'analysis.thursday' },
+  //   { value: 43333, name: 'analysis.friday' },
+  //   { value: 45555, name: 'analysis.saturday' },
+  //   { value: 1324, name: 'analysis.sunday' }
+  // ]
+  // const data2 = [
+  //   { value: 4344, name: 'analysis.monday' },
+  //   { value: 34454, name: 'analysis.tuesday' },
+  //   { value: 4322, name: 'analysis.wednesday' },
+  //   { value: 9897, name: 'analysis.thursday' },
+  //   { value: 89076, name: 'analysis.friday' },
+  //   { value: 5434, name: 'analysis.saturday' },
+  //   { value: 67674, name: 'analysis.sunday' }
+  // ]
+  // const data3 = [
+  //   { value: 4344, name: 'analysis.monday' },
+  //   { value: 34454, name: 'analysis.tuesday' },
+  //   { value: 4322, name: 'analysis.wednesday' },
+  //   { value: 9897, name: 'analysis.thursday' },
+  //   { value: 89076, name: 'analysis.friday' },
+  //   { value: 5434, name: 'analysis.saturday' },
+  //   { value: 67674, name: 'analysis.sunday' }
+  // ]
+  set(barOptionsData, 'title', {
+    text: '检测类型导入次数(近7天)',
+    left: 0,
+    textStyle: {
+      fontSize: 14
+    }
+  })
+  set(barOptionsData, 'xAxis.data', xdata)
+  set(barOptionsData, 'legend', {
+    data: ['色差', '尺寸', '光泽桔皮', '性能'],
+    top: 0,
+    right: 20
+  })
+  set(barOptionsData, 'series', [
+    {
+      name: '色差',
+      data: data,
+      // data: data.map((v) => v.value),
+      type: 'bar',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '尺寸',
+      data: data1,
+      type: 'bar',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '光泽桔皮',
+      data: data2,
+      type: 'bar',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '性能',
+      data: data3,
+      type: 'bar',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    }
+  ])
+}
+
+const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
+const getWeeklyUserActivity1 = async () => {
+  const xdata = echartsData.value.days
+  const data = echartsData.value.CHROMATIC_ABERRATION //色差
+  const data1 = echartsData.value.DIMENSION //尺寸
+  const data2 = echartsData.value.GLOSS //光泽桔皮
+  const data3 = echartsData.value.PROPERTY //性能
+  set(lineOptionsData, 'title', {
+    text: '检测类型导入次数(近7天)',
+    left: 0,
+    textStyle: {
+      fontSize: 14
+    }
+  })
+  set(lineOptionsData, 'xAxis.data', xdata)
+  set(lineOptionsData, 'legend', {
+    data: ['色差', '尺寸', '光泽桔皮', '性能'],
+    top: 0,
+    right: 20
+  })
+  set(lineOptionsData, 'series', [
+    {
+      name: '色差',
+      data: data,
+      smooth: true,
+      type: 'line',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '尺寸',
+      data: data1,
+      smooth: true,
+      type: 'line',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '光泽桔皮',
+      data: data2,
+      smooth: true,
+      type: 'line',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    },
+    {
+      name: '性能',
+      data: data3,
+      smooth: true,
+      type: 'line',
+      animationDuration: 2000,
+      animationEasing: 'cubicInOut'
+    }
+  ])
+}
+const handleResize = () => {
+  window.location.reload()
+}
+onMounted(async () => {
+  //初始化方法
+  tableData.value = await rescordAPI.getSYFrequency()
+  tableData1.value = await rescordAPI.getSYTime()
+  echartsData.value = await rescordAPI.getSYEchartsData()
+  await getWeeklyUserActivity()
+  await getWeeklyUserActivity1()
+  window.addEventListener('resize', handleResize)
+})
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+})
+</script>
+<style lang="scss" scoped>
+.title {
+  position: relative;
+  padding-left: 16px;
+  &::after {
+    content: '';
+    position: absolute;
+    left: 0px;
+    width: 5px;
+    height: 20px;
+    background: #409eff;
+    top: 50%;
+    margin-top: -9px;
+  }
+}
+</style>
diff --git a/src/views/home/types.ts b/src/views/home/types.ts
new file mode 100644
index 0000000..e6313d3
--- /dev/null
+++ b/src/views/home/types.ts
@@ -0,0 +1,55 @@
+export type WorkplaceTotal = {
+  project: number
+  access: number
+  todo: number
+}
+
+export type Project = {
+  name: string
+  icon: string
+  message: string
+  personal: string
+  time: Date | number | string
+}
+
+export type Notice = {
+  title: string
+  type: string
+  keys: string[]
+  date: Date | number | string
+}
+
+export type Shortcut = {
+  name: string
+  icon: string
+  url: string
+}
+
+export type RadarData = {
+  personal: number
+  team: number
+  max: number
+  name: string
+}
+export type AnalysisTotalTypes = {
+  users: number
+  messages: number
+  moneys: number
+  shoppings: number
+}
+
+export type UserAccessSource = {
+  value: number
+  name: string
+}
+
+export type WeeklyUserActivity = {
+  value: number
+  name: string
+}
+
+export type MonthlySales = {
+  name: string
+  estimate: number
+  actual: number
+}
diff --git a/src/views/login/components/LoginForm.vue b/src/views/login/components/LoginForm.vue
new file mode 100644
index 0000000..740a6fc
--- /dev/null
+++ b/src/views/login/components/LoginForm.vue
@@ -0,0 +1,287 @@
+<template>
+  <el-form v-show="getShow" ref="formLogin" :model="loginData.loginForm" :rules="LoginRules" class="login-form"
+    label-position="top" label-width="120px" size="large">
+    <el-row style="margin-right: -10px; margin-left: -10px">
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <!-- <LoginFormTitle style="width: 100%" /> -->
+          <div class="font-size-18px">您好,欢迎使用<span class="color-[rgb(74,154,236)]">长春华涛SPC系统</span></div>
+        </el-form-item>
+      </el-col>
+      <!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item v-if="loginData.tenantEnable === 'true'"  label="租户名称">
+          <el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
+             link type="primary" />
+        </el-form-item>
+      </el-col> -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item  label="用户名">
+          <el-input v-model="loginData.loginForm.username" :placeholder="t('login.usernamePlaceholder')"
+            :prefix-icon="iconAvatar" style="height: 42px;" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item label="密码">
+          <el-input v-model="loginData.loginForm.password" :placeholder="t('login.passwordPlaceholder')"
+            :prefix-icon="iconLock" show-password type="password" @keyup.enter="getCode()" style="height: 42px;" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="code"  label="验证码">
+          <div class="flex w-[100%]">
+            <el-input v-model="loginData.loginForm.code" :placeholder="t('login.codePlaceholder')"
+              style="width: 76%;margin-right: 10px;height: 42px;" @keyup.enter="handleLogin()">
+              <template #prefix>
+                <img src="@/assets/imgs/code.png" alt="" style="width: 16px;height: 16px;" />
+              </template>
+            </el-input>
+            <div class="login-code flex-1">
+              <img :src="codeUrl" @click="getCode" class="login-code-img" />
+            </div>
+          </div>
+
+        </el-form-item>
+      </el-col>
+
+
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px">
+        <el-form-item>
+          <el-row justify="space-between" style="width: 100%">
+            <el-col :span="6">
+              <el-checkbox v-model="loginData.loginForm.rememberMe">
+                {{ t('login.remember') }}
+              </el-checkbox>
+            </el-col>
+            <el-col :offset="6" :span="12">
+              <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
+            </el-col>
+          </el-row>
+        </el-form-item>
+      </el-col>
+
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton :loading="loginLoading" :title="t('login.login')" class="w-[100%]" type="primary"
+            @click="handleLogin()" />
+        </el-form-item>
+      </el-col>
+      <!-- <Verify
+        ref="verify"
+        :captchaType="captchaType"
+        :imgSize="{ width: '400px', height: '200px' }"
+        mode="pop"
+        @success="handleLogin"
+      /> -->
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { ElLoading } from 'element-plus'
+import LoginFormTitle from './LoginFormTitle.vue'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import * as authUtil from '@/utils/auth'
+import { usePermissionStore } from '@/store/modules/permission'
+import * as LoginApi from '@/api/login'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { getCodeImg } from "@/api/login";
+
+defineOptions({ name: 'LoginForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconAvatar = useIcon({ icon: 'ep:avatar' })
+const iconLock = useIcon({ icon: 'ep:lock' })
+const formLogin = ref()
+const { validForm } = useFormValid(formLogin)
+const { setLoginState, getLoginState } = useLoginState()
+const { currentRoute, push } = useRouter()
+const permissionStore = usePermissionStore()
+const redirect = ref<string>('')
+const loginLoading = ref(false)
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
+// 验证码开关
+const captchaEnabled = ref(true);
+const codeUrl = ref("");
+
+const LoginRules = {
+  tenantName: [required],
+  username: [required],
+  password: [required]
+}
+const loginData = reactive({
+  isShowPassword: false,
+  captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+  tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+  loginForm: {
+    tenantName: '长春华涛',
+    username: 'admin',
+    password: '123456',
+    captchaVerification: '',
+    rememberMe: false,
+    code: '',
+    uuid: ''
+  }
+})
+
+const socialList = [
+  { icon: 'ant-design:github-filled', type: 0 },
+  { icon: 'ant-design:wechat-filled', type: 30 },
+  { icon: 'ant-design:alipay-circle-filled', type: 0 },
+  { icon: 'ant-design:dingtalk-circle-filled', type: 20 }
+]
+
+// 获取验证码
+// const getCode = async () => {
+//   // 情况一,未开启:则直接登录
+//   if (loginData.captchaEnable === 'false') {
+//     await handleLogin({})
+//   } else {
+//     // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
+//     // 弹出验证码
+//     verify.value.show()
+//   }
+// }
+function getCode() {
+  getCodeImg().then(res => {
+    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
+    if (captchaEnabled.value) {
+      codeUrl.value = "data:image/gif;base64," + res.img;
+      loginData.loginForm.uuid = res.uuid;
+    }
+  });
+}
+//获取租户ID
+const getTenantId = async () => {
+  if (loginData.tenantEnable === 'true') {
+    const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+    authUtil.setTenantId(res)
+  }
+}
+// 记住我
+const getCookie = () => {
+  const loginForm = authUtil.getLoginForm()
+  if (loginForm) {
+    loginData.loginForm = {
+      ...loginData.loginForm,
+      username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+      password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+      rememberMe: loginForm.rememberMe ? true : false,
+      tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+    }
+  }
+}
+const loading = ref() // ElLoading.service 返回的实例
+// 登录
+const handleLogin = async (params) => {
+  loginLoading.value = true
+  try {
+    await getTenantId()
+    const data = await validForm()
+    if (!data) {
+      return
+    }
+    const res = await LoginApi.login(loginData.loginForm)
+    if (!res) {
+      getCode()
+      return
+    }
+    loading.value = ElLoading.service({
+      lock: true,
+      text: '正在加载系统中...',
+      background: 'rgba(0, 0, 0, 0.7)'
+    })
+    if (loginData.loginForm.rememberMe) {
+      authUtil.setLoginForm(loginData.loginForm)
+    } else {
+      authUtil.removeLoginForm()
+    }
+    authUtil.setToken(res)
+    if (!redirect.value) {
+      redirect.value = '/'
+    }
+    // 判断是否为SSO登录
+    if (redirect.value.indexOf('sso') !== -1) {
+      window.location.href = window.location.href.replace('/login?redirect=', '')
+    } else {
+      console.log(redirect.value)
+      push({ path: redirect.value || permissionStore.addRouters[0].path })
+    }
+  } finally {
+    getCode()
+    loginLoading.value = false
+    loading.value.close()
+  }
+}
+
+// 社交登录
+const doSocialLogin = async (type: number) => {
+  if (type === 0) {
+    message.error('此方式未配置')
+  } else {
+    loginLoading.value = true
+    if (loginData.tenantEnable === 'true') {
+      await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
+        const res = await LoginApi.getTenantIdByName(value)
+        authUtil.setTenantId(res)
+      })
+    }
+    // 计算 redirectUri
+    const redirectUri =
+      location.origin + '/social-login?type=' + type + '&redirect=' + (redirect.value || '/')
+    // 进行跳转
+    const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
+    console.log(33)
+    window.location.href = res
+  }
+}
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    redirect.value = route?.query?.redirect as string
+  },
+  {
+    immediate: true
+  }
+)
+onMounted(() => {
+  getCode();
+  getCookie()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+  &:hover {
+    color: var(--el-color-primary) !important;
+  }
+}
+
+.login-code {
+  float: right;
+  width: 100%;
+  height: 38px;
+
+  img {
+    width: 100%;
+    height: auto;
+    max-width: 100px;
+    vertical-align: middle;
+    cursor: pointer;
+  }
+}
+.login-form{
+  background: white;
+  width: 100%;
+  box-shadow: 0px 0px 20px rgba(0,0,0,0.1)
+}
+::v-deep.el-form-item--large{
+  // margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/login/components/LoginFormTitle.vue b/src/views/login/components/LoginFormTitle.vue
new file mode 100644
index 0000000..cdf4fac
--- /dev/null
+++ b/src/views/login/components/LoginFormTitle.vue
@@ -0,0 +1,26 @@
+<template>
+  <h2 class="enter-x mb-3 text-center text-2xl font-bold xl:text-center xl:text-3xl">
+    {{ getFormTitle }}
+  </h2>
+</template>
+<script lang="ts" setup>
+import { LoginStateEnum, useLoginState } from './useLogin'
+
+defineOptions({ name: 'LoginFormTitle' })
+
+const { t } = useI18n()
+
+const { getLoginState } = useLoginState()
+
+const getFormTitle = computed(() => {
+  const titleObj = {
+    [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
+    [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
+    [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
+    [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
+    [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
+    [LoginStateEnum.SSO]: t('sys.login.ssoFormTitle')
+  }
+  return titleObj[unref(getLoginState)]
+})
+</script>
diff --git a/src/views/login/components/MobileForm.vue b/src/views/login/components/MobileForm.vue
new file mode 100644
index 0000000..29f704c
--- /dev/null
+++ b/src/views/login/components/MobileForm.vue
@@ -0,0 +1,225 @@
+<template>
+  <el-form
+    v-show="getShow"
+    ref="formSmsLogin"
+    :model="loginData.loginForm"
+    :rules="rules"
+    class="login-form"
+    label-position="top"
+    label-width="120px"
+    size="large"
+  >
+    <el-row style="margin-right: -10px; margin-left: -10px">
+      <!-- 租户名 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <LoginFormTitle style="width: 100%" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
+          <el-input
+            v-model="loginData.loginForm.tenantName"
+            :placeholder="t('login.tenantNamePlaceholder')"
+            :prefix-icon="iconHouse"
+            type="primary"
+            link
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 手机号 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="mobileNumber">
+          <el-input
+            v-model="loginData.loginForm.mobileNumber"
+            :placeholder="t('login.mobileNumberPlaceholder')"
+            :prefix-icon="iconCellphone"
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 验证码 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="code">
+          <el-row :gutter="5" justify="space-between" style="width: 100%">
+            <el-col :span="24">
+              <el-input
+                v-model="loginData.loginForm.code"
+                :placeholder="t('login.codePlaceholder')"
+                :prefix-icon="iconCircleCheck"
+              >
+                <!-- <el-button class="w-[100%]"> -->
+                <template #append>
+                  <span
+                    v-if="mobileCodeTimer <= 0"
+                    class="getMobileCode"
+                    style="cursor: pointer"
+                    @click="getSmsCode"
+                  >
+                    {{ t('login.getSmsCode') }}
+                  </span>
+                  <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
+                    {{ mobileCodeTimer }}秒后可重新获取
+                  </span>
+                </template>
+              </el-input>
+              <!-- </el-button> -->
+            </el-col>
+          </el-row>
+        </el-form-item>
+      </el-col>
+      <!-- 登录按钮 / 返回按钮 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.login')"
+            class="w-[100%]"
+            type="primary"
+            @click="signIn()"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.backLogin')"
+            class="w-[100%]"
+            @click="handleBackLogin()"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import { setTenantId, setToken } from '@/utils/auth'
+import { usePermissionStore } from '@/store/modules/permission'
+import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+
+defineOptions({ name: 'MobileForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+const permissionStore = usePermissionStore()
+const { currentRoute, push } = useRouter()
+const formSmsLogin = ref()
+const loginLoading = ref(false)
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconCellphone = useIcon({ icon: 'ep:cellphone' })
+const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
+const { validForm } = useFormValid(formSmsLogin)
+const { handleBackLogin, getLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
+
+const rules = {
+  tenantName: [required],
+  mobileNumber: [required],
+  code: [required]
+}
+const loginData = reactive({
+  codeImg: '',
+  tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+  token: '',
+  loading: {
+    signIn: false
+  },
+  loginForm: {
+    uuid: '',
+    tenantName: '长春华涛',
+    mobileNumber: '',
+    code: ''
+  }
+})
+const smsVO = reactive({
+  smsCode: {
+    mobile: '',
+    scene: 21
+  },
+  loginSms: {
+    mobile: '',
+    code: ''
+  }
+})
+const mobileCodeTimer = ref(0)
+const redirect = ref<string>('')
+const getSmsCode = async () => {
+  await getTenantId()
+  smsVO.smsCode.mobile = loginData.loginForm.mobileNumber
+  await sendSmsCode(smsVO.smsCode).then(async () => {
+    message.success(t('login.SmsSendMsg'))
+    // 设置倒计时
+    mobileCodeTimer.value = 60
+    let msgTimer = setInterval(() => {
+      mobileCodeTimer.value = mobileCodeTimer.value - 1
+      if (mobileCodeTimer.value <= 0) {
+        clearInterval(msgTimer)
+      }
+    }, 1000)
+  })
+}
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    redirect.value = route?.query?.redirect as string
+  },
+  {
+    immediate: true
+  }
+)
+// 获取租户 ID
+const getTenantId = async () => {
+  if (loginData.tenantEnable === 'true') {
+    const res = await getTenantIdByName(loginData.loginForm.tenantName)
+    setTenantId(res)
+  }
+}
+// 登录
+const signIn = async () => {
+  await getTenantId()
+  const data = await validForm()
+  if (!data) return
+  ElLoading.service({
+    lock: true,
+    text: '正在加载系统中...',
+    background: 'rgba(0, 0, 0, 0.7)'
+  })
+  loginLoading.value = true
+  smsVO.loginSms.mobile = loginData.loginForm.mobileNumber
+  smsVO.loginSms.code = loginData.loginForm.code
+  await smsLogin(smsVO.loginSms)
+    .then(async (res) => {
+      setToken(res)
+      if (!redirect.value) {
+        redirect.value = '/'
+      }
+      push({ path: redirect.value || permissionStore.addRouters[0].path })
+    })
+    .catch(() => {})
+    .finally(() => {
+      loginLoading.value = false
+      setTimeout(() => {
+        const loadingInstance = ElLoading.service()
+        loadingInstance.close()
+      }, 400)
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+  &:hover {
+    color: var(--el-color-primary) !important;
+  }
+}
+
+.smsbtn {
+  margin-top: 33px;
+}
+</style>
diff --git a/src/views/login/components/QrCodeForm.vue b/src/views/login/components/QrCodeForm.vue
new file mode 100644
index 0000000..31d2845
--- /dev/null
+++ b/src/views/login/components/QrCodeForm.vue
@@ -0,0 +1,30 @@
+<template>
+  <el-row v-show="getShow" style="margin-right: -10px; margin-left: -10px">
+    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <LoginFormTitle style="width: 100%" />
+    </el-col>
+    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <el-card class="mb-10px text-center" shadow="hover">
+        <Qrcode :logo="logoImg" />
+      </el-card>
+    </el-col>
+    <el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
+    <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <div class="mt-15px w-[100%]">
+        <XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
+      </div>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import logoImg from '@/assets/imgs/logo.png'
+
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useLoginState } from './useLogin'
+
+defineOptions({ name: 'QrCodeForm' })
+
+const { t } = useI18n()
+const { handleBackLogin, getLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
+</script>
diff --git a/src/views/login/components/RegisterForm.vue b/src/views/login/components/RegisterForm.vue
new file mode 100644
index 0000000..23b3bd4
--- /dev/null
+++ b/src/views/login/components/RegisterForm.vue
@@ -0,0 +1,142 @@
+<template>
+  <Form
+    v-show="getShow"
+    :rules="rules"
+    :schema="schema"
+    class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
+    hide-required-asterisk
+    label-position="top"
+    size="large"
+    @register="register"
+  >
+    <template #title>
+      <LoginFormTitle style="width: 100%" />
+    </template>
+
+    <template #code="form">
+      <div class="w-[100%] flex">
+        <el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
+      </div>
+    </template>
+
+    <template #register>
+      <div class="w-[100%]">
+        <XButton
+          :loading="loading"
+          :title="t('login.register')"
+          class="w-[100%]"
+          type="primary"
+          @click="loginRegister()"
+        />
+      </div>
+      <div class="mt-15px w-[100%]">
+        <XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
+      </div>
+    </template>
+  </Form>
+</template>
+<script lang="ts" setup>
+import type { FormRules } from 'element-plus'
+
+import { useForm } from '@/hooks/web/useForm'
+import { useValidator } from '@/hooks/web/useValidator'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useLoginState } from './useLogin'
+import { FormSchema } from '@/types/form'
+
+defineOptions({ name: 'RegisterForm' })
+
+const { t } = useI18n()
+const { required } = useValidator()
+const { register, elFormRef } = useForm()
+const { handleBackLogin, getLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
+
+const schema = reactive<FormSchema[]>([
+  {
+    field: 'title',
+    colProps: {
+      span: 24
+    }
+  },
+  {
+    field: 'username',
+    label: t('login.username'),
+    value: '',
+    component: 'Input',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      placeholder: t('login.usernamePlaceholder')
+    }
+  },
+  {
+    field: 'password',
+    label: t('login.password'),
+    value: '',
+    component: 'InputPassword',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      style: {
+        width: '100%'
+      },
+      strength: true,
+      placeholder: t('login.passwordPlaceholder')
+    }
+  },
+  {
+    field: 'check_password',
+    label: t('login.checkPassword'),
+    value: '',
+    component: 'InputPassword',
+    colProps: {
+      span: 24
+    },
+    componentProps: {
+      style: {
+        width: '100%'
+      },
+      strength: true,
+      placeholder: t('login.passwordPlaceholder')
+    }
+  },
+  {
+    field: 'code',
+    label: t('login.code'),
+    colProps: {
+      span: 24
+    }
+  },
+  {
+    field: 'register',
+    colProps: {
+      span: 24
+    }
+  }
+])
+
+const rules: FormRules = {
+  username: [required()],
+  password: [required()],
+  check_password: [required()],
+  code: [required()]
+}
+
+const loading = ref(false)
+
+const loginRegister = async () => {
+  const formRef = unref(elFormRef)
+  formRef?.validate(async (valid) => {
+    if (valid) {
+      try {
+        loading.value = true
+      } finally {
+        loading.value = false
+      }
+    }
+  })
+}
+</script>
diff --git a/src/views/login/components/SSOLogin.vue b/src/views/login/components/SSOLogin.vue
new file mode 100644
index 0000000..f31ab0e
--- /dev/null
+++ b/src/views/login/components/SSOLogin.vue
@@ -0,0 +1,199 @@
+<template>
+  <div v-show="ssoVisible" class="form-cont">
+    <!-- 应用名 -->
+    <LoginFormTitle style="width: 100%" />
+    <el-tabs class="form" style="float: none" value="uname">
+      <el-tab-pane :label="client.name" name="uname" />
+    </el-tabs>
+    <div>
+      <el-form :model="formData" class="login-form">
+        <!-- 授权范围的选择 -->
+        此第三方应用请求获得以下权限:
+        <el-form-item prop="scopes">
+          <el-checkbox-group v-model="formData.scopes">
+            <el-checkbox
+              v-for="scope in queryParams.scopes"
+              :key="scope"
+              :label="scope"
+              style="display: block; margin-bottom: -10px"
+            >
+              {{ formatScope(scope) }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <!-- 下方的登录按钮 -->
+        <el-form-item class="w-1/1">
+          <el-button
+            :loading="formLoading"
+            class="w-6/10"
+            type="primary"
+            @click.prevent="handleAuthorize(true)"
+          >
+            <span v-if="!formLoading">同意授权</span>
+            <span v-else>授 权 中...</span>
+          </el-button>
+          <el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import LoginFormTitle from './LoginFormTitle.vue'
+import * as OAuth2Api from '@/api/login/oauth2'
+import { LoginStateEnum, useLoginState } from './useLogin'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+defineOptions({ name: 'SSOLogin' })
+
+const route = useRoute() // 路由
+const { currentRoute } = useRouter() // 路由
+const { getLoginState, setLoginState } = useLoginState()
+
+const client = ref({
+  // 客户端信息
+  name: '',
+  logo: ''
+})
+interface queryType {
+  responseType: string
+  clientId: string
+  redirectUri: string
+  state: string
+  scopes: string[]
+}
+const queryParams = reactive<queryType>({
+  // URL 上的 client_id、scope 等参数
+  responseType: '',
+  clientId: '',
+  redirectUri: '',
+  state: '',
+  scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
+})
+const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单
+interface formType {
+  scopes: string[]
+}
+const formData = reactive<formType>({
+  scopes: [] // 已选中的 scope 数组
+})
+const formLoading = ref(false) // 表单是否提交中
+
+/** 初始化授权信息 */
+const init = async () => {
+  // 防止在没有登录的情况下循环弹窗
+  if (typeof route.query.client_id === 'undefined') return
+  // 解析参数
+  // 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
+  // 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
+  queryParams.responseType = route.query.response_type as string
+  queryParams.clientId = route.query.client_id as string
+  queryParams.redirectUri = route.query.redirect_uri as string
+  queryParams.state = route.query.state as string
+  if (route.query.scope) {
+    queryParams.scopes = (route.query.scope as string).split(' ')
+  }
+
+  // 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
+  if (queryParams.scopes.length > 0) {
+    const data = await doAuthorize(true, queryParams.scopes, [])
+    if (data) {
+      location.href = data
+      return
+    }
+  }
+
+  // 获取授权页的基本信息
+  const data = await OAuth2Api.getAuthorize(queryParams.clientId)
+  client.value = data.client
+  // 解析 scope
+  let scopes
+  // 1.1 如果 params.scope 非空,则过滤下返回的 scopes
+  if (queryParams.scopes.length > 0) {
+    scopes = []
+    for (const scope of data.scopes) {
+      if (queryParams.scopes.indexOf(scope.key) >= 0) {
+        scopes.push(scope)
+      }
+    }
+    // 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
+  } else {
+    scopes = data.scopes
+    for (const scope of scopes) {
+      queryParams.scopes.push(scope.key)
+    }
+  }
+  // 生成已选中的 checkedScopes
+  for (const scope of scopes) {
+    if (scope.value) {
+      formData.scopes.push(scope.key)
+    }
+  }
+}
+
+/** 处理授权的提交 */
+const handleAuthorize = async (approved) => {
+  // 计算 checkedScopes + uncheckedScopes
+  let checkedScopes
+  let uncheckedScopes
+  if (approved) {
+    // 同意授权,按照用户的选择
+    checkedScopes = formData.scopes
+    uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1)
+  } else {
+    // 拒绝,则都是取消
+    checkedScopes = []
+    uncheckedScopes = queryParams.scopes
+  }
+  // 提交授权的请求
+  formLoading.value = true
+  try {
+    const data = await doAuthorize(false, checkedScopes, uncheckedScopes)
+    if (!data) {
+      return
+    }
+    location.href = data
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 调用授权 API 接口 */
+const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
+  return OAuth2Api.authorize(
+    queryParams.responseType,
+    queryParams.clientId,
+    queryParams.redirectUri,
+    queryParams.state,
+    autoApprove,
+    checkedScopes,
+    uncheckedScopes
+  )
+}
+
+/** 格式化 scope 文本 */
+const formatScope = (scope) => {
+  // 格式化 scope 授权范围,方便用户理解。
+  // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
+  switch (scope) {
+    case 'user.read':
+      return '访问你的个人信息'
+    case 'user.write':
+      return '修改你的个人信息'
+    default:
+      return scope
+  }
+}
+
+/** 监听当前路由为 SSOLogin 时,进行数据的初始化 */
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    if (route.name === 'SSOLogin') {
+      setLoginState(LoginStateEnum.SSO)
+      init()
+    }
+  },
+  { immediate: true }
+)
+</script>
diff --git a/src/views/login/components/index.ts b/src/views/login/components/index.ts
new file mode 100644
index 0000000..204ad73
--- /dev/null
+++ b/src/views/login/components/index.ts
@@ -0,0 +1,8 @@
+import LoginForm from './LoginForm.vue'
+import MobileForm from './MobileForm.vue'
+import LoginFormTitle from './LoginFormTitle.vue'
+import RegisterForm from './RegisterForm.vue'
+import QrCodeForm from './QrCodeForm.vue'
+import SSOLoginVue from './SSOLogin.vue'
+
+export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
diff --git a/src/views/login/components/useLogin.ts b/src/views/login/components/useLogin.ts
new file mode 100644
index 0000000..b4a02f8
--- /dev/null
+++ b/src/views/login/components/useLogin.ts
@@ -0,0 +1,42 @@
+import { Ref } from 'vue'
+
+export enum LoginStateEnum {
+  LOGIN,
+  REGISTER,
+  RESET_PASSWORD,
+  MOBILE,
+  QR_CODE,
+  SSO
+}
+
+const currentState = ref(LoginStateEnum.LOGIN)
+
+export function useLoginState() {
+  function setLoginState(state: LoginStateEnum) {
+    currentState.value = state
+  }
+  const getLoginState = computed(() => currentState.value)
+
+  function handleBackLogin() {
+    setLoginState(LoginStateEnum.LOGIN)
+  }
+
+  return {
+    setLoginState,
+    getLoginState,
+    handleBackLogin
+  }
+}
+
+export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
+  async function validForm() {
+    const form = unref(formRef)
+    if (!form) return
+    const data = await form.validate()
+    return data as T
+  }
+
+  return {
+    validForm
+  }
+}
diff --git a/src/views/login/login.vue b/src/views/login/login.vue
new file mode 100644
index 0000000..48965b7
--- /dev/null
+++ b/src/views/login/login.vue
@@ -0,0 +1,111 @@
+<template>
+  <div
+    :class="prefixCls"
+    class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
+  >
+  
+  <div class="box">
+    <img src="../../assets/imgs/logo_huatao.png" alt="" class="logo"/>
+ 
+    <div class="absolute w-[33%] mx-auto h-full flex right-[7%]">
+      <div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px">
+        <!-- 右上角的主题、语言选择 -->
+        <div
+          class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
+        >
+          <div class="flex items-center at-2xl:hidden at-xl:hidden">
+            <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+            <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+          </div>
+          <div class="flex items-center justify-end space-x-10px">
+            <!-- <ThemeSwitch /> -->
+            <LocaleDropdown class="dark:text-white lt-xl:text-white" />
+          </div>
+        </div>
+        <!-- 右边的登录界面 -->
+        <Transition appear enter-active-class="animate__animated animate__bounceInRight">
+          <div
+            class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
+          >
+            <!-- 账号登录 -->
+            <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 手机登录 -->
+            <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 二维码登录 -->
+            <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 注册 -->
+            <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 三方登录 -->
+            <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+          </div>
+        </Transition>
+      </div>
+    </div>
+    <div class="footer">
+      <div>Copyright ©2012-2023闻荫科技吉ICP备100114504646号-6 ICP证书:吉B2-2018005497977吉林工商吉公网安备3101150204979086755号</div>
+      <div>出版物经营许可证(吉)批字第Y8813号广播电视节目制作经营许可正(吉)字第03283号网络文化经营许可证吉网文{2012)1460-055号</div>
+    </div>
+  </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { underlineToHump } from '@/utils'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import { useAppStore } from '@/store/modules/app'
+import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+
+import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
+
+defineOptions({ name: 'Login' })
+
+const { t } = useI18n()
+const appStore = useAppStore()
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('login')
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-login;
+
+.#{$prefix-cls} {
+  overflow: auto;
+
+  &__left {
+    &::before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: -1;
+      width: 100%;
+      height: 100%;
+      background-image: url('@/assets/svgs/login-bg.svg');
+      background-position: center;
+      background-repeat: no-repeat;
+      content: '';
+    }
+  }
+}
+.box{
+  background: url(../../assets/imgs/bg_login.png) no-repeat top left;
+  background-size: 100% 100%;
+  width: 100%;
+  height:100%;
+}
+.logo{
+  position: absolute;
+  left: 4vw;
+  top: 5vh;
+  width: 30%;
+}
+.footer{
+  position: absolute;
+  bottom:4vh;
+  text-align: center;
+  padding: 0px 30px;
+  width: calc(100% - 60px);
+  color: #989898;
+  font-size: 14px;
+}
+</style>
diff --git a/src/views/profile/components/BasicInfo.vue b/src/views/profile/components/BasicInfo.vue
new file mode 100644
index 0000000..e2189b1
--- /dev/null
+++ b/src/views/profile/components/BasicInfo.vue
@@ -0,0 +1,92 @@
+<template>
+  <Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema">
+    <template #sex="form">
+      <el-radio-group v-model="form['sex']">
+        <el-radio :label="1">{{ t('profile.user.man') }}</el-radio>
+        <el-radio :label="2">{{ t('profile.user.woman') }}</el-radio>
+      </el-radio-group>
+    </template>
+  </Form>
+  <div style="text-align: center">
+    <XButton :title="t('common.save')" type="primary" @click="submit()" />
+    <XButton :title="t('common.reset')" type="danger" @click="init()" />
+  </div>
+</template>
+<script lang="ts" setup>
+import type { FormRules } from 'element-plus'
+import { FormSchema } from '@/types/form'
+import type { FormExpose } from '@/components/Form'
+import {
+  getUserProfile,
+  updateUserProfile,
+  UserProfileUpdateReqVO
+} from '@/api/system/user/profile'
+
+defineOptions({ name: 'BasicInfo' })
+
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+// 表单校验
+const rules = reactive<FormRules>({
+  nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
+  email: [
+    { required: true, message: t('profile.rules.mail'), trigger: 'blur' },
+    {
+      type: 'email',
+      message: t('profile.rules.truemail'),
+      trigger: ['blur', 'change']
+    }
+  ],
+  mobile: [
+    { required: true, message: t('profile.rules.phone'), trigger: 'blur' },
+    {
+      pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+      message: t('profile.rules.truephone'),
+      trigger: 'blur'
+    }
+  ]
+})
+const schema = reactive<FormSchema[]>([
+  {
+    field: 'nickname',
+    label: t('profile.user.nickname'),
+    component: 'Input'
+  },
+  {
+    field: 'mobile',
+    label: t('profile.user.mobile'),
+    component: 'Input'
+  },
+  {
+    field: 'email',
+    label: t('profile.user.email'),
+    component: 'Input'
+  },
+  {
+    field: 'sex',
+    label: t('profile.user.sex'),
+    component: 'InputNumber',
+    value: 0
+  }
+])
+const formRef = ref<FormExpose>() // 表单 Ref
+const submit = () => {
+  const elForm = unref(formRef)?.getElFormRef()
+  if (!elForm) return
+  elForm.validate(async (valid) => {
+    if (valid) {
+      const data = unref(formRef)?.formModel as UserProfileUpdateReqVO
+      await updateUserProfile(data)
+      message.success(t('common.updateSuccess'))
+      await init()
+    }
+  })
+}
+const init = async () => {
+  const res = await getUserProfile()
+  unref(formRef)?.setValues(res)
+}
+onMounted(async () => {
+  await init()
+})
+</script>
diff --git a/src/views/profile/components/ProfileUser.vue b/src/views/profile/components/ProfileUser.vue
new file mode 100644
index 0000000..b493499
--- /dev/null
+++ b/src/views/profile/components/ProfileUser.vue
@@ -0,0 +1,99 @@
+<template>
+  <div>
+    <div class="text-center">
+      <UserAvatar :img="userInfo?.avatar" />
+    </div>
+    <ul class="list-group list-group-striped">
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="ep:user" />
+        {{ t('profile.user.username') }}
+        <div class="pull-right">{{ userInfo?.username }}</div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="ep:phone" />
+        {{ t('profile.user.mobile') }}
+        <div class="pull-right">{{ userInfo?.mobile }}</div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="fontisto:email" />
+        {{ t('profile.user.email') }}
+        <div class="pull-right">{{ userInfo?.email }}</div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="carbon:tree-view-alt" />
+        {{ t('profile.user.dept') }}
+        <div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="ep:suitcase" />
+        {{ t('profile.user.posts') }}
+        <div v-if="userInfo?.posts" class="pull-right">
+          {{ userInfo?.posts.map((post) => post.name).join(',') }}
+        </div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="icon-park-outline:peoples" />
+        {{ t('profile.user.roles') }}
+        <div v-if="userInfo?.roles" class="pull-right">
+          {{ userInfo?.roles.map((role) => role.name).join(',') }}
+        </div>
+      </li>
+      <li class="list-group-item">
+        <Icon class="mr-5px" icon="ep:calendar" />
+        {{ t('profile.user.createTime') }}
+        <div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div>
+      </li>
+    </ul>
+  </div>
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import UserAvatar from './UserAvatar.vue'
+
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+
+defineOptions({ name: 'ProfileUser' })
+
+const { t } = useI18n()
+const userInfo = ref<ProfileVO>()
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+}
+onMounted(async () => {
+  await getUserInfo()
+})
+</script>
+
+<style scoped>
+.text-center {
+  position: relative;
+  height: 120px;
+  text-align: center;
+}
+
+.list-group-striped > .list-group-item {
+  padding-right: 0;
+  padding-left: 0;
+  border-right: 0;
+  border-left: 0;
+  border-radius: 0;
+}
+
+.list-group {
+  padding-left: 0;
+  list-style: none;
+}
+
+.list-group-item {
+  padding: 11px 0;
+  margin-bottom: -1px;
+  font-size: 13px;
+  border-top: 1px solid #e7eaec;
+  border-bottom: 1px solid #e7eaec;
+}
+
+.pull-right {
+  float: right !important;
+}
+</style>
diff --git a/src/views/profile/components/ResetPwd.vue b/src/views/profile/components/ResetPwd.vue
new file mode 100644
index 0000000..477be91
--- /dev/null
+++ b/src/views/profile/components/ResetPwd.vue
@@ -0,0 +1,73 @@
+<template>
+  <el-form ref="formRef" :model="password" :rules="rules" :label-width="200">
+    <el-form-item :label="t('profile.password.oldPassword')" prop="oldPassword">
+      <InputPassword v-model="password.oldPassword" />
+    </el-form-item>
+    <el-form-item :label="t('profile.password.newPassword')" prop="newPassword">
+      <InputPassword v-model="password.newPassword" strength />
+    </el-form-item>
+    <el-form-item :label="t('profile.password.confirmPassword')" prop="confirmPassword">
+      <InputPassword v-model="password.confirmPassword" strength />
+    </el-form-item>
+    <el-form-item>
+      <XButton :title="t('common.save')" type="primary" @click="submit(formRef)" />
+      <XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" />
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus'
+
+import { InputPassword } from '@/components/InputPassword'
+import { updateUserPassword } from '@/api/system/user/profile'
+
+defineOptions({ name: 'ResetPwd' })
+
+const { t } = useI18n()
+const message = useMessage()
+const formRef = ref<FormInstance>()
+const password = reactive({
+  oldPassword: '',
+  newPassword: '',
+  confirmPassword: ''
+})
+
+// 表单校验
+const equalToPassword = (_rule, value, callback) => {
+  if (password.newPassword !== value) {
+    callback(new Error(t('profile.password.diffPwd')))
+  } else {
+    callback()
+  }
+}
+
+const rules = reactive<FormRules>({
+  oldPassword: [
+    { required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
+    { min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
+  ],
+  newPassword: [
+    { required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
+    { min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
+    { required: true, validator: equalToPassword, trigger: 'blur' }
+  ]
+})
+
+const submit = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate(async (valid) => {
+    if (valid) {
+      await updateUserPassword(password.oldPassword, password.newPassword)
+      message.success(t('common.updateSuccess'))
+    }
+  })
+}
+
+const reset = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+}
+</script>
diff --git a/src/views/profile/components/UserAvatar.vue b/src/views/profile/components/UserAvatar.vue
new file mode 100644
index 0000000..c20168f
--- /dev/null
+++ b/src/views/profile/components/UserAvatar.vue
@@ -0,0 +1,39 @@
+<template>
+  <div class="change-avatar">
+    <CropperAvatar
+      ref="cropperRef"
+      :btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
+      :showBtn="false"
+      :value="img"
+      width="120px"
+      @change="handelUpload"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { uploadAvatar } from '@/api/system/user/profile'
+import { CropperAvatar } from '@/components/Cropper'
+
+defineOptions({ name: 'UserAvatar' })
+
+defineProps({
+  img: propTypes.string.def('')
+})
+
+const cropperRef = ref()
+const handelUpload = async ({ data }) => {
+  await uploadAvatar({ avatarFile: data })
+  cropperRef.value.close()
+}
+</script>
+
+<style lang="scss" scoped>
+.change-avatar {
+  img {
+    display: block;
+    margin-bottom: 15px;
+    border-radius: 50%;
+  }
+}
+</style>
diff --git a/src/views/profile/components/UserSocial.vue b/src/views/profile/components/UserSocial.vue
new file mode 100644
index 0000000..2f021ab
--- /dev/null
+++ b/src/views/profile/components/UserSocial.vue
@@ -0,0 +1,94 @@
+<template>
+  <el-table :data="socialUsers" :show-header="false">
+    <el-table-column fixed="left" title="序号" type="seq" width="60" />
+    <el-table-column align="left" label="社交平台" width="120">
+      <template #default="{ row }">
+        <img :src="row.img" alt="" class="h-5 align-middle" />
+        <p class="mr-5">{{ row.title }}</p>
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="操作">
+      <template #default="{ row }">
+        <template v-if="row.openid">
+          已绑定
+          <XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" />
+        </template>
+        <template v-else>
+          未绑定
+          <XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" />
+        </template>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { SystemUserSocialTypeEnum } from '@/utils/constants'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
+
+defineOptions({ name: 'UserSocial' })
+
+const message = useMessage()
+const socialUsers = ref<any[]>([])
+const userInfo = ref<ProfileVO>()
+
+const initSocial = async () => {
+  const res = await getUserProfile()
+  userInfo.value = res
+  for (const i in SystemUserSocialTypeEnum) {
+    const socialUser = { ...SystemUserSocialTypeEnum[i] }
+    socialUsers.value.push(socialUser)
+    if (userInfo.value?.socialUsers) {
+      for (const j in userInfo.value.socialUsers) {
+        if (socialUser.type === userInfo.value.socialUsers[j].type) {
+          socialUser.openid = userInfo.value.socialUsers[j].openid
+          break
+        }
+      }
+    }
+  }
+}
+const route = useRoute()
+const bindSocial = () => {
+  // 社交绑定
+  const type = route.query.type
+  const code = route.query.code
+  const state = route.query.state
+  if (!code) {
+    return
+  }
+  socialBind(type, code, state).then(() => {
+    message.success('绑定成功')
+    initSocial()
+  })
+}
+const bind = (row) => {
+  const redirectUri = location.origin + '/user/profile?type=' + row.type
+  // 进行跳转
+  socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
+    window.location.href = res
+  })
+}
+const unbind = async (row) => {
+  const res = await socialUnbind(row.type, row.openid)
+  if (res) {
+    row.openid = undefined
+  }
+  message.success('解绑成功')
+}
+
+onMounted(async () => {
+  await initSocial()
+})
+
+watch(
+  () => route,
+  (newRoute) => {
+    bindSocial()
+    console.log(newRoute)
+  },
+  {
+    immediate: true
+  }
+)
+</script>
diff --git a/src/views/profile/components/index.ts b/src/views/profile/components/index.ts
new file mode 100644
index 0000000..9e1883c
--- /dev/null
+++ b/src/views/profile/components/index.ts
@@ -0,0 +1,7 @@
+import BasicInfo from './BasicInfo.vue'
+import ProfileUser from './ProfileUser.vue'
+import ResetPwd from './ResetPwd.vue'
+import UserAvatarVue from './UserAvatar.vue'
+import UserSocial from './UserSocial.vue'
+
+export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial }
diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue
new file mode 100644
index 0000000..e813f04
--- /dev/null
+++ b/src/views/profile/index.vue
@@ -0,0 +1,64 @@
+<template>
+  <div class="flex">
+    <el-card class="user w-1/3" shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>{{ t('profile.user.title') }}</span>
+        </div>
+      </template>
+      <ProfileUser />
+    </el-card>
+    <el-card class="user ml-3 w-2/3" shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>{{ t('profile.info.title') }}</span>
+        </div>
+      </template>
+      <div>
+        <el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs">
+          <el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
+            <BasicInfo />
+          </el-tab-pane>
+          <el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
+            <ResetPwd />
+          </el-tab-pane>
+          <!-- <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial">
+            <UserSocial />
+          </el-tab-pane> -->
+        </el-tabs>
+      </div>
+    </el-card>
+  </div>
+</template>
+<script setup lang="ts" name="Profile">
+import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/'
+const { t } = useI18n()
+
+const activeName = ref('basicInfo')
+</script>
+<style scoped>
+.user {
+  max-height: 960px;
+  padding: 15px 20px 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+:deep(.el-card .el-card__header, .el-card .el-card__body) {
+  padding: 15px !important;
+}
+
+.profile-tabs > .el-tabs__content {
+  padding: 32px;
+  font-weight: 600;
+  color: #6b778c;
+}
+
+.el-tabs--left .el-tabs__content {
+  height: 100%;
+}
+</style>
diff --git a/src/views/redirect/redirect.vue b/src/views/redirect/redirect.vue
new file mode 100644
index 0000000..f7717ce
--- /dev/null
+++ b/src/views/redirect/redirect.vue
@@ -0,0 +1,28 @@
+<template>
+  <div></div>
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Redirect' })
+
+const { currentRoute, replace } = useRouter()
+const { params, query } = unref(currentRoute)
+const { path, _redirect_type = 'path' } = params
+
+Reflect.deleteProperty(params, '_redirect_type')
+Reflect.deleteProperty(params, 'path')
+
+const _path = Array.isArray(path) ? path.join('/') : path
+
+if (_redirect_type === 'name') {
+  replace({
+    name: _path,
+    query,
+    params
+  })
+} else {
+  replace({
+    path: _path.startsWith('/') ? _path : '/' + _path,
+    query
+  })
+}
+</script>