陈薪名
10 months ago
26 changed files with 3404 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||||
|
<template> |
||||
|
<Error type="403" @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error403' }) |
||||
|
|
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<Error @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error404' }) |
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<Error type="500" @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error500' }) |
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -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> |
@ -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> |
@ -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> |
@ -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: [ |
||||
|
|
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
@ -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> |
@ -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 |
||||
|
} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 } |
@ -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 |
||||
|
} |
||||
|
} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 } |
@ -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> |
@ -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> |
Loading…
Reference in new issue