Browse Source

大小写修改

master
陈薪名 10 months ago
parent
commit
852d2c8f75
  1. 8
      src/views/error/403.vue
  2. 7
      src/views/error/404.vue
  3. 7
      src/views/error/500.vue
  4. 381
      src/views/home/Index copy.vue
  5. 319
      src/views/home/Index2.vue
  6. 336
      src/views/home/components/material.vue
  7. 286
      src/views/home/components/produce.vue
  8. 313
      src/views/home/components/product.vue
  9. 452
      src/views/home/components/supplierIndex.vue
  10. 286
      src/views/home/echarts-data.ts
  11. 14
      src/views/home/index.vue
  12. 55
      src/views/home/types.ts
  13. 67
      src/views/infra/apiAccessLog/ApiAccessLogDetail.vue
  14. 167
      src/views/infra/apiAccessLog/index.vue
  15. 81
      src/views/infra/apiErrorLog/ApiErrorLogDetail.vue
  16. 249
      src/views/infra/apiErrorLog/index.vue
  17. 143
      src/views/infra/build/index.vue
  18. 222
      src/views/infra/codegen/PreviewCode.vue
  19. 87
      src/views/infra/codegen/components/BasicInfoForm.vue
  20. 153
      src/views/infra/codegen/components/ColumInfoForm.vue
  21. 391
      src/views/infra/codegen/components/GenerateInfoForm.vue
  22. 4
      src/views/infra/codegen/components/index.ts
  23. 83
      src/views/infra/codegen/editTable.vue
  24. 151
      src/views/infra/codegen/importTable.vue
  25. 258
      src/views/infra/codegen/index.vue
  26. 131
      src/views/infra/config/ConfigForm.vue
  27. 169
      src/views/infra/config/index.vue
  28. 21
      src/views/infra/customInterface/index.vue
  29. 111
      src/views/infra/dataSourceConfig/DataSourceConfigForm.vue
  30. 87
      src/views/infra/dataSourceConfig/index.vue
  31. 57
      src/views/infra/dbDoc/index.vue
  32. 25
      src/views/infra/druid/index.vue
  33. 104
      src/views/infra/file/FileForm.vue
  34. 164
      src/views/infra/file/index.vue
  35. 195
      src/views/infra/fileConfig/FileConfigForm.vue
  36. 168
      src/views/infra/fileConfig/index.vue
  37. 73
      src/views/infra/job/JobDetail.vue
  38. 131
      src/views/infra/job/JobForm.vue
  39. 255
      src/views/infra/job/index.vue
  40. 59
      src/views/infra/job/logger/JobLogDetail.vue
  41. 196
      src/views/infra/job/logger/index.vue
  42. 266
      src/views/infra/redis/index.vue
  43. 28
      src/views/infra/server/index.vue
  44. 25
      src/views/infra/skywalking/index.vue
  45. 26
      src/views/infra/swagger/index.vue
  46. 4
      src/views/infra/testDemo/index.vue
  47. 118
      src/views/infra/webSocket/index.vue
  48. 278
      src/views/login/components/LoginForm.vue
  49. 26
      src/views/login/components/LoginFormTitle.vue
  50. 225
      src/views/login/components/MobileForm.vue
  51. 30
      src/views/login/components/QrCodeForm.vue
  52. 142
      src/views/login/components/RegisterForm.vue
  53. 199
      src/views/login/components/SSOLogin.vue
  54. 8
      src/views/login/components/index.ts
  55. 42
      src/views/login/components/useLogin.ts
  56. 104
      src/views/login/login.vue
  57. 92
      src/views/profile/components/BasicInfo.vue
  58. 99
      src/views/profile/components/ProfileUser.vue
  59. 73
      src/views/profile/components/ResetPwd.vue
  60. 39
      src/views/profile/components/UserAvatar.vue
  61. 94
      src/views/profile/components/UserSocial.vue
  62. 7
      src/views/profile/components/index.ts
  63. 64
      src/views/profile/index.vue
  64. 28
      src/views/redirect/redirect.vue

8
src/views/error/403.vue

@ -0,0 +1,8 @@
<template>
<Error type="403" @error-click="push('/')" />
</template>
<script lang="ts" setup>
defineOptions({ name: 'Error403' })
const { push } = useRouter()
</script>

7
src/views/error/404.vue

@ -0,0 +1,7 @@
<template>
<Error @error-click="push('/')" />
</template>
<script lang="ts" setup>
defineOptions({ name: 'Error404' })
const { push } = useRouter()
</script>

7
src/views/error/500.vue

@ -0,0 +1,7 @@
<template>
<Error type="500" @error-click="push('/')" />
</template>
<script lang="ts" setup>
defineOptions({ name: 'Error500' })
const { push } = useRouter()
</script>

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
}

67
src/views/infra/apiAccessLog/ApiAccessLogDetail.vue

@ -0,0 +1,67 @@
<template>
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
<el-descriptions :column="1" border>
<el-descriptions-item label="日志主键" min-width="120">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="链路追踪">
{{ detailData.traceId }}
</el-descriptions-item>
<el-descriptions-item label="应用名">
{{ detailData.applicationName }}
</el-descriptions-item>
<el-descriptions-item label="用户信息">
{{ detailData.userId }}
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
</el-descriptions-item>
<el-descriptions-item label="用户 IP">
{{ detailData.userIp }}
</el-descriptions-item>
<el-descriptions-item label="用户 UA">
{{ detailData.userAgent }}
</el-descriptions-item>
<el-descriptions-item label="请求信息">
{{ detailData.requestMethod }} {{ detailData.requestUrl }}
</el-descriptions-item>
<el-descriptions-item label="请求参数">
{{ detailData.requestParams }}
</el-descriptions-item>
<el-descriptions-item label="请求时间">
{{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }}
</el-descriptions-item>
<el-descriptions-item label="请求耗时">{{ detailData.duration }} ms</el-descriptions-item>
<el-descriptions-item label="操作结果">
<div v-if="detailData.resultCode === 0">正常</div>
<div v-else-if="detailData.resultCode > 0"
>失败 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
</div>
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as ApiAccessLog from '@/api/infra/apiAccessLog'
defineOptions({ name: 'ApiAccessLogDetail' })
const dialogVisible = ref(false) //
const detailLoading = ref(false) //
const detailData = ref() //
/** 打开弹窗 */
const open = async (data: ApiAccessLog.ApiAccessLogVO) => {
dialogVisible.value = true
//
detailLoading.value = true
try {
detailData.value = data
} finally {
detailLoading.value = false
}
}
defineExpose({ open }) // open
</script>

167
src/views/infra/apiAccessLog/index.vue

