Browse Source

Merge branch 'master' into master_hella

# 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.vue
master_hella_20240701
陈薪名 8 months ago
parent
commit
38d731b070
  1. 4
      .env.production
  2. 7
      Dockerfile
  3. 61
      nginx.conf
  4. 18
      src/router/modules/remaining.ts
  5. 381
      src/views/home/Index copy.vue
  6. 319
      src/views/home/Index2.vue
  7. 336
      src/views/home/components/material.vue
  8. 286
      src/views/home/components/produce.vue
  9. 313
      src/views/home/components/product.vue
  10. 452
      src/views/home/components/supplierIndex.vue
  11. 286
      src/views/home/echarts-data.ts
  12. 14
      src/views/home/index.vue
  13. 55
      src/views/home/types.ts
  14. 2
      src/views/infra/codegen/index.vue
  15. 284
      src/views/login/components/LoginForm.vue
  16. 26
      src/views/login/components/LoginFormTitle.vue
  17. 225
      src/views/login/components/MobileForm.vue
  18. 30
      src/views/login/components/QrCodeForm.vue
  19. 142
      src/views/login/components/RegisterForm.vue
  20. 199
      src/views/login/components/SSOLogin.vue
  21. 8
      src/views/login/components/index.ts
  22. 42
      src/views/login/components/useLogin.ts
  23. 104
      src/views/login/login.vue
  24. 92
      src/views/profile/components/BasicInfo.vue
  25. 99
      src/views/profile/components/ProfileUser.vue
  26. 73
      src/views/profile/components/ResetPwd.vue
  27. 39
      src/views/profile/components/UserAvatar.vue
  28. 94
      src/views/profile/components/UserSocial.vue
  29. 7
      src/views/profile/components/index.ts
  30. 64
      src/views/profile/index.vue
  31. 28
      src/views/redirect/redirect.vue

4
.env.production

@ -31,7 +31,7 @@ VITE_BASE_PATH=/
VITE_OUT_DIR=sfms3.0
# 自定义接口路径
VITE_INTERFACE_URL='http://dev.ccwin-in.com:25110/magic/web/index.html'
VITE_INTERFACE_URL='http://10.62.193.15:25100/magic/web/index.html'
# 积木报表请求路径
VITE_JMREPORT_BASE_URL='http://dev.ccwin-in.com:25110'
VITE_JMREPORT_BASE_URL='http://10.62.193.15:25100'

7
Dockerfile

@ -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

61
nginx.conf

@ -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;
}
}
}

18
src/router/modules/remaining.ts

