陈薪名
10 months ago
64 changed files with 8483 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||||
|
<template> |
||||
|
<Error type="403" @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error403' }) |
||||
|
|
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<Error @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error404' }) |
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
<template> |
||||
|
<Error type="500" @error-click="push('/')" /> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Error500' }) |
||||
|
const { push } = useRouter() |
||||
|
</script> |
@ -0,0 +1,381 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<el-card shadow="never"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<el-row :gutter="20" justify="space-between"> |
||||
|
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24"> |
||||
|
<div class="flex items-center"> |
||||
|
<img :src="avatar" alt="" class="mr-20px h-70px w-70px rounded-[50%]" /> |
||||
|
<div> |
||||
|
<div class="text-20px"> |
||||
|
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }} |
||||
|
</div> |
||||
|
<div class="mt-10px text-14px text-gray-500"> |
||||
|
{{ t('workplace.toady') }},20℃ - 32℃! |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-col> |
||||
|
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24"> |
||||
|
<div class="h-70px flex items-center justify-end lt-sm:mt-10px"> |
||||
|
<div class="px-8px text-right"> |
||||
|
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.project') }}</div> |
||||
|
<CountTo |
||||
|
class="text-20px" |
||||
|
:start-val="0" |
||||
|
:end-val="totalSate.project" |
||||
|
:duration="2600" |
||||
|
/> |
||||
|
</div> |
||||
|
<el-divider direction="vertical" /> |
||||
|
<div class="px-8px text-right"> |
||||
|
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.toDo') }}</div> |
||||
|
<CountTo |
||||
|
class="text-20px" |
||||
|
:start-val="0" |
||||
|
:end-val="totalSate.todo" |
||||
|
:duration="2600" |
||||
|
/> |
||||
|
</div> |
||||
|
<el-divider direction="vertical" border-style="dashed" /> |
||||
|
<div class="px-8px text-right"> |
||||
|
<div class="mb-20px text-14px text-gray-400">{{ t('workplace.access') }}</div> |
||||
|
<CountTo |
||||
|
class="text-20px" |
||||
|
:start-val="0" |
||||
|
:end-val="totalSate.access" |
||||
|
:duration="2600" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</div> |
||||
|
|
||||
|
<el-row class="mt-5px" :gutter="20" justify="space-between"> |
||||
|
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-10px"> |
||||
|
<el-card shadow="never"> |
||||
|
<template #header> |
||||
|
<div class="h-3 flex justify-between"> |
||||
|
<span>{{ t('workplace.project') }}</span> |
||||
|
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link> |
||||
|
</div> |
||||
|
</template> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<el-row> |
||||
|
<el-col |
||||
|
v-for="(item, index) in projects" |
||||
|
:key="`card-${index}`" |
||||
|
:xl="8" |
||||
|
:lg="8" |
||||
|
:md="8" |
||||
|
:sm="24" |
||||
|
:xs="24" |
||||
|
> |
||||
|
<el-card shadow="hover"> |
||||
|
<div class="flex items-center"> |
||||
|
<Icon :icon="item.icon" :size="25" class="mr-10px" /> |
||||
|
<span class="text-16px">{{ item.name }}</span> |
||||
|
</div> |
||||
|
<div class="mt-15px text-14px text-gray-400">{{ t(item.message) }}</div> |
||||
|
<div class="mt-20px flex justify-between text-12px text-gray-400"> |
||||
|
<span>{{ item.personal }}</span> |
||||
|
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span> |
||||
|
</div> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
|
||||
|
<el-card shadow="never" class="mt-5px"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<el-row :gutter="20" justify="space-between"> |
||||
|
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24"> |
||||
|
<el-card shadow="hover" class="mb-10px"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<Echart :options="pieOptionsData" :height="280" /> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24"> |
||||
|
<el-card shadow="hover" class="mb-10px"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<Echart :options="barOptionsData" :height="280" /> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-10px"> |
||||
|
<el-card shadow="never"> |
||||
|
<template #header> |
||||
|
<div class="h-3 flex justify-between"> |
||||
|
<span>{{ t('workplace.shortcutOperation') }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<el-row> |
||||
|
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-10px"> |
||||
|
<div class="flex items-center"> |
||||
|
<Icon :icon="item.icon" class="mr-10px" /> |
||||
|
<el-link type="default" :underline="false" @click="setWatermark(item.name)"> |
||||
|
{{ item.name }} |
||||
|
</el-link> |
||||
|
</div> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
<el-card shadow="never" class="mt-10px"> |
||||
|
<template #header> |
||||
|
<div class="h-3 flex justify-between"> |
||||
|
<span>{{ t('workplace.notice') }}</span> |
||||
|
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link> |
||||
|
</div> |
||||
|
</template> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<div v-for="(item, index) in notice" :key="`dynamics-${index}`"> |
||||
|
<div class="flex items-center"> |
||||
|
<img :src="avatar" alt="" class="mr-20px h-35px w-35px rounded-[50%]" /> |
||||
|
<div> |
||||
|
<div class="text-14px"> |
||||
|
<Highlight :keys="item.keys.map((v) => t(v))"> |
||||
|
{{ item.type }} : {{ item.title }} |
||||
|
</Highlight> |
||||
|
</div> |
||||
|
<div class="mt-15px text-12px text-gray-400"> |
||||
|
{{ formatTime(item.date, 'yyyy-MM-dd') }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<el-divider /> |
||||
|
</div> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
import { formatTime } from '@/utils' |
||||
|
|
||||
|
import { useUserStore } from '@/store/modules/user' |
||||
|
import { useWatermark } from '@/hooks/web/useWatermark' |
||||
|
import avatarImg from '@/assets/imgs/avatar.gif' |
||||
|
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types' |
||||
|
import { pieOptions, barOptions } from './echarts-data' |
||||
|
|
||||
|
defineOptions({ name: 'Home' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const userStore = useUserStore() |
||||
|
const { setWatermark } = useWatermark() |
||||
|
const loading = ref(true) |
||||
|
const avatar = userStore.getUser.avatar ? userStore.getUser.avatar : avatarImg |
||||
|
const username = userStore.getUser.nickname |
||||
|
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption |
||||
|
// 获取统计数 |
||||
|
let totalSate = reactive<WorkplaceTotal>({ |
||||
|
project: 0, |
||||
|
access: 0, |
||||
|
todo: 0 |
||||
|
}) |
||||
|
|
||||
|
const getCount = async () => { |
||||
|
const data = { |
||||
|
project: 40, |
||||
|
access: 2340, |
||||
|
todo: 10 |
||||
|
} |
||||
|
totalSate = Object.assign(totalSate, data) |
||||
|
} |
||||
|
|
||||
|
// 获取项目数 |
||||
|
let projects = reactive<Project[]>([]) |
||||
|
const getProject = async () => { |
||||
|
const data = [ |
||||
|
{ |
||||
|
name: 'Github', |
||||
|
icon: 'akar-icons:github-fill', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Vue', |
||||
|
icon: 'logos:vue', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Angular', |
||||
|
icon: 'logos:angular-icon', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
name: 'React', |
||||
|
icon: 'logos:react', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Webpack', |
||||
|
icon: 'logos:webpack', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Vite', |
||||
|
icon: 'vscode-icons:file-type-vite', |
||||
|
message: 'workplace.introduction', |
||||
|
personal: 'Archer', |
||||
|
time: new Date() |
||||
|
} |
||||
|
] |
||||
|
projects = Object.assign(projects, data) |
||||
|
} |
||||
|
|
||||
|
// 获取通知公告 |
||||
|
let notice = reactive<Notice[]>([]) |
||||
|
const getNotice = async () => { |
||||
|
const data = [ |
||||
|
{ |
||||
|
title: '系统升级版本', |
||||
|
type: '通知', |
||||
|
keys: ['通知', '升级'], |
||||
|
date: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
title: '系统凌晨维护', |
||||
|
type: '公告', |
||||
|
keys: ['公告', '维护'], |
||||
|
date: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
title: '系统升级版本', |
||||
|
type: '通知', |
||||
|
keys: ['通知', '升级'], |
||||
|
date: new Date() |
||||
|
}, |
||||
|
{ |
||||
|
title: '系统凌晨维护', |
||||
|
type: '公告', |
||||
|
keys: ['公告', '维护'], |
||||
|
date: new Date() |
||||
|
} |
||||
|
] |
||||
|
notice = Object.assign(notice, data) |
||||
|
} |
||||
|
|
||||
|
// 获取快捷入口 |
||||
|
let shortcut = reactive<Shortcut[]>([]) |
||||
|
|
||||
|
const getShortcut = async () => { |
||||
|
const data = [ |
||||
|
{ |
||||
|
name: 'Github', |
||||
|
icon: 'akar-icons:github-fill', |
||||
|
url: 'github.io' |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Vue', |
||||
|
icon: 'logos:vue', |
||||
|
url: 'vuejs.org' |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Vite', |
||||
|
icon: 'vscode-icons:file-type-vite', |
||||
|
url: 'https://vitejs.dev/' |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Angular', |
||||
|
icon: 'logos:angular-icon', |
||||
|
url: 'github.io' |
||||
|
}, |
||||
|
{ |
||||
|
name: 'React', |
||||
|
icon: 'logos:react', |
||||
|
url: 'github.io' |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Webpack', |
||||
|
icon: 'logos:webpack', |
||||
|
url: 'github.io' |
||||
|
} |
||||
|
] |
||||
|
shortcut = Object.assign(shortcut, data) |
||||
|
} |
||||
|
|
||||
|
// 用户来源 |
||||
|
const getUserAccessSource = async () => { |
||||
|
const data = [ |
||||
|
{ value: 335, name: 'analysis.directAccess' }, |
||||
|
{ value: 310, name: 'analysis.mailMarketing' }, |
||||
|
{ value: 234, name: 'analysis.allianceAdvertising' }, |
||||
|
{ value: 135, name: 'analysis.videoAdvertising' }, |
||||
|
{ value: 1548, name: 'analysis.searchEngines' } |
||||
|
] |
||||
|
set( |
||||
|
pieOptionsData, |
||||
|
'legend.data', |
||||
|
data.map((v) => t(v.name)) |
||||
|
) |
||||
|
pieOptionsData!.series![0].data = data.map((v) => { |
||||
|
return { |
||||
|
name: t(v.name), |
||||
|
value: v.value |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
||||
|
|
||||
|
// 周活跃量 |
||||
|
const getWeeklyUserActivity = async () => { |
||||
|
const data = [ |
||||
|
{ value: 13253, name: 'analysis.monday' }, |
||||
|
{ value: 34235, name: 'analysis.tuesday' }, |
||||
|
{ value: 26321, name: 'analysis.wednesday' }, |
||||
|
{ value: 12340, name: 'analysis.thursday' }, |
||||
|
{ value: 24643, name: 'analysis.friday' }, |
||||
|
{ value: 1322, name: 'analysis.saturday' }, |
||||
|
{ value: 1324, name: 'analysis.sunday' } |
||||
|
] |
||||
|
set( |
||||
|
barOptionsData, |
||||
|
'xAxis.data', |
||||
|
data.map((v) => t(v.name)) |
||||
|
) |
||||
|
set(barOptionsData, 'series', [ |
||||
|
{ |
||||
|
name: t('analysis.activeQuantity'), |
||||
|
data: data.map((v) => v.value), |
||||
|
type: 'bar' |
||||
|
} |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const getAllApi = async () => { |
||||
|
await Promise.all([ |
||||
|
getCount(), |
||||
|
getProject(), |
||||
|
getNotice(), |
||||
|
getShortcut(), |
||||
|
getUserAccessSource(), |
||||
|
getWeeklyUserActivity() |
||||
|
]) |
||||
|
loading.value = false |
||||
|
} |
||||
|
|
||||
|
getAllApi() |
||||
|
</script> |
@ -0,0 +1,319 @@ |
|||||
|
<template> |
||||
|
<el-row :class="prefixCls" :gutter="20" justify="space-between"> |
||||
|
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" :rows="2" animated> |
||||
|
<template #default> |
||||
|
<div :class="`${prefixCls}__item flex justify-between`"> |
||||
|
<div> |
||||
|
<div |
||||
|
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`" |
||||
|
> |
||||
|
<Icon :size="40" icon="svg-icon:peoples" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="flex flex-col justify-between"> |
||||
|
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`" |
||||
|
>{{ t('analysis.newUser') }} |
||||
|
</div> |
||||
|
<CountTo |
||||
|
:duration="2600" |
||||
|
:end-val="102400" |
||||
|
:start-val="0" |
||||
|
class="text-right text-20px font-700" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" :rows="2" animated> |
||||
|
<template #default> |
||||
|
<div :class="`${prefixCls}__item flex justify-between`"> |
||||
|
<div> |
||||
|
<div |
||||
|
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`" |
||||
|
> |
||||
|
<Icon :size="40" icon="svg-icon:message" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="flex flex-col justify-between"> |
||||
|
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`" |
||||
|
>{{ t('analysis.unreadInformation') }} |
||||
|
</div> |
||||
|
<CountTo |
||||
|
:duration="2600" |
||||
|
:end-val="81212" |
||||
|
:start-val="0" |
||||
|
class="text-right text-20px font-700" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" :rows="2" animated> |
||||
|
<template #default> |
||||
|
<div :class="`${prefixCls}__item flex justify-between`"> |
||||
|
<div> |
||||
|
<div |
||||
|
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`" |
||||
|
> |
||||
|
<Icon :size="40" icon="svg-icon:money" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="flex flex-col justify-between"> |
||||
|
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`" |
||||
|
>{{ t('analysis.transactionAmount') }} |
||||
|
</div> |
||||
|
<CountTo |
||||
|
:duration="2600" |
||||
|
:end-val="9280" |
||||
|
:start-val="0" |
||||
|
class="text-right text-20px font-700" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
|
||||
|
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" :rows="2" animated> |
||||
|
<template #default> |
||||
|
<div :class="`${prefixCls}__item flex justify-between`"> |
||||
|
<div> |
||||
|
<div |
||||
|
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`" |
||||
|
> |
||||
|
<Icon :size="40" icon="svg-icon:shopping" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="flex flex-col justify-between"> |
||||
|
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`" |
||||
|
>{{ t('analysis.totalShopping') }} |
||||
|
</div> |
||||
|
<CountTo |
||||
|
:duration="2600" |
||||
|
:end-val="13600" |
||||
|
:start-val="0" |
||||
|
class="text-right text-20px font-700" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
<el-row :gutter="20" justify="space-between"> |
||||
|
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<Echart :height="300" :options="pieOptionsData" /> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" animated> |
||||
|
<Echart :height="300" :options="barOptionsData" /> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
<el-col :span="24"> |
||||
|
<el-card class="mb-20px" shadow="hover"> |
||||
|
<el-skeleton :loading="loading" :rows="4" animated> |
||||
|
<Echart :height="350" :options="lineOptionsData" /> |
||||
|
</el-skeleton> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
|
||||
|
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
import type { AnalysisTotalTypes } from './types' |
||||
|
import { barOptions, lineOptions, pieOptions } from './echarts-data' |
||||
|
|
||||
|
defineOptions({ name: 'Home2' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const loading = ref(true) |
||||
|
const { getPrefixCls } = useDesign() |
||||
|
const prefixCls = getPrefixCls('panel') |
||||
|
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption |
||||
|
|
||||
|
let totalState = reactive<AnalysisTotalTypes>({ |
||||
|
users: 0, |
||||
|
messages: 0, |
||||
|
moneys: 0, |
||||
|
shoppings: 0 |
||||
|
}) |
||||
|
|
||||
|
const getCount = async () => { |
||||
|
const data = { |
||||
|
users: 102400, |
||||
|
messages: 81212, |
||||
|
moneys: 9280, |
||||
|
shoppings: 13600 |
||||
|
} |
||||
|
totalState = Object.assign(totalState, data) |
||||
|
} |
||||
|
|
||||
|
// 用户来源 |
||||
|
const getUserAccessSource = async () => { |
||||
|
const data = [ |
||||
|
{ value: 335, name: 'analysis.directAccess' }, |
||||
|
{ value: 310, name: 'analysis.mailMarketing' }, |
||||
|
{ value: 234, name: 'analysis.allianceAdvertising' }, |
||||
|
{ value: 135, name: 'analysis.videoAdvertising' }, |
||||
|
{ value: 1548, name: 'analysis.searchEngines' } |
||||
|
] |
||||
|
set( |
||||
|
pieOptionsData, |
||||
|
'legend.data', |
||||
|
data.map((v) => t(v.name)) |
||||
|
) |
||||
|
set(pieOptionsData, 'series.data', data) |
||||
|
} |
||||
|
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
||||
|
|
||||
|
// 周活跃量 |
||||
|
const getWeeklyUserActivity = async () => { |
||||
|
const data = [ |
||||
|
{ value: 13253, name: 'analysis.monday' }, |
||||
|
{ value: 34235, name: 'analysis.tuesday' }, |
||||
|
{ value: 26321, name: 'analysis.wednesday' }, |
||||
|
{ value: 12340, name: 'analysis.thursday' }, |
||||
|
{ value: 24643, name: 'analysis.friday' }, |
||||
|
{ value: 1322, name: 'analysis.saturday' }, |
||||
|
{ value: 1324, name: 'analysis.sunday' } |
||||
|
] |
||||
|
set( |
||||
|
barOptionsData, |
||||
|
'xAxis.data', |
||||
|
data.map((v) => t(v.name)) |
||||
|
) |
||||
|
set(barOptionsData, 'series', [ |
||||
|
{ |
||||
|
name: t('analysis.activeQuantity'), |
||||
|
data: data.map((v) => v.value), |
||||
|
type: 'bar' |
||||
|
} |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption |
||||
|
|
||||
|
// 每月销售总额 |
||||
|
const getMonthlySales = async () => { |
||||
|
const data = [ |
||||
|
{ estimate: 100, actual: 120, name: 'analysis.january' }, |
||||
|
{ estimate: 120, actual: 82, name: 'analysis.february' }, |
||||
|
{ estimate: 161, actual: 91, name: 'analysis.march' }, |
||||
|
{ estimate: 134, actual: 154, name: 'analysis.april' }, |
||||
|
{ estimate: 105, actual: 162, name: 'analysis.may' }, |
||||
|
{ estimate: 160, actual: 140, name: 'analysis.june' }, |
||||
|
{ estimate: 165, actual: 145, name: 'analysis.july' }, |
||||
|
{ estimate: 114, actual: 250, name: 'analysis.august' }, |
||||
|
{ estimate: 163, actual: 134, name: 'analysis.september' }, |
||||
|
{ estimate: 185, actual: 56, name: 'analysis.october' }, |
||||
|
{ estimate: 118, actual: 99, name: 'analysis.november' }, |
||||
|
{ estimate: 123, actual: 123, name: 'analysis.december' } |
||||
|
] |
||||
|
set( |
||||
|
lineOptionsData, |
||||
|
'xAxis.data', |
||||
|
data.map((v) => t(v.name)) |
||||
|
) |
||||
|
set(lineOptionsData, 'series', [ |
||||
|
{ |
||||
|
name: t('analysis.estimate'), |
||||
|
smooth: true, |
||||
|
type: 'line', |
||||
|
data: data.map((v) => v.estimate), |
||||
|
animationDuration: 2800, |
||||
|
animationEasing: 'cubicInOut' |
||||
|
}, |
||||
|
{ |
||||
|
name: t('analysis.actual'), |
||||
|
smooth: true, |
||||
|
type: 'line', |
||||
|
itemStyle: {}, |
||||
|
data: data.map((v) => v.actual), |
||||
|
animationDuration: 2800, |
||||
|
animationEasing: 'quadraticOut' |
||||
|
} |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
const getAllApi = async () => { |
||||
|
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()]) |
||||
|
loading.value = false |
||||
|
} |
||||
|
|
||||
|
getAllApi() |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
$prefix-cls: #{$namespace}-panel; |
||||
|
|
||||
|
.#{$prefix-cls} { |
||||
|
&__item { |
||||
|
&--peoples { |
||||
|
color: #40c9c6; |
||||
|
} |
||||
|
|
||||
|
&--message { |
||||
|
color: #36a3f7; |
||||
|
} |
||||
|
|
||||
|
&--money { |
||||
|
color: #f4516c; |
||||
|
} |
||||
|
|
||||
|
&--shopping { |
||||
|
color: #34bfa3; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
:deep(.#{$namespace}-icon) { |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
|
||||
|
.#{$prefix-cls}__item--icon { |
||||
|
transition: all 0.38s ease-out; |
||||
|
} |
||||
|
|
||||
|
.#{$prefix-cls}__item--peoples { |
||||
|
background: #40c9c6; |
||||
|
} |
||||
|
|
||||
|
.#{$prefix-cls}__item--message { |
||||
|
background: #36a3f7; |
||||
|
} |
||||
|
|
||||
|
.#{$prefix-cls}__item--money { |
||||
|
background: #f4516c; |
||||
|
} |
||||
|
|
||||
|
.#{$prefix-cls}__item--shopping { |
||||
|
background: #34bfa3; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,336 @@ |
|||||
|
<!-- 原料管理员首页 --> |
||||
|
<template> |
||||
|
<div class="row"> |
||||
|
<div class="data"> |
||||
|
<div class="small-data-item small-data-item1"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>今日到货计划(已发货)</div> |
||||
|
<div>{{materialData?.preparetoissueTodayCount}}<span>单</span></div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item2 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>今日备料计划(已发料)</div> |
||||
|
<div>{{materialData?.preparetoissueTodayCount}}<span>单</span></div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon5.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item3 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>今日叫料请求(已发料)</div> |
||||
|
<div>{{materialData?.issueRequestTodayCount}}<span>单</span></div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon6.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item4 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>空闲库位数/总库位数</div> |
||||
|
<div style="font-size: 20px;">{{materialData?.freeLocationCount}}<span>个</span>/{{materialData?.totalLocationCount}}<span>个</span></div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon3.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">呆滞库存预警</div> |
||||
|
<el-table |
||||
|
:data="materialData?.stagnantBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">超期库存预警</div> |
||||
|
<el-table |
||||
|
:data="materialData?.overdueBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">高低储预警</div> |
||||
|
<el-table |
||||
|
:data="materialData?.warningBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">待处理任务</div> |
||||
|
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import * as IndexApi from '@/api/home' |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
import { barOptions } from '../echarts-data' |
||||
|
const lineIndex = ref(0) |
||||
|
|
||||
|
const materialData = ref() |
||||
|
// 获取原料管理员数据 |
||||
|
const getMaterialData = async () => { |
||||
|
await IndexApi.getMaterialData().then((res) => { |
||||
|
materialData.value = res |
||||
|
getJobCharts() |
||||
|
}) |
||||
|
} |
||||
|
const formatter = (type, dict) => { |
||||
|
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
||||
|
return str |
||||
|
} |
||||
|
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
||||
|
const getJobCharts = async () => { |
||||
|
set( |
||||
|
barOptionsData, |
||||
|
'xAxis.data', |
||||
|
Object.keys( materialData.value.jobCount) |
||||
|
) |
||||
|
set(barOptionsData, 'legend.data', ['待处理任务']) |
||||
|
set(barOptionsData, 'series', [ |
||||
|
{ |
||||
|
name:'待处理任务', |
||||
|
data: Object.values( materialData.value.jobCount), |
||||
|
type: 'bar', |
||||
|
barMaxWidth:30 |
||||
|
} |
||||
|
]) |
||||
|
lineIndex.value++ |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getMaterialData() |
||||
|
}) |
||||
|
</script> |
||||
|
<style scoped lang="scss"> |
||||
|
.title { |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 1px solid #dedede; |
||||
|
position: relative; |
||||
|
padding-left: 10px; |
||||
|
&::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 4px; |
||||
|
height: 16px; |
||||
|
background: #3c7adf; |
||||
|
left: 0px; |
||||
|
top: 3px; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
} |
||||
|
.data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.small-data-item { |
||||
|
width:25%; |
||||
|
height: 90px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
border-radius: 6px; |
||||
|
color: white; |
||||
|
padding: 0px 20px; |
||||
|
.small-data-item-txt { |
||||
|
flex: 1; |
||||
|
div { |
||||
|
&:nth-child(1) { |
||||
|
font-size: 14px; |
||||
|
|
||||
|
} |
||||
|
&:nth-child(2) { |
||||
|
font-size: 26px; |
||||
|
margin-top: 4px; |
||||
|
font-weight: bold; |
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
padding-left: 6px; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.img { |
||||
|
width: 40px; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
} |
||||
|
.small-data-item1 { |
||||
|
background: linear-gradient(to left, #fd817d, #fcad80); |
||||
|
} |
||||
|
.small-data-item2 { |
||||
|
background: linear-gradient(to left, #46c6fa, #336bfe); |
||||
|
} |
||||
|
.small-data-item3 { |
||||
|
background: linear-gradient(to left, #96a6cc, #595f82); |
||||
|
} |
||||
|
.small-data-item4 { |
||||
|
background: linear-gradient(to left, #08dcd5, #46e2bb); |
||||
|
} |
||||
|
.small-data-item5 { |
||||
|
background: linear-gradient(to left, #f4c46b, #ffb313); |
||||
|
} |
||||
|
.small-data-item6 { |
||||
|
background: linear-gradient(to left, #6eccf8, #02acfd); |
||||
|
} |
||||
|
} |
||||
|
.two-row { |
||||
|
display: flex; |
||||
|
align-content: center; |
||||
|
justify-content: space-between; |
||||
|
.data1 { |
||||
|
background: white; |
||||
|
padding: 14px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,286 @@ |
|||||
|
<!-- 生产管理员首页 --> |
||||
|
<template> |
||||
|
<div class="row"> |
||||
|
<div class="two-row"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">今日生产计划</div> |
||||
|
<el-table |
||||
|
:data="produceData?.productionTodayList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="number" label="单据号" width="180" /> |
||||
|
<el-table-column prop="displayOrder" label="顺序" width="170" /> |
||||
|
<el-table-column prop="workshop" label="车间" width="170" /> |
||||
|
<el-table-column prop="productionLine" label="生产线" width="170" /> |
||||
|
<el-table-column prop="shift" label="班次" width="170" /> |
||||
|
<el-table-column prop="team" label="班组" width="170" /> |
||||
|
<el-table-column prop="planDate" label="计划日期" width="170" |
||||
|
><template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="beginTime" label="开始日期" width="170" |
||||
|
><template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.beginTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="endTime" label="结束日期" width="170" |
||||
|
><template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.endTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="businessType" label="业务类型" /> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">线边安全库存</div> |
||||
|
<el-table :data="produceData?.safeLocationList" style="width: 100%" stripe height="240px"> |
||||
|
<el-table-column prop="code" label="代码" width="180" /> |
||||
|
<el-table-column prop="name" label="名称" width="180" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180" /> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180" /> |
||||
|
<el-table-column prop="type" label="类型" width="180" /> |
||||
|
<el-table-column prop="aisle" label="巷道" width="180" /> |
||||
|
<el-table-column prop="shelf" label="货架" width="180" /> |
||||
|
<el-table-column prop="locationRow" label="行" width="180" /> |
||||
|
<el-table-column prop="locationColum" label="列" width="180" /> |
||||
|
<el-table-column prop="pickPriority" label="拣料优先级" width="180" /> |
||||
|
<el-table-column prop="maxWeight" label="最大承重" width="180" /> |
||||
|
<el-table-column prop="maxArea" label="最大面积" width="180" /> |
||||
|
<el-table-column prop="maxVolume" label="最大体积" width="180" /> |
||||
|
<el-table-column prop="userGroupCode" label="用户组代码" width="180" /> |
||||
|
<el-table-column prop="activeTime" label="生效时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.createTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireTime" label="失效时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.createTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">待上架成品库存</div> |
||||
|
<el-table |
||||
|
:data="produceData?.productputawayJobDetailList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="number" label="单据号" width="180" /> |
||||
|
<el-table-column prop="fromWarehouseCode" label="从仓库代码" width="180" /> |
||||
|
<el-table-column prop="requestTime" label="申请时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.requestTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="requestDueTime" label="要求截止时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.requestDueTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="status" label="状态" width="140"/> |
||||
|
<el-table-column prop="departmentCode" label="部门" /> |
||||
|
<el-table-column prop="acceptUserId" label="承接人用户名" width="170" /> |
||||
|
<el-table-column prop="acceptTime" label="承接时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.acceptTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="fromLocationTypes" label="从库位类型范围" width="170"/> |
||||
|
<el-table-column prop="toLocationTypes" label="到库位类型范围" width="170" /> |
||||
|
<el-table-column prop="toWarehouseCode" label="到仓库代码" width="170"/> |
||||
|
<el-table-column prop="fromAreaCodes" label="从库区代码范围" width="170"/> |
||||
|
<el-table-column prop="toAreaCodes" label="到库区代码范围" width="170"/> |
||||
|
<el-table-column prop="autoComplete" label="自动完成" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.autoComplete, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowModifyLocation" label="允许修改库位" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowModifyLocation, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowModifyQty" label="允许修改数量" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowModifyQty, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowBiggerQty" label="允许大于推荐数量" width="140"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowBiggerQty, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowSmallerQty" label="允许小于推荐数量" width="140"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowSmallerQty, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowModifyInventoryStatus" label="允许修改库存状态" width="140"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ |
||||
|
formatter(scope.row.allowModifyInventoryStatus, DICT_TYPE.TRUE_FALSE) |
||||
|
}}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowContinuousScanning" label="允许连续扫描" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowContinuousScanning, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowPartialComplete" label="允许部分完成" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowPartialComplete, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowModifyBatch" label="允许修改批次" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowModifyBatch, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="allowModifyPackingNumber" label="允许修改箱码" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatter(scope.row.allowModifyPackingNumber, DICT_TYPE.TRUE_FALSE) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">待处理任务</div> |
||||
|
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import * as IndexApi from '@/api/home' |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
import { barOptions } from '../echarts-data' |
||||
|
const lineIndex = ref(0) |
||||
|
const produceData = ref() |
||||
|
// 获取生产管理员首页数据 |
||||
|
const getProduceData = async () => { |
||||
|
IndexApi.getProduceData().then((res) => { |
||||
|
produceData.value = res |
||||
|
getJobCharts() |
||||
|
}) |
||||
|
} |
||||
|
const formatter = (type, dict) => { |
||||
|
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
||||
|
return str |
||||
|
} |
||||
|
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
||||
|
const getJobCharts = async () => { |
||||
|
set( |
||||
|
barOptionsData, |
||||
|
'xAxis.data', |
||||
|
Object.keys( produceData.value.jobCount) |
||||
|
) |
||||
|
set(barOptionsData, 'legend.data', ['待处理任务']) |
||||
|
set(barOptionsData, 'series', [ |
||||
|
{ |
||||
|
name:'待处理任务', |
||||
|
data: Object.values( produceData.value.jobCount), |
||||
|
type: 'bar', |
||||
|
barMaxWidth:30 |
||||
|
} |
||||
|
]) |
||||
|
lineIndex.value++ |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getProduceData() |
||||
|
}) |
||||
|
</script> |
||||
|
<style scoped lang="scss"> |
||||
|
.title { |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 1px solid #dedede; |
||||
|
position: relative; |
||||
|
padding-left: 10px; |
||||
|
&::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 4px; |
||||
|
height: 16px; |
||||
|
background: #3c7adf; |
||||
|
left: 0px; |
||||
|
top: 3px; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
} |
||||
|
.data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.small-data-item { |
||||
|
width: 25%; |
||||
|
height: 90px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
border-radius: 6px; |
||||
|
color: white; |
||||
|
padding: 0px 20px; |
||||
|
.small-data-item-txt { |
||||
|
flex: 1; |
||||
|
div { |
||||
|
&:nth-child(1) { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
&:nth-child(2) { |
||||
|
font-size: 26px; |
||||
|
margin-top: 4px; |
||||
|
font-weight: bold; |
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
padding-left: 6px; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.img { |
||||
|
width: 40px; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
} |
||||
|
.small-data-item1 { |
||||
|
background: linear-gradient(to left, #fd817d, #fcad80); |
||||
|
} |
||||
|
.small-data-item2 { |
||||
|
background: linear-gradient(to left, #46c6fa, #336bfe); |
||||
|
} |
||||
|
.small-data-item3 { |
||||
|
background: linear-gradient(to left, #96a6cc, #595f82); |
||||
|
} |
||||
|
.small-data-item4 { |
||||
|
background: linear-gradient(to left, #08dcd5, #46e2bb); |
||||
|
} |
||||
|
.small-data-item5 { |
||||
|
background: linear-gradient(to left, #f4c46b, #ffb313); |
||||
|
} |
||||
|
.small-data-item6 { |
||||
|
background: linear-gradient(to left, #6eccf8, #02acfd); |
||||
|
} |
||||
|
} |
||||
|
.two-row { |
||||
|
display: flex; |
||||
|
align-content: center; |
||||
|
justify-content: space-between; |
||||
|
.data1 { |
||||
|
background: white; |
||||
|
padding: 14px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,313 @@ |
|||||
|
<!-- 成品管理员首页 --> |
||||
|
<template> |
||||
|
<div class="row"> |
||||
|
<div class="data"> |
||||
|
<div class="small-data-item small-data-item1"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>今日到货计划(已发货)</div> |
||||
|
<div>{{ productData?.deliverPlanTodayCount || 0 }}<span>单</span></div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">呆滞库存预警</div> |
||||
|
<el-table |
||||
|
:data="productData?.stagnantBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">超期库存预警</div> |
||||
|
<el-table |
||||
|
:data="productData?.overdueBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">高低储预警</div> |
||||
|
<el-table |
||||
|
:data="productData?.warningBalanceList" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="itemCode" label="物料代码" width="180px" /> |
||||
|
<el-table-column prop="batch" label="批次" width="180px" /> |
||||
|
<el-table-column prop="packingNumber" label="包装号" width="180px" /> |
||||
|
<el-table-column prop="containerNumber" label="器具代码" width="180px" /> |
||||
|
<el-table-column prop="qty" label="数量" width="180px" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom, DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationCode" label="库位代码" width="180px" /> |
||||
|
<el-table-column prop="warehouseCode" label="仓库代码" width="180px" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus, DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="locationGroupCode" label="库位组代码" width="180px" /> |
||||
|
<el-table-column prop="areaCode" label="库区代码" width="180px" /> |
||||
|
<el-table-column prop="erpLocationCode" label="ERP库位代码" width="180px" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="180px" /> |
||||
|
<el-table-column prop="arriveDate" label="到货日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="produceDate" label="生产日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="expireDate" label="失效日期" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="ownerCode" label="货主代码" width="180px" /> |
||||
|
<el-table-column prop="lockedQty" label="锁定数量" width="180px" /> |
||||
|
<el-table-column prop="usableQty" label="可用数量" width="180px" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="180px" /> |
||||
|
<el-table-column prop="amount" label="金额" width="180px" /> |
||||
|
<el-table-column prop="putInTime" label="入库时间" width="180px"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.planDate) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">待处理任务</div> |
||||
|
<Echart :options="barOptions" :height="280" :key="lineIndex" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import * as IndexApi from '@/api/home' |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
import { barOptions } from '../echarts-data' |
||||
|
const lineIndex = ref(0) |
||||
|
// 获取成品管理员首页首页数据 |
||||
|
const productData = ref() |
||||
|
const getProductData = async () => { |
||||
|
IndexApi.getProductData().then((res) => { |
||||
|
productData.value = res |
||||
|
getJobCharts() |
||||
|
}) |
||||
|
} |
||||
|
const formatter = (type, dict) => { |
||||
|
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
||||
|
return str |
||||
|
} |
||||
|
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption |
||||
|
const getJobCharts = async () => { |
||||
|
set( |
||||
|
barOptionsData, |
||||
|
'xAxis.data', |
||||
|
Object.keys( productData.value.jobCount) |
||||
|
) |
||||
|
set(barOptionsData, 'legend.data', ['待处理任务']) |
||||
|
set(barOptionsData, 'series', [ |
||||
|
{ |
||||
|
name:'待处理任务', |
||||
|
data: Object.values( productData.value.jobCount), |
||||
|
type: 'bar', |
||||
|
barMaxWidth:30 |
||||
|
} |
||||
|
]) |
||||
|
lineIndex.value++ |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getProductData() |
||||
|
}) |
||||
|
</script> |
||||
|
<style scoped lang="scss"> |
||||
|
.title { |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 1px solid #dedede; |
||||
|
position: relative; |
||||
|
padding-left: 10px; |
||||
|
&::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 4px; |
||||
|
height: 16px; |
||||
|
background: #3c7adf; |
||||
|
left: 0px; |
||||
|
top: 3px; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
} |
||||
|
.data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.small-data-item { |
||||
|
width: 25%; |
||||
|
height: 90px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
border-radius: 6px; |
||||
|
color: white; |
||||
|
padding: 0px 20px; |
||||
|
.small-data-item-txt { |
||||
|
flex: 1; |
||||
|
div { |
||||
|
&:nth-child(1) { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
&:nth-child(2) { |
||||
|
font-size: 26px; |
||||
|
margin-top: 4px; |
||||
|
font-weight: bold; |
||||
|
span { |
||||
|
font-size: 14px; |
||||
|
padding-left: 6px; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.img { |
||||
|
width: 40px; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
} |
||||
|
.small-data-item1 { |
||||
|
background: linear-gradient(to left, #fd817d, #fcad80); |
||||
|
} |
||||
|
.small-data-item2 { |
||||
|
background: linear-gradient(to left, #46c6fa, #336bfe); |
||||
|
} |
||||
|
.small-data-item3 { |
||||
|
background: linear-gradient(to left, #96a6cc, #595f82); |
||||
|
} |
||||
|
.small-data-item4 { |
||||
|
background: linear-gradient(to left, #08dcd5, #46e2bb); |
||||
|
} |
||||
|
.small-data-item5 { |
||||
|
background: linear-gradient(to left, #f4c46b, #ffb313); |
||||
|
} |
||||
|
.small-data-item6 { |
||||
|
background: linear-gradient(to left, #6eccf8, #02acfd); |
||||
|
} |
||||
|
} |
||||
|
.two-row { |
||||
|
display: flex; |
||||
|
align-content: center; |
||||
|
justify-content: space-between; |
||||
|
.data1 { |
||||
|
background: white; |
||||
|
padding: 14px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,452 @@ |
|||||
|
<!-- 供应商首页 --> |
||||
|
<template> |
||||
|
<div class="one-row"> |
||||
|
<div class="data"> |
||||
|
<div class="data-item"> |
||||
|
<div class="small-title">订单数</div> |
||||
|
<div class="small-data"> |
||||
|
<div class="small-data-item small-data-item1"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.openPurchaseCount || 0 }}<span>单</span></div> |
||||
|
<div>开放订单数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon1.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item2 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.allPurchaseCount || 0 }}<span>单</span></div> |
||||
|
<div>全部订单数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon5.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="data-item ml-14px"> |
||||
|
<div class="small-title">要货计划数</div> |
||||
|
<div class="small-data"> |
||||
|
<div class="small-data-item small-data-item3"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.openPurchasePlanCount || 0 }}<span>单</span></div> |
||||
|
<div>开放计划数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon6.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item4 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.allPurchasePlanCount || 0 }}<span>单</span></div> |
||||
|
<div>全部计划数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon3.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="data-item ml-14px"> |
||||
|
<div class="small-title">发货单数</div> |
||||
|
<div class="small-data"> |
||||
|
<div class="small-data-item small-data-item5"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.notTakeSupplierdeliverCount || 0 }}<span>单</span></div> |
||||
|
<div>未收货订单数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon4.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
<div class="small-data-item small-data-item6 ml-14px"> |
||||
|
<div class="small-data-item-txt"> |
||||
|
<div>{{ supplierData?.takeSupplierdeliverCount || 0 }}<span>单</span></div> |
||||
|
<div>已收货订单数</div> |
||||
|
</div> |
||||
|
<img src="../../../assets/imgs/icon2.png" alt="" class="img" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[65%]"> |
||||
|
<div class="title">本月发货单趋势</div> |
||||
|
<Echart :options="lineOptions" :height="280" :key="lineIndex" /> |
||||
|
</div> |
||||
|
<div class="data1 w-[35%] ml-14px"> |
||||
|
<div class="title">本月发货零件TOP10</div> |
||||
|
<Echart :options="pieOptions" :height="280" :key="lineIndex" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">最新消息</div> |
||||
|
<el-table :data="notaicList" style="width: 100%" stripe height="240px"> |
||||
|
<el-table-column label="编号" align="center" prop="id" /> |
||||
|
<el-table-column label="用户类型" align="center" prop="userType"> |
||||
|
<template #default="scope"> |
||||
|
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" /> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="用户编号" align="center" prop="userId" width="80" /> |
||||
|
<el-table-column label="模板编码" align="center" prop="templateCode" width="80" /> |
||||
|
<el-table-column label="发送人名称" align="center" prop="templateNickname" width="180" /> |
||||
|
<el-table-column |
||||
|
label="模版内容" |
||||
|
align="center" |
||||
|
prop="templateContent" |
||||
|
width="200" |
||||
|
show-overflow-tooltip |
||||
|
/> |
||||
|
<el-table-column |
||||
|
label="模版参数" |
||||
|
align="center" |
||||
|
prop="templateParams" |
||||
|
width="180" |
||||
|
show-overflow-tooltip |
||||
|
> |
||||
|
<template #default="scope"> {{ scope.row.templateParams }}</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="模版类型" align="center" prop="templateType" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<dict-tag |
||||
|
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" |
||||
|
:value="scope.row.templateType" |
||||
|
/> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="是否已读" align="center" prop="readStatus" width="100"> |
||||
|
<template #default="scope"> |
||||
|
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" /> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column |
||||
|
label="阅读时间" |
||||
|
align="center" |
||||
|
prop="readTime" |
||||
|
width="180" |
||||
|
:formatter="dateFormatter" |
||||
|
/> |
||||
|
<el-table-column |
||||
|
label="创建时间" |
||||
|
align="center" |
||||
|
prop="createTime" |
||||
|
width="180" |
||||
|
:formatter="dateFormatter" |
||||
|
/> |
||||
|
<!-- <el-table-column label="操作" align="center" fixed="right" width="120"> |
||||
|
<template #default="scope"> |
||||
|
<el-button link type="primary" @click="openDetail(scope.row)" v-hasPermi="['system:notify-message:query']"> |
||||
|
<Icon icon="ep:document-copy" /> |
||||
|
详情 |
||||
|
</el-button> |
||||
|
</template> |
||||
|
</el-table-column> --> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">最新扣分明细</div> |
||||
|
<el-table :data="tableData" style="width: 100%" stripe height="240px"> |
||||
|
<el-table-column prop="title" label="标题" /> |
||||
|
<el-table-column prop="name" label="发布人" /> |
||||
|
<el-table-column prop="date" label="发布日期" /> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="two-row mt-14px"> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">本月退货明细</div> |
||||
|
<el-table |
||||
|
:data="supplierData?.purchasereturnRecordMonth" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="number" label="单据号" width="180" /> |
||||
|
<el-table-column prop="fromPackingNumber" label="从包装号" width="180" /> |
||||
|
<el-table-column prop="toPackingNumber" label="到包装号" width="180" /> |
||||
|
<el-table-column prop="fromContainerNumber" label="从器具号" width="120" /> |
||||
|
<el-table-column prop="toContainerNumber" label="到器具号" width="120" /> |
||||
|
<el-table-column prop="fromBatch" label="从批次" width="120" /> |
||||
|
<el-table-column prop="toBatch" label="到批次" width="120" /> |
||||
|
<el-table-column prop="altBatch" label="替代批次" width="120" /> |
||||
|
<el-table-column prop="fromLocationCode" label="从库位代码" width="120" /> |
||||
|
<el-table-column prop="toLocationCode" label="到库位代码" width="120" /> |
||||
|
<el-table-column prop="fromLocationGroupCode" label="从库位组代码" width="120" /> |
||||
|
<el-table-column prop="toLocationGroupCode" label="到库位组代码" width="120" /> |
||||
|
<el-table-column prop="fromAreaCode" label="从库区代码" width="120" /> |
||||
|
<el-table-column prop="toAreaCode" label="到库区代码" width="120" /> |
||||
|
<el-table-column prop="fromOwnerCode" label="从货主代码" width="120" /> |
||||
|
<el-table-column prop="toOwnerCode" label="到货主代码" width="120" /> |
||||
|
<el-table-column prop="inventoryStatus" label="库存状态" width="120"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.inventoryStatus,DICT_TYPE.INVENTORY_STATUS) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="poNumber" label="订单号" width="120" /> |
||||
|
<el-table-column prop="poline" label="订单行" width="120" /> |
||||
|
<el-table-column prop="reason" label="原因" width="120" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="120" /> |
||||
|
<el-table-column prop="amount" label="金额" width="120" /> |
||||
|
<el-table-column prop="itemCode" label="物品代码" width="160" /> |
||||
|
<el-table-column prop="itemName" label="物品名称" width="120" /> |
||||
|
<el-table-column prop="itemDesc1" label="物品描述1" width="120" /> |
||||
|
<el-table-column prop="itemDesc2" label="物品描述2" width="120" /> |
||||
|
<el-table-column prop="qty" label="数量" width="120" /> |
||||
|
<el-table-column prop="uom" label="计量单位" width="120"> |
||||
|
<template #default="scope"> |
||||
|
{{ formatter(scope.row.uom,DICT_TYPE.UOM) }} |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="projectCode" label="项目代码" width="120" /> |
||||
|
<el-table-column prop="remark" label="备注" width="120" /> |
||||
|
<el-table-column prop="createTime" label="创建时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.createTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="creator" label="创建者" width="120" /> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
<div class="data1 w-[47.3%]"> |
||||
|
<div class="title">本月索赔明细</div> |
||||
|
<el-table |
||||
|
:data="supplierData?.purchasereturnRecordMonth" |
||||
|
style="width: 100%" |
||||
|
stripe |
||||
|
height="240px" |
||||
|
> |
||||
|
<el-table-column prop="number" label="单据号" width="180" /> |
||||
|
<el-table-column prop="fromBatch" label="批次" width="120" /> |
||||
|
<el-table-column prop="poNumber" label="订单号" width="120" /> |
||||
|
<el-table-column prop="poline" label="订单行" width="120" /> |
||||
|
<el-table-column prop="reason" label="原因" width="120" /> |
||||
|
<el-table-column prop="singlePrice" label="单价" width="120" /> |
||||
|
<el-table-column prop="amount" label="金额" width="120" /> |
||||
|
<el-table-column prop="code" label="代码" width="160" /> |
||||
|
<el-table-column prop="createTime" label="创建时间" width="170"> |
||||
|
<template #default="scope"> |
||||
|
<span>{{ formatDate(scope.row.createTime) }}</span> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column prop="creator" label="创建者" width="120" /> |
||||
|
</el-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { set } from 'lodash-es' |
||||
|
import { EChartsOption } from 'echarts' |
||||
|
import { lineOptions, pieOptions } from '../echarts-data' |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import * as NotifyMessageApi from '@/api/system/notify/message' |
||||
|
import * as IndexApi from '@/api/home' |
||||
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict' |
||||
|
const { t } = useI18n() |
||||
|
const tableData = [ |
||||
|
{ |
||||
|
date: '2016-05-03', |
||||
|
name: 'Tom', |
||||
|
title: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
}, |
||||
|
{ |
||||
|
date: '2016-05-02', |
||||
|
title: 'Tom', |
||||
|
name: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
}, |
||||
|
{ |
||||
|
date: '2016-05-04', |
||||
|
title: 'Tom', |
||||
|
name: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
}, |
||||
|
{ |
||||
|
date: '2016-05-01', |
||||
|
title: 'Tom', |
||||
|
name: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
}, |
||||
|
{ |
||||
|
date: '2016-05-01', |
||||
|
title: 'Tom', |
||||
|
name: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
}, |
||||
|
{ |
||||
|
date: '2016-05-01', |
||||
|
title: 'Tom', |
||||
|
name: 'Tom', |
||||
|
text: 'No. 189, Grove St, Los Angeles' |
||||
|
} |
||||
|
] |
||||
|
const supplierData = ref() |
||||
|
const lineIndex = ref(0) |
||||
|
// 本月发货单趋势 |
||||
|
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption |
||||
|
const getInvoiceCharts = async () => { |
||||
|
set( |
||||
|
lineOptionsData, |
||||
|
'xAxis.data', |
||||
|
supplierData.value.supplierdeliverMonthCount.map((v) => v.date) |
||||
|
) |
||||
|
set(lineOptionsData, 'legend.data', ['本月发货单趋势']) |
||||
|
set(lineOptionsData, 'series', [ |
||||
|
{ |
||||
|
name: '本月发货单趋势', |
||||
|
smooth: true, |
||||
|
type: 'line', |
||||
|
itemStyle: {}, |
||||
|
animationDuration: 2800, |
||||
|
animationEasing: 'quadraticOut', |
||||
|
data: supplierData.value.supplierdeliverMonthCount.map((v) => v.count) |
||||
|
} |
||||
|
]) |
||||
|
lineIndex.value++ |
||||
|
} |
||||
|
//本月发货零件TOP10 |
||||
|
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption |
||||
|
const getPartTOPCharts = async () => { |
||||
|
const data = [ |
||||
|
{ value: 335, name: 'analysis.directAccess' }, |
||||
|
{ value: 310, name: 'analysis.mailMarketing' }, |
||||
|
{ value: 234, name: 'analysis.allianceAdvertising' }, |
||||
|
{ value: 135, name: 'analysis.videoAdvertising' }, |
||||
|
{ value: 1548, name: 'analysis.searchEngines' } |
||||
|
] |
||||
|
console.log(supplierData.value.supplierdeliverItemMonthTop) |
||||
|
set( |
||||
|
pieOptionsData, |
||||
|
'legend.data', |
||||
|
supplierData.value.supplierdeliverItemMonthTop.map((v) => v.itemCode) |
||||
|
) |
||||
|
pieOptionsData!.series![0].data = supplierData.value.supplierdeliverItemMonthTop.map((v) => { |
||||
|
return { |
||||
|
name: v.itemCode, |
||||
|
value: v.qty |
||||
|
} |
||||
|
}) |
||||
|
lineIndex.value++ |
||||
|
} |
||||
|
// 获取供应商数据 |
||||
|
const getSupplierData = async () => { |
||||
|
IndexApi.getSupplierData().then((res) => { |
||||
|
supplierData.value = res |
||||
|
getInvoiceCharts() |
||||
|
getPartTOPCharts() |
||||
|
}) |
||||
|
} |
||||
|
// 最新消息 |
||||
|
const notaicList = ref([]) // 列表的数据 |
||||
|
const queryParams = reactive({ |
||||
|
pageNo: 1, |
||||
|
pageSize: 10, |
||||
|
userType: undefined, |
||||
|
userId: undefined, |
||||
|
templateCode: undefined, |
||||
|
templateType: undefined, |
||||
|
createTime: [] |
||||
|
}) |
||||
|
/** 查询列表 */ |
||||
|
const getList = async () => { |
||||
|
try { |
||||
|
const data = await NotifyMessageApi.getNotifyMessagePage(queryParams) |
||||
|
notaicList.value = data.list |
||||
|
} finally { |
||||
|
} |
||||
|
} |
||||
|
const formatter = (type,dict) => { |
||||
|
let str = getStrDictOptions(dict).filter((item) => type == item.value)[0]?.label |
||||
|
return str |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getSupplierData() |
||||
|
getList() |
||||
|
}) |
||||
|
</script> |
||||
|
<style scoped lang="scss"> |
||||
|
.title { |
||||
|
padding-bottom: 10px; |
||||
|
border-bottom: 1px solid #dedede; |
||||
|
position: relative; |
||||
|
padding-left: 10px; |
||||
|
&::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 4px; |
||||
|
height: 16px; |
||||
|
background: #3c7adf; |
||||
|
left: 0px; |
||||
|
top: 3px; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
} |
||||
|
.data { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
.data-item { |
||||
|
width: calc(100% / 3); |
||||
|
padding: 10px 0px; |
||||
|
background: white; |
||||
|
.small-title { |
||||
|
padding: 0px 14px 10px; |
||||
|
} |
||||
|
.small-data { |
||||
|
display: flex; |
||||
|
align-content: center; |
||||
|
justify-content: space-between; |
||||
|
padding: 0px 14px; |
||||
|
.small-data-item { |
||||
|
width: 50%; |
||||
|
height: 70px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
border-radius: 6px; |
||||
|
color: white; |
||||
|
padding: 0px 20px; |
||||
|
.small-data-item-txt { |
||||
|
flex: 1; |
||||
|
div { |
||||
|
&:nth-child(1) { |
||||
|
font-size: 20px; |
||||
|
span { |
||||
|
font-size: 12px; |
||||
|
padding-left: 6px; |
||||
|
} |
||||
|
} |
||||
|
&:nth-child(2) { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.img { |
||||
|
width: 30px; |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
} |
||||
|
.small-data-item1 { |
||||
|
background: linear-gradient(to left, #fd817d, #fcad80); |
||||
|
} |
||||
|
.small-data-item2 { |
||||
|
background: linear-gradient(to left, #46c6fa, #336bfe); |
||||
|
} |
||||
|
.small-data-item3 { |
||||
|
background: linear-gradient(to left, #96a6cc, #595f82); |
||||
|
} |
||||
|
.small-data-item4 { |
||||
|
background: linear-gradient(to left, #08dcd5, #46e2bb); |
||||
|
} |
||||
|
.small-data-item5 { |
||||
|
background: linear-gradient(to left, #f4c46b, #ffb313); |
||||
|
} |
||||
|
.small-data-item6 { |
||||
|
background: linear-gradient(to left, #6eccf8, #02acfd); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.two-row { |
||||
|
display: flex; |
||||
|
align-content: center; |
||||
|
justify-content: space-between; |
||||
|
.data1 { |
||||
|
background: white; |
||||
|
padding: 14px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,286 @@ |
|||||
|
import { EChartsOption } from 'echarts' |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
|
||||
|
export const lineOptions: EChartsOption = { |
||||
|
|
||||
|
xAxis: { |
||||
|
data: [ 1, 2, 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30 |
||||
|
], |
||||
|
boundaryGap: false, |
||||
|
axisTick: { |
||||
|
show: false |
||||
|
} |
||||
|
}, |
||||
|
grid: { |
||||
|
left: 20, |
||||
|
right: 20, |
||||
|
bottom: 20, |
||||
|
top: 50, |
||||
|
containLabel: true |
||||
|
}, |
||||
|
tooltip: { |
||||
|
trigger: 'axis', |
||||
|
axisPointer: { |
||||
|
type: 'cross' |
||||
|
}, |
||||
|
padding: [5, 10] |
||||
|
}, |
||||
|
yAxis: { |
||||
|
axisTick: { |
||||
|
show: false |
||||
|
} |
||||
|
}, |
||||
|
legend: { |
||||
|
data: ['销售','哈哈'], |
||||
|
top: 20 |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: '销售', |
||||
|
smooth: true, |
||||
|
type: 'line', |
||||
|
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123], |
||||
|
animationDuration: 2800, |
||||
|
animationEasing: 'cubicInOut' |
||||
|
}, |
||||
|
{ |
||||
|
name: '哈哈', |
||||
|
smooth: true, |
||||
|
type: 'line', |
||||
|
itemStyle: {}, |
||||
|
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123], |
||||
|
animationDuration: 2800, |
||||
|
animationEasing: 'quadraticOut' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
export const pieOptions: EChartsOption = { |
||||
|
// title: {
|
||||
|
// text: t('analysis.userAccessSource'),
|
||||
|
// left: 'center'
|
||||
|
// },
|
||||
|
tooltip: { |
||||
|
trigger: 'item', |
||||
|
formatter: '{a} <br/>{b} : {c} ({d}%)' |
||||
|
}, |
||||
|
legend: { |
||||
|
orient: 'vertical', |
||||
|
left: 'left', |
||||
|
top:20, |
||||
|
data: [ |
||||
|
t('analysis.directAccess'), |
||||
|
t('analysis.mailMarketing'), |
||||
|
t('analysis.allianceAdvertising'), |
||||
|
t('analysis.videoAdvertising'), |
||||
|
t('analysis.searchEngines') |
||||
|
] |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: t('analysis.userAccessSource'), |
||||
|
type: 'pie', |
||||
|
radius: '55%', |
||||
|
center: ['50%', '60%'], |
||||
|
data: [ |
||||
|
{ value: 335, name: t('analysis.directAccess') }, |
||||
|
{ value: 310, name: t('analysis.mailMarketing') }, |
||||
|
{ value: 234, name: t('analysis.allianceAdvertising') }, |
||||
|
{ value: 135, name: t('analysis.videoAdvertising') }, |
||||
|
{ value: 1548, name: t('analysis.searchEngines') } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
export const barOptions: EChartsOption = { |
||||
|
title: { |
||||
|
text: '', |
||||
|
left: 'center' |
||||
|
}, |
||||
|
tooltip: { |
||||
|
trigger: 'axis', |
||||
|
axisPointer: { |
||||
|
type: 'shadow' |
||||
|
} |
||||
|
}, |
||||
|
grid: { |
||||
|
left: 50, |
||||
|
right: 20, |
||||
|
bottom: 20 |
||||
|
}, |
||||
|
xAxis: { |
||||
|
type: 'category', |
||||
|
data: [], |
||||
|
axisTick: { |
||||
|
alignWithLabel: true |
||||
|
} |
||||
|
}, |
||||
|
yAxis: { |
||||
|
type: 'value' |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: t('analysis.activeQuantity'), |
||||
|
data: [], |
||||
|
type: 'bar' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
export const radarOption: EChartsOption = { |
||||
|
legend: { |
||||
|
data: [t('workplace.personal'), t('workplace.team')] |
||||
|
}, |
||||
|
radar: { |
||||
|
// shape: 'circle',
|
||||
|
indicator: [ |
||||
|
{ name: t('workplace.quote'), max: 65 }, |
||||
|
{ name: t('workplace.contribution'), max: 160 }, |
||||
|
{ name: t('workplace.hot'), max: 300 }, |
||||
|
{ name: t('workplace.yield'), max: 130 }, |
||||
|
{ name: t('workplace.follow'), max: 100 } |
||||
|
] |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: `xxx${t('workplace.index')}`, |
||||
|
type: 'radar', |
||||
|
data: [ |
||||
|
{ |
||||
|
value: [42, 30, 20, 35, 80], |
||||
|
name: t('workplace.personal') |
||||
|
}, |
||||
|
{ |
||||
|
value: [50, 140, 290, 100, 90], |
||||
|
name: t('workplace.team') |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
export const wordOptions = { |
||||
|
series: [ |
||||
|
{ |
||||
|
type: 'wordCloud', |
||||
|
gridSize: 2, |
||||
|
sizeRange: [12, 50], |
||||
|
rotationRange: [-90, 90], |
||||
|
shape: 'pentagon', |
||||
|
width: 600, |
||||
|
height: 400, |
||||
|
drawOutOfBound: true, |
||||
|
textStyle: { |
||||
|
color: function () { |
||||
|
return ( |
||||
|
'rgb(' + |
||||
|
[ |
||||
|
Math.round(Math.random() * 160), |
||||
|
Math.round(Math.random() * 160), |
||||
|
Math.round(Math.random() * 160) |
||||
|
].join(',') + |
||||
|
')' |
||||
|
) |
||||
|
} |
||||
|
}, |
||||
|
emphasis: { |
||||
|
textStyle: { |
||||
|
shadowBlur: 10, |
||||
|
shadowColor: '#333' |
||||
|
} |
||||
|
}, |
||||
|
data: [ |
||||
|
{ |
||||
|
name: 'Sam S Club', |
||||
|
value: 10000, |
||||
|
textStyle: { |
||||
|
color: 'black' |
||||
|
}, |
||||
|
emphasis: { |
||||
|
textStyle: { |
||||
|
color: 'red' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Macys', |
||||
|
value: 6181 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Amy Schumer', |
||||
|
value: 4386 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Jurassic World', |
||||
|
value: 4055 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Charter Communications', |
||||
|
value: 2467 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Chick Fil A', |
||||
|
value: 2244 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Planet Fitness', |
||||
|
value: 1898 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Pitch Perfect', |
||||
|
value: 1484 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Express', |
||||
|
value: 1112 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Home', |
||||
|
value: 965 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Johnny Depp', |
||||
|
value: 847 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Lena Dunham', |
||||
|
value: 582 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Lewis Hamilton', |
||||
|
value: 555 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'KXAN', |
||||
|
value: 550 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Mary Ellen Mark', |
||||
|
value: 462 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Farrah Abraham', |
||||
|
value: 366 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Rita Ora', |
||||
|
value: 360 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Serena Williams', |
||||
|
value: 282 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'NCAA baseball tournament', |
||||
|
value: 273 |
||||
|
}, |
||||
|
{ |
||||
|
name: 'Point Break', |
||||
|
value: 265 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<supplierIndex v-hasRole="['super_admin','supplier']"/> |
||||
|
<material v-hasRole="['super_admin']"/> |
||||
|
<product v-hasRole="['super_admin']"/> |
||||
|
<produce v-hasRole="['super_admin']"/> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import supplierIndex from './components/supplierIndex.vue' |
||||
|
import material from './components/material.vue' |
||||
|
import product from './components/product.vue' |
||||
|
import produce from './components/produce.vue' |
||||
|
</script> |
@ -0,0 +1,55 @@ |
|||||
|
export type WorkplaceTotal = { |
||||
|
project: number |
||||
|
access: number |
||||
|
todo: number |
||||
|
} |
||||
|
|
||||
|
export type Project = { |
||||
|
name: string |
||||
|
icon: string |
||||
|
message: string |
||||
|
personal: string |
||||
|
time: Date | number | string |
||||
|
} |
||||
|
|
||||
|
export type Notice = { |
||||
|
title: string |
||||
|
type: string |
||||
|
keys: string[] |
||||
|
date: Date | number | string |
||||
|
} |
||||
|
|
||||
|
export type Shortcut = { |
||||
|
name: string |
||||
|
icon: string |
||||
|
url: string |
||||
|
} |
||||
|
|
||||
|
export type RadarData = { |
||||
|
personal: number |
||||
|
team: number |
||||
|
max: number |
||||
|
name: string |
||||
|
} |
||||
|
export type AnalysisTotalTypes = { |
||||
|
users: number |
||||
|
messages: number |
||||
|
moneys: number |
||||
|
shoppings: number |
||||
|
} |
||||
|
|
||||
|
export type UserAccessSource = { |
||||
|
value: number |
||||
|
name: string |
||||
|
} |
||||
|
|
||||
|
export type WeeklyUserActivity = { |
||||
|
value: number |
||||
|
name: string |
||||
|
} |
||||
|
|
||||
|
export type MonthlySales = { |
||||
|
name: string |
||||
|
estimate: number |
||||
|
actual: number |
||||
|
} |
@ -0,0 +1,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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 - 生成 JSON;1 - 生成 Options;2 - 生成组件 |
||||
|
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 || ' ' |
||||
|
} |
||||
|
|
||||
|
/** 初始化 **/ |
||||
|
onMounted(async () => { |
||||
|
// 注册代码高亮的各种语言 |
||||
|
hljs.registerLanguage('xml', xml) |
||||
|
hljs.registerLanguage('json', json) |
||||
|
}) |
||||
|
</script> |
@ -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 = {} // key:file 的 id;value:true |
||||
|
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 || ' ' |
||||
|
} |
||||
|
|
||||
|
/** 初始化 **/ |
||||
|
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> |
@ -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> |
@ -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> |
@ -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> |
@ -0,0 +1,4 @@ |
|||||
|
import BasicInfoForm from './BasicInfoForm.vue' |
||||
|
import ColumInfoForm from './ColumInfoForm.vue' |
||||
|
import GenerateInfoForm from './GenerateInfoForm.vue' |
||||
|
export { BasicInfoForm, ColumInfoForm, GenerateInfoForm } |
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
||||
|
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> |
@ -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> |
@ -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> |
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
||||
|
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> |
@ -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> |
@ -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> |
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
||||
|
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> |
@ -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> |
@ -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> |
@ -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> |
@ -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"> |
||||
|
提示:仅允许导入 jpg、png、gif 格式文件! |
||||
|
</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> |
@ -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> |
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
||||
|
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> |
@ -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> |
@ -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> |
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 |
||||
|
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> |
@ -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> |
@ -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> |
@ -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> |
@ -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: { |
||||
|
// 属性lineStyle(详见lineStyle)控制线条样式 |
||||
|
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> |
@ -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 问题: |
||||
|
// 1)boot 参考 https://doc.iocoder.cn/server-monitor/ 解决; |
||||
|
// 2)cloud 参考 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> |
@ -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> |
@ -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> |
@ -0,0 +1,4 @@ |
|||||
|
<template> |
||||
|
<div>index</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup></script> |
@ -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> |
@ -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> |
@ -0,0 +1,26 @@ |
|||||
|
<template> |
||||
|
<h2 class="enter-x mb-3 text-center text-2xl font-bold xl:text-center xl:text-3xl"> |
||||
|
{{ getFormTitle }} |
||||
|
</h2> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { LoginStateEnum, useLoginState } from './useLogin' |
||||
|
|
||||
|
defineOptions({ name: 'LoginFormTitle' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
|
||||
|
const { getLoginState } = useLoginState() |
||||
|
|
||||
|
const getFormTitle = computed(() => { |
||||
|
const titleObj = { |
||||
|
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'), |
||||
|
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'), |
||||
|
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'), |
||||
|
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'), |
||||
|
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'), |
||||
|
[LoginStateEnum.SSO]: t('sys.login.ssoFormTitle') |
||||
|
} |
||||
|
return titleObj[unref(getLoginState)] |
||||
|
}) |
||||
|
</script> |
@ -0,0 +1,225 @@ |
|||||
|
<template> |
||||
|
<el-form |
||||
|
v-show="getShow" |
||||
|
ref="formSmsLogin" |
||||
|
:model="loginData.loginForm" |
||||
|
:rules="rules" |
||||
|
class="login-form" |
||||
|
label-position="top" |
||||
|
label-width="120px" |
||||
|
size="large" |
||||
|
> |
||||
|
<el-row style="margin-right: -10px; margin-left: -10px"> |
||||
|
<!-- 租户名 --> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item> |
||||
|
<LoginFormTitle style="width: 100%" /> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> |
||||
|
<el-input |
||||
|
v-model="loginData.loginForm.tenantName" |
||||
|
:placeholder="t('login.tenantNamePlaceholder')" |
||||
|
:prefix-icon="iconHouse" |
||||
|
type="primary" |
||||
|
link |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
<!-- 手机号 --> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item prop="mobileNumber"> |
||||
|
<el-input |
||||
|
v-model="loginData.loginForm.mobileNumber" |
||||
|
:placeholder="t('login.mobileNumberPlaceholder')" |
||||
|
:prefix-icon="iconCellphone" |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
<!-- 验证码 --> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item prop="code"> |
||||
|
<el-row :gutter="5" justify="space-between" style="width: 100%"> |
||||
|
<el-col :span="24"> |
||||
|
<el-input |
||||
|
v-model="loginData.loginForm.code" |
||||
|
:placeholder="t('login.codePlaceholder')" |
||||
|
:prefix-icon="iconCircleCheck" |
||||
|
> |
||||
|
<!-- <el-button class="w-[100%]"> --> |
||||
|
<template #append> |
||||
|
<span |
||||
|
v-if="mobileCodeTimer <= 0" |
||||
|
class="getMobileCode" |
||||
|
style="cursor: pointer" |
||||
|
@click="getSmsCode" |
||||
|
> |
||||
|
{{ t('login.getSmsCode') }} |
||||
|
</span> |
||||
|
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer"> |
||||
|
{{ mobileCodeTimer }}秒后可重新获取 |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-input> |
||||
|
<!-- </el-button> --> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
<!-- 登录按钮 / 返回按钮 --> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item> |
||||
|
<XButton |
||||
|
:loading="loginLoading" |
||||
|
:title="t('login.login')" |
||||
|
class="w-[100%]" |
||||
|
type="primary" |
||||
|
@click="signIn()" |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-form-item> |
||||
|
<XButton |
||||
|
:loading="loginLoading" |
||||
|
:title="t('login.backLogin')" |
||||
|
class="w-[100%]" |
||||
|
@click="handleBackLogin()" |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</el-form> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router' |
||||
|
|
||||
|
import { useIcon } from '@/hooks/web/useIcon' |
||||
|
|
||||
|
import { setTenantId, setToken } from '@/utils/auth' |
||||
|
import { usePermissionStore } from '@/store/modules/permission' |
||||
|
import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login' |
||||
|
import LoginFormTitle from './LoginFormTitle.vue' |
||||
|
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin' |
||||
|
|
||||
|
defineOptions({ name: 'MobileForm' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const message = useMessage() |
||||
|
const permissionStore = usePermissionStore() |
||||
|
const { currentRoute, push } = useRouter() |
||||
|
const formSmsLogin = ref() |
||||
|
const loginLoading = ref(false) |
||||
|
const iconHouse = useIcon({ icon: 'ep:house' }) |
||||
|
const iconCellphone = useIcon({ icon: 'ep:cellphone' }) |
||||
|
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' }) |
||||
|
const { validForm } = useFormValid(formSmsLogin) |
||||
|
const { handleBackLogin, getLoginState } = useLoginState() |
||||
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE) |
||||
|
|
||||
|
const rules = { |
||||
|
tenantName: [required], |
||||
|
mobileNumber: [required], |
||||
|
code: [required] |
||||
|
} |
||||
|
const loginData = reactive({ |
||||
|
codeImg: '', |
||||
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, |
||||
|
token: '', |
||||
|
loading: { |
||||
|
signIn: false |
||||
|
}, |
||||
|
loginForm: { |
||||
|
uuid: '', |
||||
|
tenantName: '闻荫源码', |
||||
|
mobileNumber: '', |
||||
|
code: '' |
||||
|
} |
||||
|
}) |
||||
|
const smsVO = reactive({ |
||||
|
smsCode: { |
||||
|
mobile: '', |
||||
|
scene: 21 |
||||
|
}, |
||||
|
loginSms: { |
||||
|
mobile: '', |
||||
|
code: '' |
||||
|
} |
||||
|
}) |
||||
|
const mobileCodeTimer = ref(0) |
||||
|
const redirect = ref<string>('') |
||||
|
const getSmsCode = async () => { |
||||
|
await getTenantId() |
||||
|
smsVO.smsCode.mobile = loginData.loginForm.mobileNumber |
||||
|
await sendSmsCode(smsVO.smsCode).then(async () => { |
||||
|
message.success(t('login.SmsSendMsg')) |
||||
|
// 设置倒计时 |
||||
|
mobileCodeTimer.value = 60 |
||||
|
let msgTimer = setInterval(() => { |
||||
|
mobileCodeTimer.value = mobileCodeTimer.value - 1 |
||||
|
if (mobileCodeTimer.value <= 0) { |
||||
|
clearInterval(msgTimer) |
||||
|
} |
||||
|
}, 1000) |
||||
|
}) |
||||
|
} |
||||
|
watch( |
||||
|
() => currentRoute.value, |
||||
|
(route: RouteLocationNormalizedLoaded) => { |
||||
|
redirect.value = route?.query?.redirect as string |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true |
||||
|
} |
||||
|
) |
||||
|
// 获取租户 ID |
||||
|
const getTenantId = async () => { |
||||
|
if (loginData.tenantEnable === 'true') { |
||||
|
const res = await getTenantIdByName(loginData.loginForm.tenantName) |
||||
|
setTenantId(res) |
||||
|
} |
||||
|
} |
||||
|
// 登录 |
||||
|
const signIn = async () => { |
||||
|
await getTenantId() |
||||
|
const data = await validForm() |
||||
|
if (!data) return |
||||
|
ElLoading.service({ |
||||
|
lock: true, |
||||
|
text: '正在加载系统中...', |
||||
|
background: 'rgba(0, 0, 0, 0.7)' |
||||
|
}) |
||||
|
loginLoading.value = true |
||||
|
smsVO.loginSms.mobile = loginData.loginForm.mobileNumber |
||||
|
smsVO.loginSms.code = loginData.loginForm.code |
||||
|
await smsLogin(smsVO.loginSms) |
||||
|
.then(async (res) => { |
||||
|
setToken(res) |
||||
|
if (!redirect.value) { |
||||
|
redirect.value = '/' |
||||
|
} |
||||
|
push({ path: redirect.value || permissionStore.addRouters[0].path }) |
||||
|
}) |
||||
|
.catch(() => {}) |
||||
|
.finally(() => { |
||||
|
loginLoading.value = false |
||||
|
setTimeout(() => { |
||||
|
const loadingInstance = ElLoading.service() |
||||
|
loadingInstance.close() |
||||
|
}, 400) |
||||
|
}) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
:deep(.anticon) { |
||||
|
&:hover { |
||||
|
color: var(--el-color-primary) !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.smsbtn { |
||||
|
margin-top: 33px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,30 @@ |
|||||
|
<template> |
||||
|
<el-row v-show="getShow" style="margin-right: -10px; margin-left: -10px"> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<LoginFormTitle style="width: 100%" /> |
||||
|
</el-col> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<el-card class="mb-10px text-center" shadow="hover"> |
||||
|
<Qrcode :logo="logoImg" /> |
||||
|
</el-card> |
||||
|
</el-col> |
||||
|
<el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider> |
||||
|
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> |
||||
|
<div class="mt-15px w-[100%]"> |
||||
|
<XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" /> |
||||
|
</div> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import logoImg from '@/assets/imgs/logo.png' |
||||
|
|
||||
|
import LoginFormTitle from './LoginFormTitle.vue' |
||||
|
import { LoginStateEnum, useLoginState } from './useLogin' |
||||
|
|
||||
|
defineOptions({ name: 'QrCodeForm' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const { handleBackLogin, getLoginState } = useLoginState() |
||||
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE) |
||||
|
</script> |
@ -0,0 +1,142 @@ |
|||||
|
<template> |
||||
|
<Form |
||||
|
v-show="getShow" |
||||
|
:rules="rules" |
||||
|
:schema="schema" |
||||
|
class="dark:(border-1 border-[var(--el-border-color)] border-solid)" |
||||
|
hide-required-asterisk |
||||
|
label-position="top" |
||||
|
size="large" |
||||
|
@register="register" |
||||
|
> |
||||
|
<template #title> |
||||
|
<LoginFormTitle style="width: 100%" /> |
||||
|
</template> |
||||
|
|
||||
|
<template #code="form"> |
||||
|
<div class="w-[100%] flex"> |
||||
|
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<template #register> |
||||
|
<div class="w-[100%]"> |
||||
|
<XButton |
||||
|
:loading="loading" |
||||
|
:title="t('login.register')" |
||||
|
class="w-[100%]" |
||||
|
type="primary" |
||||
|
@click="loginRegister()" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="mt-15px w-[100%]"> |
||||
|
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Form> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { FormRules } from 'element-plus' |
||||
|
|
||||
|
import { useForm } from '@/hooks/web/useForm' |
||||
|
import { useValidator } from '@/hooks/web/useValidator' |
||||
|
import LoginFormTitle from './LoginFormTitle.vue' |
||||
|
import { LoginStateEnum, useLoginState } from './useLogin' |
||||
|
import { FormSchema } from '@/types/form' |
||||
|
|
||||
|
defineOptions({ name: 'RegisterForm' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const { required } = useValidator() |
||||
|
const { register, elFormRef } = useForm() |
||||
|
const { handleBackLogin, getLoginState } = useLoginState() |
||||
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER) |
||||
|
|
||||
|
const schema = reactive<FormSchema[]>([ |
||||
|
{ |
||||
|
field: 'title', |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
field: 'username', |
||||
|
label: t('login.username'), |
||||
|
value: '', |
||||
|
component: 'Input', |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
}, |
||||
|
componentProps: { |
||||
|
placeholder: t('login.usernamePlaceholder') |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
field: 'password', |
||||
|
label: t('login.password'), |
||||
|
value: '', |
||||
|
component: 'InputPassword', |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
}, |
||||
|
componentProps: { |
||||
|
style: { |
||||
|
width: '100%' |
||||
|
}, |
||||
|
strength: true, |
||||
|
placeholder: t('login.passwordPlaceholder') |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
field: 'check_password', |
||||
|
label: t('login.checkPassword'), |
||||
|
value: '', |
||||
|
component: 'InputPassword', |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
}, |
||||
|
componentProps: { |
||||
|
style: { |
||||
|
width: '100%' |
||||
|
}, |
||||
|
strength: true, |
||||
|
placeholder: t('login.passwordPlaceholder') |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
field: 'code', |
||||
|
label: t('login.code'), |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
field: 'register', |
||||
|
colProps: { |
||||
|
span: 24 |
||||
|
} |
||||
|
} |
||||
|
]) |
||||
|
|
||||
|
const rules: FormRules = { |
||||
|
username: [required()], |
||||
|
password: [required()], |
||||
|
check_password: [required()], |
||||
|
code: [required()] |
||||
|
} |
||||
|
|
||||
|
const loading = ref(false) |
||||
|
|
||||
|
const loginRegister = async () => { |
||||
|
const formRef = unref(elFormRef) |
||||
|
formRef?.validate(async (valid) => { |
||||
|
if (valid) { |
||||
|
try { |
||||
|
loading.value = true |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,199 @@ |
|||||
|
<template> |
||||
|
<div v-show="ssoVisible" class="form-cont"> |
||||
|
<!-- 应用名 --> |
||||
|
<LoginFormTitle style="width: 100%" /> |
||||
|
<el-tabs class="form" style="float: none" value="uname"> |
||||
|
<el-tab-pane :label="client.name" name="uname" /> |
||||
|
</el-tabs> |
||||
|
<div> |
||||
|
<el-form :model="formData" class="login-form"> |
||||
|
<!-- 授权范围的选择 --> |
||||
|
此第三方应用请求获得以下权限: |
||||
|
<el-form-item prop="scopes"> |
||||
|
<el-checkbox-group v-model="formData.scopes"> |
||||
|
<el-checkbox |
||||
|
v-for="scope in queryParams.scopes" |
||||
|
:key="scope" |
||||
|
:label="scope" |
||||
|
style="display: block; margin-bottom: -10px" |
||||
|
> |
||||
|
{{ formatScope(scope) }} |
||||
|
</el-checkbox> |
||||
|
</el-checkbox-group> |
||||
|
</el-form-item> |
||||
|
<!-- 下方的登录按钮 --> |
||||
|
<el-form-item class="w-1/1"> |
||||
|
<el-button |
||||
|
:loading="formLoading" |
||||
|
class="w-6/10" |
||||
|
type="primary" |
||||
|
@click.prevent="handleAuthorize(true)" |
||||
|
> |
||||
|
<span v-if="!formLoading">同意授权</span> |
||||
|
<span v-else>授 权 中...</span> |
||||
|
</el-button> |
||||
|
<el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import LoginFormTitle from './LoginFormTitle.vue' |
||||
|
import * as OAuth2Api from '@/api/login/oauth2' |
||||
|
import { LoginStateEnum, useLoginState } from './useLogin' |
||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router' |
||||
|
|
||||
|
defineOptions({ name: 'SSOLogin' }) |
||||
|
|
||||
|
const route = useRoute() // 路由 |
||||
|
const { currentRoute } = useRouter() // 路由 |
||||
|
const { getLoginState, setLoginState } = useLoginState() |
||||
|
|
||||
|
const client = ref({ |
||||
|
// 客户端信息 |
||||
|
name: '', |
||||
|
logo: '' |
||||
|
}) |
||||
|
interface queryType { |
||||
|
responseType: string |
||||
|
clientId: string |
||||
|
redirectUri: string |
||||
|
state: string |
||||
|
scopes: string[] |
||||
|
} |
||||
|
const queryParams = reactive<queryType>({ |
||||
|
// URL 上的 client_id、scope 等参数 |
||||
|
responseType: '', |
||||
|
clientId: '', |
||||
|
redirectUri: '', |
||||
|
state: '', |
||||
|
scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取 |
||||
|
}) |
||||
|
const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单 |
||||
|
interface formType { |
||||
|
scopes: string[] |
||||
|
} |
||||
|
const formData = reactive<formType>({ |
||||
|
scopes: [] // 已选中的 scope 数组 |
||||
|
}) |
||||
|
const formLoading = ref(false) // 表单是否提交中 |
||||
|
|
||||
|
/** 初始化授权信息 */ |
||||
|
const init = async () => { |
||||
|
// 防止在没有登录的情况下循环弹窗 |
||||
|
if (typeof route.query.client_id === 'undefined') return |
||||
|
// 解析参数 |
||||
|
// 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write |
||||
|
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read |
||||
|
queryParams.responseType = route.query.response_type as string |
||||
|
queryParams.clientId = route.query.client_id as string |
||||
|
queryParams.redirectUri = route.query.redirect_uri as string |
||||
|
queryParams.state = route.query.state as string |
||||
|
if (route.query.scope) { |
||||
|
queryParams.scopes = (route.query.scope as string).split(' ') |
||||
|
} |
||||
|
|
||||
|
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。 |
||||
|
if (queryParams.scopes.length > 0) { |
||||
|
const data = await doAuthorize(true, queryParams.scopes, []) |
||||
|
if (data) { |
||||
|
location.href = data |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取授权页的基本信息 |
||||
|
const data = await OAuth2Api.getAuthorize(queryParams.clientId) |
||||
|
client.value = data.client |
||||
|
// 解析 scope |
||||
|
let scopes |
||||
|
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes |
||||
|
if (queryParams.scopes.length > 0) { |
||||
|
scopes = [] |
||||
|
for (const scope of data.scopes) { |
||||
|
if (queryParams.scopes.indexOf(scope.key) >= 0) { |
||||
|
scopes.push(scope) |
||||
|
} |
||||
|
} |
||||
|
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它 |
||||
|
} else { |
||||
|
scopes = data.scopes |
||||
|
for (const scope of scopes) { |
||||
|
queryParams.scopes.push(scope.key) |
||||
|
} |
||||
|
} |
||||
|
// 生成已选中的 checkedScopes |
||||
|
for (const scope of scopes) { |
||||
|
if (scope.value) { |
||||
|
formData.scopes.push(scope.key) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 处理授权的提交 */ |
||||
|
const handleAuthorize = async (approved) => { |
||||
|
// 计算 checkedScopes + uncheckedScopes |
||||
|
let checkedScopes |
||||
|
let uncheckedScopes |
||||
|
if (approved) { |
||||
|
// 同意授权,按照用户的选择 |
||||
|
checkedScopes = formData.scopes |
||||
|
uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1) |
||||
|
} else { |
||||
|
// 拒绝,则都是取消 |
||||
|
checkedScopes = [] |
||||
|
uncheckedScopes = queryParams.scopes |
||||
|
} |
||||
|
// 提交授权的请求 |
||||
|
formLoading.value = true |
||||
|
try { |
||||
|
const data = await doAuthorize(false, checkedScopes, uncheckedScopes) |
||||
|
if (!data) { |
||||
|
return |
||||
|
} |
||||
|
location.href = data |
||||
|
} finally { |
||||
|
formLoading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 调用授权 API 接口 */ |
||||
|
const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => { |
||||
|
return OAuth2Api.authorize( |
||||
|
queryParams.responseType, |
||||
|
queryParams.clientId, |
||||
|
queryParams.redirectUri, |
||||
|
queryParams.state, |
||||
|
autoApprove, |
||||
|
checkedScopes, |
||||
|
uncheckedScopes |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** 格式化 scope 文本 */ |
||||
|
const formatScope = (scope) => { |
||||
|
// 格式化 scope 授权范围,方便用户理解。 |
||||
|
// 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。 |
||||
|
switch (scope) { |
||||
|
case 'user.read': |
||||
|
return '访问你的个人信息' |
||||
|
case 'user.write': |
||||
|
return '修改你的个人信息' |
||||
|
default: |
||||
|
return scope |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 监听当前路由为 SSOLogin 时,进行数据的初始化 */ |
||||
|
watch( |
||||
|
() => currentRoute.value, |
||||
|
(route: RouteLocationNormalizedLoaded) => { |
||||
|
if (route.name === 'SSOLogin') { |
||||
|
setLoginState(LoginStateEnum.SSO) |
||||
|
init() |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
</script> |
@ -0,0 +1,8 @@ |
|||||
|
import LoginForm from './LoginForm.vue' |
||||
|
import MobileForm from './MobileForm.vue' |
||||
|
import LoginFormTitle from './LoginFormTitle.vue' |
||||
|
import RegisterForm from './RegisterForm.vue' |
||||
|
import QrCodeForm from './QrCodeForm.vue' |
||||
|
import SSOLoginVue from './SSOLogin.vue' |
||||
|
|
||||
|
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue } |
@ -0,0 +1,42 @@ |
|||||
|
import { Ref } from 'vue' |
||||
|
|
||||
|
export enum LoginStateEnum { |
||||
|
LOGIN, |
||||
|
REGISTER, |
||||
|
RESET_PASSWORD, |
||||
|
MOBILE, |
||||
|
QR_CODE, |
||||
|
SSO |
||||
|
} |
||||
|
|
||||
|
const currentState = ref(LoginStateEnum.LOGIN) |
||||
|
|
||||
|
export function useLoginState() { |
||||
|
function setLoginState(state: LoginStateEnum) { |
||||
|
currentState.value = state |
||||
|
} |
||||
|
const getLoginState = computed(() => currentState.value) |
||||
|
|
||||
|
function handleBackLogin() { |
||||
|
setLoginState(LoginStateEnum.LOGIN) |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
setLoginState, |
||||
|
getLoginState, |
||||
|
handleBackLogin |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function useFormValid<T extends Object = any>(formRef: Ref<any>) { |
||||
|
async function validForm() { |
||||
|
const form = unref(formRef) |
||||
|
if (!form) return |
||||
|
const data = await form.validate() |
||||
|
return data as T |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
validForm |
||||
|
} |
||||
|
} |
@ -0,0 +1,104 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
:class="prefixCls" |
||||
|
class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px" |
||||
|
> |
||||
|
<div class="relative mx-auto h-full flex"> |
||||
|
<div |
||||
|
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden`" |
||||
|
> |
||||
|
<!-- 左上角的 logo + 系统标题 --> |
||||
|
<div class="relative flex items-center text-white"> |
||||
|
<img alt="" class="mr-10px h-24px w-48px" src="@/assets/imgs/logo.png" /> |
||||
|
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
||||
|
</div> |
||||
|
<!-- 左边的背景图 + 欢迎语 --> |
||||
|
<div class="h-[calc(100%-60px)] flex items-center justify-center"> |
||||
|
<TransitionGroup |
||||
|
appear |
||||
|
enter-active-class="animate__animated animate__bounceInLeft" |
||||
|
tag="div" |
||||
|
> |
||||
|
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> |
||||
|
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> |
||||
|
<div key="3" class="mt-5 text-14px font-normal text-white"> |
||||
|
{{ t('login.message') }} |
||||
|
</div> |
||||
|
</TransitionGroup> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px"> |
||||
|
<!-- 右上角的主题、语言选择 --> |
||||
|
<div |
||||
|
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end" |
||||
|
> |
||||
|
<div class="flex items-center at-2xl:hidden at-xl:hidden"> |
||||
|
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" /> |
||||
|
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span> |
||||
|
</div> |
||||
|
<div class="flex items-center justify-end space-x-10px"> |
||||
|
<!-- <ThemeSwitch /> --> |
||||
|
<LocaleDropdown class="dark:text-white lt-xl:text-white" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 右边的登录界面 --> |
||||
|
<Transition appear enter-active-class="animate__animated animate__bounceInRight"> |
||||
|
<div |
||||
|
class="m-auto h-full w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px" |
||||
|
> |
||||
|
<!-- 账号登录 --> |
||||
|
<LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> |
||||
|
<!-- 手机登录 --> |
||||
|
<MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> |
||||
|
<!-- 二维码登录 --> |
||||
|
<QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> |
||||
|
<!-- 注册 --> |
||||
|
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> |
||||
|
<!-- 三方登录 --> |
||||
|
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> |
||||
|
</div> |
||||
|
</Transition> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { underlineToHump } from '@/utils' |
||||
|
|
||||
|
import { useDesign } from '@/hooks/web/useDesign' |
||||
|
import { useAppStore } from '@/store/modules/app' |
||||
|
import { ThemeSwitch } from '@/layout/components/ThemeSwitch' |
||||
|
import { LocaleDropdown } from '@/layout/components/LocaleDropdown' |
||||
|
|
||||
|
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components' |
||||
|
|
||||
|
defineOptions({ name: 'Login' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const appStore = useAppStore() |
||||
|
const { getPrefixCls } = useDesign() |
||||
|
const prefixCls = getPrefixCls('login') |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
$prefix-cls: #{$namespace}-login; |
||||
|
|
||||
|
.#{$prefix-cls} { |
||||
|
overflow: auto; |
||||
|
|
||||
|
&__left { |
||||
|
&::before { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
z-index: -1; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background-image: url('@/assets/svgs/login-bg.svg'); |
||||
|
background-position: center; |
||||
|
background-repeat: no-repeat; |
||||
|
content: ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,92 @@ |
|||||
|
<template> |
||||
|
<Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema"> |
||||
|
<template #sex="form"> |
||||
|
<el-radio-group v-model="form['sex']"> |
||||
|
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio> |
||||
|
<el-radio :label="2">{{ t('profile.user.woman') }}</el-radio> |
||||
|
</el-radio-group> |
||||
|
</template> |
||||
|
</Form> |
||||
|
<div style="text-align: center"> |
||||
|
<XButton :title="t('common.save')" type="primary" @click="submit()" /> |
||||
|
<XButton :title="t('common.reset')" type="danger" @click="init()" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { FormRules } from 'element-plus' |
||||
|
import { FormSchema } from '@/types/form' |
||||
|
import type { FormExpose } from '@/components/Form' |
||||
|
import { |
||||
|
getUserProfile, |
||||
|
updateUserProfile, |
||||
|
UserProfileUpdateReqVO |
||||
|
} from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'BasicInfo' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const message = useMessage() // 消息弹窗 |
||||
|
// 表单校验 |
||||
|
const rules = reactive<FormRules>({ |
||||
|
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }], |
||||
|
email: [ |
||||
|
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' }, |
||||
|
{ |
||||
|
type: 'email', |
||||
|
message: t('profile.rules.truemail'), |
||||
|
trigger: ['blur', 'change'] |
||||
|
} |
||||
|
], |
||||
|
mobile: [ |
||||
|
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' }, |
||||
|
{ |
||||
|
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, |
||||
|
message: t('profile.rules.truephone'), |
||||
|
trigger: 'blur' |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
const schema = reactive<FormSchema[]>([ |
||||
|
{ |
||||
|
field: 'nickname', |
||||
|
label: t('profile.user.nickname'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'mobile', |
||||
|
label: t('profile.user.mobile'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'email', |
||||
|
label: t('profile.user.email'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'sex', |
||||
|
label: t('profile.user.sex'), |
||||
|
component: 'InputNumber', |
||||
|
value: 0 |
||||
|
} |
||||
|
]) |
||||
|
const formRef = ref<FormExpose>() // 表单 Ref |
||||
|
const submit = () => { |
||||
|
const elForm = unref(formRef)?.getElFormRef() |
||||
|
if (!elForm) return |
||||
|
elForm.validate(async (valid) => { |
||||
|
if (valid) { |
||||
|
const data = unref(formRef)?.formModel as UserProfileUpdateReqVO |
||||
|
await updateUserProfile(data) |
||||
|
message.success(t('common.updateSuccess')) |
||||
|
await init() |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
const init = async () => { |
||||
|
const res = await getUserProfile() |
||||
|
unref(formRef)?.setValues(res) |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await init() |
||||
|
}) |
||||
|
</script> |
@ -0,0 +1,99 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<div class="text-center"> |
||||
|
<UserAvatar :img="userInfo?.avatar" /> |
||||
|
</div> |
||||
|
<ul class="list-group list-group-striped"> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:user" /> |
||||
|
{{ t('profile.user.username') }} |
||||
|
<div class="pull-right">{{ userInfo?.username }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:phone" /> |
||||
|
{{ t('profile.user.mobile') }} |
||||
|
<div class="pull-right">{{ userInfo?.mobile }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="fontisto:email" /> |
||||
|
{{ t('profile.user.email') }} |
||||
|
<div class="pull-right">{{ userInfo?.email }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="carbon:tree-view-alt" /> |
||||
|
{{ t('profile.user.dept') }} |
||||
|
<div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:suitcase" /> |
||||
|
{{ t('profile.user.posts') }} |
||||
|
<div v-if="userInfo?.posts" class="pull-right"> |
||||
|
{{ userInfo?.posts.map((post) => post.name).join(',') }} |
||||
|
</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="icon-park-outline:peoples" /> |
||||
|
{{ t('profile.user.roles') }} |
||||
|
<div v-if="userInfo?.roles" class="pull-right"> |
||||
|
{{ userInfo?.roles.map((role) => role.name).join(',') }} |
||||
|
</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:calendar" /> |
||||
|
{{ t('profile.user.createTime') }} |
||||
|
<div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import UserAvatar from './UserAvatar.vue' |
||||
|
|
||||
|
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'ProfileUser' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const userInfo = ref<ProfileVO>() |
||||
|
const getUserInfo = async () => { |
||||
|
const users = await getUserProfile() |
||||
|
userInfo.value = users |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getUserInfo() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.text-center { |
||||
|
position: relative; |
||||
|
height: 120px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.list-group-striped > .list-group-item { |
||||
|
padding-right: 0; |
||||
|
padding-left: 0; |
||||
|
border-right: 0; |
||||
|
border-left: 0; |
||||
|
border-radius: 0; |
||||
|
} |
||||
|
|
||||
|
.list-group { |
||||
|
padding-left: 0; |
||||
|
list-style: none; |
||||
|
} |
||||
|
|
||||
|
.list-group-item { |
||||
|
padding: 11px 0; |
||||
|
margin-bottom: -1px; |
||||
|
font-size: 13px; |
||||
|
border-top: 1px solid #e7eaec; |
||||
|
border-bottom: 1px solid #e7eaec; |
||||
|
} |
||||
|
|
||||
|
.pull-right { |
||||
|
float: right !important; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,73 @@ |
|||||
|
<template> |
||||
|
<el-form ref="formRef" :model="password" :rules="rules" :label-width="200"> |
||||
|
<el-form-item :label="t('profile.password.oldPassword')" prop="oldPassword"> |
||||
|
<InputPassword v-model="password.oldPassword" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item :label="t('profile.password.newPassword')" prop="newPassword"> |
||||
|
<InputPassword v-model="password.newPassword" strength /> |
||||
|
</el-form-item> |
||||
|
<el-form-item :label="t('profile.password.confirmPassword')" prop="confirmPassword"> |
||||
|
<InputPassword v-model="password.confirmPassword" strength /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<XButton :title="t('common.save')" type="primary" @click="submit(formRef)" /> |
||||
|
<XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" /> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { FormInstance, FormRules } from 'element-plus' |
||||
|
|
||||
|
import { InputPassword } from '@/components/InputPassword' |
||||
|
import { updateUserPassword } from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'ResetPwd' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const message = useMessage() |
||||
|
const formRef = ref<FormInstance>() |
||||
|
const password = reactive({ |
||||
|
oldPassword: '', |
||||
|
newPassword: '', |
||||
|
confirmPassword: '' |
||||
|
}) |
||||
|
|
||||
|
// 表单校验 |
||||
|
const equalToPassword = (_rule, value, callback) => { |
||||
|
if (password.newPassword !== value) { |
||||
|
callback(new Error(t('profile.password.diffPwd'))) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const rules = reactive<FormRules>({ |
||||
|
oldPassword: [ |
||||
|
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' }, |
||||
|
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' } |
||||
|
], |
||||
|
newPassword: [ |
||||
|
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' }, |
||||
|
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' } |
||||
|
], |
||||
|
confirmPassword: [ |
||||
|
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' }, |
||||
|
{ required: true, validator: equalToPassword, trigger: 'blur' } |
||||
|
] |
||||
|
}) |
||||
|
|
||||
|
const submit = (formEl: FormInstance | undefined) => { |
||||
|
if (!formEl) return |
||||
|
formEl.validate(async (valid) => { |
||||
|
if (valid) { |
||||
|
await updateUserPassword(password.oldPassword, password.newPassword) |
||||
|
message.success(t('common.updateSuccess')) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const reset = (formEl: FormInstance | undefined) => { |
||||
|
if (!formEl) return |
||||
|
formEl.resetFields() |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,39 @@ |
|||||
|
<template> |
||||
|
<div class="change-avatar"> |
||||
|
<CropperAvatar |
||||
|
ref="cropperRef" |
||||
|
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }" |
||||
|
:showBtn="false" |
||||
|
:value="img" |
||||
|
width="120px" |
||||
|
@change="handelUpload" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { propTypes } from '@/utils/propTypes' |
||||
|
import { uploadAvatar } from '@/api/system/user/profile' |
||||
|
import { CropperAvatar } from '@/components/Cropper' |
||||
|
|
||||
|
defineOptions({ name: 'UserAvatar' }) |
||||
|
|
||||
|
defineProps({ |
||||
|
img: propTypes.string.def('') |
||||
|
}) |
||||
|
|
||||
|
const cropperRef = ref() |
||||
|
const handelUpload = async ({ data }) => { |
||||
|
await uploadAvatar({ avatarFile: data }) |
||||
|
cropperRef.value.close() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.change-avatar { |
||||
|
img { |
||||
|
display: block; |
||||
|
margin-bottom: 15px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,94 @@ |
|||||
|
<template> |
||||
|
<el-table :data="socialUsers" :show-header="false"> |
||||
|
<el-table-column fixed="left" title="序号" type="seq" width="60" /> |
||||
|
<el-table-column align="left" label="社交平台" width="120"> |
||||
|
<template #default="{ row }"> |
||||
|
<img :src="row.img" alt="" class="h-5 align-middle" /> |
||||
|
<p class="mr-5">{{ row.title }}</p> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column align="center" label="操作"> |
||||
|
<template #default="{ row }"> |
||||
|
<template v-if="row.openid"> |
||||
|
已绑定 |
||||
|
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" /> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
未绑定 |
||||
|
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" /> |
||||
|
</template> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { SystemUserSocialTypeEnum } from '@/utils/constants' |
||||
|
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' |
||||
|
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser' |
||||
|
|
||||
|
defineOptions({ name: 'UserSocial' }) |
||||
|
|
||||
|
const message = useMessage() |
||||
|
const socialUsers = ref<any[]>([]) |
||||
|
const userInfo = ref<ProfileVO>() |
||||
|
|
||||
|
const initSocial = async () => { |
||||
|
const res = await getUserProfile() |
||||
|
userInfo.value = res |
||||
|
for (const i in SystemUserSocialTypeEnum) { |
||||
|
const socialUser = { ...SystemUserSocialTypeEnum[i] } |
||||
|
socialUsers.value.push(socialUser) |
||||
|
if (userInfo.value?.socialUsers) { |
||||
|
for (const j in userInfo.value.socialUsers) { |
||||
|
if (socialUser.type === userInfo.value.socialUsers[j].type) { |
||||
|
socialUser.openid = userInfo.value.socialUsers[j].openid |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const route = useRoute() |
||||
|
const bindSocial = () => { |
||||
|
// 社交绑定 |
||||
|
const type = route.query.type |
||||
|
const code = route.query.code |
||||
|
const state = route.query.state |
||||
|
if (!code) { |
||||
|
return |
||||
|
} |
||||
|
socialBind(type, code, state).then(() => { |
||||
|
message.success('绑定成功') |
||||
|
initSocial() |
||||
|
}) |
||||
|
} |
||||
|
const bind = (row) => { |
||||
|
const redirectUri = location.origin + '/user/profile?type=' + row.type |
||||
|
// 进行跳转 |
||||
|
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => { |
||||
|
window.location.href = res |
||||
|
}) |
||||
|
} |
||||
|
const unbind = async (row) => { |
||||
|
const res = await socialUnbind(row.type, row.openid) |
||||
|
if (res) { |
||||
|
row.openid = undefined |
||||
|
} |
||||
|
message.success('解绑成功') |
||||
|
} |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
await initSocial() |
||||
|
}) |
||||
|
|
||||
|
watch( |
||||
|
() => route, |
||||
|
(newRoute) => { |
||||
|
bindSocial() |
||||
|
console.log(newRoute) |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true |
||||
|
} |
||||
|
) |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
import BasicInfo from './BasicInfo.vue' |
||||
|
import ProfileUser from './ProfileUser.vue' |
||||
|
import ResetPwd from './ResetPwd.vue' |
||||
|
import UserAvatarVue from './UserAvatar.vue' |
||||
|
import UserSocial from './UserSocial.vue' |
||||
|
|
||||
|
export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial } |
@ -0,0 +1,64 @@ |
|||||
|
<template> |
||||
|
<div class="flex"> |
||||
|
<el-card class="user w-1/3" shadow="hover"> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>{{ t('profile.user.title') }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<ProfileUser /> |
||||
|
</el-card> |
||||
|
<el-card class="user ml-3 w-2/3" shadow="hover"> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>{{ t('profile.info.title') }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div> |
||||
|
<el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs"> |
||||
|
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo"> |
||||
|
<BasicInfo /> |
||||
|
</el-tab-pane> |
||||
|
<el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd"> |
||||
|
<ResetPwd /> |
||||
|
</el-tab-pane> |
||||
|
<!-- <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial"> |
||||
|
<UserSocial /> |
||||
|
</el-tab-pane> --> |
||||
|
</el-tabs> |
||||
|
</div> |
||||
|
</el-card> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup lang="ts" name="Profile"> |
||||
|
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/' |
||||
|
const { t } = useI18n() |
||||
|
|
||||
|
const activeName = ref('basicInfo') |
||||
|
</script> |
||||
|
<style scoped> |
||||
|
.user { |
||||
|
max-height: 960px; |
||||
|
padding: 15px 20px 20px; |
||||
|
} |
||||
|
|
||||
|
.card-header { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
:deep(.el-card .el-card__header, .el-card .el-card__body) { |
||||
|
padding: 15px !important; |
||||
|
} |
||||
|
|
||||
|
.profile-tabs > .el-tabs__content { |
||||
|
padding: 32px; |
||||
|
font-weight: 600; |
||||
|
color: #6b778c; |
||||
|
} |
||||
|
|
||||
|
.el-tabs--left .el-tabs__content { |
||||
|
height: 100%; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,28 @@ |
|||||
|
<template> |
||||
|
<div></div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
defineOptions({ name: 'Redirect' }) |
||||
|
|
||||
|
const { currentRoute, replace } = useRouter() |
||||
|
const { params, query } = unref(currentRoute) |
||||
|
const { path, _redirect_type = 'path' } = params |
||||
|
|
||||
|
Reflect.deleteProperty(params, '_redirect_type') |
||||
|
Reflect.deleteProperty(params, 'path') |
||||
|
|
||||
|
const _path = Array.isArray(path) ? path.join('/') : path |
||||
|
|
||||
|
if (_redirect_type === 'name') { |
||||
|
replace({ |
||||
|
name: _path, |
||||
|
query, |
||||
|
params |
||||
|
}) |
||||
|
} else { |
||||
|
replace({ |
||||
|
path: _path.startsWith('/') ? _path : '/' + _path, |
||||
|
query |
||||
|
}) |
||||
|
} |
||||
|
</script> |
Loading…
Reference in new issue