@ -0,0 +1,167 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="用户编号" prop="userId">
<el-input v-model="queryParams.userId" placeholder="请输入用户编号" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="用户类型" prop="userType">
<el-select v-model="queryParams.userType" placeholder="请选择用户类型" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="应用名" prop="applicationName">
<el-input v-model="queryParams.applicationName" placeholder="请输入应用名" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="请求时间" prop="beginTime">
<el-date-picker v-model="queryParams.beginTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" />
</el-form-item>
<el-form-item label="执行时长" prop="duration">
<el-input v-model="queryParams.duration" placeholder="请输入执行时长" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="结果码" prop="resultCode">
<el-input v-model="queryParams.resultCode" placeholder="请输入结果码" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button type="info" plain @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="success" @click="handleExport" :loading="exportLoading"
v-hasPermi="['infra:api-error-log:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="用户编号" align="center" prop="userId" />
<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="applicationName" />
<el-table-column label="请求方法" align="center" prop="requestMethod" width="80" />
<el-table-column label="请求地址" align="center" prop="requestUrl" width="250" />
<el-table-column label="请求时间" align="center" prop="beginTime" width="180">
<template #default="scope">
<span>{{ formatDate(scope.row.beginTime) }}</span>
</template>
</el-table-column>
<el-table-column label="执行时长" align="center" prop="duration" width="180">
<template #default="scope"> {{ scope.row.duration }} ms </template>
</el-table-column>
<el-table-column label="操作结果" align="center" prop="status">
<template #default="scope">
{{ scope.row.resultCode === 0 ? '成功' : '失败(' + scope.row.resultMsg + ')' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button link type="primary" @click="openDetail(scope.row)" v-hasPermi="['infra:api-access-log:query']">
<Icon icon="ep:document-copy" />
详细
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗详情 -->
<ApiAccessLogDetail ref="detailRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { formatDate } from '@/utils/formatTime'
import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
import ApiAccessLogDetail from './ApiAccessLogDetail.vue'
defineOptions({ name: 'InfraApiAccessLog' })
const message = useMessage() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
userType: null,
applicationName: null,
requestUrl: null,
duration: null,
resultCode: null,
beginTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ApiAccessLogApi.getApiAccessLogPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 详情操作 */
const detailRef = ref()
const openDetail = (data: ApiAccessLogApi.ApiAccessLogVO) => {
detailRef.value.open(data)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await ApiAccessLogApi.exportApiAccessLog(queryParams)
download.excel(data, 'API 访问日志.xlsx')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

81
src/views/infra/apiErrorLog/ApiErrorLogDetail.vue

@ -0,0 +1,81 @@
<template>
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="详情" width="800">
<el-descriptions :column="1" border>
<el-descriptions-item label="日志主键" min-width="120">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="链路追踪">
{{ detailData.traceId }}
</el-descriptions-item>
<el-descriptions-item label="应用名">
{{ detailData.applicationName }}
</el-descriptions-item>
<el-descriptions-item label="用户编号">
{{ detailData.userId }}
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
</el-descriptions-item>
<el-descriptions-item label="用户 IP">
{{ detailData.userIp }}
</el-descriptions-item>
<el-descriptions-item label="用户 UA">
{{ detailData.userAgent }}
</el-descriptions-item>
<el-descriptions-item label="请求信息">
{{ detailData.requestMethod }} {{ detailData.requestUrl }}
</el-descriptions-item>
<el-descriptions-item label="请求参数">
{{ detailData.requestParams }}
</el-descriptions-item>
<el-descriptions-item label="异常时间">
{{ formatDate(detailData.exceptionTime) }}
</el-descriptions-item>
<el-descriptions-item label="异常名">
{{ detailData.exceptionName }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.exceptionStackTrace" label="异常堆栈">
<el-input
v-model="detailData.exceptionStackTrace"
:autosize="{ maxRows: 20 }"
:readonly="true"
type="textarea"
/>
</el-descriptions-item>
<el-descriptions-item label="处理状态">
<dict-tag
:type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
:value="detailData.processStatus"
/>
</el-descriptions-item>
<el-descriptions-item v-if="detailData.processUserId" label="处理人">
{{ detailData.processUserId }}
</el-descriptions-item>
<el-descriptions-item v-if="detailData.processTime" label="处理时间">
{{ formatDate(detailData.processTime) }}
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as ApiErrorLog from '@/api/infra/apiErrorLog'
defineOptions({ name: 'ApiErrorLogDetail' })
const dialogVisible = ref(false) //
const detailLoading = ref(false) //
const detailData = ref() //
/** 打开弹窗 */
const open = async (data: ApiErrorLog.ApiErrorLogVO) => {
dialogVisible.value = true
//
detailLoading.value = true
try {
detailData.value = data
} finally {
detailLoading.value = false
}
}
defineExpose({ open }) // open
</script>

249
src/views/infra/apiErrorLog/index.vue

@ -0,0 +1,249 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="用户编号" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="用户类型" prop="userType">
<el-select
v-model="queryParams.userType"
placeholder="请选择用户类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="应用名" prop="applicationName">
<el-input
v-model="queryParams.applicationName"
placeholder="请输入应用名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="异常时间" prop="exceptionTime">
<el-date-picker
v-model="queryParams.exceptionTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="处理状态" prop="processStatus">
<el-select
v-model="queryParams.processStatus"
placeholder="请选择处理状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button type="info" plain @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="success"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:api-error-log:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="用户编号" align="center" prop="userId" />
<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="applicationName" width="200" />
<el-table-column label="请求方法" align="center" prop="requestMethod" width="80" />
<el-table-column label="请求地址" align="center" prop="requestUrl" width="180" />
<el-table-column
label="异常发生时间"
align="center"
prop="exceptionTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="异常名" align="center" prop="exceptionName" width="180" />
<el-table-column label="处理状态" align="center" prop="processStatus">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
:value="scope.row.processStatus"
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="240">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row)"
v-hasPermi="['infra:api-error-log:query']"
><Icon icon="ep:document-copy" />
详细
</el-button>
<el-button
link
type="primary"
v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
@click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.DONE)"
v-hasPermi="['infra:api-error-log:update-status']"
><Icon icon="ep:finished" />
已处理
</el-button>
<el-button
link
type="primary"
v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
@click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.IGNORE)"
v-hasPermi="['infra:api-error-log:update-status']"
><Icon icon="ep:remove" />
已忽略
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗详情 -->
<ApiErrorLogDetail ref="detailRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
import ApiErrorLogDetail from './ApiErrorLogDetail.vue'
import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
defineOptions({ name: 'InfraApiErrorLog' })
const message = useMessage() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: null,
userType: null,
applicationName: null,
requestUrl: null,
processStatus: null,
exceptionTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ApiErrorLogApi.getApiErrorLogPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 详情操作 */
const detailRef = ref()
const openDetail = (data: ApiErrorLogApi.ApiErrorLogVO) => {
detailRef.value.open(data)
}
/** 处理已处理 / 已忽略的操作 **/
const handleProcess = async (id: number, processStatus: number) => {
try {
//
const type = processStatus === InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'
await message.confirm('确认标记为' + type + '?')
//
await ApiErrorLogApi.updateApiErrorLogPage(id, processStatus)
await message.success(type)
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await ApiErrorLogApi.exportApiErrorLog(queryParams)
download.excel(data, '异常日志.xlsx')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

143
src/views/infra/build/index.vue

@ -0,0 +1,143 @@
<template>
<ContentWrap>
<el-row>
<el-col>
<div class="float-right mb-2">
<el-button size="small" type="primary" @click="showJson">生成 JSON</el-button>
<el-button size="small" type="success" @click="showOption">生成 Options</el-button>
<el-button size="small" type="danger" @click="showTemplate">生成组件</el-button>
</div>
</el-col>
<!-- 表单设计器 -->
<el-col>
<FcDesigner ref="designer" height="780px" />
</el-col>
</el-row>
</ContentWrap>
<!-- 弹窗表单预览 -->
<Dialog v-model="dialogVisible" :title="dialogTitle" max-height="600">
<div v-if="dialogVisible" ref="editor">
<el-button style="float: right" @click="copy(formData)">
{{ t('common.copy') }}
</el-button>
<el-scrollbar height="580">
<div>
<pre><code class="hljs" v-dompurify-html="highlightedCode(formData)"></code></pre>
</div>
</el-scrollbar>
</div>
</Dialog>
</template>
<script lang="ts" setup>
defineOptions({ name: 'InfraBuild' })
import FcDesigner from '@form-create/designer'
import { useClipboard } from '@vueuse/core'
import { isString } from '@/utils/is'
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import xml from 'highlight.js/lib/languages/java'
import json from 'highlight.js/lib/languages/json'
import formCreate from '@form-create/element-ui'
const { t } = useI18n() //
const message = useMessage() //
const designer = ref() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formType = ref(-1) // 0 - JSON1 - Options2 -
const formData = ref('') //
/** 打开弹窗 */
const openModel = (title: string) => {
dialogVisible.value = true
dialogTitle.value = title
}
/** 生成 JSON */
const showJson = () => {
openModel('生成 JSON')
formType.value = 0
formData.value = designer.value.getRule()
}
/** 生成 Options */
const showOption = () => {
openModel('生成 Options')
formType.value = 1
formData.value = designer.value.getOption()
}
/** 生成组件 */
const showTemplate = () => {
openModel('生成组件')
formType.value = 2
formData.value = makeTemplate()
}
const makeTemplate = () => {
const rule = designer.value.getRule()
const opt = designer.value.getOption()
return `<template>
<form-create
v-model="fapi"
:rule="rule"
:option="option"
@submit="onSubmit"
></form-create>
</template>
<script setup lang=ts>
import formCreate from "@form-create/element-ui";
const faps = ref(null)
const rule = ref('')
const option = ref('')
const init = () => {
rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
option.value = formCreate.parseJson('${JSON.stringify(opt)}')
}
const onSubmit = (formData) => {
//todo
}
init()
<\/script>`
}
/** 复制 **/
const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
}
/**
* 代码高亮
*/
const highlightedCode = (code) => {
//
let language = 'json'
if (formType.value === 2) {
language = 'xml'
}
if (!isString(code)) {
code = JSON.stringify(code)
}
//
const result = hljs.highlight(language, code, true)
return result.value || '&nbsp;'
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('json', json)
})
</script>

222
src/views/infra/codegen/PreviewCode.vue

@ -0,0 +1,222 @@
<template>
<Dialog
v-model="dialogVisible"
align-center
class="app-infra-codegen-preview-container"
title="代码预览"
width="80%"
>
<div class="flex">
<!-- 代码目录树 -->
<el-card
v-loading="loading"
:gutter="12"
class="w-1/3"
element-loading-text="生成文件目录中..."
shadow="hover"
>
<el-scrollbar height="calc(100vh - 88px - 40px)">
<el-tree
ref="treeRef"
:data="preview.fileTree"
:expand-on-click-node="false"
highlight-current
default-expand-all
node-key="id"
@node-click="handleNodeClick"
/>
</el-scrollbar>
</el-card>
<!-- 代码 -->
<el-card
v-loading="loading"
:gutter="12"
class="ml-3 w-2/3"
element-loading-text="加载代码中..."
shadow="hover"
>
<el-tabs v-model="preview.activeName">
<el-tab-pane
v-for="item in previewCodegen"
:key="item.filePath"
:label="item.filePath.substring(item.filePath.lastIndexOf('/') + 1)"
:name="item.filePath"
>
<el-button class="float-right" text type="primary" @click="copy(item.code)">
{{ t('common.copy') }}
</el-button>
<el-scrollbar height="600px">
<pre><code v-dompurify-html="highlightedCode(item)" class="hljs"></code></pre>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</Dialog>
</template>
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { handleTree2 } from '@/utils/tree'
import * as CodegenApi from '@/api/infra/codegen'
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import java from 'highlight.js/lib/languages/java'
import xml from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import sql from 'highlight.js/lib/languages/sql'
import typescript from 'highlight.js/lib/languages/typescript'
defineOptions({ name: 'InfraCodegenPreviewCode' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const loading = ref(false) //
const preview = reactive({
fileTree: [], //
activeName: '' //
})
const previewCodegen = ref<CodegenApi.CodegenPreviewVO[]>()
/** 点击文件 */
const handleNodeClick = async (data, node) => {
if (node && !node.isLeaf) {
return false
}
preview.activeName = data.id
}
/** 生成 files 目录 **/
interface filesType {
id: string
label: string
parentId: string
}
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
try {
loading.value = true
//
const data = await CodegenApi.previewCodegen(id)
previewCodegen.value = data
//
let file = handleFiles(data)
preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
//
preview.activeName = data[0].filePath
} finally {
loading.value = false
}
}
defineExpose({ open }) // open
/** 处理文件 */
const handleFiles = (datas: CodegenApi.CodegenPreviewVO[]) => {
let exists = {} // keyfile idvaluetrue
let files: filesType[] = []
//
for (const data of datas) {
let paths = data.filePath.split('/')
let fullPath = '' // id
// java
if (paths[paths.length - 1].indexOf('.java') >= 0) {
let newPaths: string[] = []
for (let i = 0; i < paths.length; i++) {
let path = paths[i]
if (path !== 'java') {
newPaths.push(path)
continue
}
newPaths.push(path)
// package
let tmp = ''
while (i < paths.length) {
path = paths[i + 1]
if (
path === 'controller' ||
path === 'convert' ||
path === 'dal' ||
path === 'enums' ||
path === 'service' ||
path === 'vo' || //
path === 'mysql' ||
path === 'dataobject'
) {
break
}
tmp = tmp ? tmp + '.' + path : path
i++
}
if (tmp) {
newPaths.push(tmp)
}
}
paths = newPaths
}
// path
for (let i = 0; i < paths.length; i++) {
// files
let oldFullPath = fullPath
// replaceAll tabs replaceAll
fullPath = fullPath.length === 0 ? paths[i] : fullPath.replaceAll('.', '/') + '/' + paths[i]
if (exists[fullPath]) {
continue
}
// files
exists[fullPath] = true
files.push({
id: fullPath,
label: paths[i],
parentId: oldFullPath || '/' // "/" 为根节点
})
}
}
return files
}
/** 复制 **/
const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
message.error(t('common.copyError'))
return
}
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
/**
* 代码高亮
*/
const highlightedCode = (item) => {
const language = item.filePath.substring(item.filePath.lastIndexOf('.') + 1)
const result = hljs.highlight(language, item.code || '', true)
return result.value || '&nbsp;'
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('java', java)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('vue', xml)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('typescript', typescript)
})
</script>
<style lang="scss">
.app-infra-codegen-preview-container {
.el-scrollbar .el-scrollbar__wrap .el-scrollbar__view {
display: inline-block;
white-space: nowrap;
}
}
</style>