@ -41,7 +41,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
{
path: '/redirect/:path(.*)',
name: 'Redirect',
component: () => import('@/views/Redirect/Redirect.vue'),
component: () => import('@/views/redirect/redirect.vue'),
meta: {}
}
],
@ -59,7 +59,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [
{
path: 'index',
component: () => import('@/views/Home/Index.vue'),
component: () => import('@/views/home/index.vue'),
name: 'Index',
meta: {
title: t('router.home'),
@ -80,7 +80,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [
{
path: 'profile',
component: () => import('@/views/Profile/Index.vue'),
component: () => import('@/views/profile/index.vue'),
name: 'Profile',
meta: {
canTo: true,
@ -139,7 +139,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [
{
path: 'edit',
component: () => import('@/views/infra/codegen/EditTable.vue'),
component: () => import('@/views/infra/codegen/editTable.vue'),
name: 'InfraCodegenEditTable',
meta: {
noCache: true,
@ -177,7 +177,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: '/login',
component: () => import('@/views/Login/Login.vue'),
component: () => import('@/views/login/login.vue'),
name: 'Login',
meta: {
hidden: true,
@ -187,7 +187,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: '/sso',
component: () => import('@/views/Login/Login.vue'),
component: () => import('@/views/login/login.vue'),
name: 'SSOLogin',
meta: {
hidden: true,
@ -197,7 +197,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: '/403',
component: () => import('@/views/Error/403.vue'),
component: () => import('@/views/error/403.vue'),
name: 'NoAccess',
meta: {
hidden: true,
@ -207,7 +207,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: '/404',
component: () => import('@/views/Error/404.vue'),
component: () => import('@/views/error/404.vue'),
name: 'NoFound',
meta: {
hidden: true,
@ -217,7 +217,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: '/500',
component: () => import('@/views/Error/500.vue'),
component: () => import('@/views/error/500.vue'),
name: 'Error',
meta: {
hidden: true,

381
src/views/home/Index copy.vue

@ -0,0 +1,381 @@
<template>
<div>
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<img :src="avatar" alt="" class="mr-20px h-70px w-70px rounded-[50%]" />
<div>
<div class="text-20px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }}20 - 32
</div>
</div>
</div>
</el-col>
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
<div class="px-8px text-right">
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
/>
</div>
<el-divider direction="vertical" />
<div class="px-8px text-right">
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<el-divider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
/>
</div>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</div>
<el-row class="mt-5px" :gutter="20" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-10px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card shadow="hover">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-10px" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-20px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-5px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-10px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-10px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-10px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-10px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-10px" />
<el-link type="default" :underline="false" @click="setWatermark(item.name)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-10px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<img :src="avatar" alt="" class="mr-20px h-35px w-35px rounded-[50%]" />
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-15px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import avatarImg from '@/assets/imgs/avatar.gif'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
defineOptions({ name: 'Home' })
const { t } = useI18n()
const userStore = useUserStore()
const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar ? userStore.getUser.avatar : avatarImg
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
//
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
//
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Vue',
icon: 'logos:vue',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Angular',
icon: 'logos:angular-icon',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'React',
icon: 'logos:react',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Webpack',
icon: 'logos:webpack',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
message: 'workplace.introduction',
personal: 'Archer',
time: new Date()
}
]
projects = Object.assign(projects, data)
}
//
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统升级版本',
type: '通知',
keys: ['通知', '升级'],
date: new Date()
},
{
title: '系统凌晨维护',
type: '公告',
keys: ['公告', '维护'],
date: new Date()
},
{
title: '系统升级版本',
type: '通知',
keys: ['通知', '升级'],
date: new Date()
},
{
title: '系统凌晨维护',
type: '公告',
keys: ['公告', '维护'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
//
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill',
url: 'github.io'
},
{
name: 'Vue',
icon: 'logos:vue',
url: 'vuejs.org'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
url: 'https://vitejs.dev/'
},
{
name: 'Angular',
icon: 'logos:angular-icon',
url: 'github.io'
},
{
name: 'React',
icon: 'logos:react',
url: 'github.io'
},
{
name: 'Webpack',
icon: 'logos:webpack',
url: 'github.io'
}
]
shortcut = Object.assign(shortcut, data)
}
//
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
//
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
}
getAllApi()
</script>

319
src/views/home/Index2.vue

@ -0,0 +1,319 @@
<template>
<el-row :class="prefixCls" :gutter="20" justify="space-between">
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:peoples" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.newUser') }}
</div>
<CountTo
:duration="2600"
:end-val="102400"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:message" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.unreadInformation') }}
</div>
<CountTo
:duration="2600"
:end-val="81212"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:money" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.transactionAmount') }}
</div>
<CountTo
:duration="2600"
:end-val="9280"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:shopping" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.totalShopping') }}
</div>
<CountTo
:duration="2600"
:end-val="13600"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" justify="space-between">
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="pieOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="barOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="4" animated>
<Echart :height="350" :options="lineOptionsData" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { useDesign } from '@/hooks/web/useDesign'
import type { AnalysisTotalTypes } from './types'
import { barOptions, lineOptions, pieOptions } from './echarts-data'
defineOptions({ name: 'Home2' })
const { t } = useI18n()
const loading = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('panel')
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
let totalState = reactive<AnalysisTotalTypes>({
users: 0,
messages: 0,
moneys: 0,
shoppings: 0
})
const getCount = async () => {
const data = {
users: 102400,
messages: 81212,
moneys: 9280,
shoppings: 13600
}
totalState = Object.assign(totalState, data)
}
//
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
set(pieOptionsData, 'series.data', data)
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
//
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
//
const getMonthlySales = async () => {
const data = [
{ estimate: 100, actual: 120, name: 'analysis.january' },
{ estimate: 120, actual: 82, name: 'analysis.february' },
{ estimate: 161, actual: 91, name: 'analysis.march' },
{ estimate: 134, actual: 154, name: 'analysis.april' },
{ estimate: 105, actual: 162, name: 'analysis.may' },
{ estimate: 160, actual: 140, name: 'analysis.june' },
{ estimate: 165, actual: 145, name: 'analysis.july' },
{ estimate: 114, actual: 250, name: 'analysis.august' },
{ estimate: 163, actual: 134, name: 'analysis.september' },
{ estimate: 185, actual: 56, name: 'analysis.october' },
{ estimate: 118, actual: 99, name: 'analysis.november' },
{ estimate: 123, actual: 123, name: 'analysis.december' }
]
set(
lineOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: data.map((v) => v.actual),
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
const getAllApi = async () => {
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
loading.value = false
}
getAllApi()
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-panel;
.#{$prefix-cls} {
&__item {
&--peoples {
color: #40c9c6;
}
&--message {
color: #36a3f7;
}
&--money {
color: #f4516c;
}
&--shopping {
color: #34bfa3;
}
&:hover {
:deep(.#{$namespace}-icon) {
color: #fff !important;
}
.#{$prefix-cls}__item--icon {
transition: all 0.38s ease-out;
}
.#{$prefix-cls}__item--peoples {
background: #40c9c6;
}
.#{$prefix-cls}__item--message {
background: #36a3f7;
}
.#{$prefix-cls}__item--money {
background: #f4516c;
}
.#{$prefix-cls}__item--shopping {
background: #34bfa3;
}
}
}
}
</style>

336
src/views/home/components/material.vue

@ -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>

286
src/views/home/components/produce.vue

@ -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>

313
src/views/home/components/product.vue

@ -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>

452
src/views/home/components/supplierIndex.vue

@ -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>

286
src/views/home/echarts-data.ts

@ -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
}
]
}
]
}

