feat: add vue3(element-plus)

This commit is contained in:
xingyu
2022-07-18 19:06:37 +08:00
parent c6b58dca52
commit 80a3ae8d74
423 changed files with 41039 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const errorClick = () => {
push('/')
}
</script>
<template>
<Error type="403" @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const errorClick = () => {
push('/')
}
</script>
<template>
<Error @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { Error } from '@/components/Error'
import { useRouter } from 'vue-router'
const { push } = useRouter()
const errorClick = () => {
push('/')
}
</script>
<template>
<Error type="500" @error-click="errorClick" />
</template>

View File

@@ -0,0 +1,316 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { Echart } from '@/components/Echart'
import { useI18n } from '@/hooks/web/useI18n'
import { CountTo } from '@/components/CountTo'
import type { AnalysisTotalTypes } from './types'
import { useDesign } from '@/hooks/web/useDesign'
import { ElRow, ElCol, ElCard, ElSkeleton } from 'element-plus'
import { pieOptions, barOptions, lineOptions } from './echarts-data'
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>
<template>
<el-row :gutter="20" justify="space-between" :class="prefixCls">
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated :rows="2">
<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 icon="svg-icon:peoples" :size="40" />
</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
class="text-20px font-700 text-right"
:start-val="0"
:end-val="102400"
:duration="2600"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated :rows="2">
<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 icon="svg-icon:message" :size="40" />
</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
class="text-20px font-700 text-right"
:start-val="0"
:end-val="81212"
:duration="2600"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated :rows="2">
<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 icon="svg-icon:money" :size="40" />
</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
class="text-20px font-700 text-right"
:start-val="0"
:end-val="9280"
:duration="2600"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="6" :lg="6" :md="12" :sm="12" :xs="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated :rows="2">
<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 icon="svg-icon:shopping" :size="40" />
</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
class="text-20px font-700 text-right"
:start-val="0"
:end-val="13600"
:duration="2600"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<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-20px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="300" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="300" />
</el-skeleton>
</el-card>
</el-col>
<el-col :span="24">
<el-card shadow="hover" class="mb-20px">
<el-skeleton :loading="loading" animated :rows="4">
<Echart :options="lineOptionsData" :height="350" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<style lang="less" 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>

View File