87
src/views/infra/codegen/components/BasicInfoForm.vue

@ -0,0 +1,87 @@
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="表名称" prop="tableName">
<el-input v-model="formData.tableName" placeholder="请输入仓库名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表描述" prop="tableComment">
<el-input v-model="formData.tableComment" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
实体类名称
<el-tooltip
content="默认去除表名的前缀。如果存在重复,则需要手动添加前缀,避免 MyBatis 报 Alias 重复的问题。"
placement="top"
>
<Icon class="" icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.className" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<el-input v-model="formData.author" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" :rows="3" type="textarea" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import * as CodegenApi from '@/api/infra/codegen'
import { PropType } from 'vue'
defineOptions({ name: 'InfraCodegenBasicInfoForm' })
const props = defineProps({
table: {
type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
default: () => null
}
})
const formRef = ref()
const formData = ref({
tableName: '',
tableComment: '',
className: '',
author: '',
remark: ''
})
const rules = reactive({
tableName: [required],
tableComment: [required],
className: [required],
author: [required]
})
/** 监听 table 属性,复制给 formData 属性 */
watch(
() => props.table,
(table) => {
if (!table) return
formData.value = table
},
{
deep: true,
immediate: true
}
)
defineExpose({
validate: async () => unref(formRef)?.validate()
})
</script>

153
src/views/infra/codegen/components/ColumInfoForm.vue

@ -0,0 +1,153 @@
<template>
<el-table ref="dragTable" :data="formData" :max-height="tableHeight" row-key="columnId">
<el-table-column
:show-overflow-tooltip="true"
label="字段列名"
min-width="10%"
prop="columnName"
/>
<el-table-column label="字段描述" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.columnComment" />
</template>
</el-table-column>
<el-table-column
:show-overflow-tooltip="true"
label="物理类型"
min-width="10%"
prop="dataType"
/>
<el-table-column label="Java类型" min-width="11%">
<template #default="scope">
<el-select v-model="scope.row.javaType">
<el-option label="Long" value="Long" />
<el-option label="String" value="String" />
<el-option label="Integer" value="Integer" />
<el-option label="Double" value="Double" />
<el-option label="BigDecimal" value="BigDecimal" />
<el-option label="LocalDateTime" value="LocalDateTime" />
<el-option label="Boolean" value="Boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="java属性" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.javaField" />
</template>
</el-table-column>
<el-table-column label="插入" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.createOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="编辑" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.updateOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="列表" min-width="4%">
<template #default="scope">
<el-checkbox
v-model="scope.row.listOperationResult"
false-label="false"
true-label="true"
/>
</template>
</el-table-column>
<el-table-column label="查询" min-width="4%">
<template #default="scope">
<el-checkbox v-model="scope.row.listOperation" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="查询方式" min-width="10%">
<template #default="scope">
<el-select v-model="scope.row.listOperationCondition">
<el-option label="=" value="=" />
<el-option label="!=" value="!=" />
<el-option label=">" value=">" />
<el-option label=">=" value=">=" />
<el-option label="<" value="<>" />
<el-option label="<=" value="<=" />
<el-option label="LIKE" value="LIKE" />
<el-option label="BETWEEN" value="BETWEEN" />
</el-select>
</template>
</el-table-column>
<el-table-column label="允许空" min-width="5%">
<template #default="scope">
<el-checkbox v-model="scope.row.nullable" false-label="false" true-label="true" />
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.htmlType">
<el-option label="文本框" value="input" />
<el-option label="文本域" value="textarea" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />
<el-option label="图片上传" value="imageUpload" />
<el-option label="文件上传" value="fileUpload" />
<el-option label="富文本控件" value="editor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" min-width="12%">
<template #default="scope">
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择">
<el-option
v-for="dict in dictOptions"
:key="dict.id"
:label="dict.name"
:value="dict.type"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="示例" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.example" />
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import * as CodegenApi from '@/api/infra/codegen'
import * as DictDataApi from '@/api/system/dict/dict.type'
defineOptions({ name: 'InfraCodegenColumInfoForm' })
const props = defineProps({
columns: {
type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
default: () => null
}
})
const formData = ref<CodegenApi.CodegenColumnVO[]>([])
const tableHeight = document.documentElement.scrollHeight - 350 + 'px'
/** 查询字典下拉列表 */
const dictOptions = ref<DictDataApi.DictTypeVO[]>()
const getDictOptions = async () => {
dictOptions.value = await DictDataApi.getSimpleDictTypeList()
}
watch(
() => props.columns,
(columns) => {
if (!columns) return
formData.value = columns
},
{
deep: true,
immediate: true
}
)
onMounted(async () => {
await getDictOptions()
})
</script>

391
src/views/infra/codegen/components/GenerateInfoForm.vue

@ -0,0 +1,391 @@
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
<el-row>
<el-col :span="12">
<el-form-item label="生成模板" prop="templateType">
<el-select v-model="formData.templateType" @change="tplSelectChange">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="前端类型" prop="frontType">
<el-select v-model="formData.frontType">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生成场景" prop="scene">
<el-select v-model="formData.scene">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
上级菜单
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-tree-select
v-model="formData.parentMenuId"
:data="menus"
:props="menuTreeProps"
check-strictly
node-key="id"
placeholder="请选择系统菜单"
/>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="packageName">-->
<!-- <span slot="label">-->
<!-- 生成包路径-->
<!-- <el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.packageName" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="moduleName">
<template #label>
<span>
模块名
<el-tooltip
content="模块名,即一级目录,例如 system、infra、tool 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.moduleName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="businessName">
<template #label>
<span>
业务名
<el-tooltip
content="业务名,即二级目录,例如 user、permission、dict 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.businessName" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item prop="businessPackage">-->
<!-- <span slot="label">-->
<!-- 业务包-->
<!-- <el-tooltip content="业务包,自定义二级目录。例如说,我们希望将 dictType 和 dictData 归类成 dict 业务" placement="top">-->
<!-- <i class="el-icon-question"></i>-->
<!-- </el-tooltip>-->
<!-- </span>-->
<!-- <el-input v-model="formData.businessPackage" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item prop="className">
<template #label>
<span>
类名称
<el-tooltip
content="类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.className" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="classComment">
<template #label>
<span>
类描述
<el-tooltip content="用作类描述,例如 用户" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.classComment" />
</el-form-item>
</el-col>
<el-col v-if="formData.genType === '1'" :span="24">
<el-form-item prop="genPath">
<template #label>
<span>
自定义路径
<el-tooltip
content="填写磁盘绝对路径,若不填写,则生成到当前Web项目下"
placement="top"
>
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-input v-model="formData.genPath">
<template #append>
<el-dropdown>
<el-button type="primary">
最近路径快速选择
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="formData.genPath = '/'">
恢复默认的生成基础路径
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'tree'">
<h4 class="form-header">其他信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树编码字段
<el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树父编码字段
<el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeParentCode" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
树名称字段
<el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.treeName" placeholder="请选择">
<el-option
v-for="(column, index) in formData.columns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="formData.tplCategory === 'sub'">
<h4 class="form-header">关联信息</h4>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
关联子表的表名
<el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableName" placeholder="请选择" @change="subSelectChange">
<el-option
v-for="(table0, index) in tables"
:key="index"
:label="table0.tableName + ':' + table0.tableComment"
:value="table0.tableName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
子表关联的外键名
<el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
<Icon icon="ep:question-filled" />
</el-tooltip>
</span>
</template>
<el-select v-model="formData.subTableFkName" placeholder="请选择">
<el-option
v-for="(column, index) in subColumns"
:key="index"
:label="column.columnName + ':' + column.columnComment"
:value="column.columnName"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { handleTree } from '@/utils/tree'
import * as CodegenApi from '@/api/infra/codegen'
import * as MenuApi from '@/api/system/menu'
import { PropType } from 'vue'
defineOptions({ name: 'InfraCodegenGenerateInfoForm' })
const message = useMessage() //
const props = defineProps({
table: {
type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
default: () => null
}
})
const formRef = ref()
const formData = ref({
templateType: null,
frontType: null,
scene: null,
moduleName: '',
businessName: '',
className: '',
classComment: '',
parentMenuId: null,
genPath: '',
treeCode: '',
treeParentCode: '',
treeName: '',
tplCategory: '',
subTableName: '',
subTableFkName: '',
genType: ''
})
const rules = reactive({
templateType: [required],
frontType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
className: [required],
classComment: [required]
})
const tables = ref([])
const subColumns = ref([])
const menus = ref<any[]>([])
const menuTreeProps = {
label: 'name'
}
/** 选择子表名触发 */
const subSelectChange = () => {
formData.value.subTableFkName = ''
}
/** 选择生成模板触发 */
const tplSelectChange = (value) => {
if (value !== 1) {
// TODO
message.error(
'暂时不考虑支持【树形】和【主子表】的代码生成。原因是:导致 vm 模板过于复杂,不利于胖友二次开发'
)
return false
}
if (value !== 'sub') {
formData.value.subTableName = ''
formData.value.subTableFkName = ''
}
}
watch(
() => props.table,
(table) => {
if (!table) return
formData.value = table as any
},
{
deep: true,
immediate: true
}
)
onMounted(async () => {
try {
const resp = await MenuApi.getSimpleMenusList()
menus.value = handleTree(resp)
} catch {}
})
defineExpose({
validate: async () => unref(formRef)?.validate()
})
</script>