14
src/views/home/index.vue

@ -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>

55
src/views/home/types.ts

@ -0,0 +1,55 @@
export type WorkplaceTotal = {
project: number
access: number
todo: number
}
export type Project = {
name: string
icon: string
message: string
personal: string
time: Date | number | string
}
export type Notice = {
title: string
type: string
keys: string[]
date: Date | number | string
}
export type Shortcut = {
name: string
icon: string
url: string
}
export type RadarData = {
personal: number
team: number
max: number
name: string
}
export type AnalysisTotalTypes = {
users: number
messages: number
moneys: number
shoppings: number
}
export type UserAccessSource = {
value: number
name: string
}
export type WeeklyUserActivity = {
value: number
name: string
}
export type MonthlySales = {
name: string
estimate: number
actual: number
}

2
src/views/infra/codegen/index.vue

@ -156,7 +156,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as CodegenApi from '@/api/infra/codegen'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import ImportTable from './ImportTable.vue'
import ImportTable from './importTable.vue'
import PreviewCode from './PreviewCode.vue'
defineOptions({ name: 'InfraCodegen' })

284
src/views/login/components/LoginForm.vue

@ -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>

26
src/views/login/components/LoginFormTitle.vue

@ -0,0 +1,26 @@
<template>
<h2 class="enter-x mb-3 text-center text-2xl font-bold xl:text-center xl:text-3xl">
{{ getFormTitle }}
</h2>
</template>
<script lang="ts" setup>
import { LoginStateEnum, useLoginState } from './useLogin'
defineOptions({ name: 'LoginFormTitle' })
const { t } = useI18n()
const { getLoginState } = useLoginState()
const getFormTitle = computed(() => {
const titleObj = {
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
[LoginStateEnum.SSO]: t('sys.login.ssoFormTitle')
}
return titleObj[unref(getLoginState)]
})
</script>

225
src/views/login/components/MobileForm.vue