@@ -0,0 +1,371 @@
<script setup lang="ts">
import { useTimeAgo } from '@/hooks/web/useTimeAgo'
import { ElRow, ElCol, ElSkeleton, ElCard, ElDivider, ElLink } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { ref, reactive } from 'vue'
import { CountTo } from '@/components/CountTo'
import { formatTime } from '@/utils'
import { Echart } from '@/components/Echart'
import { EChartsOption } from 'echarts'
import { radarOption } from './echarts-data'
import { Highlight } from '@/components/Highlight'
import type { WorkplaceTotal, Project, Dynamic, Team } from './types'
import { set } from 'lodash-es'
import { useCache } from '@/hooks/web/useCache'
const { t } = useI18n()
const { wsCache } = useCache()
const loading = ref(true)
const avatar = wsCache.get('user').user.avatar
const username = wsCache.get('user').user.nickname
// 获取统计数
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 dynamics = reactive<Dynamic[]>([])
const getDynamic = async () => {
const data = [
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
},
{
keys: ['workplace.push', 'Github'],
time: new Date()
}
]
dynamics = Object.assign(dynamics, data)
}
// 获取团队
let team = reactive<Team[]>([])
const getTeam = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill'
},
{
name: 'Vue',
icon: 'logos:vue'
},
{
name: 'Angular',
icon: 'logos:angular-icon'
},
{
name: 'React',
icon: 'logos:react'
},
{
name: 'Webpack',
icon: 'logos:webpack'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite'
}
]
team = Object.assign(team, data)
}
// 获取指数
let radarOptionData = reactive<EChartsOption>(radarOption) as EChartsOption
const getRadar = async () => {
const data = [
{ name: 'workplace.quote', max: 65, personal: 42, team: 50 },
{ name: 'workplace.contribution', max: 160, personal: 30, team: 140 },
{ name: 'workplace.hot', max: 300, personal: 20, team: 28 },
{ name: 'workplace.yield', max: 130, personal: 35, team: 35 },
{ name: 'workplace.follow', max: 100, personal: 80, team: 90 }
]
set(
radarOptionData,
'radar.indicator',
data.map((v) => {
return {
name: t(v.name),
max: v.max
}
})
)
set(radarOptionData, 'series', [
{
name: '指数',
type: 'radar',
data: [
{
value: data.map((v) => v.personal),
name: t('workplace.personal')
},
{
value: data.map((v) => v.team),
name: t('workplace.team')
}
]
}
])
}
const getAllApi = async () => {
await Promise.all([getCount(), getProject(), getDynamic(), getTeam(), getRadar()])
loading.value = false
}
getAllApi()
</script>
<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="w-70px h-70px rounded-[50%] mr-20px" />
<div>
<div class="text-20px text-700">
{{ t('workplace.goodMorning') }} {{ 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="flex h-70px items-center justify-end <sm:mt-20px">
<div class="px-8px text-right">
<div class="text-14px text-gray-400 mb-20px">{{ 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="text-14px text-gray-400 mb-20px">{{ 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="text-14px text-gray-400 mb-20px">{{ 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-20px" :gutter="20" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-20px">
<el-card shadow="never">
<template #header>
<div class="flex justify-between">
<span>{{ t('workplace.project') }}</span>
<ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
</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="12"
: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 text-12px text-gray-400 flex justify-between">
<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-20px">
<template #header>
<div class="flex justify-between">
<span>{{ t('workplace.dynamic') }}</span>
<ElLink type="primary" :underline="false">{{ t('workplace.more') }}</ElLink>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in dynamics" :key="`dynamics-${index}`">
<div class="flex items-center">
<img :src="avatar" alt="" class="w-35px h-35px rounded-[50%] mr-20px" />
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ username }} {{ t('workplace.pushCode') }}
</Highlight>
</div>
<div class="mt-15px text-12px text-gray-400">
{{ useTimeAgo(item.time) }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-20px">
<el-card shadow="never">
<template #header>
<span>{{ t('workplace.shortcutOperation') }}</span>
</template>
<el-skeleton :loading="loading" animated>
<el-col
v-for="item in 9"
:key="`card-${item}`"
:xl="12"
:lg="12"
:md="12"
:sm="24"
:xs="24"
class="mb-10px"
>
<ElLink type="default" :underline="false">
{{ t('workplace.operation') }}{{ item }}
</ElLink>
</el-col>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-20px">
<template #header>
<span>{{ t('workplace.index') }}</span>
</template>
<el-skeleton :loading="loading" animated>
<Echart :options="radarOptionData" :height="400" />
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-20px">
<template #header>
<span>{{ t('workplace.team') }}</span>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in team" :key="`team-${item.name}`" :span="12" class="mb-20px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-10px" />
<ElLink type="default" :underline="false">
{{ item.name }}
</ElLink>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>

View File

@@ -0,0 +1,310 @@
import { EChartsOption } from 'echarts'
import { EChartsOption as EChartsWordOption } from 'echarts-wordcloud'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
title: {
text: t('analysis.monthlySales'),
left: 'center'
},
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: [t('analysis.estimate'), t('analysis.actual')],
top: 50
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
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',
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: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
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: EChartsWordOption = {
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
}
]
}
]
}

View File

@@ -0,0 +1,52 @@
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 Dynamic = {
keys: string[]
time: Date | number | string
}
export type Team = {
name: string
icon: 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
}

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { LoginForm } from './components'
import { MobileForm } from './components'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { LocaleDropdown } from '@/components/LocaleDropdown'
import { useI18n } from '@/hooks/web/useI18n'
import { underlineToHump } from '@/utils'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('login')
const appStore = useAppStore()
const { t } = useI18n()
</script>
<template>
<div
:class="prefixCls"
class="h-[100%] relative overflow-hidden <xl:bg-v-dark <sm:px-10px <xl:px-10px <md:px-10px"
>
<div class="relative h-full flex mx-auto">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px <xl:hidden`"
>
<!-- 左上角的 logo + 系统标题 -->
<div class="flex items-center relative text-white">
<img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<!-- 左边的背景图 + 欢迎语 -->
<div class="flex justify-center items-center h-[calc(100%-60px)]">
<TransitionGroup
appear
tag="div"
enter-active-class="animate__animated animate__bounceInLeft"
>
<img src="@/assets/svgs/login-box-bg.svg" key="1" alt="" class="w-350px" />
<div class="text-3xl text-white" key="2">{{ t('login.welcome') }}</div>
<div class="mt-5 font-normal text-white text-14px" key="3">
{{ t('login.message') }}
</div>
</TransitionGroup>
</div>
</div>
<div class="flex-1 p-30px <sm:p-10px dark:bg-v-dark relative">
<!-- 右上角的主题语言选择 -->
<div class="flex justify-between items-center text-white @2xl:justify-end @xl:justify-end">
<div class="flex items-center @2xl:hidden @xl:hidden">
<img src="@/assets/imgs/logo.png" alt="" class="w-48px h-48px mr-10px" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
</div>
<div class="flex justify-end items-center space-x-10px">
<ThemeSwitch />
<LocaleDropdown class="<xl:text-white dark:text-white" />
</div>
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="h-full flex items-center m-auto w-[100%] @2xl:max-w-500px @xl:max-w-500px @md:max-w-500px @lg:max-w-500px"
>
<!-- 账号登录 -->
<LoginForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" />
<!-- 手机登录 -->
<MobileForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" />
</div>
</Transition>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-login';
.@{prefix-cls} {
&__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>

View File

@@ -0,0 +1,281 @@
<script lang="ts" setup>
import { useIcon } from '@/hooks/web/useIcon'
import LoginFormTitle from './LoginFormTitle.vue'
import {
ElForm,
ElFormItem,
ElInput,
ElCheckbox,
ElCol,
ElLink,
ElRow,
ElDivider
} from 'element-plus'
import { reactive, ref, unref, onMounted, computed, watch } from 'vue'
import { getCodeImgApi, getTenantIdByNameApi, loginApi, getAsyncRoutesApi } from '@/api/login'
import { setToken } from '@/utils/auth'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useCache } from '@/hooks/web/useCache'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { Icon } from '@/components/Icon'
import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
const { currentRoute, addRoute, push } = useRouter()
const permissionStore = usePermissionStore()
const userStore = useUserStoreWithOut()
const formLogin = ref()
const { validForm } = useFormValid(formLogin)
const { wsCache } = useCache()
const { setLoginState, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
const iconSize = 30
const iconColor = '#999'
const redirect = ref<string>('')
const { t } = useI18n()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const remember = ref(false)
const LoginRules = {
tenantName: [required],
username: [required],
password: [required],
code: [required]
}
const loginLoading = ref(false)
const loginData = reactive({
codeImg: '',
isShowPassword: false,
captchaEnable: true,
tenantEnable: true,
token: '',
loading: {
signIn: false
},
loginForm: {
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
code: '',
uuid: ''
}
})
// 获取验证码
const getCode = async () => {
const res = await getCodeImgApi()
loginData.codeImg = 'data:image/gif;base64,' + res.img
loginData.loginForm.uuid = res.uuid
}
//获取租户ID
const getTenantId = async () => {
const res = await getTenantIdByNameApi(loginData.loginForm.tenantName)
wsCache.set('tenantId', res)
}
// 登录
const handleLogin = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
loginLoading.value = true
await loginApi(loginData.loginForm)
.then(async (res) => {
setToken(res)
await userStore.getUserInfoAction()
await getRoutes()
})
.catch(() => {
getCode()
})
.finally(() => {
loginLoading.value = false
})
}
// 获取路由
const getRoutes = async () => {
// 后端过滤菜单
const routers = await getAsyncRoutesApi()
wsCache.set('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
onMounted(() => {
getCode()
})
</script>
<template>
<el-form
:model="loginData.loginForm"
:rules="LoginRules"
label-position="top"
class="login-form"
label-width="120px"
size="large"
v-show="getShow"
ref="formLogin"
>
<el-row style="maring-left: -10px; maring-right: -10px">
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="tenantName">
<el-input
type="text"
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="username">
<el-input
v-model="loginData.loginForm.username"
:placeholder="t('login.usernamePlaceholder')"
:prefix-icon="iconAvatar"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="password">
<el-input
v-model="loginData.loginForm.password"
type="password"
:placeholder="t('login.password')"
show-password
@keyup.enter="handleLogin"
:prefix-icon="iconLock"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="code">
<el-row justify="space-between" style="width: 100%">
<el-col :span="14">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.code')"
@keyup.enter="handleLogin"
:prefix-icon="iconCircleCheck"
style="width: 90%"
/>
</el-col>
<el-col :span="10">
<div class="login-code">
<img :src="loginData.codeImg" @click="getCode" class="login-code-img" alt="" />
</div>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col
:span="24"
style="padding-left: 10px; padding-right: 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="remember">{{ t('login.remember') }}</el-checkbox>
</el-col>
<el-col :span="12" :offset="6">
<el-link type="primary" style="float: right">{{ t('login.forgetPassword') }}</el-link>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="handleLogin">{{
t('login.login')
}}</el-button>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<el-row justify="space-between" style="width: 100%" :gutter="5">
<el-col :span="8">
<el-button class="w-[100%]" @click="setLoginState(LoginStateEnum.MOBILE)">{{
t('login.btnMobile')
}}</el-button>
</el-col>
<el-col :span="8">
<el-button class="w-[100%]">{{ t('login.btnQRCode') }}</el-button>
</el-col>
<el-col :span="8">
<el-button class="w-[100%]">{{ t('login.btnRegister') }}</el-button>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<div class="flex justify-between w-[100%]">
<Icon
icon="ant-design:github-filled"
:size="iconSize"
class="cursor-pointer anticon"
:color="iconColor"
/>
<Icon
icon="ant-design:wechat-filled"
:size="iconSize"
class="cursor-pointer anticon"
:color="iconColor"
/>
<Icon
icon="ant-design:alipay-circle-filled"
:size="iconSize"
:color="iconColor"
class="cursor-pointer anticon"
/>
<Icon
icon="ant-design:weibo-circle-filled"
:size="iconSize"
:color="iconColor"
class="cursor-pointer anticon"
/>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<style lang="less" scoped>
.login-code {
width: 100%;
height: 38px;
float: right;
img {
cursor: pointer;
width: 100%;
max-width: 100px;
height: auto;
vertical-align: middle;
}
}
</style>

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import { reactive, ref, unref, watch, onMounted } from 'vue'
import { Form } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { ElCheckbox, ElLink } from 'element-plus'
import { required } from '@/utils/formRules'
import { useForm } from '@/hooks/web/useForm'
import { getTenantIdByNameApi, getCodeImgApi, loginApi, getAsyncRoutesApi } from '@/api/login'
import { useCache } from '@/hooks/web/useCache'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import { setToken } from '@/utils/auth'
import { useUserStoreWithOut } from '@/store/modules/user'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { UserLoginVO } from '@/api/login/types'
const { wsCache } = useCache()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStore()
const { currentRoute, addRoute, push } = useRouter()
const { t } = useI18n()
const rules = {
tenantName: [required],
username: [required],
password: [required],
code: [required]
}
const loginData = reactive({
codeImg: '',
isShowPassword: false,
captchaEnable: true,
tenantEnable: true,
token: '',
loading: {
signIn: false
},
loginForm: {
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
code: '',
uuid: ''
}
})
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
}
},
{
field: 'tenantName',
label: t('login.tenantname'),
value: loginData.loginForm.tenantName,
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: t('login.tenantNamePlaceholder')
}
},
{
field: 'username',
label: t('login.username'),
value: loginData.loginForm.username,
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: t('login.usernamePlaceholder')
}
},
{
field: 'password',
label: t('login.password'),
value: loginData.loginForm.password,
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
placeholder: t('login.passwordPlaceholder')
}
},
{
field: 'code',
label: t('login.code'),
value: loginData.loginForm.code,
component: 'Input',
colProps: {
span: 12
},
componentProps: {
style: {
width: '100%'
},
placeholder: t('login.codePlaceholder')
}
},
{
field: 'codeImg',
colProps: {
span: 12
}
},
{
field: 'tool',
colProps: {
span: 24
}
},
{
field: 'login',
colProps: {
span: 24
}
},
{
field: 'other',
component: 'Divider',
label: t('login.otherLogin'),
componentProps: {
contentPosition: 'center'
}
},
{
field: 'otherIcon',
colProps: {
span: 24
}
}
])
const iconSize = 30
const remember = ref(false)
const { register, elFormRef, methods } = useForm()
const loading = ref(false)
const iconColor = '#999'
const redirect = ref<string>('')
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// 获取验证码
const getCode = async () => {
const res = await getCodeImgApi()
loginData.codeImg = 'data:image/gif;base64,' + res.img
loginData.loginForm.uuid = res.uuid
}
//获取租户ID
const getTenantId = async () => {
const res = await getTenantIdByNameApi(loginData.loginForm.tenantName)
wsCache.set('tenantId', res)
}
// 登录
const signIn = async () => {
await getTenantId()
const formRef = unref(elFormRef)
await formRef?.validate(async (isValid) => {
if (isValid) {
loading.value = true
const { getFormData } = methods
const formData = await getFormData<UserLoginVO>()
formData.uuid = loginData.loginForm.uuid
await loginApi(formData)
.then(async (res) => {
setToken(res)
getRoutes()
await userStore.getUserInfoAction()
})
.catch(() => {
getCode()
})
.finally(() => (loading.value = false))
}
})
}
// 获取路由
const getRoutes = async () => {
// 后端过滤菜单
const routers = await getAsyncRoutesApi()
wsCache.set('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
onMounted(() => {
getCode()
})
</script>
<template>
<Form
:schema="schema"
:rules="rules"
label-position="top"
hide-required-asterisk
size="large"
@register="register"
v-show="false"
>
<template #header>
<h2 class="text-2xl font-bold text-center w-[100%]">{{ t('login.login') }}</h2>
</template>
<template #codeImg>
<img :src="loginData.codeImg" @click="getCode" alt="" />
</template>
<template #tool>
<div class="flex justify-between items-center w-[100%]">
<ElCheckbox v-model="remember" :label="t('login.remember')" size="small" />
<ElLink type="primary" :underline="false">{{ t('login.forgetPassword') }}</ElLink>
</div>
</template>
<template #login>
<ElButton :loading="loading" type="primary" class="w-[100%]" @click="signIn">
{{ t('login.login') }}
</ElButton>
</template>
<template #otherIcon>
<div class="flex justify-between w-[100%]">
<Icon
icon="ant-design:github-filled"
:size="iconSize"
class="cursor-pointer anticon"
:color="iconColor"
/>
<Icon
icon="ant-design:wechat-filled"
:size="iconSize"
class="cursor-pointer anticon"
:color="iconColor"
/>
<Icon
icon="ant-design:alipay-circle-filled"
:size="iconSize"
:color="iconColor"
class="cursor-pointer anticon"
/>
<Icon
icon="ant-design:weibo-circle-filled"
:size="iconSize"
:color="iconColor"
class="cursor-pointer anticon"
/>
</div>
</template>
</Form>
</template>
<style lang="less" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
</style>

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import { computed, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { LoginStateEnum, useLoginState } from './useLogin'
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')
}
return titleObj[unref(getLoginState)]
})
</script>
<template>
<h2 class="mb-3 text-2xl font-bold text-center xl:text-3xl enter-x xl:text-center">
{{ getFormTitle }}
</h2>
</template>

View File

@@ -0,0 +1,248 @@
<script setup lang="ts">
import { useIcon } from '@/hooks/web/useIcon'
import { reactive, ref, unref, watch, onMounted, computed } from 'vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { ElForm, ElFormItem, ElInput, ElRow, ElCol, ElMessage } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import {
getTenantIdByNameApi,
getCodeImgApi,
getAsyncRoutesApi,
sendSmsCodeApi,
smsLoginApi
} from '@/api/login'
import { useCache } from '@/hooks/web/useCache'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import { setToken } from '@/utils/auth'
import { useUserStoreWithOut } from '@/store/modules/user'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { useLoginState, LoginStateEnum, useFormValid } from './useLogin'
const formSmsLogin = ref()
const { validForm } = useFormValid(formSmsLogin)
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { wsCache } = useCache()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStore()
const { currentRoute, addRoute, push } = useRouter()
const loginLoading = ref(false)
const { t } = useI18n()
const rules = {
tenantName: [required],
mobileNumber: [required],
code: [required]
}
const loginData = reactive({
codeImg: '',
// TODO @jinz多余的变量 isShowPassword、captchaEnable
isShowPassword: false,
captchaEnable: true,
tenantEnable: true,
token: '',
loading: {
signIn: false
},
loginForm: {
uuid: '',
tenantName: '芋道源码',
mobileNumber: '',
code: ''
}
})
// TODO @jinzsmsVO 小写哈
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
console.log('getSmsCode begin:', SmsVO.smsCode)
await sendSmsCodeApi(SmsVO.smsCode)
.then(async (res) => {
// 提示验证码发送成功
ElMessage({
type: 'success',
message: t('login.SmsSendMsg')
})
console.log('res', res)
// 设置倒计时
mobileCodeTimer.value = 60
let msgTimer = setInterval(() => {
mobileCodeTimer.value = mobileCodeTimer.value - 1
if (mobileCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
.catch(() => {
console.log('error')
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
// 获取验证码 TODO @jinz是不是可以去掉手机这里暂时不用验证码
const getCode = async () => {
const res = await getCodeImgApi()
loginData.codeImg = 'data:image/gif;base64,' + res.img
loginData.loginForm.uuid = res.uuid
}
// 获取租户 ID
const getTenantId = async () => {
const res = await getTenantIdByNameApi(loginData.loginForm.tenantName)
wsCache.set('tenantId', res)
}
// 登录
const signIn = async () => {
await getTenantId()
const data = await validForm()
if (!data) return
loginLoading.value = true
SmsVO.loginSms.mobile = loginData.loginForm.mobileNumber
SmsVO.loginSms.code = loginData.loginForm.code
await smsLoginApi(SmsVO.loginSms)
.then(async (res) => {
setToken(res?.token)
await userStore.getUserInfoAction()
getRoutes()
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
})
}
// 获取路由
const getRoutes = async () => {
// 后端过滤菜单
// TODO @jinz这块 getRoutes 的代码,是不是可以统一到 store 里,类似 ruoyi-vue 的做法,可能要找作者沟通下
const routers = await getAsyncRoutesApi()
wsCache.set('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
onMounted(() => {
getCode()
})
</script>
<template>
<el-form
:model="loginData.loginForm"
:rules="rules"
label-position="top"
class="login-form"
label-width="120px"
size="large"
v-show="getShow"
ref="formSmsLogin"
>
<el-row style="margin-left: -10px; margin-right: -10px">
<!-- 租户名 -->
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="tenantName">
<el-input
type="text"
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
/>
</el-form-item>
</el-col>
<!-- 手机号 -->
<el-col :span="24" style="padding-left: 10px; padding-right: 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-left: 10px; padding-right: 10px">
<el-form-item prop="code">
<el-row justify="space-between" style="width: 100%" :gutter="5">
<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"
@click="getSmsCode"
class="getMobileCode"
style="cursor: pointer"
>
{{ 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-left: 10px; padding-right: 10px">
<el-form-item>
<el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="signIn">
{{ t('login.login') }}
</el-button>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<el-button :loading="loginLoading" class="w-[100%]" @click="handleBackLogin">
{{ t('login.backLogin') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<style lang="less" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>

View File

@@ -0,0 +1,4 @@
export { default as LoginForm } from './LoginForm.vue'
export { default as MobileForm } from './MobileForm.vue'
export { default as LoginFormOld } from './LoginFormOld.vue' // TODO jinzold 是不是可以删除哈git 可以管理的
export { default as LoginFormTitle } from './LoginFormTitle.vue'

View File

@@ -0,0 +1,136 @@
import { ref, computed, unref, Ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE
}
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
}
}
// TODO @jinz多余的是不是可以删除哈
export function useFormRules(formData?: Recordable) {
const { t } = useI18n()
const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')))
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')))
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')))
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')))
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve()
}
const validateConfirmPassword = (password: string) => {
return async (_: RuleObject, value: string) => {
if (!value) {
return Promise.reject(t('sys.login.passwordPlaceholder'))
}
if (value !== password) {
return Promise.reject(t('sys.login.diffPwd'))
}
return Promise.resolve()
}
}
const getFormRules = computed(
(): {
[k: string]: ValidationRule | ValidationRule[]
} => {
const accountFormRule = unref(getAccountFormRule)
const passwordFormRule = unref(getPasswordFormRule)
const smsFormRule = unref(getSmsFormRule)
const mobileFormRule = unref(getMobileFormRule)
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule
}
switch (unref(currentState)) {
// register form rules
case LoginStateEnum.REGISTER:
return {
account: accountFormRule,
password: passwordFormRule,
confirmPassword: [
{
validator: validateConfirmPassword(formData?.password),
trigger: 'change'
}
],
policy: [
{
validator: validatePolicy,
trigger: 'change'
}
],
...mobileRule
}
// reset password form rules
case LoginStateEnum.RESET_PASSWORD:
return {
account: accountFormRule,
...mobileRule
}
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule
// login form rules
default:
return {
account: accountFormRule,
password: passwordFormRule
}
}
}
)
return {
getFormRules
}
}
function createRule(message: string) {
return [
{
required: true,
message,
trigger: 'change'
}
]
}

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { ElCard, ElTabs, ElTabPane } from 'element-plus'
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/'
const { t } = useI18n()
const activeName = ref('basicInfo')
</script>
<template>
<div class="flex">
<el-card class="w-1/3 user" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ t('profile.user.title') }}</span>
</div>
</template>
<ProfileUser />
</el-card>
<el-card class="w-2/3 user" style="margin-left: 10px" 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>
<style scoped>
.user {
max-height: 960px;
padding: 15px 20px 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;
color: #6b778c;
font-weight: 600;
}
.el-tabs--left .el-tabs__content {
height: 100%;
}
</style>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormRules, FormInstance } from 'element-plus'
import { ElForm, ElFormItem, ElInput, ElRadioGroup, ElRadio, ElMessage } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { getUserProfileApi, updateUserProfileApi } from '@/api/system/user/profile'
const { t } = useI18n()
const formRef = ref<FormInstance>()
interface BasicUserInfoVO {
id: number
nickname: string
email: string
mobile: string
sex: number
}
interface userInfoType {
basicUserInfo: BasicUserInfoVO
}
const user = reactive<userInfoType>({
basicUserInfo: {
id: 0,
nickname: '',
mobile: '',
email: '',
sex: 0
}
})
// 表单校验
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 submit = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
await updateUserProfileApi({ params: user.basicUserInfo })
ElMessage.success(t('common.updateSuccess'))
}
})
}
const reset = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await getUserInfo()
}
const getUserInfo = async () => {
const users = await getUserProfileApi()
user.basicUserInfo = users
}
onMounted(async () => {
await getUserInfo()
})
</script>
<template>
<el-form ref="form" :model="user.basicUserInfo" :rules="rules" label-width="80px">
<el-form-item :label="t('profile.user.nickname')" prop="nickname">
<el-input v-model="user.basicUserInfo.nickname" />
</el-form-item>
<el-form-item :label="t('profile.user.mobile')" prop="mobile">
<el-input v-model="user.basicUserInfo.mobile" maxlength="11" />
</el-form-item>
<el-form-item :label="t('profile.user.email')" prop="email">
<el-input v-model="user.basicUserInfo.email" maxlength="50" />
</el-form-item>
<el-form-item :label="t('profile.user.sex')" prop="sex">
<el-radio-group v-model="user.basicUserInfo.sex">
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio>
<el-radio :label="2">{{ t('profile.user.woman') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit(formRef)">{{ t('common.save') }}</el-button>
<el-button type="danger" @click="reset(formRef)">{{ t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</template>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { getUserProfileApi } from '@/api/system/user/profile'
import { onMounted, reactive } from 'vue'
import dayjs from 'dayjs'
import { UserAvatarVue } from './'
import { ProfileVO } from '@/api/system/user/profile/types'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
interface userInfoType {
user: ProfileVO
}
const userInfo = reactive<userInfoType>({
user: {
id: 0,
username: '',
nickname: '',
dept: {
id: 0,
name: ''
},
roles: [],
posts: [],
socialUsers: [],
email: '',
mobile: '',
sex: 0,
avatar: '',
status: 0,
remark: '',
loginIp: '',
loginDate: new Date(),
createTime: new Date()
}
})
const getUserInfo = async () => {
const users = await getUserProfileApi()
userInfo.user = users
}
onMounted(async () => {
await getUserInfo()
})
</script>
<template>
<div>
<div class="text-center">
<UserAvatarVue :img="userInfo.user.avatar" />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<Icon icon="ep:user" class="mr-5px" />{{ t('profile.user.username') }}
<div class="pull-right">{{ userInfo.user.username }}</div>
</li>
<li class="list-group-item">
<Icon icon="ep:phone" class="mr-5px" />{{ t('profile.user.mobile') }}
<div class="pull-right">{{ userInfo.user.mobile }}</div>
</li>
<li class="list-group-item">
<Icon icon="fontisto:email" class="mr-5px" />{{ t('profile.user.email') }}
<div class="pull-right">{{ userInfo.user.email }}</div>
</li>
<li class="list-group-item">
<Icon icon="carbon:tree-view-alt" class="mr-5px" />{{ t('profile.user.dept') }}
<div class="pull-right" v-if="userInfo.user.dept">{{ userInfo.user.dept.name }}</div>
</li>
<li class="list-group-item">
<Icon icon="ep:suitcase" class="mr-5px" />{{ t('profile.user.posts') }}
<div class="pull-right" v-if="userInfo.user.posts">
{{ userInfo.user.posts.map((post) => post.name).join(',') }}
</div>
</li>
<li class="list-group-item">
<Icon icon="icon-park-outline:peoples" class="mr-5px" />{{ t('profile.user.roles') }}
<div class="pull-right" v-if="userInfo.user.roles">
{{ userInfo.user.roles.map((role) => role.name).join(',') }}
</div>
</li>
<li class="list-group-item">
<Icon icon="ep:calendar" class="mr-5px" />{{ t('profile.user.createTime') }}
<div class="pull-right">{{ dayjs(userInfo.user.createTime).format('YYYY-MM-DD') }}</div>
</li>
</ul>
</div>
</template>
<style scoped>
.text-center {
text-align: center;
position: relative;
height: 120px;
}
.list-group-striped > .list-group-item {
border-left: 0;
border-right: 0;
border-radius: 0;
padding-left: 0;
padding-right: 0;
}
.list-group {
padding-left: 0px;
list-style: none;
}
.list-group-item {
border-bottom: 1px solid #e7eaec;
border-top: 1px solid #e7eaec;
margin-bottom: -1px;
padding: 11px 0px;
font-size: 13px;
}
.pull-right {
float: right !important;
}
</style>

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import { InputPassword } from '@/components/InputPassword'
import { ElForm, ElFormItem, ElMessage } from 'element-plus'
import type { FormRules, FormInstance } from 'element-plus'
import { updateUserPwdApi } from '@/api/system/user/profile'
import { ref, reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const formRef = ref<FormInstance>()
const password = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 表单校验
const equalToPassword = (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: 3, max: 5, 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 updateUserPwdApi(password.oldPassword, password.newPassword)
ElMessage.success(t('common.updateSuccess'))
}
})
}
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<template>
<el-form ref="formRef" :model="password" :rules="rules" label-width="80px">
<el-form-item :label="t('profile.password.oldPassword')">
<InputPassword v-model="password.oldPassword" />
</el-form-item>
<el-form-item :label="t('profile.password.newPassword')">
<InputPassword v-model="password.newPassword" strength />
</el-form-item>
<el-form-item :label="t('profile.password.confirmPassword')">
<InputPassword v-model="password.confirmPassword" strength />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit(formRef)">{{ t('common.save') }}</el-button>
<el-button type="danger" @click="reset(formRef)">{{ t('common.reset') }}</el-button>
</el-form-item>
</el-form>
</template>

View File

@@ -0,0 +1,202 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'
import { ElRow, ElCol, ElUpload, ElMessage } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { uploadAvatarApi } from '@/api/system/user/profile'
const cropper = ref()
const props = defineProps({
img: propTypes.string.def('')
})
const state = reactive({
dialogVisible: false,
cropperVisible: false,
dialogTitle: '编辑头像',
options: {
img: props.img, //裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true // 固定截图框大小 不允许改变
},
previews: {
img: '',
url: ''
}
})
/** 编辑头像 */
const editCropper = () => {
state.dialogVisible = true
state.cropperVisible = true
}
/** 覆盖默认上传行为 */
const requestUpload = () => {}
/** 向左旋转 */
const rotateLeft = () => {
cropper.value.rotateLeft()
}
/** 向右旋转 */
const rotateRight = () => {
cropper.value.rotateRight()
}
/** 图片缩放 */
const changeScale = (num: number) => {
num = num || 1
cropper.value.changeScale(num)
}
/** 上传预处理 */
const beforeUpload = (file: Blob) => {
if (file.type.indexOf('image/') == -1) {
ElMessage('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
} else {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
state.options.img = reader.result
}
}
}
/** 上传图片 */
const uploadImg = () => {
cropper.value.getCropBlob((data) => {
let formData = new FormData()
formData.append('avatarfile', data)
uploadAvatarApi(formData)
})
}
/** 实时预览 */
const realTime = (data) => {
state.previews = data
}
watch(
() => props.img,
() => {
if (props.img) {
state.options.img = props.img
state.previews.img = props.img
state.previews.url = props.img
}
}
)
</script>
<template>
<div class="user-info-head" @click="editCropper()">
<img :src="state.options.img" title="点击上传头像" class="img-circle img-lg" alt="" />
</div>
<Dialog
v-model="state.dialogVisible"
:title="state.dialogTitle"
width="50%"
:maxHeight="350"
style="padding: 30px 20px"
>
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<VueCropper
ref="cropper"
:img="state.options.img"
:info="true"
:autoCrop="state.options.autoCrop"
:autoCropWidth="state.options.autoCropWidth"
:autoCropHeight="state.options.autoCropHeight"
:fixedBox="state.options.fixedBox"
@real-time="realTime"
v-if="state.cropperVisible"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<div class="avatar-upload-preview">
<img
:src="state.previews.url"
:style="state.previews.img"
style="!max-width: 100%"
alt=""
/>
</div>
</el-col>
</el-row>
<template #footer>
<el-row>
<el-col :lg="2" :md="2">
<el-upload
action="#"
:http-request="requestUpload"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-button size="small">
<Icon icon="ep:upload-filled" class="mr-5px" />
选择
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button size="small" @click="changeScale(1)">
<Icon icon="ep:zoom-in" class="mr-5px" />
</el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button size="small" @click="changeScale(-1)">
<Icon icon="ep:zoom-out" class="mr-5px" />
</el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button size="small" @click="rotateLeft()">
<Icon icon="ep:arrow-left-bold" class="mr-5px" />
</el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button size="small" @click="rotateRight()">
<Icon icon="ep:arrow-right-bold" class="mr-5px" />
</el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button size="small" type="primary" @click="uploadImg()"> </el-button>
</el-col>
</el-row>
</template>
</Dialog>
</template>
<style scoped>
.user-info-head {
position: relative;
display: inline-block;
}
.img-circle {
border-radius: 50%;
}
.img-lg {
width: 120px;
height: 120px;
}
.avatar-upload-preview {
position: absolute;
top: 50%;
-webkit-transform: translate(50%, -50%);
transform: translate(50%, -50%);
width: 200px;
height: 200px;
border-radius: 50%;
-webkit-box-shadow: 0 0 4px #ccc;
box-shadow: 0 0 4px #ccc;
overflow: hidden;
}
.user-info-head:hover:after {
content: '+';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #eee;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 110px;
border-radius: 50%;
}
</style>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { ElTable, ElTableColumn } from 'element-plus'
import { onMounted, reactive } from 'vue'
interface sociaType {
title: string
type: string
source: string
img: string
}
interface socialUserType {
socialUser: {
socia: sociaType[]
}
}
const state = reactive<socialUserType>({
socialUser: {
socia: []
}
})
const initSocial = () => {
console.info(1)
}
const bind = () => {
console.info(1)
}
const unbind = () => {
console.info(1)
}
onMounted(async () => {
await initSocial()
})
</script>
<template>
<el-table :data="state.socialUser.socia" :show-header="false">
<el-table-column label="社交平台" align="left" width="120" prop="socia">
<template #socia="{ row }">
<img style="height: 20px; vertical-align: middle" :src="row.img" alt="" />
{{ row.title }}
</template>
</el-table-column>
<el-table-column label="操作" align="left" prop="action">
<template #action="{ row }">
<div v-if="row.openid">
已绑定
<el-button link type="primary" @click="unbind()">(解绑)</el-button>
</div>
<div v-else>
未绑定
<el-button link type="primary" @click="bind()">(绑定)</el-button>
</div>
</template>
</el-table-column>
</el-table>
</template>

View File

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

View File

@@ -0,0 +1,30 @@
<template>
<div></div>
</template>
<script setup lang="ts">
import { unref } from 'vue'
import { useRouter } from 'vue-router'
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index'
},
{
label: '链路追踪',
field: 'traceId'
},
{
label: '用户编号',
field: 'userId',
search: {
show: true
}
},
{
label: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
search: {
show: true
}
},
{
label: '应用名',
field: 'applicationName',
search: {
show: true
}
},
{
label: '请求方法名',
field: 'requestMethod'
},
{
label: '请求地址',
field: 'requestUrl',
search: {
show: true
}
},
{
label: '请求时间',
field: 'beginTime'
},
{
label: '执行时长',
field: 'duration'
},
{
label: '操作结果',
field: 'resultCode',
search: {
show: true
}
},
{
label: t('table.action'),
field: 'action',
width: '300px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import type { ApiAccessLogVO } from '@/api/infra/apiAccessLog/types'
import { allSchemas } from './apiAccessLog.data'
import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<ApiAccessLogVO>, ApiAccessLogVO>({
getListApi: ApiAccessLogApi.getApiAccessLogPageApi
})
const { getList, setSearchParams } = methods
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// 详情操作
const handleDetail = (row: ApiAccessLogVO) => {
// 设置数据
detailRef.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #beginTime="{ row }">
<span>{{ dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:api-access-log:query']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #beginTime="{ row }">
<span>{{ dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败(' + row.resultMsg + ')' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,87 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index'
},
{
label: '链路追踪',
field: 'traceId'
},
{
label: '用户编号',
field: 'userId',
search: {
show: true
}
},
{
label: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
search: {
show: true
}
},
{
label: '应用名',
field: 'applicationName',
search: {
show: true
}
},
{
label: '请求方法名',
field: 'requestMethod'
},
{
label: '请求地址',
field: 'requestUrl',
search: {
show: true
}
},
{
label: '异常发生时间',
field: 'exceptionTime'
},
{
label: '异常名',
field: 'exceptionName'
},
{
label: '处理状态',
field: 'processStatus',
dictType: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
search: {
show: true
}
},
{
label: '处理人',
field: 'processUserId'
},
{
label: '处理时间',
field: 'processTime'
},
{
label: t('table.action'),
field: 'action',
width: '300px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import type { ApiErrorLogVO } from '@/api/infra/apiErrorLog/types'
import { allSchemas } from './apiErrorLog.data'
import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<ApiErrorLogVO>, ApiErrorLogVO>({
getListApi: ApiErrorLogApi.getApiErrorLogPageApi,
exportListApi: ApiErrorLogApi.exportApiErrorLogApi
})
const { getList, setSearchParams, exportList } = methods
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// 导出操作
const handleExport = async () => {
await exportList('用户数据.xls')
}
// 详情操作
const handleDetail = (row: ApiErrorLogVO) => {
// 设置数据
detailRef.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// 异常处理操作
const handleProcessClick = (row: ApiErrorLogVO, processSttatus: number, type: string) => {
ElMessageBox.confirm('确认标记为' + type + '?', t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
ApiErrorLogApi.updateApiErrorLogPageApi(row.id, processSttatus).then(() => {
ElMessage.success(t('common.updateSuccess'))
getList()
})
})
.catch(() => {})
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<el-button v-hasPermi="['infra:api-error-log:export']" @click="handleExport">
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #processStatus="{ row }">
<DictTag :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" :value="row.processStatus" />
</template>
<template #exceptionTime="{ row }">
<span>{{ dayjs(row.exceptionTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #processTime="{ row }">
<span v-if="row.processTime">{{
dayjs(row.processTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:api-error-log:export']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
v-hasPermi="['infra:api-error-log:update-status']"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.DONE, '已处理')"
>
<Icon icon="ep:cpu" class="mr-5px" /> 已处理
</el-button>
<el-button
link
type="primary"
v-if="row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
v-hasPermi="['infra:api-error-log:update-status']"
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')"
>
<Icon icon="ep:mute-notification" class="mr-5px" /> 已忽略
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #processStatus="{ row }">
<DictTag :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" :value="row.processStatus" />
</template>
<template #exceptionTime="{ row }">
<span>{{ dayjs(row.exceptionTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

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

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import { ref, unref, onMounted } from 'vue'
import { ContentDetailWrap } from '@/components/ContentDetailWrap'
import BasicInfoForm from './components/BasicInfoForm.vue'
import CloumInfoFormVue from './components/CloumInfoForm.vue'
import GenInfoFormVue from './components/GenInfoForm.vue'
import { ElTabs, ElTabPane, ElButton } from 'element-plus'
import { getCodegenTableApi } from '@/api/infra/codegen'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from '@/hooks/web/useI18n'
import { CodegenColumnVO, CodegenTableVO } from '@/api/infra/codegen/types'
const { t } = useI18n()
const { push } = useRouter()
const { query } = useRoute()
const tableCurrentRow = ref<Nullable<CodegenTableVO>>(null)
const cloumCurrentRow = ref<CodegenColumnVO[]>()
const getList = async () => {
const id = query.id as unknown as number
if (id) {
// 获取表详细信息
const res = await getCodegenTableApi(id)
tableCurrentRow.value = res.table
cloumCurrentRow.value = res.columns
}
}
const loading = ref(false)
const activeName = ref('cloum')
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
const genInfoRef = ref<ComponentRef<typeof GenInfoFormVue>>()
// TODO: 提交
const submitForm = async () => {
const basicInfo = unref(basicInfoRef)
const genInfo = unref(genInfoRef)
const basicValidate = await basicInfo?.elFormRef?.validate()?.catch(() => {})
const genValidate = await genInfo?.elFormRef?.validate()?.catch(() => {})
if (basicValidate && genValidate) {
const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
const genInfoData = (await genInfo?.getFormData()) as CodegenTableVO
console.info(basicInfoData)
console.info(genInfoData)
}
console.info(1)
}
onMounted(() => {
getList()
})
</script>
<template>
<ContentDetailWrap title="代码生成" @back="push('/infra/codegen')">
<el-tabs v-model="activeName">
<el-tab-pane label="基本信息" name="basic">
<BasicInfoForm ref="basicInfoRef" :current-row="tableCurrentRow" />
</el-tab-pane>
<el-tab-pane label="字段信息" name="cloum">
<CloumInfoFormVue ref="cloumInfoRef" :current-row="cloumCurrentRow" />
</el-tab-pane>
<el-tab-pane label="生成信息" name="genInfo">
<GenInfoFormVue ref="basicInfoRef" :current-row="tableCurrentRow" />
</el-tab-pane>
</el-tabs>
<template #right>
<el-button type="primary" :loading="loading" @click="submitForm">
{{ t('action.save') }}
</el-button>
</template>
</ContentDetailWrap>
</template>

View File

@@ -0,0 +1,74 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
title: [required],
type: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '表名称',
field: 'tableName',
search: {
show: true
}
},
{
label: '表描述',
field: 'tableComment',
search: {
show: true
}
},
{
label: '实体',
field: 'className',
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('common.updateTime'),
field: 'updateTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '500px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { PropType, reactive, watch } from 'vue'
import { required } from '@/utils/formRules'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<CodegenTableVO>>,
default: () => null
}
})
const rules = reactive({
tableName: [required],
tableComment: [required],
className: [required],
author: [required]
})
const schema = reactive<FormSchema[]>([
{
label: '表名称',
field: 'tableName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '表描述',
field: 'tableComment',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '实体类名称',
field: 'className',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '作者',
field: 'author',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '备注',
field: 'remark',
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 12
}
}
])
const { register, methods, elFormRef } = useForm({
schema
})
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
const { setValues } = methods
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
elFormRef,
getFormData: methods.getFormData
})
</script>
<template>
<Form :rules="rules" @register="register" />
</template>

View File

@@ -0,0 +1,137 @@
<script setup lang="ts">
import { ElTable, ElTableColumn, ElInput, ElSelect, ElOption, ElCheckbox } from 'element-plus'
import { onMounted, PropType, ref } from 'vue'
import { CodegenColumnVO } from '@/api/infra/codegen/types'
import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'
import { DictTypeVO } from '@/api/system/dict/types'
defineProps({
currentRow: {
type: Array as unknown as PropType<CodegenColumnVO[]>,
default: () => null
}
})
/** 查询字典下拉列表 */
const dictOptions = ref<DictTypeVO[]>()
const getDictOptions = async () => {
const res = await listSimpleDictTypeApi()
dictOptions.value = res
}
const tableHeight = document.documentElement.scrollHeight - 245 + 'px'
onMounted(async () => {
await getDictOptions()
})
</script>
<template>
<el-table ref="dragTable" :data="currentRow" row-key="columnId" :max-height="tableHeight">
<el-table-column
label="字段列名"
prop="columnName"
min-width="10%"
:show-overflow-tooltip="true"
/>
<el-table-column label="字段描述" min-width="10%">
<template #default="scope">
<el-input v-model="scope.row.columnComment" />
</template>
</el-table-column>
<el-table-column
label="物理类型"
prop="dataType"
min-width="10%"
:show-overflow-tooltip="true"
/>
<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="Date" value="Date" />
<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 true-label="true" false-label="false" v-model="scope.row.createOperation" />
</template>
</el-table-column>
<el-table-column label="编辑" min-width="4%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.updateOperation" />
</template>
</el-table-column>
<el-table-column label="列表" min-width="4%">
<template #default="scope">
<el-checkbox
true-label="true"
false-label="false"
v-model="scope.row.listOperationResult"
/>
</template>
</el-table-column>
<el-table-column label="查询" min-width="4%">
<template #default="scope">
<el-checkbox true-label="true" false-label="false" v-model="scope.row.listOperation" />
</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 true-label="true" false-label="false" v-model="scope.row.nullable" />
</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>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import { PropType, reactive, watch } from 'vue'
import { required } from '@/utils/formRules'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<CodegenTableVO>>,
default: () => null
}
})
const rules = reactive({
templateType: [required],
scene: [required],
moduleName: [required],
businessName: [required],
businessPackage: [required],
className: [required],
classComment: [required]
})
const schema = reactive<FormSchema[]>([
{
label: '生成模板',
field: 'templateType',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
},
colProps: {
span: 12
}
},
{
label: '生成场景',
field: 'scene',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
},
colProps: {
span: 12
}
},
{
label: '模块名',
field: 'moduleName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '业务名',
field: 'businessName',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '类名称',
field: 'className',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '类描述',
field: 'classComment',
component: 'Input',
colProps: {
span: 12
}
},
{
label: '上级菜单',
field: 'parentMenuId',
component: 'Input',
colProps: {
span: 12
}
}
])
const { register, methods, elFormRef } = useForm({
schema
})
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
const { setValues } = methods
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
defineExpose({
elFormRef,
getFormData: methods.getFormData
})
</script>
<template>
<Form :rules="rules" @register="register" />
</template>

View File

@@ -0,0 +1,128 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { getSchemaTableListApi, createCodegenListApi } from '@/api/infra/codegen'
import {
ElMessage,
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElInput,
ElEmpty
} from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useEmitt } from '@/hooks/web/useEmitt'
import { getDataSourceConfigListApi } from '@/api/infra/dataSourceConfig'
import type { DataSourceConfigVO } from '@/api/infra/dataSourceConfig/types'
import type { DatabaseTableVO } from '@/api/infra/codegen/types'
const { t } = useI18n() // 国际化
const { emitter } = useEmitt()
// ======== 显示页面 ========
const visible = ref(false)
const queryParams = reactive({
tableName: undefined,
tableComment: undefined,
dataSourceConfigId: 0
})
const dataSourceConfigs = ref<DataSourceConfigVO[]>([])
const show = async () => {
const res = await getDataSourceConfigListApi()
dataSourceConfigs.value = res
queryParams.dataSourceConfigId = dataSourceConfigs.value[0].id
visible.value = true
await getList()
}
/** 查询表数据 */
const dbTableList = ref<DatabaseTableVO[]>([])
/** 查询表数据 */
const getList = async () => {
const res = await getSchemaTableListApi(queryParams)
dbTableList.value = res
}
// 查询操作
const handleQuery = async () => {
await getList()
}
// 重置操作
const resetQuery = async () => {
queryParams.tableName = undefined
queryParams.tableComment = undefined
await getList()
}
/** 多选框选中数据 */
const tables = ref<string[]>([])
const handleSelectionChange = (val: DatabaseTableVO[]) => {
tables.value = val.map((item) => item.name)
}
/** 导入按钮操作 */
const handleImportTable = () => {
if (tables.value.length === 0) {
ElMessage.error('请选择要导入的表')
return
}
createCodegenListApi({
dataSourceConfigId: queryParams.dataSourceConfigId,
tableNames: tables.value
}).then((res) => {
ElMessage.success(res.msg)
visible.value = false
emitter.emit('ok')
})
}
defineExpose({
show
})
</script>
<template>
<!-- 导入表 -->
<Dialog title="导入表" v-model="visible" maxHeight="500px" width="50%">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<!-- <el-form-item label="数据源" prop="dataSourceConfigId">
<el-select v-model="queryParams.dataSourceConfigId" placeholder="请选择数据源" clearable>
<el-option
v-for="config in dataSourceConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
/>
</el-select>
</el-form-item> -->
<el-form-item label="表名称" prop="tableName">
<el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable />
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input v-model="queryParams.tableComment" placeholder="请输入表描述" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('common.query') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh-right" class="mr-5px" />
{{ t('common.reset') }}
</el-button>
</el-form-item>
</el-form>
<el-table
ref="table"
:data="dbTableList"
@selection-change="handleSelectionChange"
height="400px"
>
<template #empty>
<el-empty description="加载中" />
</template>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="表名称" :show-overflow-tooltip="true" />
<el-table-column prop="comment" label="表描述" :show-overflow-tooltip="true" />
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleImportTable">{{ t('action.import') }}</el-button>
<el-button @click="visible = false">{{ t('dialog.close') }}</el-button>
</div>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,148 @@
<script setup lang="ts">
import { reactive, ref, unref } from 'vue'
import { handleTree2 } from '@/utils/tree'
import { ElCard, ElTree, ElTabs, ElTabPane, ElMessage } from 'element-plus'
import { previewCodegenApi } from '@/api/infra/codegen'
import { CodegenTableVO, CodegenPreviewVO } from '@/api/infra/codegen/types'
import { useI18n } from '@/hooks/web/useI18n'
import { useClipboard } from '@vueuse/core'
const { t } = useI18n()
// ======== 显示页面 ========
const preview = reactive({
open: false,
titel: '代码预览',
fileTree: [],
activeName: ''
})
const previewCodegen = ref<CodegenPreviewVO[]>()
const show = async (row: CodegenTableVO) => {
const res = await previewCodegenApi(row.id)
let file = handleFiles(res)
previewCodegen.value = res
preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
preview.activeName = res[0].filePath
preview.open = true
}
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 handleFiles = (datas: CodegenPreviewVO[]) => {
let exists = {} // keyfile 的 idvaluetrue
let files: filesType[] = []
// 遍历每个元素
for (const data of datas) {
let paths = data.filePath.split('/')
let fullPath = '' // 从头开始的路径,用于生成 id
// 特殊处理 java 文件
if (paths[paths.length - 1].indexOf('.java') >= 0) {
let newPaths: string[] = []
for (let i = 0; i < paths.length; i++) {
let path = paths[i]
if (path !== 'java') {
newPaths.push(path)
continue
}
newPaths.push(path)
// 特殊处理中间的 package进行合并
let tmp = ''
while (i < paths.length) {
path = paths[i + 1]
if (
path === 'controller' ||
path === 'convert' ||
path === 'dal' ||
path === 'enums' ||
path === 'service' ||
path === 'vo' || // 下面三个,主要是兜底。可能考虑到有人改了包结构
path === 'mysql' ||
path === 'dataobject'
) {
break
}
tmp = tmp ? tmp + '.' + path : path
i++
}
if (tmp) {
newPaths.push(tmp)
}
}
paths = newPaths
}
// 遍历每个 path 拼接成树
for (let i = 0; i < paths.length; i++) {
// 已经添加到 files 中,则跳过
let oldFullPath = fullPath
// 下面的 replaceAll 的原因,是因为上面包处理了,导致和 tabs 不匹配,所以 replaceAll 下
fullPath = fullPath.length === 0 ? paths[i] : fullPath.replaceAll('.', '/') + '/' + paths[i]
if (exists[fullPath]) {
continue
}
// 添加到 files 中
exists[fullPath] = true
files.push({
id: fullPath,
label: paths[i],
parentId: oldFullPath || '/' // "/" 为根节点
})
}
}
return files
}
/** 复制 **/
const copy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })
if (!isSupported) {
ElMessage.error(t('common.copyError'))
} else {
await copy()
if (unref(copied)) {
ElMessage.success(t('common.copySuccess'))
}
}
}
defineExpose({
show
})
</script>
<template>
<Dialog title="预览" v-model="preview.open" top="5vh" maxHeight="800px" width="90%">
<div class="flex">
<el-card class="w-1/4" :gutter="12" shadow="hover">
<el-tree
ref="treeRef"
node-key="id"
:data="preview.fileTree"
:expand-on-click-node="false"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</el-card>
<el-card class="w-3/4" style="margin-left: 10px" :gutter="12" shadow="hover">
<el-tabs v-model="preview.activeName">
<el-tab-pane
v-for="item in previewCodegen"
:label="item.filePath.substring(item.filePath.lastIndexOf('/') + 1)"
:name="item.filePath"
:key="item.filePath"
>
<el-button text style="float: right" @click="copy(item.code)">
{{ t('common.copy') }}
</el-button>
<pre>{{ item.code }}</pre>
<!-- <pre><code class="language-html" v-html="highlightedCode(item)"></code></pre> -->
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</Dialog>
</template>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import * as CodegenApi from '@/api/infra/codegen'
import { useTable } from '@/hooks/web/useTable'
import { CodegenTableVO } from '@/api/infra/codegen/types'
import { allSchemas } from './codegen.data'
import { useI18n } from '@/hooks/web/useI18n'
import ImportTable from './components/ImportTable.vue'
import Preview from './components/Preview.vue'
import download from '@/utils/download'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
const { t } = useI18n() // 国际化
const { push } = useRouter()
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<CodegenTableVO>, CodegenTableVO>({
getListApi: CodegenApi.getCodegenTablePageApi,
delListApi: CodegenApi.deleteCodegenTableApi
})
const { getList, setSearchParams, delList } = methods
// 导入操作
const importRef = ref()
const openImportTable = () => {
importRef.value.show()
}
// 预览操作
const previewRef = ref()
const handlePreview = (row: CodegenTableVO) => {
previewRef.value.show(row)
}
// 编辑操作
const handleEditTable = (row: CodegenTableVO) => {
push('/codegen/edit?id=' + row.id)
}
// 同步操作
const handleSynchDb = (row: CodegenTableVO) => {
// 基于 DB 同步
const tableName = row.tableName
ElMessageBox.confirm('确认要强制同步' + tableName + '表结构吗?', t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}).then(async () => {
await CodegenApi.syncCodegenFromDBApi(row.id)
ElMessage.success('同步成功')
})
}
// 生成代码操作
const handleGenTable = (row: CodegenTableVO) => {
const res = CodegenApi.downloadCodegenApi(row.id)
download.zip(res, 'codegen-' + row.className + '.zip')
}
// 删除操作
const handleDelete = (row: CodegenTableVO) => {
delList(row.id, false)
}
// 查询操作
const handleQuery = () => {
getList()
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['infra:codegen:create']" @click="openImportTable">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.import') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #updateTime="{ row }">
<span>{{ dayjs(row.updateTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:codegen:preview']"
@click="handlePreview(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.preview') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:codegen:update']"
@click="handleEditTable(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:codegen:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:codegen:update']"
@click="handleSynchDb(row)"
>
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('action.sync') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:codegen:download']"
@click="handleGenTable(row)"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.generate') }}
</el-button>
</template>
</Table>
</ContentWrap>
<ImportTable ref="importRef" @ok="handleQuery" />
<Preview ref="previewRef" />
</template>

View File

@@ -0,0 +1,108 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
category: [required],
name: [required],
key: [required],
value: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '参数分类',
field: 'category'
},
{
label: '参数名称',
field: 'name',
search: {
show: true
}
},
{
label: '参数键名',
field: 'key',
search: {
show: true
}
},
{
label: '参数键值',
field: 'value'
},
{
label: '系统内置',
field: 'type',
dictType: DICT_TYPE.INFRA_CONFIG_TYPE,
search: {
show: true
}
},
{
label: '是否可见',
field: 'visible',
form: {
component: 'RadioButton',
componentProps: {
options: [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
}
},
{
label: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
table: {
show: false
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,204 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { ConfigVO } from '@/api/infra/config/types'
import { rules, allSchemas } from './config.data'
import * as ConfigApi from '@/api/infra/config'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<ConfigVO>, ConfigVO>({
getListApi: ConfigApi.getConfigPageApi,
delListApi: ConfigApi.deleteConfigApi,
exportListApi: ConfigApi.exportConfigApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('参数配置.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: ConfigVO) => {
setDialogTile('update')
// 设置数据
const res = await ConfigApi.getConfigApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as ConfigVO
if (actionType.value === 'create') {
await ConfigApi.createConfigApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await ConfigApi.updateConfigApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: ConfigVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: ConfigVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['infra:config:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['infra:config:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #visible="{ row }">
<span>{{ row.visible ? '是' : '否' }} </span>
</template>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="row.type" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:config:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:config:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:config:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #visible="{ row }">
<span>{{ row.visible ? '是' : '否' }} </span>
</template>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="row.type" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,75 @@
import { reactive } from 'vue'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
url: [required],
username: [required],
password: [required]
})
// 新增 + 修改
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '数据源名称',
field: 'name'
},
{
label: '数据源连接',
field: 'url',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
label: '用户名',
field: 'username'
},
{
label: '密码',
field: 'password',
table: {
show: false
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
field: 'action',
width: '240px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,161 @@
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './dataSourceConfig.data'
import type { DataSourceConfigVO } from '@/api/infra/dataSourceConfig/types'
import * as DataSourceConfiggApi from '@/api/infra/dataSourceConfig'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n() // 国际化
const tableData = ref()
const getList = async () => {
const res = await DataSourceConfiggApi.getDataSourceConfigListApi()
tableData.value = res
}
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: DataSourceConfigVO) => {
setDialogTile('update')
// 设置数据
const res = await DataSourceConfiggApi.getDataSourceConfigApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as DataSourceConfigVO
if (actionType.value === 'create') {
await DataSourceConfiggApi.createDataSourceConfigApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await DataSourceConfiggApi.updateDataSourceConfigApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
loading.value = false
}
}
// 删除操作
const handleDelete = async (row: DataSourceConfigVO) => {
await DataSourceConfiggApi.deleteDataSourceConfigApi(row.id)
ElMessage.success(t('common.delSuccess'))
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: DataSourceConfigVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
onMounted(async () => {
await getList()
})
</script>
<template>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button
v-hasPermi="['infra:data-source-config:create']"
type="primary"
@click="handleCreate"
>
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<Table :columns="allSchemas.tableColumns" :data="tableData">
<template #createTime="{ row }">
<span>{{ row.createTime ? dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:data-source-config:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:data-source-config:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:data-source-config:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #createTime="{ row }">
<span>{{ row.createTime ? dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { IFrame } from '@/components/IFrame'
import * as DbDocApi from '@/api/infra/dbDoc'
import { onMounted, ref } from 'vue'
import download from '@/utils/download'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n() // 国际化
const loding = ref(true)
const src = ref('')
/** 页面加载 */
const init = async () => {
const res = await DbDocApi.exportHtmlApi()
let blob = new Blob([res], { type: 'text/html' })
let blobUrl = window.URL.createObjectURL(blob)
src.value = blobUrl
loding.value = false
}
/** 处理导出 HTML */
const handleExportHtml = async () => {
const res = await DbDocApi.exportHtmlApi()
download.html(res, '数据库文档.html')
}
/** 处理导出 Word */
const handleExportWord = async () => {
const res = await DbDocApi.exportHtmlApi()
download.word(res, '数据库文档.doc')
}
/** 处理导出 Markdown */
const handleExportMarkdown = async () => {
const res = await DbDocApi.exportHtmlApi()
download.markdown(res, '数据库文档.md')
}
onMounted(async () => {
await init()
})
</script>
<template>
<ContentWrap title="数据库文档" message="https://doc.iocoder.cn/db-doc/">
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" @click="handleExportHtml">
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') + ' HTML' }}
</el-button>
<el-button type="primary" @click="handleExportWord">
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') + ' Word' }}
</el-button>
<el-button type="primary" @click="handleExportMarkdown">
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') + ' Markdown' }}
</el-button>
</div>
<IFrame v-if="!loding" v-loading="loding" :src="src" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import { IFrame } from '@/components/IFrame'
import { ref } from 'vue'
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/druid/index.html')
</script>
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,53 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '文件名',
field: 'path',
search: {
show: true
}
},
{
label: 'URL',
field: 'url'
},
{
label: '文件类型',
field: 'type'
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '300px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,192 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElUpload, UploadInstance, UploadRawFile, ElImage } from 'element-plus'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import type { FileVO } from '@/api/infra/file/types'
import { allSchemas } from './fileList.data'
import * as FileApi from '@/api/infra/file'
import { useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache()
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<FileVO>, FileVO>({
getListApi: FileApi.getFilePageApi,
delListApi: FileApi.deleteFileApi
})
const { getList, setSearchParams, delList } = methods
// ========== 上传相关 ==========
const uploadDialogVisible = ref(false)
const uploadDialogTitle = ref('上传')
const updateSupport = ref(0)
const uploadDisabled = ref(false)
const uploadRef = ref<UploadInstance>()
let updateUrl = import.meta.env.VITE_UPLOAD_URL
const uploadHeaders = ref()
// 文件上传之前判断
const beforeUpload = (file: UploadRawFile) => {
const isImg = file.type === 'image/jpeg' || 'image/gif' || 'image/png'
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImg) ElMessage.error('上传文件只能是 xls / xlsx 格式!')
if (!isLt5M) ElMessage.error('上传文件大小不能超过 5MB!')
return isImg && isLt5M
}
// 处理上传的文件发生变化
// const handleFileChange = (uploadFile: UploadFile): void => {
// uploadRef.value.data.path = uploadFile.name
// }
// 文件上传
const submitFileForm = () => {
uploadHeaders.value = {
Authorization: 'Bearer ' + wsCache.get('ACCESS_TOKEN'),
'tenant-id': wsCache.get('tenantId')
}
uploadDisabled.value = true
uploadRef.value!.submit()
}
// 文件上传成功
const handleFileSuccess = (response: any): void => {
if (response.code !== 0) {
ElMessage.error(response.msg)
return
}
ElMessage.success('上传成功')
getList()
uploadDialogVisible.value = false
uploadDisabled.value = false
}
// 文件数超出提示
const handleExceed = (): void => {
ElMessage.error('最多只能上传一个文件!')
}
// 上传错误提示
const excelUploadError = (): void => {
ElMessage.error('导入数据失败,请您重新上传!')
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// 删除操作
const handleDelete = (row: FileVO) => {
delList(row.id, false)
}
// 详情操作
const handleDetail = (row: FileVO) => {
// 设置数据
detailRef.value = row
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<el-button type="primary" @click="uploadDialogVisible = true">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
</el-button>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #url="{ row }">
<el-image
v-if="row.type === 'jpg' || 'png' || 'gif'"
style="width: 80px; height: 50px"
:src="row.url"
:key="row.url"
fit="contain"
lazy
/>
<span v-else>{{ row.url }}</span>
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button link type="primary" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:file:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #url="{ row }">
<el-image
v-if="row.type === 'jpg' || 'png' || 'gif'"
style="width: 100px; height: 100px"
:src="row.url"
:key="row.url"
lazy
/>
<span>{{ row.url }}</span>
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
<Dialog v-model="uploadDialogVisible" :title="uploadDialogTitle" :destroy-on-close="true">
<el-upload
ref="uploadRef"
:action="updateUrl + '?updateSupport=' + updateSupport"
:headers="uploadHeaders"
:drag="true"
:limit="1"
:multiple="true"
:show-file-list="true"
:disabled="uploadDisabled"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-success="handleFileSuccess"
:on-error="excelUploadError"
:auto-upload="false"
accept=".jpg, .png, .gif"
>
<Icon icon="ep:upload-filled" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">请上传 .jpg, .png, .gif 标准格式文件</div>
</template>
</el-upload>
<template #footer>
<el-button type="primary" @click="submitFileForm">
<Icon icon="ep:upload-filled" />
{{ t('action.save') }}
</el-button>
<el-button @click="uploadDialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,93 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
storage: [required],
config: {
basePath: [required],
host: [required],
port: [required],
username: [required],
password: [required],
mode: [required],
endpoint: [required],
bucket: [required],
accessKey: [required],
accessSecret: [required],
domain: [required]
}
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '配置名',
field: 'name',
search: {
show: true
}
},
{
label: '存储器',
field: 'storage',
dictType: DICT_TYPE.INFRA_FILE_STORAGE,
search: {
show: true
}
},
{
label: '主配置',
field: 'primary',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING
},
{
label: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '400px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,220 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { FileConfigVO } from '@/api/infra/fileConfig/types'
import { rules, allSchemas } from './fileConfig.data'
import * as FileConfigApi from '@/api/infra/fileConfig'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<FileConfigVO>, FileConfigVO>({
getListApi: FileConfigApi.getFileConfigPageApi,
delListApi: FileConfigApi.deleteFileConfigApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: FileConfigVO) => {
setDialogTile('update')
// 设置数据
const res = await FileConfigApi.getFileConfigApi(row.id)
unref(formRef)?.setValues(res)
}
// 主配置操作
const handleMaster = (row: FileConfigVO) => {
ElMessageBox.confirm('是否确认修改配置【 ' + row.name + ' 】为主配置?', t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
await FileConfigApi.updateFileConfigMasterApi(row.id)
await getList()
})
.catch(() => {})
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as FileConfigVO
if (actionType.value === 'create') {
await FileConfigApi.createFileConfigApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await FileConfigApi.updateFileConfigApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: FileConfigVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: FileConfigVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['infra:file-config:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #storage="{ row }">
<DictTag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="row.storage" />
</template>
<template #primary="{ row }">
<DictTag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.master" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['infra:file-config:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:file-config:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:file-config:update']"
@click="handleMaster(row)"
>
<Icon icon="ep:flag" class="mr-5px" /> 主配置
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:file-config:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:share" class="mr-5px" /> {{ t('action.test') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['infra:file-config:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #storage="{ row }">
<DictTag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="row.storage" />
</template>
<template #primary="{ row }">
<DictTag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.master" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,122 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import dayjs from 'dayjs'
import DictTag from '@/components/DictTag/src/DictTag.vue'
import * as JobLogApi from '@/api/infra/jobLog'
import { JobLogVO } from '@/api/infra/jobLog/types'
import Icon from '@/components/Icon/src/Icon.vue'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { useRoute } from 'vue-router'
import { allSchemas } from './jobLog.data'
const { t } = useI18n() // 国际化
const { query } = useRoute()
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<JobLogVO>, JobLogVO>({
getListApi: JobLogApi.getJobLogPageApi,
exportListApi: JobLogApi.exportJobLogApi
})
const { getList, setSearchParams, exportList } = methods
const getTableList = async () => {
const id = (query.id as unknown as number) && (query.jobId as unknown as number)
tableObject.paramsObj.params = {
jobId: id
}
await getList()
}
// 导出操作
const handleExport = async () => {
await exportList('定时任务日志.xls')
}
// ========== CRUD 相关 ==========
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('') // 弹出层标题
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: JobLogVO) => {
// 设置数据
const res = JobLogApi.getJobLogApi(row.id)
detailRef.value = res
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// ========== 初始化 ==========
onMounted(() => {
getTableList()
})
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button
type="warning"
v-hasPermi="['infra:job:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #beginTime="{ row }">
<span>{{
dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') +
' ~ ' +
dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss')
}}</span>
</template>
<template #duration="{ row }">
<span>{{ row.duration + ' 毫秒' }}</span>
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="row.status" />
</template>
<template #action="{ row }">
<el-button link type="primary" v-hasPermi="['infra:job:query']" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #status="{ row }">
<DictTag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="row.status" />
</template>
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template>
<template #monitorTimeout="{ row }">
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,215 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import DictTag from '@/components/DictTag/src/DictTag.vue'
import * as JobApi from '@/api/infra/job'
import { JobVO } from '@/api/infra/job/types'
import Icon from '@/components/Icon/src/Icon.vue'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './job.data'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
const { t } = useI18n() // 国际化
const { push } = useRouter()
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<JobVO>, JobVO>({
getListApi: JobApi.getJobPageApi,
delListApi: JobApi.deleteJobApi,
exportListApi: JobApi.exportJobApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('定时任务.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: JobVO) => {
setDialogTile('update')
// 设置数据
const res = await JobApi.getJobApi(row.id)
unref(formRef)?.setValues(res)
}
// 执行日志
const handleJobLog = (row: JobVO) => {
if (row.id) {
push('/job/job-log?id=' + row.id)
} else {
push('/job/job-log')
}
}
// 执行一次
const handleRun = (row: JobVO) => {
ElMessageBox.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
JobApi.runJobApi(row.id).then(() => {
ElMessage.success('执行成功')
getList()
})
})
.catch(() => {})
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as JobVO
if (actionType.value === 'create') {
await JobApi.createJobApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await JobApi.updateJobApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: JobVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: JobVO) => {
// 设置数据
const res = JobApi.getJobApi(row.id)
detailRef.value = res
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['infra:job:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['infra:job:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
<el-button type="info" v-hasPermi="['infra:job:query']" @click="handleJobLog">
<Icon icon="el:zoom-in" class="mr-5px" /> 执行日志
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="row.status" />
</template>
<template #action="{ row }">
<el-button link type="primary" v-hasPermi="['infra:job:update']" @click="handleUpdate(row)">
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button link type="primary" v-hasPermi="['infra:job:query']" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button link type="primary" v-hasPermi="['infra:job:delete']" @click="handleDelete(row)">
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
<el-button link type="primary" v-hasPermi="['infra:job:trigger']" @click="handleRun(row)">
<Icon icon="ep:view" class="mr-5px" /> 执行一次
</el-button>
<el-button link type="primary" v-hasPermi="['infra:job:query']" @click="handleJobLog(row)">
<Icon icon="ep:view" class="mr-5px" /> 调度日志
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="row.status" />
</template>
<template #retryInterval="{ row }">
<span>{{ row.retryInterval + '毫秒' }} </span>
</template>
<template #monitorTimeout="{ row }">
<span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,93 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
handlerName: [required],
cronExpression: [required],
retryCount: [required],
retryInterval: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '任务名称',
field: 'name',
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_STATUS
},
{
label: '处理器的名字',
field: 'handlerName',
search: {
show: true
}
},
{
label: '处理器的参数',
field: 'handlerParam',
table: {
show: false
}
},
{
label: 'CRON 表达式',
field: 'cronExpression'
},
{
label: '重试次数',
field: 'retryCount',
table: {
show: false
}
},
{
label: '重试间隔',
field: 'retryInterval',
table: {
show: false
}
},
{
label: '监控超时时间',
field: 'monitorTimeout',
table: {
show: false
}
},
{
field: 'action',
width: '400px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,94 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '任务编号',
field: 'jobId',
search: {
show: true
}
},
{
label: '处理器的名字',
field: 'handlerName',
search: {
show: true
}
},
{
label: '处理器的参数',
field: 'handlerParam'
},
{
label: '第几次执行',
field: 'executeIndex'
},
{
label: '开始执行时间',
field: 'beginTime',
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'date',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
}
},
{
label: '结束执行时间',
field: 'endTime',
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'date',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
},
table: {
show: false
}
},
{
label: '执行时长',
field: 'duration'
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.INFRA_JOB_LOG_STATUS,
search: {
show: true
}
},
{
field: 'action',
width: '100px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,205 @@
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
import * as RedisApi from '@/api/infra/redis'
import DictTag from '@/components/DictTag/src/DictTag.vue'
import { DICT_TYPE } from '@/utils/dict'
import * as echarts from 'echarts'
import { RedisKeyInfo, RedisMonitorInfoVO } from '@/api/infra/redis/types'
import {
ElRow,
ElCard,
ElCol,
ElTable,
ElTableColumn,
ElScrollbar,
ElDescriptions,
ElDescriptionsItem
} from 'element-plus'
const cache = ref<RedisMonitorInfoVO>()
const keyListLoad = ref(true)
const keyList = ref<RedisKeyInfo[]>([])
// 基本信息
const readRedisInfo = async () => {
const data = await RedisApi.redisMonitorInfo()
cache.value = data
loadEchartOptions(cache.value.commandStats)
const redisKeysInfo = await RedisApi.redisKeysInfo()
keyList.value = redisKeysInfo
keyListLoad.value = false //加载完成
}
// 图表
const commandStatsRef = ref<HTMLElement>()
const usedmemory = ref<HTMLDivElement>()
const loadEchartOptions = (stats) => {
const commandStats = [] as any[]
const nameList = [] as string[]
stats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
const commandStatsInstance = echarts.init(commandStatsRef.value!, 'macarons')
commandStatsInstance.setOption({
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: nameList,
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: commandStats,
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: {
show: true
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
const usedMemoryInstance = echarts.init(usedmemory.value!, 'macarons')
usedMemoryInstance.setOption({
title: {
text: '内存使用情况',
left: 'center'
},
tooltip: {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 100,
progress: {
show: true
},
detail: {
formatter: cache.value!.info.used_memory_human
},
data: [
{
value: parseFloat(cache.value!.info.used_memory_human),
name: '内存消耗'
}
]
}
]
})
}
onBeforeMount(() => {
readRedisInfo()
})
</script>
<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" style="margin-top: 10px">
<el-card :gutter="12" shadow="hover">
<div ref="commandStatsRef" style="height: 350px"></div>
</el-card>
</el-col>
<el-col :span="12" style="margin-top: 10px">
<el-card style="margin-left: 10px" :gutter="12" shadow="hover">
<div ref="usedmemory" style="height: 350px"></div>
</el-card>
</el-col>
</el-row>
<el-row style="margin-top: 10px">
<el-col :span="24" class="card-box" shadow="hover">
<el-card>
<el-table v-loading="keyListLoad" :data="keyList" row-key="id">
<el-table-column prop="keyTemplate" label="Key 模板" width="200" />
<el-table-column prop="keyType" label="Key 类型" width="100" />
<el-table-column prop="valueType" label="Value 类型" />
<el-table-column prop="timeoutType" label="超时时间" width="200">
<template #default="{ row }">
<DictTag :type="DICT_TYPE.INFRA_REDIS_TIMEOUT_TYPE" :value="row?.timeoutType" />
<span v-if="row?.timeout > 0">({{ row?.timeout / 1000 }} )</span>
</template>
</el-table-column>
<el-table-column prop="memo" label="备注" />
</el-table>
</el-card>
</el-col>
</el-row>
</el-scrollbar>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import { IFrame } from '@/components/IFrame'
import { ref } from 'vue'
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/admin/applications')
</script>
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { IFrame } from '@/components/IFrame'
import { ref } from 'vue'
const src = ref('http://skywalking.shop.iocoder.cn')
</script>
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import { IFrame } from '@/components/IFrame'
import { ref } from 'vue'
const BASE_URL = import.meta.env.VITE_BASE_URL
const src = ref(BASE_URL + '/doc.html')
</script>
<template>
<ContentWrap>
<IFrame :src="src" />
</ContentWrap>
</template>

View File

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

View File

@@ -0,0 +1,70 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '应用名',
field: 'name',
search: {
show: true
}
},
{
label: '商户名称',
field: 'payMerchant',
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { AppVO } from '@/api/pay/app/types'
import { rules, allSchemas } from './app.data'
import * as AppApi from '@/api/pay/app'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<AppVO>, AppVO>({
getListApi: AppApi.getAppPageApi,
delListApi: AppApi.deleteAppApi,
exportListApi: AppApi.exportAppApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('应用数据.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: AppVO) => {
setDialogTile('update')
// 设置数据
const res = await AppApi.getAppApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as AppVO
if (actionType.value === 'create') {
await AppApi.createAppApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await AppApi.updateAppApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: AppVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: AppVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:post:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['system:post:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { MerchantVO } from '@/api/pay/merchant/types'
import { rules, allSchemas } from './merchant.data'
import * as MerchantApi from '@/api/pay/merchant'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<MerchantVO>, MerchantVO>({
getListApi: MerchantApi.getMerchantPageApi,
delListApi: MerchantApi.deleteMerchantApi,
exportListApi: MerchantApi.exportMerchantApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('商户数据.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: MerchantVO) => {
setDialogTile('update')
// 设置数据
const res = await MerchantApi.getMerchantApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as MerchantVO
if (actionType.value === 'create') {
await MerchantApi.createMerchantApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await MerchantApi.updateMerchantApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: MerchantVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: MerchantVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:post:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['system:post:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,94 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '商户号',
field: 'no',
search: {
show: true
}
},
{
label: '商户全称',
field: 'code',
search: {
show: true
}
},
{
label: '商户简称',
field: 'shortName',
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
table: {
show: false
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,177 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { OrderVO } from '@/api/pay/order/types'
import { rules, allSchemas } from './order.data'
import * as OrderApi from '@/api/pay/order'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<OrderVO>, OrderVO>({
getListApi: OrderApi.getOrderPageApi,
delListApi: OrderApi.deleteOrderApi,
exportListApi: OrderApi.exportOrderApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('订单数据.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: OrderVO) => {
setDialogTile('update')
// 设置数据
const res = await OrderApi.getOrderApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as OrderVO
if (actionType.value === 'create') {
await OrderApi.createOrderApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await OrderApi.updateOrderApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: OrderVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: OrderVO) => {
setDialogTile('detail')
// 设置数据
const res = await OrderApi.getOrderApi(row.id as number)
detailRef.value = res
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['pay:order:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['pay:order:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #action="{ row }">
<el-button link type="primary" v-hasPermi="['pay:order:update']" @click="handleUpdate(row)">
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button link type="primary" v-hasPermi="['pay:order:update']" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button link type="primary" v-hasPermi="['pay:order:delete']" @click="handleDelete(row)">
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,169 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
merchantId: [required],
appId: [required],
merchantOrderId: [required],
subject: [required],
body: [required],
notifyUrl: [required],
notifyStatus: [required],
amount: [required],
status: [required],
userIp: [required],
expireTime: [required],
refundStatus: [required],
refundTimes: [required],
refundAmount: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '商户编号',
field: 'merchantId',
search: {
show: true
}
},
{
label: '应用编号',
field: 'appId',
search: {
show: true
}
},
{
label: '渠道编号',
field: 'channelId'
},
{
label: '渠道编码',
field: 'channelCode',
search: {
show: true
}
},
{
label: '渠道订单号',
field: 'merchantOrderId',
search: {
show: true
}
},
{
label: '商品标题',
field: 'subject'
},
{
label: '商品描述',
field: 'body'
},
{
label: '异步通知地址',
field: 'notifyUrl'
},
{
label: '回调商户状态',
field: 'notifyStatus',
dictType: DICT_TYPE.PAY_ORDER_NOTIFY_STATUS
},
{
label: '支付金额',
field: 'amount',
search: {
show: true
}
},
{
label: '渠道手续费',
field: 'channelFeeRate',
search: {
show: true
}
},
{
label: '渠道手续金额',
field: 'channelFeeAmount',
search: {
show: true
}
},
{
label: '支付状态',
field: 'status',
dictType: DICT_TYPE.PAY_ORDER_STATUS,
search: {
show: true
}
},
{
label: '用户 IP',
field: 'userIp'
},
{
label: '订单失效时间',
field: 'expireTime'
},
{
label: '订单支付成功时间',
field: 'successTime'
},
{
label: '订单支付通知时间',
field: 'notifyTime'
},
{
label: '支付成功的订单拓展单编号',
field: 'successExtensionId'
},
{
label: '退款状态',
field: 'refundStatus',
dictType: DICT_TYPE.PAY_ORDER_REFUND_STATUS,
search: {
show: true
}
},
{
label: '退款次数',
field: 'refundTimes'
},
{
label: '退款总金额',
field: 'refundAmount'
},
{
label: '渠道用户编号',
field: 'channelUserId'
},
{
label: '渠道订单号',
field: 'channelOrderNo'
},
{
label: t('table.action'),
field: 'action',
width: '270px',
form: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import type { RefundVO } from '@/api/pay/refund/types'
import { allSchemas } from './refund.data'
import * as RefundApi from '@/api/pay/refund'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<RefundVO>, RefundVO>({
getListApi: RefundApi.getRefundPageApi,
delListApi: RefundApi.deleteRefundApi,
exportListApi: RefundApi.exportRefundApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('退款订单.xls')
}
// ========== CRUD 相关 ==========
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
// 删除操作
const handleDelete = (row: RefundVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: RefundVO) => {
// 设置数据
detailRef.value = RefundApi.getRefundApi(row.id)
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button
type="warning"
v-hasPermi="['system:post:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,101 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '应用编号',
field: 'appId',
search: {
show: true
}
},
{
label: '商户名称',
field: 'merchantName'
},
{
label: '应用名称',
field: 'appName'
},
{
label: '商品名称',
field: 'subject'
},
{
label: '商户退款单号',
field: 'merchantRefundNo'
},
{
label: '商户订单号',
field: 'merchantOrderId'
},
{
label: '交易订单号',
field: 'tradeNo'
},
{
label: '支付金额',
field: 'payAmount'
},
{
label: '退款金额',
field: 'refundAmount'
},
{
label: '渠道编码',
field: 'channelCode',
search: {
show: true
}
},
{
label: '退款类型',
field: 'type',
dictType: DICT_TYPE.PAY_REFUND_ORDER_TYPE,
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.PAY_REFUND_ORDER_STATUS,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,55 @@
import { required } from '@/utils/formRules'
import { reactive } from 'vue'
export const modelSchema = reactive<FormSchema[]>([
{
label: '上级部门',
field: 'parentId',
component: 'Input'
},
{
label: '部门名称',
field: 'name',
component: 'Input',
formItemProps: {
rules: [required]
}
},
{
label: '负责人',
field: 'email',
component: 'Input'
},
{
label: '联系电话',
field: 'phone',
component: 'Input'
},
{
label: '邮箱',
field: 'email',
component: 'Input'
},
{
label: '显示排序',
field: 'sort',
component: 'Input'
},
{
label: '状态',
field: 'status',
component: 'RadioButton',
componentProps: {
options: [
{
label: '开启',
value: 0
},
{
label: '关闭',
value: 1
}
]
}
}
])

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'
import { ElCard, ElTree, ElTreeSelect, ElMessage, ElMessageBox } from 'element-plus'
import { handleTree } from '@/utils/tree'
import { onMounted, ref, unref } from 'vue'
import * as DeptApi from '@/api/system/dept'
import { Form, FormExpose } from '@/components/Form'
import { modelSchema } from './dept.data'
import { DeptVO } from '@/api/system/dept/types'
interface Tree {
id: number
name: string
children?: Tree[]
}
const defaultProps = {
children: 'children',
label: 'name',
value: 'id'
}
const { t } = useI18n() // 国际化
const loading = ref(false) // 遮罩层
const dialogVisible = ref(false) // 是否显示弹出层
const showForm = ref(false) // 显示form表单
const formTitle = ref('部门信息') // 显示form标题
const deptParentId = ref(0) // 上级ID
// 创建form表单
const formRef = ref<FormExpose>()
// ========== 创建部门树结构 ==========
const deptOptions = ref([]) // 树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
const getTree = async () => {
const res = await DeptApi.listSimpleDeptApi()
deptOptions.value = handleTree(res)
}
const filterNode = (value: string, data: Tree) => {
if (!value) return true
return data.name.includes(value)
}
// 新增
const handleAdd = () => {
// 重置表单
formTitle.value = '新增部门'
unref(formRef)?.getElFormRef()?.resetFields()
showForm.value = true
}
// 编辑
const handleUpdate = async (data: { id: number }) => {
showForm.value = true
const res = await DeptApi.getDeptApi(data.id)
formTitle.value = '修改- ' + res?.name
deptParentId.value = res.parentId
unref(formRef)?.setValues(res)
}
// 删除
const handleDelete = async (data: { id: number }) => {
ElMessageBox.confirm(t('common.delDataMessage'), t('common.confirmTitle'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
await DeptApi.deleteDeptApi(data.id)
ElMessage.success(t('common.delSuccess'))
})
.catch(() => {})
await getTree()
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as DeptVO
deptParentId.value = data.parentId
// TODO: 表单提交待完善
if (formTitle.value.startsWith('新增')) {
await DeptApi.createDeptApi(data)
} else if (formTitle.value.startsWith('修改')) {
await DeptApi.updateDeptApi(data)
}
// 操作成功,重新加载列表
dialogVisible.value = false
} finally {
loading.value = false
}
}
onMounted(async () => {
await getTree()
})
</script>
<template>
<div class="flex">
<el-card class="w-1/3 dept" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>部门列表</span>
<el-button type="primary" v-hasPermi="['system:dept:create']" @click="handleAdd">
新增根节点
</el-button>
</div>
</template>
<div class="custom-tree-container">
<!-- <p>部门列表</p> -->
<!-- 操作工具栏 -->
<el-tree
ref="treeRef"
node-key="id"
:data="deptOptions"
:props="defaultProps"
:highlight-current="true"
default-expand-all
:filter-method="filterNode"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<el-button link v-hasPermi="['system:dept:create']" @click="handleAdd()">
<Icon icon="ep:plus" class="mr-5px" />
</el-button>
<el-button link v-hasPermi="['system:dept:update']" @click="handleUpdate(data)">
<Icon icon="ep:edit" class="mr-5px" />
</el-button>
<el-button link v-hasPermi="['system:dept:delete']" @click="handleDelete(data)">
<Icon icon="ep:delete" class="mr-5px" />
</el-button>
</span>
</span>
</template>
</el-tree>
</div>
</el-card>
<el-card class="w-2/3 dept" style="margin-left: 10px" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ formTitle }}</span>
</div>
</template>
<div v-if="!showForm">
<span><p>请从左侧选择部门</p></span>
</div>
<div v-if="showForm">
<!-- 操作工具栏 -->
<Form :schema="modelSchema" ref="formRef">
<template #parentId>
<el-tree-select
node-key="id"
v-model="deptParentId"
:props="defaultProps"
:data="deptOptions"
check-strictly
/>
</template>
</Form>
<!-- 操作按钮 -->
<el-button
type="primary"
v-hasPermi="['system:dept:update']"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button type="danger" @click="showForm = false">取消</el-button>
</div>
</el-card>
</div>
</template>
<style scoped>
.dept {
height: 600px;
max-height: 1800px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>

View File

@@ -0,0 +1,128 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const dictDataRules = reactive({
dictType: [required],
label: [required],
value: [required]
})
// crudSchemas
export const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '字典类型',
field: 'dictType',
table: {
show: false
}
},
{
label: '数据标签',
field: 'label',
search: {
show: true
}
},
{
label: '数据键值',
field: 'value'
},
{
label: '颜色类型',
field: 'colorType',
form: {
component: 'Select',
componentProps: {
options: [
{
label: 'default',
value: ''
},
{
label: 'success',
value: 'success'
},
{
label: 'info',
value: 'info'
},
{
label: 'warning',
value: 'warning'
},
{
label: 'danger',
value: 'danger'
}
]
}
},
table: {
show: false
}
},
{
label: 'CSS Class',
field: 'cssClass',
table: {
show: false
}
},
{
label: '显示排序',
field: 'sort',
table: {
show: false
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS
},
{
label: t('form.remark'),
field: 'remark',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
},
table: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '180px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,74 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const dictTypeRules = reactive({
name: [required],
type: [required]
})
// 新增 + 修改
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '字典名称',
field: 'name',
search: {
show: true
}
},
{
label: '字典类型',
field: 'type',
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS
},
{
label: t('form.remark'),
field: 'remark',
table: {
show: false
},
form: {
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
field: 'action',
width: '180px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,298 @@
<script setup lang="ts">
import { ref, unref, onMounted } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import * as DictTypeSchemas from './dict.type'
import * as DictDataSchemas from './dict.data'
import { useTable } from '@/hooks/web/useTable'
import { ElCard, ElMessage } from 'element-plus'
import * as DictTypeApi from '@/api/system/dict/dict.type'
import * as DictDataApi from '@/api/system/dict/dict.data'
import { DictDataVO, DictTypeVO } from '@/api/system/dict/types'
const { t } = useI18n() // 国际化
// ========== 字典分类列表相关 ==========
const {
register: typeRegister,
tableObject: typeTableObject,
methods: typeMethods
} = useTable<PageResult<DictTypeVO>, DictTypeVO>({
getListApi: DictTypeApi.getDictTypePageApi,
delListApi: DictTypeApi.deleteDictTypeApi
})
const {
getList: getTypeList,
setSearchParams: setTypeSearchParams,
delList: delTypeList
} = typeMethods
// 字典分类修改操作
const handleTypeCreate = () => {
setDialogTile('typeCreate')
// 重置表单
unref(typeFormRef)?.getElFormRef()?.resetFields()
}
const handleTypeUpdate = async (row: DictTypeVO) => {
setDialogTile('typeUpdate')
// 设置数据
const res = await DictTypeApi.getDictTypeApi(row.id)
unref(typeFormRef)?.setValues(res)
}
// 字典分类删除操作
const handleTypeDelete = async (row: DictTypeVO) => {
await delTypeList(row.id, false)
}
// ========== 字典数据列表相关 ==========
const tableTypeSelect = ref(false)
const {
register: dataRegister,
tableObject: dataTableObject,
methods: dataMethods
} = useTable<PageResult<DictDataVO>, DictDataVO>({
getListApi: DictDataApi.getDictDataPageApi,
delListApi: DictDataApi.deleteDictDataApi
})
const {
getList: getDataList,
setSearchParams: setDataSearchParams,
delList: delDataList
} = dataMethods
// 字典数据修改操作
const handleDataCreate = () => {
setDialogTile('dataCreate')
// 重置表单
unref(dataFormRef)?.getElFormRef()?.resetFields()
}
const handleDataUpdate = async (row: DictDataVO) => {
setDialogTile('dataUpdate')
// 设置数据
const res = await DictDataApi.getDictDataApi(row.id)
unref(dataFormRef)?.setValues(res)
}
// 字典数据删除操作
const handleDataDelete = async (row: DictTypeVO) => {
await delDataList(row.id, false)
}
// 字典分类点击行事件
const parentType = ref('')
const onClickType = async (data: { [key: string]: any }) => {
tableTypeSelect.value = true
dataTableObject.paramsObj.params = {
dictType: data.type
}
getDataList()
parentType.value = data.type
}
// 弹出框
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const actionLoading = ref(false) // 遮罩层
const typeFormRef = ref<FormExpose>() // 分类表单 Ref
const dataFormRef = ref<FormExpose>() // 数据表单 Ref
const actionType = ref('') // 操作按钮的类型
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 提交按钮
const submitTypeForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(typeFormRef)?.formModel as DictTypeVO
if (actionType.value === 'typeCreate') {
await DictTypeApi.createDictTypeApi(data)
ElMessage.success(t('common.createSuccess'))
} else if (actionType.value === 'typeUpdate') {
await DictTypeApi.updateDictTypeApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
await getTypeList()
dialogVisible.value = false
} finally {
actionLoading.value = false
}
}
const submitDataForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(dataFormRef)?.formModel as DictDataVO
if (actionType.value === 'dataCreate') {
data.dictType = parentType.value
await DictDataApi.createDictDataApi(data)
ElMessage.success(t('common.createSuccess'))
} else if (actionType.value === 'dataUpdate') {
await DictDataApi.updateDictDataApi(data)
ElMessage.success(t('common.updateSuccess'))
}
await getDataList()
// 操作成功,重新加载列表
dialogVisible.value = false
} finally {
actionLoading.value = false
}
}
// 初始化查询
onMounted(async () => {
await getTypeList()
typeTableObject.tableList[0] && onClickType(typeTableObject.tableList[0])
})
</script>
<template>
<div class="flex">
<el-card class="w-1/2 dict" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>字典分类</span>
</div>
</template>
<Search
:schema="DictTypeSchemas.allSchemas.searchSchema"
@search="setTypeSearchParams"
@reset="setTypeSearchParams"
/>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:dict:create']" @click="handleTypeCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
@row-click="onClickType"
:columns="DictTypeSchemas.allSchemas.tableColumns"
:selection="false"
:data="typeTableObject.tableList"
:loading="typeTableObject.loading"
:pagination="{
total: typeTableObject.total
}"
:highlight-current-row="true"
v-model:pageSize="typeTableObject.pageSize"
v-model:currentPage="typeTableObject.currentPage"
@register="typeRegister"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:dict:update']"
@click="handleTypeUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:dict:delete']"
@click="handleTypeDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</el-card>
<el-card class="w-1/2 dict" style="margin-left: 10px" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>字典数据</span>
</div>
</template>
<!-- 列表 -->
<div v-if="!tableTypeSelect">
<span>请从左侧选择</span>
</div>
<div v-if="tableTypeSelect">
<Search
:schema="DictDataSchemas.allSchemas.searchSchema"
@search="setDataSearchParams"
@reset="setDataSearchParams"
/>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:dict:create']" @click="handleDataCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<Table
:columns="DictDataSchemas.allSchemas.tableColumns"
:selection="false"
:data="dataTableObject.tableList"
:loading="dataTableObject.loading"
:pagination="{
total: dataTableObject.total
}"
v-model:pageSize="dataTableObject.pageSize"
v-model:currentPage="dataTableObject.currentPage"
@register="dataRegister"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:dict:update']"
@click="handleDataUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:dict:delete']"
@click="handleDataDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</div>
</el-card>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Form
v-if="['typeCreate', 'typeUpdate'].includes(actionType)"
:schema="DictTypeSchemas.allSchemas.formSchema"
:rules="DictTypeSchemas.dictTypeRules"
ref="typeFormRef"
/>
<Form
v-if="['dataCreate', 'dataUpdate'].includes(actionType)"
:schema="DictDataSchemas.allSchemas.formSchema"
:rules="DictDataSchemas.dictDataRules"
ref="dataFormRef"
/>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['typeCreate', 'typeUpdate'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitTypeForm"
>
{{ t('action.save') }}
</el-button>
<el-button
v-if="['dataCreate', 'dataUpdate'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitDataForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</div>
</template>

View File

@@ -0,0 +1,94 @@
import { reactive } from 'vue'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
import { DICT_TYPE } from '@/utils/dict'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
applicationName: [required],
code: [required],
message: [required]
})
// 新增 + 修改
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '错误码类型',
field: 'type',
component: 'InputNumber',
dictType: DICT_TYPE.SYSTEM_ERROR_CODE_TYPE,
search: {
show: true
}
},
{
label: '应用名',
field: 'applicationName',
search: {
show: true
}
},
{
label: '错误码编码',
field: 'code',
search: {
show: true
}
},
{
label: '错误码错误提示',
field: 'message'
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('common.createTime'),
field: 'daterange',
table: {
show: false
},
form: {
show: false
},
detail: {
show: false
},
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD'
}
}
},
{
field: 'action',
width: '240px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { ErrorCodeVO } from '@/api/system/errorCode/types'
import { rules, allSchemas } from './errorCode.data'
import * as ErrorCodeApi from '@/api/system/errorCode'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<ErrorCodeVO>, ErrorCodeVO>({
getListApi: ErrorCodeApi.getErrorCodePageApi,
delListApi: ErrorCodeApi.deleteErrorCodeApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: ErrorCodeVO) => {
setDialogTile('update')
// 设置数据
const res = await ErrorCodeApi.getErrorCodeApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as ErrorCodeVO
if (actionType.value === 'create') {
await ErrorCodeApi.createErrorCodeApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await ErrorCodeApi.updateErrorCodeApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
loading.value = false
}
}
// 删除操作
const handleDelete = (row: ErrorCodeVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: ErrorCodeVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button v-hasPermi="['system:error-code:create']" type="primary" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_ERROR_CODE_TYPE" :value="row.type" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:error-code:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:error-code:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:error-code:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_ERROR_CODE_TYPE" :value="row.type" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue'
import dayjs from 'dayjs'
import { useTable } from '@/hooks/web/useTable'
import { allSchemas } from './loginLog.data'
import { DICT_TYPE } from '@/utils/dict'
import type { LoginLogVO } from '@/api/system/loginLog/types'
import { getLoginLogPageApi, exportLoginLogApi } from '@/api/system/loginLog'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<LoginLogVO>, LoginLogVO>({
getListApi: getLoginLogPageApi,
exportListApi: exportLoginLogApi
})
const { getList, setSearchParams } = methods
// 详情操作
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
const detailRef = ref() // 详情 Ref
const handleDetail = async (row: LoginLogVO) => {
// 设置数据
detailRef.value = row
dialogVisible.value = true
}
getList()
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #logType="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="row.logType" />
</template>
<template #result="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="row.result" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button link type="primary" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle" maxHeight="500px" width="50%">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #logType="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="row.logType" />
</template>
<template #result="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="row.result" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,92 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
const { t } = useI18n() // 国际化
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '日志类型',
field: 'logType',
dictType: DICT_TYPE.SYSTEM_LOGIN_TYPE,
search: {
show: true
}
},
{
label: '用户名称',
field: 'username',
search: {
show: true
}
},
{
label: '登录地址',
field: 'userIp',
search: {
show: true
}
},
{
label: 'userAgent',
field: 'userAgent'
},
{
label: '登陆结果',
field: 'result',
dictType: DICT_TYPE.SYSTEM_LOGIN_RESULT,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('common.createTime'),
field: 'daterange',
table: {
show: false
},
form: {
show: false
},
detail: {
show: false
},
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD'
}
}
},
{
label: t('table.action'),
field: 'action',
width: '80px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,404 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { handleTree } from '@/utils/tree'
import dayjs from 'dayjs'
import { IconSelect } from '@/components/Icon'
import { Tooltip } from '@/components/Tooltip'
import * as MenuApi from '@/api/system/menu'
import { useI18n } from '@/hooks/web/useI18n'
import {
ElRow,
ElCol,
ElTable,
ElTableColumn,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElSelect,
ElOption,
ElMessageBox,
ElMessage,
ElCascader,
ElRadioGroup,
ElRadioButton
} from 'element-plus'
import { MenuVO } from '@/api/system/menu/types'
import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const { t } = useI18n() // 国际化
// ========== 创建菜单树结构 ==========
const loading = ref(true)
const menuData = ref([]) // 树形结构
const getList = async () => {
const res = await MenuApi.getMenuListApi(queryParams)
menuData.value = handleTree(res)
loading.value = false
}
const menuProps = {
checkStrictly: true,
children: 'children',
label: 'name',
value: 'id'
}
const menuOptions = ref([]) // 树形结构
const getTree = async () => {
const res = await MenuApi.listSimpleMenusApi()
menuOptions.value = handleTree(res)
}
// ========== 查询 ==========
const queryParams = reactive({
name: undefined,
status: undefined
})
// 查询操作
const handleQuery = async () => {
await getList()
}
// 重置操作
const resetQuery = async () => {
queryParams.name = undefined
queryParams.status = undefined
await getList()
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const menuForm = ref<MenuVO>({
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: 1,
parentId: 0,
path: '',
icon: '',
component: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
createTime: ''
})
// 表单校验
const rules = {
name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
}
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新建操作
const handleCreate = () => {
// 重置表单
setDialogTile('create')
}
// 修改操作
const handleUpdate = async (row: MenuVO) => {
// 设置数据
const res = await MenuApi.getMenuApi(row.id)
menuForm.value = res
setDialogTile('update')
}
// 删除操作
const handleDelete = async (row: MenuVO) => {
ElMessageBox.confirm(t('common.delDataMessage'), t('common.confirmTitle'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
await MenuApi.deleteMenuApi(row.id)
ElMessage.success(t('common.delSuccess'))
})
.catch(() => {})
await getList()
}
// 保存操作
function isExternal(path: string) {
return /^(https?:|mailto:|tel:)/.test(path)
}
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
if (
menuForm.value.type === SystemMenuTypeEnum.DIR ||
menuForm.value.type === SystemMenuTypeEnum.MENU
) {
if (!isExternal(menuForm.value.path)) {
if (menuForm.value.parentId === 0 && menuForm.value.path.charAt(0) !== '/') {
ElMessage.error('路径必须以 / 开头')
return
} else if (menuForm.value.parentId !== 0 && menuForm.value.path.charAt(0) === '/') {
ElMessage.error('路径不能以 / 开头')
return
}
}
}
if (actionType.value === 'create') {
await MenuApi.createMenuApi(menuForm.value)
ElMessage.success(t('common.createSuccess'))
} else {
await MenuApi.updateMenuApi(menuForm.value)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// ========== 初始化 ==========
onMounted(async () => {
await getList()
getTree()
})
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择菜单状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('common.query') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh-right" class="mr-5px" />
{{ t('common.reset') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:notice:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<el-table
v-loading="loading"
table-layout="auto"
row-key="id"
:data="menuData"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column label="菜单名称" prop="name" width="240px">
<template #default="scope">
<Icon :icon="scope.row.icon" />
<span class="ml-3">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="菜单类型" prop="type">
<template #default="scope">
<DictTag :type="DICT_TYPE.SYSTEM_MENU_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="路由地址" prop="path" />
<el-table-column label="组件路径" prop="component" />
<el-table-column label="权限标识" prop="permission" />
<el-table-column label="排序" prop="sort" />
<el-table-column label="状态" prop="status">
<template #default="scope">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime">
<template #default="scope">
<span>{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
link
type="primary"
v-hasPermi="['system:menu:update']"
@click="handleUpdate(scope.row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:menu:delete']"
@click="handleDelete(scope.row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<Dialog v-model="dialogVisible" :title="dialogTitle" maxHeight="400px" width="40%">
<el-form
:model="menuForm"
:rules="rules"
:inline="true"
label-width="120px"
label-position="right"
>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="上级菜单">
<el-cascader
:options="menuData"
:props="menuProps"
placeholder="请选择上级菜单"
v-model="menuForm.parentId"
class="w-100"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="menuForm.type">
<el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="menuForm.name" placeholder="请输入菜单名称" clearable />
</el-form-item>
</el-col>
<template v-if="menuForm.type !== 3">
<el-col :span="12">
<el-form-item label="菜单图标">
<IconSelect v-model="menuForm.icon" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="路由地址" prop="path">
<template #label>
<Tooltip
titel="路由地址"
message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
/>
</template>
<el-input v-model="menuForm.path" placeholder="请输入路由地址" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="12">
<el-form-item label="路由地址" prop="component">
<el-input v-model="menuForm.component" placeholder="请输入组件地址" clearable />
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type !== 1">
<el-col :span="12">
<el-form-item label="权限标识" prop="permission">
<template #label>
<Tooltip
titel="权限标识"
message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
/>
</template>
<el-input v-model="menuForm.permission" placeholder="请输入权限标识" clearable />
</el-form-item>
</el-col>
</template>
<el-col :span="12">
<el-form-item label="显示排序" prop="sort">
<el-input-number v-model="menuForm.sort" controls-position="right" :min="0" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单状态" prop="status">
<el-radio-group v-model="menuForm.status">
<el-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<template v-if="menuForm.type !== 3">
<el-col :span="12">
<el-form-item label="显示状态" prop="status">
<template #label>
<Tooltip
titel="显示状态"
message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问"
/>
</template>
<el-radio-group v-model="menuForm.visible">
<el-radio-button key="true" :label="true">显示</el-radio-button>
<el-radio-button key="false" :label="false">隐藏</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</template>
<template v-if="menuForm.type === 2">
<el-col :span="12">
<el-form-item label="缓存状态" prop="keepAlive">
<template #label>
<Tooltip
titel="缓存状态"
message="选择缓存时,则会被 `keep-alive` 缓存,需要匹配组件的 `name` 和路由地址保持一致"
/>
</template>
<el-radio-group v-model="menuForm.keepAlive">
<el-radio-button key="true" :label="true">缓存</el-radio-button>
<el-radio-button key="false" :label="false">不缓存</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>
<style lang="less" scoped>
:deep(.el-button.is-text) {
margin-left: 0;
padding: 8px 10px;
}
</style>

View File

@@ -0,0 +1,166 @@
import { reactive } from 'vue'
import { required } from '@/utils/formRules'
import { useI18n } from '@/hooks/web/useI18n'
// 国际化
const { t } = useI18n()
// 修改
export const modelSchema = reactive<FormSchema[]>([
{
label: '上级菜单',
field: 'parentId',
component: 'Input',
formItemProps: {
rules: [required]
}
},
{
label: '菜单类型',
field: 'type',
component: 'RadioButton',
formItemProps: {
rules: [required]
},
componentProps: {
options: [
{
label: '目录',
value: 1
},
{
label: '菜单',
value: 2
},
{
label: '按钮',
value: 3
}
]
}
},
{
label: '菜单图标',
field: 'icon',
component: 'Input',
formItemProps: {
rules: [required]
}
},
{
label: '菜单名称',
field: 'name',
component: 'Input',
formItemProps: {
rules: [required]
}
},
{
label: '显示排序',
field: 'sort',
component: 'Input'
},
{
label: '路由地址',
field: 'path',
component: 'Input',
labelMessage: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头'
},
{
label: '组件路径',
field: 'component',
component: 'Input'
},
{
label: '权限标识',
field: 'permission',
component: 'Input',
labelMessage:
'Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission(`system:user:list`)`)'
},
{
label: t('common.status'),
field: 'status',
component: 'RadioButton',
value: 0,
formItemProps: {
rules: [required]
},
componentProps: {
options: [
{
label: '开启',
value: 0
},
{
label: '关闭',
value: 1
}
]
},
labelMessage: '选择停用时,路由将不会出现在侧边栏,也不能被访问'
},
{
label: '是否显示',
field: 'visible',
component: 'RadioButton',
value: 0,
formItemProps: {
rules: [required]
},
componentProps: {
options: [
{
label: '显示',
value: 0
},
{
label: '隐藏',
value: 1
}
]
},
labelMessage: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问'
},
{
label: '是否缓存',
field: 'keepAlive',
component: 'RadioButton',
value: 0,
componentProps: {
options: [
{
label: '缓存',
value: 0
},
{
label: '不缓存',
value: 1
}
]
},
labelMessage: '选择缓存时,则会被 `keep-alive` 缓存,需要匹配组件的 `name` 和路由地址保持一致'
}
])
// 列表
export const columns = reactive<TableColumn[]>([
{
label: '菜单名称',
field: 'name'
},
{
label: '权限标识',
field: 'permission',
component: 'Input',
formItemProps: {
rules: [required]
}
},
{
label: '排序',
field: 'sort'
},
{
label: t('table.action'),
field: 'action',
width: '180px'
}
])