4
src/views/infra/codegen/components/index.ts

@ -0,0 +1,4 @@
import BasicInfoForm from './BasicInfoForm.vue'
import ColumInfoForm from './ColumInfoForm.vue'
import GenerateInfoForm from './GenerateInfoForm.vue'
export { BasicInfoForm, ColumInfoForm, GenerateInfoForm }

83
src/views/infra/codegen/editTable.vue

@ -0,0 +1,83 @@
<template>
<ContentWrap v-loading="formLoading">
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basicInfo">
<basic-info-form ref="basicInfoRef" :table="formData.table" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="colum">
<colum-info-form ref="columInfoRef" :columns="formData.columns" />
</el-tab-pane>
<el-tab-pane label="生成信息" name="generateInfo">
<generate-info-form ref="generateInfoRef" :table="formData.table" />
</el-tab-pane>
</el-tabs>
<el-form>
<el-form-item style="float: right">
<el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="close">返回</el-button>
</el-form-item>
</el-form>
</ContentWrap>
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
import * as CodegenApi from '@/api/infra/codegen'
defineOptions({ name: 'InfraCodegenEditTable' })
const { t } = useI18n() //
const message = useMessage() //
const { push, currentRoute } = useRouter() //
const { query } = useRoute() //
const { delView } = useTagsViewStore() //
const formLoading = ref(false) // 12
const activeName = ref('colum') // Tag
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
const formData = ref<CodegenApi.CodegenUpdateReqVO>({
table: {},
columns: []
})
/** 获得详情 */
const getDetail = async () => {
const id = query.id as unknown as number
if (!id) {
return
}
formLoading.value = true
try {
formData.value = await CodegenApi.getCodegenTable(id)
} finally {
formLoading.value = false
}
}
/** 提交按钮 */
const submitForm = async () => {
//
if (!unref(formData)) return
await unref(basicInfoRef)?.validate()
await unref(generateInfoRef)?.validate()
try {
//
await CodegenApi.updateCodegenTable(formData.value)
message.success(t('common.updateSuccess'))
close()
} catch {}
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
push('/infra/codegen')
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>

151
src/views/infra/codegen/importTable.vue

@ -0,0 +1,151 @@
<template>
<Dialog v-model="dialogVisible" title="导入表" width="800px">
<!-- 搜索栏 -->
<el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
<el-form-item label="数据源" prop="dataSourceConfigId">
<el-select
v-model="queryParams.dataSourceConfigId"
class="!w-240px"
placeholder="请选择数据源"
>
<el-option
v-for="config in dataSourceConfigList"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入表名称"
@keyup.enter="getList"
/>
</el-form-item>
<el-form-item label="表描述" prop="comment">
<el-input
v-model="queryParams.comment"
class="!w-240px"
clearable
placeholder="请输入表描述"
@keyup.enter="getList"
/>
</el-form-item>
<el-form-item>
<el-button @click="getList">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-row>
<el-table
ref="tableRef"
v-loading="dbTableLoading"
:data="dbTableList"
height="260px"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column :show-overflow-tooltip="true" label="表名称" prop="name" />
<el-table-column :show-overflow-tooltip="true" label="表描述" prop="comment" />
</el-table>
</el-row>
<!-- 操作 -->
<template #footer>
<el-button :disabled="tableList.length === 0" type="primary" @click="handleImportTable">
导入
</el-button>
<el-button @click="close">关闭</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as CodegenApi from '@/api/infra/codegen'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import { ElTable } from 'element-plus'
defineOptions({ name: 'InfraCodegenImportTable' })
const message = useMessage() //
const dialogVisible = ref(false) //
const dbTableLoading = ref(true) //
const dbTableList = ref<CodegenApi.DatabaseTableVO[]>([]) //
const queryParams = reactive({
name: undefined,
comment: undefined,
dataSourceConfigId: 0
})
const queryFormRef = ref() //
const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) //
/** 查询表数据 */
const getList = async () => {
dbTableLoading.value = true
try {
dbTableList.value = await CodegenApi.getSchemaTableList(queryParams)
} finally {
dbTableLoading.value = false
}
}
/** 重置操作 */
const resetQuery = async () => {
queryParams.name = undefined
queryParams.comment = undefined
queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
await getList()
}
/** 打开弹窗 */
const open = async () => {
//
dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
dialogVisible.value = true
//
await getList()
}
defineExpose({ open }) // open
/** 关闭弹窗 */
const close = () => {
dialogVisible.value = false
tableList.value = []
}
const tableRef = ref<typeof ElTable>() // Ref
const tableList = ref<string[]>([]) //
/** 处理某一行的点击 */
const handleRowClick = (row) => {
unref(tableRef)?.toggleRowSelection(row)
}
/** 多选框选中数据 */
const handleSelectionChange = (selection) => {
tableList.value = selection.map((item) => item.name)
}
/** 导入按钮操作 */
const handleImportTable = async () => {
await CodegenApi.createCodegenList({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tableList.value
})
message.success('导入成功')
emit('success')
close()
}
const emit = defineEmits(['success'])
</script>

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