@ -0,0 +1,225 @@
<template>
<el-form
v-show="getShow"
ref="formSmsLogin"
:model="loginData.loginForm"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item>
</el-col>
<!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mobileNumber">
<el-input
v-model="loginData.loginForm.mobileNumber"
:placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone"
/>
</el-form-item>
</el-col>
<!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<!-- <el-button class="w-[100%]"> -->
<template #append>
<span
v-if="mobileCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getSmsCode"
>
{{ t('login.getSmsCode') }}
</span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
{{ mobileCodeTimer }}秒后可重新获取
</span>
</template>
</el-input>
<!-- </el-button> -->
</el-col>
</el-row>
</el-form-item>
</el-col>
<!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="signIn()"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import { setTenantId, setToken } from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
defineOptions({ name: 'MobileForm' })
const { t } = useI18n()
const message = useMessage()
const permissionStore = usePermissionStore()
const { currentRoute, push } = useRouter()
const formSmsLogin = ref()
const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsLogin)
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
const rules = {
tenantName: [required],
mobileNumber: [required],
code: [required]
}
const loginData = reactive({
codeImg: '',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
token: '',
loading: {
signIn: false
},
loginForm: {
uuid: '',
tenantName: '闻荫源码',
mobileNumber: '',
code: ''
}
})
const smsVO = reactive({
smsCode: {
mobile: '',
scene: 21
},
loginSms: {
mobile: '',
code: ''
}
})
const mobileCodeTimer = ref(0)
const redirect = ref<string>('')
const getSmsCode = async () => {
await getTenantId()
smsVO.smsCode.mobile = loginData.loginForm.mobileNumber
await sendSmsCode(smsVO.smsCode).then(async () => {
message.success(t('login.SmsSendMsg'))
//
mobileCodeTimer.value = 60
let msgTimer = setInterval(() => {
mobileCodeTimer.value = mobileCodeTimer.value - 1
if (mobileCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await getTenantIdByName(loginData.loginForm.tenantName)
setTenantId(res)
}
}
//
const signIn = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
loginLoading.value = true
smsVO.loginSms.mobile = loginData.loginForm.mobileNumber
smsVO.loginSms.code = loginData.loginForm.code
await smsLogin(smsVO.loginSms)
.then(async (res) => {
setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
push({ path: redirect.value || permissionStore.addRouters[0].path })
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>

30
src/views/login/components/QrCodeForm.vue

@ -0,0 +1,30 @@
<template>
<el-row v-show="getShow" style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<LoginFormTitle style="width: 100%" />
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-card class="mb-10px text-center" shadow="hover">
<Qrcode :logo="logoImg" />
</el-card>
</el-col>
<el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<div class="mt-15px w-[100%]">
<XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
</div>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import logoImg from '@/assets/imgs/logo.png'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useLoginState } from './useLogin'
defineOptions({ name: 'QrCodeForm' })
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
</script>

142
src/views/login/components/RegisterForm.vue

@ -0,0 +1,142 @@
<template>
<Form
v-show="getShow"
:rules="rules"
:schema="schema"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
hide-required-asterisk
label-position="top"
size="large"
@register="register"
>
<template #title>
<LoginFormTitle style="width: 100%" />
</template>
<template #code="form">
<div class="w-[100%] flex">
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
</div>
</template>
<template #register>
<div class="w-[100%]">
<XButton
:loading="loading"
:title="t('login.register')"
class="w-[100%]"
type="primary"
@click="loginRegister()"
/>
</div>
<div class="mt-15px w-[100%]">
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
</div>
</template>
</Form>
</template>
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { useValidator } from '@/hooks/web/useValidator'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useLoginState } from './useLogin'
import { FormSchema } from '@/types/form'
defineOptions({ name: 'RegisterForm' })
const { t } = useI18n()
const { required } = useValidator()
const { register, elFormRef } = useForm()
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
}
},
{
field: 'username',
label: t('login.username'),
value: '',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: t('login.usernamePlaceholder')
}
},
{
field: 'password',
label: t('login.password'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
}
},
{
field: 'check_password',
label: t('login.checkPassword'),
value: '',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
strength: true,
placeholder: t('login.passwordPlaceholder')
}
},
{
field: 'code',
label: t('login.code'),
colProps: {
span: 24
}
},
{
field: 'register',
colProps: {
span: 24
}
}
])
const rules: FormRules = {
username: [required()],
password: [required()],
check_password: [required()],
code: [required()]
}
const loading = ref(false)
const loginRegister = async () => {
const formRef = unref(elFormRef)
formRef?.validate(async (valid) => {
if (valid) {
try {
loading.value = true
} finally {
loading.value = false
}
}
})
}
</script>

199
src/views/login/components/SSOLogin.vue