View File

@@ -0,0 +1,226 @@
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue'
import { handleTree } from '@/utils/tree'
import { useI18n } from '@/hooks/web/useI18n'
import { IconSelect } from '@/components/Icon'
import { ElCard, ElMessage, ElMessageBox, ElTree, ElTreeSelect } from 'element-plus'
import { columns, modelSchema } from './menu.data'
import { Form, FormExpose } from '@/components/Form'
import * as MenuApi from '@/api/system/menu'
import { MenuVO } from '@/api/system/menu/types'
const { t } = useI18n() // 国际化
interface Tree {
id: number
name: string
children?: Tree[]
}
const defaultProps = {
children: 'children',
label: 'name',
value: 'id'
}
// ========== 创建菜单树结构 ==========
const menuOptions = ref([]) // 树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
const getTree = async () => {
const res = await MenuApi.listSimpleMenusApi()
menuOptions.value = handleTree(res)
}
const filterNode = (value: string, data: Tree) => {
if (!value) return true
return data.name.includes(value)
}
// ========== 菜单信息form表单 ==========
const loading = ref(false) // 遮罩层
const formRef = ref<FormExpose>()
const iconModel = ref('ep:user')
const menuParentId = ref()
const menuStatus = ref('add')
const menuTitle = ref('菜单信息')
const showEdit = ref(false)
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as MenuVO
data.parentId = menuParentId.value
// TODO: 表单提交待完善
if (menuStatus.value === 'add') {
await MenuApi.createMenuApi(data)
} else if (menuStatus.value === 'edit') {
await MenuApi.updateMenuApi(data)
}
} finally {
loading.value = false
}
}
// ========== 按钮列表相关 ==========
const tableData = ref([])
const tableLoading = ref(false)
const onDisabled = ref(true)
const tableTitle = ref('按钮信息')
// 树点击事件
const handleMenuNodeClick = async (data: { [key: string]: any }) => {
showEdit.value = true
const res = await MenuApi.getMenuApi(data.id)
menuTitle.value = res.name + '-菜单信息'
tableTitle.value = res.name + '-按钮列表'
menuParentId.value = data.id
tableData.value = await MenuApi.getMenuListApi({ name: res.name })
unref(formRef)?.setValues(res)
onDisabled.value = true
changeDisabled()
}
const handleCreate = () => {
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
menuParentId.value = 0
onDisabled.value = false
changeDisabled()
}
const handleEdit = () => {
onDisabled.value = false
changeDisabled()
}
const changeDisabled = () => {
unref(formRef)?.setProps({
disabled: onDisabled
})
}
// 修改操作
const handleUpdate = async (row: MenuVO) => {
// 设置数据
const res = await MenuApi.getMenuApi(row.id)
unref(formRef)?.setValues(res)
}
// 删除操作
const handleDelete = (row: MenuVO) => {
ElMessageBox.confirm(t('common.delDataMessage'), t('common.confirmTitle'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
.then(async () => {
await MenuApi.deleteMenuApi(row.id)
ElMessage.success(t('common.delSuccess'))
})
.catch(() => {})
}
onMounted(async () => {
await getTree()
})
</script>
<template>
<div class="flex">
<el-card class="w-1/4 menu" :gutter="12" shadow="always">
<template #header>
<div class="card-header">
<span>菜单列表</span>
<el-button type="primary" v-hasPermi="['system:menu:create']" @click="handleCreate">
新增根节点
</el-button>
</div>
</template>
<!-- <p>菜单列表</p> -->
<el-tree
ref="treeRef"
node-key="id"
:accordion="true"
:data="menuOptions"
:props="defaultProps"
:highlight-current="true"
:filter-method="filterNode"
@node-click="handleMenuNodeClick"
/>
</el-card>
<el-card class="w-1/2 menu" style="margin-left: 10px" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ menuTitle }}</span>
</div>
</template>
<div v-if="!showEdit">
<span>请从左侧选择菜单</span>
</div>
<div v-if="showEdit">
<Form :loading="loading" :schema="modelSchema" ref="formRef">
<template #parentId>
<el-tree-select
node-key="id"
v-model="menuParentId"
:props="defaultProps"
:data="menuOptions"
check-strictly
/>
</template>
<template #icon>
<IconSelect v-model="iconModel" />
</template>
</Form>
<el-button
v-if="!onDisabled"
type="primary"
v-hasPermi="['system:menu:update']"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button v-if="!onDisabled" :loading="loading" @click="showEdit = false">
{{ t('common.cancel') }}
</el-button>
<el-button
v-if="onDisabled"
v-hasPermi="['system:menu:update']"
type="primary"
:loading="loading"
@click="handleEdit"
>
{{ t('action.edit') }}
</el-button>
</div>
</el-card>
<el-card class="w-1/2 menu" style="margin-left: 10px" :gutter="12" shadow="hover">
<template #header>
<div class="card-header">
<span>{{ tableTitle }}</span>
<!-- <el-button type="primary">新增根节点</el-button> -->
</div>
</template>
<!-- 列表 -->
<Table :loading="tableLoading" :columns="columns" :data="tableData">
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:menu:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:menu:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</el-card>
</div>
</template>
<style scoped>
.menu {
height: 1000px;
max-height: 1800px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { NoticeVO } from '@/api/system/notice/types'
import { rules, allSchemas } from './notice.data'
import * as NoticeApi from '@/api/system/notice'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<NoticeVO>, NoticeVO>({
getListApi: NoticeApi.getNoticePageApi,
delListApi: NoticeApi.deleteNoticeApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: NoticeVO) => {
setDialogTile('update')
// 设置数据
const res = await NoticeApi.getNoticeApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as NoticeVO
if (actionType.value === 'create') {
await NoticeApi.createNoticeApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await NoticeApi.updateNoticeApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: NoticeVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: NoticeVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:notice:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="row.type" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:notice:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:notice:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:notice:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle" maxHeight="500px" width="50%">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="row.type" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,86 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { DICT_TYPE } from '@/utils/dict'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
title: [required],
type: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '公告标题',
field: 'title',
search: {
show: true
}
},
{
label: '公告类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_NOTICE_TYPE,
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
},
form: {
component: 'RadioButton'
}
},
{
label: '公告内容',
field: 'content',
form: {
component: 'Editor',
colProps: {
span: 24
},
componentProps: {
valueHtml: ''
}
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,141 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n()
// 国际化
// 表单校验
export const rules = reactive({
clientId: [required],
secret: [required],
name: [required],
logo: [required],
status: [required],
accessTokenValiditySeconds: [required],
refreshTokenValiditySeconds: [required],
redirectUris: [required],
authorizedGrantTypes: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: '客户端编号',
field: 'clientId',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '客户端密钥',
field: 'secret'
},
{
label: '应用名',
field: 'name',
search: {
show: true
}
},
{
label: '应用图标',
field: 'logo'
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: '访问令牌的有效期',
field: 'accessTokenValiditySeconds'
},
{
label: '刷新令牌的有效期',
field: 'refreshTokenValiditySeconds'
},
{
label: '授权类型',
field: 'authorizedGrantTypes',
dictType: DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE
},
{
label: '授权范围',
field: 'scopes',
table: {
show: false
}
},
{
label: '自动授权范围',
field: 'autoApproveScopes',
table: {
show: false
}
},
{
label: '可重定向的 URI 地址',
field: 'redirectUris',
table: {
show: false
}
},
{
label: '权限',
field: 'authorities',
table: {
show: false
}
},
{
label: '资源',
field: 'resourceIds',
table: {
show: false
}
},
{
label: '附加信息',
field: 'additionalInformation',
table: {
show: false
},
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,187 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElImage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { OAuth2ClientVo } from '@/api/system/oauth2/client.types'
import { rules, allSchemas } from './client.data'
import * as ClientApi from '@/api/system/oauth2/client'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<OAuth2ClientVo>, OAuth2ClientVo>({
getListApi: ClientApi.getOAuth2ClientPageApi,
delListApi: ClientApi.deleteOAuth2ClientApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: OAuth2ClientVo) => {
setDialogTile('update')
// 设置数据
const res = await ClientApi.getOAuth2ClientApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as OAuth2ClientVo
if (actionType.value === 'create') {
await ClientApi.createOAuth2ClientApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await ClientApi.updateOAuth2ClientApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: OAuth2ClientVo) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: OAuth2ClientVo) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:oauth2-client:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #logo="{ row }">
<el-image :src="row.logo" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:oauth2-client:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:oauth2-client:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:oauth2-client:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,92 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { useTable } from '@/hooks/web/useTable'
import { allSchemas } from './token.data'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import type { OAuth2TokenVo } from '@/api/system/oauth2/token.types'
import * as TokenApi from '@/api/system/oauth2/token'
import { ref } from 'vue'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<OAuth2TokenVo>, OAuth2TokenVo>({
getListApi: TokenApi.getAccessTokenPageApi,
delListApi: TokenApi.deleteAccessTokenApi
})
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
const { getList, setSearchParams, delList } = methods
// 详情
const handleDetail = (row: OAuth2TokenVo) => {
// 设置数据
detailRef.value = row
dialogVisible.value = true
}
// 强退操作
const handleForceLogout = (row: OAuth2TokenVo) => {
delList(row.id, false)
}
getList()
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #expiresTime="{ row }">
<span>{{ dayjs(row.expiresTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button link type="primary" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:oauth2-token:delete']"
@click="handleForceLogout(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.logout') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #userType="{ row }">
<DictTag :type="DICT_TYPE.USER_TYPE" :value="row.userType" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #expiresTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,68 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '用户编号',
field: 'userId',
search: {
show: true
}
},
{
label: '访问令牌',
field: 'accessToken'
},
{
label: '刷新令牌',
field: 'refreshToken'
},
{
label: '用户类型',
field: 'userType',
dictType: DICT_TYPE.USER_TYPE,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: '过期时间',
field: 'expiresTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '200px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { useTable } from '@/hooks/web/useTable'
import { allSchemas } from './operateLog.data'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import type { OperateLogVO } from '@/api/system/operateLog/types'
import * as OperateLogApi from '@/api/system/operateLog'
import { ref } from 'vue'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<OperateLogVO>, OperateLogVO>({
getListApi: OperateLogApi.getOperateLogPageApi,
exportListApi: OperateLogApi.exportOperateLogApi
})
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref(t('action.detail')) // 弹出层标题
const { getList, setSearchParams, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('数据.xls')
}
// 详情
const handleDetail = (row: OperateLogVO) => {
// 设置数据
detailRef.value = row
dialogVisible.value = true
}
getList()
</script>
<template>
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button
type="warning"
v-hasPermi="['system:operate-log:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_OPERATE_TYPE" :value="row.type" />
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
<template #startTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button link type="primary" @click="handleDetail(row)">
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
<Descriptions :schema="allSchemas.detailSchema" :data="detailRef">
<template #resultCode="{ row }">
<span>{{ row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_OPERATE_TYPE" :value="row.type" />
</template>
<template #duration="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
<template #startTime="{ row }">
<span>{{ dayjs(row.startTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,112 @@
import { reactive } from 'vue'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
const { t } = useI18n() // 国际化
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
}
},
{
label: '操作模块',
field: 'module',
search: {
show: true
}
},
{
label: '操作名',
field: 'name'
},
{
label: '操作类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_OPERATE_TYPE,
search: {
show: true
}
},
{
label: '请求方法名',
field: 'requestMethod'
},
{
label: '请求地址',
field: 'requestUrl'
},
{
label: '操作人员',
field: 'userNickname'
},
{
label: '操作明细',
field: 'content',
table: {
show: false
}
},
{
label: '用户 IP',
field: 'userIp',
table: {
show: false
}
},
{
label: 'userAgent',
field: 'userAgent'
},
{
label: '操作结果',
field: 'resultCode'
},
{
label: '操作日期',
field: 'startTime',
form: {
show: false
}
},
{
label: '执行时长',
field: 'duration'
},
{
label: '操作日期',
field: 'daterange',
table: {
show: false
},
form: {
show: false
},
detail: {
show: false
},
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD'
}
}
},
{
label: t('table.action'),
field: 'action',
width: '80px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,198 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { PostVO } from '@/api/system/post/types'
import { rules, allSchemas } from './post.data'
import * as PostApi from '@/api/system/post'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<PostVO>, PostVO>({
getListApi: PostApi.getPostPageApi,
delListApi: PostApi.deletePostApi,
exportListApi: PostApi.exportPostApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('用户数据.xls')
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: PostVO) => {
setDialogTile('update')
// 设置数据
const res = await PostApi.getPostApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as PostVO
if (actionType.value === 'create') {
await PostApi.createPostApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await PostApi.updatePostApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: PostVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: PostVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:post:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['system:post:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,74 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '岗位名称',
field: 'name',
search: {
show: true
}
},
{
label: '岗位编码',
field: 'code',
search: {
show: true
}
},
{
label: '岗位顺序',
field: 'sort'
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,327 @@
<script setup lang="ts">
import { reactive, ref, unref } from 'vue'
import dayjs from 'dayjs'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { RoleVO } from '@/api/system/role/types'
import { rules, allSchemas } from './role.data'
import * as RoleApi from '@/api/system/role'
import Dialog from '@/components/Dialog/src/Dialog.vue'
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElMessage,
ElTree,
ElCard,
ElCheckbox
} from 'element-plus'
import { listSimpleMenusApi } from '@/api/system/menu'
import { listSimpleDeptApi } from '@/api/system/dept'
import { handleTree } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<RoleVO>, RoleVO>({
getListApi: RoleApi.getRolePageApi,
delListApi: RoleApi.deleteRoleApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: RoleVO) => {
setDialogTile('update')
// 设置数据
const res = await RoleApi.getRoleApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as RoleVO
if (actionType.value === 'create') {
await RoleApi.createRoleApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await RoleApi.updateRoleApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
loading.value = false
}
}
// 删除操作
const handleDelete = (row: RoleVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: RoleVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 数据权限 ==========
const dataScopeForm = reactive({
name: '',
code: '',
dataScope: 0,
checkStrictly: true,
checkList: []
})
const defaultProps = {
children: 'children',
label: 'name',
value: 'id'
}
const treeOptions = ref([]) // 菜单树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
const dialogScopeVisible = ref(false)
const dialogScopeTitle = ref('数据权限')
const actionScopeType = ref('')
const dataScopeDictDatas = ref()
// 选项
const treeNodeAll = ref(false)
// 权限操作
const handleScope = async (type: string, row: RoleVO) => {
dataScopeForm.name = row.name
dataScopeForm.code = row.code
if (type === 'menu') {
const menuRes = await listSimpleMenusApi()
treeOptions.value = handleTree(menuRes)
dataScopeDictDatas.value = getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)
} else if (type === 'dept') {
const deptRes = await listSimpleDeptApi()
treeOptions.value = handleTree(deptRes)
}
actionScopeType.value = type
dialogScopeVisible.value = true
}
// 树权限(父子联动)
const handleCheckedTreeConnect = (value) => {
dataScopeForm.checkStrictly = value ? true : false
}
// 全选/全不选
const handleCheckedTreeNodeAll = (value) => {
treeRef.value?.setCheckedNodes(value ? dataScopeForm.checkList : [])
}
// TODO:保存
const submitScope = () => {
console.info()
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:role:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_ROLE_TYPE" :value="row.type" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:role:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:role:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:permission:assign-role-menu']"
@click="handleScope('menu', row)"
>
<Icon icon="ep:basketball" class="mr-5px" /> 菜单权限
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:permission:assign-role-data-scope']"
@click="handleScope('data', row)"
>
<Icon icon="ep:coin" class="mr-5px" /> 数据权限
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:role:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #type="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_ROLE_TYPE" :value="row.type" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
<Dialog v-model="dialogScopeVisible" :title="dialogScopeTitle">
<el-form :model="dataScopeForm">
<el-form-item label="角色名称">
<el-input v-model="dataScopeForm.name" :disabled="true" />
</el-form-item>
<el-form-item label="角色标识">
<el-input v-model="dataScopeForm.code" :disabled="true" />
</el-form-item>
<!-- 分配角色的数据权限对话框 -->
<el-form-item label="权限范围" v-if="actionScopeType === 'data'">
<el-select v-model="dataScopeForm.dataScope">
<el-option
v-for="item in dataScopeDictDatas"
:key="parseInt(item.value)"
:label="item.label"
:value="parseInt(item.value)"
/>
</el-select>
</el-form-item>
<!-- 分配角色的菜单权限对话框 -->
<el-form-item
label="权限范围"
v-if="
actionScopeType === 'menu' || dataScopeForm.dataScope === SystemDataScopeEnum.DEPT_CUSTOM
"
>
<el-card class="box-card">
<template #header>
<el-checkbox
:checked="!dataScopeForm.checkStrictly"
@change="handleCheckedTreeConnect($event)"
>父子联动(选中父节点,自动选择子节点)
</el-checkbox>
<el-checkbox v-model="treeNodeAll" @change="handleCheckedTreeNodeAll($event)">
全选/全不选
</el-checkbox>
</template>
<el-tree
ref="treeRef"
node-key="id"
show-checkbox
default-expand-all
:check-strictly="dataScopeForm.checkStrictly"
:props="defaultProps"
:data="treeOptions"
empty-text="加载中,请稍后"
/>
</el-card>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<el-button type="primary" :loading="loading" @click="submitScope">
{{ t('action.save') }}
</el-button>
<el-button @click="dialogScopeVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,89 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { DICT_TYPE } from '@/utils/dict'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
// 国际化
const { t } = useI18n()
// 表单校验
export const rules = reactive({
name: [required],
code: [required],
sort: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '角色名称',
field: 'name',
search: {
show: true
}
},
{
label: '角色类型',
field: 'type',
dictType: DICT_TYPE.SYSTEM_ROLE_TYPE
},
{
label: '角色标识',
field: 'code',
search: {
show: true
}
},
{
label: '显示顺序',
field: 'sort',
form: {
component: 'InputNumber',
value: 0
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
},
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD'
}
}
},
{
field: 'action',
width: '450px',
label: t('table.action'),
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,222 @@
<script setup lang="ts">
import { onMounted, ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage, ElTag, ElSelect, ElOption } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { SensitiveWordVO } from '@/api/system/sensitiveWord/types'
import { rules, allSchemas } from './sensitiveWord.data'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<SensitiveWordVO>, SensitiveWordVO>({
getListApi: SensitiveWordApi.getSensitiveWordPageApi,
delListApi: SensitiveWordApi.deleteSensitiveWordApi,
exportListApi: SensitiveWordApi.exportSensitiveWordApi
})
const { getList, setSearchParams, delList, exportList } = methods
// 导出操作
const handleExport = async () => {
await exportList('敏感词数据.xls')
}
// 获取标签
const tagsOptions = ref()
const getTags = async () => {
const res = await SensitiveWordApi.getSensitiveWordTagsApi()
tagsOptions.value = res
}
// ========== CRUD 相关 ==========
const actionLoading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
const tags = ref()
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: SensitiveWordVO) => {
setDialogTile('update')
// 设置数据
const res = await SensitiveWordApi.getSensitiveWordApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
actionLoading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SensitiveWordVO
if (actionType.value === 'create') {
await SensitiveWordApi.createSensitiveWordApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await SensitiveWordApi.updateSensitiveWordApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
actionLoading.value = false
}
}
// 删除操作
const handleDelete = (row: SensitiveWordVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: SensitiveWordVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
onMounted(async () => {
await getTags()
await getList()
})
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:post:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
<el-button
type="warning"
v-hasPermi="['system:post:export']"
:loading="tableObject.exportLoading"
@click="handleExport"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('action.export') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #tags="{ row }">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(tag, index) in row.tags"
:index="index"
>
{{ tag }}
</el-tag>
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
>
<template #tags>
<el-select v-model="tags" multiple placeholder="请选择">
<el-option v-for="item in tagsOptions" :key="item" :label="item" :value="item" />
</el-select>
</template>
</Form>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="actionLoading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,79 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
name: [required],
tags: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '敏感词',
field: 'name',
search: {
show: true
}
},
{
label: '标签',
field: 'tags'
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: '描述',
field: 'description',
form: {
component: 'Input',
componentProps: {
type: 'textarea',
rows: 4
},
colProps: {
span: 24
}
}
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

View File

@@ -0,0 +1,190 @@
<script setup lang="ts">
import { ref, unref } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import { DICT_TYPE } from '@/utils/dict'
import { useTable } from '@/hooks/web/useTable'
import { useI18n } from '@/hooks/web/useI18n'
import { FormExpose } from '@/components/Form'
import type { SmsChannelVO } from '@/api/system/sms/smsChannel/types'
import { rules, allSchemas } from './sms.channel.data'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
const { t } = useI18n() // 国际化
// ========== 列表相关 ==========
const { register, tableObject, methods } = useTable<PageResult<SmsChannelVO>, SmsChannelVO>({
getListApi: SmsChannelApi.getSmsChannelPageApi,
delListApi: SmsChannelApi.deleteSmsChannelApi
})
const { getList, setSearchParams, delList } = methods
// ========== CRUD 相关 ==========
const loading = ref(false) // 遮罩层
const actionType = ref('') // 操作按钮的类型
const dialogVisible = ref(false) // 是否显示弹出层
const dialogTitle = ref('edit') // 弹出层标题
const formRef = ref<FormExpose>() // 表单 Ref
// 设置标题
const setDialogTile = (type: string) => {
dialogTitle.value = t('action.' + type)
actionType.value = type
dialogVisible.value = true
}
// 新增操作
const handleCreate = () => {
setDialogTile('create')
// 重置表单
unref(formRef)?.getElFormRef()?.resetFields()
}
// 修改操作
const handleUpdate = async (row: SmsChannelVO) => {
setDialogTile('update')
// 设置数据
const res = await SmsChannelApi.getSmsChannelApi(row.id)
unref(formRef)?.setValues(res)
}
// 提交按钮
const submitForm = async () => {
loading.value = true
// 提交请求
try {
const data = unref(formRef)?.formModel as SmsChannelVO
if (actionType.value === 'create') {
await SmsChannelApi.createSmsChannelApi(data)
ElMessage.success(t('common.createSuccess'))
} else {
await SmsChannelApi.updateSmsChannelApi(data)
ElMessage.success(t('common.updateSuccess'))
}
// 操作成功,重新加载列表
dialogVisible.value = false
await getList()
} finally {
loading.value = false
}
}
// 删除操作
const handleDelete = (row: SmsChannelVO) => {
delList(row.id, false)
}
// ========== 详情相关 ==========
const detailRef = ref() // 详情 Ref
// 详情操作
const handleDetail = async (row: SmsChannelVO) => {
// 设置数据
detailRef.value = row
setDialogTile('detail')
}
// ========== 初始化 ==========
getList()
</script>
<template>
<!-- 搜索工作区 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams" />
</ContentWrap>
<ContentWrap>
<!-- 操作工具栏 -->
<div class="mb-10px">
<el-button type="primary" v-hasPermi="['system:sms-channel:create']" @click="handleCreate">
<Icon icon="el:zoom-in" class="mr-5px" /> {{ t('action.add') }}
</el-button>
</div>
<!-- 列表 -->
<Table
:columns="allSchemas.tableColumns"
:selection="false"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
@register="register"
>
<template #code="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="row.code" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #action="{ row }">
<el-button
link
type="primary"
v-hasPermi="['system:sms-channel:update']"
@click="handleUpdate(row)"
>
<Icon icon="ep:edit" class="mr-5px" /> {{ t('action.edit') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:sms-channel:update']"
@click="handleDetail(row)"
>
<Icon icon="ep:view" class="mr-5px" /> {{ t('action.detail') }}
</el-button>
<el-button
link
type="primary"
v-hasPermi="['system:sms-channel:delete']"
@click="handleDelete(row)"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('action.del') }}
</el-button>
</template>
</Table>
</ContentWrap>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
<Form
v-if="['create', 'update'].includes(actionType)"
:schema="allSchemas.formSchema"
:rules="rules"
ref="formRef"
/>
<!-- 对话框(详情) -->
<Descriptions
v-if="actionType === 'detail'"
:schema="allSchemas.detailSchema"
:data="detailRef"
>
<template #code="{ row }">
<DictTag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="row.code" />
</template>
<template #status="{ row }">
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</template>
<template #createTime="{ row }">
<span>{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</Descriptions>
<!-- 操作按钮 -->
<template #footer>
<el-button
v-if="['create', 'update'].includes(actionType)"
type="primary"
:loading="loading"
@click="submitForm"
>
{{ t('action.save') }}
</el-button>
<el-button @click="dialogVisible = false">{{ t('dialog.close') }}</el-button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,104 @@
import { reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { required } from '@/utils/formRules'
import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
import { DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
signature: [required],
code: [required],
apiKey: [required],
status: [required]
})
// CrudSchema
const crudSchemas = reactive<CrudSchema[]>([
{
label: t('common.index'),
field: 'id',
type: 'index',
form: {
show: false
},
detail: {
show: false
}
},
{
label: '短信签名',
field: 'signature',
search: {
show: true
}
},
{
label: '渠道编码',
field: 'code',
dictType: DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE,
search: {
show: true
}
},
{
label: t('common.status'),
field: 'status',
dictType: DICT_TYPE.COMMON_STATUS,
search: {
show: true
}
},
{
label: '短信 API 的账号',
field: 'apiKey'
},
{
label: '短信 API 的密钥',
field: 'apiSecret'
},
{
label: '短信发送回调 URL',
field: 'callbackUrl'
},
{
label: t('common.createTime'),
field: 'createTime',
form: {
show: false
}
},
{
label: t('common.createTime'),
field: 'daterange',
table: {
show: false
},
form: {
show: false
},
detail: {
show: false
},
search: {
show: true,
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD'
}
}
},
{
label: t('table.action'),
field: 'action',
width: '240px',
form: {
show: false
},
detail: {
show: false
}
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)

Some files were not shown because too many files have changed in this diff Show More