Browse Source
# Conflicts: # src/views/home/Index copy.vue # src/views/home/Index2.vue # src/views/home/components/material.vue # src/views/home/components/produce.vue # src/views/home/components/product.vue # src/views/home/components/supplierIndex.vue # src/views/home/echarts-data.ts # src/views/home/index.vue # src/views/home/types.ts # src/views/login/components/LoginForm.vue # src/views/login/components/LoginFormTitle.vue # src/views/login/components/MobileForm.vue # src/views/login/components/QrCodeForm.vue # src/views/login/components/RegisterForm.vue # src/views/login/components/SSOLogin.vue # src/views/login/components/index.ts # src/views/login/components/useLogin.ts # src/views/login/login.vue # src/views/profile/components/BasicInfo.vue # src/views/profile/components/ProfileUser.vue # src/views/profile/components/ResetPwd.vue # src/views/profile/components/UserAvatar.vue # src/views/profile/components/UserSocial.vue # src/views/profile/components/index.ts # src/views/profile/index.vue # src/views/redirect/redirect.vuemaster_hella_20240701
陈薪名
9 months ago
31 changed files with 4078 additions and 12 deletions
@ -0,0 +1,7 @@ |
|||
# 设置基础镜像 |
|||
FROM nginx:WIN-1.25.3 |
|||
|
|||
WORKDIR /opt/sfms3.0 |
|||
COPY nginx.conf /usr/local/nginx/conf/nginx.conf |
|||
# 将dist文件中的内容复制到 /opt/sfms3.0 这个目录下面 |
|||
COPY sfms3.0/ /opt/sfms3.0 |
@ -0,0 +1,61 @@ |
|||
user root; |
|||
worker_processes 2; |
|||
|
|||
events { |
|||
worker_connections 1024; |
|||
} |
|||
|
|||
http { |
|||
include mime.types; |
|||
charset utf-8,gbk; |
|||
default_type application/octet-stream; |
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
|||
'$status $body_bytes_sent "$http_referer" ' |
|||
'"$http_user_agent" "$http_x_forwarded_for" "$request_time $upstream_response_time"'; |
|||
|
|||
proxy_cache_path /opt/nginx_cache/ levels=1:2 keys_zone=my_zone:10m inactive=300s max_size=5g; |
|||
access_log logs/access.log main; |
|||
sendfile on; |
|||
#tcp_nopush on; |
|||
keepalive_timeout 600s; |
|||
client_max_body_size 200m; |
|||
gzip on; |
|||
gzip_min_length 10k; |
|||
gzip_comp_level 9; |
|||
gzip_buffers 4 16k; |
|||
gzip_types text/plain application/javascript text/css application/xml text/javascript image/jpeg image/gif image/png; |
|||
gzip_vary on; |
|||
gzip_disable "MSIE [1-6]\."; |
|||
upstream sfms3.0 { |
|||
server localhost:25110 weight=10 max_fails=3 fail_timeout=10s; |
|||
} |
|||
server { |
|||
listen 25100; |
|||
server_name_in_redirect off; |
|||
server_name dev.ccwin-in.com; |
|||
location /api/ { |
|||
proxy_pass http://sfms3.0/; |
|||
proxy_next_upstream http_500 http_502 http_503 http_504 error timeout invalid_header; |
|||
proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header Host $http_host; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $http_x_forwarded_for; |
|||
} |
|||
location /profile/ { |
|||
proxy_cache my_zone; |
|||
proxy_cache_valid 200 304 12h; |
|||
proxy_cache_key $host$uri$is_args$args; |
|||
alias /opt/profile/; |
|||
index index.html index.htm; |
|||
} |
|||
location / { |
|||
proxy_cache my_zone; |
|||
proxy_cache_valid 200 304 12h; |
|||
proxy_cache_key $host$uri$is_args$args; |
|||
try_files $uri $uri/ /index.html; |
|||
root /opt/sfms3.0; |
|||
index index.html index.htm; |
|||
} |
|||
} |
|||
} |
|||
|
@ -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,336 @@ |
|||
<!-- 原料管理员首页 --> |
|||
<template> |
|||
<div class="row"> |
|||
<div class="data"> |
|||
<div class="small-data-item small-data-item1"> |
|||
<div class="small-data-item-txt"> |
|||
<div>今日到货计划(已发货)</div> |
|||
<div>{{materialData?.preparetoissueTodayCount}}<span>单</span></div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item2 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>今日备料计划(已发料)</div> |
|||
<div>{{materialData?.preparetoissueTodayCount}}<span>单</span></div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon5.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item3 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>今日叫料请求(已发料)</div> |
|||
<div>{{materialData?.issueRequestTodayCount}}<span>单</span></div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon6.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item4 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>空闲库位数/总库位数</div> |
|||
<div style="font-size: 20px;">{{materialData?.freeLocationCount}}<span>个</span>/{{materialData?.totalLocationCount}}<span>个</span></div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon3.png" alt="" class="img" /> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">呆滞库存预警</div> |
|||
<el-table |
|||
:data="materialData?.stagnantBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">超期库存预警</div> |
|||
<el-table |
|||
:data="materialData?.overdueBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">高低储预警</div> |
|||
<el-table |
|||
:data="materialData?.warningBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">待处理任务</div> |
|||
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import * as IndexApi from '@/api/home' |
|||
import { formatDate } from '@/utils/formatTime' |
|||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
|||
import { set } from 'lodash-es' |
|||
import { EChartsOption } from 'echarts' |
|||
import { barOptions } from '../echarts-data' |
|||
const lineIndex = ref(0) |
|||
|
|||
const materialData = ref() |
|||
// 获取原料管理员数据 |
|||
const getMaterialData = async () => { |
|||
await IndexApi.getMaterialData().then((res) => { |
|||
materialData.value = res |
|||
getJobCharts() |
|||
}) |
|||
} |
|||
const formatter = (type, dict) => { |
|||
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
|||
return str |
|||
} |
|||
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
|||
const getJobCharts = async () => { |
|||
set( |
|||
barOptionsData, |
|||
'xAxis.data', |
|||
Object.keys( materialData.value.jobCount) |
|||
) |
|||
set(barOptionsData, 'legend.data', ['待处理任务']) |
|||
set(barOptionsData, 'series', [ |
|||
{ |
|||
name:'待处理任务', |
|||
data: Object.values( materialData.value.jobCount), |
|||
type: 'bar', |
|||
barMaxWidth:30 |
|||
} |
|||
]) |
|||
lineIndex.value++ |
|||
} |
|||
onMounted(async () => { |
|||
await getMaterialData() |
|||
}) |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
.title { |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #dedede; |
|||
position: relative; |
|||
padding-left: 10px; |
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
width: 4px; |
|||
height: 16px; |
|||
background: #3c7adf; |
|||
left: 0px; |
|||
top: 3px; |
|||
border-radius: 8px; |
|||
} |
|||
} |
|||
.data { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.small-data-item { |
|||
width:25%; |
|||
height: 90px; |
|||
display: flex; |
|||
align-items: center; |
|||
border-radius: 6px; |
|||
color: white; |
|||
padding: 0px 20px; |
|||
.small-data-item-txt { |
|||
flex: 1; |
|||
div { |
|||
&:nth-child(1) { |
|||
font-size: 14px; |
|||
|
|||
} |
|||
&:nth-child(2) { |
|||
font-size: 26px; |
|||
margin-top: 4px; |
|||
font-weight: bold; |
|||
span { |
|||
font-size: 14px; |
|||
padding-left: 6px; |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.img { |
|||
width: 40px; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
.small-data-item1 { |
|||
background: linear-gradient(to left, #fd817d, #fcad80); |
|||
} |
|||
.small-data-item2 { |
|||
background: linear-gradient(to left, #46c6fa, #336bfe); |
|||
} |
|||
.small-data-item3 { |
|||
background: linear-gradient(to left, #96a6cc, #595f82); |
|||
} |
|||
.small-data-item4 { |
|||
background: linear-gradient(to left, #08dcd5, #46e2bb); |
|||
} |
|||
.small-data-item5 { |
|||
background: linear-gradient(to left, #f4c46b, #ffb313); |
|||
} |
|||
.small-data-item6 { |
|||
background: linear-gradient(to left, #6eccf8, #02acfd); |
|||
} |
|||
} |
|||
.two-row { |
|||
display: flex; |
|||
align-content: center; |
|||
justify-content: space-between; |
|||
.data1 { |
|||
background: white; |
|||
padding: 14px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,286 @@ |
|||
<!-- 生产管理员首页 --> |
|||
<template> |
|||
<div class="row"> |
|||
<div class="two-row"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">今日生产计划</div> |
|||
<el-table |
|||
:data="produceData?.productionTodayList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="number" label="单据号" width="180" /> |
|||
<el-table-column prop="displayOrder" label="顺序" width="170" /> |
|||
<el-table-column prop="workshop" label="车间" width="170" /> |
|||
<el-table-column prop="productionLine" label="生产线" width="170" /> |
|||
<el-table-column prop="shift" label="班次" width="170" /> |
|||
<el-table-column prop="team" label="班组" width="170" /> |
|||
<el-table-column prop="planDate" label="计划日期" width="170" |
|||
><template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="beginTime" label="开始日期" width="170" |
|||
><template #default="scope"> |
|||
<span>{{ formatDate(scope.row.beginTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="endTime" label="结束日期" width="170" |
|||
><template #default="scope"> |
|||
<span>{{ formatDate(scope.row.endTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="businessType" label="业务类型" /> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">线边安全库存</div> |
|||
<el-table :data="produceData?.safeLocationList" style="width: 100%" stripe height="240px"> |
|||
<el-table-column prop="code" label="代码" width="180" /> |
|||
<el-table-column prop="name" label="名称" width="180" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180" /> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180" /> |
|||
<el-table-column prop="type" label="类型" width="180" /> |
|||
<el-table-column prop="aisle" label="巷道" width="180" /> |
|||
<el-table-column prop="shelf" label="货架" width="180" /> |
|||
<el-table-column prop="locationRow" label="行" width="180" /> |
|||
<el-table-column prop="locationColum" label="列" width="180" /> |
|||
<el-table-column prop="pickPriority" label="拣料优先级" width="180" /> |
|||
<el-table-column prop="maxWeight" label="最大承重" width="180" /> |
|||
<el-table-column prop="maxArea" label="最大面积" width="180" /> |
|||
<el-table-column prop="maxVolume" label="最大体积" width="180" /> |
|||
<el-table-column prop="userGroupCode" label="用户组代码" width="180" /> |
|||
<el-table-column prop="activeTime" label="生效时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireTime" label="失效时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">待上架成品库存</div> |
|||
<el-table |
|||
:data="produceData?.productputawayJobDetailList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="number" label="单据号" width="180" /> |
|||
<el-table-column prop="fromWarehouseCode" label="从仓库代码" width="180" /> |
|||
<el-table-column prop="requestTime" label="申请时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.requestTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="requestDueTime" label="要求截止时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.requestDueTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="status" label="状态" width="140"/> |
|||
<el-table-column prop="departmentCode" label="部门" /> |
|||
<el-table-column prop="acceptUserId" label="承接人用户名" width="170" /> |
|||
<el-table-column prop="acceptTime" label="承接时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.acceptTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="fromLocationTypes" label="从库位类型范围" width="170"/> |
|||
<el-table-column prop="toLocationTypes" label="到库位类型范围" width="170" /> |
|||
<el-table-column prop="toWarehouseCode" label="到仓库代码" width="170"/> |
|||
<el-table-column prop="fromAreaCodes" label="从库区代码范围" width="170"/> |
|||
<el-table-column prop="toAreaCodes" label="到库区代码范围" width="170"/> |
|||
<el-table-column prop="autoComplete" label="自动完成" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.autoComplete, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowModifyLocation" label="允许修改库位" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowModifyLocation, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowModifyQty" label="允许修改数量" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowModifyQty, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowBiggerQty" label="允许大于推荐数量" width="140"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowBiggerQty, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowSmallerQty" label="允许小于推荐数量" width="140"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowSmallerQty, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowModifyInventoryStatus" label="允许修改库存状态" width="140"> |
|||
<template #default="scope"> |
|||
<span>{{ |
|||
formatter(scope.row.allowModifyInventoryStatus, DICT_TYPE.TRUE_FALSE) |
|||
}}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowContinuousScanning" label="允许连续扫描" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowContinuousScanning, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowPartialComplete" label="允许部分完成" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowPartialComplete, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowModifyBatch" label="允许修改批次" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowModifyBatch, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="allowModifyPackingNumber" label="允许修改箱码" width="120"> |
|||
<template #default="scope"> |
|||
<span>{{ formatter(scope.row.allowModifyPackingNumber, DICT_TYPE.TRUE_FALSE) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">待处理任务</div> |
|||
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import * as IndexApi from '@/api/home' |
|||
import { formatDate } from '@/utils/formatTime' |
|||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
|||
import { set } from 'lodash-es' |
|||
import { EChartsOption } from 'echarts' |
|||
import { barOptions } from '../echarts-data' |
|||
const lineIndex = ref(0) |
|||
const produceData = ref() |
|||
// 获取生产管理员首页数据 |
|||
const getProduceData = async () => { |
|||
IndexApi.getProduceData().then((res) => { |
|||
produceData.value = res |
|||
getJobCharts() |
|||
}) |
|||
} |
|||
const formatter = (type, dict) => { |
|||
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
|||
return str |
|||
} |
|||
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
|||
const getJobCharts = async () => { |
|||
set( |
|||
barOptionsData, |
|||
'xAxis.data', |
|||
Object.keys( produceData.value.jobCount) |
|||
) |
|||
set(barOptionsData, 'legend.data', ['待处理任务']) |
|||
set(barOptionsData, 'series', [ |
|||
{ |
|||
name:'待处理任务', |
|||
data: Object.values( produceData.value.jobCount), |
|||
type: 'bar', |
|||
barMaxWidth:30 |
|||
} |
|||
]) |
|||
lineIndex.value++ |
|||
} |
|||
onMounted(async () => { |
|||
await getProduceData() |
|||
}) |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
.title { |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #dedede; |
|||
position: relative; |
|||
padding-left: 10px; |
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
width: 4px; |
|||
height: 16px; |
|||
background: #3c7adf; |
|||
left: 0px; |
|||
top: 3px; |
|||
border-radius: 8px; |
|||
} |
|||
} |
|||
.data { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.small-data-item { |
|||
width: 25%; |
|||
height: 90px; |
|||
display: flex; |
|||
align-items: center; |
|||
border-radius: 6px; |
|||
color: white; |
|||
padding: 0px 20px; |
|||
.small-data-item-txt { |
|||
flex: 1; |
|||
div { |
|||
&:nth-child(1) { |
|||
font-size: 14px; |
|||
} |
|||
&:nth-child(2) { |
|||
font-size: 26px; |
|||
margin-top: 4px; |
|||
font-weight: bold; |
|||
span { |
|||
font-size: 14px; |
|||
padding-left: 6px; |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.img { |
|||
width: 40px; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
.small-data-item1 { |
|||
background: linear-gradient(to left, #fd817d, #fcad80); |
|||
} |
|||
.small-data-item2 { |
|||
background: linear-gradient(to left, #46c6fa, #336bfe); |
|||
} |
|||
.small-data-item3 { |
|||
background: linear-gradient(to left, #96a6cc, #595f82); |
|||
} |
|||
.small-data-item4 { |
|||
background: linear-gradient(to left, #08dcd5, #46e2bb); |
|||
} |
|||
.small-data-item5 { |
|||
background: linear-gradient(to left, #f4c46b, #ffb313); |
|||
} |
|||
.small-data-item6 { |
|||
background: linear-gradient(to left, #6eccf8, #02acfd); |
|||
} |
|||
} |
|||
.two-row { |
|||
display: flex; |
|||
align-content: center; |
|||
justify-content: space-between; |
|||
.data1 { |
|||
background: white; |
|||
padding: 14px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,313 @@ |
|||
<!-- 成品管理员首页 --> |
|||
<template> |
|||
<div class="row"> |
|||
<div class="data"> |
|||
<div class="small-data-item small-data-item1"> |
|||
<div class="small-data-item-txt"> |
|||
<div>今日到货计划(已发货)</div> |
|||
<div>{{ productData?.deliverPlanTodayCount || 0 }}<span>单</span></div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">呆滞库存预警</div> |
|||
<el-table |
|||
:data="productData?.stagnantBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">超期库存预警</div> |
|||
<el-table |
|||
:data="productData?.overdueBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">高低储预警</div> |
|||
<el-table |
|||
:data="productData?.warningBalanceList" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
|||
<el-table-column prop="batch" label="批次" width="180px" /> |
|||
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
|||
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
|||
<el-table-column prop="qty" label="数量" width="180px" /> |
|||
<el-table-column prop="uom" label="计量单位" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
|||
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
|||
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
|||
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
|||
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
|||
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
|||
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
|||
<el-table-column prop="amount" label="金额" width="180px" /> |
|||
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.planDate) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">待处理任务</div> |
|||
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import * as IndexApi from '@/api/home' |
|||
import { formatDate } from '@/utils/formatTime' |
|||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
|||
import { set } from 'lodash-es' |
|||
import { EChartsOption } from 'echarts' |
|||
import { barOptions } from '../echarts-data' |
|||
const lineIndex = ref(0) |
|||
// 获取成品管理员首页首页数据 |
|||
const productData = ref() |
|||
const getProductData = async () => { |
|||
IndexApi.getProductData().then((res) => { |
|||
productData.value = res |
|||
getJobCharts() |
|||
}) |
|||
} |
|||
const formatter = (type, dict) => { |
|||
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
|||
return str |
|||
} |
|||
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
|||
const getJobCharts = async () => { |
|||
set( |
|||
barOptionsData, |
|||
'xAxis.data', |
|||
Object.keys( productData.value.jobCount) |
|||
) |
|||
set(barOptionsData, 'legend.data', ['待处理任务']) |
|||
set(barOptionsData, 'series', [ |
|||
{ |
|||
name:'待处理任务', |
|||
data: Object.values( productData.value.jobCount), |
|||
type: 'bar', |
|||
barMaxWidth:30 |
|||
} |
|||
]) |
|||
lineIndex.value++ |
|||
} |
|||
onMounted(async () => { |
|||
await getProductData() |
|||
}) |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
.title { |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #dedede; |
|||
position: relative; |
|||
padding-left: 10px; |
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
width: 4px; |
|||
height: 16px; |
|||
background: #3c7adf; |
|||
left: 0px; |
|||
top: 3px; |
|||
border-radius: 8px; |
|||
} |
|||
} |
|||
.data { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.small-data-item { |
|||
width: 25%; |
|||
height: 90px; |
|||
display: flex; |
|||
align-items: center; |
|||
border-radius: 6px; |
|||
color: white; |
|||
padding: 0px 20px; |
|||
.small-data-item-txt { |
|||
flex: 1; |
|||
div { |
|||
&:nth-child(1) { |
|||
font-size: 14px; |
|||
} |
|||
&:nth-child(2) { |
|||
font-size: 26px; |
|||
margin-top: 4px; |
|||
font-weight: bold; |
|||
span { |
|||
font-size: 14px; |
|||
padding-left: 6px; |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.img { |
|||
width: 40px; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
.small-data-item1 { |
|||
background: linear-gradient(to left, #fd817d, #fcad80); |
|||
} |
|||
.small-data-item2 { |
|||
background: linear-gradient(to left, #46c6fa, #336bfe); |
|||
} |
|||
.small-data-item3 { |
|||
background: linear-gradient(to left, #96a6cc, #595f82); |
|||
} |
|||
.small-data-item4 { |
|||
background: linear-gradient(to left, #08dcd5, #46e2bb); |
|||
} |
|||
.small-data-item5 { |
|||
background: linear-gradient(to left, #f4c46b, #ffb313); |
|||
} |
|||
.small-data-item6 { |
|||
background: linear-gradient(to left, #6eccf8, #02acfd); |
|||
} |
|||
} |
|||
.two-row { |
|||
display: flex; |
|||
align-content: center; |
|||
justify-content: space-between; |
|||
.data1 { |
|||
background: white; |
|||
padding: 14px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,452 @@ |
|||
<!-- 供应商首页 --> |
|||
<template> |
|||
<div class="one-row"> |
|||
<div class="data"> |
|||
<div class="data-item"> |
|||
<div class="small-title">订单数</div> |
|||
<div class="small-data"> |
|||
<div class="small-data-item small-data-item1"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.openPurchaseCount || 0 }}<span>单</span></div> |
|||
<div>开放订单数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item2 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.allPurchaseCount || 0 }}<span>单</span></div> |
|||
<div>全部订单数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon5.png" alt="" class="img" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="data-item ml-14px"> |
|||
<div class="small-title">要货计划数</div> |
|||
<div class="small-data"> |
|||
<div class="small-data-item small-data-item3"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.openPurchasePlanCount || 0 }}<span>单</span></div> |
|||
<div>开放计划数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon6.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item4 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.allPurchasePlanCount || 0 }}<span>单</span></div> |
|||
<div>全部计划数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon3.png" alt="" class="img" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="data-item ml-14px"> |
|||
<div class="small-title">发货单数</div> |
|||
<div class="small-data"> |
|||
<div class="small-data-item small-data-item5"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.notTakeSupplierdeliverCount || 0 }}<span>单</span></div> |
|||
<div>未收货订单数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon4.png" alt="" class="img" /> |
|||
</div> |
|||
<div class="small-data-item small-data-item6 ml-14px"> |
|||
<div class="small-data-item-txt"> |
|||
<div>{{ supplierData?.takeSupplierdeliverCount || 0 }}<span>单</span></div> |
|||
<div>已收货订单数</div> |
|||
</div> |
|||
<img src="../../../assets/imgs/icon2.png" alt="" class="img" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[65%]"> |
|||
<div class="title">本月发货单趋势</div> |
|||
<Echart :options="lineOptions" :height="280" :key="lineIndex" /> |
|||
</div> |
|||
<div class="data1 w-[35%] ml-14px"> |
|||
<div class="title">本月发货零件TOP10</div> |
|||
<Echart :options="pieOptions" :height="280" :key="lineIndex" /> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">最新消息</div> |
|||
<el-table :data="notaicList" style="width: 100%" stripe height="240px"> |
|||
<el-table-column label="编号" align="center" prop="id" /> |
|||
<el-table-column label="用户类型" align="center" prop="userType"> |
|||
<template #default="scope"> |
|||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="用户编号" align="center" prop="userId" width="80" /> |
|||
<el-table-column label="模板编码" align="center" prop="templateCode" width="80" /> |
|||
<el-table-column label="发送人名称" align="center" prop="templateNickname" width="180" /> |
|||
<el-table-column |
|||
label="模版内容" |
|||
align="center" |
|||
prop="templateContent" |
|||
width="200" |
|||
show-overflow-tooltip |
|||
/> |
|||
<el-table-column |
|||
label="模版参数" |
|||
align="center" |
|||
prop="templateParams" |
|||
width="180" |
|||
show-overflow-tooltip |
|||
> |
|||
<template #default="scope"> {{ scope.row.templateParams }}</template> |
|||
</el-table-column> |
|||
<el-table-column label="模版类型" align="center" prop="templateType" width="120"> |
|||
<template #default="scope"> |
|||
<dict-tag |
|||
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" |
|||
:value="scope.row.templateType" |
|||
/> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="是否已读" align="center" prop="readStatus" width="100"> |
|||
<template #default="scope"> |
|||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" /> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column |
|||
label="阅读时间" |
|||
align="center" |
|||
prop="readTime" |
|||
width="180" |
|||
:formatter="dateFormatter" |
|||
/> |
|||
<el-table-column |
|||
label="创建时间" |
|||
align="center" |
|||
prop="createTime" |
|||
width="180" |
|||
:formatter="dateFormatter" |
|||
/> |
|||
<!-- <el-table-column label="操作" align="center" fixed="right" width="120"> |
|||
<template #default="scope"> |
|||
<el-button link type="primary" @click="openDetail(scope.row)" v-hasPermi="['system:notify-message:query']"> |
|||
<Icon icon="ep:document-copy" /> |
|||
详情 |
|||
</el-button> |
|||
</template> |
|||
</el-table-column> --> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">最新扣分明细</div> |
|||
<el-table :data="tableData" style="width: 100%" stripe height="240px"> |
|||
<el-table-column prop="title" label="标题" /> |
|||
<el-table-column prop="name" label="发布人" /> |
|||
<el-table-column prop="date" label="发布日期" /> |
|||
</el-table> |
|||
</div> |
|||
</div> |
|||
<div class="two-row mt-14px"> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">本月退货明细</div> |
|||
<el-table |
|||
:data="supplierData?.purchasereturnRecordMonth" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="number" label="单据号" width="180" /> |
|||
<el-table-column prop="fromPackingNumber" label="从包装号" width="180" /> |
|||
<el-table-column prop="toPackingNumber" label="到包装号" width="180" /> |
|||
<el-table-column prop="fromContainerNumber" label="从器具号" width="120" /> |
|||
<el-table-column prop="toContainerNumber" label="到器具号" width="120" /> |
|||
<el-table-column prop="fromBatch" label="从批次" width="120" /> |
|||
<el-table-column prop="toBatch" label="到批次" width="120" /> |
|||
<el-table-column prop="altBatch" label="替代批次" width="120" /> |
|||
<el-table-column prop="fromLocationCode" label="从库位代码" width="120" /> |
|||
<el-table-column prop="toLocationCode" label="到库位代码" width="120" /> |
|||
<el-table-column prop="fromLocationGroupCode" label="从库位组代码" width="120" /> |
|||
<el-table-column prop="toLocationGroupCode" label="到库位组代码" width="120" /> |
|||
<el-table-column prop="fromAreaCode" label="从库区代码" width="120" /> |
|||
<el-table-column prop="toAreaCode" label="到库区代码" width="120" /> |
|||
<el-table-column prop="fromOwnerCode" label="从货主代码" width="120" /> |
|||
<el-table-column prop="toOwnerCode" label="到货主代码" width="120" /> |
|||
<el-table-column prop="inventoryStatus" label="库存状态" width="120"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.inventoryStatus,DICT_TYPE.INVENTORY_STATUS) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="poNumber" label="订单号" width="120" /> |
|||
<el-table-column prop="poline" label="订单行" width="120" /> |
|||
<el-table-column prop="reason" label="原因" width="120" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="120" /> |
|||
<el-table-column prop="amount" label="金额" width="120" /> |
|||
<el-table-column prop="itemCode" label="物料代码" width="160" /> |
|||
<el-table-column prop="itemName" label="物料名称" width="120" /> |
|||
<el-table-column prop="itemDesc1" label="物料描述1" width="120" /> |
|||
<el-table-column prop="itemDesc2" label="物料描述2" width="120" /> |
|||
<el-table-column prop="qty" label="数量" width="120" /> |
|||
<el-table-column prop="uom" label="计量单位" width="120"> |
|||
<template #default="scope"> |
|||
{{ formatter(scope.row.uom,DICT_TYPE.UOM) }} |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="projectCode" label="项目代码" width="120" /> |
|||
<el-table-column prop="remark" label="备注" width="120" /> |
|||
<el-table-column prop="createTime" label="创建时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="creator" label="创建者" width="120" /> |
|||
</el-table> |
|||
</div> |
|||
<div class="data1 w-[47.3%]"> |
|||
<div class="title">本月索赔明细</div> |
|||
<el-table |
|||
:data="supplierData?.purchasereturnRecordMonth" |
|||
style="width: 100%" |
|||
stripe |
|||
height="240px" |
|||
> |
|||
<el-table-column prop="number" label="单据号" width="180" /> |
|||
<el-table-column prop="fromBatch" label="批次" width="120" /> |
|||
<el-table-column prop="poNumber" label="订单号" width="120" /> |
|||
<el-table-column prop="poline" label="订单行" width="120" /> |
|||
<el-table-column prop="reason" label="原因" width="120" /> |
|||
<el-table-column prop="singlePrice" label="单价" width="120" /> |
|||
<el-table-column prop="amount" label="金额" width="120" /> |
|||
<el-table-column prop="code" label="代码" width="160" /> |
|||
<el-table-column prop="createTime" label="创建时间" width="170"> |
|||
<template #default="scope"> |
|||
<span>{{ formatDate(scope.row.createTime) }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="creator" label="创建者" width="120" /> |
|||
</el-table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { set } from 'lodash-es' |
|||
import { EChartsOption } from 'echarts' |
|||
import { lineOptions, pieOptions } from '../echarts-data' |
|||
import { formatDate } from '@/utils/formatTime' |
|||
import * as NotifyMessageApi from '@/api/system/notify/message' |
|||
import * as IndexApi from '@/api/home' |
|||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
|||
const { t } = useI18n() |
|||
const tableData = [ |
|||
{ |
|||
date: '2016-05-03', |
|||
name: 'Tom', |
|||
title: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
}, |
|||
{ |
|||
date: '2016-05-02', |
|||
title: 'Tom', |
|||
name: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
}, |
|||
{ |
|||
date: '2016-05-04', |
|||
title: 'Tom', |
|||
name: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
}, |
|||
{ |
|||
date: '2016-05-01', |
|||
title: 'Tom', |
|||
name: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
}, |
|||
{ |
|||
date: '2016-05-01', |
|||
title: 'Tom', |
|||
name: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
}, |
|||
{ |
|||
date: '2016-05-01', |
|||
title: 'Tom', |
|||
name: 'Tom', |
|||
text: 'No. 189, Grove St, Los Angeles' |
|||
} |
|||
] |
|||
const supplierData = ref() |
|||
const lineIndex = ref(0) |
|||
// 本月发货单趋势 |
|||
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption |
|||
const getInvoiceCharts = async () => { |
|||
set( |
|||
lineOptionsData, |
|||
'xAxis.data', |
|||
supplierData.value.supplierdeliverMonthCount.map((v) => v.date) |
|||
) |
|||
set(lineOptionsData, 'legend.data', ['本月发货单趋势']) |
|||
set(lineOptionsData, 'series', [ |
|||
{ |
|||
name: '本月发货单趋势', |
|||
smooth: true, |
|||
type: 'line', |
|||
itemStyle: {}, |
|||
animationDuration: 2800, |
|||
animationEasing: 'quadraticOut', |
|||
data: supplierData.value.supplierdeliverMonthCount.map((v) => v.count) |
|||
} |
|||
]) |
|||
lineIndex.value++ |
|||
} |
|||
//本月发货零件TOP10 |
|||
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption |
|||
const getPartTOPCharts = 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' } |
|||
] |
|||
console.log(supplierData.value.supplierdeliverItemMonthTop) |
|||
set( |
|||
pieOptionsData, |
|||
'legend.data', |
|||
supplierData.value.supplierdeliverItemMonthTop.map((v) => v.itemCode) |
|||
) |
|||
pieOptionsData!.series![0].data = supplierData.value.supplierdeliverItemMonthTop.map((v) => { |
|||
return { |
|||
name: v.itemCode, |
|||
value: v.qty |
|||
} |
|||
}) |
|||
lineIndex.value++ |
|||
} |
|||
// 获取供应商数据 |
|||
const getSupplierData = async () => { |
|||
IndexApi.getSupplierData().then((res) => { |
|||
supplierData.value = res |
|||
getInvoiceCharts() |
|||
getPartTOPCharts() |
|||
}) |
|||
} |
|||
// 最新消息 |
|||
const notaicList = ref([]) // 列表的数据 |
|||
const queryParams = reactive({ |
|||
pageNo: 1, |
|||
pageSize: 10, |
|||
userType: undefined, |
|||
userId: undefined, |
|||
templateCode: undefined, |
|||
templateType: undefined, |
|||
createTime: [] |
|||
}) |
|||
/** 查询列表 */ |
|||
const getList = async () => { |
|||
try { |
|||
const data = await NotifyMessageApi.getNotifyMessagePage(queryParams) |
|||
notaicList.value = data.list |
|||
} finally { |
|||
} |
|||
} |
|||
const formatter = (type,dict) => { |
|||
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
|||
return str |
|||
} |
|||
onMounted(async () => { |
|||
await getSupplierData() |
|||
getList() |
|||
}) |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
.title { |
|||
padding-bottom: 10px; |
|||
border-bottom: 1px solid #dedede; |
|||
position: relative; |
|||
padding-left: 10px; |
|||
&::after { |
|||
content: ''; |
|||
position: absolute; |
|||
width: 4px; |
|||
height: 16px; |
|||
background: #3c7adf; |
|||
left: 0px; |
|||
top: 3px; |
|||
border-radius: 8px; |
|||
} |
|||
} |
|||
.data { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.data-item { |
|||
width: calc(100% / 3); |
|||
padding: 10px 0px; |
|||
background: white; |
|||
.small-title { |
|||
padding: 0px 14px 10px; |
|||
} |
|||
.small-data { |
|||
display: flex; |
|||
align-content: center; |
|||
justify-content: space-between; |
|||
padding: 0px 14px; |
|||
.small-data-item { |
|||
width: 50%; |
|||
height: 70px; |
|||
display: flex; |
|||
align-items: center; |
|||
border-radius: 6px; |
|||
color: white; |
|||
padding: 0px 20px; |
|||
.small-data-item-txt { |
|||
flex: 1; |
|||
div { |
|||
&:nth-child(1) { |
|||
font-size: 20px; |
|||
span { |
|||
font-size: 12px; |
|||
padding-left: 6px; |
|||
} |
|||
} |
|||
&:nth-child(2) { |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
} |
|||
.img { |
|||
width: 30px; |
|||
opacity: 0.5; |
|||
} |
|||
} |
|||
.small-data-item1 { |
|||
background: linear-gradient(to left, #fd817d, #fcad80); |
|||
} |
|||
.small-data-item2 { |
|||
background: linear-gradient(to left, #46c6fa, #336bfe); |
|||
} |
|||
.small-data-item3 { |
|||
background: linear-gradient(to left, #96a6cc, #595f82); |
|||
} |
|||
.small-data-item4 { |
|||
background: linear-gradient(to left, #08dcd5, #46e2bb); |
|||
} |
|||
.small-data-item5 { |
|||
background: linear-gradient(to left, #f4c46b, #ffb313); |
|||
} |
|||
.small-data-item6 { |
|||
background: linear-gradient(to left, #6eccf8, #02acfd); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.two-row { |
|||
display: flex; |
|||
align-content: center; |
|||
justify-content: space-between; |
|||
.data1 { |
|||
background: white; |
|||
padding: 14px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,286 @@ |
|||
import { EChartsOption } from 'echarts' |
|||
|
|||
const { t } = useI18n() |
|||
|
|||
export const lineOptions: EChartsOption = { |
|||
|
|||
xAxis: { |
|||
data: [ 1, 2, 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30 |
|||
], |
|||
boundaryGap: false, |
|||
axisTick: { |
|||
show: false |
|||
} |
|||
}, |
|||
grid: { |
|||
left: 20, |
|||
right: 20, |
|||
bottom: 20, |
|||
top: 50, |
|||
containLabel: true |
|||
}, |
|||
tooltip: { |
|||
trigger: 'axis', |
|||
axisPointer: { |
|||
type: 'cross' |
|||
}, |
|||
padding: [5, 10] |
|||
}, |
|||
yAxis: { |
|||
axisTick: { |
|||
show: false |
|||
} |
|||
}, |
|||
legend: { |
|||
data: ['销售','哈哈'], |
|||
top: 20 |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '销售', |
|||
smooth: true, |
|||
type: 'line', |
|||
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123], |
|||
animationDuration: 2800, |
|||
animationEasing: 'cubicInOut' |
|||
}, |
|||
{ |
|||
name: '哈哈', |
|||
smooth: true, |
|||
type: 'line', |
|||
itemStyle: {}, |
|||
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123], |
|||
animationDuration: 2800, |
|||
animationEasing: 'quadraticOut' |
|||
} |
|||
] |
|||
} |
|||
|
|||
export const pieOptions: EChartsOption = { |
|||
// title: {
|
|||
// text: t('analysis.userAccessSource'),
|
|||
// left: 'center'
|
|||
// },
|
|||
tooltip: { |
|||
trigger: 'item', |
|||
formatter: '{a} <br/>{b} : {c} ({d}%)' |
|||
}, |
|||
legend: { |
|||
orient: 'vertical', |
|||
left: 'left', |
|||
top:20, |
|||
data: [ |
|||
t('analysis.directAccess'), |
|||
t('analysis.mailMarketing'), |
|||
t('analysis.allianceAdvertising'), |
|||
t('analysis.videoAdvertising'), |
|||
t('analysis.searchEngines') |
|||
] |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: t('analysis.userAccessSource'), |
|||
type: 'pie', |
|||
radius: '55%', |
|||
center: ['50%', '60%'], |
|||
data: [ |
|||
{ value: 335, name: t('analysis.directAccess') }, |
|||
{ value: 310, name: t('analysis.mailMarketing') }, |
|||
{ value: 234, name: t('analysis.allianceAdvertising') }, |
|||
{ value: 135, name: t('analysis.videoAdvertising') }, |
|||
{ value: 1548, name: t('analysis.searchEngines') } |
|||
] |
|||
} |
|||
] |
|||
} |
|||
|
|||
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: t('analysis.activeQuantity'), |
|||
data: [], |
|||
type: 'bar' |
|||
} |
|||
] |
|||
} |
|||
|
|||
export const radarOption: EChartsOption = { |
|||
legend: { |
|||
data: [t('workplace.personal'), t('workplace.team')] |
|||
}, |
|||
radar: { |
|||
// shape: 'circle',
|
|||
indicator: [ |
|||
{ name: t('workplace.quote'), max: 65 }, |
|||
{ name: t('workplace.contribution'), max: 160 }, |
|||
{ name: t('workplace.hot'), max: 300 }, |
|||
{ name: t('workplace.yield'), max: 130 }, |
|||
{ name: t('workplace.follow'), max: 100 } |
|||
] |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: `xxx${t('workplace.index')}`, |
|||
type: 'radar', |
|||
data: [ |
|||
{ |
|||
value: [42, 30, 20, 35, 80], |
|||
name: t('workplace.personal') |
|||
}, |
|||
{ |
|||
value: [50, 140, 290, 100, 90], |
|||
name: t('workplace.team') |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
|
|||
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: [ |
|||
{ |
|||
name: 'Sam S Club', |
|||
value: 10000, |
|||
textStyle: { |
|||
color: 'black' |
|||
}, |
|||
emphasis: { |
|||
textStyle: { |
|||
color: 'red' |
|||
} |
|||
} |
|||
}, |
|||
{ |
|||
name: 'Macys', |
|||
value: 6181 |
|||
}, |
|||
{ |
|||
name: 'Amy Schumer', |
|||
value: 4386 |
|||
}, |
|||
{ |
|||
name: 'Jurassic World', |
|||
value: 4055 |
|||
}, |
|||
{ |
|||
name: 'Charter Communications', |
|||
value: 2467 |
|||
}, |
|||
{ |
|||
name: 'Chick Fil A', |
|||
value: 2244 |
|||
}, |
|||
{ |
|||
name: 'Planet Fitness', |
|||
value: 1898 |
|||
}, |
|||
{ |
|||
name: 'Pitch Perfect', |
|||
value: 1484 |
|||
}, |
|||
{ |
|||
name: 'Express', |
|||
value: 1112 |
|||
}, |
|||
{ |
|||
name: 'Home', |
|||
value: 965 |
|||
}, |
|||
{ |
|||
name: 'Johnny Depp', |
|||
value: 847 |
|||
}, |
|||
{ |
|||
name: 'Lena Dunham', |
|||
value: 582 |
|||
}, |
|||
{ |
|||
name: 'Lewis Hamilton', |
|||
value: 555 |
|||
}, |
|||
{ |
|||
name: 'KXAN', |
|||
value: 550 |
|||
}, |
|||
{ |
|||
name: 'Mary Ellen Mark', |
|||
value: 462 |
|||
}, |
|||
{ |
|||
name: 'Farrah Abraham', |
|||
value: 366 |
|||
}, |
|||
{ |
|||
name: 'Rita Ora', |
|||
value: 360 |
|||
}, |
|||
{ |
|||
name: 'Serena Williams', |
|||
value: 282 |
|||
}, |
|||
{ |
|||
name: 'NCAA baseball tournament', |
|||
value: 273 |
|||
}, |
|||
{ |
|||
name: 'Point Break', |
|||
value: 265 |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,14 @@ |
|||
<template> |
|||
<div> |
|||
<supplierIndex v-hasRole="['super_admin','supplier']"/> |
|||
<material v-hasRole="['super_admin']"/> |
|||
<product v-hasRole="['super_admin']"/> |
|||
<produce v-hasRole="['super_admin']"/> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import supplierIndex from './components/supplierIndex.vue' |
|||
import material from './components/material.vue' |
|||
import product from './components/product.vue' |
|||
import produce from './components/produce.vue' |
|||
</script> |
@ -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,284 @@ |
|||
<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%" /> |
|||
</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" link type="primary" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
|||
<el-form-item prop="username"> |
|||
<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 prop="password"> |
|||
<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"> |
|||
<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"; |
|||
|
|||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache' |
|||
import * as DeptApi from '@/api/system/dept' |
|||
|
|||
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 }) |
|||
} |
|||
// 存储 部门信息 |
|||
const { wsCache } = useCache() |
|||
wsCache.set(CACHE_KEY.DEPT, await DeptApi.getSimpleDeptList()) |
|||
} 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; |
|||
} |
|||
} |
|||
</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,104 @@ |
|||
<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="relative mx-auto h-full flex"> |
|||
<div |
|||
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" |
|||
> |
|||
<!-- 左上角的 logo + 系统标题 --> |
|||
<div class="relative flex items-center text-white"> |
|||
<img alt="" class="mr-10px h-24px w-48px" src="@/assets/imgs/logo.png" /> |
|||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
|||
</div> |
|||
<!-- 左边的背景图 + 欢迎语 --> |
|||
<div class="h-[calc(100%-60px)] flex items-center justify-center"> |
|||
<TransitionGroup |
|||
appear |
|||
enter-active-class="animate__animated animate__bounceInLeft" |
|||
tag="div" |
|||
> |
|||
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> |
|||
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> |
|||
<div key="3" class="mt-5 text-14px font-normal text-white"> |
|||
{{ t('login.message') }} |
|||
</div> |
|||
</TransitionGroup> |
|||
</div> |
|||
</div> |
|||
<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> |
|||
</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: ''; |
|||
} |
|||
} |
|||
} |
|||
</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