@ -0,0 +1,199 @@
<template>
<div v-show="ssoVisible" class="form-cont">
<!-- 应用名 -->
<LoginFormTitle style="width: 100%" />
<el-tabs class="form" style="float: none" value="uname">
<el-tab-pane :label="client.name" name="uname" />
</el-tabs>
<div>
<el-form :model="formData" class="login-form">
<!-- 授权范围的选择 -->
此第三方应用请求获得以下权限
<el-form-item prop="scopes">
<el-checkbox-group v-model="formData.scopes">
<el-checkbox
v-for="scope in queryParams.scopes"
:key="scope"
:label="scope"
style="display: block; margin-bottom: -10px"
>
{{ formatScope(scope) }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 下方的登录按钮 -->
<el-form-item class="w-1/1">
<el-button
:loading="formLoading"
class="w-6/10"
type="primary"
@click.prevent="handleAuthorize(true)"
>
<span v-if="!formLoading">同意授权</span>
<span v-else> 中...</span>
</el-button>
<el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts" setup>
import LoginFormTitle from './LoginFormTitle.vue'
import * as OAuth2Api from '@/api/login/oauth2'
import { LoginStateEnum, useLoginState } from './useLogin'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
defineOptions({ name: 'SSOLogin' })
const route = useRoute() //
const { currentRoute } = useRouter() //
const { getLoginState, setLoginState } = useLoginState()
const client = ref({
//
name: '',
logo: ''
})
interface queryType {
responseType: string
clientId: string
redirectUri: string
state: string
scopes: string[]
}
const queryParams = reactive<queryType>({
// URL client_idscope
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>

8
src/views/login/components/index.ts

@ -0,0 +1,8 @@
import LoginForm from './LoginForm.vue'
import MobileForm from './MobileForm.vue'
import LoginFormTitle from './LoginFormTitle.vue'
import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }

42
src/views/login/components/useLogin.ts

@ -0,0 +1,42 @@
import { Ref } from 'vue'
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE,
SSO
}
const currentState = ref(LoginStateEnum.LOGIN)
export function useLoginState() {
function setLoginState(state: LoginStateEnum) {
currentState.value = state
}
const getLoginState = computed(() => currentState.value)
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN)
}
return {
setLoginState,
getLoginState,
handleBackLogin
}
}
export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
async function validForm() {
const form = unref(formRef)
if (!form) return
const data = await form.validate()
return data as T
}
return {
validForm
}
}

104
src/views/login/login.vue

@ -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>

92
src/views/profile/components/BasicInfo.vue

@ -0,0 +1,92 @@
<template>
<Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema">
<template #sex="form">
<el-radio-group v-model="form['sex']">
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio>
<el-radio :label="2">{{ t('profile.user.woman') }}</el-radio>
</el-radio-group>
</template>
</Form>
<div style="text-align: center">
<XButton :title="t('common.save')" type="primary" @click="submit()" />
<XButton :title="t('common.reset')" type="danger" @click="init()" />
</div>
</template>
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import { FormSchema } from '@/types/form'
import type { FormExpose } from '@/components/Form'
import {
getUserProfile,
updateUserProfile,
UserProfileUpdateReqVO
} from '@/api/system/user/profile'
defineOptions({ name: 'BasicInfo' })
const { t } = useI18n()
const message = useMessage() //
//
const rules = reactive<FormRules>({
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
email: [
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
],
mobile: [
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: t('profile.rules.truephone'),
trigger: 'blur'
}
]
})
const schema = reactive<FormSchema[]>([
{
field: 'nickname',
label: t('profile.user.nickname'),
component: 'Input'
},
{
field: 'mobile',
label: t('profile.user.mobile'),
component: 'Input'
},
{
field: 'email',
label: t('profile.user.email'),
component: 'Input'
},
{
field: 'sex',
label: t('profile.user.sex'),
component: 'InputNumber',
value: 0
}
])
const formRef = ref<FormExpose>() // Ref
const submit = () => {
const elForm = unref(formRef)?.getElFormRef()
if (!elForm) return
elForm.validate(async (valid) => {
if (valid) {
const data = unref(formRef)?.formModel as UserProfileUpdateReqVO
await updateUserProfile(data)
message.success(t('common.updateSuccess'))
await init()
}
})
}
const init = async () => {
const res = await getUserProfile()
unref(formRef)?.setValues(res)
}
onMounted(async () => {
await init()
})
</script>