@ -0,0 +1,258 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
class="!w-240px"
clearable
placeholder="请输入表名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
class="!w-240px"
clearable
placeholder="请输入表描述"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button type="info" plain @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button v-hasPermi="['infra:codegen:create']" type="primary" @click="openImportTable()">
<Icon class="mr-5px" icon="ep:zoom-in" />
导入
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="数据源">
<template #default="scope">
{{
dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
}}
</template>
</el-table-column>
<el-table-column align="center" label="表名称" prop="tableName" width="200" />
<el-table-column
:show-overflow-tooltip="true"
align="center"
label="表描述"
prop="tableComment"
width="200"
/>
<el-table-column align="center" label="实体" prop="className" width="200" />
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="更新时间"
prop="createTime"
width="180"
/>
<el-table-column align="center" fixed="right" label="操作" width="300px">
<template #default="scope">
<el-button
v-hasPermi="['infra:codegen:preview']"
link
type="primary"
@click="handlePreview(scope.row)"
>
<Icon icon="ep:view" />
预览
</el-button>
<el-button
v-hasPermi="['infra:codegen:update']"
link
type="primary"
@click="handleUpdate(scope.row.id)"
>
<Icon icon="ep:edit" />
编辑
</el-button>
<el-button
v-hasPermi="['infra:codegen:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
>
<Icon icon="ep:delete" />
删除
</el-button>
<el-button
v-hasPermi="['infra:codegen:update']"
link
type="primary"
@click="handleSyncDB(scope.row)"
>
<Icon icon="ep:refresh" />
同步
</el-button>
<el-button
v-hasPermi="['infra:codegen:download']"
link
type="primary"
@click="handleGenTable(scope.row)"
>
<Icon icon="fa:code" />
生成代码
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 弹窗导入表 -->
<ImportTable ref="importRef" @success="getList" />
<!-- 弹窗预览代码 -->
<PreviewCode ref="previewRef" />
</template>
<script lang="ts" setup>
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 PreviewCode from './PreviewCode.vue'
defineOptions({ name: 'InfraCodegen' })
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
tableName: undefined,
tableComment: undefined,
createTime: []
})
const queryFormRef = ref() //
const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CodegenApi.getCodegenTablePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 导入操作 */
const importRef = ref()
const openImportTable = () => {
importRef.value.open()
}
/** 编辑操作 */
const handleUpdate = (id: number) => {
push('/codegen/edit?id=' + id)
}
/** 预览操作 */
const previewRef = ref()
const handlePreview = (row: CodegenApi.CodegenTableVO) => {
previewRef.value.open(row.id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await CodegenApi.deleteCodegenTable(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 同步操作 */
const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => {
// DB
const tableName = row.tableName
try {
await message.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'))
await CodegenApi.syncCodegenFromDB(row.id)
message.success('同步成功')
} catch {}
}
/** 生成代码操作 */
const handleGenTable = async (row: CodegenApi.CodegenTableVO) => {
const res = await CodegenApi.downloadCodegen(row.id)
download.zip(res, 'codegen-' + row.className + '.zip')
}
/** 初始化 **/
onMounted(async () => {
await getList()
//
dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
})
</script>

131
src/views/infra/config/ConfigForm.vue

@ -0,0 +1,131 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="参数分类" prop="category">
<el-input v-model="formData.category" placeholder="请输入参数分类" />
</el-form-item>
<el-form-item label="参数名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数键名" prop="key">
<el-input v-model="formData.key" placeholder="请输入参数键名" />
</el-form-item>
<el-form-item label="参数键值" prop="value">
<el-input v-model="formData.value" placeholder="请输入参数键值" />
</el-form-item>
<el-form-item label="是否可见" prop="visible">
<el-radio-group v-model="formData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraConfigForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
category: '',
name: '',
key: '',
value: '',
visible: true,
remark: ''
})
const formRules = reactive({
category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await ConfigApi.getConfig(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as ConfigApi.ConfigVO
if (formType.value === 'create') {
await ConfigApi.createConfig(data)
message.success(t('common.createSuccess'))
} else {
await ConfigApi.updateConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
category: '',
name: '',
key: '',
value: '',
visible: true,
remark: ''
}
formRef.value?.resetFields()
}
</script>

169
src/views/infra/config/index.vue

@ -0,0 +1,169 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="参数名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入参数名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="参数键名" prop="key">
<el-input v-model="queryParams.key" placeholder="请输入参数键名" clearable @keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item label="系统内置" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择系统内置" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE)" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button type="info" plain @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['infra:config:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" @click="handleExport" :loading="exportLoading" v-hasPermi="['infra:config:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="参数主键" align="center" prop="id" />
<el-table-column label="参数分类" align="center" prop="category" />
<el-table-column label="参数名称" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="参数键名" align="center" prop="key" :show-overflow-tooltip="true" />
<el-table-column label="参数键值" align="center" prop="value" />
<el-table-column label="是否可见" align="center" prop="visible">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.visible" />
</template>
</el-table-column>
<el-table-column label="系统内置" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" fixed="right" width="180">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['infra:config:update']">
<Icon icon="ep:edit" />
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['infra:config:delete']">
<Icon icon="ep:delete" />
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ConfigApi from '@/api/infra/config'
import ConfigForm from './ConfigForm.vue'
defineOptions({ name: 'InfraConfig' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
key: undefined,
type: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ConfigApi.getConfigPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await ConfigApi.deleteConfig(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await ConfigApi.exportConfig(queryParams)
download.excel(data, '参数配置.xlsx')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

21
src/views/infra/customInterface/index.vue

@ -0,0 +1,21 @@
<template>
<IFrame :src="src"/>
</template>
<script lang="ts" setup name="CustomInterface">
// import { getAccessToken, getTenantId } from '@/utils/auth'
const src = import.meta.env.VITE_INTERFACE_URL
// window.MAGIC_EDITOR_CONFIG = {
// request: {
// beforeSend: function (config) {
// // Cookie
// config.headers.token = getAccessToken() // Token
// return config;
// }
// },
// getMagicTokenValue: function () {
// var token = getAccessToken(); // token
// return token;
// }
// }
</script>

111
src/views/infra/dataSourceConfig/DataSourceConfigForm.vue

@ -0,0 +1,111 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="数据源名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="数据源连接" prop="url">
<el-input v-model="formData.url" placeholder="请输入数据源连接" />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
defineOptions({ name: 'InfraDataSourceConfigForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref<DataSourceConfigApi.DataSourceConfigVO>({
id: undefined,
name: '',
url: '',
username: '',
password: ''
})
const formRules = reactive({
name: [{ required: true, message: '数据源名称不能为空', trigger: 'blur' }],
url: [{ required: true, message: '数据源连接不能为空', trigger: 'blur' }],
username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await DataSourceConfigApi.getDataSourceConfig(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as DataSourceConfigApi.DataSourceConfigVO
if (formType.value === 'create') {
await DataSourceConfigApi.createDataSourceConfig(data)
message.success(t('common.createSuccess'))
} else {
await DataSourceConfigApi.updateDataSourceConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
url: '',
username: '',
password: ''
}
formRef.value?.resetFields()
}
</script>

87
src/views/infra/dataSourceConfig/index.vue

@ -0,0 +1,87 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :inline="true">
<el-form-item>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['infra:data-source-config:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="主键编号" align="center" prop="id" />
<el-table-column label="数据源名称" align="center" prop="name" />
<el-table-column label="数据源连接" align="center" prop="url" :show-overflow-tooltip="true" />
<el-table-column label="用户名" align="center" prop="username" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" fixed="right" width="180">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['infra:data-source-config:update']" :disabled="scope.row.id === 0">
<Icon icon="ep:edit" />
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['infra:data-source-config:delete']" :disabled="scope.row.id === 0">
<Icon icon="ep:delete" />
删除
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DataSourceConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
import DataSourceConfigForm from './DataSourceConfigForm.vue'
defineOptions({ name: 'InfraDataSourceConfig' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
list.value = await DataSourceConfigApi.getDataSourceConfigList()
} finally {
loading.value = false
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await DataSourceConfigApi.deleteDataSourceConfig(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

57
src/views/infra/dbDoc/index.vue

@ -0,0 +1,57 @@
<template>
<ContentWrap title="数据库文档">
<div class="mb-10px">
<el-button type="primary" @click="handleExport('HTML')">
<Icon icon="ep:download" /> 导出 HTML
</el-button>
<el-button type="primary" @click="handleExport('Word')">
<Icon icon="ep:download" /> 导出 Word
</el-button>
<el-button type="primary" @click="handleExport('Markdown')">
<Icon icon="ep:download" /> 导出 Markdown
</el-button>
</div>
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import download from '@/utils/download'
import * as DbDocApi from '@/api/infra/dbDoc'
defineOptions({ name: 'InfraDBDoc' })
const loading = ref(true) //
const src = ref('') // HTML
/** 页面加载 */
const init = async () => {
try {
const data = await DbDocApi.exportHtml()
const blob = new Blob([data], { type: 'text/html' })
src.value = window.URL.createObjectURL(blob)
} finally {
loading.value = false
}
}
/** 处理导出 */
const handleExport = async (type: string) => {
if (type === 'HTML') {
const res = await DbDocApi.exportHtml()
download.html(res, '数据库文档.html')
}
if (type === 'Word') {
const res = await DbDocApi.exportWord()
download.word(res, '数据库文档.doc')
}
if (type === 'Markdown') {
const res = await DbDocApi.exportMarkdown()
download.markdown(res, '数据库文档.md')
}
}
/** 初始化 */
onMounted(async () => {
await init()
})
</script>

25
src/views/infra/druid/index.vue

@ -0,0 +1,25 @@
<template>
<ContentWrap>
<IFrame v-if="!loading" :src="url" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraDruid' })
const loading = ref(true) //
const url = ref(import.meta.env.VITE_BASE_URL + '/druid/index.html')
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.druid')
if (data && data.length > 0) {
url.value = data
}
} finally {
loading.value = false
}
})
</script>

104
src/views/infra/file/FileForm.vue

@ -0,0 +1,104 @@
<template>
<Dialog v-model="dialogVisible" title="上传文件">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="url"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".jpg, .png, .gif"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入 jpgpnggif 格式文件
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitFileForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'InfraFileForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const url = import.meta.env.VITE_UPLOAD_URL
const uploadHeaders = ref() // Header
const fileList = ref([]) //
const data = ref({ path: '' })
const uploadRef = ref()
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // open
/** 处理上传的文件发生变化 */
const handleFileChange = (file) => {
data.value.path = file.name
}
/** 提交表单 */
const submitFileForm = () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
unref(uploadRef)?.submit()
}
/** 文件上传成功处理 */
const emit = defineEmits(['success']) // success
const submitFormSuccess = () => {
//
dialogVisible.value = false
formLoading.value = false
unref(uploadRef)?.clearFiles()
//
message.success(t('common.createSuccess'))
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
//
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

164
src/views/infra/file/index.vue

@ -0,0 +1,164 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件路径" prop="path">
<el-input
v-model="queryParams.path"
placeholder="请输入文件路径"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="文件类型" prop="type" width="80">
<el-input
v-model="queryParams.type"
placeholder="请输入文件类型"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/>
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button type="info" plain @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button type="primary" @click="openForm">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="文件名" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="文件路径" align="center" prop="path" :show-overflow-tooltip="true" />
<el-table-column label="URL" align="center" prop="url" :show-overflow-tooltip="true" />
<el-table-column
label="文件大小"
align="center"
prop="size"
width="120"
:formatter="fileSizeFormatter"
/>
<el-table-column label="文件类型" align="center" prop="type" width="180px" />
<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="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['infra:config:delete']"
>
<Icon icon="ep:delete" />
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { fileSizeFormatter } from '@/utils'
import { dateFormatter } from '@/utils/formatTime'
import * as FileApi from '@/api/infra/file'
import FileForm from './FileForm.vue'
defineOptions({ name: 'InfraFile' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
type: undefined,
createTime: []
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await FileApi.getFilePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = () => {
formRef.value.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await FileApi.deleteFile(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

195
src/views/infra/fileConfig/FileConfigForm.vue

@ -0,0 +1,195 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="配置名" prop="name">
<el-input v-model="formData.name" placeholder="请输入配置名" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="存储器" prop="storage">
<el-select
v-model="formData.storage"
:disabled="formData.id !== undefined"
placeholder="请选择存储器"
>
<el-option
v-for="dict in getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
:key="dict.value"
:label="dict.label"
:value="parseInt(dict.value)"
/>
</el-select>
</el-form-item>
<!-- DB -->
<!-- Local / FTP / SFTP -->
<el-form-item
v-if="formData.storage >= 10 && formData.storage <= 12"
label="基础路径"
prop="config.basePath"
>
<el-input v-model="formData.config.basePath" placeholder="请输入基础路径" />
</el-form-item>
<el-form-item
v-if="formData.storage >= 11 && formData.storage <= 12"
label="主机地址"
prop="config.host"
>
<el-input v-model="formData.config.host" placeholder="请输入主机地址" />
</el-form-item>
<el-form-item
v-if="formData.storage >= 11 && formData.storage <= 12"
label="主机端口"
prop="config.port"
>
<el-input-number v-model="formData.config.port" :min="0" placeholder="请输入主机端口" />
</el-form-item>
<el-form-item
v-if="formData.storage >= 11 && formData.storage <= 12"
label="用户名"
prop="config.username"
>
<el-input v-model="formData.config.username" placeholder="请输入密码" />
</el-form-item>
<el-form-item
v-if="formData.storage >= 11 && formData.storage <= 12"
label="密码"
prop="config.password"
>
<el-input v-model="formData.config.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item v-if="formData.storage === 11" label="连接模式" prop="config.mode">
<el-radio-group v-model="formData.config.mode">
<el-radio key="Active" label="Active">主动模式</el-radio>
<el-radio key="Passive" label="Passive">被动模式</el-radio>
</el-radio-group>
</el-form-item>
<!-- S3 -->
<el-form-item v-if="formData.storage === 20" label="节点地址" prop="config.endpoint">
<el-input v-model="formData.config.endpoint" placeholder="请输入节点地址" />
</el-form-item>
<el-form-item v-if="formData.storage === 20" label="存储 bucket" prop="config.bucket">
<el-input v-model="formData.config.bucket" placeholder="请输入 bucket" />
</el-form-item>
<el-form-item v-if="formData.storage === 20" label="accessKey" prop="config.accessKey">
<el-input v-model="formData.config.accessKey" placeholder="请输入 accessKey" />
</el-form-item>
<el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
<el-input v-model="formData.config.accessSecret" placeholder="请输入 accessSecret" />
</el-form-item>
<!-- 通用 -->
<el-form-item v-if="formData.storage === 20" label="自定义域名">
<!-- 无需参数校验所以去掉 prop -->
<el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
</el-form-item>
<el-form-item v-else-if="formData.storage" label="自定义域名" prop="config.domain">
<el-input v-model="formData.config.domain" placeholder="请输入自定义域名" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import * as FileConfigApi from '@/api/infra/fileConfig'
defineOptions({ name: 'InfraFileConfigForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: '',
storage: 0,
remark: '',
config: {}
})
const formRules = reactive({
name: [{ required: true, message: '配置名不能为空', trigger: 'blur' }],
storage: [{ required: true, message: '存储器不能为空', trigger: 'change' }],
config: {
basePath: [{ required: true, message: '基础路径不能为空', trigger: 'blur' }],
host: [{ required: true, message: '主机地址不能为空', trigger: 'blur' }],
port: [{ required: true, message: '主机端口不能为空', trigger: 'blur' }],
username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
mode: [{ required: true, message: '连接模式不能为空', trigger: 'change' }],
endpoint: [{ required: true, message: '节点地址不能为空', trigger: 'blur' }],
bucket: [{ required: true, message: '存储 bucket 不能为空', trigger: 'blur' }],
accessKey: [{ required: true, message: 'accessKey 不能为空', trigger: 'blur' }],
accessSecret: [{ required: true, message: 'accessSecret 不能为空', trigger: 'blur' }],
domain: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
}
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await FileConfigApi.getFileConfig(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as FileConfigApi.FileConfigVO
if (formType.value === 'create') {
await FileConfigApi.createFileConfig(data)
message.success(t('common.createSuccess'))
} else {
await FileConfigApi.updateFileConfig(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
storage: 0,
remark: '',
config: {}
}
formRef.value?.resetFields()
}
</script>

168
src/views/infra/fileConfig/index.vue

@ -0,0 +1,168 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="配置名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入配置名" clearable @keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item label="存储器" prop="storage">
<el-select v-model="queryParams.storage" placeholder="请选择存储器" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button type="info" plain @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['infra:file-config:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="配置名" align="center" prop="name" />
<el-table-column label="存储器" align="center" prop="storage">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="scope.row.storage" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="主配置" align="center" prop="primary">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" width="320px" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['infra:file-config:update']">
<Icon icon="ep:edit" />
编辑
</el-button>
<el-button link type="primary" :disabled="scope.row.master" @click="handleMaster(scope.row.id)"
v-hasPermi="['infra:file-config:update']">
<Icon icon="ep:tools" />
主配置
</el-button>
<el-button link type="primary" @click="handleTest(scope.row.id)">
<Icon icon="ep:cellphone" />测试
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['infra:config:delete']">
<Icon icon="ep:delete" />
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<FileConfigForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import * as FileConfigApi from '@/api/infra/fileConfig'
import FileConfigForm from './FileConfigForm.vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
defineOptions({ name: 'InfraFileConfig' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
storage: undefined,
createTime: []
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await FileConfigApi.getFileConfigPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await FileConfigApi.deleteFileConfig(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** 主配置按钮操作 */
const handleMaster = async (id) => {
try {
await message.confirm('是否确认修改配置编号为"' + id + '"的数据项为主配置?')
await FileConfigApi.updateFileConfigMaster(id)
message.success(t('common.updateSuccess'))
await getList()
} catch { }
}
/** 测试按钮操作 */
const handleTest = async (id) => {
try {
const response = await FileConfigApi.testFileConfig(id)
message.alert('测试通过,上传文件成功!访问地址:' + response)
} catch { }
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

73
src/views/infra/job/JobDetail.vue

@ -0,0 +1,73 @@
<template>
<Dialog v-model="dialogVisible" title="任务详细" width="700px">
<el-descriptions :column="1" border>
<el-descriptions-item label="任务编号" min-width="60">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="任务名称">
{{ detailData.name }}
</el-descriptions-item>
<el-descriptions-item label="任务名称">
<dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="detailData.status" />
</el-descriptions-item>
<el-descriptions-item label="处理器的名字">
{{ detailData.handlerName }}
</el-descriptions-item>
<el-descriptions-item label="处理器的参数">
{{ detailData.handlerParam }}
</el-descriptions-item>
<el-descriptions-item label="Cron 表达式">
{{ detailData.cronExpression }}
</el-descriptions-item>
<el-descriptions-item label="重试次数">
{{ detailData.retryCount }}
</el-descriptions-item>
<el-descriptions-item label="重试间隔">
{{ detailData.retryInterval + ' 毫秒' }}
</el-descriptions-item>
<el-descriptions-item label="监控超时时间">
{{ detailData.monitorTimeout > 0 ? detailData.monitorTimeout + ' 毫秒' : '未开启' }}
</el-descriptions-item>
<el-descriptions-item label="后续执行时间">
<el-timeline>
<el-timeline-item
v-for="(nextTime, index) in nextTimes"
:key="index"
:timestamp="formatDate(nextTime)"
>
{{ index + 1 }}
</el-timeline-item>
</el-timeline>
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as JobApi from '@/api/infra/job'
defineOptions({ name: 'InfraJobDetail' })
const dialogVisible = ref(false) //
const detailLoading = ref(false) //
const detailData = ref({}) //
const nextTimes = ref([]) //
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
//
if (id) {
detailLoading.value = true
try {
detailData.value = await JobApi.getJob(id)
//
nextTimes.value = await JobApi.getJobNextTimes(id)
} finally {
detailLoading.value = false
}
}
}
defineExpose({ open }) // open
</script>

131
src/views/infra/job/JobForm.vue

@ -0,0 +1,131 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="任务名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="处理器的名字" prop="handlerName">
<el-input
:readonly="formData.id !== undefined"
v-model="formData.handlerName"
placeholder="请输入处理器的名字"
/>
</el-form-item>
<el-form-item label="处理器的参数" prop="handlerParam">
<el-input v-model="formData.handlerParam" placeholder="请输入处理器的参数" />
</el-form-item>
<el-form-item label="CRON 表达式" prop="cronExpression">
<crontab v-model="formData.cronExpression" />
</el-form-item>
<el-form-item label="重试次数" prop="retryCount">
<el-input
v-model="formData.retryCount"
placeholder="请输入重试次数。设置为 0 时,不进行重试"
/>
</el-form-item>
<el-form-item label="重试间隔" prop="retryInterval">
<el-input
v-model="formData.retryInterval"
placeholder="请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔"
/>
</el-form-item>
<el-form-item label="监控超时时间" prop="monitorTimeout">
<el-input v-model="formData.monitorTimeout" placeholder="请输入监控超时时间,单位:毫秒" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submitForm" :loading="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as JobApi from '@/api/infra/job'
defineOptions({ name: 'JobForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: '',
handlerName: '',
handlerParam: '',
cronExpression: ''
})
const formRules = reactive({
name: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }],
handlerName: [{ required: true, message: '处理器的名字不能为空', trigger: 'blur' }],
cronExpression: [{ required: true, message: 'CRON 表达式不能为空', trigger: 'blur' }],
retryCount: [{ required: true, message: '重试次数不能为空', trigger: 'blur' }],
retryInterval: [{ required: true, message: '重试间隔不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await JobApi.getJob(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交按钮 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value as unknown as JobApi.JobVO
if (formType.value === 'create') {
await JobApi.createJob(data)
message.success(t('common.createSuccess'))
} else {
await JobApi.updateJob(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
handlerName: '',
handlerParam: '',
cronExpression: ''
}
formRef.value?.resetFields()
}
</script>

255
src/views/infra/job/index.vue

@ -0,0 +1,255 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px">
<el-form-item label="任务名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入任务名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable class="!w-240px">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_JOB_STATUS)" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="处理器的名字" prop="handlerName">
<el-input v-model="queryParams.handlerName" placeholder="请输入处理器的名字" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button type="info" plain @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" @click="openForm('create')" v-hasPermi="['infra:job:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" @click="handleExport" :loading="exportLoading" v-hasPermi="['infra:job:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="warning" @click="handleJobLog" v-hasPermi="['infra:job:query']">
<Icon icon="ep:zoom-in" class="mr-5px" /> 执行日志
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="任务编号" align="center" prop="id" />
<el-table-column label="任务名称" align="center" prop="name" />
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="处理器的名字" align="center" prop="handlerName" />
<el-table-column label="处理器的参数" align="center" prop="handlerParam" />
<el-table-column label="CRON 表达式" align="center" prop="cronExpression" />
<el-table-column label="操作" align="center" fixed="right" width="260">
<template #default="scope">
<el-button type="primary" link @click="openForm('update', scope.row.id)" v-hasPermi="['infra:job:update']">
<Icon icon="ep:edit" />
修改
</el-button>
<el-button type="primary" link @click="handleChangeStatus(scope.row)" v-hasPermi="['infra:job:update']">
<Icon :icon="scope.row.status === InfraJobStatusEnum.STOP ? 'ep:video-play' : 'ep:video-pause'" />
{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}
</el-button>
<el-button type="danger" link @click="handleDelete(scope.row.id)" v-hasPermi="['infra:job:delete']">
<Icon icon="ep:delete" />
删除
</el-button>
<el-dropdown @command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['infra:job:trigger', 'infra:job:query']">
<el-button type="primary" link>
<Icon icon="ep:d-arrow-right" /> 更多
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleRun" v-if="checkPermi(['infra:job:trigger'])">
<Icon icon="ep:video-play" /> 执行一次
</el-dropdown-item>
<el-dropdown-item command="openDetail" v-if="checkPermi(['infra:job:query'])">
<Icon icon="fa-solid:tasks" />任务详细
</el-dropdown-item>
<el-dropdown-item command="handleJobLog" v-if="checkPermi(['infra:job:query'])">
<Icon icon="ep:document" /> 调度日志
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<JobForm ref="formRef" @success="getList" />
<!-- 表单弹窗查看 -->
<JobDetail ref="detailRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { checkPermi } from '@/utils/permission'
import JobForm from './JobForm.vue'
import JobDetail from './JobDetail.vue'
import download from '@/utils/download'
import * as JobApi from '@/api/infra/job'
import { InfraJobStatusEnum } from '@/utils/constants'
defineOptions({ name: 'InfraJob' })
const { t } = useI18n() //
const message = useMessage() //
const { push } = useRouter() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
handlerName: undefined
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await JobApi.getJobPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await JobApi.exportJob(queryParams)
download.excel(data, '定时任务.xlsx')
} catch {
} finally {
exportLoading.value = false
}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 修改状态操作 */
const handleChangeStatus = async (row: JobApi.JobVO) => {
try {
//
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
await message.confirm(
'确认要' + text + '定时任务编号为"' + row.id + '"的数据项?',
t('common.reminder')
)
const status =
row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
await JobApi.updateJobStatus(row.id, status)
message.success(text + '成功')
//
await getList()
} catch {
//
row.status =
row.status === InfraJobStatusEnum.NORMAL ? InfraJobStatusEnum.STOP : InfraJobStatusEnum.NORMAL
}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await JobApi.deleteJob(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** '更多'操作按钮 */
const handleCommand = (command, row) => {
switch (command) {
case 'handleRun':
handleRun(row)
break
case 'openDetail':
openDetail(row.id)
break
case 'handleJobLog':
handleJobLog(row?.id)
break
default:
break
}
}
/** 执行一次 */
const handleRun = async (row: JobApi.JobVO) => {
try {
//
await message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder'))
//
await JobApi.runJob(row.id)
message.success('执行成功')
//
await getList()
} catch { }
}
/** 查看操作 */
const detailRef = ref()
const openDetail = (id: number) => {
detailRef.value.open(id)
}
/** 跳转执行日志 */
const handleJobLog = (id: number) => {
if (id > 0) {
push('/job/job-log?id=' + id)
} else {
push('/job/job-log')
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

59
src/views/infra/job/logger/JobLogDetail.vue

@ -0,0 +1,59 @@
<template>
<Dialog v-model="dialogVisible" title="任务详细" width="700px">
<el-descriptions :column="1" border>
<el-descriptions-item label="日志编号" min-width="60">
{{ detailData.id }}
</el-descriptions-item>
<el-descriptions-item label="任务编号">
{{ detailData.jobId }}
</el-descriptions-item>
<el-descriptions-item label="处理器的名字">
{{ detailData.handlerName }}
</el-descriptions-item>
<el-descriptions-item label="处理器的参数">
{{ detailData.handlerParam }}
</el-descriptions-item>
<el-descriptions-item label="第几次执行">
{{ detailData.executeIndex }}
</el-descriptions-item>
<el-descriptions-item label="执行时间">
{{ formatDate(detailData.beginTime) + ' ~ ' + formatDate(detailData.endTime) }}
</el-descriptions-item>
<el-descriptions-item label="执行时长">
{{ detailData.duration + ' 毫秒' }}
</el-descriptions-item>
<el-descriptions-item label="任务状态">
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="detailData.status" />
</el-descriptions-item>
<el-descriptions-item label="执行结果">
{{ detailData.duration + ' result' }}
</el-descriptions-item>
</el-descriptions>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import * as JobLogApi from '@/api/infra/jobLog'
defineOptions({ name: 'JobLogDetail' })
const dialogVisible = ref(false) //
const detailLoading = ref(false) //
const detailData = ref({}) //
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
//
if (id) {
detailLoading.value = true
try {
detailData.value = await JobLogApi.getJobLog(id)
} finally {
detailLoading.value = false
}
}
}
defineExpose({ open }) // open
</script>

196
src/views/infra/job/logger/index.vue

@ -0,0 +1,196 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="120px"
>
<el-form-item label="处理器的名字" prop="handlerName">
<el-input
v-model="queryParams.handlerName"
placeholder="请输入处理器的名字"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="开始执行时间" prop="beginTime">
<el-date-picker
v-model="queryParams.beginTime"
type="date"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择开始执行时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item label="结束执行时间" prop="endTime">
<el-date-picker
v-model="queryParams.endTime"
type="date"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择结束执行时间"
clearable
:default-time="new Date('1 23:59:59')"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="任务状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择任务状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:job:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="日志编号" align="center" prop="id" />
<el-table-column label="任务编号" align="center" prop="jobId" />
<el-table-column label="处理器的名字" align="center" prop="handlerName" />
<el-table-column label="处理器的参数" align="center" prop="handlerParam" />
<el-table-column label="第几次执行" align="center" prop="executeIndex" />
<el-table-column label="执行时间" align="center" width="170s">
<template #default="scope">
<span>{{ formatDate(scope.row.beginTime) + ' ~ ' + formatDate(scope.row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column label="执行时长" align="center" prop="duration">
<template #default="scope">
<span>{{ scope.row.duration + ' 毫秒' }}</span>
</template>
</el-table-column>
<el-table-column label="任务状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
link
@click="openDetail(scope.row.id)"
v-hasPermi="['infra:job:query']"
>
详细
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗查看 -->
<JobLogDetail ref="detailRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import JobLogDetail from './JobLogDetail.vue'
import * as JobLogApi from '@/api/infra/jobLog'
defineOptions({ name: 'InfraJobLog' })
const message = useMessage() //
const { query } = useRoute() //
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
jobId: query.id,
handlerName: undefined,
beginTime: undefined,
endTime: undefined,
status: undefined
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await JobLogApi.getJobLogPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 查看操作 */
const detailRef = ref()
const openDetail = (rowId?: number) => {
detailRef.value.open(rowId)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await JobLogApi.exportJobLog(queryParams)
download.excel(data, '定时任务执行日志.xlsx')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

266
src/views/infra/redis/index.vue

@ -0,0 +1,266 @@
<template>
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
<el-row>
<!-- 基本信息 -->
<el-col :span="24" class="card-box" shadow="hover">
<el-card>
<el-descriptions title="基本信息" :column="6" border>
<el-descriptions-item label="Redis版本 :">
{{ cache?.info?.redis_version }}
</el-descriptions-item>
<el-descriptions-item label="运行模式 :">
{{ cache?.info?.redis_mode == 'standalone' ? '单机' : '集群' }}
</el-descriptions-item>
<el-descriptions-item label="端口 :">
{{ cache?.info?.tcp_port }}
</el-descriptions-item>
<el-descriptions-item label="客户端数 :">
{{ cache?.info?.connected_clients }}
</el-descriptions-item>
<el-descriptions-item label="运行时间(天) :">
{{ cache?.info?.uptime_in_days }}
</el-descriptions-item>
<el-descriptions-item label="使用内存 :">
{{ cache?.info?.used_memory_human }}
</el-descriptions-item>
<el-descriptions-item label="使用CPU :">
{{ cache?.info ? parseFloat(cache?.info?.used_cpu_user_children).toFixed(2) : '' }}
</el-descriptions-item>
<el-descriptions-item label="内存配置 :">
{{ cache?.info?.maxmemory_human }}
</el-descriptions-item>
<el-descriptions-item label="AOF是否开启 :">
{{ cache?.info?.aof_enabled == '0' ? '否' : '是' }}
</el-descriptions-item>
<el-descriptions-item label="RDB是否成功 :">
{{ cache?.info?.rdb_last_bgsave_status }}
</el-descriptions-item>
<el-descriptions-item label="Key数量 :">
{{ cache?.dbSize }}
</el-descriptions-item>
<el-descriptions-item label="网络入口/出口 :">
{{ cache?.info?.instantaneous_input_kbps }}kps/
{{ cache?.info?.instantaneous_output_kbps }}kps
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<!-- 命令统计 -->
<el-col :span="12" class="mt-3">
<el-card :gutter="12" shadow="hover">
<Echart :options="commandStatsRefChika" :height="420" />
</el-card>
</el-col>
<!-- 内存使用量统计 -->
<el-col :span="12" class="mt-3">
<el-card class="ml-3" :gutter="12" shadow="hover">
<Echart :options="usedmemoryEchartChika" :height="420" />
</el-card>
</el-col>
</el-row>
</el-scrollbar>
</template>
<script lang="ts" setup>
import * as RedisApi from '@/api/infra/redis'
import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
const cache = ref<RedisMonitorInfoVO>()
//
const readRedisInfo = async () => {
const data = await RedisApi.getCache()
cache.value = data
}
// 使
const usedmemoryEchartChika = reactive<any>({
title: {
//
text: '内存使用情况',
left: 'center',
show: true, // , true
offsetCenter: [0, '20%'], //
color: 'yellow', // , #333
fontSize: 20 // , 15
},
toolbox: {
show: false,
feature: {
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 50,
splitNumber: 10,
//
color: '#F5C74E',
radius: '85%',
center: ['50%', '50%'],
startAngle: 225,
endAngle: -45,
axisLine: {
// 线
lineStyle: {
// lineStyle线
color: [
[0.2, '#7FFF00'],
[0.8, '#00FFFF'],
[1, '#FF0000']
],
//width: 6
width: 10
}
},
axisTick: {
//
//线5线
length: 5, // length线
lineStyle: {
// lineStyle线
color: '#76D9D7'
}
},
splitLine: {
// 线
length: 20, // length线
lineStyle: {
// lineStylelineStyle线
color: '#76D9D7'
}
},
axisLabel: {
color: '#76D9D7',
distance: 15,
fontSize: 15
},
pointer: {
//
width: 7,
show: true
},
detail: {
textStyle: {
fontWeight: 'normal',
// 50
fontSize: 15,
color: '#FFFFFF'
},
valueAnimation: true
},
progress: {
show: true
}
}
]
})
// 使
const commandStatsRefChika = reactive({
title: {
text: '命令统计',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 30,
top: 10,
bottom: 20,
data: [] as any[],
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: [] as any[],
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: {
show: true
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
/** 加载数据 */
const getSummary = () => {
//
initCommandStatsChart()
usedMemoryInstance()
}
/** 命令使用情况 */
const initCommandStatsChart = async () => {
usedmemoryEchartChika.series[0].data = []
//
try {
const data = await RedisApi.getCache()
cache.value = data
//
const commandStats = [] as any[]
const nameList = [] as string[]
data.commandStats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
commandStatsRefChika.legend.data = nameList
commandStatsRefChika.series[0].data = commandStats
} catch {}
}
const usedMemoryInstance = async () => {
try {
const data = await RedisApi.getCache()
cache.value = data
//
usedmemoryEchartChika.series[0].detail = {
show: true, // , true
offsetCenter: [0, '50%'], //
color: 'auto', // , auto
fontSize: 30, // , 15
formatter: cache.value!.info.used_memory_human //
}
usedmemoryEchartChika.series[0].data[0] = {
value: cache.value!.info.used_memory_human,
name: '内存消耗'
}
console.log(cache.value!.info)
usedmemoryEchartChika.tooltip = {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
}
} catch {}
}
/** 初始化 **/
onMounted(() => {
// redis
readRedisInfo()
//
getSummary()
})
</script>

28
src/views/infra/server/index.vue

@ -0,0 +1,28 @@
<template>
<ContentWrap>
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraAdminServer' })
const loading = ref(true) //
const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications')
/** 初始化 */
onMounted(async () => {
try {
// 访 404
// 1boot https://doc.iocoder.cn/server-monitor/
// 2cloud https://cloud.iocoder.cn/server-monitor/
const data = await ConfigApi.getConfigKey('url.spring-boot-admin')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

25
src/views/infra/skywalking/index.vue

@ -0,0 +1,25 @@
<template>
<ContentWrap>
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraSkyWalking' })
const loading = ref(true) //
const src = ref('http://skywalking.shop.iocoder.cn')
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.skywalking')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

26
src/views/infra/swagger/index.vue

@ -0,0 +1,26 @@
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraSwagger' })
const loading = ref(true) //
const src = ref(import.meta.env.VITE_BASE_URL + '/doc.html') // Knife4j UI
// const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI
/** 初始化 */
onMounted(async () => {
try {
const data = await ConfigApi.getConfigKey('url.swagger')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

4
src/views/infra/testDemo/index.vue

@ -0,0 +1,4 @@
<template>
<div>index</div>
</template>
<script lang="ts" setup></script>

118
src/views/infra/webSocket/index.vue

@ -0,0 +1,118 @@
<template>
<div class="flex">
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>连接</span>
</div>
</template>
<div class="flex items-center">
<span class="mr-4 text-lg font-medium"> 连接状态: </span>
<el-tag :color="getTagColor">{{ status }}</el-tag>
</div>
<hr class="my-4" />
<div class="flex">
<el-input v-model="server" disabled>
<template #prepend> 服务地址</template>
</el-input>
<el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggle">
{{ getIsOpen ? '关闭连接' : '开启连接' }}
</el-button>
</div>
<p class="mt-4 text-lg font-medium">设置</p>
<hr class="my-4" />
<el-input
v-model="sendValue"
:autosize="{ minRows: 2, maxRows: 4 }"
:disabled="!getIsOpen"
clearable
type="textarea"
/>
<el-button :disabled="!getIsOpen" block class="mt-4" type="primary" @click="handlerSend">
发送
</el-button>
</el-card>
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>消息记录</span>
</div>
</template>
<div class="max-h-80 overflow-auto">
<ul>
<li v-for="item in getList" :key="item.time" class="mt-2">
<div class="flex items-center">
<span class="text-primary mr-2 font-medium">收到消息:</span>
<span>{{ formatDate(item.time) }}</span>
</div>
<div>
{{ item.res }}
</div>
</li>
</ul>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { useUserStore } from '@/store/modules/user'
import { useWebSocket } from '@vueuse/core'
defineOptions({ name: 'InfraWebSocket' })
const userStore = useUserStore()
const sendValue = ref('')
const server = ref(
(import.meta.env.VITE_BASE_URL + '/websocket/message').replace('http', 'ws') +
'?userId=' +
userStore.getUser.id
)
const state = reactive({
recordList: [] as { id: number; time: number; res: string }[]
})
const { status, data, send, close, open } = useWebSocket(server.value, {
autoReconnect: false,
heartbeat: true
})
watchEffect(() => {
if (data.value) {
try {
const res = JSON.parse(data.value)
state.recordList.push(res)
} catch (error) {
state.recordList.push({
res: data.value,
id: Math.ceil(Math.random() * 1000),
time: new Date().getTime()
})
}
}
})
const getIsOpen = computed(() => status.value === 'OPEN')
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red'))
const getList = computed(() => {
return [...state.recordList].reverse()
})
function handlerSend() {
send(sendValue.value)
sendValue.value = ''
}
function toggle() {
if (getIsOpen.value) {
close()
} else {
open()
}
}
</script>

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

@ -0,0 +1,278 @@
<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";
defineOptions({ name: 'LoginForm' })
const { t } = useI18n()
const message = useMessage()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const formLogin = ref()
const { validForm } = useFormValid(formLogin)
const { setLoginState, getLoginState } = useLoginState()
const { currentRoute, push } = useRouter()
const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
//
const captchaEnabled = ref(true);
const codeUrl = ref("");
const LoginRules = {
tenantName: [required],
username: [required],
password: [required]
}
const loginData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: {
tenantName: '闻荫源码',
username: 'admin',
password: '123456',
captchaVerification: '',
rememberMe: false,
code: '',
uuid: ''
}
})
const socialList = [
{ icon: 'ant-design:github-filled', type: 0 },
{ icon: 'ant-design:wechat-filled', type: 30 },
{ icon: 'ant-design:alipay-circle-filled', type: 0 },
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 }
]
//
// const getCode = async () => {
// //
// if (loginData.captchaEnable === 'false') {
// await handleLogin({})
// } else {
// //
// //
// verify.value.show()
// }
// }
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginData.loginForm.uuid = res.uuid;
}
});
}
//ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
authUtil.setTenantId(res)
}
}
//
const getCookie = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe ? true : false,
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
}
}
}
const loading = ref() // ElLoading.service
//
const handleLogin = async (params) => {
loginLoading.value = true
try {
await getTenantId()
const data = await validForm()
if (!data) {
return
}
const res = await LoginApi.login(loginData.loginForm)
if (!res) {
getCode()
return
}
loading.value = ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
if (loginData.loginForm.rememberMe) {
authUtil.setLoginForm(loginData.loginForm)
} else {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
// SSO
if (redirect.value.indexOf('sso') !== -1) {
window.location.href = window.location.href.replace('/login?redirect=', '')
} else {
console.log(redirect.value)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
} finally {
getCode()
loginLoading.value = false
loading.value.close()
}
}
//
const doSocialLogin = async (type: number) => {
if (type === 0) {
message.error('此方式未配置')
} else {
loginLoading.value = true
if (loginData.tenantEnable === 'true') {
await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
const res = await LoginApi.getTenantIdByName(value)
authUtil.setTenantId(res)
})
}
// redirectUri
const redirectUri =
location.origin + '/social-login?type=' + type + '&redirect=' + (redirect.value || '/')
//
const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
console.log(33)
window.location.href = res
}
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
onMounted(() => {
getCode();
getCookie()
})
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.login-code {
float: right;
width: 100%;
height: 38px;
img {
width: 100%;
height: auto;
max-width: 100px;
vertical-align: middle;
cursor: pointer;
}
}
</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