99
src/views/profile/components/ProfileUser.vue

@ -0,0 +1,99 @@
<template>
<div>
<div class="text-center">
<UserAvatar :img="userInfo?.avatar" />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:user" />
{{ t('profile.user.username') }}
<div class="pull-right">{{ userInfo?.username }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:phone" />
{{ t('profile.user.mobile') }}
<div class="pull-right">{{ userInfo?.mobile }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="fontisto:email" />
{{ t('profile.user.email') }}
<div class="pull-right">{{ userInfo?.email }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="carbon:tree-view-alt" />
{{ t('profile.user.dept') }}
<div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:suitcase" />
{{ t('profile.user.posts') }}
<div v-if="userInfo?.posts" class="pull-right">
{{ userInfo?.posts.map((post) => post.name).join(',') }}
</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="icon-park-outline:peoples" />
{{ t('profile.user.roles') }}
<div v-if="userInfo?.roles" class="pull-right">
{{ userInfo?.roles.map((role) => role.name).join(',') }}
</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:calendar" />
{{ t('profile.user.createTime') }}
<div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import UserAvatar from './UserAvatar.vue'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
defineOptions({ name: 'ProfileUser' })
const { t } = useI18n()
const userInfo = ref<ProfileVO>()
const getUserInfo = async () => {
const users = await getUserProfile()
userInfo.value = users
}
onMounted(async () => {
await getUserInfo()
})
</script>
<style scoped>
.text-center {
position: relative;
height: 120px;
text-align: center;
}
.list-group-striped > .list-group-item {
padding-right: 0;
padding-left: 0;
border-right: 0;
border-left: 0;
border-radius: 0;
}
.list-group {
padding-left: 0;
list-style: none;
}
.list-group-item {
padding: 11px 0;
margin-bottom: -1px;
font-size: 13px;
border-top: 1px solid #e7eaec;
border-bottom: 1px solid #e7eaec;
}
.pull-right {
float: right !important;
}
</style>

73
src/views/profile/components/ResetPwd.vue

@ -0,0 +1,73 @@
<template>
<el-form ref="formRef" :model="password" :rules="rules" :label-width="200">
<el-form-item :label="t('profile.password.oldPassword')" prop="oldPassword">
<InputPassword v-model="password.oldPassword" />
</el-form-item>
<el-form-item :label="t('profile.password.newPassword')" prop="newPassword">
<InputPassword v-model="password.newPassword" strength />
</el-form-item>
<el-form-item :label="t('profile.password.confirmPassword')" prop="confirmPassword">
<InputPassword v-model="password.confirmPassword" strength />
</el-form-item>
<el-form-item>
<XButton :title="t('common.save')" type="primary" @click="submit(formRef)" />
<XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" />
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import { InputPassword } from '@/components/InputPassword'
import { updateUserPassword } from '@/api/system/user/profile'
defineOptions({ name: 'ResetPwd' })
const { t } = useI18n()
const message = useMessage()
const formRef = ref<FormInstance>()
const password = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
//
const equalToPassword = (_rule, value, callback) => {
if (password.newPassword !== value) {
callback(new Error(t('profile.password.diffPwd')))
} else {
callback()
}
}
const rules = reactive<FormRules>({
oldPassword: [
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
],
newPassword: [
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
})
const submit = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
await updateUserPassword(password.oldPassword, password.newPassword)
message.success(t('common.updateSuccess'))
}
})
}
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>

39
src/views/profile/components/UserAvatar.vue

@ -0,0 +1,39 @@
<template>
<div class="change-avatar">
<CropperAvatar
ref="cropperRef"
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
:showBtn="false"
:value="img"
width="120px"
@change="handelUpload"
/>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { uploadAvatar } from '@/api/system/user/profile'
import { CropperAvatar } from '@/components/Cropper'
defineOptions({ name: 'UserAvatar' })
defineProps({
img: propTypes.string.def('')
})
const cropperRef = ref()
const handelUpload = async ({ data }) => {
await uploadAvatar({ avatarFile: data })
cropperRef.value.close()
}
</script>
<style lang="scss" scoped>
.change-avatar {
img {
display: block;
margin-bottom: 15px;
border-radius: 50%;
}
}
</style>

94
src/views/profile/components/UserSocial.vue

@ -0,0 +1,94 @@
<template>
<el-table :data="socialUsers" :show-header="false">
<el-table-column fixed="left" title="序号" type="seq" width="60" />
<el-table-column align="left" label="社交平台" width="120">
<template #default="{ row }">
<img :src="row.img" alt="" class="h-5 align-middle" />
<p class="mr-5">{{ row.title }}</p>
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template #default="{ row }">
<template v-if="row.openid">
已绑定
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" />
</template>
<template v-else>
未绑定
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" />
</template>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { SystemUserSocialTypeEnum } from '@/utils/constants'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
defineOptions({ name: 'UserSocial' })
const message = useMessage()
const socialUsers = ref<any[]>([])
const userInfo = ref<ProfileVO>()
const initSocial = async () => {
const res = await getUserProfile()
userInfo.value = res
for (const i in SystemUserSocialTypeEnum) {
const socialUser = { ...SystemUserSocialTypeEnum[i] }
socialUsers.value.push(socialUser)
if (userInfo.value?.socialUsers) {
for (const j in userInfo.value.socialUsers) {
if (socialUser.type === userInfo.value.socialUsers[j].type) {
socialUser.openid = userInfo.value.socialUsers[j].openid
break
}
}
}
}
}
const route = useRoute()
const bindSocial = () => {
//
const type = route.query.type
const code = route.query.code
const state = route.query.state
if (!code) {
return
}
socialBind(type, code, state).then(() => {
message.success('绑定成功')
initSocial()
})
}
const bind = (row) => {
const redirectUri = location.origin + '/user/profile?type=' + row.type
//
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
window.location.href = res
})
}
const unbind = async (row) => {
const res = await socialUnbind(row.type, row.openid)
if (res) {
row.openid = undefined
}
message.success('解绑成功')
}
onMounted(async () => {
await initSocial()
})
watch(
() => route,
(newRoute) => {
bindSocial()
console.log(newRoute)
},
{
immediate: true
}
)
</script>

7
src/views/profile/components/index.ts

@ -0,0 +1,7 @@
import BasicInfo from './BasicInfo.vue'
import ProfileUser from './ProfileUser.vue'
import ResetPwd from './ResetPwd.vue'
import UserAvatarVue from './UserAvatar.vue'
import UserSocial from './UserSocial.vue'
export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial }

64
src/views/profile/index.vue

@ -0,0 +1,64 @@
<template>
<div class="flex">
<el-card class="user w-1/3" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ t('profile.user.title') }}</span>
</div>
</template>
<ProfileUser />
</el-card>
<el-card class="user ml-3 w-2/3" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ t('profile.info.title') }}</span>
</div>
</template>
<div>
<el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs">
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
<BasicInfo />
</el-tab-pane>
<el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
<ResetPwd />
</el-tab-pane>
<!-- <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial">
<UserSocial />
</el-tab-pane> -->
</el-tabs>
</div>
</el-card>
</div>
</template>
<script setup lang="ts" name="Profile">
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/'
const { t } = useI18n()
const activeName = ref('basicInfo')
</script>
<style scoped>
.user {
max-height: 960px;
padding: 15px 20px 20px;
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
}
:deep(.el-card .el-card__header, .el-card .el-card__body) {
padding: 15px !important;
}
.profile-tabs > .el-tabs__content {
padding: 32px;
font-weight: 600;
color: #6b778c;
}
.el-tabs--left .el-tabs__content {
height: 100%;
}
</style>

28
src/views/redirect/redirect.vue

@ -0,0 +1,28 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'Redirect' })
const { currentRoute, replace } = useRouter()
const { params, query } = unref(currentRoute)
const { path, _redirect_type = 'path' } = params
Reflect.deleteProperty(params, '_redirect_type')
Reflect.deleteProperty(params, 'path')
const _path = Array.isArray(path) ? path.join('/') : path
if (_redirect_type === 'name') {
replace({
name: _path,
query,
params
})
} else {
replace({
path: _path.startsWith('/') ? _path : '/' + _path,
query
})
}
</script>
Loading…
Cancel
Save