This commit is contained in:
2025-10-17 10:31:13 +08:00
commit e6e86f2ce0
1043 changed files with 1031839 additions and 0 deletions

8
src/views/Error/403.vue Normal file
View File

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

7
src/views/Error/404.vue Normal file
View File

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

7
src/views/Error/500.vue Normal file
View File

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

384
src/views/Home/Index.vue Normal file
View File

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

1800
src/views/Home/Index10.vue Normal file

File diff suppressed because it is too large Load Diff

466
src/views/Home/Index11.vue Normal file
View File

@@ -0,0 +1,466 @@
<template>
<div class="w-100%" v-loading="loading">
<div class="search-top flex justify-right">
<!-- <div class="search-top flex justify-between"> -->
<!-- 租户搜索 -->
<!-- <div>
<span class="text-13px c-#656565"> 租户名称</span>
<el-select
v-model="zhName"
placeholder="请选择"
size="large"
style="width: 264px"
@change="selectChange"
>
<el-option
v-for="item in zhOption"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div> -->
<!-- 切换日期 -->
<div class="flex">
<div class="w-200px grid grid-cols-3 mr-20px items-center">
<div
class="text-13px text-center cursor-pointer"
:style="{ color: btnActive == item.name ? '#692AFB' : '#656565' }"
v-for="item in btnList"
:key="item.type"
@click="btnClick(item)"
>
{{ item.name }}
</div>
</div>
<div>
<el-date-picker
v-model="pickerValue"
type="daterange"
range-separator="~"
start-placeholder="Start date"
end-placeholder="End date"
value-format="YYYY-MM-DD"
size="default"
:editable="false"
@change="pirckerChange"
/>
</div>
</div>
</div>
<div class="mt-20px">
<div
class="text-13px c-#333333 font-700 bg-white h-45px pl-20px flex items-center b-1px b-solid b-#E4E4E4 b-t-0 b-l-0 b-r-0 .dark:b-#6B7280 .dark:c-#6B7280 .dark:bg-#1D1E1F"
>
登录次数/使用人数对比分析
</div>
<Echart :options="login_option" width="100%" height="423px" />
</div>
<div class="grid grid-cols-2 gap-x-20px mt-30px">
<div class="bg-#fff b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid .dark:bg-#1D1E1F">
<div
class="h-45px flex items-center text-13px c-#333333 font-700 pl-20px b-1px b-solid b-#E4E4E4 b-t-0 b-l-0 b-r-0 .dark:c-#6B7280 .dark:b-#6B7280"
style="font-family: Arial-BoldMT, 'Arial Bold', Arial, sans-serif"
>
使用人数最多的模块top5
</div>
<div class="p-20px">
<avue-crud :option="mostOption" :data="mostData"></avue-crud>
</div>
</div>
<div class="bg-#fff b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid">
<div
class="h-45px flex items-center text-13px c-#333333 font-700 pl-20px b-1px b-solid b-#E4E4E4 b-t-0 b-l-0 b-r-0 .dark:c-#6B7280 .dark:b-#6B7280"
style="font-family: Arial-BoldMT, 'Arial Bold', Arial, sans-serif"
>
使用人数最少的模块bottom5
</div>
<div class="p-20px">
<avue-crud :option="mostOption" :data="leastData"></avue-crud>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/store/modules/app'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { getTableList, batchGetTableList } from '@/api/design/report'
import { formatDate } from '@/utils/formatTime'
import Icon from '@/components/Icon/src/Icon.vue'
defineOptions({ name: 'Home11' })
interface LoginXdata {
value: string | number
textStyle: {
color: string
fontSize: number
}
}
interface TabData {
mk: string
pm: number
syyhs: number
zzf: string
zzfFlag: number
}
const cardCellColor = ref('rgb(0 0 0 / 64.7%)')
const loading = ref(false)
const zhName = ref<string>('')
// 租户字典
const zhOption = ref([
// {
// value: 'Option1',
// label: 'Option1'
// }
])
// 租户字典的change
const selectChange = (val) => {
}
// 获取日期
let now = new Date()
const pickerValue = ref<Date | [Date, Date] | [string, string]>()
const dayArr = ref<[string, string]>()
const weekArr = ref<[string, string]>()
const monthArr = ref<[string, string]>()
const getDay = () => {
let day = formatDate(now, 'YYYY-MM-DD')
dayArr.value = [day, day]
}
const getWeek = () => {
var firstDayOfWeek = new Date(now)
firstDayOfWeek.setDate(firstDayOfWeek.getDate() - firstDayOfWeek.getDay() + 1)
var lastDayOfWeek = new Date(firstDayOfWeek)
lastDayOfWeek.setDate(firstDayOfWeek.getDate() + 6)
var first =
firstDayOfWeek.getFullYear() +
'-' +
String(firstDayOfWeek.getMonth() + 1).padStart(2, '0') +
'-' +
String(firstDayOfWeek.getDate()).padStart(2, '0')
var last =
lastDayOfWeek.getFullYear() +
'-' +
String(lastDayOfWeek.getMonth() + 1).padStart(2, '0') +
'-' +
String(lastDayOfWeek.getDate()).padStart(2, '0')
weekArr.value = [first, last]
}
const getMonth = () => {
let month = now.getMonth()
let year = now.getFullYear()
var first = formatDate(new Date(year, month, 1), 'YYYY-MM-DD')
var last = formatDate(new Date(year, month + 1, 0), 'YYYY-MM-DD')
monthArr.value = [first, last]
}
// 切换日期
const btnList = ref([
{ name: '本日', type: 1 },
{ name: '本周', type: 2 },
{ name: '本月', type: 3 }
])
const btnActive = ref('本月')
const btnClick = (val) => {
btnActive.value = val.name
if (val.type == 1) {
pickerValue.value = dayArr.value
} else if (val.type == 2) {
pickerValue.value = weekArr.value
} else if (val.type == 3) {
pickerValue.value = monthArr.value
}
}
const pirckerChange = (val) => {
btnActive.value = ''
}
// 登录分析柱形图
const loginSeries1 = ref<number[]>([])
const loginSeries2 = ref<number[]>([])
const loginX = ref<any>([])
const loginXutil = ref({
color: 'rgba(0, 0, 0, 0.647058823529412)',
fontSize: 14
})
const loginXdata = ref<LoginXdata[]>([])
const login_option = ref({
backgroundColor: 'white',
grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true },
tooltip: { trigger: 'item' },
legend: {
left: 'center',
bottom: '3%',
itemGap: 20,
itemWidth: 34,
itemHeight: 13,
textStyle: {
color: '#656565'
},
data: ['登录次数', '使用人数']
},
xAxis: {
axisTick: {
alignWithLabel: true
},
data: loginXdata.value
},
yAxis: {
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#E8E8E8'
}
}
},
series: [
{
name: '登录次数',
type: 'bar',
color: 'rgba(105, 42, 251, 1)',
itemStyle: {
borderRadius: [25, 25, 0, 0]
},
barGap: 0,
data: loginSeries1.value
},
{
name: '使用人数',
type: 'bar',
color: 'rgba(54, 203, 203, 0.996078431372549)',
itemStyle: {
borderRadius: [25, 25, 0, 0]
},
data: loginSeries2.value
}
]
})
const mostOption = ref({
menu: false,
header: false,
border: false,
height: 350,
column: [
{
// headerAlign: 'right',
label: '排名',
prop: 'pm'
},
{
// headerAlign: 'left',
label: '模块',
prop: 'mk'
},
{
// headerAlign: 'left',
label: '使用用户数',
prop: 'syyhs'
},
{
label: '模块使用数',
prop: 'mksys'
}
// {
// headerAlign: 'right',
// label: '周涨幅',
// prop: 'zzf',
// render: ({ row }) => {
// let icon
// if (row.zzfFlag == 1) {
// icon = h(Icon, { icon: 'ep:top', size: 20, color: '#00A854' })
// } else {
// icon = h(Icon, { icon: 'ep:bottom', size: 20, color: '#F04134' })
// }
// return h(
// 'div',
// {
// style: { display: 'flex', alignItems: 'center', justifyContent: 'right' }
// },
// [h('span', { style: { marginRight: '5px' } }, row.zzf), icon]
// )
// }
// }
]
})
const mostData = ref<TabData[]>()
const leastData = ref<TabData[]>()
const appStore = useAppStore()
const init = async () => {
let oneres = await batchGetTableList('example_sytj_syzdmk,example_sytj_syzsmk')
mostData.value = oneres.example_sytj_syzdmk.records
leastData.value = oneres.example_sytj_syzsmk.records
}
onMounted(async () => {
getDay()
getWeek()
getMonth()
let item = btnList.value.find((r) => r.name == btnActive.value)
if (item) {
let arr = [dayArr.value, weekArr.value, monthArr.value]
pickerValue.value = arr[item.type - 1]
} else {
pickerValue.value = monthArr.value
}
await init()
// 判断是否为暗色模式
const { wsCache } = useCache()
if (wsCache.get(CACHE_KEY.IS_DARK)) {
cardCellColor.value = ''
login_option.value.backgroundColor = '#1D1E1F'
login_option.value.legend.textStyle.color = '#6B7280'
loginXdata.value.forEach((ele) => {
ele.textStyle.color = '#6B7280'
})
}
})
watch(
() => appStore.isDark,
(val) => {
if (val) {
cardCellColor.value = ''
login_option.value.backgroundColor = '#1D1E1F'
login_option.value.legend.textStyle.color = '#6B7280'
loginXdata.value.forEach((ele) => {
ele.textStyle.color = '#6B7280'
})
} else {
cardCellColor.value = 'rgb(0 0 0 / 64.7%)'
login_option.value.backgroundColor = 'white'
login_option.value.legend.textStyle.color = '#656565'
loginXdata.value.forEach((ele) => {
ele.textStyle.color = 'rgba(0, 0, 0, 0.647058823529412)'
})
}
}
)
watch(
() => pickerValue.value,
async (val) => {
if (!val) return
let date = val?.[0] + ',' + val?.[1]
loading.value = true
let res = await getTableList('example_sytj_dlsyrs', { sj: date })
let dlsyrs = res.records
if (dlsyrs && dlsyrs.length > 0) {
loginXdata.value = []
loginX.value = []
loginSeries1.value = []
loginSeries2.value = []
dlsyrs.forEach((ele) => {
loginX.value.push(ele.sj)
loginSeries1.value.push(ele.login_num)
loginSeries2.value.push(ele.users_num)
})
loginX.value.forEach((ele) => {
if (val?.[0] == val?.[1]) {
loginXdata.value.push({
value: ele + ':00',
textStyle: loginXutil.value
})
} else {
loginXdata.value.push({
value: ele,
textStyle: loginXutil.value
})
}
})
login_option.value.xAxis.data = loginXdata.value
login_option.value.series[0].data = loginSeries1.value
login_option.value.series[1].data = loginSeries2.value
loading.value = false
}
}
)
</script>
<style lang="scss" scoped>
h5,
p {
padding: 0;
margin: 0;
}
.search-top {
::v-deep(.el-select) {
.el-select__wrapper {
min-height: 32px;
}
}
::v-deep(.el-date-editor) {
width: 280px;
.el-range__icon {
font-size: 12px;
}
.el-range-input {
font-size: 12px;
color: #666;
}
.el-range-separator {
font-size: 12px;
color: #999;
}
}
}
::v-deep(.avue-crud) {
.el-form {
.el-table__inner-wrapper {
.el-table__header-wrapper {
.el-table__cell {
height: 57px;
.cell {
font-family: 'Microsoft Tai Le Bold', 'Microsoft Tai Le Regular', 'Microsoft Tai Le',
sans-serif;
color: v-bind('cardCellColor');
}
}
}
.el-table__body {
.el-table__row {
height: 57px;
&:last-child {
.el-table__cell {
border-bottom: none;
}
}
}
}
&::before {
height: 0;
}
}
}
}
</style>

319
src/views/Home/Index2.vue Normal file
View File

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

347
src/views/Home/Index3.vue Normal file
View File

@@ -0,0 +1,347 @@
<template>
<div class="w-100%">
<div class="flex justify-between items-center">
<div
class="w-[calc(50%-50px)] py-16px px-20px bg-#fff border-rd-10px b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<h5 class="text">服务器基本信息</h5>
<div class="w-100% inline-grid grid-cols-4 gap-4 text-center">
<div class="left-bottom-box" v-for="(item, index) in topLeftData" :key="index">
<p>{{ item.title }}</p>
<div>
<span>{{ item.num }}</span> {{ item.dw }}
</div>
</div>
</div>
</div>
<div
class="flex w-[calc(50%-50px)] px-20px bg-#fff border-rd-10px justify-between items-center b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<div class="conter-right-box" v-for="(item, index) in arr" :key="index">
<p class="text-conter">{{ item.title }}</p>
<div class="chart">
<Echart :options="syl_pie_option" width="140px" height="130px"></Echart>
</div>
</div>
<!-- <div class="conter-right-box">
<p class="text-conter">内存使用率</p>
<div class="chart">
<Echart :options="syl_pie_option" width="140px" height="130px"></Echart>
</div>
</div>
<div class="conter-right-box">
<p class="text-conter">系统平均负载1m</p>
<div class="chart">
<Echart :options="syl_pie_option" width="140px" height="130px"></Echart>
</div>
</div> -->
</div>
</div>
<div class="flex justify-between mt-20px">
<div
class="w-[calc(50%-50px)] bg-#fff border-rd-10px p-20px b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<Echart :options="cpu_line_option" width="100%" height="350px" />
</div>
<div
class="w-[calc(50%-50px)] bg-#fff border-rd-10px p-20px b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<Echart :options="nc_line_option" width="100%" height="350px" />
</div>
</div>
<div class="flex justify-between mt-20px">
<div
class="w-[calc(50%-50px)] bg-#fff border-rd-10px p-20px b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<Echart :options="ll_line_option" width="100%" height="350px" />
</div>
<div
class="w-[calc(50%-50px)] bg-#fff border-rd-10px p-20px b-1px .dark:bg-#1D1E1F .dark:b-#2A2B2C .dark:b-solid"
>
<Echart :options="cp_line_option" width="100%" height="350px" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/store/modules/app'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { batchGetTableList } from '@/api/design/report'
defineOptions({ name: 'Home3' })
interface TopLeftData {
title: string
dw: string
dictionary: string
num?: number
}
const topLeftData = ref<TopLeftData[]>([
{ title: '系统运行时间', dw: '天', dictionary: 'xtyxsj' },
{ title: 'cpu核心数', dw: '', dictionary: 'cpuhxs' },
{ title: '内存总量', dw: 'G', dictionary: 'nczl' },
{ title: '系统平均负数', dw: '', dictionary: 'xtpjfz' }
])
const arr = ref<any>([
{ title: 'CPU使用率1m', dictionary: 'cpusyl' },
{ title: '内存使用率', dictionary: 'ncsyl' },
{ title: '系统平均负载1m', dictionary: 'xtpjfzl' }
])
const syl_pie_option = ref<any>({
title: { text: '50%', left: 'center', top: 'center' },
series: [
{
type: 'pie',
stillShowZeroSum: false,
label: { show: false },
hoverAnimation: false,
data: [
{ value: 100, name: 'A' },
{ value: 0, name: 'B' }
],
itemStyle: {
normal: {
color: function (params) {
var colors = ['#e9e9e9', '#0099ff', '#51d351']
return colors[params.dataIndex % colors.length]
}
}
},
radius: ['50%', '70%']
}
]
})
const cpuLineXAxis = ref<string[]>([])
const cpuLineSeries = ref<string[]>([])
const cpu_line_option = reactive<any>({
title: {
text: 'CPU使用率',
textStyle: {}
},
tooltip: {},
legend: { data: ['使用率'], x: 'right', selectedMode: false },
xAxis: {
data: cpuLineXAxis.value
},
yAxis: { type: 'value' },
series: [
{
name: '使用率',
data: cpuLineSeries.value,
type: 'line',
stack: 'x',
areaStyle: { color: '#ffd7dc' },
itemStyle: { normal: { color: '#ffd7dc', lineStyle: { color: '#ff7a8c' } } }
}
]
})
const ncLineXAxis = ref<string[]>([])
const ncLineSeries = ref<string[]>([])
const nc_line_option = reactive<any>({
title: {
text: '内存使用率',
textStyle: {}
},
tooltip: {},
legend: { data: ['使用率'], x: 'right', selectedMode: false },
xAxis: {
data: ncLineXAxis.value
},
yAxis: { type: 'value' },
series: [
{
name: '使用率',
data: ncLineSeries.value,
type: 'line',
stack: 'x',
areaStyle: { color: '#b2e0ff' },
itemStyle: { normal: { color: '#b2e0ff', lineStyle: { color: '#0099ff' } } }
}
]
})
const llLineXAxis = ref<string[]>([])
const llLineSeries1 = ref<string[]>([])
const llLineSeries2 = ref<string[]>([])
const ll_line_option = reactive<any>({
title: {
text: '服务器流量',
textStyle: {}
},
tooltip: {},
legend: { data: ['上传', '下载'], x: 'right', selectedMode: false },
xAxis: { data: llLineXAxis.value },
yAxis: { type: 'value' },
series: [
{
name: '上传',
data: llLineSeries1.value,
type: 'line',
// stack: 'x',
areaStyle: { color: '#caf1ca' },
itemStyle: { normal: { color: '#51d351', lineStyle: { color: '#51d351' } } }
},
{
name: '下载',
data: llLineSeries2.value,
type: 'line',
// stack: 'x',
areaStyle: { color: '#ffe5c9' },
itemStyle: { normal: { color: '#ffb465', lineStyle: { color: '#ffb465' } } }
}
]
})
const cpLineXAxis = ref<string[]>([])
const cpLineSeries1 = ref<string[]>([])
const cpLineSeries2 = ref<string[]>([])
const cp_line_option = reactive<any>({
title: {
text: '服务器磁盘IO',
textStyle: {}
},
tooltip: {},
legend: { data: ['输出', '输入'], x: 'right', selectedMode: false },
xAxis: { data: cpLineXAxis.value },
yAxis: { type: 'value' },
series: [
{
name: '输出',
data: cpLineSeries1.value,
type: 'line',
// stack: 'x',
areaStyle: { color: '#bdf1f1' },
itemStyle: { normal: { color: '#78e3e4', lineStyle: { color: '#78e3e4' } } }
},
{
name: '输入',
data: cpLineSeries2.value,
type: 'line',
// stack: 'x',
areaStyle: { color: '#d9d1fc' },
itemStyle: { normal: { color: '#8167f5', lineStyle: { color: '#8167f5' } } }
}
]
})
const textStyle = reactive({
color: ''
})
const appStore = useAppStore()
const init = async () => {
let oneres = await batchGetTableList(
'example_systemmonitor_server_information,example_systemmonitor_cpu_utilization,example_systemmonitor_memory_utilization,example_systemmonitor_server_traffic,example_systemmonitor_disk_io'
)
let serverInformation = oneres.example_systemmonitor_server_information.records[0]
topLeftData.value = topLeftData.value.map((item) => {
return (item = {
...item,
num: serverInformation[item.dictionary]
})
})
arr.value = arr.value.map((item) => {
syl_pie_option.value.title.text = serverInformation[item.dictionary] + '%'
syl_pie_option.value.series[0].data[1].value = serverInformation[item.dictionary]
return item
})
//CPU使用率
let cpuUtilization = oneres.example_systemmonitor_cpu_utilization.records
cpuUtilization.forEach((ele) => {
cpuLineXAxis.value.push(ele.sj)
cpuLineSeries.value.push(ele.syl)
})
//内存使用率
let memoryUtilization = oneres.example_systemmonitor_memory_utilization.records
memoryUtilization.forEach((ele) => {
ncLineXAxis.value.push(ele.sj)
ncLineSeries.value.push(ele.syl)
})
//服务器流量
let serverTraffic = oneres.example_systemmonitor_server_traffic.records
serverTraffic.forEach((ele) => {
llLineXAxis.value.push(ele.sj)
llLineSeries1.value.push(ele.sc)
llLineSeries2.value.push(ele.xz)
})
//服务器磁盘IO
let diskIo = oneres.example_systemmonitor_disk_io.records
diskIo.forEach((ele) => {
cpLineXAxis.value.push(ele.sj)
cpLineSeries1.value.push(ele.sc)
cpLineSeries2.value.push(ele.sr)
})
}
onMounted(async () => {
await init()
// 判断是否为暗色模式
const { wsCache } = useCache()
if (wsCache.get(CACHE_KEY.IS_DARK)) {
textStyle.color = '#E5EAF3'
cpu_line_option.title.textStyle = textStyle
nc_line_option.title.textStyle = textStyle
ll_line_option.title.textStyle = textStyle
cp_line_option.title.textStyle = textStyle
}
})
watch(
() => appStore.isDark,
(val) => {
if (val) {
textStyle.color = '#E5EAF3'
} else {
textStyle.color = 'black'
}
cpu_line_option.title.textStyle = textStyle
nc_line_option.title.textStyle = textStyle
ll_line_option.title.textStyle = textStyle
cp_line_option.title.textStyle = textStyle
}
)
</script>
<style lang="scss" scoped>
h5,
p {
padding: 0;
margin: 0;
}
.conter-right-box {
width: 200px;
text-align: center;
.text-conter {
margin-top: 15px;
font-size: 14px;
}
.chart {
display: flex;
width: 100%;
justify-content: center;
}
}
.left-bottom-box {
margin: 20px 0;
font-size: 12px;
text-align: center;
span {
font-size: 28px;
font-weight: 600;
}
}
.text {
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
</style>

601
src/views/Home/Index4.vue Normal file
View File

@@ -0,0 +1,601 @@
<template>
<div class="w-100%">
<div class="w-100% flex gap-x-20px">
<div class="el-card top-card">
<div class="card-box">
<div class="text text-gray-500">月销售金额()</div>
<Icon color="#bdbdbd" class="cursor-pointer" icon="quill:warning-alt" />
</div>
<span class="text-value">
<avue-count-up :end="topList?.yxsje.value"></avue-count-up>
</span>
<div class="card-box mt-20px">
<div class="text">
<span class="mr-5px text-gray-500">周同比</span>
<Icon
v-if="topList?.yxsje.ztbType == 'top'"
:size="12"
color="#F56C6C"
icon="ep:caret-top"
></Icon>
<Icon
v-else-if="topList?.yxsje.ztbType == 'bottom'"
:size="12"
color="#19BE6B"
icon="ep:caret-bottom"
></Icon>
<span>{{ topList?.yxsje.ztb }}</span>
</div>
<div class="text">
<span class="mr-5px text-gray-500">日环比</span>
<Icon
v-if="topList?.yxsje.rhbType == 'top'"
:size="12"
color="#F56C6C"
icon="ep:caret-top"
></Icon>
<Icon
v-else-if="topList?.yxsje.rhbType == 'bottom'"
:size="12"
color="#19BE6B"
icon="ep:caret-bottom"
></Icon>
<span>{{ topList?.yxsje.rhb }}</span>
</div>
</div>
</div>
<div class="el-card top-card">
<div class="card-box">
<div class="text text-gray-500">月回款金额()</div>
<Icon color="#bdbdbd" class="cursor-pointer" icon="quill:warning-alt" />
</div>
<span class="text-value">
<avue-count-up :end="topList?.yhkje.value"></avue-count-up>
</span>
<div class="card-box w-[calc(100%+20px)]! ml--10px">
<div class="pos-relative w-100%">
<Echart
class="w-100% mt-10px pos-absolute left-0 top--30px"
:options="returned_money_option"
width="100%"
height="60px"
/>
</div>
</div>
</div>
<div class="el-card top-card">
<div class="card-box">
<div class="text text-gray-500">本月成交订单</div>
<Icon color="#bdbdbd" class="cursor-pointer" icon="quill:warning-alt" />
</div>
<span class="text-value">
<avue-count-up :end="topList?.bycjdd.value"></avue-count-up>
</span>
<div class="card-box w-[calc(100%+20px)]! ml--10px">
<div class="pos-relative w-100%">
<Echart
class="w-100% mt-10px pos-absolute left-0 top--30px"
:options="submit_order_option"
width="100%"
height="60px"
/>
</div>
</div>
</div>
<div class="el-card top-card pos-relative">
<div class="card-box">
<div class="text text-gray-500">完成销售目标</div>
<Icon class="cursor-pointer" color="#bdbdbd" icon="quill:warning-alt" />
</div>
<div class="text-value">{{ topList?.wcxsmb.value }}%</div>
<div class="pos-absolute right-30px top-50px">
<el-progress
type="circle"
:percentage="completeValue"
:stroke-width="9"
:width="60"
:show-text="false"
:indeterminate="true"
></el-progress>
</div>
</div>
<div class="el-card top-card">
<div class="card-box">
<div class="text text-gray-500">回款达成率</div>
<Icon color="#bdbdbd" class="cursor-pointer" icon="quill:warning-alt" />
</div>
<div class="text-value mb-30px!">{{ topList?.hkdcl.value }}%</div>
<el-progress
:percentage="returnedValue"
:stroke-width="9"
:show-text="false"
class="jdt"
></el-progress>
</div>
</div>
<div class="w-100% flex gap-x-20px">
<div class="flex-grow-3 flex-shrink flex-basis-0">
<el-card header="数据简报">
<div class="data-bulletin grid grid-cols-4 gap-y-4 text-center py-10px">
<template v-for="(item, index) in dataBulletin" :key="index">
<div class="p-14px">
<div class="text-gray-500 text-14px">{{ item.label }}</div>
<div class="text-28px fw-bold c-#666 dark:c-[var(--el-text-color-primary)]">
<avue-count-up :end="item.value"></avue-count-up>
</div>
<div :style="{ color: item.type == 'pos' ? '#19BE6B' : '#F56C6C' }"
>{{ item.type == 'pos' ? '+' : '-' }}{{ item.percent }}</div
>
</div>
</template>
</div>
</el-card>
<el-card header="业绩目标">
<Echart :options="target_bar_option" width="100%" height="350px" />
</el-card>
<el-card header="销售预测">
<Echart :options="forecast_bar_option" width="100%" height="350px" />
</el-card>
<el-card header="销售漏斗">
<div id="forecast-funnel-chart" class="w-100% h-350px"></div>
</el-card>
</div>
<div class="flex-grow-2 flex-shrink flex-basis-0">
<el-card header="销售排名">
<div class="ranking-box">
<div class="flex items-center gap-x-10px text-left text-12px c-#999 px-14px mb-10px">
<div v-for="(item, index) in rankingTopList" :key="index" :class="item.width">{{
item.label
}}</div>
</div>
<template v-for="(item, index) in rankingList" :key="index">
<div
class="flex items-center gap-x-10px text-left text-12px c-#666 p-14px b-0 b-b-1px b-[var(--el-card-border-color)] b-solid"
>
<div class="w-10% text-left">
<div
class="w-23px h-23px text-center line-height-23px bg-#ccc border-rd-4px c-#fff"
:style="index < 3 ? { background: '#ff9900' } : ''"
>{{ index + 1 }}</div
>
</div>
<div class="w-20%">{{ item.name }}</div>
<div class="w-25%">{{ item.money }}</div>
<div class="w-45%">
<el-progress
:percentage="item.percent"
:stroke-width="8"
:color="rankingColor[index] || '#409EFF'"
></el-progress>
</div>
</div>
</template>
</div>
</el-card>
<el-card header="快捷菜单">
<div class="fast-menu-box grid grid-cols-3 gap-y-20px c-#666 dark:c-#cfd3dc">
<template v-for="(item, index) in fastMenuList" :key="index">
<div
class="flex justify-center items-center flex-col hover:bg-#f1f1f1 hover:b-#dedddd! hover:dark:bg-#1e1e1e cursor-pointer"
>
<div class="h-70px w-70px flex justify-center items-center">
<Icon :icon="item.icon" :size="item.size"></Icon>
</div>
<div class="mt-4px text-12px">{{ item.label }}</div>
</div>
</template>
</div>
</el-card>
<el-card header="系统通知">
<div class="notifier-box">
<template v-for="(item, index) in notifierList" :key="index">
<div class="flex items-center py-12px px-10px">
<div class="bg-#F56C6C w-4px h-4px border-rd-50% flex-shrink-0"></div>
<div class="text-14px flex-1 ml-5px mr-20px">{{ item.label }}</div>
<div class="text-gray-500 text-12px flex-shrink-0">{{ item.time }}</div>
</div>
</template>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import * as echarts from 'echarts'
import { batchGetTableList } from '@/api/design/report'
defineOptions({ name: 'Home4' })
interface TopList {
bycjdd: {
data: number[]
value: string
}
hkdcl: {
value: number
}
wcxsmb: {
value: number
}
yhkje: {
data: number[]
value: string
}
yxsje: {
value: string
rhb: string
rhbType: string
ztb: string
ztbType: string
}
}
interface DataBulletin {
label: string
dictionary: string
value?: string
percent?: string
type?: string
}
interface RankingList {
name: string
money: string
percent: number
}
interface RankingTopList {
label: string
width: string
}
interface SalesTarget {
cjje: string
sj: string
mbje: string
}
interface SalesForecast {
yjxsje: string
sj: string
glje: string
}
interface SalesFunnelgraphic {
dictionary: string
name: string
value?: number
}
interface NotifierList {
label: string
time: string
}
let topList = ref<TopList>()
let returned_money_option = ref({
grid: { bottom: '60%', containLabel: false },
xAxis: {
show: false,
type: 'category',
boundaryGap: false
},
yAxis: {
show: false,
type: 'value'
},
series: [
{
data: topList.value?.yhkje.data,
type: 'line',
symbol: 'none',
smooth: true,
areaStyle: {
color: '#FF7A8C'
},
lineStyle: {
color: '#FF7A8C'
}
}
]
})
const submit_order_option = ref({
grid: { bottom: '60%', containLabel: false },
xAxis: {
show: false,
type: 'category'
},
yAxis: {
show: false,
type: 'value'
},
series: [
{
data: topList.value?.bycjdd.data,
type: 'bar',
itemStyle: {
color: '#52C1F5'
}
}
]
})
const completeValue = ref(0)
const returnedValue = ref(0)
const dataBulletin = ref<DataBulletin[]>([
{ label: '新增客户', dictionary: 'xzkh' },
{ label: '新增线索', dictionary: 'xzxs' },
{ label: '新增商机', dictionary: 'xzsj' },
{ label: '新增订单', dictionary: 'xzdd' },
{ label: '新增联系人', dictionary: 'xnlxr' },
{ label: '跟进次数', dictionary: 'gjcs' },
{ label: '处理任务', dictionary: 'clrw' },
{ label: '处理工单', dictionary: 'clgd' }
])
const salesTarget = ref<SalesTarget[]>()
let salesTargetXAxis = ref<string[]>([])
let salesTargetSeries1 = ref<string[]>([])
let salesTargetSeries2 = ref<string[]>([])
const target_bar_option = ref({
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
tooltip: { trigger: 'item' },
legend: { data: ['目标金额', '成交金额'] },
xAxis: {
data: salesTargetXAxis.value
},
yAxis: {},
series: [
{
name: '目标金额',
type: 'bar',
data: salesTargetSeries1.value,
color: '#6CCAF6'
},
{
name: '成交金额',
type: 'bar',
data: salesTargetSeries2.value,
color: '#93D99A'
}
]
})
const salesForecast = ref<SalesForecast[]>()
let salesForecastXAxis = ref<string[]>([])
let salesForecastSeries1 = ref<string[]>([])
let salesForecastSeries2 = ref<string[]>([])
const forecast_bar_option = ref({
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
tooltip: { trigger: 'item' },
legend: { data: ['预计销售金额', '概率金额'] },
xAxis: {
data: salesForecastXAxis.value
},
yAxis: {},
series: [
{
name: '预计销售金额',
type: 'bar',
data: salesForecastSeries1.value,
color: '#FF8E9D'
},
{
name: '概率金额',
type: 'bar',
data: salesForecastSeries2.value,
color: '#88AEFB'
}
]
})
const salesFunnelgraphic = ref<SalesFunnelgraphic[]>([
{ name: '初步洽谈', dictionary: 'cbqt' },
{ name: '深入沟通', dictionary: 'srgt' },
{ name: '产品报价', dictionary: 'cpbj' },
{ name: '成交商机', dictionary: 'cjsj' },
{ name: '流失商机', dictionary: 'lssj' }
])
const forecast_funnel_option = ref({
title: {
text: ''
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}%'
},
series: [
{
name: 'Funnel',
type: 'funnel',
width: '90%',
height: '100%',
left: '5%',
top: '3%',
label: {
position: 'right'
},
data: salesFunnelgraphic.value
}
]
})
const rankingColor = ref(['#FB6260', '#FFA94C', '#4BCED0'])
const rankingTopList = ref<RankingTopList[]>([
{
label: '排名',
width: 'w-10%'
},
{
label: '姓名',
width: 'w-20%'
},
{
label: '销售金额',
width: 'w-25%'
},
{
label: '完成目标',
width: 'w-45%'
}
])
const rankingList = ref<RankingList[]>()
const fastMenuList = ref([
{ label: '客户管理', size: 65, icon: 'la:user-tie' },
{ label: '线索管理', size: 55, icon: 'akar-icons:light-bulb' },
{ label: '商机管理', size: 55, icon: 'hugeicons:money-add-01' },
{ label: '联系人管理', size: 45, icon: 'uiw:user' },
{ label: '写新跟进', size: 55, icon: 'ph:pen' },
{ label: '回款管理', size: 55, icon: 'fluent:calendar-reply-32-light' },
{ label: '发票管理', size: 55, icon: 'hugeicons:invoice-01' },
{ label: '费用管理', size: 50, icon: 'teenyicons:cost-estimate-outline' },
{ label: '报销管理', size: 55, icon: 'hugeicons:money-bag-02' },
{ label: '工作报告', size: 55, icon: 'line-md:document-report' },
{ label: '工单管理', size: 60, icon: 'material-symbols-light:order-approve-outline-sharp' },
{ label: '产品管理', size: 55, icon: 'fluent-mdl2:product' }
])
const notifierList = ref<NotifierList[]>()
const init = async () => {
let oneres = await batchGetTableList(
'example_client_month_data,example_client_data_briefs,example_client_salesman_rank,example_client_sales_target'
)
topList.value = oneres.example_client_month_data.records[0]
returned_money_option.value.series[0].data = topList.value?.yhkje.data
submit_order_option.value.series[0].data = topList.value?.bycjdd.data
completeValue.value = topList.value?.wcxsmb.value || 0
returnedValue.value = topList.value?.hkdcl.value || 0
dataBulletin.value = dataBulletin.value.map((item) => {
return (item = {
...item,
...oneres.example_client_data_briefs.records[0][item.dictionary]
})
})
rankingList.value = oneres.example_client_salesman_rank.records
salesTarget.value = oneres.example_client_sales_target.records
salesTarget.value?.forEach((ele) => {
salesTargetXAxis.value.push(ele.sj)
salesTargetSeries1.value.push(ele.mbje)
salesTargetSeries2.value.push(ele.cjje)
})
target_bar_option.value.xAxis.data = salesTargetXAxis.value
target_bar_option.value.series[0].data = salesTargetSeries1.value
target_bar_option.value.series[1].data = salesTargetSeries2.value
let endres = await batchGetTableList(
'example_client_sales_forecast,example_client_sales_funnelgraphic,example_client_system_notification'
)
salesForecast.value = endres.example_client_sales_forecast.records
salesForecast.value?.forEach((ele) => {
salesForecastXAxis.value.push(ele.sj)
salesForecastSeries1.value.push(ele.yjxsje)
salesForecastSeries2.value.push(ele.glje)
})
forecast_bar_option.value.xAxis.data = salesForecastXAxis.value
forecast_bar_option.value.series[0].data = salesForecastSeries1.value
forecast_bar_option.value.series[1].data = salesForecastSeries2.value
salesFunnelgraphic.value = salesFunnelgraphic.value.map((item) => {
return (item = {
...item,
value: endres.example_client_sales_funnelgraphic.records[0][item.dictionary]
})
})
forecast_funnel_option.value.series[0].data = salesFunnelgraphic.value
notifierList.value = endres.example_client_system_notification.records
}
onMounted(async () => {
await init()
let xsldId = document.getElementById('forecast-funnel-chart')
if (xsldId) {
let forecastFunnelChart = echarts.init(xsldId)
forecastFunnelChart.setOption(forecast_funnel_option.value)
}
const timer_1 = setInterval(() => {
completeValue.value = completeValue.value + 10
if (completeValue.value >= 75) {
completeValue.value = 75
clearInterval(timer_1)
}
}, 20)
const timer_2 = setInterval(() => {
returnedValue.value = returnedValue.value + 10
if (returnedValue.value >= 80) {
returnedValue.value = 80
clearInterval(timer_2)
}
}, 20)
})
</script>
<style lang="scss" scoped>
.top-card {
position: relative;
padding: 15px;
margin-top: 0 !important;
border-radius: 10px;
box-shadow: var(--el-box-shadow-light);
flex: 1;
.card-box {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
.text-value {
margin-bottom: 15px;
font-size: 30px;
font-weight: 700;
}
}
.text-value {
margin-bottom: 15px;
font-size: 28px;
font-weight: bold;
}
}
::v-deep(.el-card) {
margin-top: 20px;
border-radius: 8px;
.el-card__header {
font-weight: bold;
}
.el-card__body {
.data-bulletin {
& > div {
border-right: 1px solid var(--el-card-border-color);
&:nth-child(4n) {
border-right: none;
}
}
}
.ranking-box {
.el-progress__text {
font-size: 12px !important;
}
}
.fast-menu-box > div {
padding: 10px 0;
border: 1px solid transparent;
}
.notifier-box > div {
border-bottom: 1px solid var(--el-card-border-color);
}
.notifier-box > div:nth-child(1) {
padding-top: 0;
}
}
}
</style>

1102
src/views/Home/Index5.vue Normal file

File diff suppressed because it is too large Load Diff

913
src/views/Home/Index6.vue Normal file
View File

@@ -0,0 +1,913 @@
<template>
<div class="w-100%">
<div class="w-100% top-box">
<div
class="top-card bg-#fff .dark:bg-#1D1E1F! b-1px b-transparent .dark:b-#2A2B2C b-solid"
v-for="(item, index) in topCardList"
:class="item.border"
:key="index"
>
<div class="card-box">
<div class="title">{{ item.title }}</div>
<Icon
icon="akar-icons:info-fill"
width="48"
height="48"
class="cursor-pointer"
style="color: #bdbdbd"
/>
</div>
<span class="text-value .dark:c-#fff!">
<span v-if="item.unit == '¥'">{{ item.unit }}</span>
<avue-count-up :end="item.num"></avue-count-up>
<span v-if="item.unit == '家'" class="text-dw">{{ item.unit }}</span>
</span>
<div class="card-box mt-10px">
<div class="" style="display: flex; align-items: center; height: 12px">
<span v-if="item.type == 'pos'" class="icon">
<Icon icon="ep:top" width="12" height="12" style="color: #f56c6c" />
</span>
<span v-else-if="item.type == 'neg'" class="icons">
<Icon icon="ep:bottom" width="12" height="12" style="color: #19be6b" />
</span>
<span
class="text-ts"
:class="item.type == 'pos' ? 'text-zeng' : item.type == 'neg' ? 'text-jian' : ''"
>{{ item.percentage }}</span
>
<span class="mr-5px text"> {{ item.tendency }}</span>
</div>
</div>
</div>
</div>
<div class="content .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="content-left">
<p class="content-title .dark:c-#fff!">商户外拓</p>
<div class="jdt" v-for="(item, index) in jdtData" :key="index">
<div class="jdt-top">
<div style="display: flex; align-items: center" class="jdt-top-text">
<span class="acitive" :class="'bj' + (index >= 3 ? 0 : index)"></span>
{{ item.title }}
</div>
<div class=".dark:c-#fff!">
<span class="num .dark:c-#fff!">{{ item.leftNum }}</span>
<span class="num" style="font-size: 14px; font-weight: 400"
>/{{ item.rightNum }}</span
>
<span class="jdt-top-text .dark:c-#fff!">{{ item.dw }}</span>
</div>
</div>
<div class="jdt-bottom demo-progress">
<el-progress
:text-inside="true"
:stroke-width="12"
:percentage="item.jd"
:status="item.type"
/>
</div>
</div>
</div>
<div class="content-center">
<p class="content-title .dark:c-#fff!">商户分布</p>
<div id="main" style="width: 380px; height: 300px"></div>
</div>
<div class="content-right">
<p class="content-title .dark:c-#fff!">交易漏斗</p>
<div id="ldMain" style="width: 380px; height: 280px"></div>
</div>
</div>
<div class="eCharts">
<div
class="eCharts-left .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent .dark:c-#fff!"
>
<div class="eCharts-title .dark:c-#fff!">
<p>日交易额</p>
<p style="font-size: 14px" class="font">6,000,000</p>
</div>
<div id="zxtMain" style="width: 380px; height: 300px"></div>
</div>
<div class="eCharts-center .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="eCharts-title .dark:c-#fff!">
<p>周订单量</p>
<p style="font-size: 16px" class="font">10,000<span class="fontFamily"></span></p>
</div>
<div id="zddlMain" style="width: 380px; height: 300px"></div>
</div>
<div
class="eCharts-right .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent"
style="position: relative"
>
<div style="position: absolute">
<div class="eCharts-title .dark:c-#fff! w-100%">
<p>开拓商家</p>
<p style="margin-left: 235px; font-size: 14px">1,000</p>
</div>
</div>
<div id="ktsjMain" style="width: 360px; height: 335px" class=".dark:c-#fff!"></div>
</div>
</div>
<div class="eCharts-cjje .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="cjje-top">
<div class="cjje-top-left .dark:c-#fff!">成交金额趋势</div>
<div class="cjje-top-right">
<el-radio-group v-model="tabPosition" style="margin-bottom: 30px">
<el-radio-button
v-for="(item, index) in cjjeTabList"
:key="index"
:value="item.label"
@change="clickCjje(item.type)"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
</div>
<div id="cjjeMain" style="width: 1400px; height: 300px"></div>
</div>
<div
class="eCharts-cjje .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent"
style="padding-top: 0"
>
<div class="cjje-top">
<div class="cjje-top-left .dark:c-#fff!" style="height: 60px; line-height: 60px">
成交金额趋势
</div>
</div>
<div>
<Table></Table>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import * as echarts from 'echarts'
import Table from './components/table.vue'
import { getTableList, batchGetTableList } from '@/api/design/report'
defineOptions({ name: 'Home6' })
interface TopCardList {
dictionary: string
border: string
tendency: string
title: string
unit: string
type?: string
num?: string
percentage?: string
}
interface JdtData {
dictionary: string
title: string
dw: string
jd?: number
type: any
leftNum?: number
rightNum?: number
}
interface OptionSeriesData {
name: string
value: number
}
type Ktsj = [string, string | number, string | number]
let topCardList = ref<TopCardList[]>([
{ title: '商户总数', dictionary: 'shzs', tendency: '环比上周', border: '', unit: '家' },
{ title: '开拓中商户', dictionary: 'ktzsh', tendency: '环比上周', border: '', unit: '家' },
{ title: '总访问量', dictionary: 'zfwl', tendency: '环比上周', border: '', unit: '' },
{ title: '总交易额', dictionary: 'zjye', tendency: '环比上周', border: '', unit: '¥' },
{ title: '总订单量', dictionary: 'zddl', tendency: '环比上周', border: '', unit: '' },
{ title: '客单价', dictionary: 'kdj', tendency: '环比上周', border: '', unit: '¥' }
])
const jdtData = ref<JdtData[]>([
{ title: '已计划', dictionary: 'yjh', dw: '家', type: '' },
{ title: '已完成', dictionary: 'ywc', dw: '家', type: 'exception' },
{ title: '开拓中', dictionary: 'ktz', dw: '家', type: 'warning' },
{ title: '洽谈中', dictionary: 'qtz', dw: '家', type: '' }
])
//环形图
let currentIndex = ref(0) // 当前高亮图形在饼图数据中的下标
const highlightPie = (option, myChart) => {
// 遍历饼图数据,取消所有图形的高亮效果
for (var idx in option) {
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: idx
})
}
// 高亮当前图形
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex.value
})
}
// 饼图自动轮播
const handleChartLoop = (option, myChart) => {
if (!myChart) {
return
}
highlightPie(option, myChart)
// 用户鼠标悬浮到某一图形时,高亮鼠标悬浮的图形
myChart.on('mouseover', (params) => {
currentIndex.value = params.dataIndex
highlightPie(option, myChart)
})
}
let optionSeriesData = ref<OptionSeriesData[]>([])
const option = reactive({
tooltip: {
trigger: 'item'
},
legend: {
left: 'right',
orient: 'vertical',
align: 'left',
top: 'center',
icon: 'circle',
itemGap: 25,
// 图例文本格式化
formatter: function (name) {
// 在这里可以获取到对应数据项的值
var data = optionSeriesData.value
for (var i = 0; i < data.length; i++) {
if (data[i].name === name) {
return name + ' ' + data[i].value
}
}
return name
}
},
color: ['#50b5ff', '#ffc542', '#ff7474', '#50b5ff', '#8167f5'],
series: [
{
type: 'pie',
label: {
show: false,
position: 'center',
formatter: '{d}%\n\n{b}',
fontSize: 20
},
legendHoverLink: false,
center: ['30%', '45%'],
data: [],
labelLine: {
show: false // 关闭指示线
},
radius: ['48%', '70%'],
emphasis: {
label: {
show: true
}
}
}
]
})
//漏斗
const ldOption = reactive({
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
containLabel: true
},
series: [
{
name: '',
type: 'funnel',
left: '10%',
top: 20,
bottom: 60,
width: '75%',
min: 0,
max: 50000,
minSize: '30%',
maxSize: '70%',
sort: 'descending',
gap: 0,
color: ['#5b8ff9', '#5ad8a6', '#5d7092', '#f6bd16'],
label: {
show: true,
position: 'inside',
formatter: '{b} : {c}'
},
itemStyle: {
borderWidth: 0
},
data: []
}
]
})
//日交易额
let dailyTurnoverXAxis = ref<string[]>([])
let dailyTurnoverSeries = ref<number[]>([])
const zxtOption = reactive({
grid: {
top: '10%', // 调整顶部的空白间距,也可以使用像素值,例如 '50px'
bottom: '20%'
},
xAxis: {
type: 'category',
data: dailyTurnoverXAxis.value,
axisTick: {
show: false // 设置X轴不显示刻度
}
},
yAxis: {
type: 'value',
offset: -7,
max: '50000',
axisLabel: {
textStyle: {
fontSize: 12 // 增大字体大小
}
}
},
series: [
{
data: dailyTurnoverSeries.value,
type: 'line',
symbol: 'none',
smooth: false, // 是否平滑曲线显示
lineStyle: {
color: '#51d351', // 折线颜色
width: 2 // 折线宽度
}
}
]
})
// 周订单量
let weeklyOrdersXAxis = ref<string[]>([])
let weeklyOrdersSeries = ref<number[]>([])
const zddlOption = reactive({
grid: {
top: '10%', // 调整顶部的空白间距,也可以使用像素值,例如 '50px'
bottom: '20%'
},
xAxis: {
type: 'category',
data: weeklyOrdersXAxis.value,
axisTick: {
show: false // 设置X轴不显示刻度
}
},
yAxis: {
type: 'value'
},
series: [
{
data: weeklyOrdersSeries.value,
type: 'bar',
barWidth: '35%',
itemStyle: {
// 设置柱形图的颜色
color: '#409eff',
borderRadius: [4, 4, 0, 0]
}
}
]
})
//开拓商家
let expansionNumberDataset = ref<Ktsj[]>([['product', '目标开拓数', '实际开拓数']])
const ktsjOption = reactive({
theme: 'dark',
grid: {
top: '15%', // 调整顶部的空白间距,也可以使用像素值,例如 '50px'
bottom: '20%'
},
legend: {
icon: 'circle' // 设置图例标记为圆形
},
tooltip: {},
dataset: {
source: expansionNumberDataset.value
},
xAxis: {
type: 'category',
axisTick: {
show: false // 设置X轴不显示刻度
}
},
yAxis: {},
// Declare several bar series, each will be mapped
// to a column of dataset.source by default.
series: [
{
type: 'bar',
barWidth: '20%',
itemStyle: {
// 设置柱形图的颜色
color: '#52c1f5',
borderRadius: [4, 4, 0, 0]
}
},
{
type: 'bar',
barWidth: '20%',
itemStyle: {
// 设置柱形图的颜色
color: '#ff7a8c',
borderRadius: [5, 5, 0, 0]
}
}
]
})
//成交金额趋势
const tabPosition = ref('最近1周')
const cjjeTabList = ref([
{
label: '最近1周',
type: 1
},
{
label: '最近30天',
type: 2
},
{
label: '最近半年',
type: 3
},
{
label: '最近1年',
type: 4
}
])
let transactionAmountXAxis = ref<string[]>([])
let transactionAmountSeries1 = ref<string[]>([])
let transactionAmountSeries2 = ref<string[]>([])
const cjjeOption = ref({
grid: {
left: '4%',
top: '5%', // 调整顶部的空白间距,也可以使用像素值,例如 '50px'
bottom: '20%'
},
legend: {
top: 'bottom',
data: ['线上成交', '线下成交'],
icon: 'rectangle'
},
xAxis: {
data: transactionAmountXAxis.value
},
yAxis: {},
series: [
{
name: '线上成交',
data: transactionAmountSeries1.value,
type: 'line',
symbol: 'none',
smooth: false,
areaStyle: {
color: '#b2e0ff',
opacity: 0.8
}
// symbol: 'circle',
},
{
name: '线下成交',
data: transactionAmountSeries2.value,
type: 'line',
symbol: 'none',
smooth: false,
areaStyle: {
color: '#ffd7dc',
opacity: 0.8
}
// symbol: 'circle',
}
]
})
const clickCjje = async (type) => {
let res = await getTableList('example_trader_transaction_amount', { type: type })
transactionAmountXAxis.value = []
transactionAmountSeries1.value = []
transactionAmountSeries2.value = []
res.records.forEach((ele) => {
transactionAmountXAxis.value.push(ele.sj)
transactionAmountSeries1.value.push(ele.xscj)
transactionAmountSeries2.value.push(ele.xxcj)
})
cjjeOption.value.xAxis.data = transactionAmountXAxis.value
cjjeOption.value.series[0].data = transactionAmountSeries1.value
cjjeOption.value.series[1].data = transactionAmountSeries2.value
nextTick(() => {
const cjjeChart = echarts.init(document.getElementById('cjjeMain'))
cjjeChart.setOption(cjjeOption.value)
})
}
// 通用函数,用于初始化 ECharts 图表
const initEcharts = (domElementId, option) => {
const domElement = document.getElementById(domElementId)
if (domElement) {
const chart = echarts.init(domElement)
domElement.removeAttribute('_echarts_instance_')
chart.setOption(option)
if (domElementId == 'main') {
handleChartLoop(option.series[0].data, chart)
}
}
}
const echartFun = () => {
initEcharts('ldMain', ldOption)
initEcharts('zxtMain', zxtOption)
initEcharts('zddlMain', zddlOption)
initEcharts('ktsjMain', ktsjOption)
initEcharts('main', option)
initEcharts('cjjeMain', cjjeOption.value)
}
const init = async () => {
let oneres = await batchGetTableList(
'example_trader_count_data,example_trader_expansion,example_trader_merchant_distribution,example_trader_transaction_funneldiagram'
)
topCardList.value = topCardList.value.map((item, index) => {
const cardData = oneres.example_trader_count_data.records[0][item.dictionary]
item = { ...item, ...cardData, border: 'border' + index }
return item
})
jdtData.value = jdtData.value.map((item) => {
const cardData = oneres.example_trader_expansion.records[0][item.dictionary]
item = { ...item, ...cardData, jd: (cardData.leftNum / cardData.rightNum) * 100 }
return item
})
option.series[0].data = oneres.example_trader_merchant_distribution.records
optionSeriesData.value = oneres.example_trader_merchant_distribution.records
ldOption.series[0].data = oneres.example_trader_transaction_funneldiagram?.records
let endres = await batchGetTableList(
'example_trader_daily_turnover,example_trader_weekly_orders,example_trader_expansion_number,example_trader_transaction_amount'
)
endres.example_trader_daily_turnover?.records.forEach((ele) => {
dailyTurnoverXAxis.value.push(ele.sj)
dailyTurnoverSeries.value.push(ele.data)
})
zxtOption.xAxis.data = dailyTurnoverXAxis.value
zxtOption.series[0].data = dailyTurnoverSeries.value
endres.example_trader_weekly_orders.records.forEach((ele) => {
weeklyOrdersXAxis.value.push(ele.sj)
weeklyOrdersSeries.value.push(ele.data)
})
zddlOption.xAxis.data = weeklyOrdersXAxis.value
zddlOption.series[0].data = weeklyOrdersSeries.value
expansionNumberDataset.value.push(
...endres.example_trader_expansion_number.records.map((item) => [
item.sj,
item.mbkts,
item.sjkts
])
)
endres.example_trader_transaction_amount.records.forEach((ele) => {
transactionAmountXAxis.value.push(ele.sj)
transactionAmountSeries1.value.push(ele.xscj)
transactionAmountSeries2.value.push(ele.xxcj)
})
cjjeOption.value.xAxis.data = transactionAmountXAxis.value
cjjeOption.value.series[0].data = transactionAmountSeries1.value
cjjeOption.value.series[1].data = transactionAmountSeries2.value
}
onMounted(async () => {
await init()
echartFun()
})
</script>
<style lang="scss" scoped>
.real-time-data {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 12px;
font-weight: 400;
color: #999;
align-items: flex-end;
.title {
// color: #666666;
font-size: 15px;
font-weight: 700;
}
}
.top-box {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
}
.top-card {
flex-basis: calc(16.6% - 12px);
flex-shrink: 1;
min-width: 168px;
padding: 15px;
// border-radius: 10px;
box-shadow: var(--el-box-shadow-light);
box-sizing: border-box;
.card-box {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 32px;
color: #999;
}
.tendency {
width: 24px;
height: 17px;
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
}
.text-ts {
margin: 0 3px;
font-family: Montserrat, sans-serif;
font-size: 12px;
font-weight: 400;
line-height: 24px;
}
.text {
margin-left: 1px;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-weight: 400;
line-height: 24px;
color: #999;
}
}
.text-value {
font-family: 'Montserrat Bold', Montserrat, sans-serif;
font-size: 24px;
font-weight: 700;
font-weight: bold;
line-height: 32px;
color: #333;
}
.card-box.top {
.text {
color: rgb(7 183 138);
}
.ascent {
background-image: url('@/assets/svgs/top.svg');
}
}
.card-box.bottom {
.text {
color: rgb(250 80 135);
}
.descent {
background-image: url('@/assets/svgs/bottom.svg');
}
}
.text-dw {
margin-left: 8px;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
color: #333;
}
}
.content {
display: flex;
padding: 10px 30px 0;
margin-top: 20px;
background: #fff;
justify-content: space-between;
p {
height: 60px;
padding: 0;
margin: 0;
line-height: 60px;
}
.content-title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
color: #666;
text-align: left;
}
.content-left {
width: 30%;
.jdt {
margin-top: 20px;
.jdt-top {
display: flex;
width: 100%;
justify-content: space-between;
font-size: 14px;
color: #666;
.jdt-top-text {
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
color: #666;
}
.num {
font-family: 'Montserrat Bold', Montserrat, sans-serif;
font-size: 16px;
font-weight: 700;
}
.acitive {
display: block;
width: 10px;
height: 10px;
margin-right: 5px;
// background: red;
border-radius: 50%;
}
}
.jdt-bottom {
margin-top: 10px;
}
}
}
.content-center {
width: 30%;
}
.content-right {
width: 30%;
}
}
.eCharts {
display: flex;
margin-top: 20px;
justify-content: space-between;
.eCharts-title {
display: flex;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 28px;
color: #666;
text-align: left;
justify-content: space-between;
p {
padding: 0;
margin: 0;
}
}
.eCharts-left {
width: 27.5%;
padding: 10px 30px 0;
background: #fff;
}
.eCharts-center {
width: 27.5%;
padding: 10px 30px 0;
background: #fff;
}
.eCharts-right {
width: 27.5%;
padding: 10px 30px 0;
background: #fff;
}
}
.eCharts-cjje {
padding: 10px 30px 30px;
margin-top: 20px;
background: #fff;
p {
padding: 0;
margin: 0;
}
.cjje-top {
display: flex;
justify-content: space-between;
.cjje-top-left {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 28px;
color: #686a6d;
text-align: left;
}
}
}
.border0 {
border-left: 5px solid #fb6260;
}
.border1 {
border-left: 5px solid #09f;
}
.border2 {
border-left: 5px solid #8167f5;
}
.border3 {
border-left: 5px solid #51d351;
}
.border4 {
border-left: 5px solid #ff7a8c;
}
.border5 {
border-left: 5px solid #ffa94c;
}
.text-zeng {
color: #f56c6c;
}
.text-jian {
color: #19be6b;
}
.demo-progress .el-progress--line {
max-width: 600px;
margin-bottom: 15px;
}
.bj0 {
background: #409eff;
}
.bj1 {
background: #f56c6c;
}
.bj2 {
background: #e6a23c;
}
.icon {
margin-top: 7px;
}
.icons {
margin-top: 4px;
}
.font {
font-family: 'Montserrat Bold', Montserrat, sans-serif !important;
}
.fontFamily {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important;
}
::v-deep .el-progress-bar__innerText {
margin-top: -8px !important;
line-height: 12px !important;
}
::v-deep .style {
top: -50px !important;
}
</style>

799
src/views/Home/Index7.vue Normal file
View File

@@ -0,0 +1,799 @@
<template>
<div class="w-100%">
<div class="box">
<div class="box-left">
<div class="top .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<p class="title .dark:c-#fff!">我的待办</p>
<div class="top-content">
<div class="top-content-box" v-for="(item, index) in data" :key="index">
<img :src="item.img" alt="" />
<div style="margin-left: 15px">
<p class="text .dark:c-#fff!" style="font-size: 28px">{{ item.num }}</p>
<p class="text font .dark:c-#ccc!">{{ item.title }}</p>
</div>
</div>
</div>
</div>
<div class="center .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<p class="title .dark:c-#fff!">常用功能</p>
<div class="center-box">
<div class="content-box" v-for="(item, index) in cygnData" :key="index">
<div class="img" :class="'bj' + index">
<img :src="item.src" alt="" />
</div>
<div class="font .dark:c-#fff!" style="margin-top: 15px">{{ item.title }}</div>
</div>
</div>
</div>
<div class="bottom .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="bottom-top">
<p class="bottom-top-title .dark:c-#fff!">业务处理</p>
<el-menu
:default-active="cwclType"
:ellipsis="false"
mode="horizontal"
@select="handleSelect"
style="padding: 0 !important; margin: 0 !important"
>
<el-menu-item v-for="item in cwclTypeList" :key="item.type" :index="item.type">{{
item.name
}}</el-menu-item>
</el-menu>
</div>
<el-table
class="custom-row-gap"
:data="cwclTableData"
style="width: 100%"
:header-cell-style="{
height: '50px',
fontFamily: `'微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important`,
fontWeight: ' 700 !important',
color: '#666666 !important'
}"
:cell-style="{
height: '44.2px',
fontFamily: `'微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important`,
fontWeight: ' 400 !important',
color: '#666666 !important'
}"
>
<el-table-column prop="tjsj" label="提交时间" width="170" />
<el-table-column prop="bxlx" label="报销类型" width="140" />
<el-table-column prop="spdh" label="审批单号" width="140" />
<el-table-column prop="bxje" label="报销金额" width="140" />
<el-table-column prop="spdx" label="审批对象" width="140" />
<el-table-column fixed="right" label="操作" min-width="120" align="center">
<template #default>
<el-button link type="primary" size="small" @click="handleClick">
<Icon
icon="icon-park-solid:right-c"
width="12"
height="12"
style="color: #2391ff"
/>
<span style="margin-left: 5px; font-size: 14px">查看详情</span>
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="box-right">
<div class="box-right-top .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="title .dark:c-#fff!">预警信息</div>
<div class="list" v-for="(item, index) in yjxxdata" :key="index">
<span class="list-left .dark:c-#ccc!">{{ item.name }}</span>
<span class="list-right" :class="index % 2 == 0 ? 'text-color' : ''">{{
item.num
}}</span>
</div>
</div>
<div class="box-right-center .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div style="display: flex; justify-content: space-between">
<div class="title .dark:c-#fff!"> 年度经营目标</div>
<div class="title" style="font-size: 24px; color: #279df5">
<span class="fontFamily .dark:c-#fff!">{{ annualIndicators1?.ndjymb }}</span>
<span style="font-size: 14px; color: #666" class=".dark:c-#ccc!"> </span>
</div>
</div>
<div class="ndmb">
<el-progress
type="circle"
:percentage="annualIndicators1?.wcl"
:stroke-width="16"
:width="166"
>
<template #default>
<span class="percentage-label .dark:c-#ccc!" style="color: #20a0ff"> 完成率 </span>
<span class="percentage-value" style="color: #20a0ff">
{{ annualIndicators1?.wcl }}%
</span>
</template>
</el-progress>
</div>
<div class="text .dark:c-#ccc!">
已完成目标
<span class="fontFamily" style="font-size: 24px; font-weight: 700; color: #fbaf4f">
{{ annualIndicators1?.ywcmb }}
</span>
</div>
</div>
<div class="box-right-center .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div style="display: flex; justify-content: space-between">
<div class="title .dark:c-#fff!">年度回款目标</div>
<div class="title" style="font-size: 24px; color: #279df5">
<span class="fontFamily .dark:c-#fff!">{{ annualIndicators2?.ndhkmb }}</span>
<span style="font-size: 14px; color: #666" class=".dark:c-#ccc!"> </span>
</div>
</div>
<div class="ndmb">
<el-progress
type="circle"
:percentage="annualIndicators2?.wcl"
:stroke-width="16"
status="success"
:width="166"
>
<template #default>
<span class="percentage-label .dark:c-#ccc!">完成率</span>
<span class="percentage-value">{{ annualIndicators2?.wcl }}%</span>
</template>
</el-progress>
</div>
<div class="text .dark:c-#ccc!">
已完成目标
<span class="fontFamily" style="font-size: 24px; font-weight: 700; color: #fbaf4f">
{{ annualIndicators2?.ywcmb }}
</span>
</div>
</div>
</div>
</div>
<div class="w-100% eCharts">
<div class="eCharts-left .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="eCharts-left-top">
<div class="title .dark:c-#fff!">账户余额</div>
<div class="font .dark:c-#ccc!">
<span>合计</span>
<span
class="fontFamily .dark:c-#fff!"
style="font-size: 20px; font-weight: 700; color: #2b9ef7"
>{{ zhyeNum }}</span
>
<span style="margin-left: 5px"></span>
</div>
</div>
<div>
<div
style="display: flex; align-items: center; line-height: 50px"
v-for="(item, index) in zhyedata"
:key="index"
>
<div class="font-title .dark:c-#ccc!" style="width: 17%">{{ item.name }}</div>
<div style="width: 79%">
<el-progress :percentage="item.jdt" :stroke-width="10">
<span
style="
margin-left: 30px;
font-family: ArialMT, Arial, sans-serif;
font-size: 16px;
"
>{{ item.num }}</span
>
</el-progress>
</div>
</div>
</div>
</div>
<div class="eCharts-right .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="eCharts-right-top">
<div class="title .dark:c-#fff!">收支预测</div>
</div>
<div id="szycMain" style="width: 600px; height: 300px"></div>
</div>
</div>
<div class="w-100% eCharts">
<div class="eCharts-left .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="eCharts-left-top">
<div class="title .dark:c-#fff!">费用结构</div>
</div>
<div id="fyjgMain" style="width: 600px; height: 300px"></div>
</div>
<div class="eCharts-right .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<div class="eCharts-right-top">
<div class="title .dark:c-#fff!">收入结构</div>
</div>
<div id="srjgMain" style="width: 600px; height: 300px"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import * as echarts from 'echarts'
import { getTableList, batchGetTableList } from '@/api/design/report'
import { ElMessage } from 'element-plus'
interface Data {
dictionary: string
img: string
title: string
num?: string
}
interface CwclTableData {
tjsj: string
bxlx: string
spdh: string
bxje: string
spdx: string
}
interface Yjxxdata {
dictionary: string
name: string
num?: string
}
interface CircularProgress {
ndjymb: number
wcl: number
ywcmb: number
ndhkmb: number
}
interface Zhyedata {
name: string
num: number
jdt: number
}
type Szyc = [[string, string | number, string | number]]
//我的待办
let data = ref<Data[]>([
{ dictionary: 'dclgd', img: '/img/img1.svg', title: '待处理工单' },
{ dictionary: 'dbxmx', img: '/img/img2.svg', title: '待报销明细' },
{ dictionary: 'dtjhb', img: '/img/img3.svg', title: '待提交汇报' },
{ dictionary: 'dtjbb', img: '/img/img4.svg', title: '待提交报表' }
])
//常用功能
const cygnData = reactive([
{ src: '/img/tb1.svg', title: '资金收支报表' },
{ src: '/img/tb2.svg', title: '资产负债报表' },
{ src: '/img/tb3.svg', title: '企业利润报表' },
{ src: '/img/tb4.svg', title: '现金流量报表' },
{ src: '/img/tb5.svg', title: '项目报告列表' },
{ src: '/img/tb6.svg', title: '责任中心报告' }
])
//预警信息
let yjxxdata = ref<Yjxxdata[]>([
{
dictionary: 'dqysk',
name: '到期应收款'
},
{
dictionary: 'yqysk',
name: '逾期应收款'
},
{
dictionary: 'dqyfk',
name: '到期应付款'
},
{
dictionary: 'yqyfk',
name: '逾期应付款'
}
])
// 年度经营目标
const annualIndicators1 = ref<CircularProgress>()
// 年度回款目标
const annualIndicators2 = ref<CircularProgress>()
//账户余额
const zhyeNum = ref('')
let zhyedata = ref<Zhyedata[]>()
//收支预测
const szycDataset = ref<Szyc>([['product', '预计收入', '预计支出']])
const szycOption = reactive({
legend: {
// data: ['系列1', '系列2'],
orient: 'horizontal',
x: 'center', // 可以是 'center'、'left'、'right' 等
y: 'bottom' // 可以是 'top'、'center'、'bottom' 等,或者是具体的像素值
},
tooltip: {},
dataset: {
source: szycDataset.value
},
xAxis: {
type: 'category',
axisTick: {
show: false // 设置X轴不显示刻度
}
},
yAxis: {},
// Declare several bar series, each will be mapped
// to a column of dataset.source by default.
series: [
{
type: 'bar',
itemStyle: {
// 设置柱形图的颜色
color: '#2c9ef7'
}
},
{
type: 'bar',
itemStyle: {
// 设置柱形图的颜色
color: '#0dd78d'
}
}
]
})
//费用结构
const fyjgOption = reactive({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'horizontal',
left: 'center',
top: 'bottom',
icon: 'circle'
},
series: [
{
name: '',
type: 'pie',
radius: '58%',
center: ['50%', '40%'],
color: ['#2c9ef7', '#aa89fe', '#fdad4e', '#0dd78d'],
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
label: {
normal: {
formatter: '{b}\n\n¥{c}',
textStyle: {
color: '#999999',
fontSize: 12
}
}
}
}
]
})
//收入结构
const srjgOption = reactive({
tooltip: {
trigger: 'item'
},
legend: {
orient: 'horizontal',
left: 'center',
top: 'bottom',
icon: 'circle'
},
series: [
{
name: '',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '40%'],
color: ['#2c9ef7', '#5cb9ff', '#abdbff', '#d0ebff'],
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
label: {
normal: {
formatter: '{b}\n \n ¥{c}',
textStyle: {
color: '#999999',
fontSize: 12
}
}
}
}
]
})
// 业务处理
const cwclType = ref('1')
const cwclTableData = ref<CwclTableData[]>()
const cwclTypeList = ref([
{ name: '待我审批', type: '1' },
{ name: '超期待审', type: '2' },
{ name: '我提交的', type: '3' },
{ name: '被退回的', type: '4' }
])
const handleSelect = async (key: string, keyPath: string[]) => {
let res = await getTableList('example_systemmonitor_business_processing', { type: key })
cwclTableData.value = res.records
}
const handleClick = () => {
ElMessage({
message: '查看详情',
type: 'success'
})
}
// 通用函数,用于初始化 ECharts 图表
const initEcharts = (domElementId, option) => {
const domElement = document.getElementById(domElementId)
if (domElement) {
const chart = echarts.init(domElement)
domElement.removeAttribute('_echarts_instance_')
chart.setOption(option)
}
}
const echartFun = () => {
initEcharts('szycMain', szycOption)
initEcharts('fyjgMain', fyjgOption)
initEcharts('srjgMain', srjgOption)
}
const init = async () => {
let oneres = await batchGetTableList(
'example_signagetwo_pending,example_systemmonitor_business_processing,example_systemmonitor_warning_messages,example_systemmonitor_business_target'
)
let signagetwoPending = oneres.example_signagetwo_pending.records[0]
let businessProcessing = oneres.example_systemmonitor_business_processing.records
let warningMessages = oneres.example_systemmonitor_warning_messages.records[0]
let businessTarget = oneres.example_systemmonitor_business_target.records[0]
data.value = data.value.map((item) => {
return (item = {
...item,
num: signagetwoPending[item.dictionary]
})
})
cwclTableData.value = businessProcessing
yjxxdata.value = yjxxdata.value.map((item) => {
return (item = {
...item,
num: warningMessages[item.dictionary]
})
})
annualIndicators1.value = businessTarget
let endres = await batchGetTableList(
'example_systemmonitor_payback_target,example_systemmonitor_account_balance,example_systemmonitor_Income_expenditure_forecasts,example_systemmonitor_fee_structure,example_systemmonitor_Income_structure'
)
let paybackTarget = endres.example_systemmonitor_payback_target.records[0]
let accountBalance = endres.example_systemmonitor_account_balance.records
let expenditureForecasts = endres.example_systemmonitor_Income_expenditure_forecasts.records
let feeStructure = endres.example_systemmonitor_fee_structure.records
let incomestructure = endres.example_systemmonitor_Income_structure.records
annualIndicators2.value = paybackTarget
zhyedata.value = accountBalance
zhyeNum.value = accountBalance.reduce((a, b) => {
return a + b.num
}, 0)
expenditureForecasts.forEach((ele) => {
szycDataset.value.push([ele.sj, ele.yjsr, ele.yjzc])
})
szycOption.dataset.source = szycDataset.value
fyjgOption.series[0].data = feeStructure
srjgOption.series[0].data = incomestructure
}
onMounted(async () => {
await init()
echartFun()
})
</script>
<style lang="scss" scoped>
.box {
display: flex;
width: 100%;
justify-content: space-between;
p {
padding: 0;
margin: 0;
}
.box-left {
width: 72.3%;
// height: 300px;
// background: red;
.title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 60px;
color: #333;
text-align: left;
}
.top {
padding: 0 20px 20px;
background: #fff;
border-radius: 5px;
.text {
font-family: 'Montserrat Bold', 'Montserrat Regular', Montserrat, sans-serif;
font-weight: 700;
color: #333;
}
.top-content {
display: flex;
justify-content: space-between;
margin: 25px;
.top-content-box {
display: flex;
justify-content: space-between;
align-items: center;
img {
width: 48px;
height: 48px;
}
}
}
}
.center {
padding: 0 20px 10px;
margin-top: 20px;
background: #fff;
border-radius: 5px;
.center-box {
display: flex;
padding-bottom: 20px;
margin-top: 10px;
justify-content: space-evenly;
.content-box {
width: 100px;
text-align: center;
// background: red;
.img {
display: flex;
width: 60px;
height: 60px;
margin: 0 auto;
border-radius: 50%;
align-items: center;
justify-content: center;
}
}
}
}
.bottom {
padding: 0 20px;
padding-bottom: 35px;
margin-top: 20px;
background: #fff;
border-radius: 5px;
.bottom-top {
display: flex;
margin-bottom: 8px;
justify-content: space-between;
.bottom-top-title {
height: 40px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
line-height: 52px;
color: #333;
text-align: left;
}
}
}
}
.box-right {
width: 26.3%;
// height: 300px;
// background: blue;
.title {
// margin-bottom: 20px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 60px;
color: #333;
text-align: left;
}
.box-right-top {
padding: 0 20px 20px;
background: #fff;
border-radius: 5px;
.list {
display: flex;
justify-content: space-between;
// margin-bottom: 15px;
line-height: 40px;
.list-left {
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
color: #666;
text-align: left;
}
.list-right {
font-family: 'Montserrat Bold', 'Montserrat Regular', Montserrat, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
color: #0fd1b6;
text-align: right;
}
}
}
.box-right-center {
padding: 0 20px 20px;
margin-top: 20px;
background: #fff;
border-radius: 5px;
.ndmb {
display: flex;
width: 100%;
justify-content: center;
margin-top: 10px;
}
.text {
margin-top: 20px;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 40px;
color: #797979;
text-align: center;
}
}
}
}
.eCharts {
display: flex;
margin-top: 20px;
justify-content: space-between;
.title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 60px;
color: #333;
text-align: left;
}
.eCharts-left {
width: 46.1%;
padding: 0 20px 20px;
line-height: 60px;
background: #fff;
border-radius: 5px;
.eCharts-left-top {
display: flex;
justify-content: space-between;
}
.eCharts-right-top {
display: flex;
justify-content: space-between;
}
}
.eCharts-right {
width: 46.1%;
padding: 0 20px;
background: #fff;
border-radius: 5px;
}
}
.percentage-value {
display: block;
margin-top: 10px;
font-family: Arial-BoldMT, 'Arial Bold', Arial, sans-serif;
font-size: 28px;
font-weight: 700;
color: #13ce66;
}
.percentage-label {
display: block;
margin-top: 10px;
font-family: ArialMT, Arial, sans-serif;
font-size: 14px;
font-weight: 400;
color: #666 !important;
}
.el-menu--horizontal.el-menu {
border: 0 !important;
}
.bj0 {
background: #2c9ef7;
}
.bj1 {
background: #b591f5;
}
.bj2 {
background: #11d88f;
}
.bj3 {
background: #ffad52;
}
.bj4 {
background: #10d1b7;
}
.bj5 {
background: #f67263;
}
.text-color {
color: #f6716e !important;
}
.font {
font-family: '微软雅黑', sans-serif !important;
font-size: 14px !important;
font-weight: 400 !important;
color: #666;
}
.fontFamily {
font-family: 'Montserrat Bold', 'Montserrat Regular', Montserrat, sans-serif !important;
}
.font-title {
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif !important;
font-size: 14px !important;
font-weight: 400;
color: #666;
}
::v-deep .el-table--default .el-table__cell {
padding: 0 !important; /* 根据需要调整这个值来增加或减少行间距 */
}
</style>

432
src/views/Home/Index8.vue Normal file
View File

@@ -0,0 +1,432 @@
<template>
<div class="w-100%">
<div class="w-100% flex gap-y-20px home-top">
<div class="el-card top-card flex items-center" v-for="(item, index) in tabList" :key="index">
<div class="left">
<span class="text-value">
<avue-count-up :end="item.countUp"></avue-count-up>
</span>
<div class="text mt-10px">
<span class="mr-5px">{{ item.title }}</span>
</div>
</div>
<div>
<el-image :src="item.imgUrl" fit="contain"></el-image>
</div>
</div>
</div>
<div class="w-100% flex gap-x-20px mt-30px ml-10px mr-10px">
<div class="w-35%">
<el-calendar v-model="value">
<template #header>
<span>{{ calendarDate }}</span>
</template>
</el-calendar>
</div>
<div class="echart-util bold bg-white .dark:bg-#1D1E1F">
<Echart :options="visitor_option" width="100%" height="314px" />
</div>
</div>
<div class="w-100% flex gap-x-20px mt-30px ml-10px mr-10px">
<div class="echart-util bold bg-white .dark:bg-#1D1E1F">
<Echart :options="salesVolume_option" width="100%" height="314px" />
</div>
<div class="echart-util reverse bg-white .dark:bg-#1D1E1F">
<Echart :options="cyclic_annular_option" width="100%" height="314px" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useAppStore } from '@/store/modules/app'
import { batchGetTableList } from '@/api/design/report'
defineOptions({ name: 'Home8' })
interface CyclicAnnular {
name: string
dictionary: string
value?: number
}
const tabList = ref([
{ countUp: 0, title: '今日访客数(人)', imgUrl: '/img/sjhy1.png', dictionary: 'today_visitors' },
{
countUp: 0,
title: '今日会员访问数(人)',
imgUrl: '/img/sjhy2.png',
dictionary: 'today_member_visitors'
},
{
countUp: 0,
title: '今日会员付费数(人)',
imgUrl: '/img/sjhy3.png',
dictionary: 'today_paid_member'
},
{ countUp: 0, title: '新增会员数(人)', imgUrl: '/img/sjhy4.png', dictionary: 'today_new_member' }
])
const value = ref(new Date())
const calendarDate = formatDate(new Date(), 'YYYY-MM-DD')
// 实时访客量
const visitorXAxis = ref<string[]>([])
const visitorSeries = ref<number[]>([])
const visitor_option = ref({
tooltip: {
trigger: 'axis'
},
title: {
text: '实时访客量',
textStyle: {
fontSize: 20,
color: 'black'
},
left: 25
},
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#EDE8FE' // 0% 处的颜色
},
{
offset: 1,
color: '#F8F7FC' // 100% 处的颜色
}
],
global: false // 缺省为 false
},
grid: {
left: '4%',
right: '4%',
bottom: 20,
top: '16%',
containLabel: true
},
xAxis: {
data: visitorXAxis.value,
type: 'category',
boundaryGap: false,
axisLine: {
symbol: 'none',
lineStyle: {
color: '#EBEBEB'
}
},
axisTick: {
show: false
},
axisLabel: {
interval: 0,
color: 'black',
fontSize: 12,
padding: [10, 0, 0, 0],
align: 'center'
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#6071A9',
fontSize: 12
},
splitLine: {
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
offset: 10
},
series: [
{
name: '访客量',
type: 'line',
data: visitorSeries.value,
smooth: true,
lineStyle: {
width: 5,
color: '#5D77FF'
},
areaStyle: {
opacity: 0.7
},
emphasis: { disabled: true },
symbol: 'none'
}
]
})
// 销售额
const salesVolumeXAxis = ref<string[]>([])
const salesVolumeSeries = ref<number[]>([])
const salesVolume_option = ref({
tooltip: {
trigger: 'axis'
},
title: {
text: '销售额(元)',
textStyle: {
fontSize: 20,
color: 'black'
},
left: 25
},
color: ['#6F7EFD'],
grid: {
left: '4%',
right: '4%',
bottom: 20,
top: '16%',
containLabel: true
},
xAxis: {
data: salesVolumeXAxis.value,
type: 'category',
boundaryGap: false,
axisLine: {
symbol: 'none',
lineStyle: {
color: '#EBEBEB'
}
},
axisTick: {
show: false
},
axisLabel: {
interval: 0,
color: 'black',
fontSize: 12,
padding: [10, 0, 0, 0],
align: 'center'
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#6071A9',
fontSize: 12
},
splitLine: {
lineStyle: {
width: 0
}
},
offset: 10
},
series: [
{
type: 'line',
data: salesVolumeSeries.value,
smooth: true,
lineStyle: {
width: 0
},
areaStyle: {
opacity: 1
},
emphasis: { disabled: true },
symbol: 'none'
}
]
})
// 环状图
const cyclic_annular_option_data = ref<CyclicAnnular[]>([
{ name: '男装', dictionary: 'nz' },
{ name: '鞋包', dictionary: 'xb' },
{ name: '母婴', dictionary: 'my' },
{ name: '数码', dictionary: 'sm' }
])
const cyclic_annular_option = ref({
title: {
text: '',
left: 'center',
top: 'center',
textStyle: {
fontSize: 24,
fontWeight: 'bold',
color: '#5E78FD'
},
subtext: '',
subtextStyle: {
color: '#999999',
fontWeight: '600'
},
itemGap: 0
},
color: ['#5E78FD', '#6E86FD', '#8DA0FF', '#AEBBFF'],
series: [
{
type: 'pie',
radius: ['35%', '55%'],
label: {
formatter: (parms) => {
return [`{str|${parms.percent}% ${parms.data.name}}`].join('\n')
},
rich: {
str: {
fontWeight: 'bold',
align: 'center'
}
}
},
data: cyclic_annular_option_data.value
}
]
})
const appStore = useAppStore()
watch(
() => appStore.isDark,
(val) => {
if (val) {
visitor_option.value.title.textStyle.color = 'white'
salesVolume_option.value.title.textStyle.color = 'white'
} else {
visitor_option.value.title.textStyle.color = 'black'
salesVolume_option.value.title.textStyle.color = 'black'
}
}
)
const init = async () => {
let oneres = await batchGetTableList('example_member_count_data,example_member_realtime_visits')
let countData = oneres.example_member_count_data.records[0]
let realtimeVisits = oneres.example_member_realtime_visits.records
tabList.value = tabList.value.map((item) => {
return (item = {
...item,
countUp: countData[item.dictionary]
})
})
realtimeVisits.forEach((ele) => {
visitorXAxis.value?.push(ele.sj)
visitorSeries.value?.push(ele.ssfkl)
})
let endres = await batchGetTableList('example_member_sale,example_member_product_percentage')
let sale = endres.example_member_sale.records
let productPercentage = endres.example_member_product_percentage.records[0]
sale.forEach((ele) => {
salesVolumeXAxis.value?.push(ele.sj)
salesVolumeSeries.value?.push(ele.xse)
})
cyclic_annular_option_data.value = cyclic_annular_option_data.value.map((item) => {
return (item = {
...item,
value: productPercentage[item.dictionary]
})
})
cyclic_annular_option.value.series[0].data = cyclic_annular_option_data.value
cyclic_annular_option.value.title.text = '¥' + productPercentage.je
cyclic_annular_option.value.title.subtext = productPercentage.sj
}
onMounted(async () => {
// 判断是否为暗色模式
const { wsCache } = useCache()
if (wsCache.get(CACHE_KEY.IS_DARK)) {
visitor_option.value.title.textStyle.color = 'white'
salesVolume_option.value.title.textStyle.color = 'white'
}
await init()
})
</script>
<style lang="scss" scoped>
.home-top {
flex-wrap: wrap;
.top-card {
width: calc(25% - 50px);
height: 85px;
padding: 15px;
margin: 0 10px;
margin-top: 0 !important;
border: none;
border-radius: inherit;
flex-shrink: 0;
justify-content: space-around;
box-shadow: 0 0 20px rgb(204 204 204 / 34.9%);
.left {
font-weight: bold;
.text-value {
margin-bottom: 45px;
font-size: 28px;
}
.text {
font-size: 13px;
}
}
}
}
.echart-util {
padding: 20px;
box-shadow: 0 0 20px rgb(204 204 204 / 34.9%);
}
.echart-util.bold {
width: calc(65% - 80px);
}
.echart-util.reverse {
width: calc(35% - 40px);
}
::v-deep(.el-calendar) {
box-shadow: 0 0 20px rgb(204 204 204 / 34.9%);
.el-calendar__header {
font-size: 18px;
font-weight: bold;
border-bottom: none;
}
.el-calendar__body {
th {
font-size: 15px;
font-weight: bold;
}
.el-calendar-table__row {
.is-selected {
background-color: rgb(0 255 255 / 0%);
}
.current,
.next,
.prev {
border: none;
.el-calendar-day {
display: flex;
height: 43px;
font-size: 14px;
font-weight: 700;
justify-content: center;
align-items: center;
}
}
}
td {
// border: none;
}
}
}
</style>

1016
src/views/Home/Index9.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
<template>
<div>
<el-table
:data="tableData"
stripe
style="width: 100%"
:header-cell-style="{
height: '50px',
fontFamily: `'微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important`,
fontWeight: ' 700 !important',
color: '#666666 !important'
}"
:cell-style="{
height: '50px',
fontFamily: `'微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important`,
fontWeight: ' 400 !important',
color: '#666666 !important'
}"
>
<el-table-column prop="ranking" label="排名" width="180" />
<el-table-column prop="name" label="商户名称" />
<el-table-column prop="visit" label="访问量" />
<el-table-column prop="quantity" label="订单量" />
<el-table-column prop="cancel" label="取消量" />
<el-table-column label="综合评分">
<template #default="scope">
<el-rate
v-model="scope.row.pj"
disabled
allow-half
show-score
size="large"
:score-template="scope.row.score"
/>
</template>
</el-table-column>
<el-table-column label="成交金额(元)">
<template #default="scope">
<div v-if="scope.row.zeng" style="display: flex; align-items: center; height: 12px">
<span>{{ scope.row.money }}</span>
<span class="icon"
><Icon icon="ep:top" width="12" height="12" style="color: #f56c6c"
/></span>
<span style="color: #f7716f !important">{{ scope.row.zeng }}%</span>
</div>
<div v-else-if="scope.row.jian" style="display: flex; align-items: center; height: 12px">
<span>{{ scope.row.money }}</span>
<span class="icon"
><Icon icon="ep:bottom" width="12" height="12" style="color: #19be6b"
/></span>
<span style="color: #73c883 !important">{{ scope.row.jian }}%</span>
</div>
<div v-else>
<span>{{ scope.row.money }}</span>
</div>
</template>
</el-table-column>
</el-table>
<div class="page">
<el-pagination background layout="prev, pager, next" :total="tableDataTotal" />
</div>
</div>
</template>
<script lang="ts" setup>
import * as TableApi from '@/api/design/table'
interface TableData {
cancel: string
ranking: string
name: string
visit: string
quantity: string
pj: string
score: string
money: string
zeng: string
jian: string
}
const tableData = ref<TableData[]>()
const tableDataTotal = ref(0)
onMounted(async () => {
let { records } = await TableApi.getTableList(
'1847537155101040642',
{ pageNo: 1, pageSize: 10 },
false
)
tableData.value = records.map((item) => {
item.pj = Number(item.pj || 0)
return item
})
tableDataTotal.value = records.length
})
</script>
<style lang="scss" scoped>
.page {
display: flex;
width: 100%;
padding: 20px;
justify-content: center;
align-items: center;
}
.icon {
margin-top: 4px;
}
/* 将默认的星星颜色改为蓝色 */
::v-deep .el-rate .el-rate__icon.is-active {
color: #f56c6c !important;
}
::v-deep .custom-rate .el-rate__item.active {
color: #f56c6c; /* 选中的颜色 */
}
::v-deep .el-rate .el-rate__item {
color: #ccc;
}
</style>

View File

@@ -0,0 +1,308 @@
import { EChartsOption } from 'echarts'
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 = {
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
}
]
}
]
}

55
src/views/Home/types.ts Normal file
View File

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

View File

242
src/views/Login/Login.vue Normal file
View File

@@ -0,0 +1,242 @@
<template>
<div :class="prefixCls" class="relative h-[100%]">
<div class="relative mx-auto h-full flex">
<div
v-if="!appStore.getMobile"
:class="`${prefixCls}__left flex-1 bg-#409eff dark:bg-[var(--login-bg-color)] relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
>
<!-- 左上角的 logo + 系统标题 -->
<div
class="relative flex items-center text-white mt-30px"
:style="{ marginTop: leftStyle.logo.mt }"
>
<img
alt=""
class="mr-10px"
:style="{ height: leftStyle.logo.height }"
src="@/assets/imgs/logo_white.png"
/>
</div>
<!-- 左边的背景图 + 欢迎语 -->
<Transition
appear
enter-active-class="animate__animated animate__bounceInLeft"
tag="div"
>
<div :style="{ marginTop: leftStyle.bottomText.textMt }">
<div
class="pos-relative w-100% overflow-hidden"
:style="{ height: leftStyle.lottie.boxHeight }"
>
<img
class="pos-absolute"
:style="{
left: 0,
top: 0,
right: 0,
bottom: 0,
width: '100%',
height: '100%',
objectFit: 'contain',
objectPosition: 'center center',
}"
src="@/assets/imgs/singBj2.png"
alt="静态图片描述"
/>
<!-- <vue3-lottie
class="pos-absolute"
:style="{ left: leftStyle.lottie.left, top: leftStyle.lottie.top }"
:animation-data="loginAnimationData"
:height="leftStyle.lottie.height"
:width="leftStyle.lottie.width"
:auto-play="true"
:loop="true"
/> -->
</div>
<div
class="w-100% flex items-center justify-center ml--20px gap-x-5px"
:style="{ marginTop: leftStyle.bottomText.linkMT }"
>
</div>
<div
class="flex justify-center w-100%"
:style="{ marginTop: leftStyle.bottomText.textMt }"
>
<div
class="pos-relative overflow-hidden"
:style="{ width: leftStyle.bottomText.width, height: leftStyle.bottomText.height }"
>
<span class="w-100% flex items-center justify-center ml--20px gap-x-5px" style="color:white;font-size:50px">综合监控系统</span>
</div>
</div>
</div>
</Transition>
</div>
<div
class="relative flex-1 p-30px bg-#e3f0ff lt-sm:p-10px overflow-x-hidden overflow-y-auto"
:class="`${prefixCls}__right`"
>
<!-- 右上角的主题语言选择 -->
<div
class="relative z-2 flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<!-- <div class="flex items-center at-2xl:hidden at-xl:hidden">
<img alt="" class="mr-10px h-32px" src="@/assets/imgs/logo1.png" />
</div> -->
<div class="flex items-center justify-end space-x-10px h-48px">
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
:class="newClass"
class="pos-relative m-auto z-2 h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
>
<div class="bg-#fff dark:bg-[var(--login-bg-color)] b-rounded-20px px-20px">
<!-- 账号登录 -->
<LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 手机登录 -->
<MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 二维码登录 -->
<QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 三方登录 -->
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div>
</div>
</Transition>
</div>
<div
class="pos-fixed bottom-0px left-50% lt-md:left-0px lt-sm:left-0px lt-xl:left-0px lt-xl:left-0px z-1"
>
<img class="w-138px block" src="@/assets/imgs/login/login_right_bg_1.png" alt="" />
</div>
<div class="pos-fixed right--38px top--23px">
<img class="w-160px block" src="@/assets/imgs/login/login_right_bg_2.png" alt="" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useDesign } from '@/hooks/web/useDesign'
import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { useWindowSize } from '@vueuse/core'
import { LoginForm, MobileForm, QrCodeForm, SSOLoginVue } from './components'
import * as loginAnimation from '@/assets/json/login_left.json'
defineOptions({ name: 'Login' })
const { t } = useI18n()
const appStore = useAppStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('login')
const windowSize = useWindowSize()
const loginAnimationData = ref(loginAnimation['default'])
const leftStyle = computed(() => {
const winW = windowSize.width.value
const leftW = winW / 2
const logo = {
height: leftW / 16.5 + 'px',
mt: leftW / 31.9 + 'px'
}
const lottieMagn = leftW / 435
const lottie = {
boxHeight: leftW / 2.39 + 'px',
height: 335 * lottieMagn,
width: 434 * lottieMagn,
left: -(leftW / 38) + 'px',
top: -(leftW / 5.3) + 'px'
}
const bottomText = {
width: leftW / 1.54 + 'px',
height: leftW / 8.68 + 'px',
left: -(leftW / 31.54) + 'px',
linkMT: leftW / 23.87 + 'px',
textMt: leftW / 16.2 + 'px'
}
return { lottie, bottomText, logo }
})
const newClass = computed(() => {
const mobile = appStore.getMobile ? 'is-mobile' : ''
const toggle = appStore.getFullscreen ? 'is-toggle' : ''
return toggle ? toggle : mobile
})
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-login;
.#{$prefix-cls} {
overflow: auto;
}
.#{$prefix-cls}__left {
background-image: url('@/assets/imgs/singBj1.png');
background-repeat: no-repeat; // 背景图片不重复
background-size: cover; // 背景图片覆盖整个容器
background-position: center center; // 背景图片居中显示
}
.margin-left-50 {
margin-left: 50%;
}
.is-mobile {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(2.5, 1.8);
}
.is-toggle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.7, 0.6);
}
.is-mobile-bj {
position: absolute;
width: 50vw !important;
height: 100vh;
top: 0;
left: 0;
overflow: hidden;
}
.is-mobile-bj img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
.is-mobile-bj .bjOne {
z-index: 1; /* 底层 */
}
.is-mobile-bj .bjTwo {
z-index: 2; /* 上层(值比 bjOne 大即可) */
width: 80%;
height: auto;
float: left;
left: 50%;
top: 50%;
transform: translate(-50%,-45%);
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,419 @@
<template>
<div
:class="prefixCls"
class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
v-if="$route.query.showLogin"
>
<div class="relative mx-auto h-full flex">
<div
:class="`${prefixCls}__left flex-1 bg-#409eff dark:bg-[var(--login-bg-color)] relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
>
<!-- 左上角的 logo + 系统标题 -->
<div
class="relative flex items-center text-white mt-30px"
:style="{ marginTop: leftStyle.logo.mt }"
>
<img
alt=""
class="mr-10px"
:style="{ height: leftStyle.logo.height }"
src="@/assets/imgs/logo_white.png"
/>
</div>
<!-- 左边的背景图 + 欢迎语 -->
<Transition
appear
enter-active-class="animate__animated animate__bounceInLeft"
tag="div"
>
<div :style="{ marginTop: leftStyle.bottomText.textMt }">
<div
class="pos-relative w-100% overflow-hidden"
:style="{ height: leftStyle.lottie.boxHeight }"
>
<img
class="pos-absolute"
:style="{
left: 0,
top: 0,
right: 0,
bottom: 0,
width: '100%',
height: '100%',
objectFit: 'contain',
objectPosition: 'center center',
}"
src="@/assets/imgs/singBj2.png"
alt="静态图片描述"
/>
<!-- <vue3-lottie
class="pos-absolute"
:style="{ left: leftStyle.lottie.left, top: leftStyle.lottie.top }"
:animation-data="loginAnimationData"
:height="leftStyle.lottie.height"
:width="leftStyle.lottie.width"
:auto-play="true"
:loop="true"
/> -->
</div>
<div
class="w-100% flex items-center justify-center ml--20px gap-x-5px"
:style="{ marginTop: leftStyle.bottomText.linkMT }"
>
</div>
<div
class="flex justify-center w-100%"
:style="{ marginTop: leftStyle.bottomText.textMt }"
>
<div
class="pos-relative overflow-hidden"
:style="{ width: leftStyle.bottomText.width, height: leftStyle.bottomText.height }"
>
<span class="w-100% flex items-center justify-center ml--20px gap-x-5px" style="color:white;font-size:50px">综合监控系统</span>
</div>
</div>
</div>
</Transition>
</div>
<div class="relative flex-1 p-30px bg-#e3f0ff lt-sm:p-10px overflow-x-hidden overflow-y-auto">
<!-- 右上角的主题语言选择 -->
<div
class="relative z-2 flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center at-2xl:hidden at-xl:hidden">
<img alt="" class="mr-10px h-32px" src="@/assets/imgs/logo.png" />
</div>
<div class="flex items-center justify-end space-x-10px">
<ThemeSwitch />
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
</div>
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="pos-relative z-2 m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
>
<!-- 账号登录 -->
<div class="bg-#fff dark:bg-[var(--login-bg-color)] b-rounded-20px px-40px py-40px">
<el-form
v-show="getShow"
ref="formLogin"
:model="loginData.loginForm"
:rules="LoginRules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
link
type="primary"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input
v-model="loginData.loginForm.username"
:placeholder="t('login.usernamePlaceholder')"
:prefix-icon="iconAvatar"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
v-model="loginData.loginForm.password"
:placeholder="t('login.passwordPlaceholder')"
:prefix-icon="iconLock"
show-password
type="password"
@keyup.enter="getCode()"
/>
</el-form-item>
</el-col>
<el-col
:span="24"
style="
padding-right: 10px;
padding-left: 10px;
margin-top: -20px;
margin-bottom: -20px;
"
>
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<el-col :span="6">
<el-checkbox v-model="loginData.loginForm.rememberMe">
{{ t('login.remember') }}
</el-checkbox>
</el-col>
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{
t('login.forgetPassword')
}}</el-link>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="getCode()"
/>
</el-form-item>
</el-col>
<Verify
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="handleLogin"
/>
</el-row>
</el-form>
</div>
</div>
</Transition>
</div>
<div
class="pos-fixed bottom-0px left-50% lt-md:left-0px lt-sm:left-0px lt-xl:left-0px lt-xl:left-0px z-1"
>
<img class="w-138px block" src="@/assets/imgs/login/login_right_bg_1.png" alt="" />
</div>
<div class="pos-fixed right--38px top--23px">
<img class="w-160px block" src="@/assets/imgs/login/login_right_bg_2.png" alt="" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ElLoading } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { useAppStore } from '@/store/modules/app'
import { useIcon } from '@/hooks/web/useIcon'
import { usePermissionStore } from '@/store/modules/permission'
import * as LoginApi from '@/api/login'
import * as authUtil from '@/utils/auth'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin'
import LoginFormTitle from './components/LoginFormTitle.vue'
import router from '@/router'
import { useWindowSize } from '@vueuse/core'
import * as loginAnimation from '@/assets/json/login_left.json'
defineOptions({ name: 'SocialLogin' })
const { t } = useI18n()
const route = useRoute()
const appStore = useAppStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('login')
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const formLogin = ref<any>()
const { validForm } = useFormValid(formLogin)
const { getLoginState } = useLoginState()
const { push } = useRouter()
const permissionStore = usePermissionStore()
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
const windowSize = useWindowSize()
const loginAnimationData = ref(loginAnimation['default'])
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
const LoginRules = {
tenantName: [required],
username: [required],
password: [required]
}
const loginData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
loginForm: {
tenantName: '000000',
username: 'admin',
password: 'admin123',
captchaVerification: '',
rememberMe: false
}
})
const leftStyle = computed(() => {
const winW = windowSize.width.value
const leftW = winW / 2
const logo = {
height: leftW / 16.5 + 'px',
mt: leftW / 31.9 + 'px'
}
const lottieMagn = leftW / 435
const lottie = {
boxHeight: leftW / 2.39 + 'px',
height: 335 * lottieMagn,
width: 434 * lottieMagn,
left: -(leftW / 38) + 'px',
top: -(leftW / 5.3) + 'px'
}
const bottomText = {
width: leftW / 1.54 + 'px',
height: leftW / 8.68 + 'px',
left: -(leftW / 31.54) + 'px',
linkMT: leftW / 23.87 + 'px',
textMt: leftW / 16.2 + 'px'
}
return { lottie, bottomText, logo }
})
// 获取验证码
const getCode = async () => {
// 情况一,未开启:则直接登录
if (!loginData.captchaEnable) {
await handleLogin({})
} else {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
// 弹出验证码
verify.value.show()
}
}
//获取租户ID
const getTenantId = async () => {
if (loginData.tenantEnable) {
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
authUtil.setTenantId(res)
}
}
// 记住我
const getCookie = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe ? true : false,
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
}
}
}
const loading = ref() // ElLoading.service 返回的实例
// tricky: 配合LoginForm.vue中redirectUri需要对参数进行encode需要在回调后进行decode
function getUrlValue(key: string): string {
const url = new URL(decodeURIComponent(location.href))
return url.searchParams.get(key) ?? ''
}
// 尝试登录: 当账号已经绑定socialLogin会直接获得token
const tryLogin = async () => {
try {
debugger
const type = getUrlValue('type')
const redirect = getUrlValue('redirect')
const code = route?.query?.code as string
const state = route?.query?.state as string
const res = await LoginApi.socialLogin(type, code, state)
authUtil.setToken(res)
router.push({ path: redirect || '/' })
} catch (err) {}
}
// 登录
const handleLogin = async (params) => {
loginLoading.value = true
try {
await getTenantId()
const data = await validForm()
if (!data) {
return
}
let redirect = getUrlValue('redirect')
const type = getUrlValue('type')
const code = route?.query?.code as string
const state = route?.query?.state as string
const res = await LoginApi.login({
// 账号密码登录
username: loginData.loginForm.username,
password: loginData.loginForm.password,
captchaVerification: params.captchaVerification,
// 社交登录
socialCode: code,
socialState: state,
socialType: type
})
if (!res) {
return
}
loading.value = ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
if (loginData.loginForm.rememberMe) {
authUtil.setLoginForm(loginData.loginForm)
} else {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
if (!redirect) {
redirect = '/'
}
// 判断是否为SSO登录
if (redirect.indexOf('sso') !== -1) {
window.location.href = window.location.href.replace('/login?redirect=', '')
} else {
push({ path: redirect || permissionStore.addRouters[0].path })
}
} finally {
loginLoading.value = false
loading.value.close()
}
}
onMounted(() => {
getCookie()
tryLogin()
})
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-login;
.#{$prefix-cls} {
overflow: auto;
}
</style>

View File

@@ -0,0 +1,388 @@
<template>
<el-form
v-show="getShow"
ref="formLogin"
:model="loginData.loginForm"
:rules="LoginRules"
class="login-form login-form-default"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%"/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
link
type="primary"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input
v-model="loginData.loginForm.username"
:placeholder="t('login.usernamePlaceholder')"
:prefix-icon="iconAvatar"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
v-model="loginData.loginForm.password"
:placeholder="t('login.passwordPlaceholder')"
:prefix-icon="iconLock"
show-password
type="password"
@keyup.enter="getCode()"
/>
</el-form-item>
</el-col>
<el-col
:span="24"
style="padding-right: 10px; padding-left: 10px; margin-top: -20px; margin-bottom: -20px"
>
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<el-col :span="6">
<el-checkbox v-model="loginData.loginForm.rememberMe">
{{ t('login.remember') }}
</el-checkbox>
</el-col>
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px" class="login-btn-col">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="getCode()"
/>
<div v-if="loginType == 'easy'" class="w-100% mt-6px flex justify-end">
<el-dropdown @command="easyCommand">
<div class="mt--2px cursor-pointer">
<el-text type="primary">
<span class="text-14px">{{ t('login.otherLogin') }}</span>
<Icon :size="16" icon="iconamoon:arrow-down-2-light"/>
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in easyDropdownList" :key="item.label">
<el-dropdown-item :command="item">
<Icon :size="16" :icon="item.icon"/>
<span>{{ item.label }}</span>
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-form-item>
</el-col>
<Verify
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="handleLogin"
/>
<template v-if="!loginType">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<!-- <el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="12">
<XButton
:title="t('login.btnMobile')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.MOBILE)"
/>
</el-col>
<el-col :span="12">
<XButton
:title="t('login.btnQRCode')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.QR_CODE)"
/>
</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-right: 10px; padding-left: 10px">
<el-form-item>
<div class="w-[100%] flex justify-center">
<Icon
v-for="(item, key) in socialList"
:key="key"
:icon="item.icon"
:size="50"
class="anticon cursor-pointer"
color="#999"
@click="doSocialLogin(item.type)"
/>
</div>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import {ElLoading} from 'element-plus'
import LoginFormTitle from './LoginFormTitle.vue'
import type {RouteLocationNormalizedLoaded} from 'vue-router'
import {useIcon} from '@/hooks/web/useIcon'
import * as authUtil from '@/utils/auth'
import {usePermissionStore} from '@/store/modules/permission'
import * as LoginApi from '@/api/login'
import {LoginStateEnum, useFormValid, useLoginState} from './useLogin'
defineOptions({name: 'LoginForm'})
interface Props {
loginType?: 'easy'
}
const props = defineProps<Props>()
const {t} = useI18n()
const message = useMessage()
const iconHouse = useIcon({icon: 'ep:house'})
const iconAvatar = useIcon({icon: 'ep:avatar'})
const iconLock = useIcon({icon: 'ep:lock'})
const formLogin = ref()
const {validForm} = useFormValid(formLogin)
const {setLoginState, getLoginState} = useLoginState()
const {currentRoute, push} = useRouter()
const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
const LoginRules = {
tenantName: [required],
username: [required],
password: [required]
}
const loginData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: {
tenantName: '000000',
username: import.meta.env.VITE_LOGIN_USERNAME || '',
password: import.meta.env.VITE_LOGIN_PASSWORD || '',
captchaVerification: '',
rememberMe: false
}
})
const socialList = [
// { icon: 'ant-design:wechat-filled', type: 30 },
// { icon: 'ant-design:dingtalk-circle-filled', type: 20 },
// { icon: 'ant-design:github-filled', type: 0 },
// { icon: 'ant-design:alipay-circle-filled', type: 0 },
{icon: 'bx:planet', type: 100},
]
const easyDropdownList = [
{
label: t('login.btnMobile'),
icon: 'ant-design:phone-twotone',
type: LoginStateEnum.MOBILE,
code: 'page'
},
{
label: t('login.btnQRCode'),
icon: 'ant-design:qrcode-outlined',
type: LoginStateEnum.QR_CODE,
code: 'page'
},
{label: t('login.btnWechat'), icon: 'ant-design:wechat-filled', type: 30, code: 'url'},
{
label: t('login.btnDingtalk'),
icon: 'ant-design:dingtalk-circle-filled',
type: 20,
code: 'url'
},
{label: t('login.btnGitHub'), icon: 'ant-design:github-filled', type: 0, code: 'url'},
{label: t('login.btnAlipay'), icon: 'ant-design:alipay-circle-filled', type: 0, code: 'url'}
]
const easyCommand = (row) => {
if (row.code == 'page') setLoginState(row.type)
else if (row.code == 'url') doSocialLogin(row.type)
}
// 获取验证码
const getCode = async () => {
// 情况一,未开启:则直接登录
if (loginData.captchaEnable === 'false') {
await handleLogin({})
} else {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
// 弹出验证码
verify.value.show()
}
}
// 获取租户 ID
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
authUtil.setTenantId(res)
}
}
// 记住我
const getCookie = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe ? true : false,
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
}
}
}
// 根据域名,获得租户信息
const getTenantByWebsite = async () => {
const website = location.host
const res = await LoginApi.getTenantByWebsite(website)
if (res) {
loginData.loginForm.tenantName = res.name
authUtil.setTenantId(res.id)
}
}
const loading = ref() // ElLoading.service 返回的实例
// 登录
const handleLogin = async (params) => {
loginLoading.value = true
try {
const data = await validForm()
if (!data) return
loading.value = ElLoading.service({
lock: true,
text: '登录中...',
background: 'rgba(0, 0, 0, 0.7)'
})
await getTenantId()
loginData.loginForm.captchaVerification = params.captchaVerification
const res = await LoginApi.login(loginData.loginForm)
if (!res) return
if (loginData.loginForm.rememberMe) {
authUtil.setLoginForm(loginData.loginForm)
} else {
authUtil.removeLoginForm()
}
authUtil.setToken(res)
if (!redirect.value) {
redirect.value = '/'
}
// 判断是否为SSO登录
if (redirect.value.indexOf('sso') !== -1) {
window.location.href = window.location.href.replace('/login?redirect=', '')
} else {
push({path: redirect.value || permissionStore.addRouters[0].path})
}
} finally {
loginLoading.value = false
loading.value?.close()
}
}
// 社交登录
const doSocialLogin = async (type: number) => {
if (type === 0) {
message.error('此方式未配置')
} else {
loginLoading.value = true
if (loginData.tenantEnable === 'true') {
// 尝试先通过 tenantName 获取租户
await getTenantId()
// 如果获取不到,则需要弹出提示,进行处理
if (!authUtil.getTenantId()) {
await message.prompt('请输入租户名称', t('common.reminder')).then(async ({value}) => {
const res = await LoginApi.getTenantIdByName(value)
authUtil.setTenantId(res)
})
}
}
// 计算 redirectUri
// tricky: type、redirect需要先encode一次否则钉钉回调会丢失。
// 配合 Login/SocialLogin.vue#getUrlValue() 使用
const redirectUri =
location.origin +
'/social-login?' +
encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
// 进行跳转
const res = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
window.location.href = res
}
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
onMounted(() => {
if(redirect.value && import.meta.env.VITE_DEFAULT_SSO =='true'){
//默认
doSocialLogin(100)
}
getCookie()
getTenantByWebsite()
})
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.login-code {
float: right;
width: 100%;
height: 38px;
img {
width: 100%;
height: auto;
max-width: 100px;
vertical-align: middle;
cursor: pointer;
}
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
<template>
<el-row
class="login-form login-form-qrcode"
v-show="getShow"
style="margin-right: -10px; margin-left: -10px"
>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<LoginFormTitle style="width: 100%" />
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-card class="mb-10px text-center" shadow="hover">
<Qrcode :logo="logoImg" />
<div class="">{{ t('login.qrcode') }}</div>
</el-card>
<div class="w-100% flex justify-end mt-5px mr-2px">
<el-button link type="primary" @click="handleBackLogin()">{{
t('login.backLogin')
}}</el-button>
</div>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import logoImg from '@/assets/imgs/logo_min.png'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useLoginState } from './useLogin'
defineOptions({ name: 'QrCodeForm' })
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
</script>

View File

@@ -0,0 +1,199 @@
<template>
<div v-show="ssoVisible" class="form-cont">
<!-- 应用名 -->
<LoginFormTitle style="width: 100%" />
<el-tabs class="form" style="float: none" value="uname">
<el-tab-pane :label="client.name" name="uname" />
</el-tabs>
<div>
<el-form :model="formData" class="login-form">
<!-- 授权范围的选择 -->
此第三方应用请求获得以下权限
<el-form-item prop="scopes">
<el-checkbox-group v-model="formData.scopes">
<el-checkbox
v-for="scope in queryParams.scopes"
:key="scope"
:label="scope"
style="display: block; margin-bottom: -10px"
>
{{ formatScope(scope) }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 下方的登录按钮 -->
<el-form-item class="w-1/1">
<el-button
:loading="formLoading"
class="w-6/10"
type="primary"
@click.prevent="handleAuthorize(true)"
>
<span v-if="!formLoading">同意授权</span>
<span v-else> 中...</span>
</el-button>
<el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script lang="ts" setup>
import LoginFormTitle from './LoginFormTitle.vue'
import * as OAuth2Api from '@/api/login/oauth2'
import { LoginStateEnum, useLoginState } from './useLogin'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
defineOptions({ name: 'SSOLogin' })
const route = useRoute() // 路由
const { currentRoute } = useRouter() // 路由
const { getLoginState, setLoginState } = useLoginState()
const client = ref({
// 客户端信息
name: '',
logo: ''
})
interface queryType {
responseType: string
clientId: string
redirectUri: string
state: string
scopes: string[]
}
const queryParams = reactive<queryType>({
// URL 上的 client_id、scope 等参数
responseType: '',
clientId: '',
redirectUri: '',
state: '',
scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
})
const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单
interface formType {
scopes: string[]
}
const formData = reactive<formType>({
scopes: [] // 已选中的 scope 数组
})
const formLoading = ref(false) // 表单是否提交中
/** 初始化授权信息 */
const init = async () => {
// 防止在没有登录的情况下循环弹窗
if (typeof route.query.client_id === 'undefined') return
// 解析参数
// 例如说【自动授权不通过】client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
// 例如说【自动授权通过】client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
queryParams.responseType = route.query.response_type as string
queryParams.clientId = route.query.client_id as string
queryParams.redirectUri = route.query.redirect_uri as string
queryParams.state = route.query.state as string
if (route.query.scope) {
queryParams.scopes = (route.query.scope as string).split(' ')
}
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
if (queryParams.scopes.length > 0) {
const data = await doAuthorize(true, queryParams.scopes, [])
if (data) {
location.href = data
return
}
}
// 获取授权页的基本信息
const data = await OAuth2Api.getAuthorize(queryParams.clientId)
client.value = data.client
// 解析 scope
let scopes
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
if (queryParams.scopes.length > 0) {
scopes = []
for (const scope of data.scopes) {
if (queryParams.scopes.indexOf(scope.key) >= 0) {
scopes.push(scope)
}
}
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
} else {
scopes = data.scopes
for (const scope of scopes) {
queryParams.scopes.push(scope.key)
}
}
// 生成已选中的 checkedScopes
for (const scope of scopes) {
if (scope.value) {
formData.scopes.push(scope.key)
}
}
}
/** 处理授权的提交 */
const handleAuthorize = async (approved) => {
// 计算 checkedScopes + uncheckedScopes
let checkedScopes
let uncheckedScopes
if (approved) {
// 同意授权,按照用户的选择
checkedScopes = formData.scopes
uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1)
} else {
// 拒绝,则都是取消
checkedScopes = []
uncheckedScopes = queryParams.scopes
}
// 提交授权的请求
formLoading.value = true
try {
const data = await doAuthorize(false, checkedScopes, uncheckedScopes)
if (!data) {
return
}
location.href = data
} finally {
formLoading.value = false
}
}
/** 调用授权 API 接口 */
const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
return OAuth2Api.authorize(
queryParams.responseType,
queryParams.clientId,
queryParams.redirectUri,
queryParams.state,
autoApprove,
checkedScopes,
uncheckedScopes
)
}
/** 格式化 scope 文本 */
const formatScope = (scope) => {
// 格式化 scope 授权范围,方便用户理解。
// 这里仅仅是一个 demo可以考虑录入到字典数据中例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
switch (scope) {
case 'user.read':
return '访问你的个人信息'
case 'user.write':
return '修改你的个人信息'
default:
return scope
}
}
/** 监听当前路由为 SSOLogin 时,进行数据的初始化 */
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
if (route.name === 'SSOLogin') {
setLoginState(LoginStateEnum.SSO)
init()
}
},
{ immediate: true }
)
</script>

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
<template>
<div :class="prefixCls" class="relative w-100% h-100vh">
<div
class="content flex pos-relative left-0 top-50% z-2 w-100% h-31.25vw transform-translate-y--50%"
>
<div
class="pos-absolute top-50% right-10% transform-translate-y--50% content-form bg-#fff inline-block border-rd-10px"
>
<div class="pos-relative w-300px p-34px">
<div
class="pos-absolute top-8px right-8px flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center justify-end space-x-10px">
<LocaleDropdown class="dark:text-white lt-xl:text-white" />
</div>
</div>
<!-- 账号登录 -->
<LoginForm loginType="easy" />
<!-- 手机登录 -->
<MobileForm />
<!-- 二维码登录 -->
<QrCodeForm />
<!-- 三方登录 -->
<SSOLoginVue />
</div>
</div>
</div>
<!-- transform-translate-y--330px -->
<div
class="w-100% pos-absolute left-0 w-100% h-auto z-10 transform-translate-y--50%"
:style="{ top: bgTop }"
>
<el-divider>
<div class="flex items-center">
<el-image fit="cover" alt="" class="h-48px w-auto" :src="logoImage" />
<span class="ml-10px text-20px">JeeLowCode 低代码开发平台</span>
</div>
</el-divider>
</div>
<div class="pos-absolute left-0 bottom-0 w-100% h-auto z-1">
<el-image class="w-100% h-auto" :src="footerImage" fit="cover" />
<div class="pos-absolute left-0 bottom-5px w-100% text-center">
<a href="http://beian.miit.gov.cn" class="c-#0008 text-12px">粤ICP备xxx号</a>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useDesign } from '@/hooks/web/useDesign'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { useWindowSize } from '@vueuse/core'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from '../components'
import footerImage from '@/assets/imgs/login/login_sutra_2.png'
import logoImage from '@/assets/imgs/logo_min.png'
defineOptions({ name: 'SutraLogin' })
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('sutra-login')
const windowSize = useWindowSize()
const bgTop = computed(() => {
const bgHeight = windowSize.width.value / 3.2
const topHeight = (windowSize.height.value - bgHeight) / 4 + 20
if (topHeight < 30) return '30px'
return topHeight + 'px'
})
</script>
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-sutra-login;
.#{$prefix-cls} {
overflow: hidden;
.content {
overflow: hidden;
background-image: url('@/assets/imgs/login/login_sutra_1.png');
background-size: cover;
.content-form {
::v-deep(.login-form-default) {
.el-divider {
margin: 5px 0 20px;
}
}
::v-deep(.login-form) {
.form-title {
margin: 0 !important;
font-family: '微软雅黑', sans-serif;
font-size: 30px;
font-weight: 500;
color: var(--el-color-primary);
}
.login-btn-col {
.el-form-item {
margin-bottom: 5px;
}
}
}
::v-deep(.login-form-qrcode) {
.form-title {
margin-bottom: 15px !important;
}
}
}
}
}
</style>

View File

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

View File

@@ -0,0 +1,88 @@
<template>
<div class="py-10px">
<avue-form
v-model="formData"
:option="formOption"
@submit="submit"
@reset-change="init"
v-loading="loading"
>
</avue-form>
</div>
</template>
<script lang="ts" setup>
import { getUserProfile, updateUserProfile } from '@/api/system/user/profile'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'BasicInfo' })
const { t } = useI18n()
const message = useMessage() // 消息弹窗
const userStore = useUserStore()
const formData = ref<any>({})
const formOption = ref({
labelWidth: 120,
span: 24,
submitText: t('common.save'),
emptyText: t('common.reset'),
column: {
nickname: {
label: t('profile.user.nickname'),
rules: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }]
},
mobile: {
label: t('profile.user.mobile'),
rules: [
{ 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'
}
]
},
email: {
label: t('profile.user.email'),
rules: [
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
]
},
sex: {
label: t('profile.user.sex'),
type: 'radio',
dicData: [
{ label: t('profile.user.man'), value: 1 },
{ label: t('profile.user.woman'), value: 2 }
],
value: 0
}
}
})
const loading = ref(false)
const submit = async (form, done) => {
loading.value = true
done()
await updateUserProfile(form)
message.success(t('common.updateSuccess'))
const profile = await init()
userStore.setUserNicknameAction(profile.nickname)
userStore.setUserMobilAction(profile.mobile)
}
const init = async () => {
loading.value = true
const res = await getUserProfile()
formData.value = res
loading.value = false
return res
}
onMounted(async () => {
await init()
})
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div>
<div class="text-center">
<UserAvatar :img="userInfo?.avatar" />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:user" />
{{ t('profile.user.username') }}
<div class="pull-right">{{ userInfo?.username }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:phone" />
{{ t('profile.user.mobile') }}
<div class="pull-right">{{ userInfo?.mobile }}</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="fontisto:email" />
{{ t('profile.user.email') }}
<div class="pull-right">{{ userInfo?.email }}</div>
</li>
<li class="list-group-item flex justify-between">
<div class="flex-basis-100px flex-shrink-0">
<Icon class="mr-5px" icon="carbon:tree-view-alt" />
{{ t('profile.user.dept') }}
</div>
<div class="flex-1 flex flex-wrap justify-end gap-5px">
<div v-for="dept in userInfo.deptInfoList" :key="dept.deptId">
<el-popover placement="right" width="240" trigger="hover">
<template #reference>
<el-tag type="primary" class="cursor-pointer">{{ dept.deptName }}</el-tag>
</template>
<div class="flex flex-col gap-y-4px">
<template v-for="type in keyList" :key="type.value">
<div class="flex" v-if="dept[type.key]">
<span class="flex-basis-42px flex-shrink-0 c-#909399">{{ type.label }}</span>
<span class="flex-1">{{ dept[type.key] }}</span>
</div>
</template>
</div>
</el-popover>
</div>
</div>
</li>
<li class="list-group-item" v-if="userInfo.rankInfoList?.length">
<Icon class="mr-5px" icon="ep:suitcase" />
{{ t('profile.user.rank') }}
<div v-if="userInfo?.rankInfoList" class="pull-right">
{{ userInfo?.rankInfoList.map((rank) => rank.name).join(',') }}
</div>
</li>
<li class="list-group-item">
<Icon class="mr-5px" icon="ep:calendar" />
{{ t('profile.user.createTime') }}
<div class="pull-right">{{ formatDate(userInfo.createTime) }}</div>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import UserAvatar from './UserAvatar.vue'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
defineOptions({ name: 'ProfileUser' })
const { t } = useI18n()
const userInfo = ref({} as ProfileVO)
const keyList = [
{ label: '部门', value: 'deptName', key: 'deptName' },
{ label: '角色', value: 'roleInfoList', key: 'role' },
{ label: '职务', value: 'dutyInfoList', key: 'duty' },
{ label: '职位', value: 'positionInfoList', key: 'position' },
{ label: '岗位', value: 'postInfoList', key: 'post' }
]
const getUserInfo = async () => {
const users = await getUserProfile()
users.deptInfoList = users.deptInfoList.map((dept) => {
keyList.forEach(({ value, key }) => {
if (value === 'deptName') return
if (dept[value]?.length) dept[key] = dept[value].map((info) => info.name).join('、')
else dept[key] = ''
})
return dept
})
userInfo.value = users
}
onMounted(async () => {
await getUserInfo()
})
</script>
<style scoped>
.text-center {
position: relative;
height: 120px;
text-align: center;
}
.list-group-striped > .list-group-item {
padding-right: 0;
padding-left: 0;
border-right: 0;
border-left: 0;
border-radius: 0;
}
.list-group {
padding-left: 0;
list-style: none;
}
.list-group-item {
padding: 11px 0;
margin-bottom: -1px;
font-size: 13px;
border-top: 1px solid #e7eaec;
border-bottom: 1px solid #e7eaec;
}
.pull-right {
float: right !important;
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="py-10px">
<avue-form v-model="formData" :option="formOption" @submit="submit" v-loading="loading">
<template #oldPassword>
<InputPassword v-model="formData.oldPassword" />
</template>
<template #newPassword>
<InputPassword v-model="formData.newPassword" strength />
</template>
<template #confirmPassword>
<InputPassword v-model="formData.confirmPassword" strength />
</template>
</avue-form>
</div>
</template>
<script lang="ts" setup>
import { InputPassword } from '@/components/InputPassword'
import { updateUserPassword } from '@/api/system/user/profile'
defineOptions({ name: 'ResetPwd' })
const { t } = useI18n()
const message = useMessage()
const equalToPassword = (_rule, value, callback) => {
if (formData.value.newPassword !== value) {
callback(new Error(t('profile.password.diffPwd')))
} else {
callback()
}
}
const loading = ref(false)
const formData = ref<any>({})
const formOption = ref({
labelWidth: 120,
span: 24,
submitText: t('common.save'),
emptyText: t('common.reset'),
column: {
oldPassword: {
label: t('profile.password.oldPassword'),
rules: [
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
]
},
newPassword: {
label: t('profile.password.newPassword'),
rules: [
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
]
},
confirmPassword: {
label: t('profile.password.confirmPassword'),
rules: [
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
}
}
})
const submit = async (form, done) => {
done()
loading.value = true
await updateUserPassword(form.oldPassword, form.newPassword).finally(
() => (loading.value = false)
)
message.success(t('common.updateSuccess'))
}
</script>

View File

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

View File

@@ -0,0 +1,107 @@
<template>
<el-table :data="socialUsers" :show-header="false">
<el-table-column fixed="left" title="序号" type="seq" width="60" />
<el-table-column align="left" label="社交平台" width="120">
<template #default="{ row }">
<img :src="row.img" alt="" class="h-5 align-middle" />
<p class="mr-5">{{ row.title }}</p>
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template #default="{ row }">
<template v-if="row.openid">
已绑定
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" />
</template>
<template v-else>
未绑定
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" />
</template>
</template>
</el-table-column>
</el-table>
</template>
<script lang="ts" setup>
import { SystemUserSocialTypeEnum } from '@/utils/constants'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
defineOptions({ name: 'UserSocial' })
defineProps<{
activeName: string
}>()
const message = useMessage()
const socialUsers = ref<any[]>([])
const userInfo = ref<ProfileVO>()
const initSocial = async () => {
socialUsers.value = [] // 重置避免无限增长
const res = await getUserProfile()
userInfo.value = res
for (const i in SystemUserSocialTypeEnum) {
const socialUser = { ...SystemUserSocialTypeEnum[i] }
socialUsers.value.push(socialUser)
if (userInfo.value?.socialUsers) {
for (const j in userInfo.value.socialUsers) {
if (socialUser.type === userInfo.value.socialUsers[j].type) {
socialUser.openid = userInfo.value.socialUsers[j].openid
break
}
}
}
}
}
const route = useRoute()
const emit = defineEmits<{
(e: 'update:activeName', v: string): void
}>()
const bindSocial = () => {
// 社交绑定
const type = getUrlValue('type')
const code = route.query.code
const state = route.query.state
if (!code) {
return
}
socialBind(type, code, state).then(() => {
message.success('绑定成功')
emit('update:activeName', 'userSocial')
})
}
// 双层 encode 需要在回调后进行 decode
function getUrlValue(key: string): string {
const url = new URL(decodeURIComponent(location.href))
return url.searchParams.get(key) ?? ''
}
const bind = (row) => {
// 双层 encode 解决钉钉回调 type 参数丢失的问题
const redirectUri = location.origin + '/user/profile?' + encodeURIComponent(`type=${row.type}`)
// 进行跳转
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
window.location.href = res
})
}
const unbind = async (row) => {
const res = await socialUnbind(row.type, row.openid)
if (res) {
row.openid = undefined
}
message.success('解绑成功')
}
onMounted(async () => {
await initSocial()
})
watch(
() => route,
() => {
bindSocial()
},
{
immediate: true
}
)
</script>

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

View File

@@ -0,0 +1,165 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model:page="tablePage"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu="{ row }">
<el-button
type="primary"
text
@click="handleAssignRule(row)"
v-hasPermi="['bpm:task-assign-rule:query']"
>
分配规则
</el-button>
</template>
<template #name="{ row }">
<el-button type="primary" link @click="handleBpmnDetail(row)">
<span>{{ row.name }}</span>
</el-button>
</template>
<template #category="{ row }">
<dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="row.category || ''" />
</template>
<template #version="{ row }">
<el-tag>v{{ row.version }}</el-tag>
</template>
<template #suspensionState="{ row }">
<el-tag type="success" v-if="row.suspensionState === 1">激活</el-tag>
<el-tag type="warning" v-if="row.suspensionState === 2">挂起</el-tag>
</template>
</avue-crud>
</ContentWrap>
<!-- 弹窗流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML || ''"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as DefinitionApi from '@/api/bpm/definition'
defineOptions({ name: 'ModelVersions' })
const { getCurrPermi } = useCrudPermi()
const { query } = useRoute() // 查询参数
const { push } = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
editBtn: false,
delBtn: false,
addBtn: false,
menuWidth: 140,
align: 'center',
headerAlign: 'center',
calcHeight: 20,
column: [
{ prop: 'id', label: '定义编号', width: 370 },
{ prop: 'name', label: '流程名称' },
{
prop: 'category',
label: '定义分类',
width: 100,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY)
},
{ prop: 'version', label: '流程版本', width: 100, bind: 'processDefinition.version' },
{ prop: 'suspensionState', label: '状态', width: 80 },
{
prop: 'deploymentTime',
label: '部署时间',
display: false,
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
{
prop: 'description',
label: '定义描述',
type: 'textarea'
}
]
}) //表格配置
const tableData = ref([])
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['bpm:model'])
const crudRef = ref()
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
/** 点击任务分配按钮 */
const handleAssignRule = (row) => {
push({
name: 'BpmTaskAssignRuleList',
query: {
modelId: row.id
}
})
}
const handleBpmnDetail = async (row) => {
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
bpmnDetailVisible.value = true
}
const getTableData = async () => {
loading.value = true
const searchObj = {
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
key: query.key
}
const data = await DefinitionApi.getProcessDefinitionPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
loading.value = false
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
getTableData()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
useCrudHeight(crudRef)
onMounted(() => {
tablePage.value.currentPage = 1
getTableData()
})
</script>

View File

@@ -0,0 +1,221 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as UserGroupApi from '@/api/bpm/userGroup'
import * as UserApi from '@/api/system/user'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemDictType' })
interface DictType {
label: string
value: number
}
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: {
id: {
label: '编号',
display: false
},
name: {
label: '组名',
search: true,
rules: [{ required: true, message: '组名不能为空', trigger: 'blur' }]
},
description: {
label: '描述',
type: 'textarea',
minRows: 2,
maxRows: 4
},
memberUserIds: {
label: '成员',
type: 'select',
span: 12,
multiple: true,
dicData: [] as DictType[],
rules: [{ required: true, message: '成员不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'radio',
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
search: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['bpm:user-group'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await UserGroupApi.getUserGroupPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await UserGroupApi.getUserGroup(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await UserGroupApi.createUserGroup(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await UserGroupApi.updateUserGroup(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await UserGroupApi.deleteUserGroup(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
// 加载用户列表
const data = await UserApi.getSimpleUserList()
tableOption.column.memberUserIds.dicData = data.map((item) => {
return {
label: item.nickname,
value: item.id
}
})
await getTableData()
})
</script>

View File

@@ -0,0 +1,140 @@
<template>
<Dialog v-model="dialogVisible" title="导入流程" width="400">
<div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:data="formData"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".bpmn, .xml"
drag
name="bpmnFile"
>
<Icon class="el-icon--upload" icon="ep:upload-filled" />
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入bpmxml格式文件
</div>
<div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
placeholder="请输入流标标识"
style="width: 250px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
</el-form>
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'ModelImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
key: '',
name: '',
description: ''
})
const formRules = reactive({
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const uploadRef = ref() // 上传 Ref
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = async (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
// 提示成功
message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
// 发送操作成功的事件
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('导入流程失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
// 重置表单
formData.value = {
key: '',
name: '',
description: ''
}
formRef.value?.resetFields()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

View File

@@ -0,0 +1,105 @@
<template>
<ContentWrap>
<!-- 流程设计器负责绘制流程等 -->
<MyProcessDesigner
key="designer"
v-if="xmlString !== undefined"
v-model="xmlString"
:value="xmlString"
v-bind="controlForm"
keyboard
ref="processDesigner"
@init-finished="initModeler"
:additionalModel="controlForm.additionalModel"
@save="save"
/>
<!-- 流程属性器负责编辑每个流程节点的属性 -->
<MyProcessPenal
key="penal"
:bpmnModeler="modeler as any"
:prefix="controlForm.prefix"
class="process-panel"
:model="model"
/>
</ContentWrap>
</template>
<script lang="ts" setup>
import { MyProcessDesigner, MyProcessPenal } from '@/components/bpmnProcessDesigner/package'
// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务)
import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad'
// 自定义左侧菜单(修改 默认任务 为 用户任务)
import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette'
import * as ModelApi from '@/api/bpm/model'
defineOptions({ name: 'BpmModelEditor' })
const router = useRouter() // 路由
const { query } = useRoute() // 路由的查询
const message = useMessage() // 国际化
const xmlString = ref(undefined) // BPMN XML
const modeler = ref(null) // BPMN Modeler
const controlForm = ref({
simulation: true,
labelEditing: false,
labelVisible: false,
prefix: 'flowable',
headerButtonSize: 'mini',
additionalModel: [CustomContentPadProvider, CustomPaletteProvider]
})
const model = ref<ModelApi.ModelVO>() // 流程模型的信息
/** 初始化 modeler */
const initModeler = (item) => {
setTimeout(() => {
modeler.value = item
}, 10)
}
/** 添加/修改模型 */
const save = async (bpmnXml) => {
const data = {
...model.value,
bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
} as unknown as ModelApi.ModelVO
// 提交
if (data.id) {
await ModelApi.updateModel(data)
message.success('修改成功')
} else {
await ModelApi.createModel(data)
message.success('新增成功')
}
// 跳转回去
close()
}
/** 关闭按钮 */
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
/** 初始化 */
onMounted(async () => {
const modelId = query.modelId as unknown as number
if (!modelId) {
message.error('缺少模型 modelId 编号')
return
}
// 查询模型
const data = await ModelApi.getModel(modelId)
xmlString.value = data.bpmnXml
model.value = {
...data,
bpmnXml: undefined // 清空 bpmnXml 属性
}
})
</script>
<style lang="scss">
.process-panel__container {
position: absolute;
top: 90px;
right: 60px;
}
</style>

View File

@@ -0,0 +1,523 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-del="rowDel"
@row-update="rowUpdate"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left>
<el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
<Icon icon="ep:upload" class="mr-5px" /> 导入流程
</el-button>
</template>
<template #menu="scope">
<el-button
link
type="primary"
@click="crudRef.rowEdit(scope.row, scope.index)"
v-hasPermi="['bpm:model:update']"
>
修改流程
</el-button>
<el-button
link
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
>
设计流程
</el-button>
<el-button
link
type="primary"
@click="handleAssignRule(scope.row)"
v-hasPermi="['bpm:task-assign-rule:query']"
>
分配规则
</el-button>
<el-button
link
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
>
发布流程
</el-button>
<el-button
link
type="primary"
v-hasPermi="['bpm:process-definition:query']"
@click="handleDefinitionList(scope.row)"
>
流程定义
</el-button>
<el-button
link
type="danger"
@click="crudRef.rowDel(scope.row, scope.index)"
v-hasPermi="['bpm:model:delete']"
>
删除
</el-button>
</template>
<template #name="scope">
<el-button type="primary" link @click="handleBpmnDetail(scope.row)">
<span>{{ scope.row.name }}</span>
</el-button>
</template>
<template #category="scope">
<dict-tag
v-if="scope.row.category"
:type="DICT_TYPE.BPM_MODEL_CATEGORY"
:value="scope.row.category"
/>
</template>
<template #formType="{ row }">
<el-button v-if="row.formType === 10" type="primary" link @click="handleFormDetail(row)">
<span>{{ row.formName }}</span>
</el-button>
<el-button
v-else-if="row.formType === 20"
type="primary"
link
@click="handleFormDetail(row)"
>
<span>{{ row.formCustomCreatePath }}</span>
</el-button>
<label v-else>暂无表单</label>
</template>
<template #processDefinitionVersion="scope">
<el-tag v-if="scope.row.processDefinition">
v{{ scope.row.processDefinition.version }}
</el-tag>
<el-tag v-else type="warning">未部署</el-tag>
</template>
<template #processDefinitionSuspensionState="scope">
<el-switch
v-if="scope.row.processDefinition"
v-model="scope.row.processDefinition.suspensionState"
:active-value="1"
:inactive-value="2"
@change="handleChangeState(scope.row)"
/>
</template>
<template #deploymentTime="scope">
<span v-if="scope.row.processDefinition">
{{ formatDate(scope.row.processDefinition.deploymentTime) }}
</span>
</template>
</avue-crud>
</ContentWrap>
<!-- 弹窗流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
<MyProcessViewer
key="designer"
v-model="bpmnXML"
:value="bpmnXML || ''"
v-bind="bpmnControlForm"
:prefix="bpmnControlForm.prefix"
/>
</Dialog>
<!-- 表单弹窗导入流程 -->
<ModelImportForm ref="importFormRef" @success="getTableData" />
<!-- 表单预览 -->
<FormView
v-model="showFormView"
formType="add"
showType="dialog"
:showButton="false"
v-bind="formOption"
></FormView>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as ModelApi from '@/api/bpm/model'
import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtnText: '新增流程',
editBtn: false,
delBtn: false,
menuWidth: 300,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: [
{
prop: 'key',
label: '流程标识',
search: true,
rules: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
editDisabled: true
},
{
prop: 'name',
label: '流程名称',
search: true,
rules: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
editDisabled: true
},
{
prop: 'category',
label: '流程分类',
width: 100,
addDisplay: false,
editDisplay: true,
search: true,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY).map(el=>{return{...el, value: el.value + ''}}),
rules: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }]
},
{
prop: 'description',
label: '流程描述',
type: 'textarea',
hide: true,
minRows: 2,
maxRows: 4
},
{
prop: 'formType',
label: '表单类型',
type: 'radio',
dicData: getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE),
hide: true,
addDisplay: false,
editDisplay: true,
control: (val) => {
return {
formId: { display: val == 10 },
formCustomCreatePath: { display: val == 20 },
formCustomViewPath: { display: val == 20 }
}
}
},
{
prop: 'formId',
label: '流程表单',
type: 'select',
hide: true,
filterable: true,
display: false,
dicUrl: '/jeelowcode/desform/page',
dicMethod: 'post',
props: { label: 'desformName', value: 'id' },
dicFormatter: (data) => data.records
},
{
prop: 'formCustomCreatePath',
label: '表单提交路由',
hide: true,
display: false,
labelTip: '自定义表单的提交路径,使用 Vue 的路由地址'
},
{
prop: 'formCustomViewPath',
label: '表单查看地址',
hide: true,
display: false,
labelTip: '自定义表单的查看组件地址,使用 Vue 的组件地址'
},
{
prop: 'formType',
label: '表单信息',
display: false
},
{
prop: 'createTime',
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
{
label: '最新部署的流程定义',
children: [
{
prop: 'processDefinitionVersion',
label: '流程版本',
width: 100,
display: false
},
{
prop: 'processDefinitionSuspensionState',
label: '激活状态',
width: 100,
display: false
},
{
prop: 'deploymentTime',
label: '部署时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
]
}
]
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const showFormView = ref(false)
const formOption = ref({
formId: '',
popOption: {
title: ''
}
})
const permission = getCurrPermi(['bpm:model'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ModelApi.getModelPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ModelApi.getModel(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let params = {
name: form.name,
key: form.key,
description: form.description,
formCustomViewPath: '',
formCustomCreatePath: '',
formId: '',
formType: 10
}
let bool = await ModelApi.createModel(params).catch(() => false)
if (bool) {
// 提示,引导用户做后续的操作
await ElMessageBox.alert(
'<strong>新建模型成功!</strong>后续需要执行如下 4 个步骤:' +
'<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
'<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>3. 点击【分配规则】按钮,设置每个用户任务的审批人</div>' +
'<div>4. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
'重要提示',
{
dangerouslyUseHTMLString: true,
type: 'success'
}
)
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ModelApi.updateModel(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
resetChange()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ModelApi.deleteModel(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row) => {
const state = row.processDefinition.suspensionState
try {
// 修改状态的二次确认
const id = row.id
const statusState = state === 1 ? '激活' : '挂起'
const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
await message.confirm(content)
// 发起修改状态
await ModelApi.updateModelState(id, state)
// 刷新列表
await getTableData()
} catch {
// 取消后,进行恢复按钮
row.processDefinition.suspensionState = state === 1 ? 2 : 1
}
}
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXML = ref(null)
const bpmnControlForm = ref({
prefix: 'flowable'
})
const handleBpmnDetail = async (row) => {
const data = await ModelApi.getModel(row.id)
bpmnXML.value = data.bpmnXml || ''
bpmnDetailVisible.value = true
}
/** 流程表单的详情按钮操作 */
const handleFormDetail = async (row) => {
if (row.formType == 10) {
formOption.value.formId = row.formId
formOption.value.popOption.title = row.formName
showFormView.value = true
} else {
await push({
path: row.formCustomCreatePath
})
}
}
/** 添加/修改操作 */
const importFormRef = ref()
const openImportForm = () => {
importFormRef.value.open()
}
/** 设计流程 */
const handleDesign = (row) => {
push({
name: 'BpmModelEditor',
query: {
modelId: row.id
}
})
}
/** 点击任务分配按钮 */
const handleAssignRule = (row) => {
push({
name: 'BpmTaskAssignRuleList',
query: {
modelId: row.id
}
})
}
/** 发布流程 */
const handleDeploy = async (row) => {
try {
// 删除的二次确认
await message.confirm('是否部署该流程!!')
// 发起部署
await ModelApi.deployModel(row.id)
message.success(t('部署成功'))
// 刷新列表
await getTableData()
} catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
push({
name: 'BpmProcessDefinition',
query: {
key: row.key
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,164 @@
<template>
<!-- 第一步通过流程定义的列表选择对应的流程 -->
<ContentWrap v-if="!selectProcessInstance">
<avue-crud
ref="crudRef"
v-model="tableForm"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
@refresh-change="getTableData"
>
<template #menu="{ row }">
<el-button link type="primary" @click="handleSelect(row)" v-hasPermi="['bpm:model:update']">
选择
</el-button>
</template>
<template #category="{ row }">
<el-tag>{{ row['$category'] }}</el-tag>
</template>
<template #version="{ row }">
<el-tag>v{{ row.version }}</el-tag>
</template>
</avue-crud>
</ContentWrap>
<!-- 第二步填写表单进行流程的提交 -->
<ContentWrap v-else>
<el-card class="box-card">
<div class="clearfix">
<span class="el-icon-document">申请信息{{ selectProcessInstance.name }}</span>
<el-button style="float: right" type="primary" @click="selectProcessInstance = undefined">
<Icon icon="ep:delete" /> 选择其它流程
</el-button>
</div>
<el-col :span="24" style="margin-top: 20px">
<FormView
:form-id="formId"
formType="add"
showType="view"
:beforeClose="submitForm"
></FormView>
</el-col>
</el-card>
<!-- 流程图预览 -->
<ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML || ''" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const router = useRouter() // 路由
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
header: false,
editBtn: false,
delBtn: false,
menuWidth: 300,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: [
{
prop: 'name',
label: '流程名称',
minWidth: 120
},
{
prop: 'category',
label: '流程分类',
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY),
minWidth: 90
},
{
prop: 'version',
label: '流程版本',
minWidth: 90
},
{
prop: 'description',
label: '流程描述',
type: 'textarea',
minWidth: 120
}
]
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const selectProcessInstance = ref() // 选择的流程实例
const bpmnXML = ref(null)
const formId = ref('')
const permission = getCurrPermi(['bpm:model'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
try {
tableData.value = await DefinitionApi.getProcessDefinitionList({ suspensionState: 1 })
} finally {
loading.value = false
}
}
/** 处理选择流程的按钮操作 **/
const handleSelect = async (row) => {
// 设置选择的流程
selectProcessInstance.value = row
// 情况一:流程表单
if (row.formType == 10) {
// 设置表单
formId.value = row.formId
// 加载流程图
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(row.id)
// 情况二:业务表单
} else if (row.formCustomCreatePath) {
await router.push({
path: row.formCustomCreatePath
})
// 这里暂时无需加载流程图,因为跳出到另外个 Tab
}
}
/** 提交按钮 */
const submitForm = async (type, done, formData, loading) => {
if (type == 'submit') {
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: selectProcessInstance.value.id,
variables: formData
})
// 提示
message.success('发起流程成功')
router.go(-1)
} finally {
done()
}
} else {
if (loading) loading()
else done()
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,57 @@
<template>
<el-card v-loading="loading" class="box-card">
<template #header>
<span class="el-icon-picture-outline">流程图</span>
</template>
<MyProcessViewer
key="designer"
:activityData="activityList"
:prefix="bpmnControlForm.prefix"
:processInstanceData="processInstance"
:taskData="tasks"
:value="bpmnXml"
v-bind="bpmnControlForm"
/>
</el-card>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as ActivityApi from '@/api/bpm/activity'
defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
const props = defineProps({
loading: propTypes.bool, // 是否加载中
id: propTypes.string, // 流程实例的编号
processInstance: propTypes.any, // 流程实例的信息
tasks: propTypes.array, // 流程任务的数组
bpmnXml: propTypes.string // BPMN XML
})
const bpmnControlForm = ref({
prefix: 'flowable'
})
const activityList = ref([]) // 任务列表
// const bpmnXML = computed(() => { // 不晓得为啊哈不能这么搞
// if (!props.processInstance || !props.processInstance.processDefinition) {
// return
// }
// return DefinitionApi.getProcessDefinitionBpmnXML(props.processInstance.processDefinition.id)
// })
/** 初始化 */
onMounted(async () => {
if (props.id) {
activityList.value = await ActivityApi.getActivityList({
processInstanceId: props.id
})
}
})
</script>
<style>
.box-card {
width: 100%;
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<el-drawer v-model="drawerVisible" title="子任务" size="70%">
<!-- 当前任务 -->
<template #header>
<h4>{{ baseTask.name }} 审批人{{ baseTask.assigneeUser?.nickname }}</h4>
<el-button
style="margin-left: 5px"
v-if="isSubSignButtonVisible(baseTask)"
type="danger"
plain
@click="handleSubSign(baseTask)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
<!-- 子任务列表 -->
<el-table :data="baseTask.children" style="width: 100%" row-key="id" border>
<el-table-column prop="assigneeUser.nickname" label="审批人" />
<el-table-column prop="assigneeUser.deptName" label="所在部门" />
<el-table-column label="审批状态" prop="result">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="scope.row.result" />
</template>
</el-table-column>
<el-table-column
label="提交时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="结束时间"
align="center"
prop="endTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" prop="operation">
<template #default="scope">
<el-button
v-if="isSubSignButtonVisible(scope.row)"
type="danger"
plain
@click="handleSubSign(scope.row)"
>
<Icon icon="ep:remove" /> 减签
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 减签 -->
<TaskSubSignDialogForm ref="taskSubSignDialogForm" />
</el-drawer>
</template>
<script lang="ts" setup>
import { isEmpty } from '@/utils/is'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import TaskSubSignDialogForm from './TaskSubSignDialogForm.vue'
defineOptions({ name: 'ProcessInstanceChildrenTaskList' })
const message = useMessage() // 消息弹窗
const drawerVisible = ref(false) // 抽屉的是否展示
const baseTask = ref<object>({})
/** 打开弹窗 */
const open = async (task: any) => {
if (isEmpty(task.children)) {
message.warning('该任务没有子任务')
return
}
baseTask.value = task
// 展开抽屉
drawerVisible.value = true
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 发起减签 */
const taskSubSignDialogForm = ref()
const handleSubSign = (item) => {
taskSubSignDialogForm.value.open(item.id)
}
/** 是否显示减签按钮 */
const isSubSignButtonVisible = (task: any) => {
if (task && task.children && !isEmpty(task.children)) {
// 有子任务,且子任务有任意一个是 待处理 和 待前置任务完成 则显示减签按钮
const subTask = task.children.find((item) => item.result === 1 || item.result === 9)
return !isEmpty(subTask)
}
return false
}
</script>

View File

@@ -0,0 +1,128 @@
<template>
<el-card v-loading="loading" class="box-card">
<template #header>
<span class="el-icon-picture-outline">审批记录</span>
</template>
<el-col :span="24">
<div class="block">
<el-timeline>
<el-timeline-item
v-for="(item, index) in tasks"
:key="index"
:icon="getTimelineItemIcon(item)"
:type="getTimelineItemType(item)"
>
<p style="font-weight: 700">
任务{{ item.name }}
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT" :value="item.result" />
<el-button
style="margin-left: 5px"
v-if="!isEmpty(item.children)"
@click="openChildrenTask(item)"
>
<Icon icon="ep:memo" />
子任务
</el-button>
</p>
<el-card :body-style="{ padding: '10px' }">
<label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal">
审批人{{ item.assigneeUser.nickname }}
<el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
</label>
<label v-if="item.createTime" style="font-weight: normal">创建时间</label>
<label style="font-weight: normal; color: #8a909c">
{{ formatDate(item?.createTime) }}
</label>
<label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
审批时间
</label>
<label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
{{ formatDate(item?.endTime) }}
</label>
<label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
耗时
</label>
<label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
{{ formatPast2(item?.durationInMillis) }}
</label>
<p v-if="item.reason">
<el-tag :type="getTimelineItemType(item)">{{ item.reason }}</el-tag>
</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-col>
<!-- 子任务 -->
<ProcessInstanceChildrenTaskList ref="processInstanceChildrenTaskList" />
</el-card>
</template>
<script lang="ts" setup>
import { formatDate, formatPast2 } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import ProcessInstanceChildrenTaskList from './ProcessInstanceChildrenTaskList.vue'
defineOptions({ name: 'BpmProcessInstanceTaskList' })
defineProps({
loading: propTypes.bool, // 是否加载中
tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
})
/** 获得任务对应的 icon */
const getTimelineItemIcon = (item) => {
if (item.result === 1) {
return 'el-icon-time'
}
if (item.result === 2) {
return 'el-icon-check'
}
if (item.result === 3) {
return 'el-icon-close'
}
if (item.result === 4) {
return 'el-icon-remove-outline'
}
if (item.result === 5) {
return 'el-icon-back'
}
return ''
}
/** 获得任务对应的颜色 */
const getTimelineItemType = (item) => {
if (item.result === 1) {
return 'primary'
}
if (item.result === 2) {
return 'success'
}
if (item.result === 3) {
return 'danger'
}
if (item.result === 4) {
return 'info'
}
if (item.result === 5) {
return 'warning'
}
if (item.result === 6) {
return 'default'
}
if (item.result === 7 || item.result === 8) {
return 'warning'
}
return ''
}
/**
* 子任务
*/
const processInstanceChildrenTaskList = ref()
const openChildrenTask = (item) => {
processInstanceChildrenTaskList.value.open(item)
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<Dialog v-model="dialogVisible" title="加签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="加签处理人" prop="userIdList">
<userSelect v-model="formData.userIdList" v-bind="userVBind" class="w-100%"></userSelect>
</el-form-item>
<el-form-item label="加签理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入加签理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm('before')">
向前加签
</el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm('after')">
向后加签
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
const message = useMessage() // 消息弹窗
defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
const userStore = useUserStoreWithOut()
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
userIdList: '',
type: ''
})
const formRules = ref({
userIdList: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
const userVBind = {
prop: 'delegateUserId',
type: 'edit',
column: {
label: '加签处理人',
findType: 'all',
multiple: true,
columnKey: ['sex', 'post', 'deptName'],
disabledIds: [userStore.user.id]
}
}
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async (type: string) => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
formData.value.type = type
try {
await TaskApi.taskAddSign(formData.value)
message.success('加签成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
userIdList: '',
type: ''
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,243 @@
<template>
<Dialog v-model="dialogVisible" title="修改任务规则" width="600">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="formData.taskName" disabled placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="任务标识" prop="taskId">
<el-input v-model="formData.taskId" disabled placeholder="请输入任务标识" />
</el-form-item>
<el-form-item label="流程名称" prop="processInstanceName">
<el-input v-model="formData.processInstanceName" disabled placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程标识" prop="processInstanceKey">
<el-input v-model="formData.processInstanceKey" disabled placeholder="请输入流程标识" />
</el-form-item>
<el-form-item label="规则类型" prop="type">
<el-select v-model="formData.type" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
<el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.type === 20 || formData.type === 21"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="formData.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中请稍后"
multiple
node-key="id"
show-checkbox
/>
</el-form-item>
<el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
<el-select v-model="formData.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="parseInt(item.id)"
:label="item.name"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
label="指定用户"
prop="userIds"
span="24"
>
<userSelect v-model="formData.userIds" v-bind="userVBind" class="w-100%"></userSelect>
</el-form-item>
<el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
<el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="parseInt(item.id)"
:label="item.name"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
<el-select v-model="formData.scripts" clearable multiple style="width: 100%">
<el-option
v-for="dict in taskAssignScriptDictDatas"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="抄送原因" prop="reason">
<el-input v-model="formData.reason" placeholder="请输入抄送原因" type="textarea" />
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps, handleTree } from '@/utils/tree'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref({
type: Number(undefined),
taskName: '',
taskId: '',
processInstanceName: '',
processInstanceKey: '',
startUserId: '',
options: [] as any[],
roleIds: [],
deptIds: [],
postIds: [],
userIds: '',
userGroupIds: [],
scripts: [],
reason: ''
})
const formRules = reactive({
type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }],
reason: [{ required: true, message: '抄送原因不能为空', trigger: 'change' }]
})
const userVBind = {
prop: 'assigneeUserId',
type: 'edit',
column: {
label: '用户',
findType: 'all',
multiple: true,
columnKey: ['sex', 'post', 'deptName']
}
}
const formRef = ref() // 表单 Ref
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref() // 部门树
const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
/** 打开弹窗 */
const open = async (row) => {
// 1. 先重置表单
resetForm()
// 2. 再设置表单
if (row != null) {
formData.value.type = undefined as unknown as number
formData.value.taskName = row.name
formData.value.taskId = row.id
formData.value.processInstanceName = row.processInstance.name
formData.value.processInstanceKey = row.processInstance.id
formData.value.startUserId = row.processInstance.startUserId
}
// 打开弹窗
dialogVisible.value = true
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList()
// 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value, 'id')
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
// 获得用户组列表
userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 构建表单
const form = {
...formData.value
}
// 将 roleIds 等选项赋值到 options 中
if (form.type === 10) {
form.options = form.roleIds
} else if (form.type === 20 || form.type === 21) {
form.options = form.deptIds
} else if (form.type === 22) {
form.options = form.postIds
} else if (form.type === 30 || form.type === 31 || form.type === 32) {
form.options = form.userIds.split(',')
} else if (form.type === 40) {
form.options = form.userGroupIds
} else if (form.type === 50) {
form.options = form.scripts
}
form.roleIds = undefined
form.deptIds = undefined
form.postIds = undefined
form.userIds = ''
form.userGroupIds = undefined
form.scripts = undefined
// 提交请求
formLoading.value = true
try {
const data = form as unknown as ProcessInstanceApi.ProcessInstanceCCVO
await ProcessInstanceApi.createProcessInstanceCC(data)
message.success(t('common.createSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,96 @@
<template>
<Dialog v-model="dialogVisible" title="委派任务" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="接收人" prop="delegateUserId">
<userSelect
v-model="formData.delegateUserId"
v-bind="userVBind"
class="w-100%"
></userSelect>
</el-form-item>
<el-form-item label="委派理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入委派理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmTaskDelegateForm' })
const userStore = useUserStoreWithOut()
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
delegateUserId: undefined
})
const formRules = ref({
delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }]
})
const userVBind = {
prop: 'delegateUserId',
type: 'edit',
column: {
label: '接收人',
findType: 'all',
multiple: false,
columnKey: ['sex', 'post', 'deptName'],
disabledIds: [userStore.user.id]
}
}
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.delegateTask(formData.value)
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
delegateUserId: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,90 @@
<template>
<Dialog v-model="dialogVisible" title="回退" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="退回节点" prop="targetDefinitionKey">
<el-select v-model="formData.targetDefinitionKey" clearable style="width: 100%">
<el-option
v-for="item in returnList"
:key="item.definitionKey"
:label="item.name"
:value="item.definitionKey"
/>
</el-select>
</el-form-item>
<el-form-item label="回退理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入回退理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="TaskRollbackDialogForm" setup>
import * as TaskApi from '@/api/bpm/task'
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
targetDefinitionKey: undefined,
reason: ''
})
const formRules = ref({
targetDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const returnList = ref([])
/** 打开弹窗 */
const open = async (id: string) => {
returnList.value = await TaskApi.getReturnList({ taskId: id })
if (returnList.value.length === 0) {
message.warning('当前没有可回退的节点')
return false
}
dialogVisible.value = true
resetForm()
formData.value.id = id
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.returnTask(formData.value)
message.success('回退成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
targetDefinitionKey: undefined,
reason: ''
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<Dialog v-model="dialogVisible" title="减签" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="减签任务" prop="id">
<el-radio-group v-model="formData.id">
<el-radio-button v-for="item in subTaskList" :key="item.id" :label="item.id">
{{ item.name }}({{ item.assigneeUser.deptName }}{{ item.assigneeUser.nickname }}--审批)
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="减签理由" prop="reason">
<el-input v-model="formData.reason" clearable placeholder="请输入减签理由" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="TaskRollbackDialogForm" setup>
import * as TaskApi from '@/api/bpm/task'
import { isEmpty } from '@/utils/is'
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
reason: ''
})
const formRules = ref({
id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }],
reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const subTaskList = ref([])
/** 打开弹窗 */
const open = async (id: string) => {
subTaskList.value = await TaskApi.getChildrenTaskList(id)
if (isEmpty(subTaskList.value)) {
message.warning('当前没有可减签的任务')
return false
}
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.taskSubSign(formData.value)
message.success('减签成功')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
reason: ''
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<Dialog v-model="dialogVisible" title="转派审批人" width="500">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="新审批人" prop="assigneeUserId">
<userSelect
v-model="formData.assigneeUserId"
v-bind="userVBind"
class="w-100%"
></userSelect>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as TaskApi from '@/api/bpm/task'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'BpmTaskUpdateAssigneeForm' })
const userStore = useUserStoreWithOut()
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
id: '',
assigneeUserId: undefined
})
const formRules = ref({
assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const userList = ref<any[]>([]) // 用户列表
const userVBind = {
prop: 'assigneeUserId',
type: 'edit',
column: {
label: '新审批人',
findType: 'all',
multiple: false,
columnKey: ['sex', 'post', 'deptName'],
disabledIds: [userStore.user.id]
}
}
/** 打开弹窗 */
const open = async (id: string) => {
dialogVisible.value = true
resetForm()
formData.value.id = id
// 获得用户列表
userList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await TaskApi.updateTaskAssignee(formData.value)
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: '',
assigneeUserId: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,327 @@
<template>
<ContentWrap>
<!-- 审批信息 -->
<el-card
v-for="(item, index) in runningTasks"
:key="index"
v-loading="processInstanceLoading"
class="box-card"
>
<template #header>
<span class="el-icon-picture-outline">审批任务{{ item.name }}</span>
</template>
<el-col :span="24">
<el-form
:ref="'form' + index"
:model="auditForms[index]"
:rules="auditRule"
label-width="100px"
>
<el-form-item v-if="processInstance && processInstance.name" label="流程名">
{{ processInstance.name }}
</el-form-item>
<el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
{{ processInstance.startUser.nickname }}
<el-tag size="small" type="info">{{ processInstance.startUser.deptName }}</el-tag>
</el-form-item>
<el-form-item label="审批建议" prop="reason">
<el-input
v-model="auditForms[index].reason"
placeholder="请输入审批建议"
type="textarea"
/>
</el-form-item>
</el-form>
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)">
<Icon icon="ep:select" />
通过
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<Icon icon="ep:close" />
不通过
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<Icon icon="ep:edit" />
转办
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<Icon icon="ep:position" />
委派
</el-button>
<el-button type="primary" @click="handleSign(item)">
<Icon icon="ep:plus" />
加签
</el-button>
<el-button type="warning" @click="handleBack(item)">
<Icon icon="ep:back" />
回退
</el-button>
</div>
</el-col>
</el-card>
<div id="Printer">
<!-- 申请信息 -->
<el-card v-loading="processInstanceLoading" class="box-card"
>
<template #header>
<span class="el-icon-document">申请信息{{ processInstance.name }}</span>
<el-button type="success" @click="printPage()">
<Icon icon="ep:printer" />
打印表单
</el-button>
</template>
<!-- 情况一流程表单 -->
<el-col v-if="processInstance?.processDefinition?.formType === 10" :span="24">
<FormView form-type="view" show-type="view" v-bind="detailForm"></FormView>
</el-col>
<!-- 情况二业务表单 -->
<div v-if="processInstance?.processDefinition?.formType === 20">
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</el-card>
<!-- 审批记录 -->
<ProcessInstanceTaskList :loading="tasksLoad" :tasks="tasks" />
</div>
<!-- 高亮流程图 -->
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXML"
:loading="processInstanceLoading"
:process-instance="processInstance"
:tasks="tasks"
/>
<!-- 弹窗转派审批人 -->
<TaskUpdateAssigneeForm ref="taskUpdateAssigneeFormRef" @success="getDetail" />
<!-- 弹窗回退节点 -->
<TaskReturnDialog ref="taskReturnDialogRef" @success="getDetail" />
<!-- 委派将任务委派给别人处理处理完成后会重新回到原审批人手中-->
<TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
<!-- 加签当前任务审批人为A向前加签选了一个C则需要C先审批然后再是A审批向后加签BA审批完需要B再审批完才算完成这个任务节点 -->
<TaskAddSignDialogForm ref="taskAddSignDialogForm" @success="getDetail" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task'
import TaskUpdateAssigneeForm from './TaskUpdateAssigneeForm.vue'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import TaskReturnDialog from './TaskReturnDialogForm.vue'
import TaskDelegateForm from './TaskDelegateForm.vue'
import TaskAddSignDialogForm from './TaskAddSignDialogForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is'
import router from '@/router/index'
import { $Print } from '@smallwei/avue'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const { query } = useRoute() // 查询参数
const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any
const userId = useUserStore().getUser.id // 当前登录的编号
const id = query.id as unknown as number // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const bpmnXML = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
// ========== 审批信息 ==========
const runningTasks = ref<any[]>([]) // 运行中的任务
const auditForms = ref<any[]>([]) // 审批任务的表单
const auditRule = reactive({
reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
})
// ========== 申请信息 ==========
const detailForm = ref({
formId: '',
optionsData: {},
defaultData: {}
})
/** 处理审批通过和不通过的操作 */
const handleAudit = async (task, pass) => {
// 1.1 获得对应表单
const index = runningTasks.value.indexOf(task)
const auditFormRef = proxy.$refs['form' + index][0]
// 1.2 校验表单
const elForm = unref(auditFormRef)
if (!elForm) return
const valid = await elForm.validate()
if (!valid) return
// 2.1 提交审批
const data = {
id: task.id,
reason: auditForms.value[index].reason
}
if (pass) {
await TaskApi.approveTask(data)
message.success('审批通过成功')
} else {
await TaskApi.rejectTask(data)
message.success('审批不通过成功')
}
// 2.2 加载最新数据
getDetail()
}
const printPage = async () => {
const { href } = router.resolve({ name: 'BpmProcessInstanceInfo',
query: { id: String(id), isPrint: '1' }
})
window.open(href, '_blank', 'noopener,noreferrer')
}
/** 转派审批人 */
const taskUpdateAssigneeFormRef = ref()
const openTaskUpdateAssigneeForm = (id: string) => {
taskUpdateAssigneeFormRef.value.open(id)
}
const taskDelegateForm = ref()
/** 处理审批退回的操作 */
const handleDelegate = async (task) => {
taskDelegateForm.value.open(task.id)
}
//回退弹框组件
const taskReturnDialogRef = ref()
/** 处理审批退回的操作 */
const handleBack = async (task) => {
taskReturnDialogRef.value.open(task.id)
}
const taskAddSignDialogForm = ref()
/** 处理审批加签的操作 */
const handleSign = async (task) => {
taskAddSignDialogForm.value.open(task.id)
}
/** 获得详情 */
const getDetail = () => {
// 1. 获得流程实例相关
getProcessInstance()
// 2. 获得流程任务列表(审批记录)
getTaskList()
}
/** 加载流程实例 */
const BusinessFormComponent = ref(null) // 异步组件
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(id)
if (!data) {
message.error('查询不到流程信息!')
return
}
processInstance.value = data
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
detailForm.value.formId = processDefinition.formId
detailForm.value.optionsData = JSON.parse(processDefinition.formConf)
detailForm.value.defaultData = data.formVariables
// setConfAndFields2(
// detailForm,
// processDefinition.formConf,
// processDefinition.formFields,
// data.formVariables
// )
// nextTick().then(() => {
// fApi.value?.fapi?.btn.show(false)
// fApi.value?.fapi?.resetBtn.show(false)
// fApi.value?.fapi?.disabled(true)
// })
} else {
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
}
// 加载流程图
bpmnXML.value = await DefinitionApi.getProcessDefinitionBpmnXML(processDefinition.id as number)
} finally {
processInstanceLoading.value = false
}
}
/** 加载任务列表 */
const getTaskList = async () => {
try {
// 获得未取消的任务
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
tasks.value = []
// 1.1 移除已取消的审批
data.forEach((task) => {
if (task.result !== 4) {
tasks.value.push(task)
}
})
// 1.2 排序,将未完成的排在前面,已完成的排在后面;
tasks.value.sort((a, b) => {
// 有已完成的情况,按照完成时间倒序
if (a.endTime && b.endTime) {
return b.endTime - a.endTime
} else if (a.endTime) {
return 1
} else if (b.endTime) {
return -1
// 都是未完成,按照创建时间倒序
} else {
return b.createTime - a.createTime
}
})
// 获得需要自己审批的任务
runningTasks.value = []
auditForms.value = []
loadRunningTask(tasks.value)
} finally {
tasksLoad.value = false
}
}
/**
* 设置 runningTasks 中的任务
*/
const loadRunningTask = (tasks) => {
tasks.forEach((task) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
}
// 2.1 只有待处理才需要
if (task.result !== 1 && task.result !== 6) {
return
}
// 2.2 自己不是处理人
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2.3 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
reason: ''
})
})
}
/** 初始化 */
onMounted(async() => {
await getDetail()
await nextTick()
if (query.isPrint === '1') {
await new Promise(resolve => setTimeout(resolve, 2000))
$Print('#Printer')
}
})
</script>

View File

@@ -0,0 +1,254 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #category="{ row }">
<dict-tag :type="DICT_TYPE.BPM_MODEL_CATEGORY" :value="row.category || ''" />
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
:value="scope.row.status"
/>
</template>
<template #tasks="scope">
<el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
<span>{{ task.name }}</span>
</el-button>
</template>
<template #result="scope">
<dict-tag
v-if="scope.row.result !== undefined"
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT"
:value="scope.row.result"
/>
</template>
<template #menu-left>
<el-button type="primary" v-hasPermi="['bpm:process-instance:query']" @click="handleCreate">
<Icon icon="ep:plus" class="mr-5px" /> 发起流程
</el-button>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
link
type="primary"
v-hasPermi="['bpm:process-instance:cancel']"
@click="handleDetail(row)"
>
详情
</el-button>
<!-- <el-button
link
type="danger"
v-if="row.result === 1"
v-hasPermi="['bpm:process-instance:query']"
@click="handleCancel(row)"
>
取消
</el-button> -->
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
defineOptions({ name: 'BpmCCProcessInstance' })
const router = useRouter() // 路由
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 170,
column: {
id: {
label: '流程编号',
width: 320
},
name: {
label: '流程名称',
search: true
},
category: {
label: '流程分类',
search: true,
type: 'select',
span: 12,
dicData: getIntDictOptions(DICT_TYPE.BPM_MODEL_CATEGORY),
rules: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }]
},
tasks: {
label: '当前审批任务'
},
status: {
label: '状态',
search: true,
type: 'select',
span: 12,
dicData: getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
},
result: {
label: '结果',
search: true,
type: 'select',
span: 12,
dicData: getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT),
rules: [{ required: true, message: '结果不能为空', trigger: 'blur' }]
},
createTime: {
label: '提交时间',
searchRange: true,
search: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
endTime: {
label: '结束时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
})
//表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['bpm:process-instance'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ProcessInstanceApi.getMyProcessInstancePage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 取消按钮操作 */
const handleCancel = async (row) => {
// 二次确认
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
inputErrorMessage: '取消原因不能为空'
})
// 发起取消
await ProcessInstanceApi.cancelProcessInstance(row.id, value)
message.success('取消成功')
// 刷新列表
await getTableData()
}
/** 发起流程操作 **/
const handleCreate = () => {
router.push({
name: 'BpmProcessInstanceCreate'
})
}
/** 查看详情 */
const handleDetail = (row) => {
router.push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.id
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,178 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu="{ row }">
<el-button link type="primary" @click="handleAudit(row)">流程</el-button>
</template>
<template #result="scope">
<dict-tag
v-if="scope.row.result"
:type="DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT"
:value="scope.row.result"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TaskApi from '@/api/bpm/task'
defineOptions({ name: 'BpmDoneTask' })
const { push } = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: 'none',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: {
id: {
label: '任务编号',
width: 300
},
name: {
label: '任务名称',
search: true
},
processInstanceName: {
label: '所属流程',
bind: 'processInstance.name'
},
processInstanceStartUserNickname: {
label: '流程发起人',
bind: 'processInstance.startUserNickname'
},
result: {
label: '状态',
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
},
reason: {
label: '原因'
},
searchCreateTime: {
label: '创建时间',
search: true,
display: false,
hide: true,
type: 'daterange',
searchRange: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
createTime: {
label: '创建时间',
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.searchCreateTime?.length) {
searchObj.createTime = getSearchDate(searchObj.searchCreateTime)
} else delete searchObj.createTime
delete searchObj.searchCreateTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await TaskApi.getDoneTaskPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
const beforeOpen = (done, type) => {
done()
}
/** 处理审批按钮 */
const handleAudit = (row) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,183 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:data="tableData"
:option="tableOption"
:permission="permission"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #suspensionState="scope">
<el-tag v-if="scope.row.suspensionState === 1" type="success">激活</el-tag>
<el-tag v-if="scope.row.suspensionState === 2" type="warning">挂起</el-tag>
</template>
<template #processInstanceName="scope">
{{ scope.row.processInstance.name }}
</template>
<template #processInstanceStartUserNickname="scope">
{{ scope.row.processInstance.startUserNickname }}
</template>
<template #menu="{ row }">
<el-button link type="primary" @click="handleAudit(row)">审批</el-button>
<!-- <el-button link type="primary" @click="handleCC(row)">抄送</el-button>-->
</template>
</avue-crud>
<TaskCCDialogForm ref="taskCCDialogForm" />
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as TaskApi from '@/api/bpm/task'
import TaskCCDialogForm from '../../processInstance/detail/TaskCCDialogForm.vue'
const { getCurrPermi } = useCrudPermi()
defineOptions({ name: 'BpmDoneTask' })
// const message = useMessage() // 消息弹窗
// const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: {
id: {
label: '任务编号',
width: 300,
display: false
},
name: {
label: '任务名称',
search: true
},
processInstanceName: {
label: '所属流程'
},
processInstanceStartUserNickname: {
label: '流程发起人'
},
suspensionState: {
label: '任务状态',
type: 'select',
span: 12,
dicData: []
},
createTime: {
label: '创建时间',
searchRange: true,
search: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const popupObj = ref({ detail: false })
const currRow = ref<any>({})
const permission = getCurrPermi(['bpm:task'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await TaskApi.getTodoTaskPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 处理审批按钮 */
const handleAudit = (row) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
}
})
}
const taskCCDialogForm = ref()
/** 处理抄送按钮 */
const handleCC = (row) => {
taskCCDialogForm.value.open(row)
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,250 @@
<template>
<Dialog v-model="dialogVisible" title="修改任务规则" width="600">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item label="任务名称" prop="taskDefinitionName">
<el-input v-model="formData.taskDefinitionName" disabled placeholder="请输入流标标识" />
</el-form-item>
<el-form-item label="任务标识" prop="taskDefinitionKey">
<el-input v-model="formData.taskDefinitionKey" disabled placeholder="请输入任务标识" />
</el-form-item>
<el-form-item label="规则类型" prop="type">
<el-select v-model="formData.type" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.type === 10" label="指定角色" prop="roleIds">
<el-select v-model="formData.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.type === 20 || formData.type === 21"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="formData.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中请稍后"
multiple
node-key="id"
show-checkbox
/>
</el-form-item>
<el-form-item v-if="formData.type === 22" label="指定岗位" prop="postIds" span="24">
<el-select v-model="formData.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="parseInt(item.id)"
:label="item.name"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="formData.type === 30 || formData.type === 31 || formData.type === 32"
label="指定用户"
prop="userIds"
span="24"
>
<el-select v-model="formData.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="parseInt(item.id)"
:label="item.nickname"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.type === 40" label="指定用户组" prop="userGroupIds">
<el-select v-model="formData.userGroupIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="parseInt(item.id)"
:label="item.name"
:value="parseInt(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.type === 50" label="指定脚本" prop="scripts">
<el-select v-model="formData.scripts" clearable multiple style="width: 100%">
<el-option
v-for="dict in taskAssignScriptDictDatas"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps, handleTree } from '@/utils/tree'
import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({ name: 'BpmTaskAssignRuleForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref({
type: Number(undefined),
modelId: '',
options: [],
roleIds: [],
deptIds: [],
postIds: [],
userIds: [],
userGroupIds: [],
scripts: []
})
const formRules = reactive({
type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '指定角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '指定部门不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '指定岗位不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '指定用户不能为空', trigger: 'change' }],
userGroupIds: [{ required: true, message: '指定用户组不能为空', trigger: 'change' }],
scripts: [{ required: true, message: '指定脚本不能为空', trigger: 'change' }]
})
const formRef = ref() // 表单 Ref
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref() // 部门树
const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
const taskAssignScriptDictDatas = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT)
/** 打开弹窗 */
const open = async (modelId: string, row: TaskAssignRuleApi.TaskAssignVO) => {
// 1. 先重置表单
resetForm()
// 2. 再设置表单
formData.value = {
...row,
modelId: modelId,
options: [],
roleIds: [],
deptIds: [],
postIds: [],
userIds: [],
userGroupIds: [],
scripts: []
}
// 将 options 赋值到对应的 roleIds 等选项
if (row.type === 10) {
formData.value.roleIds.push(...row.options)
} else if (row.type === 20 || row.type === 21) {
formData.value.deptIds.push(...row.options)
} else if (row.type === 22) {
formData.value.postIds.push(...row.options)
} else if (row.type === 30 || row.type === 31 || row.type === 32) {
formData.value.userIds.push(...row.options)
} else if (row.type === 40) {
formData.value.userGroupIds.push(...row.options)
} else if (row.type === 50) {
formData.value.scripts.push(...row.options)
}
// 打开弹窗
dialogVisible.value = true
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList()
// 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value, 'id')
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
// 获得用户组列表
userGroupOptions.value = await UserGroupApi.getSimpleUserGroupList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 构建表单
const form = {
...formData.value,
taskDefinitionName: undefined
}
// 将 roleIds 等选项赋值到 options 中
if (form.type === 10) {
form.options = form.roleIds
} else if (form.type === 20 || form.type === 21) {
form.options = form.deptIds
} else if (form.type === 22) {
form.options = form.postIds
} else if (form.type === 30 || form.type === 31 || form.type === 32) {
form.options = form.userIds
} else if (form.type === 40) {
form.options = form.userGroupIds
} else if (form.type === 50) {
form.options = form.scripts
}
form.roleIds = undefined
form.deptIds = undefined
form.postIds = undefined
form.userIds = undefined
form.userGroupIds = undefined
form.scripts = undefined
// 提交请求
formLoading.value = true
try {
const data = form as unknown as TaskAssignRuleApi.TaskAssignVO
if (!data.id) {
await TaskAssignRuleApi.createTaskAssignRule(data)
message.success(t('common.createSuccess'))
} else {
await TaskAssignRuleApi.updateTaskAssignRule(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,278 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
:data="tableData"
:permission="permission"
:option="tableOption"
:table-loading="loading"
@row-update="rowUpdate"
@refresh-change="getTableData"
>
<template #deptIds-form="{ column, disabled }">
<DeptSelect
v-model="tableForm.deptIds"
:column="column"
:disabled="disabled"
type="edit"
prop="deptIds"
></DeptSelect>
</template>
<template #userIds-form="{ column, disabled }">
<UserSelect
v-model="tableForm.userIds"
:column="column"
:disabled="disabled"
type="edit"
prop="userIds"
></UserSelect>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TaskAssignRuleApi from '@/api/bpm/taskAssignRule'
import * as RoleApi from '@/api/system/role'
import * as PostApi from '@/api/system/post'
import * as UserGroupApi from '@/api/bpm/userGroup'
import { setUserAndDeptName } from '@/components/LowDesign/src/utils/getName'
import { useLowStoreWithOut } from '@/store/modules/low'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const lowStore = useLowStoreWithOut()
defineOptions({ name: 'BpmTaskAssignRule' })
const { query } = useRoute() // 查询参数
const loading = ref(true)
const getRules = (label) => {
return [
{
required: true,
message: '请选择' + label,
trigger: 'change'
}
]
}
const getUserKey = (val) => {
let key = ''
if (val == 10) key = 'roleIds'
else if ([20, 21].includes(val)) key = 'deptIds'
else if (val == 22) key = 'postIds'
else if ([30, 31, 32].includes(val)) key = 'userIds'
else if (val == 40) key = 'userGroupIds'
else if (val == 50) key = 'scripts'
return key
}
const tableOption = reactive({
border: true,
align: 'center',
headerAlign: 'center',
labelSuffix: ' ',
span: 24,
labelWidth: 120,
dialogWidth: 500,
addBtn: false,
delBtn: false,
calcHeight: 20,
column: {
taskDefinitionName: { label: '任务名', disabled: true },
taskDefinitionKey: { label: '任务标识', disabled: true },
type: {
label: '规则类型',
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_RULE_TYPE),
rules: getRules('规则类型'),
control: (val) => {
const columnObj = {
roleIds: { display: false },
deptIds: { display: false },
postIds: { display: false },
userIds: { display: false },
userGroupIds: { display: false },
scripts: { display: false }
}
const key = getUserKey(val)
if (key) columnObj[key].display = true
return columnObj
}
},
options: {
label: '规则范围',
display: false,
formatter: (row) => {
if (row.ruleModel == 'startEvent') return '-'
if (!row.type) return '未指定'
const key = getUserKey(row.type)
let arr = row.options.map((id) => {
if (key == 'deptIds') return lowStore.dicObj.deptSelect?.[id] || id
else if (key == 'userIds') return lowStore.dicObj.userSelect?.[id] || id
else return dicObj.value[key][id]
})
return `${row.$type}${arr.join('、')}`
}
},
roleIds: {
label: '指定角色',
type: 'select',
dicData: [],
hide: true,
display: false,
rules: getRules('指定角色')
},
deptIds: {
label: '指定部门',
hide: true,
display: false,
findType: 'all',
multiple: true,
checkStrictly: true,
rules: getRules('指定部门')
},
postIds: {
label: '指定岗位',
type: 'select',
dicData: [],
hide: true,
display: false,
rules: getRules('指定岗位')
},
userIds: {
label: '指定用户',
hide: true,
display: false,
findType: 'all',
multiple: true,
columnKey: ['sex', 'post', 'deptName'],
rules: getRules('指定用户')
},
userGroupIds: {
label: '指定用户组',
type: 'select',
dicData: [],
hide: true,
display: false,
rules: getRules('指定用户组')
},
scripts: {
label: '指定脚本',
type: 'select',
dicData: [],
hide: true,
display: false,
rules: getRules('指定脚本')
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref<any[]>([])
const dicObj = ref<any>({})
const permission = getCurrPermi(['bpm:task-assign-rule'])
const crudRef = ref()
/** 查询列表 */
const getTableData = async () => {
loading.value = true
try {
const data = await TaskAssignRuleApi.getTaskAssignRuleList({
modelId: query.modelId,
processDefinitionId: query.processDefinitionId
})
tableData.value = await formattingTableData(data)
} finally {
loading.value = false
}
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
const key = getUserKey(form.type)
const delKey = ['roleIds', 'deptIds', 'postIds', 'userIds', 'userGroupIds', 'scripts']
if (key) {
if (typeof form[key] == 'number') form[key] = form[key] + ''
const value = form[key]
form.options = typeof value == 'string' ? value.split(',') : value
}
delKey.forEach((prop) => delete form[prop])
if (!form.modelId) form.modelId = query.modelId
const apiName = form.id ? 'updateTaskAssignRule' : 'createTaskAssignRule'
let bool = await TaskAssignRuleApi[apiName](form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
const getDicData = () => {
return new Promise(async (resolve) => {
const keyList = ['roleIds', 'postIds', 'userGroupIds']
const promiseArr = [
RoleApi.getSimpleRoleList(),
PostApi.getSimplePostList(),
UserGroupApi.getSimpleUserGroupList()
]
const resData = await Promise.all(promiseArr)
resData.forEach((data, index) => {
const key = keyList[index]
dicObj.value[key] = {}
tableOption.column[key].dicData = data.map((item) => {
const id = item.id + ''
const name = item.name
dicObj.value[key][id] = name
return { label: name, value: id }
})
})
dicObj.value.scripts = {}
tableOption.column.scripts.dicData = getIntDictOptions(DICT_TYPE.BPM_TASK_ASSIGN_SCRIPT).map(
(item) => {
dicObj.value.scripts[item.value] = item.label
return { value: item.value + '', label: item.label }
}
) as never[]
dicObj.value.type = {}
tableOption.column.type.dicData.forEach((item) => {
dicObj.value.type[item.value] = item.label
})
resolve(true)
})
}
const formattingTableData = (data): Promise<any[]> => {
return new Promise(async (resolve) => {
const deptIdList: any[] = []
const userIdList: any[] = []
data = data.map((item) => {
const key = getUserKey(item.type)
item.$type = dicObj.value['type'][item.type]
if (key) {
if (!item.options) item.options = []
item[key] = item.options.join(',')
if (['deptIds', 'userIds'].includes(key)) {
if (key == 'deptIds') deptIdList.push(...item.options)
else userIdList.push(...item.options)
}
}
return item
})
await setUserAndDeptName({ userIdList, deptIdList })
resolve(data)
})
}
/** 初始化 **/
onMounted(async () => {
loading.value = true
await getDicData()
await getTableData()
})
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,90 @@
<template>
<el-tabs v-model="activeName" type="border-card" class="demo-tabs" @tab-click="handleClick">
<template v-for="item in tabsPaneList" :key="item.name">
<el-tab-pane :label="item.label" :name="item.name">
<LowTable
:ref="(el) => (tableRef[item.name] = el)"
:tableId="item.formId"
:calcHeight="item.calcHeight || undefined"
:enhanceData="item.enhanceData"
:fixed-search="item.fixedSearch || {}"
></LowTable>
</el-tab-pane>
</template>
</el-tabs>
</template>
<script setup lang="ts">
// 引入依赖低代码的table代码这样就可以动态引入表格
import {LowTable} from "@/components/LowDesign";
// 定义该组件的选项
defineOptions({name: 'TabsCardDocument'})
const activeName = ref('institution')
const tableRef = ref({})
const tabsPaneList = ref([
{
label: '制度',
name: 'institution',
formId: '1966386366515343361',
calcHeight: 200,
enhanceData: {hideHeader: 'disabled'},
fixedSearch: {file_main_type: 0}
},
{
label: '预案',
name: 'plan',
formId: '1966386366515343361',
calcHeight: 200,
fixedSearch: {file_main_type: 1}
},
{
label: '标准',
name: 'standard',
formId: '1966386366515343361',
calcHeight: 200,
fixedSearch: {file_main_type: 2}
},
{
label: '规程',
name: 'regulations',
formId: '1966386366515343361',
calcHeight: 200,
fixedSearch: {file_main_type: 3}
},
{
label: '目标责任',
name: 'responsibility',
formId: '1966386366515343361',
calcHeight: 200,
fixedSearch: {file_main_type: 4}
},
])
// 定义点击tab的事件动作
const handleClick = (tab) => {
const key = tab.props.name
tableRef.value[key].initTableLayout()
}
</script>
<style lang="scss" scoped>
.demo-tabs > .el-tabs__content {
padding: 32px;
font-size: 32px;
font-weight: 600;
color: #6b778c;
}
.demo-tabs {
::v-deep(.el-tabs__nav-wrap) {
.el-tabs__item {
height: 50px;
font-size: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,262 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:config:export']"
>导出</el-button
>
</template>
<template #accountCount="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
</template>
<template #visible="scope">
<dict-tag
v-if="scope.row.visible !== undefined"
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.visible"
/>
</template>
<template #type="scope">
<dict-tag
v-if="scope.row.type !== undefined"
:type="DICT_TYPE.INFRA_CONFIG_TYPE"
:value="scope.row.type"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
dialogWidth: '50%',
menuWidth: 180,
column: {
category: {
label: '参数分类',
width: 90,
rules: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }]
},
name: {
label: '参数名称',
search: true,
minWidth: 120,
rules: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }]
},
key: {
label: '参数键名',
search: true,
minWidth: 120,
rules: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }]
},
value: {
label: '参数键值',
rules: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }]
},
visible: {
label: '是否可见',
type: 'radio',
width: 85,
dicData: getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING),
rules: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
value: true
},
type: {
label: '系统内置',
search: true,
width: 95,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE),
display: false
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
span: 24,
maxRows: 4,
overHidden: true
},
createTime: {
label: '创建时间',
search: true,
searchRange: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:config'])
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await ConfigApi.getConfigPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ConfigApi.getConfig(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await ConfigApi.createConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ConfigApi.updateConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ConfigApi.deleteConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj = { ...tableSearch.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await ConfigApi.exportConfig(searchObj)
download.excel(data, '配置管理列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,268 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 自定义操作栏 -->
<template #menu="{ row, index }">
<el-button
link
type="primary"
@click="crudRef.rowEdit(row, index)"
v-hasPermi="['infra:data-source-config:update']"
v-if="row.id !== 0"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="crudRef.rowDel(row, index)"
v-hasPermi="['infra:data-source-config:delete']"
v-if="row.id !== 0"
>
删除
</el-button>
</template>
<template #dbCode="{ row }">
<span> {{ row.dbCode }} </span>
<el-tag
size="small"
:type="row.isConnect == 'Y' ? 'primary' : 'danger'"
effect="dark"
class="pos-absolute right-2px top-2px"
>
{{ row.isConnect == 'Y' ? '已连接' : '已断开' }}
</el-tag>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
defineOptions({ name: 'InfraDataSourceConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const DBList = [
{
label: 'MySQL 5.7+',
value: 'MySQL',
driverClass: 'com.mysql.cj.jdbc.Driver',
url: 'jdbc:mysql://127.0.0.1:3306/jeelowcode?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai'
},
{
label: 'Oracle',
value: 'Oracle',
driverClass: 'oracle.jdbc.OracleDriver',
url: 'jdbc:oracle:thin:@127.0.0.1:1521:ORCL'
},
{
label: 'postgresql',
value: 'postgresql',
driverClass: 'org.postgresql.Driver',
url: 'jdbc:postgresql://127.0.0.1:5432/jeelowcode'
},
{
label: '达梦',
value: 'DM',
driverClass: 'dm.jdbc.driver.DmDriver',
url: 'jdbc:dm://127.0.0.1:5236/?jeelowcode&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8'
}
]
const tableOption = reactive({
editBtn: false,
delBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
dbCode: {
label: '数据源编码',
minWidth: 120,
rules: [{ required: true, message: '数据源编码不能为空', trigger: 'blur' }],
editDisabled: true
},
name: {
label: '数据源名称',
minWidth: 120,
rules: [{ required: true, message: '数据源名称不能为空', trigger: 'blur' }]
},
dbType: {
label: '数据库类型',
type: 'select',
dicData: DBList,
width: 120,
rules: [{ required: true, message: '数据库类型不能为空', trigger: 'change' }],
change: ({ item }) => {
if (item) {
tableForm.value.driverClass = item.driverClass
tableForm.value.url = item.url
}
}
},
driverClass: {
label: '驱动类',
hide: true,
rules: [{ required: true, message: '驱动类不能为空', trigger: 'blur' }]
},
url: {
label: '数据源连接',
type: 'textarea',
minRows: 1,
maxRows: 3,
overHidden: true,
minWidth: 120,
rules: [{ required: true, message: '数据源连接不能为空', trigger: 'blur' }]
},
username: {
label: '用户名',
width: 120,
rules: [{ required: true, message: '用户名不能为空', trigger: 'blur' }]
},
password: {
label: '密码',
rules: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
hide: true
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:data-source-config'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await DataSourceConfigApi.getDataSourceConfigList()
tableData.value = data
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await DataSourceConfigApi.getDataSourceConfig(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await DataSourceConfigApi.createDataSourceConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await DataSourceConfigApi.updateDataSourceConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DataSourceConfigApi.deleteDataSourceConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
<template>
<DesignPopup v-model="dialogVisible" title="上传文件" width="40%" :is-footer="true">
<div class="p-20px">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="uploadUrl"
:auto-upload="false"
:data="data"
:disabled="formLoading"
:limit="1"
:on-change="handleFileChange"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
:http-request="httpRequest"
accept=".jpg, .png, .gif"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text"> 将文件拖到此处 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示仅允许导入 jpgpnggif 格式文件
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitFileForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'InfraFileForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const fileList = ref([]) // 文件列表
const data = ref({ path: '' })
const uploadRef = ref()
const { uploadUrl, httpRequest } = useUpload()
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 处理上传的文件发生变化 */
const handleFileChange = (file) => {
data.value.path = file.name
}
/** 提交表单 */
const submitFileForm = () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
unref(uploadRef)?.submit()
}
/** 文件上传成功处理 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = () => {
// 清理
dialogVisible.value = false
formLoading.value = false
unref(uploadRef)?.clearFiles()
// 提示成功,并刷新
setTimeout(() => {
message.success(t('common.createSuccess'))
emit('success')
}, 1000)
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>

View File

@@ -0,0 +1,210 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
@search-change="searchChange"
@search-reset="resetChange"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left>
<el-button type="primary" plain @click="openForm">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
</el-button>
</template>
<template #urlValue="scope">
{{ scope.row.url }}
</template>
<template #url="{ row }">
<el-image
v-if="row.type.includes('image')"
class="h-80px w-80px"
lazy
:src="row.url"
:preview-src-list="[row.url]"
preview-teleported
fit="cover"
/>
<el-link
v-else-if="row.type.includes('pdf')"
type="primary"
:href="row.url"
:underline="false"
target="_blank"
>预览</el-link
>
<el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank"
>下载</el-link
>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<FileForm ref="formRef" @success="getTableData" />
</template>
<script lang="ts" setup>
import { fileSizeFormatter } from '@/utils'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as FileApi from '@/api/infra/file'
import FileForm from './FileForm.vue'
defineOptions({ name: 'InfraFile' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
name: {
label: '文件名',
minWidth: 120,
overHidden: true
},
path: {
label: '文件路径',
minWidth: 120,
search: true,
overHidden: true
},
urlValue: {
label: 'URL',
minWidth: 100,
overHidden: true
},
size: {
label: '文件大小',
minWidth: 100,
formatter: fileSizeFormatter,
width: '120px'
},
type: {
label: '文件类型',
minWidth: 120,
search: true,
overHidden: true
},
url: {
label: '文件内容',
width: 120
},
createTime: {
label: '上传时间',
searchRange: true,
search: true,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 160,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:file'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await FileApi.getFilePage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FileApi.deleteFile(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = () => {
formRef.value.open()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,452 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 表格 -->
<template #storage="scope">
<dict-tag
:type="DICT_TYPE.INFRA_FILE_STORAGE"
:value="scope.row.storage ? scope.row.storage : ''"
/>
</template>
<template #master="scope">
<dict-tag
v-if="scope.row.master !== undefined"
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.master"
/>
</template>
<!-- 表单 -->
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
link
type="primary"
class="is-text"
icon="el-icon-operation"
:disabled="row.master"
@click="handleMaster(row.id)"
v-hasPermi="['infra:file-config:update']"
>
主配置
</el-button>
<el-button
link
class="is-text"
icon="el-icon-tickets"
type="primary"
@click="handleTest(row.id)"
>
测试
</el-button>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as FileConfigApi from '@/api/infra/fileConfig'
defineOptions({ name: 'InfraFileConfig' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
menuWidth: 300,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 100,
span: 24,
dialogWidth: '50%',
column: {
id: {
label: '编号',
width: 80,
display: false
},
name: {
label: '配置名',
search: true,
minWidth: 90,
rules: [{ required: true, message: '配置名不能为空', trigger: 'blur' }]
},
remark: {
label: '备注'
},
storage: {
label: '存储器',
type: 'select',
disabled: false,
search: true,
span: 8,
minWidth: 90,
dicData: getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE),
rules: [{ required: true, message: '存储器不能为空', trigger: 'blur' }],
change: ({ value, column }) => {
let {
basePath,
host,
port,
username,
password,
mode,
endpoint,
bucket,
accessKey,
accessSecret,
domain
} = tableOption.column
if (value) {
domain.display = true
if (value === 20) {
domain.rules = []
} else {
domain.rules = [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
}
} else {
domain.display = false
}
if (value >= 10 && value <= 12) {
basePath.display = true
} else {
basePath.display = false
}
if (value >= 11 && value <= 12) {
host.display = true
port.display = true
username.display = true
password.display = true
} else {
host.display = false
port.display = false
username.display = false
password.display = false
}
if (value === 11) {
mode.display = true
} else {
mode.display = false
}
if (value === 20) {
endpoint.display = true
bucket.display = true
accessKey.display = true
accessSecret.display = true
} else {
endpoint.display = false
bucket.display = false
accessKey.display = false
accessSecret.display = false
}
}
},
master: {
label: '主配置',
type: 'select',
display: false,
span: 8,
minWidth: 90,
dicData: getIntDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)
},
basePath: {
label: '基础路径',
hide: true,
display: false,
rules: [{ required: true, message: '基础路径不能为空', trigger: 'blur' }]
},
host: {
label: '主机地址',
hide: true,
display: false,
rules: [{ required: true, message: '主机地址不能为空', trigger: 'blur' }]
},
port: {
label: '主机端口',
type: 'number',
span: 8,
hide: true,
display: false,
rules: [{ required: true, message: '主机端口不能为空', trigger: 'blur' }]
},
username: {
label: '用户名',
hide: true,
display: false,
rules: [{ required: true, message: '用户名不能为空', trigger: 'blur' }]
},
password: {
label: '密码',
hide: true,
display: false,
rules: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
},
mode: {
label: '连接模式',
type: 'radio',
hide: true,
display: false,
dicData: [
{ label: '主动模式', value: 'Active' },
{ label: '被动模式', value: 'Passive' }
],
rules: [{ required: true, message: '连接模式不能为空', trigger: 'blur' }]
},
endpoint: {
label: '节点地址',
hide: true,
display: false,
rules: [{ required: true, message: '节点地址不能为空', trigger: 'blur' }]
},
bucket: {
label: '存储 bucket',
hide: true,
display: false,
rules: [{ required: true, message: '存储 bucket不能为空', trigger: 'blur' }]
},
accessKey: {
label: 'accessKey',
hide: true,
display: false,
rules: [{ required: true, message: 'accessKey不能为空', trigger: 'blur' }]
},
accessSecret: {
label: 'accessSecret',
hide: true,
display: false,
rules: [{ required: true, message: 'accessSecret不能为空', trigger: 'blur' }]
},
domain: {
label: '自定义域名',
hide: true,
display: false,
rules: [{ required: true, message: '自定义域名不能为空', trigger: 'blur' }]
},
createTime: {
label: '创建时间',
searchRange: true,
search: true,
display: false,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
let tableChildForm: string[] = reactive([]) // 保存参数,方便提交
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['infra:file-config'])
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.createTime?.length) {
searchObj.createTime = getSearchDate(searchObj.createTime)
} else delete searchObj.createTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await FileConfigApi.getFileConfigPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableOption.column.storage.disabled = true
let data = await FileConfigApi.getFileConfig(tableForm.value.id)
tableChildForm = Object.keys(data.config)
// 数据处理
data = {
storage: data.storage,
remark: data.remark,
name: data.name,
master: data.master,
id: data.id,
createTime: data.createTime,
...data.config
}
tableForm.value = data
} else {
tableOption.column.storage.disabled = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
// 删除多余表单
Object.keys(tableOption.column).forEach((item) => {
if (tableOption.column[item].display != undefined && !tableOption.column[item].display)
delete form[item]
})
// 添加config参数
let config = {}
Object.keys(form).forEach((item) => {
if (item !== 'storage' && item !== 'remark' && item !== 'name') {
config[item] = form[item]
delete form[item]
}
})
form.config = config
// 发送请求
let bool = await FileConfigApi.createFileConfig(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
// 添加config参数
let config = {}
tableChildForm.forEach((item) => {
if (form[item]) {
config[item] = form[item]
}
})
Object.keys(form).forEach((item) => {
if (
item !== 'storage' &&
item !== 'remark' &&
item !== 'name' &&
item !== 'master' &&
item !== 'id' &&
item !== 'createTime'
)
delete form[item] //清除多余参数(已经放在config中)
})
form.config = config
let bool = await FileConfigApi.updateFileConfig(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FileConfigApi.deleteFileConfig(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 主配置按钮操作 */
const handleMaster = async (id) => {
try {
await message.confirm('是否确认修改配置编号为"' + id + '"的数据项为主配置?')
await FileConfigApi.updateFileConfigMaster(id)
message.success(t('common.updateSuccess'))
await getTableData()
} catch {}
}
/** 测试按钮操作 */
const handleTest = async (id) => {
loading.value = true
try {
const response = await FileConfigApi.testFileConfig(id)
message.alert(
`<div>测试通过,上传文件成功!访问地址:</div>
<div style="word-break: break-word;">${response}</div>
`,
'',
{
dangerouslyUseHTMLString: true
}
)
} finally {
loading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,398 @@
<template>
<ContentWrap>
<avue-crud
v-model="tableForm"
ref="crudRef"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['infra:job:export']"
>导出</el-button
>
<el-button type="info" plain @click="handleJobLog()" v-hasPermi="['infra:job:query']">
<Icon icon="ep:zoom-in" class="mr-5px" /> 执行日志
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
link
class="is-text"
:icon="
scope.row.status === InfraJobStatusEnum.STOP
? 'el-icon-video-play'
: 'el-icon-video-pause'
"
@click="handleChangeStatus(scope.row)"
v-hasPermi="['infra:job:update']"
>
{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}
</el-button>
<el-dropdown
@command="(command) => handleCommand(command, scope.row)"
v-hasPermi="['infra:job:trigger', 'infra:job:query']"
>
<div class="pt-3px pr-4px pb-3px pl-4px cursor-pointer">
<el-text type="primary">
<span>更多</span>
<Icon :size="16" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="handleRun" v-if="checkPermi(['infra:job:trigger'])">
执行一次
</el-dropdown-item>
<el-dropdown-item
@click="crudRef.rowView(scope.row, scope.index)"
v-if="checkPermi(['infra:job:query'])"
>
任务详细
</el-dropdown-item>
<el-dropdown-item command="handleJobLog" v-if="checkPermi(['infra:job:query'])">
调度日志
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #accountCount="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.INFRA_JOB_STATUS"
:value="scope.row.status"
/>
</template>
<template #cronExpression-form="{ type }">
<div v-if="type == 'view'">{{ tableForm.cronExpression }}</div>
<Crontab v-else v-model="tableForm.cronExpression" />
</template>
<template #stayus-form="{ value }">
<dict-tag v-if="value" :type="DICT_TYPE.INFRA_JOB_STATUS" :value="value" />
</template>
<template #executionTime-form>
<div class="pt-10px">
<el-timeline>
<el-timeline-item
v-for="(nextTime, index) in nextTimes"
:key="index"
:timestamp="formatDate(nextTime)"
>
{{ index + 1 }}
</el-timeline-item>
</el-timeline>
</div>
</template>
</avue-crud>
</ContentWrap>
<DesignPopup v-model="logPopup.show" :title="logPopup.title" width="80%" controlType="drawer">
<div class="p-20px">
<InfraJobLog ref="logRef" :jobId="logPopup.jobId"></InfraJobLog>
</div>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { checkPermi } from '@/utils/permission'
import { formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as JobApi from '@/api/infra/job'
import { InfraJobStatusEnum } from '@/utils/constants'
import InfraJobLog from './logger/index.vue'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const nextTimes = ref([]) // 下一轮执行时间的数组
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 120,
menuWidth: 300,
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
name: {
label: '任务名称',
minWidth: 110,
search: true,
rules: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }]
},
status: {
label: '任务状态',
width: 90,
search: true,
type: 'select',
display: false,
dicData: getIntDictOptions(DICT_TYPE.INFRA_JOB_STATUS)
},
handlerName: {
label: '处理器的名字',
minWidth: 100,
search: true,
editDisabled: true,
rules: [{ required: true, message: '处理器的名字不能为空', trigger: 'blur' }]
},
handlerParam: {
label: '处理器的参数',
minWidth: 110
},
cronExpression: {
label: 'CRON 表达式',
minWidth: 110,
rules: [{ required: true, message: 'CRON 表达式不能为空', trigger: 'blur' }]
},
retryCount: {
label: '重试次数',
hide: true,
rules: [{ required: true, message: '重试次数不能为空', trigger: 'blur' }],
placeholder: '请输入重试次数。设置为 0 时,不进行重试'
},
retryInterval: {
label: '重试间隔',
hide: true,
rules: [{ required: true, message: '重试间隔不能为空', trigger: 'blur' }],
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔'
},
monitorTimeout: {
label: '监控超时时间',
placeholder: '请输入监控超时时间,单位:毫秒',
hide: true
},
executionTime: {
label: '后续执行时间',
display: false,
hide: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const logPopup = ref({ show: false, title: '', jobId: 0 })
const permission = getCurrPermi(['infra:job'])
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
const logRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await JobApi.getJobPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
tableForm.value = await JobApi.getJob(tableForm.value.id)
if (type == 'view') nextTimes.value = await JobApi.getJobNextTimes(tableForm.value.id!)
loading.value = false
}
if (type === 'view') {
tableOption.column.status.display = true
tableOption.column.executionTime.display = true
} else {
tableOption.column.status.display = false
tableOption.column.executionTime.display = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await JobApi.createJob(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await JobApi.updateJob(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await JobApi.deleteJob(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 修改状态操作 */
const handleChangeStatus = async (row: JobApi.JobVO) => {
try {
// 修改状态的二次确认
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
await message.confirm(
'确认要' + text + '定时任务编号为"' + row.id + '"的数据项?',
t('common.reminder')
)
const status =
row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
await JobApi.updateJobStatus(row.id, status)
message.success(text + '成功')
// 刷新列表
await getTableData()
} catch {
// 取消后,进行恢复按钮
row.status =
row.status === InfraJobStatusEnum.NORMAL ? InfraJobStatusEnum.STOP : InfraJobStatusEnum.NORMAL
}
}
/** '更多'操作按钮 */
const handleCommand = (command, row) => {
switch (command) {
case 'handleRun':
handleRun(row)
break
case 'handleJobLog':
handleJobLog(row)
break
default:
break
}
}
/** 执行一次 */
const handleRun = async (row: JobApi.JobVO) => {
try {
// 二次确认
await message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder'))
// 提交执行
await JobApi.runJob(row.id)
message.success('执行成功')
// 刷新列表
await getTableData()
} catch {}
}
/** 跳转执行日志 */
const handleJobLog = (row?) => {
logPopup.value = {
show: true,
title: row ? `${row.name} 调度日志` : '所有的调度日志',
jobId: row?.id || 0
}
setTimeout(() => {
if (logRef.value) logRef.value.resetChange()
}, 30)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj = { ...tableSearch.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await JobApi.exportJob(searchObj)
download.excel(data, '定时任务列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss" scoped>
.el-dropdown {
padding: 4px 2px;
}
</style>

View File

@@ -0,0 +1,230 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:tenant:export']"
>导出</el-button
>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
:value="scope.row.status"
/>
</template>
<template #beginTimeText-form>
{{ formatDate(tableForm.beginTime) + ' ~ ' + formatDate(tableForm.endTime) }}
</template>
<template #duration-form="{ value }">
{{ value + ' 毫秒' }}
</template>
<template #status-form="{ value }">
<dict-tag
v-if="value !== undefined"
:type="DICT_TYPE.INFRA_JOB_LOG_STATUS"
:value="value"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as JobLogApi from '@/api/infra/jobLog'
defineOptions({ name: 'InfraJobLog' })
interface Props {
jobId?: number
}
const props = defineProps<Props>()
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 100,
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
jobId: {
label: '任务编号',
width: 90
},
handlerName: {
label: '处理器的名字',
search: true,
searchSpan: 5
},
beginTime: {
label: '执行时间',
hide: true,
display: false,
search: true,
type: 'date',
searchRange: true,
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始执行时间',
endPlaceholder: '结束执行时间',
searchSpan: 8,
},
handlerParam: {
label: '处理器的参数'
},
executeIndex: {
label: '第几次执行',
width: 100
},
beginTimeText: {
label: '执行时间',
html: true,
width: 160,
formatter: (row) => {
return `<div>
<div>${formatDate(row.beginTime)}</div>
<div>${formatDate(row.endTime)}</div>
</div>`
}
},
duration: {
label: '执行时长',
width: 100,
formatter: (row) => {
return row.duration + '毫秒'
}
},
status: {
label: '任务状态',
searchSpan: 5,
search: true,
width: 90,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)
},
result: {
label: '执行结果',
hide: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const crudRef = ref()
useCrudHeight(crudRef)
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj: any = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize
}
if (searchObj.beginTime?.length) {
const dateArr = getSearchDate(searchObj.beginTime)
searchObj.beginTime = dateArr[0]
searchObj.endTime = dateArr[1]
} else delete searchObj.beginTime
if (props.jobId !== 0) searchObj['jobId'] = props.jobId
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await JobLogApi.getJobLogPage(searchObj)
tableData.value = data.list
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
tablePage.value.currentPage = 1
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
const sizeChange = (pageSize) => {
tablePage.value.pageSize = pageSize
resetChange()
}
const currentChange = (currentPage) => {
tablePage.value.currentPage = currentPage
getTableData()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj: any = { ...tableSearch.value }
if (props.jobId) searchObj.jobId = props.jobId
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await JobLogApi.exportJobLog(searchObj)
download.excel(data, '调度日志列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
defineExpose({ resetChange })
</script>

View File

@@ -0,0 +1,265 @@
<template>
<el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
<el-row>
<!-- 基本信息 -->
<el-col :span="24" class="card-box" shadow="hover">
<el-card>
<el-descriptions title="基本信息" :column="6" border>
<el-descriptions-item label="Redis版本 :">
{{ cache?.info?.redis_version }}
</el-descriptions-item>
<el-descriptions-item label="运行模式 :">
{{ cache?.info?.redis_mode == 'standalone' ? '单机' : '集群' }}
</el-descriptions-item>
<el-descriptions-item label="端口 :">
{{ cache?.info?.tcp_port }}
</el-descriptions-item>
<el-descriptions-item label="客户端数 :">
{{ cache?.info?.connected_clients }}
</el-descriptions-item>
<el-descriptions-item label="运行时间(天) :">
{{ cache?.info?.uptime_in_days }}
</el-descriptions-item>
<el-descriptions-item label="使用内存 :">
{{ cache?.info?.used_memory_human }}
</el-descriptions-item>
<el-descriptions-item label="使用CPU :">
{{ cache?.info ? parseFloat(cache?.info?.used_cpu_user_children).toFixed(2) : '' }}
</el-descriptions-item>
<el-descriptions-item label="内存配置 :">
{{ cache?.info?.maxmemory_human }}
</el-descriptions-item>
<el-descriptions-item label="AOF是否开启 :">
{{ cache?.info?.aof_enabled == '0' ? '否' : '是' }}
</el-descriptions-item>
<el-descriptions-item label="RDB是否成功 :">
{{ cache?.info?.rdb_last_bgsave_status }}
</el-descriptions-item>
<el-descriptions-item label="Key数量 :">
{{ cache?.dbSize }}
</el-descriptions-item>
<el-descriptions-item label="网络入口/出口 :">
{{ cache?.info?.instantaneous_input_kbps }}kps/
{{ cache?.info?.instantaneous_output_kbps }}kps
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<!-- 命令统计 -->
<el-col :span="12" class="mt-3">
<el-card :gutter="12" shadow="hover">
<Echart :options="commandStatsRefChika" :height="420" />
</el-card>
</el-col>
<!-- 内存使用量统计 -->
<el-col :span="12" class="mt-3">
<el-card class="ml-3" :gutter="12" shadow="hover">
<Echart :options="usedmemoryEchartChika" :height="420" />
</el-card>
</el-col>
</el-row>
</el-scrollbar>
</template>
<script lang="ts" setup>
import * as RedisApi from '@/api/infra/redis'
import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
const cache = ref<RedisMonitorInfoVO>()
// 基本信息
const readRedisInfo = async () => {
const data = await RedisApi.getCache()
cache.value = data
}
// 内存使用情况
const usedmemoryEchartChika = reactive<any>({
title: {
// 仪表盘标题。
text: '内存使用情况',
left: 'center',
show: true, // 是否显示标题,默认 true。
offsetCenter: [0, '20%'], //相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
color: 'yellow', // 文字的颜色,默认 #333。
fontSize: 20 // 文字的字体大小,默认 15。
},
toolbox: {
show: false,
feature: {
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: '峰值',
type: 'gauge',
min: 0,
max: 50,
splitNumber: 10,
//这是指针的颜色
color: '#F5C74E',
radius: '85%',
center: ['50%', '50%'],
startAngle: 225,
endAngle: -45,
axisLine: {
// 坐标轴线
lineStyle: {
// 属性lineStyle控制线条样式
color: [
[0.2, '#7FFF00'],
[0.8, '#00FFFF'],
[1, '#FF0000']
],
//width: 6 外框的大小(环的宽度)
width: 10
}
},
axisTick: {
// 坐标轴小标记
//里面的线长是5短线
length: 5, // 属性length控制线长
lineStyle: {
// 属性lineStyle控制线条样式
color: '#76D9D7'
}
},
splitLine: {
// 分隔线
length: 20, // 属性length控制线长
lineStyle: {
// 属性lineStyle详见lineStyle控制线条样式
color: '#76D9D7'
}
},
axisLabel: {
color: '#76D9D7',
distance: 15,
fontSize: 15
},
pointer: {
// 指针的大小
width: 7,
show: true
},
detail: {
textStyle: {
fontWeight: 'normal',
// 里面文字下的数值大小50
fontSize: 15,
color: '#FFFFFF'
},
valueAnimation: true
},
progress: {
show: true
}
}
]
})
// 指令使用情况
const commandStatsRefChika = reactive({
title: {
text: '命令统计',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 30,
top: 10,
bottom: 20,
data: [] as any[],
textStyle: {
color: '#a1a1a1'
}
},
series: [
{
name: '命令',
type: 'pie',
radius: [20, 120],
center: ['40%', '60%'],
data: [] as any[],
roseType: 'radius',
label: {
show: true
},
emphasis: {
label: {
show: true
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
/** 加载数据 */
const getSummary = () => {
// 初始化命令图表
initCommandStatsChart()
usedMemoryInstance()
}
/** 命令使用情况 */
const initCommandStatsChart = async () => {
usedmemoryEchartChika.series[0].data = []
// 发起请求
try {
const data = await RedisApi.getCache()
cache.value = data
// 处理数据
const commandStats = [] as any[]
const nameList = [] as string[]
data.commandStats.forEach((row) => {
commandStats.push({
name: row.command,
value: row.calls
})
nameList.push(row.command)
})
commandStatsRefChika.legend.data = nameList
commandStatsRefChika.series[0].data = commandStats
} catch {}
}
const usedMemoryInstance = async () => {
try {
const data = await RedisApi.getCache()
cache.value = data
// 仪表盘详情,用于显示数据。
usedmemoryEchartChika.series[0].detail = {
show: true, // 是否显示详情,默认 true。
offsetCenter: [0, '50%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。
color: 'auto', // 文字的颜色,默认 auto。
fontSize: 30, // 文字的字体大小,默认 15。
formatter: cache.value!.info.used_memory_human // 格式化函数或者字符串
}
usedmemoryEchartChika.series[0].data[0] = {
value: cache.value!.info.used_memory_human,
name: '内存消耗'
}
usedmemoryEchartChika.tooltip = {
formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
}
} catch {}
}
/** 初始化 **/
onMounted(() => {
// 读取 redis 信息
readRedisInfo()
// 加载数据
getSummary()
})
</script>

View File

@@ -0,0 +1,27 @@
<template>
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="src" />
</ContentWrap>
</template>
<script lang="ts" setup>
import * as ConfigApi from '@/api/infra/config'
defineOptions({ name: 'InfraAdminServer' })
const loading = ref(true) // 是否加载中
const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications')
/** 初始化 */
onMounted(async () => {
try {
// 友情提示:如果访问出现 404 问题:
const data = await ConfigApi.getConfigKey('url.spring-boot-admin')
if (data && data.length > 0) {
src.value = data
}
} finally {
loading.value = false
}
})
</script>

View File

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

View File

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

View File

@@ -0,0 +1,184 @@
<template>
<div class="flex">
<!-- 左侧建立连接发送消息 -->
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>连接</span>
</div>
</template>
<div class="flex items-center">
<span class="mr-4 text-lg font-medium"> 连接状态: </span>
<el-tag :color="getTagColor">{{ status }}</el-tag>
</div>
<hr class="my-4" />
<div class="flex">
<el-input v-model="server" disabled>
<template #prepend>服务地址</template>
</el-input>
<el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggleConnectStatus">
{{ getIsOpen ? '关闭连接' : '开启连接' }}
</el-button>
</div>
<p class="mt-4 text-lg font-medium">消息输入框</p>
<hr class="my-4" />
<el-input
v-model="sendText"
:autosize="{ minRows: 2, maxRows: 4 }"
:disabled="!getIsOpen"
clearable
placeholder="请输入你要发送的消息"
type="textarea"
/>
<el-select v-model="sendUserId" class="mt-4" placeholder="请选择发送人">
<el-option key="" label="所有人" value="" />
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
<el-button :disabled="!getIsOpen" block class="ml-2 mt-4" type="primary" @click="handlerSend">
发送
</el-button>
</el-card>
<!-- 右侧消息记录 -->
<el-card :gutter="12" class="w-1/2" shadow="always">
<template #header>
<div class="card-header">
<span>消息记录</span>
</div>
</template>
<div class="max-h-80 overflow-auto">
<ul>
<li v-for="msg in messageReverseList" :key="msg.time" class="mt-2">
<div class="flex items-center">
<span class="text-primary mr-2 font-medium">收到消息:</span>
<span>{{ formatDate(new Date(msg.time)) }}</span>
</div>
<div>
{{ msg.text }}
</div>
</li>
</ul>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import { useWebSocket } from '@vueuse/core'
import { getRefreshToken } from '@/utils/auth'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'InfraWebSocket' })
const message = useMessage() // 消息弹窗
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色
/** 发起 WebSocket 连接 */
const { status, data, send, close, open } = useWebSocket(server.value, {
autoReconnect: true,
heartbeat: true
})
/** 监听接收到的数据 */
const messageList = ref([] as { time: number; text: string }[]) // 消息列表
const messageReverseList = computed(() => messageList.value.slice().reverse())
watchEffect(() => {
if (!data.value) {
return
}
try {
// 1. 收到心跳
if (data.value === 'pong') {
// state.recordList.push({
// text: '【心跳】',
// time: new Date().getTime()
// })
return
}
// 2.1 解析 type 消息类型
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
const content = JSON.parse(jsonMessage.content)
if (!type) {
message.error('未知的消息类型:' + data.value)
return
}
// 2.2 消息类型demo-message-receive
if (type === 'demo-message-receive') {
const single = content.single
if (single) {
messageList.value.push({
text: `【单发】用户编号(${content.fromUserId})${content.text}`,
time: new Date().getTime()
})
} else {
messageList.value.push({
text: `【群发】用户编号(${content.fromUserId})${content.text}`,
time: new Date().getTime()
})
}
return
}
// 2.3 消息类型notice-push
if (type === 'notice-push') {
messageList.value.push({
text: `【系统通知】:${content.title}`,
time: new Date().getTime()
})
return
}
message.error('未处理消息:' + data.value)
} catch (error) {
message.error('处理消息发生异常:' + data.value)
console.error(error)
}
})
/** 发送消息 */
const sendText = ref('') // 发送内容
const sendUserId = ref('') // 发送人
const handlerSend = () => {
// 1.1 先 JSON 化 message 消息内容
const messageContent = JSON.stringify({
text: sendText.value,
toUserId: sendUserId.value
})
// 1.2 再 JSON 化整个消息
const jsonMessage = JSON.stringify({
type: 'demo-message-send',
content: messageContent
})
// 2. 最后发送消息
send(jsonMessage)
sendText.value = ''
}
/** 切换 websocket 连接状态 */
const toggleConnectStatus = () => {
if (getIsOpen.value) {
close()
} else {
open()
}
}
/** 初始化 **/
const userList = ref<any[]>([]) // 用户列表
onMounted(async () => {
// 获取用户列表
userList.value = await UserApi.getSimpleUserList()
})
</script>

View File

@@ -0,0 +1,274 @@
<template>
<div class="w-100%">
<div class="xc_box">
<FormView
:ref="(el) => (formViewRef[formTopData.refKey] = el)"
formType="add"
handleType="returnData"
showType="view"
:defaultData="formTopData.defaultData"
:showButton="false"
:enhanceData="{isShowFun}"
:formId="formTopData.formId"
></FormView>
<div class="Tips">
<div style="display: flex;align-items: center;" v-if="is_show">
<span class="span"></span>
<span>图片列表</span>
</div>
<div v-else>
<el-checkbox v-model="checked" label="全选" size="large" style="margin-right:10px;font-size: 16px;" @click="toggleAllSelection()"/>
<el-button @click="uploadFun(true)">移动图片</el-button>
<el-button @click="delFun()">删除图片</el-button>
<el-button @click="isShowFun()">退出操作</el-button>
</div>
<div>
<el-select v-model="value" placeholder="Select" style="width: 120px">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<LowTable :ref="(el)=>(ref1 = el)" :tableId="tableDate.tableId" :enhanceData="tableDate.enhanceData" > </LowTable>
</div>
<el-dialog v-model="upload" title="选择相册" width="728px" :close-on-click-modal="false">
<el-checkbox-group v-model="checkList">
<div class="checkbox-box">
<div class="list" v-for="(item,index) in listData" :key="index">
<img class="img" :src="item.url" alt=""/>
<div class="radio">
<el-checkbox label="" :value="index" />
<p>{{item.title}}</p>
</div>
</div>
</div>
</el-checkbox-group>
<div class="btn">
<el-button class="qx" @click="uploadFun(false)">取消</el-button>
<el-button class="bc" type="primary" @click="uploadFun(false)">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { messageConfig ,ElMessage, ElMessageBox} from 'element-plus';
const ref1 = ref()
const formViewRef = ref({})
const value = ref('1')
const upload = ref(false)
const radio = ref('selected and disabled')
const formTopData = ref({
type:'table',
refKey: 'xcgl',
formId: '1852223511034294274',
defaultData: {},
enhanceData: { hideHeader: 'disabled'}
})
const tableDate = ref({
label:'图片列表',
name:'fourth',
defaultData:{},
tableId:'1852241979997646849',
type:'table',
enhanceData: { hideHeader: 'disabled',}
})
const options = [
{
value: '1',
label: '文件名称',
},
{
value: '2',
label: '文件大小',
},
{
value: '3',
label: '更新时间',
},
]
const checked = ref()
const is_show = ref(true)
const isShowFun = () =>{
ref1.value.crudRef.tableOption.selection=is_show.value
is_show.value = !is_show.value
checked.value = !is_show.value ? '' : true
ref1.value.crudRef.refreshTable()
}
//全选
const toggleAllSelection = ()=> {
ref1.value.crudRef.toggleAllSelection()
}
//删除图片
const delFun = () =>{
ElMessageBox.confirm(
'是否确定删除数据?',
'操作确认',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
ElMessage({
type: 'success',
message: '数据删除成功',
})
}).catch(() => {})
}
//上传图片
const checkList = ref([])
const listData = ref([
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
{title:'相册名称',url:'../../../../../public/img/xc.png'},
])
const uploadFun = (type) =>{
upload.value = type
}
const xzFun = ()=>{
}
</script>
<style lang="scss" scoped>
.xc_box{
padding-bottom: 20px;
.Tips{
display: flex;
height: 40px;
padding-right:18px;
margin-bottom: 15px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 28px;
color: #666;
text-align: left;
align-items: center;
justify-content: space-between;
.span{
display: inline-block;
width: 8.41px;
height: 19px;
margin-right:10px;
background: #409eff;
}
}
}
.checkbox-box{
display: flex;
padding: 0 0 30px 30px;
flex-wrap: wrap;
.list{
width: 150px;
height: 180px;
margin: 20px 20px 0 0;
border: 1px solid #e4e4e4;
.img{
width: 150px;
height: 150px;
}
.radio{
display: flex;
height: 30px;
padding-left: 10px;
// margin-left:5px;
line-height: 30px;
// .el-checkbox{
.el-checkbox__input{
top: -5px;
left: 6px;
height: 30px;
margin: 0;
}
// }
p{
padding: 0;
margin: 0;
margin-left: 5px;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 30px;
color: #999;
text-align: left;
}
}
}
}
.btn{
display: flex;
justify-content: end;
padding: 20px 30px;
border-top:1px solid #e9e9e9;
.el-button{
width: 80px;
height: 30px;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
}
.qx{
color: #999;
background: #f9f9f9;
}
.qx:hover{
color: #409eff;
border-color: #409eff;
}
.qr{
color: #FFF;
}
}
::v-deep .el-select__placeholder{
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
color: #666;
text-align: left;
}
</style>

View File

@@ -0,0 +1,274 @@
<template>
<div class="grzx_box w-95% bg-white flex">
<el-menu
:default-active="mentDefaultActive"
class="pt-25px el-menu-vertical-demo"
@select="handleSelect"
>
<template v-for="item in menuList" :key="item.id">
<el-menu-item class="flex justify-center" :index="item.index">
<span> {{ item.menuItemTitle }}</span>
</el-menu-item>
</template>
</el-menu>
<div class="use-lowdesign">
<template v-if="richFormView.defaultData.richText">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="richFormView.defaultData"
:enhanceData="richFormView.enhanceData"
:formId="richFormView.formId"
></FormView>
</template>
<template v-else>
<template v-if="menuContent.type == 'exploit'">
<div class="content p-40px">
<div class="title text-18px pl-7px mb-20px fw600 c-#666666">
{{ menuContent.title }}
</div>
<FormView
formType="edit"
handleType="returnData"
showType="view"
:defaultData="menuContent.defaultData"
:formId="menuContent.formId"
></FormView>
</div>
</template>
<template v-if="menuContent.type == 'design'">
<div class="content p-40px">
<div>
<div class="title text-18px pl-7px mb-20px fw600 c-#666666">
{{ menuContent.title }}
</div>
<div class="xttz-util flex" v-if="menuContent.tableId == '1840642756645044226'">
<div v-for="item in xttzBtnList" :key="item.id">
<el-button
:class="xttzActiveClass == item.id ? 'is-active' : ''"
@click="xttzBtnClick(item.id)"
>{{ item.title }}</el-button
>
</div>
<div class="all-read">
<el-button>全部标记为已读</el-button>
</div>
</div>
</div>
<LowTable
ref="xttzTable"
:tableId="menuContent.tableId"
:enhanceData="menuContent.defaultData"
:fixed-search="xttzActiveClass != 1 ? { type: xttzActiveClass } : {}"
>
</LowTable>
</div>
</template>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { FormView, LowTable } from '@/components/LowDesign/index'
const menuList = ref([
{ id: 1, index: 'zlsz', menuItemTitle: '资料设置' },
{ id: 2, index: 'xgmm', menuItemTitle: '修改密码' },
{ id: 3, index: 'dljl', menuItemTitle: '登录记录' },
{ id: 4, index: 'xttz', menuItemTitle: '系统通知' },
{ id: 5, index: 'xxsz', menuItemTitle: '消息设置' }
])
const xttzBtnList = ref([
{ id: 1, title: '全部消息' },
{ id: 2, title: '服务消息' },
{ id: 3, title: '活动消息' },
{ id: 4, title: '产品消息' },
{ id: 5, title: '安全消息' },
{ id: 6, title: '故障消息' }
])
const xttzActiveClass = ref(1)
const mentDefaultActive = ref('zlsz')
const menuContentList = ref({
zlsz: {
title: '基本资料',
formId: '1840295435986018306',
type: 'exploit',
defaultData: {
dlzh: 'Windir',
updateImg: 'http://oss.yckxt.com/chatgpt/upload/1/2024-09-30/1/user.png',
updateBtn: 'http://oss.yckxt.com/chatgpt/upload/1/2024-09-30/1/user.png'
}
},
xgmm: {
title: '修改密码',
formId: '1840567733712330753',
type: 'exploit'
},
dljl: {
title: '登录记录',
tableId: '1840577516125323266',
type: 'design',
defaultData: {
type: 'view'
}
},
xttz: {
title: '系统通知',
tableId: '1840642756645044226',
type: 'design',
defaultData: {
type: 'view',
rowClick(row) {
if (row.rich_text) {
richFormView.value.defaultData = {
richText: row.rich_text,
type: row.$type,
title: row.title,
createTime: row.create_time
}
}
}
}
},
xxsz: {
title: '消息设置',
tableId: '1843476389638443010',
type: 'design',
defaultData: {
type: 'view'
}
}
})
const xttzTable = ref()
interface RichFormViewDefault {
createTime: string
title?: string
type?: string
richText?: String
}
const richFormView = ref({
formId: '1840671420115873794',
enhanceData: {
richDisplay: false
},
defaultData: {
type: '',
title: '',
createTime: '',
richText: ''
} as RichFormViewDefault
})
watch(
() => richFormView.value.enhanceData.richDisplay,
(val) => {
richFormView.value.defaultData.richText = ''
}
)
const menuContent = ref(menuContentList.value[mentDefaultActive.value])
const handleSelect = (key: string, keyPath: string[]) => {
if (menuContent.value !== menuContentList.value[key] && richFormView.value.defaultData.richText) {
richFormView.value.defaultData.richText = ''
}
menuContent.value = menuContentList.value[key]
}
const xttzBtnClick = (id: number) => {
xttzActiveClass.value = id
xttzTable.value.resetChange()
}
</script>
<style lang="scss" scoped>
.grzx_box {
min-height: 800px;
margin: 0 auto;
border-radius: 5px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
.el-menu {
width: 180px;
min-height: 800px;
flex-shrink: 0;
background-color: #fcfcfc;
border-radius: 5px 0 0 5px;
.el-menu-item {
height: 45px;
span {
display: flex;
height: 100%;
align-items: center;
font-size: 14px;
}
&.is-active {
font-weight: bold;
background-color: rgb(64 158 255 / 9.8%);
border-right: 3px solid rgb(64 158 255 / 100%);
}
}
}
.use-lowdesign {
width: calc(100% - 180px);
.content {
width: calc(100% - 80px);
height: calc(100% - 80px);
.title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
line-height: 16px;
border-left: 6px solid #409eff;
}
.xttz-util {
margin-bottom: 20px;
.el-button {
display: flex;
width: 80px;
height: 30px;
margin-right: 10px;
font-size: 12px;
color: #999;
&.is-active {
color: #409eff;
background-color: white;
border-color: #409eff;
}
}
.all-read {
margin-left: auto;
.el-button {
border: none;
&:hover {
background-color: rgb(255 255 255 / 0%);
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,292 @@
<template>
<!-- <div> 个人中心</div> -->
<div>
<div class="box">
<div class="box-left .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent ">
<div class="box-left-top">
<div class="head ">
<div style="margin: 20px 0;">
<el-avatar :size="60" :src="circleUrl" />
</div>
<p class="font color6 .dark:c-#fff!" style="margin-bottom:5px;font-size: 18px;">a123</p>
<p class="fontFamily color9 .dark:c-#ccc!" style="font-size: 14px;">专注交互原型设计</p>
</div>
<div class="title-box">
<el-row class="demo-avatar demo-basic">
<el-col :span="8">
<div class="content">
<p class="font color3 .dark:c-#fff!" style="font-size: 16px;">1000</p>
<p class="font-family color9 .dark:c-#ccc!" style="font-size: 12px;">文章</p>
</div>
</el-col>
<el-col :span="8">
<div class="content" style="border-right: 1px solid #f4f4f4;border-left: 1px solid #f4f4f4;">
<p class="font color3 .dark:c-#fff!" style="font-size: 16px;">1000</p>
<p class="font-family color9 .dark:c-#ccc!" style="font-size: 12px;">应用</p>
</div>
</el-col>
<el-col :span="8">
<div class="content">
<p class="font color3 .dark:c-#fff!" style="font-size: 16px;">1000</p>
<p class="font-family color9 .dark:c-#ccc!" style="font-size: 12px;">项目</p>
</div>
</el-col>
</el-row>
</div>
</div>
<div class="list">
<p class="font color6 .dark:c-#fff!" style="margin:20px 0 10px 20px;font-size: 14px;">个人名片</p>
<el-row>
<div class="list-box">
<el-col :span="2">
<Icon icon="fa6-solid:user" width="18" height="18" style="color: #999" />
</el-col>
<el-col :span="20">
<p class="fontFamily color6 .dark:c-#ccc!" style="font-size: 14px;">10</p>
</el-col>
</div>
</el-row>
<el-row>
<div class="list-box">
<el-col :span="2">
<Icon icon="fa6-brands:qq" width="18" height="18" style="color: #999" />
</el-col>
<el-col :span="20">
<p class="fontFamily color6 .dark:c-#ccc!" style="font-size: 14px;">20841030</p>
</el-col>
</div>
</el-row>
<el-row>
<div class="list-box">
<el-col :span="2">
<Icon icon="fa:weixin" width="18" height="18" style="color: #999" />
</el-col>
<el-col :span="20">
<p class="fontFamily color6 .dark:c-#ccc!" style="font-size: 14px;">a123</p>
</el-col>
</div>
</el-row>
<el-row>
<div class="list-box">
<el-col :span="2">
<Icon icon="fa-brands:weibo" width="18" height="18" style="color: #999" />
</el-col>
<el-col :span="20">
<p class="fontFamily color6 .dark:c-#ccc!" style="font-size: 14px;">淘宝创意部</p>
</el-col>
</div>
</el-row>
</div>
<div class="list" style="padding-bottom: 30px;">
<p class="font color6 .dark:c-#fff!" style="margin:20px 0 10px 20px;font-size: 14px;">所属团队</p>
<el-row v-for="(item,index) in sstdData" :key="index">
<div class="list-sstd">
<el-avatar :size="46" :src="item.src" />
<div class="list-sstd-text">
<p class="font color6 .dark:c-#fff!" style="font-size: 14px;">{{item.title}}</p>
<p class="fontFamily color9 .dark:c-#ccc!" style="font-size: 12px;">{{item.content}}</p>
</div>
</div>
</el-row>
</div>
<div class="list">
<p class="font color6 .dark:c-#fff!" style="margin:20px 0 10px 20px;font-size: 14px;">标签</p>
<div style="margin-left: 20px;">
<el-badge class="item" v-for="(item, index) in data" :key="index">
<el-button>{{ item.name }}</el-button>
</el-badge>
</div>
</div>
</div>
<div class="box-right .dark:b-#2A2B2C b-solid .dark:bg-#1D1E1F! b-1px b-transparent">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<template v-for="(item,index) in tableData" :key="index">
<el-tab-pane :label="item.label" :name="item.name">
<div class="tabs-box">
<!-- <FormView
formType="add"
handleType="returnData"
showType="view"
:defaultData="item.defaultData"
:showButton="true"
:formId="item.formId"
></FormView> -->
<upDate v-if="item.type == 'jurisdiction'"></upDate>
<template v-if="item.type == 'table'">
<LowTable :tableId="item.tableId" :enhanceData="item.enhanceData" > </LowTable>
</template>
</div>
</el-tab-pane>
</template>
</el-tabs>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs,ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const state = reactive({
circleUrl:
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
})
const data = ref([
{name:'很有想法'},
{name:'海纳百川'},
{name:'川妹子'},
{name:'专注设计'},
{name:'喜欢吃辣'},
{name:'好看'},
{name:'能力强'},
{name:'爱臭美'},
])
const sstdData = ref([
{src:'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',title:'淘宝创意部',content:'设计呢,最重要的是开心'},
{src:'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',title:'淘宝创意部',content:'设计呢,最重要的是开心'},
{src:'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',title:'淘宝创意部',content:'设计呢,最重要的是开心'},
])
const tableData = reactive([
{label:'应用',name:'first',defaultData:{},tableId:'1848631600935510017',type:'table',enhanceData: { hideHeader: 'disabled'}},
{label:'项目',name:'second',defaultData:{},tableId:'1848637143087489025',type:'table',enhanceData: { hideHeader: 'disabled'}},
{label:'文章',name:'fourth',defaultData:{},tableId:'1848649270435160066',type:'table',enhanceData: { hideHeader: 'disabled'}},
])
const activeName = ref('first')
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
const { circleUrl } = toRefs(state)
</script>
<style lang="scss" scoped>
*{
padding: 0;
margin: 0;
}
.box{
display: flex;
width: 100%;
.box-left{
width: 22.6%;
max-height: 950px;
// padding: 0 20px;
margin-right:21px;
background: #fff;
border-radius: 10px;
.box-left-top{
width: 100%;
// padding: 0 20px;
.head{
// margin: 20px 0;
text-align: center;
}
.title-box{
margin:30px 0;
.content{
text-align: center;
}
}
}
.list{
padding-bottom: 20px;
border-top: 1px solid #ececec;
.list-box{
display: flex;
width: 100%;
margin-left:20px;
line-height: 28px;
align-content: center;
}
.list-sstd{
display: flex;
margin-top: 20px;
margin-left:20px;
.list-sstd-text{
margin-left: 10px;
p{
line-height: 24px;
}
}
}
}
}
.box-right{
width: 77%;
padding: 0 20px;
background: #fff;
border-radius: 10px;
}
}
.item {
margin-top: 10px;
margin-right: 10px;
}
.el-dropdown {
margin-top: 1.1rem;
}
::v-deep .el-button{
padding: 8px 15px !important;
font-family: '微软雅黑', sans-serif !important;
font-size: 12px !important;
font-weight: 400 !important;
color: #999 !important;
}
::v-deep .el-tabs__item{
height: 60px !important;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important;
font-size: 16px !important;
font-weight: 700 !important;
line-height: 30px !important;
color: #666 !important;
}
.font{
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-weight: 700;
}
.fontFamily{
font-family: '微软雅黑', sans-serif;
font-weight: 400;
}
.font-family{
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-weight: 400;
}
.color3{
color: #333;
}
.color6{
color: #666;
}
.color9{
color: #999;
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<!-- <div> 发布表单页(横向分步) </div> -->
<ContentWrap>
<el-steps :active="active" align-center>
<el-step v-for="(item, index) in stepList" :key="item.refKey">
<template #icon>
<div class="step-icon-box" :class="{ avtive: active == index }">
<div v-if="active == index" class="mt--1px">{{ index + 1 }}</div>
<div v-else class="mt-1px">
<Icon :size="14" icon="ep:select" color="var(--el-color-primary)"></Icon>
</div>
</div>
</template>
<template #title>
<div class="c-[var(--el-text-color-primary)]">{{ item.title }}</div>
</template>
<template #description>
<div
class="c-[var(--el-color-info-light-3)] text-12px"
:class="{ 'des-hide': active != index }"
>{{ item.des }}</div
>
</template>
</el-step>
</el-steps>
<template v-for="(item, index) in stepList" :key="item.refKey">
<div v-if="item.formId" v-show="active == index">
<FormView
:ref="(el) => (formViewRef[item.refKey] = el)"
formType="add"
handleType="returnData"
showType="view"
:showButton="false"
:formId="item.formId"
></FormView>
</div>
<div v-if="active == index && active < 3" class="mt-40px flex justify-center">
<el-button v-if="active != 0" @click="active = active - 1">上一步</el-button>
<el-button type="primary" v-if="active <= 2" @click="setNextForm">{{
active == 2 ? '提交' : '下一步'
}}</el-button>
</div>
</template>
</ContentWrap>
</template>
<script setup lang="ts">
import { FormView } from '@/components/LowDesign/index'
defineOptions({ name: 'FormHxfb' })
const active = ref(0)
const stepList = ref([
{
refKey: 'basic_sp',
title: '基本信息',
des: '请填写公司基本信息',
formId: '1829756210681274369'
},
{
refKey: 'detail_sp',
title: '详细信息',
des: '请填写公司详细信息',
formId: '1829756992465985538'
},
{
refKey: 'financial_sp',
title: '财务信息',
des: '请填写公司财务信息',
formId: '1829757052494864386'
},
{
refKey: 'submit_sp',
title: '提交成功',
des: '公司信息提交成功'
}
])
const formViewRef = ref({})
const formData = ref({})
const setNextForm = async () => {
const currStep = stepList.value[active.value]
if (currStep.formId) {
const formRef = formViewRef.value[currStep.refKey].controlRef
const data = await formRef.handleSubmit(true)
formData.value[currStep.refKey] = data
}
active.value++
}
</script>
<style lang="scss" scoped>
::v-deep(.low-form) {
padding-bottom: 0;
}
.step-icon-box {
display: flex;
width: 30px;
height: 30px;
background-color: #fff;
border: 2px solid var(--el-color-primary);
border-radius: 50%;
box-sizing: border-box;
align-items: center;
justify-content: center;
& > div {
display: flex;
font-weight: bold;
}
&.avtive {
color: #fff;
background-color: var(--el-color-primary);
}
}
::v-deep(.el-steps) {
.el-step__head {
.el-step__line {
background-color: var(--el-color-info-light-7);
}
.el-step__icon {
width: 30px !important;
}
&.is-finish {
.el-step__line {
background-color: var(--el-color-primary);
}
}
}
.el-step:not(:last-child) {
.el-step__head {
&.is-process::before {
position: absolute;
top: calc(50% - 1px);
right: 0;
z-index: 1;
display: block;
width: 50%;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
}
}
}
</style>

View File

@@ -0,0 +1,201 @@
<template>
<!-- <div> 发布表单页(纵向分步) </div> -->
<ContentWrap>
<el-steps direction="vertical" :active="active">
<el-step v-for="(item, index) in stepList" :key="item.refKey">
<template #icon>
<div class="step-icon-box" :class="{ avtive: active == index }">
<div v-if="active == index" class="mt--1px">{{ index + 1 }}</div>
<div v-else class="mt-1px">
<Icon :size="14" icon="ep:select" color="var(--el-color-primary)"></Icon>
</div>
</div>
</template>
<template #title>
<div class="c-[var(--el-text-color-primary)]">{{ item.title }}</div>
</template>
<template #description>
<div
class="c-[var(--el-color-info-light-3)] text-12px"
:class="{ 'des-hide': active != index }"
>{{ item.des }}</div
>
<div v-if="item.formId" v-show="active == index">
<FormView
:ref="(el) => (formViewRef[item.refKey] = el)"
formType="add"
handleType="returnData"
showType="view"
:showButton="false"
:formId="item.formId"
></FormView>
</div>
<div v-if="active == index && active < 3" class="ml-65px mb-20px">
<el-button v-if="active != 0" @click="active = active - 1">上一步</el-button>
<el-button type="primary" v-if="active <= 2" @click="setNextForm">{{
active == 2 ? '提交' : '下一步'
}}</el-button>
</div>
</template>
</el-step>
</el-steps>
</ContentWrap>
</template>
<script setup lang="ts">
import { FormView } from '@/components/LowDesign/index'
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'FormZxfb' })
interface HandleClickProps {
(formData: object): string
}
const props = defineProps<{
addFun: HandleClickProps
}>()
const active = ref(0)
const stepList = ref([
{
refKey: 'basic',
title: '基本信息',
des: '请填写公司基本信息',
formId: '1829756210681274369'
},
{
refKey: 'detail',
title: '详细信息',
des: '请填写公司详细信息',
formId: '1829756992465985538'
},
{
refKey: 'financial',
title: '财务信息',
des: '请填写公司财务信息',
formId: '1829757052494864386'
},
{
refKey: 'submit',
title: '提交成功',
des: '公司信息提交成功'
}
])
const formViewRef = ref({})
const formData = ref({})
const debounce = (func, wait) => {
let timeout
return function () {
const args = arguments
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(args), wait)
}
}
const setNextForm = debounce(async () => {
const currStep = stepList.value[active.value]
if (currStep.formId) {
const formRef = formViewRef.value[currStep.refKey].controlRef
const data = await formRef.handleSubmit(true)
formData.value[currStep.refKey] = data
if (active.value == 2 && data) {
interface primordialTableDataType {
basic: {
company_name: string
client_state: string
industry_type: string
client_source: string
client_level: string
}
}
const userInfo = ref({} as ProfileVO)
const users = await getUserProfile()
userInfo.value = users
let primordialTableData = {
company_name: (formData.value as primordialTableDataType).basic.company_name,
customer_status: (formData.value as primordialTableDataType).basic.client_state,
industry_type: (formData.value as primordialTableDataType).basic.industry_type,
customer_source: (formData.value as primordialTableDataType).basic.client_source,
customer_star: (formData.value as primordialTableDataType).basic.client_level,
add_people: userInfo.value.id,
update_date: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
bh: '20241001'
}
props.addFun(primordialTableData)
}
active.value++
}
}, 500)
</script>
<style lang="scss" scoped>
::v-deep(.low-form) {
padding-bottom: 0;
}
.step-icon-box {
display: flex;
width: 30px;
height: 30px;
background-color: #fff;
border: 2px solid var(--el-color-primary);
border-radius: 50%;
box-sizing: border-box;
align-items: center;
justify-content: center;
& > div {
display: flex;
font-weight: bold;
}
&.avtive {
color: #fff;
background-color: var(--el-color-primary);
}
}
::v-deep(.el-steps) {
.el-step__head {
width: 30px;
.el-step__line {
left: 14px;
background-color: var(--el-color-info-light-7);
}
.el-step__icon {
width: 30px !important;
}
&.is-finish {
.el-step__line {
background-color: var(--el-color-primary);
}
}
&.is-process::before {
position: absolute;
top: 0;
left: 14px;
z-index: 1;
display: block;
width: 2px;
height: 50%;
background-color: var(--el-color-primary);
content: '';
}
}
.des-hide {
margin-bottom: 50px;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<!-- <div> 高级筛选页 </div>
顶部搜索表单设计
底部表格表单开发 -->
<div class="box">
<div class="order-top">
<FormView
formType="edit"
handleType="returnData"
showType="view"
:defaultData="orderTopData.defaultData"
:showButton="false"
:enhanceData="{ setSearch}"
:formId="orderTopData.formId"
:ref="(el) => (tableRef[tableData.name] = el)"
></FormView>
</div>
<div class="flex gap-2">
<span>已选条件</span>
<template v-for="tag in dynamicTags" :key="tag.label">
<el-tag
v-if="tag.show"
closable
:disable-transitions="false"
@close="handleClose(tag)"
>
{{ tag.label }}
</el-tag>
</template>
<span class="qcsx" @click="delFun">清除筛选</span>
</div>
<div class="table">
<LowTable :tableId="tableData.tableId" :enhanceData="tableData.enhanceData"> </LowTable>
</div>
</div>
</template>
<script setup lang="ts">
import { FormView, LowTable } from '@/components/LowDesign/index'
import type { TagProps } from 'element-plus'
interface TagsItem {
name: string
type: TagProps['type']
}
// const dynamicTags = ref(['选择项一', '选择项一', '选择项一'])
const dynamicTags = ref<any>([
// { label: '选项一', type: 'primary',lx:'kh',show:true },
// { label: '选项二', type: 'primary',lx:'kh',show:false },
])
const orderTopData = ref({
type: 'exploit',
refKey: 'orderTop',
formId: '1838814762506964994',
defaultData: {
type1:'1',
type2:'1',
type3:'1'
}
})
const tableRef = ref({})
const formData = ref({})
const tableData = ref(
{
label: '高级筛选',
name: 'gjsx',
tableId: '1838824808796180481',
enhanceData: { hideHeader: 'disabled' }
},
)
const tableCrud = ref()
const tableSearch = ref({})
const label = ref<any>()
// 多选处理
const khArr = ref<any>([])
const hyArr = ref<any>([])
const setSearch = (searchObj) => {
if(searchObj.type == 'kh'){
searchObj.dic.map(item=>{
item.type = 'primary'
item.show = searchObj.value.includes(item.value) ? true : false
item.lx = searchObj.type
return item
})
khArr.value = searchObj.dic
}
if(searchObj.type == 'hy'){
searchObj.dic.map(item=>{
item.type = 'primary'
item.show = searchObj.value.includes(item.value) ? true : false
item.lx = searchObj.type
return item
})
hyArr.value = searchObj.dic
}
strFnu(searchObj)
}
const strFnu = (data) =>{
let array = khArr.value.concat(hyArr.value)
dynamicTags.value = array
}
const delFun = () =>{
dynamicTags.value = []
}
//已选条件处理
const str = ref<any>([])
const handleClose = (tag) => {
dynamicTags.value.map(item =>{
if(item.label == tag.label){
item.show = false
}
if(item.lx == tag.lx && item.show){
str.value.push(item.value)
}
return item
})
if(tag.lx == 'kh'){
orderTopData.value.defaultData.type1 = str.value.join()
str.value = []
} else if(tag.lx == 'hy'){
orderTopData.value.defaultData.type2 = str.value.join()
str.value = []
}
}
// watch()
</script>
<style lang="scss" scoped>
.box{
background: #fff;
}
.flex{
margin-left: 68px;
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #999;
text-align: left;
.qcsx{
margin-top: -3px;
margin-left:40px;
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
color: #409EFF;
}
}
.table{
width: calc(90% + 40px);
padding: 20px;
margin: auto;
}
::v-deep .el-form-item__label{
color: #999;
}
::v-deep .el-checkbox-group {
display: contents;
}
::v-deep .low-form {
padding-bottom: 0;
}
</style>

View File

@@ -0,0 +1,583 @@
<template>
<div class="group_gjxqy_box">
<!-- 顶部信息 -->
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="topInformation.defaultData"
:showButton="false"
:formId="topInformation.formId"
></FormView>
<!-- 分类页面X -->
<el-tabs v-model="tabsOneActive" class="demo-tabs" @tab-click="handleClick">
<template v-for="item in tabsOneList" :key="item.name">
<el-tab-pane :label="item.label" :name="item.name"></el-tab-pane>
</template>
</el-tabs>
<!-- 流程进度 -->
<el-card>
<template #header>
<div class="card-header">
<span class="text-16px">流程进度</span>
</div>
</template>
<el-steps :active="stepsActive" align-center>
<template v-for="item in stepsList" :key="item.id">
<el-step
:title="item.title"
:description="item.description"
:class="item.id == stepsActive ? 'stepsActive' : ''"
/>
</template>
</el-steps>
</el-card>
<!-- 操作记录 -->
<el-card>
<template #header>
<div class="card-header">
<span class="text-16px">操作记录</span>
</div>
</template>
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="operationRecord.defaultData"
:showButton="false"
:formId="operationRecord.formId"
></FormView>
</el-card>
<!-- 操作记录流程 -->
<el-card>
<template #header>
<div class="card-header">
<span class="text-16px">操作记录</span>
</div>
</template>
<el-steps
direction="vertical"
:active="stepsVertical.length"
align-center
class="steps-vertical-class"
>
<template v-for="item in stepsVertical" :key="item.id">
<el-step :title="item.title">
<template #description>
<el-card>
<div>
<el-avatar :size="34" :src="item.cardContent.avatarImg" />
</div>
<div class="ml-5px grid">
<div class="flex items-center">
<p class="c-#bcaeae m-0 font">{{ item.cardContent.username }}</p>
<p class="pl-5px m-0 c-#999 text-11px font-family">{{
item.cardContent.userjob
}}</p>
</div>
<div
class="flex items-center c-#bcaeae text-11px font-family"
style="color: #666"
>
<p class="m-0">{{ item.cardContent.examineApproveGrade }}</p>
<p class="pl-3px m-0">{{ item.cardContent.examineApproveName }}</p>
<p class="pl-3px m-0">{{ item.cardContent.examineApproveResult }}</p>
</div>
<div class="flex items-center c-#999">
<el-icon><Clock /></el-icon>
<p class="pl-3px m-0" style="font-family: '微软雅黑', sans-serif">{{
item.cardContent.examineApproveTime
}}</p>
</div>
</div>
<div class="ml-auto c-#bcaeae text-11px font" style="font-size: 12px">
{{ item.cardContent.type }}
</div>
</el-card>
</template>
</el-step>
</template>
</el-steps>
</el-card>
<!-- 日志记录 -->
<el-card>
<template #header>
<div class="card-header">
<span class="text-16px">日志记录</span>
</div>
</template>
<el-empty description="未查询到相关数据" image="/img/img5.svg" />
</el-card>
<!-- 分类标签 -->
<el-card class="card-and-tabs">
<template #header>
<div class="card-header">
<el-tabs v-model="tabsOneActive" @tab-click="handleClick">
<template v-for="item in tabsOneList" :key="item.name">
<el-tab-pane :label="item.label" :name="item.name"></el-tab-pane>
</template>
</el-tabs>
</div>
</template>
<LowTable :tableId="tabsTableData.tabId" :enhanceData="tabsTableData.enhanceData"></LowTable>
</el-card>
</div>
</template>
<script setup lang="ts">
import { FormView, LowTable } from '@/components/LowDesign/index'
import type { TabsPaneContext } from 'element-plus'
import { Clock } from '@element-plus/icons-vue'
defineOptions({ name: 'GroupGjxqy' })
const topInformationData = reactive({
create_date: '2024-09-11 09:04:00',
customer_rating: 4,
customer_source: '7',
customer_status: '3',
fields_4858197: '',
fields_5452530: '',
head_contact: '1833323260582391810',
system_number: '202010001',
tabs_title: '广州某科技有限公司',
type_of_industry: '3',
vesting_officer: '1833323260582391810'
})
const topInformation = ref({
title: '基本信息',
formId: '1838820624684339201',
defaultData: topInformationData
})
const tabsOneActive = ref('1')
const tabsOneList = ref([
{ label: '分类页面一', name: '1' },
{ label: '分类页面二', name: '2' },
{ label: '分类页面三', name: '3' },
{ label: '分类页面四', name: '4' }
])
const stepsActive = ref(4)
const stepsList = ref([
{ id: 1, title: '提交申请', description: '2024-09-24 15:03' },
{ id: 2, title: '部门审批', description: '2024-09-24 15:03' },
{ id: 3, title: '行政审批', description: '2024-09-24 15:03' },
{ id: 4, title: '申请成功', description: '2024-09-24 15:03' }
])
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
const operationRecordData = ref({
tabs_customer_name: '广州某科技有限公司',
tabs_customer_rating: 4,
tabs_customer_source: '7',
tabs_customer_status: '3',
tabs_now_city: '130000',
tabs_system_number: '202010001',
tabs_type_of_industry: '3',
tabs_update_date: '2024-09-25 00:00:00',
tabs_vesting_officer: '1833323260582391810',
tabs_company_tax: '91440300MA5FQY****',
tabs_company_tel: '3666-756614',
tabs_company_web: 'http://www.xx.com',
tabs_current_title: '5',
tabs_deposit_bank: '交通银行深圳井南山支行',
tabs_detailed_address: '广东省深圳市南山区',
tabs_head_contact: '李小红',
tabs_invoice_title: '广州某科技有限公司',
tabs_phone_number: '15238683333'
})
const operationRecord = ref({
title: '操作记录',
formId: '1838842046060228609',
defaultData: operationRecordData
})
const stepsVertical = ref([
{
id: 1,
title: '09-24',
cardContent: {
avatarImg: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
username: '赵小刚',
userjob: '销售经理',
examineApproveGrade: '1级审批人',
examineApproveName: '李小明',
examineApproveResult: '审批通过,通过原因:无',
examineApproveTime: '2019-08-23 22:31',
type: '订单'
}
},
{
id: 2,
title: '09-24',
cardContent: {
avatarImg: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
username: '赵小刚',
userjob: '销售经理',
examineApproveGrade: '1级审批人',
examineApproveName: '李小明',
examineApproveResult: '审批通过,通过原因:无',
examineApproveTime: '2019-08-23 22:31',
type: '订单'
}
},
{
id: 3,
title: '09-24',
cardContent: {
avatarImg: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
username: '赵小刚',
userjob: '销售经理',
examineApproveGrade: '1级审批人',
examineApproveName: '李小明',
examineApproveResult: '审批通过,通过原因:无',
examineApproveTime: '2019-08-23 22:31',
type: '订单'
}
},
{
id: 4,
title: '09-24',
cardContent: {
avatarImg: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
username: '赵小刚',
userjob: '销售经理',
examineApproveGrade: '1级审批人',
examineApproveName: '李小明',
examineApproveResult: '审批通过,通过原因:无',
examineApproveTime: '2019-08-23 22:31',
type: '订单'
}
}
])
const tabsTableData = ref({
tabId: '1840268794366795777',
enhanceData: {
buttonType: 'hidden'
}
})
</script>
<style lang="scss" scoped>
.group_gjxqy_box {
.el-tabs {
::v-deep(.el-tabs__header) {
margin-bottom: 0;
background-color: white;
.el-tabs__nav-wrap {
border: 1px solid #e4e7ed;
border-top: none;
.el-tabs__active-bar {
height: 1px;
}
.el-tabs__item {
margin-bottom: 15px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-weight: 700;
color: #666;
&:hover {
color: rgb(64 158 255);
}
}
.el-tabs__item.is-active {
color: rgb(64 158 255);
}
&::after {
height: auto;
}
}
}
}
::v-deep(.el-card) {
width: 95%;
margin: 20px auto 0;
border-radius: 10px;
.el-card__header {
height: 60px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 700;
color: #666;
background-color: rgb(249 249 249 / 100%);
border-bottom: 1px solid rgb(233 233 233 / 100%);
}
.el-card__body {
.el-steps {
height: 90px;
padding-top: 10px;
.el-step {
.el-step__head {
.el-step__icon {
width: 13px;
height: 13px;
border: 3px solid;
.el-step__icon-inner {
display: none;
}
}
}
.el-step__main {
.el-step__title {
font-size: 12px;
font-weight: 400;
line-height: 30px;
color: #666;
}
.el-step__description {
color: #ccc;
}
}
&.stepsActive {
.el-step__head {
.el-step__icon {
background-color: #409eff;
}
}
}
}
}
.steps-vertical-class {
height: auto;
.el-step__head {
display: flex;
justify-content: center;
border-color: #f2f2f2;
.el-step__line {
bottom: -50%;
left: 11.5px;
background-color: #f2f2f200;
}
.el-step__icon {
width: 9px !important;
height: 9px !important;
border-width: 2px !important;
}
}
.el-step__main {
display: flex;
align-items: center;
height: 125px;
margin-top: 10px;
.el-step__title {
position: absolute;
top: -9px;
color: #999 !important;
}
.el-step__description {
width: 100%;
padding-right: 0;
.el-card {
width: 100%;
height: 90px;
margin: 0;
border: none;
box-shadow: 0 0 5px rgb(0 0 0 / 9.8%);
.el-card__body {
display: flex;
height: calc(100% - 16px);
padding: 8px 30px 8px 10px;
}
}
}
}
}
.el-empty {
.el-empty__image {
width: 120px;
height: 120px;
}
.el-empty__description > p {
font-size: 13px;
color: #ccc;
}
}
.avue-crud {
.el-card {
border-radius: 0;
.el-form {
.el-table__header-wrapper {
.el-table__header {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 700;
.el-table__cell {
color: #666;
}
}
}
.el-table__body {
.el-table__row {
background-color: #f5f5f5;
.el-table__cell {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
.el-button {
.el-icon {
line-height: 0.9em;
}
span {
font-size: 14px !important;
}
}
}
}
.el-table__row--striped .el-table__cell {
background-color: #fff;
}
}
}
}
}
}
&.card-and-tabs {
.el-card__header {
height: 60px;
padding: 0 20px 0 0;
.el-tabs {
--el-tabs-header-height: 60px;
.el-tabs__header {
background-color: rgb(249 249 249);
.el-tabs__nav-wrap {
height: 60px;
margin-bottom: 0;
border: none;
}
}
}
}
.el-card__body {
padding-bottom: 40px;
.el-card {
width: calc(100% - 10px);
padding: 0 5px;
.el-card__body {
padding-bottom: 0;
}
}
}
}
}
}
::v-deep .el-form-item {
.el-form-item__label {
font-size: 14px !important;
}
.el-form-item__content {
.el-input__inner {
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-weight: 400;
color: #666 !important;
}
}
}
::v-deep .avue-title p {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important;
font-size: 18px !important;
font-weight: 700 !important;
color: #666 !important;
}
::v-deep .demo-tabs .el-tabs__nav {
margin-left: 25px;
.el-tabs__item {
margin-bottom: 15px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
}
.el-tabs__active-bar {
left: -1px;
height: 2px !important;
}
}
::v-deep .card-header .el-tabs__nav {
// margin-left: 25px;
.el-tabs__item {
margin-bottom: 0 !important;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
text-align: left;
}
.el-tabs__active-bar {
left: 2px;
height: 2px !important;
}
}
.font {
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
color: #666;
}
.font-family {
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 12px;
font-weight: 400;
color: #999;
}
// .fonts{
// font-family: 'MicrosoftYaHei', '微软雅黑 Regular', '微软雅黑', sans-serif;
// font-weight: 400;
// font-style: normal;
// font-size: 12px;
// }
</style>

View File

@@ -0,0 +1,92 @@
<template>
<!-- 基础详情页 -->
<ContentWrap>
<div v-for="item in stepList" :key="item.refKey">
<div class="pl-8px b-l-6px b-solid b-#409EFF mb-15px title">{{ item.title }}</div>
<div class="mb-20px" v-if="item.type == 'exploit'">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="item.defaultData"
:showButton="false"
:formId="item.formId"
></FormView>
</div>
<div class="mt-20px" v-if="item.type == 'design'">
<LowTable :tableId="item.tableId" :enhanceData="item.enhanceData"></LowTable>
</div>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { FormView, LowTable } from '@/components/LowDesign/index'
defineOptions({ name: 'GroupJcxqy' })
const basicDefaultData = reactive({
customer_name: '广州某科技有限公司',
customer_rating: 3,
customer_source: '3',
customer_status: '4',
now_city: '120000,120100,120102',
system_number: '202010033',
type_of_industry: '4',
update_date: '2024-01-23 23:23:23',
vesting_officer: '1836227753657548801'
})
const detailDefaultData = reactive({
company_tax: '91645530MA5FQY4QGB',
company_tel: '15238680350',
company_web: 'http://www.xx.cn',
current_title: '2',
deposit_bank: '建设银行广州白云支行',
detailed_address: '440000,440100,440111',
head_contact: '1836227753657548801',
invoice_title: '发票抬头',
phone_number: '15239683355'
})
const financialEnhanceData = reactive({
hideHeader: 'disabled'
})
const stepList = ref([
{
refKey: 'basic',
title: '基本信息',
type: 'exploit',
formId: '1833063456320286721',
defaultData: basicDefaultData
},
{
refKey: 'detail',
title: '详细信息',
type: 'exploit',
formId: '1833076541672169474',
defaultData: detailDefaultData
},
{
refKey: 'financial',
title: '下级客户',
type: 'design',
tableId: '1833323260582391810',
enhanceData: financialEnhanceData
}
])
</script>
<style lang="scss" scoped>
.title {
height: 18px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 18px;
color: #666670;
border-top: none;
border-right: none;
border-bottom: none;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<!-- tabs标签页风格表单开发示例 -->
<el-tabs v-model="activeName" type="border-card" class="demo-tabs" @tab-click="handleClick">
<template v-for="item in tabsPaneList" :key="item.name">
<el-tab-pane :label="item.label" :name="item.name">
<LowTable
:ref="(el) => (tableRef[item.name] = el)"
:tableId="item.tableId"
:calcHeight="item.calcHeight || undefined"
:enhanceData="item.enhanceData"
></LowTable>
</el-tab-pane>
</template>
</el-tabs>
</template>
<script setup lang="ts">
import { LowTable } from '@/components/LowDesign/index'
defineOptions({ name: 'GroupSjbby' })
const activeName = ref('cjje')
const tableRef = ref({})
const tabsPaneList = ref([
{
label: '成交金额',
name: 'cjje',
tableId: '1834136609367400450',
enhanceData: { hideHeader: 'disabled' }
},
{ label: '回款金额', name: 'hkje', tableId: '1851509263837597697', calcHeight: 200 }
])
const handleClick = (tab) => {
const key = tab.props.name
if (key == 'hkje' && tableRef.value[key]) tableRef.value[key].initTableLayout()
}
</script>
<style lang="scss" scoped>
.demo-tabs > .el-tabs__content {
padding: 32px;
font-size: 32px;
font-weight: 600;
color: #6b778c;
}
.demo-tabs {
::v-deep(.el-tabs__nav-wrap) {
.el-tabs__item {
height: 50px;
font-size: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<div> 高级筛选页 </div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="w-100%">
<div class="box w-100%">
<div class="w-[calc(100%-60px)] header">
<el-radio-group v-model="radio" >
<template v-for="(item,index) in tabbleDate" :key="index">
<el-radio-button :label="item.label" :value="index"/>
</template>
</el-radio-group>
</div>
<div class="tabel">
<LowTable :ref="(el)=>(customRef = el)" :tableId="tabbleDate[radio].tableId" :enhanceData="tabbleDate[radio].enhanceData" > </LowTable>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const radio = ref(0)
const customRef = ref()
const tabbleDate = ref([
{
label:'操作日志',
type:'table',
defaultData:{},
tableId:'1854067250698612737',
enhanceData: { hideHeader: 'disabled'}
},
{
label:'审批日志',
type:'table',
defaultData:{},
tableId:'1854331399533998082',
enhanceData: { hideHeader: 'disabled'}
},
{
label:'登录日志',
type:'table',
defaultData:{},
tableId:'1854341001851940865',
enhanceData: { hideHeader: 'disabled'}
}
])
</script>
<style lang="scss" scoped>
.box{
background-color: rgb(255 255 255 / 100%);
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
box-sizing: border-box;
.header{
display: flex;
height: 60px;
padding: 0 30px;
background-color: rgb(249 249 249 / 100%);
border-bottom: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px 10px 0 0;
align-content: center;;
::v-deep(.el-radio-group){
.el-radio-button{
.el-radio-button__inner{
display: flex;
width: 101px;
height: 35px;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
color: #999;
align-items: center;
justify-content: center;
}
}
.is-active {
.el-radio-button__inner{
color: #FFF ;
}
}
}
}
.tabel{
padding: 30px;
}
}
</style>

View File

@@ -0,0 +1,340 @@
<template>
<div class="step-orde-form-box">
<div class="order-top">
<div style="display: flex">
<div class="tb-box">
<img src="/img/zwgl.jpg" style="height: 30px" />
</div>
<div class="title-box">
<p class="title">销售总监</p>
<div class="text-box">
<span>暂无相关描述</span>
</div>
</div>
</div>
<FormView
:ref="(el) => (formViewRef[orderTopData.refKey] = el)"
formType="add"
handleType="returnData"
showType="view"
:defaultData="orderTopData.defaultData"
:showButton="false"
:formId="orderTopData.formId"
></FormView>
</div>
<footer class="footer">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<template v-for="(item, index) in data" :key="index">
<el-tab-pane :label="item.label" :name="item.name">
<div class="tabs-box">
<FormView
v-if="item.formId"
formType="add"
handleType="returnData"
showType="view"
:defaultData="item.defaultData"
:showButton="true"
:formId="item.formId"
></FormView>
<template v-if="item.tableId">
<LowTable :tableId="item.tableId" :enhanceData="item.enhanceData"> </LowTable>
</template>
</div>
</el-tab-pane>
</template>
</el-tabs>
</footer>
</div>
</template>
<script setup lang="ts">
import type { TabsPaneContext } from 'element-plus'
interface Props {
type?: string //控件名称
}
const props = defineProps<Props>()
const orderTopData = ref({
refKey: 'gdgl',
formId: '1846114280447025153',
defaultData: {
gdbh: '202062976600',
gdbt: '处理客户反馈问题',
gdzt: 1,
lx: props.type
}
})
const formViewRef = ref({})
const activeName = ref('first')
const data = reactive([
{
label: '操作权限',
name: 'first',
defaultData: {},
formId: '1846364734045270018'
},
{
label: '数据权限',
name: 'second',
defaultData: {},
formId: '1846383339671629825'
},
{
label: '字段权限',
name: 'third',
defaultData: {},
formId: '1846399447287967745'
},
{
label: '成员列表',
name: 'fourth',
defaultData: {},
tableId: '1846429439782793217',
enhanceData: { hideHeader: 'disabled' }
},
{
label: '操作记录',
name: 'czjl',
defaultData: {},
tableId: '1846103236139950082',
enhanceData: { hideHeader: 'disabled' }
}
])
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
</script>
<style lang="scss" scoped>
.step-orde-form-box {
.order-top {
display: flex;
height: 110px;
padding: 20px 25px;
margin-bottom: 20px;
background-color: #fff;
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
align-items: center;
justify-content: space-between;
.tb-box {
display: flex;
width: 65px;
height: 65px;
line-height: 65px;
justify-content: center;
text-align: center;
background: #fb6260;
border-radius: 50%;
align-items: center;
}
.title-box {
display: flex;
height: 60px;
margin-left: 15px;
flex-direction: column;
justify-content: center;
.title {
padding: 0;
margin: 0;
// margin-bottom: 5px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 700;
line-height: 28px;
text-align: left;
}
.text-box {
display: flex;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #999;
text-align: left;
align-items: center;
.text-box-left {
display: flex;
align-items: center;
.dian {
display: inline-block;
width: 7px;
height: 7px;
margin-right: 5px;
margin-left: 10px;
background: #f90;
border-radius: 50%;
}
}
}
}
}
.steps {
width: calc(90% + 40px);
padding: 60px 0 30px;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
::v-deep(.el-steps) {
.el-step {
.el-step__head.is-process {
color: #409eff;
.el-step__icon.is-text {
color: white;
background-color: #409eff;
border-color: #409eff;
}
}
.el-step__head {
.el-step__line {
top: 13px;
}
.el-step__icon.is-text {
width: 25px;
height: 30px;
.el-step__icon-inner {
font-size: 20px;
line-height: 30px;
}
}
}
.el-step__main {
.el-step__title {
font-size: 13px;
font-weight: 400;
line-height: 25px;
color: #666;
}
.el-step__description {
font-size: 13px;
color: #ccc;
}
}
}
}
}
::v-deep .card > .el-card {
width: calc(90% + 40px);
border-radius: 10px;
}
.card {
width: 100%;
// &>::v-deep(.crud){
// width: calc(90% + 40px);
// }
::v-deep(.el-card) {
// width: calc(90% + 40px);
.el-card__header {
background-color: #f9f9f9;
.card-header {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-weight: 700;
color: #666;
}
}
.el-card__body {
.avue-crud {
.el-card {
margin: 0 auto;
border-radius: 0;
.el-table__header {
height: 55px;
th {
height: 55px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 13px;
font-weight: 700;
color: #666;
background-color: #f2f2f2;
}
}
.el-table__body {
.el-table__row {
.el-table__cell {
font-size: 13px;
border-right: none;
}
}
}
}
}
}
}
}
}
.footer {
// padding: 0px 10p;
margin-top: 20px;
margin-bottom: 60px;
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(11 11 11 / 5.7%);
::v-deep .el-tabs__nav-wrap::after {
// background: none !important;
}
.tabs-box {
padding: 0 20px;
}
::v-deep .el-tabs__nav-scroll {
display: flex;
height: 60px;
line-height: 60px;
background: #f9f9f9;
align-items: center;
border-radius: 10px 10px 0 0;
.el-tabs__nav {
height: 60px;
align-items: center;
.el-tabs__item {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
color: #666;
}
.is-active {
color: #409eff;
}
}
}
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div >
<div class="box">
<el-progress :percentage="jdt" :stroke-width="11"/>
</div>
</div>
</template>
<script lang="ts" setup>
interface Props {
jdt?: number //控件名称
}
const props =defineProps<Props>()
</script>
<style lang="scss" scoped>
.box{
margin-top: 8px;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="w-100%">
<header class="header ">
<div class="list" v-for="(item,index) in listData" :key="index">
<p class="title">{{item.title}}</p>
<p class="text">{{item.num}}</p>
</div>
</header>
<footer class="content">
<div class="content-top" >
<el-radio-group v-model="radio1" size="large">
<el-radio-button label="全部项目" value="qbxm" />
<el-radio-button label="进行中" value="jxz" />
<el-radio-button label="已完成" value="ywc" />
<el-radio-button label="已延期" value="yyq" />
<el-radio-button label="已取消" value="yqx" />
</el-radio-group>
<el-input
v-model="input"
style="width: 200px;height: 35px;margin-left:20px;"
size="large"
placeholder="输入搜索关键词"
:suffix-icon="Search"
clearable
/>
</div>
<div class="table">
</div>
<LowTable :tableId="data.tableId" :enhanceData="data.enhanceData" > </LowTable>
</footer>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
const listData = reactive([
{title:'项目总数',num:'200'},
{title:'进行中',num:'50'},
{title:'已完成',num:'50'},
{title:'已延期',num:'50'},
{title:'已取消',num:'50'},
])
const data = reactive({
label:'全部项目',
name:'qbxm',
defaultData:{},
tableId:'1847094468974710786',
type:'table',
enhanceData: { hideHeader: 'disabled'}
})
const radio1 = ref('qbxm')
const input = ref('')
</script>
<style lang="scss" scoped>
.header{
display: flex;
height: 150px;
padding: 0 20px;
margin-bottom:20px;
background: #fff;
border-radius: 10px;
justify-content: space-between;
align-items: center;
.list{
width: 292px;
width: 100%;
p{
padding: 0;
margin: 0;
text-align: center;
}
.title{
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 42px;
color: #999;
}
.text{
font-family: '微软雅黑 Bold', '微软雅黑', sans-serif;
font-size: 36px;
font-weight: 700;
line-height: 42px;
color: #666;
}
}
}
.content{
position: relative;
// height: 600px;
padding: 20px;
background: #fff;
border-radius: 10px;
.content-top{
position: absolute;
top: 30px;
z-index: 99;
display: flex;
align-items: center;
::v-deep(.el-radio-group) {
.el-radio-button {
.el-radio-button__inner {
display: flex;
width: 101px;
height: 35px;
padding: 0;
align-items: center;
justify-content: center;
color: #999;
}
&.is-active{
.el-radio-button__inner {
color: white;
}
}
}
}
}
.table{
// position: absolute;
margin-top:10px;
}
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<div class="step-orde-form-box">
<div class="order-top">
<FormView
:ref="(el) => (formViewRef[orderTopData.refKey] = el)"
formType="add"
handleType="returnData"
showType="view"
:defaultData="orderTopData.defaultData"
:showButton="false"
:formId="orderTopData.formId"
></FormView>
</div>
<footer class="footer">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<template v-for="(item,index) in data" :key="index">
<el-tab-pane :label="item.label" :name="item.name">
<div class="tabs-box">
<upDate v-if="item.type == 'jurisdiction'"></upDate>
<template v-if="item.type == 'table'">
<LowTable :tableId="item.tableId" :enhanceData="item.enhanceData" > </LowTable>
</template>
</div>
</el-tab-pane>
</template>
</el-tabs>
</footer>
</div>
</template>
<script setup lang="ts">
import type { TabsPaneContext } from 'element-plus'
import upDate from './update.vue'
interface Props {
type?: string //控件名称
}
const props =defineProps<Props>()
const orderTopData = ref({
type:'table',
refKey: 'gdgl',
formId: '1847209781133185025',
defaultData: {},
enhanceData: { hideHeader: 'disabled'}
})
const formViewRef = ref({})
const activeName = ref('first')
const data = reactive([
{label:'任务列表',name:'first',defaultData:{},tableId:'1847472439438737409',type:'table'},
{label:'更新记录',name:'second',defaultData:{},tableId:'',type:'jurisdiction'},
{label:'附件记录',name:'third',defaultData:{},tableId:'1845012516400803842',type:'table',enhanceData: { hideHeader: 'disabled'}},
{label:'成员列表',name:'fourth',defaultData:{},tableId:'1848301752228683777',type:'table',enhanceData: { hideHeader: 'disabled'}},
])
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
</script>
<style lang="scss" scoped>
.step-orde-form-box {
.order-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 25px;
margin-bottom: 20px;
background-color: #fff;
// border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(11 11 11 / 5.7%);
.tb-box {
display: flex;
width: 50px;
height: 50px;
line-height: 50px;
justify-content: center;
text-align: center;
background: #fb6260;
border-radius: 50%;
align-items: center;
}
.title-box {
margin-left: 10px;
.title {
padding: 0;
margin: 0;
// margin-bottom: 5px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 700;
line-height: 28px;
text-align: left;
}
.text-box {
display: flex;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #999;
text-align: left;
align-items: center;
.text-box-left {
display: flex;
align-items: center;
.dian {
display: inline-block;
width: 7px;
height: 7px;
margin-right: 5px;
margin-left: 10px;
background: #f90;
border-radius: 50%;
}
}
}
}
}
.steps {
width: calc(90% + 40px);
padding: 60px 0 30px;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
::v-deep(.el-steps) {
.el-step {
.el-step__head.is-process {
color: #409eff;
.el-step__icon.is-text {
color: white;
background-color: #409eff;
border-color: #409eff;
}
}
.el-step__head {
.el-step__line {
top: 13px;
}
.el-step__icon.is-text {
width: 25px;
height: 30px;
.el-step__icon-inner {
font-size: 20px;
line-height: 30px;
}
}
}
.el-step__main {
.el-step__title {
font-size: 13px;
font-weight: 400;
line-height: 25px;
color: #666;
}
.el-step__description {
font-size: 13px;
color: #ccc;
}
}
}
}
}
::v-deep .card > .el-card {
width: calc(90% + 40px);
border-radius: 10px;
}
.card {
width: 100%;
// &>::v-deep(.crud){
// width: calc(90% + 40px);
// }
::v-deep(.el-card) {
// width: calc(90% + 40px);
.el-card__header {
background-color: #f9f9f9;
.card-header {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-weight: 700;
color: #666;
}
}
.el-card__body {
.avue-crud {
.el-card {
margin: 0 auto;
border-radius: 0;
.el-table__header {
height: 55px;
th {
height: 55px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 13px;
font-weight: 700;
color: #666;
background-color: #f2f2f2;
}
}
.el-table__body {
.el-table__row {
.el-table__cell {
font-size: 13px;
border-right: none;
}
}
}
}
}
}
}
}
}
.footer{
// padding: 0px 10p;
margin-top: 30px;
margin-bottom: 60px;
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(11 11 11 / 5.7%);
::v-deep .el-tabs__nav-wrap::after {
background: none !important;
}
::v-deep .el-tabs__nav-scroll {
background: #f9f9f9 !important;
border-radius: 10px 10px 0 0;
}
.tabs-box{
padding: 0 20px;
margin-top: 10px;
}
}
::v-deep .el-tabs__item{
font-family: '微软雅黑 Bold', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 32px;
}
::v-deep th.el-table__cell {
height: 55px;
font-family: '微软雅黑 Bold', '微软雅黑', sans-serif;
font-weight: 700;
color: #666 !important;
}
::v-deep .el-tabs .el-tabs__nav-wrap {
height: 60px;
.el-tabs__nav{
height: 60px;
.el-tabs__item{
height: 60px;
// line-height: 60px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 32px;
color: #666;
&.is-active{
color: #409eff;
}
}
}
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<div>
<div class="footer">
<!-- <div style="display: flex; align-items: center; margin-bottom: 20px">
<span class="span"></span><span>操作记录</span>
</div> -->
<div class="footer-list">
<el-steps :space="150" :active="czjlData.length" direction="vertical">
<template v-for="(item, index) in czjlData" :key="index">
<el-step :title="item.title" :description="item.description" last>
<template #title>
<div class="content flex h-100px">
<div class="img mt-10px"
><Icon icon="mingcute:user-3-fill" :size="25" style="color: #fff"
/></div>
<div class="w-[calc(100%-45px)] mt-6px mb-6px grid grid-rows-3">
<div class="content-top">
<div class="list-box-top-left">
<span style="font-size: 14px; color: #666">{{ item.name }}</span>
<p class="font" style="margin-left: 5px">{{ item.text }}</p>
</div>
<div class="list-box-top-right pr-20px mt-30px">
<!-- <div> -->
<span class="box"></span>
<span style="margin: 0 10px 0 5px">正常</span>
<span>完成进度50%</span>
<!-- </div> -->
</div>
</div>
<!-- <div class="content-bottom"> -->
<div>
<p class="text">{{ item.content }}</p>
</div>
<div class="lzgd">
<Icon icon="mingcute:time-line" :size="14" style="color: #999" />
<span style="color: #999 !important">{{ item.time }}</span>
</div>
<!-- </div> -->
</div>
</div>
</template>
</el-step>
</template>
</el-steps>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const czjlData = ref([
{
title: '',
description: '08-23',
text: '销售经理',
name: '赵小刚',
content:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
gd: '工单标题',
time: '2020-12-23 22:31'
},
{
title: '',
description: '08-23',
text: '销售经理',
name: '赵小刚',
content:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
gd: '工单标题',
time: '2020-12-23 22:31'
},
{
title: '',
description: '08-23',
text: '销售经理',
name: '赵小刚',
content:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
gd: '工单标题',
time: '2020-12-23 22:31'
},
{
title: '',
description: '08-23',
text: '销售经理',
name: '赵小刚',
content:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
gd: '工单标题',
time: '2020-12-23 22:31'
},
{
title: '',
description: '08-23',
text: '销售经理',
name: '赵小刚',
content:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
gd: '工单标题',
time: '2020-12-23 22:31'
}
])
</script>
<style lang="scss" scoped>
.footer {
margin: 30px 50px;
margin-bottom: 50px;
.span {
display: inline-block;
width: 8.5px;
height: 19px;
margin-right: 10px;
background: #409eff;
}
.footer-list {
width: 100%;
// height: 200px;
// padding: 20px;
::v-deep(.el-steps) {
.el-step.is-vertical {
.el-step__head {
.el-step__line {
width: 1px;
margin-top: 25px;
background-color: #f2f2f2;
}
.el-step__icon {
width: 10px !important;
height: 10px;
margin-top: -20px;
margin-left: 7px;
overflow: hidden;
.el-step__icon-inner {
color: #fff;
}
}
}
.el-step__main {
margin-top: 2px;
.el-step__description.is-finish {
color: #999;
}
}
}
// .el-step:last-child {
// .el-step__head::after {
// position: absolute;
// top: 25px;
// right: 11px;
// z-index: 1;
// display: block;
// width: 1.6px;
// height: 110px;
// background-color: #f2f2f2;
// border-color: red;
// content: '';
// }
// }
}
.content {
position: absolute;
top: 35px;
left: 53px;
width: 95.5%;
padding:0 10px;
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 9.8%);
.img {
display: flex;
width: 35px;
height: 35px;
margin-right: 10px;
background: #ccc;
border-radius: 50%;
justify-content: center;
align-items: center;
}
.content-top {
display: flex;
justify-content: space-between;
.list-box-top-left {
display: flex;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-weight: 400;
color: #999;
align-items: center;
}
.list-box-top-right {
display: flex;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #666;
align-items: center;
.box {
display: inline-block;
width: 14px;
height: 14px;
background: #51d351;
border-radius: 50%;
}
}
}
// .content-bottom {
// margin-top: -10px;
// margin-left: 38px;
// font-size: 12px;
p {
padding: 0;
margin: 0;
}
.text {
margin: 5px 0;
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
color: #666;
text-align: left;
}
.lzgd {
display: flex;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 400;
color: #666;
text-align: left;
align-items: center;
}
}
// }
}
}
::v-deep(.el-steps) {
.el-step__head {
.el-step__line {
background-color: var(--el-color-info-light-7);
}
.el-step__icon {
width: 30px !important;
}
&.is-finish {
margin-top: 5px;
color: #f2f2f2 !important;
border-color: #f2f2f2 !important;
.el-step__line {
background-color: var(--el-color-primary);
}
}
}
.el-step:not(:last-child) {
.el-step__head {
&.is-process::before {
position: absolute;
top: calc(50% - 1px);
right: 0;
z-index: 1;
display: block;
width: 50%;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
}
}
}
::v-deep .el-step__icon.is-text {
// margin-top: -20px;
color: #666 !important;
border-color: #409eff !important;
}
.font {
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<div class="article">
<div
class="w-[calc(100%-80px)] h-110px flex items-center bg-#f9f9f9 b-1px b-solid b-#e9e9e9 pl-40px pr-40px"
>
<div>
<div class="h-36px flex items-center">
<div class="font-wryh font-700 text-20px c-#666666 mr-15px">
{{ data.htbt }}
</div>
<div class="flex mr-10px" v-if="data.sd && data.sd == '1'">
<Icon icon="solar:lock-linear"></Icon>
</div>
<div
class="w-52px h-20px bg-#0099ff rounded-2xl c-white flex justify-center items-center text-12px mr-10px"
v-if="data.zd && data.zd == '1'"
>
置顶
</div>
<div
class="w-52px h-20px bg-#ff7a8c rounded-2xl c-white flex justify-center items-center text-12px"
v-if="data.rm && data.rm == '1'"
>
热门
</div>
</div>
<div class="h-38.4px flex items-center">
<el-avatar :size="20" :src="data.cytx" />
<span class="font-wryh ml-10px text-14px c-#999999">{{ data.$create_user }}</span>
<span class="font-wryh ml-10px text-14px c-#999999 flex items-center">
<Icon icon="mingcute:time-line" :size="17"></Icon>
{{ data.create_time }}
</span>
</div>
</div>
<div class="ml-auto">
<el-button type="primary" plain style="width: 80px; height: 35px" @click="bjBtn">
<Icon icon="fa6-solid:pen" :size="15" class="mr-2px"></Icon>
编辑
</el-button>
</div>
</div>
<div class="w-[calc(100%-80px)] pl-40px pr-40px b-1px b-t-0 b-solid b-#e9e9e9">
<div class="pt-30px" v-html="vHtml" style="line-height: 32px"> </div>
<div class="w-100% text-right c-#999 font-wryh"> 最后编辑时间2020-11-24 10:00:00 </div>
<div class="mt-50px">
<div class="flex items-center mb-30px">
<div class="w-6px h-18px bg-#409EFF mr-8px"></div>
<span class="font-wryh font-700 c-#666666"> 发布评论 </span>
</div>
<div class="flex h-60px mb-10px bottom">
<el-input type="text" placeholder="请输入批阅内容" v-model="inputVal" />
<el-button type="primary" @click="fbBtn">发布</el-button>
</div>
</div>
<div>
<div>
<div class="flex items-center mt-40px mb-20px">
<div class="w-6px h-18px bg-#409EFF mr-8px"></div>
<span class="font-wryh font-700 c-#666666 mr-3px"> 评论记录 </span>
<span class="font-400 c-#666666"> 10 </span>
</div>
</div>
<el-alert v-for="item in defineData" :key="item.id" type="info" :closable="false">
<template #title>
<el-avatar :size="28" class="mr-5px" :src="item.imgUrl" />
<span class="c-#666666 mr-10px">{{ item.name }}</span>
<span class="c-#999999 text-12px">{{ item.time }}</span>
</template>
<template #default>
<span class="c-#999999">{{ item.content }}</span>
<div class="like flex">
<Icon
icon="iconamoon:like-bold"
class="mr-2px cursor-pointer"
:class="item.isLike ? 'c-red' : ''"
@click="likeClick(item.id)"
></Icon>
{{ item.like }}
</div>
</template>
</el-alert>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
interface Data {
htbt: string
create_time: string
$create_user: string
cytx: string
rm: string
sd: string
zd: string
}
const prop = defineProps<{
data: Data
hideFun: Function
}>()
const data = prop.data
const bjBtn = () => {
prop.hideFun!()
}
const vHtml = ref(
'<div id="u90044_text" class="text "> <p><span>近日在阿里云2020年云栖大会上阿里云智能网络产品研究员祝顺民重磅发布了多款网络新品其中之一就是应用负载均衡ALB。ALB产品定位应用层高级负载具备超强性能、安全可靠、面向云原生、即开即用等优势价值提供弹性自动伸缩、QUIC协议支持、基于内容的高级路由、自带DDoS安全防护、云原生应用、弹性灵活计费等产品能力满足越来越多元化的应用层负载需求。</span></p><br><p><span>说到负载均衡很多朋友或许都会想到经典负载均衡阿里云负载均衡SLB发布近十年为各行各业的用户提供强大稳定的负载分担能力解决大并发流量负载分担消除单点故障提高业务可用性。但随着企业和互联网业务高速发展业务形态和需求不断变化诸多业务场景已经无法单纯的用传统负载均衡来满足全部需求。在大互联网业务、电商大促、音视频、移动互联网应用、游戏业务、金融服务、云原生开发应用等场景中存在大量高性能、弹性、多协议七层转发、安全、云原生等需求急需新产品设计来满足。</span></p><p><span><br></span></p> </div>'
)
const defineData = ref([
{
id: 1,
imgUrl:
'https://oss.yckxt.com/chatgpt/upload/1/2024-11-06/1/f0ee8a3c7c9638a54940382568c9dpng_5.png',
name: '赵小刚',
time: '2019-03-23 22:31',
content: '非常不错的分享,对我们的工作很有参考价值。',
like: 100,
isLike: false
},
{
id: 2,
imgUrl:
'https://oss.yckxt.com/chatgpt/upload/1/2024-11-06/1/f0ee8a3c7c9638a54940382568c9dpng_5.png',
name: '赵小刚',
time: '2019-03-23 22:31',
content: '非常不错的分享,对我们的工作很有参考价值。',
like: 100,
isLike: false
},
{
id: 3,
imgUrl:
'https://oss.yckxt.com/chatgpt/upload/1/2024-11-06/1/f0ee8a3c7c9638a54940382568c9dpng_5.png',
name: '赵小刚',
time: '2019-03-23 22:31',
content: '非常不错的分享,对我们的工作很有参考价值。',
like: 100,
isLike: false
}
])
let likeIdList = ref<number[]>([])
const inputVal = ref('')
let dataLength = ref(0)
const fbBtn = () => {
if (!inputVal.value) return
dataLength.value = defineData.value?.length || 0
let endVal = defineData.value![dataLength.value - 1]
let id = endVal.id + 1
defineData.value![dataLength.value] = {
id: id,
imgUrl:
'https://oss.yckxt.com/chatgpt/upload/1/2024-11-06/1/f0ee8a3c7c9638a54940382568c9dpng_5.png',
name: '赵小刚',
time: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
content: inputVal.value,
like: 0,
isLike: false
}
inputVal.value = ''
}
const likeClick = (id) => {
defineData.value = defineData.value.map((item) => {
if (id == item.id) {
let index = likeIdList.value.indexOf(id)
if (index == -1) {
likeIdList.value.push(item.id)
return {
...item,
like: item.like + 1,
isLike: true
}
} else {
likeIdList.value.splice(index, 1)
return {
...item,
like: item.like - 1,
isLike: false
}
}
}
return item
})
}
</script>
<style lang="scss" scoped>
.article {
.font-wryh {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
}
.bottom {
::v-deep(.el-input) {
border: 1px solid rgb(233 233 233 / 100%);
.el-input__wrapper {
border-radius: 0;
box-shadow: none;
}
&:hover {
border-color: #409eff;
}
}
.el-button {
width: 80px;
height: 60px;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
border-radius: 0;
}
}
::v-deep(.el-alert) {
height: 90px;
padding: 0 20px;
margin-bottom: 20px;
background-color: rgb(249 249 249 / 49.8%);
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 0;
.el-alert__content {
height: 100%;
align-items: baseline;
.el-alert__title {
display: flex;
height: 50px;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
align-items: center;
}
.el-alert__description {
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
.like {
position: absolute;
top: 50%;
right: 20px;
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 12px;
color: #999;
transform: translateY(-50%);
}
}
.el-alert__close-btn {
font-size: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="w-80% m-auto search">
<div class="bg-white h-60px p-20px mt-10px flex items-center top b-1px b-solid b-#e9e9e9">
<div class="mr-15px">
<Icon icon="flowbite:messages-solid" :size="52" class="c-#999999"></Icon>
</div>
<div class="">
<div class="h-24px c-#666666"> 欢迎来到内部交流论坛 </div>
<div class="h-24px c-#999999 text-12px font-wryh flex items-center">
你可以自由选择你感兴趣的话题
</div>
</div>
<div class="ml-auto flex top-right">
<el-input v-model="inputValue" placeholder="输入搜索关键字" class="mr-20px h-35px" />
<Icon icon="ic:outline-search" :size="22" class="cursor-pointer search"></Icon>
<el-button type="primary" style="width: 100px; height: 35px" @click="inputVisible = true">
<Icon icon="vaadin:plus" :size="18"></Icon>
创建话题
</el-button>
</div>
</div>
<div class="bg-white b-1px b-solid b-#E9E9E9 w-100%">
<div
class="h-70px flex items-center pl-20px pr-20px"
style="border-bottom: 1px solid #e9e9e9"
>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="最新话题" name="first"></el-tab-pane>
<el-tab-pane label="热门话题" name="second"></el-tab-pane>
</el-tabs>
<div class="ml-auto text-14px c-#999999" style="font-family: '微软雅黑', sans-serif">
总贴数量1000
</div>
</div>
<div class="w-[calc(100%-60px)] pl-30px pr-30px mt-20px">
<LowTable
:ref="(el) => (tabRef = el)"
tableId="1854716968965484545"
:enhanceData="tabEnhanceData"
>
</LowTable>
</div>
</div>
<el-dialog
v-model="inputVisible"
title="创建话题"
width="800"
:before-close="dialogHandleClose"
destroy-on-close
>
<FormView
formType="add"
handleType="returnData"
showType="view"
:showButton="true"
:enhanceData="{ updateDialog, type: 'add' }"
formId="1854442042421391362"
></FormView>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { FormView, LowTable } from '@/components/LowDesign/index'
import type { TabsPaneContext } from 'element-plus'
const inputValue = ref('')
const inputVisible = ref(false)
const updateDialog = () => {
inputVisible.value = false
tabRef.value.crudRef.refreshChange()
}
const dialogHandleClose = (done: () => void) => {
done()
}
const tabEnhanceData = ref({
type: 'view'
})
const tabRef = ref()
const activeName = ref('first')
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
</script>
<style lang="scss" scoped>
.search {
.border-style {
border: 1px solid rgb(233 233 233 / 100%);
}
.font-wryh {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
}
.top {
margin-bottom: 20px;
::v-deep(.el-input) {
width: 250px;
.el-input__wrapper {
// color: rgb(204, 204, 204);
border-radius: 200px;
&:hover {
box-shadow: 0 0 0 1px var(--el-input-focus-border-color) inset;
}
}
}
.top-right {
position: relative;
.search {
position: absolute;
top: 50%;
left: 220px;
color: #999;
transform: translateY(-50%);
&:hover {
color: #409eff !important;
}
}
}
}
::v-deep(.el-tabs) {
.el-tabs__header {
margin: 0;
.el-tabs__nav-wrap {
&::after {
height: 0;
}
.el-tabs__nav-scroll {
.el-tabs__item {
width: 100px;
height: 70px;
padding: 0;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: 700;
color: #666;
&.is-active {
color: rgb(64 158 255);
}
}
.el-tabs__active-bar {
height: 1px;
}
}
}
}
}
.c-group {
::v-deep(.el-radio-button) {
.el-radio-button__inner {
width: 100px;
height: 30px;
border-width: 0;
}
&.is-active {
.el-radio-button__inner {
border-radius: 50px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div class="riendshipForum-box">
<div class="bg-white h-60px p-20px mt-10px flex items-center top b-1px b-solid b-#e9e9e9">
<div class="mr-15px">
<Icon icon="flowbite:messages-solid" :size="52" class="c-#999999"></Icon>
</div>
<div class="">
<div class="h-24px c-#666666"> 欢迎来到内部交流论坛 </div>
<div class="h-24px c-#999999 text-12px font-wryh flex items-center">
你可以自由选择你感兴趣的话题
</div>
</div>
<div class="ml-auto flex top-right">
<el-input v-model="inputValue" placeholder="输入搜索关键字" class="mr-20px h-35px" />
<Icon
icon="ic:outline-search"
:size="22"
class="cursor-pointer search"
></Icon>
<el-button type="primary" style="width: 100px; height: 35px" @click="inputVisible = true">
<Icon icon="vaadin:plus" :size="18"></Icon>
创建话题
</el-button>
</div>
</div>
<div class="bg-white mt-20px b-1px b-solid b-#e9e9e9">
<div
class="h-70px flex items-center pl-20px pr-20px"
style="border-bottom: 1px solid #e9e9e9"
>
<div
class="text-18px font-700 h-19px flex items-center c-#666666 b-l-6px b-r-0 b-t-0 b-b-0 b-#409EFF b-solid pl-10px font-wryh"
>
讨论板块
</div>
<div class="ml-auto text-14px c-#999999" style="font-family: '微软雅黑', sans-serif">
总贴数量1000
</div>
</div>
<div>
<LowTable tableId="1854408557228961794" :enhanceData="tabEnhanceData"> </LowTable>
</div>
</div>
<el-dialog
v-model="inputVisible"
title="创建话题"
width="800"
:before-close="dialogHandleClose"
destroy-on-close
>
<FormView
formType="add"
handleType="returnData"
showType="view"
:showButton="true"
:enhanceData="{ updateDialog }"
formId="1854442042421391362"
></FormView>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { LowTable } from '@/components/LowDesign/index'
defineOptions({ name: 'RiendshipForum' })
const inputValue = ref('')
const tabEnhanceData = ref({
type: 'view'
})
const inputVisible = ref(false)
const updateDialog = () => {
inputVisible.value = false
}
const dialogHandleClose = (done: () => void) => {
done()
}
</script>
<style lang="scss" scoped>
.riendshipForum-box {
width: 1200px;
min-height: 500px;
margin: 0 auto;
// border: 1px solid rgb(233 233 233 / 100%);
// box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
.font-wryh {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
}
.top {
::v-deep(.el-input) {
width: 250px;
.el-input__wrapper {
// color: rgb(204, 204, 204);
border-radius: 200px;
&:hover {
box-shadow: 0 0 0 1px var(--el-input-focus-border-color) inset;
}
}
}
.top-right {
position: relative;
.search {
position: absolute;
top: 50%;
left: 220px;
color: #999;
transform: translateY(-50%);
&:hover {
color: #409eff !important;
}
}
}
}
}
::v-deep(.el-overlay) {
.el-dialog {
border-radius: 5px;
.el-dialog__header {
padding: 18px 13px 18px 15px;
background-color: #f5f5f5;
border-radius: 5px 5px 0 0;
.el-dialog__title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-weight: bold;
color: #666;
}
.el-dialog__headerbtn {
height: 60px;
}
}
}
.el-drawer {
background-color: rgb(240 242 245 / 100%);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
<template>
<div>
<el-card class="card-box">
<template #header>
<div class="card-header">
<span>员工管理</span>
</div>
</template>
<div class="flex" style="min-height: 450px">
<div class="flex-basis-200px flex-shrink-0">
<avue-tree
ref="tree"
:option="option"
:data="data"
v-model="form"
@node-expand="nodeExpand"
@node-contextmenu="nodeContextmenu"
@node-click="nodeClick"
>
</avue-tree>
</div>
<div class="flex-1" style="max-width: calc(100% - 220px)">
<LowTable tableId="1846121741006647298" :enhanceData="{ type: 'view' }"> </LowTable>
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { LowTable } from '@/components/LowDesign/index'
defineOptions({ name: 'StaffManagement' })
import { ElMessage } from 'element-plus'
const form = ref({})
const data = ref([
{
value: 0,
label: '一级部门',
children: [
{ value: 1, label: '一级部门1' },
{ value: 2, label: '一级部门2' },
{ value: 3, label: '一级部门3' },
{ value: 4, label: '一级部门4' }
]
},
{
value: 3,
label: '二级部门',
children: [
{ value: 4, label: '二级部门1' },
{ value: 5, label: '二级部门2' }
]
}
])
const option = ref({
defaultExpandAll: true,
filter: false,
formOption: {
labelWidth: 100,
column: [
{ label: '自定义项', prop: 'label' },
{ label: '测试', prop: 'test' }
]
}
})
const tree = ref()
onMounted(() => {
tree.value.setCurrentKey(0)
})
const nodeContextmenu = (data) => {
ElMessage.success(JSON.stringify(data))
}
const nodeExpand = (data) => {
ElMessage.success(JSON.stringify(data))
}
const nodeClick = (data) => {
ElMessage.success(JSON.stringify(data))
}
</script>
<style lang="scss" scoped>
::v-deep(.el-card) {
&.card-box {
border-radius: 10px;
}
.el-card__header {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 15px;
font-weight: 700;
color: #666;
background-color: #f9f9f9;
// .el-card__body { }
}
}
::v-deep(.el-tree) {
.el-tree-node.is-current > .el-tree-node__content {
background-color: white;
}
.el-tree-node__content {
height: 40px;
.el-tree-node__expand-icon {
font-size: 16px;
}
.el-tree-node__label {
font-family: '微软雅黑', sans-serif;
}
}
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div class="userManagement-box">
<div class="b-#F0F2F5 b-2px b-solid rounded-xl">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="staffViewTopData.defaultData"
:showButton="false"
:formId="staffViewTopData.formId"
></FormView>
</div>
<div class="mt-20px pb-20px b-#F0F2F5 b-2px b-solid" style="border-radius: 5px">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<template v-for="item in tabsList" :key="item.labelKey">
<el-tab-pane :label="item.label" :name="item.labelKey">
<template v-if="item.formId">
<div style="padding: 5px 18px">
<FormView
:formType="item.formType ? item.formType : 'view'"
handleType="returnData"
showType="view"
:defaultData="item.defaultData"
:showButton="true"
:formId="item.formId"
></FormView>
</div>
</template>
<template v-else-if="item.tableId">
<div style="padding: 5px 18px">
<LowTable :tableId="item.tableId" :enhanceData="item.defaultData"> </LowTable>
</div>
</template>
<template v-else-if="!item.formId && !item.tableId">
<div style="padding: 30px 48px">
<div class="mb-26px text-14px">
<span
class="font-bold"
style="font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif"
>数据权限</span
>
<span>设置该角色的用户可以操作的数据的范围</span>
</div>
<div>
<el-radio-group v-model="radio" class="grid-cols-1" style="display: grid">
<el-radio :value="Ritem.value" v-for="Ritem in radioList" :key="Ritem.value">
<template #default>
<span class="inline-block w-170px text-14px">{{ Ritem.title }}</span>
<span class="text-13px c-#999999">{{ Ritem.introduce }} </span>
</template>
</el-radio>
</el-radio-group>
</div>
<div class="flex justify-center mt-80px">
<el-button type="primary" style="width: 140px; height: 40px" @click="sjxqCick">
保存
</el-button>
</div>
</div>
</template>
</el-tab-pane>
</template>
</el-tabs>
</div>
</div>
</template>
<script lang="ts" setup>
import { FormView, LowTable } from '@/components/LowDesign/index'
import type { TabsPaneContext } from 'element-plus'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'UserManagement' })
const props = defineProps({
data: {
// 自定义的翻译文件
type: Object,
default: () => {}
},
controlView: {
type: Object
}
})
const controlView = ref(props.controlView)
const staffViewTopDefaultData = ref(props.data)
staffViewTopDefaultData.value = {
...staffViewTopDefaultData.value,
avatar_img:
'http://oss.yckxt.com/chatgpt/upload/1/2024-10-14/1/f0ee8a3c7c9638a54940382568c9dpng.png'
}
const staffViewTopData = ref({
formId: '1846457022071132161',
defaultData: staffViewTopDefaultData.value
})
const radio = ref(1)
interface TableList {
labelKey: string
label: string
formId?: string
tableId?: string
defaultData: object
formType?: 'add' | 'edit' | 'view'
showButton?: string
}
let tabsList = ref<TableList[]>([
{
labelKey: 'dlrz',
label: '登录日志',
tableId: '1846734665412726786',
defaultData: {
type: 'view'
}
},
{
labelKey: 'czjl',
label: '操作记录',
tableId: '1846747363458351106',
defaultData: {
type: 'view'
}
},
{
labelKey: 'czqx',
label: '操作权限',
formId: '1846489623477571585',
defaultData: {
ref: controlView.value
},
formType: 'edit',
showButton: 'true'
},
{
labelKey: 'sjqx',
label: '数据权限',
defaultData: {}
},
{
labelKey: 'zdqx',
label: '字段权限',
formId: '1846758342212648962',
defaultData: {
ref: controlView.value
},
formType: 'edit',
showButton: 'true'
}
])
const activeName = ref('dlrz')
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
const radioList = ref([
{ title: '个人', introduce: '只能操作自己和下属的数据', value: 1 },
{ title: '所属部门', introduce: '能操作自己、下属、和自己所属部门的数据', value: 2 },
{
title: '所属部门及下属部门',
introduce: '所属部门及下属部门 能操作自己、下属和自己所属部门及其子部门的数据',
value: 3
},
{ title: '全公司', introduce: '能操作全公司的数据', value: 4 }
])
const sjxqCick = () => {
ElMessage({
message: '保存成功',
type: 'success'
})
controlView.value!.show = false
}
</script>
<style lang="scss" scoped>
::v-deep(.el-tabs) {
.el-tabs__nav-wrap {
height: 53px;
background-color: #f9f9f9;
.el-tabs__item {
height: 53px;
font-weight: bold;
line-height: 53px;
color: rgb(102 102 102);
&.is-active {
color: rgb(64 158 255);
}
}
&::after {
height: 1px;
}
}
.el-tabs__content {
.el-radio-group {
.el-radio {
height: 40px;
}
}
}
}
.left-bule-text {
margin-bottom: 20px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
border-left: 6px solid #409eff;
}
::v-deep(.el-tag) {
width: 110px;
height: 35px;
margin-right: 10px;
font-size: 13px;
color: #999;
background-color: white;
border-color: #ebeef5;
&:hover {
background-color: #f5f5f5;
}
.el-icon {
font-size: 16px;
color: #999;
&:hover {
background-color: #f5f5f5;
}
}
}
.button-new-tag {
background-color: white;
border: none;
}
::v-deep(.el-overlay) {
.el-dialog {
border-radius: 5px;
.el-dialog__header {
padding: 13px 13px 13px 15px;
background-color: #f5f5f5;
border-radius: 5px 5px 0 0;
.el-dialog__title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 13px;
font-weight: bold;
color: #666;
}
.el-dialog__headerbtn {
height: 56.8px;
}
}
.el-dialog__body {
.el-form-item {
margin-bottom: 0;
.el-checkbox-group {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
.el-checkbox {
.el-checkbox__inner {
width: 13px;
height: 13px;
}
.el-checkbox__label {
font-size: 13px;
}
}
}
}
}
.el-dialog__footer {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
.el-button {
width: 70px;
height: 28px;
font-size: 10px;
color: #999;
background-color: #f9f9f9;
}
.el-button--primary {
color: white;
background-color: #409eff;
}
}
}
}
::v-deep .el-tabs .el-tabs__nav-wrap {
height: 60px;
.el-tabs__nav {
height: 60px;
.el-tabs__item {
height: 60px;
// line-height: 60px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 32px;
color: #666;
&.is-active {
color: #409eff;
}
}
}
}
</style>

View File

@@ -0,0 +1,555 @@
<template>
<div class="taskManagement-box flex rounded-xl">
<div class="flex-shrink-0 flex-basis-180px bg-#F9F9F9 taskManagement-left">
<div class="h-50px flex items-center ml-55px mt-20px text-14px c-#999999">
<Icon icon="ion:flag-outline" :size="15" class="mr-1px"></Icon>
任务分类
</div>
<el-menu
:default-active="menuDefaultActive"
class="el-menu-vertical-demo bg-#F9F9F9"
@open="menuHandleOpen"
@close="menuHandleClose"
>
<template v-for="item in meunList" :key="item.value">
<el-menu-item :index="item.value">
<span>{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
<div class="flex-1 bg-white p-31px taskManagement-right" style="max-width: calc(100% - 180px)">
<div class="mt-10px flex items-center util">
<el-radio-group v-model="viewOrListRadio">
<el-radio-button label="看板视图" value="view" />
<el-radio-button label="列表显示" value="list" />
</el-radio-group>
<el-select v-model="selectValue" :fit-input-width="true">
<el-option
v-for="item in selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button class="ml-auto w-100px" type="primary" @click="dialogDisplay = true">
<Icon icon="oi:plus" :size="11"></Icon>
<span>创建任务</span>
</el-button>
</div>
<div v-if="viewOrListRadio == 'view'" class="w-100% grid grid-cols-4 gap-x-15px mt-30px">
<div v-for="(LItem, LIndex) in viewList" :key="LIndex">
<div class="c-#666666 text-14px mb-15px">
<span
class="font-700"
style="font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif"
>
{{ LItem.title }}
</span>
<span class="ml-5px" style="font-family: '微软雅黑', sans-serif">
<span v-if="LItem.title == '今日任务'">0/</span>{{ LItem.len }}
</span>
</div>
<div v-for="item in LItem.list" :key="item.id" class="flex card-item">
<div class="h-29px flex items-center">
<div
v-if="ViewVhecked.indexOf(LItem.title + item.id) !== -1"
class="check flex items-center justify-center"
>
<Icon
icon="fa-solid:check"
:size="11"
@click="checkedClick(LItem.title + item.id)"
></Icon>
</div>
<div v-else @click="checkedClick(LItem.title + item.id)" class="nocheck"> </div>
</div>
<div class="grid grid-rows-3 h-100% ml-10px">
<div
class="text-14px flex items-center c-#666666 cursor-pointer title-hover"
:class="ViewVhecked.indexOf(LItem.title + item.id) !== -1 ? 'line-through' : ''"
@click="titleClick(item)"
>
{{ item.title }}点击查看详情
</div>
<div class="flex text-12px items-center">
<span>
{{ item.xmmc }}
</span>
<div class="c-#CCCCCC flex items-center ml-15px">
<Icon icon="mage:message" :size="14"></Icon>
<span class="ml-3px">{{ item.lys }}</span>
</div>
<div class="c-#CCCCCC flex items-center ml-5px">
<Icon icon="line-md:link" :size="14"></Icon>
<span class="ml-3px">{{ item.ljs }}</span>
</div>
</div>
<div class="flex items-center" :class="LItem.title == '未完成' ? 'c-#F56C6C' : ''">
<Icon icon="icon-park-outline:alarm-clock" :size="14"></Icon>
<span class="text-12px ml-3px"> {{ item.jzsjTit }}</span>
</div>
</div>
<div class="flex items-center justify-center flex-1">
<el-avatar :size="24" :src="item.tx" />
</div>
</div>
</div>
</div>
<div v-else-if="viewOrListRadio == 'list'" class="mt-20px">
<LowTable
:ref="(el) => (tableListRef = el)"
tableId="1847103675560071169"
:enhanceData="{ type: 'view' }"
>
</LowTable>
</div>
</div>
</div>
<div>
<div class="add-dialog">
<el-dialog
v-model="dialogDisplay"
title="新建任务"
width="550"
:before-close="dialogHandleClose"
>
<FormView
formType="add"
:ref="(el) => (dialogRef = el)"
handleType="returnData"
showType="view"
:showButton="false"
formId="1846825131881762817"
></FormView>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogDisplay = false">取消</el-button>
<el-button type="primary" @click="dialogSuccess"> 保存 </el-button>
</div>
</template>
</el-dialog>
</div>
<div class="row-dialog">
<el-dialog
v-model="rowDialog"
title="任务详情"
width="900px"
:before-close="dialogHandleClose"
>
<FormView
formType="edit"
handleType="returnData"
showType="view"
:showButton="false"
:defaultData="rowDefaultData"
formId="1850013430948507650"
></FormView>
</el-dialog>
</div>
</div>
</template>
<script lang="ts" setup>
import { FormView, LowTable } from '@/components/LowDesign/index'
import * as TableApi from '@/api/design/table'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'TaskManagement' })
const viewOrListRadio = ref('view')
const selectValue = ref('default')
watch(
() => selectValue.value,
(val) => {
console.log(val, '111')
}
)
watch(
() => viewOrListRadio.value,
(val) => {
console.log(val, '111')
}
)
interface Record {
fj?: string
jzsj?: Date
kssj?: Date
status?: string
xmmc?: string
tx?: string
title?: string
id?: string
lys?: string
ljs?: string
jzsjTit?: string
}
interface ViewList {
title: string
list?: Record[]
len?: number
}
const viewList = ref<ViewList[]>([])
let ViewVhecked = ref<any>([])
const checkedClick = (id) => {
let index = ViewVhecked.value.indexOf(id)
if (index === -1) {
ViewVhecked.value.push(id)
} else {
ViewVhecked.value.splice(index, 1)
}
}
const meunList = ref([
{ value: '1', title: '全部任务' },
{ value: '2', title: '我创建的任务' },
{ value: '3', title: '我参与的任务' },
{ value: '4', title: '下属的任务' },
{ value: '5', title: '关注的任务' }
])
const menuDefaultActive = ref('1')
const menuHandleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const menuHandleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const selectOptions = [
{
value: 'default',
label: '按默认视图',
subItem: [
{ title: '今日任务', total: 0, completed: 0 },
{ title: '进行中', total: 0, completed: 0 },
{ title: '未完成', total: 0, completed: 0 },
{ title: '已完成', total: 0, completed: 0 }
]
},
{
value: 'degree',
label: '按紧要程度',
subItem: [
{ title: '重要', total: 0, completed: 0 },
{ title: '紧急', total: 0, completed: 0 },
{ title: '普通', total: 0, completed: 0 },
{ title: '较低', total: 0, completed: 0 }
]
},
{
value: 'endTime',
label: '按截止时间',
subItem: [
{ title: '已逾期', total: 0, completed: 0 },
{ title: '今天', total: 0, completed: 0 },
{ title: '三天内', total: 0, completed: 0 },
{ title: '七天内', total: 0, completed: 0 }
]
},
{
value: 'updateTime',
label: '按更新时间',
subItem: [
{ title: '今天', total: 0, completed: 0 },
{ title: '七天内', total: 0, completed: 0 },
{ title: '十五天', total: 0, completed: 0 },
{ title: '更远', total: 0, completed: 0 }
]
}
]
const dialogHandleClose = (done: () => void) => {
done()
}
const dialogDisplay = ref(false)
const rowDialog = ref(false)
const dialogRef = ref()
const tableListRef = ref()
const dialogSuccess = async () => {
const formRef = dialogRef.value.controlRef
const listRef = tableListRef.value.crudRef
const data = await formRef.handleSubmit(true)
if (data) {
listRef.refreshChange()
dialogDisplay.value = false
}
}
const rowDefaultData = ref({})
const titleClick = (row) => {
row.jzsj ? (row.jzsj = formatDate(row.jzsj)) : (row.jzsj = '')
row.kssj ? (row.kssj = formatDate(row.kssj)) : (row.kssj = '')
rowDefaultData.value = row
rowDialog.value = true
}
onMounted(async () => {
let { records } = await TableApi.getTableList(
'1847103675560071169',
{ pageNo: 1, pageSize: 40 },
false
)
const categorizedRecords: { [key: string]: Record[] } = {
今日任务: [],
进行中: [],
未完成: [],
已完成: []
}
records.forEach((ele) => {
const ljs = ele.fj ? (ele.fj.includes(',') ? ele.fj.split(',').length : 1) : 0
ele.ljs = ljs
ele.jzsjTit = ele.jzsj ? `${formatDate(ele.jzsj)} 截止` : ''
let keys: string[] = []
if (ele.kssj && formatDate(ele.kssj).startsWith('2024-10-03')) {
keys.push('今日任务')
}
let statusDictionary = [
{ id: 1, label: '进行中' },
{ id: 2, label: '已完成' },
{ id: 3, label: '未完成' }
]
statusDictionary.forEach((item) => {
if (ele.status == item.id) {
keys.push(item.label)
}
})
keys.forEach((item) => {
if (categorizedRecords[item]) {
categorizedRecords[item].push(ele)
if (item == '已完成') {
ViewVhecked.value.push('已完成' + ele.id)
}
}
})
})
for (const [title, list] of Object.entries(categorizedRecords)) {
viewList.value.push({
title,
list,
len: list.length
})
}
})
</script>
<style lang="scss" scoped>
.taskManagement-box {
min-height: 500px;
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
.taskManagement-left {
border: 1px solid rgb(233 233 233 / 100%);
border-right: none;
border-radius: 10px 0 0 10px;
.el-menu {
background-color: rgb(255 255 255 / 0%);
border-right: none;
.is-active {
font-weight: bold;
background-color: #e7f0fa;
border-right: 3px solid #409eff;
}
.el-menu-item {
height: 50px;
padding-left: 55px;
font-size: 14px;
}
}
}
.taskManagement-right {
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 0 10px 10px 0;
.util {
::v-deep(.el-radio-group) {
.el-radio-button {
.el-radio-button__inner {
display: flex;
width: 101px;
height: 35px;
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
color: #999;
align-items: center;
justify-content: center;
}
&.is-active {
.el-radio-button__inner {
color: white;
}
}
}
}
::v-deep(.el-select) {
width: 239px;
margin-left: 20px;
.el-select__wrapper {
min-height: 35px;
font-size: 14px;
line-height: 30px;
}
}
.el-button {
height: 35px;
font-size: 14px;
}
}
.card-item {
height: 87px;
padding: 10px;
margin-bottom: 15px;
color: #999;
border-radius: 5px;
box-shadow: 0 0 5px rgb(153 153 153 / 34.9%);
.title-hover:hover {
color: rgb(51 51 51);
}
.check,
.nocheck {
width: 14px;
height: 14px;
cursor: pointer;
border-radius: 2px;
}
.check {
color: white;
background-color: #409eff;
border: 1px solid #409eff;
border-radius: 2px;
}
.nocheck {
border: 1px solid #ccc;
&:hover {
border-color: #409eff;
}
}
}
}
}
.add-dialog {
::v-deep(.el-overlay) {
.el-dialog {
border-radius: 5px;
.el-dialog__header {
padding: 13px 13px 13px 15px;
background-color: #f5f5f5;
border-radius: 5px 5px 0 0;
.el-dialog__title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: bold;
color: #666;
}
.el-dialog__headerbtn {
height: 56.8px;
}
}
.el-dialog__body {
.el-form-item {
margin-bottom: 0;
.el-checkbox-group {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
.el-checkbox {
.el-checkbox__inner {
width: 14px;
height: 14px;
}
.el-checkbox__label {
font-size: 14px;
}
}
}
}
}
.el-dialog__footer {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
.el-button {
width: 70px;
height: 28px;
font-size: 10px;
color: #999;
background-color: #f9f9f9;
}
.el-button--primary {
color: white;
background-color: #409eff;
}
}
}
}
}
.row-dialog {
::v-deep(.el-overlay) {
.el-dialog__header {
height: 24.8px;
padding: 16px 10px 16px 16px;
font-size: 18px;
border-bottom: 1px solid rgb(240 240 240);
.el-dialog__headerbtn {
height: 58.6px;
}
}
}
}
::v-deep .el-select__placeholder {
font-family: '微软雅黑', sans-serif;
font-style: normal;
font-weight: 400;
color: #666;
text-align: left;
}
</style>
<style>
.el-select__popper {
.el-select-dropdown__item {
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
&.is-selected {
font-weight: 100;
}
}
}
</style>

View File

@@ -0,0 +1,686 @@
<template>
<div>
<div class="w-100%" v-if="isShow">
<div class="search-box" :class="{'bj':!isTable}">
<div class="mt-4 ">
<el-input
v-model="input"
style="width: 900px"
clearable
placeholder="输入搜索关键字"
>
<template #append>
<div class="search-btn" @click="searchFun(true)">
<el-icon><Search /></el-icon>
<span style="margin-left: 3px;">搜索</span>
</div>
</template>
</el-input>
</div>
<div class="fhsyy" v-if="!isTable" @click="isTable=true,is_px=false,input=''">
<el-icon><Back /></el-icon>
<span style="margin-left: 5px">返回</span>
</div>
</div>
<div class="table" v-if="isTable">
<div class="table-header">
<span></span>
<span class="c-#666666">知识分类</span>
</div>
<LowTable :ref="(el)=>(customRef = el)" :tableId="tabbleDate.tableId" :enhanceData="tabbleDate.enhanceData" > </LowTable>
</div>
<div class="ssjg" v-else>
<template v-if="listData.length">
<div class="ssjg-top" >
<div class="ssjg-top-left">
<p>为您找到相关结果约{{total}} 搜索用时 (0.23)</p>
</div>
<div class="ssjg-top-right">
<span :class="{'color':is_px}" @click="searchFun(true)">默认排序</span>
<span :class="{'color':!is_px}" @click="searchFun(false)">按时间排序</span>
</div>
</div>
<template v-if="is_show">
<div v-loading="loading">
<div class="box" v-for="item in listData" :key="item.id">
<p class="box-title acitive" @click="viewFun(item)">{{item.title}}</p>
<p class="box-text">{{item.text}}</p>
<div class="box-lls">
<el-icon size="18"><View /></el-icon>
<span style="margin: 0 8px;">浏览</span>
<span>{{item.llcs}}</span>
</div>
</div>
</div>
<div class="page">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10,20,30,40,50,100]"
:size="size"
:disabled="disabled"
background
layout="prev, pager, next,sizes "
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
</template>
<template v-else>
<div class="null" v-loading="loading">
<div>
<img src="/img/null.svg" alt=""/>
<p class="ts">未查询到相关知识内容</p>
<p>建议您修改搜索关键词重新再试</p>
</div>
</div>
</template>
</div>
</div>
<div class="list" v-else>
<div class="list-left">
<div class="title">
<span></span>
<span @click="isShow=true,isTable=true,is_show=true" class="tabs">知识分类</span>
<span @click="is_show=true" class="tabs"> > 产品知识</span>
<span @click="is_show=false" v-if="!is_show" class="tabs"> > 内容详情 </span>
</div>
<template v-if="is_show">
<div v-loading="loading">
<div class="box" v-for="item in listData" :key="item.id">
<p class="box-title acitive" @click="viewFun(item)">{{item.title}}</p>
<p class="box-text">{{item.text}}</p>
<div class="box-lls">
<el-icon size="18"><View /></el-icon>
<span style="margin: 0 8px;">浏览</span>
<span>{{item.llcs}}</span>
</div>
</div>
</div>
<div class="page">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10,50, 100, 200, 400]"
:size="size"
:disabled="disabled"
background
layout="prev, pager, next,sizes "
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<template v-else>
<div class="view" v-loading="loading">
<div class="top-box">
<p>{{objData.title}}</p>
<div class="top-box-text">
<el-icon size="16"><Clock /></el-icon>
<span style="margin: 0 12px 0 5px;">{{objData.time}}</span>
<el-icon size="16"><View /></el-icon>
<span style="margin: 0 5px;">浏览</span>
<span> {{objData.llcs}}</span>
</div>
</div>
<div class="html-text" v-html="objData.content"></div>
<div class="tips" v-if="is_fk">
<p>以上信息是否对您有帮助</p>
<div>
<el-button type="primary" style="padding: 19px 28px;" @click="is_fk=false">有帮助</el-button>
<el-button type="primary" style="padding: 19px 28px;" @click="is_fk=false" plain>没帮助</el-button>
</div>
</div>
<div class="tips" v-else>
<div class="fk">
<el-icon size="28px" color="#7ed96d" style="margin-right:10px"><CircleCheck /></el-icon>
<span>感谢您的反馈</span>
</div>
</div>
</div>
</template>
</div>
<div class="list-right">
<header class="header">
<p class="header-title">热门知识</p>
</header>
<template v-for="(item,index) in listData" :key="item.id">
<div class="rm" v-if="index < 6">
<p class="rm-title acitive" @click="viewFun(item)">{{item.title}}</p>
<p class="rm-text">{{item.text}}</p>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Search,View,Clock,CircleCheck,Back } from '@element-plus/icons-vue'
import {getTableList,updateTableData} from '@/api/design/table/index'
import type { ComponentSize } from 'element-plus'
import { formatDate } from '@/utils/formatTime'
const isShow = ref<any>(true)
const input = ref('')
const customRef = ref()
const size = ref<ComponentSize>('default')
const loading = ref(false)
const is_show = ref(true)
const is_fk = ref(true)
const isTable = ref(true)
const is_px = ref(true)
//知识分类点击
const clickFun = () =>{
isShow.value = false
initDate({ pageNo: 1 , pageSize: 10 })
}
const tabbleDate = ref({
label:'知识分类',
type:'table',
defaultData:{},
tableId:'1854438272522235905',
enhanceData: { hideHeader: 'disabled',clickFun}
})
//搜索
const searchFun = (type) =>{
isTable.value = false
is_px.value = type
initDate(type ? { pageNo: 1 , pageSize: 10,title:input.value } : { pageNo: 1 , pageSize: 10,column: "create_time",order: "asc"})
}
const listData = ref<any>([])
const pageSize = ref(10)
const total = ref()
//初始化数据
const initDate = (data) =>{
new Promise(async (resolve) => {
loading.value = true
await getTableList('1854438272522235905',data,false).then((res)=>{
listData.value = res.records || []
total.value = res.total
loading.value = false
resolve(res.records)
})
})
}
//分页
const currentPage = ref(1)
const disabled = ref(false)
const handleSizeChange = (val: number) => {
pageSize.value = val
initDate({ pageNo: 1 , pageSize: val })
}
const handleCurrentChange = (val: number) => {
initDate({ pageNo: val , pageSize: 10 })
}
//详情
const objData = ref()
const viewFun = (data) =>{
loading.value = true
isShow.value = false
is_show.value = false
data.time = formatDate(data.create_time,'YYYY-MM-DD HH:mm:ss')
data.llcs = ++data.llcs
objData.value = data
updateTableData('1854438272522235905',data)
loading.value = false
}
onMounted(()=>{
})
</script>
<style lang="scss" scoped>
.search-box{
position: relative;
display: flex;
height: 180px;
background: #FFF;
align-items: center;
justify-content: center;
::v-deep .el-input-group__append {
padding: 0;
.search-btn{
display: flex;
padding: 4px 19px;
font-family: "微软雅黑", sans-serif;
font-size: 14px;
font-weight: 400;
color: #fff;
background: #409eff;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
align-items: center;
justify-content: center;
}
}
.fhsyy{
position: absolute;
top: 10px;
left: 20px;
display: flex;
font-size: 12px;
color: #666;
align-items: center;
cursor: pointer;
}
.fhsyy:hover{
color: #409eff;
}
}
.table{
width:100%;
margin-top: 20px;
.table-header span:nth-child(1){
display: inline-block;
width: 5.5px;
height: 17px;
margin-right: 7px;
background: #409eff;
}
.table-header{
display: flex;
align-items: center;
margin: 20px 0;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 700;
line-height: 20px;
text-align: left;
}
}
.ssjg{
padding: 32px;
background: #fff;
border: 1px solid rgb(233 233 233 / 100%);
border-top: 0;
.ssjg-top{
display: flex;
padding: 6px 0;
justify-content: space-between;
align-items: center;
margin-bottom: 33px;
.ssjg-top-left{
p{
padding: 0;
margin: 0;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #999;
text-align: left;
}
}
.ssjg-top-right{
span{
display: inline-block;
padding: 5.5px 22px;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
letter-spacing: normal;
color: #666;
border-radius: 20px;
}
}
}
.null{
display: flex;
min-height: 410px;
background: #fff;
align-items: center;
justify-content: center;
p{
padding: 0;
margin: 0;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #999;
text-align: center;
}
.ts{
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif !important;
font-size: 16px !important;
font-weight: 700 !important;
color: #666 !important;
}
}
}
.list{
display: flex;
gap: 25px;
.list-left{
padding: 40px;
flex: 8;
background: #fff;
.title span:nth-child(1){
display: inline-block;
width: 5.2px;
height: 16px;
margin-right: 5px;
background: #409eff;
}
.title{
display: flex;
margin-bottom: 30px;
font-family: '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
color: #666;
align-items: center;
}
// .box{
// position: relative;
// width: 100%;
// padding-bottom: 20px ;
// border-bottom: 1px solid rgb(242 242 242 / 100%);
// p{
// padding: 0;
// margin: 0;
// }
// .box-title{
// font-family: "微软雅黑 Bold", "微软雅黑 Regular", "微软雅黑", sans-serif;
// font-size: 18px;
// font-style: normal;
// font-weight: 700;
// line-height: 30px;
// color:#666;
// cursor: pointer;
// }
// .box-text{
// font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
// font-size: 14px;
// font-style: normal;
// font-weight: 400;
// line-height: 30px;
// color: #999;
// text-align: left;
// }
// .box-lls{
// position: absolute;
// right: 50px;
// bottom: 50px;
// display: flex;
// font-family: '微软雅黑', sans-serif;
// font-size: 12px;
// font-weight: 400;
// color: #999;
// align-items: center;
// }
// }
// .page{
// display: flex;
// padding: 20px;
// justify-content: center;
// }
.view{
.top-box{
padding-bottom: 10px;
border-bottom: 1px solid rgb(242 242 242 / 100%);
p{
padding: 0;
margin: 0;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 20px;
font-weight: 700;
line-height: 36px;
color: #666;
text-transform: none;
word-wrap: break-word;
}
.top-box-text{
display: flex;
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 36px;
color: #999;
align-items: center;
}
}
.html-text{
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 28px;
color: #666;
text-align: left;
}
.tips{
display: flex;
padding: 19px;
background:#f9f9f9;
border: 1px solid rgb(233 233 233 / 100%);
justify-content: space-between;
align-items: center;
p{
padding: 0;
margin: 0;
font-family: '微软雅黑', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 36px;
color: #666;
text-align: left;
}
.fk{
display: flex;
align-items: center;
span{
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 36px;
color: #666;
text-align: left;
}
}
}
}
}
.list-right{
flex: 2;
background: #fff;
.header{
display: flex;
height: 50px;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid rgb(233 233 233 / 100%);
.header-title{
padding: 0;
margin: 0;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 20px;
color: #666;
text-align: left;
}
}
.rm{
padding: 15px 0;
margin: 0 20px;
border-bottom: 1px solid rgb(233 233 233 / 100%);
p{
padding: 0;
margin: 0;
}
.rm-title{
font-family: "微软雅黑 Bold", "微软雅黑 Regular", "微软雅黑", sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 28px;
color: #666;
text-align: left;
text-transform: none;
word-wrap: break-word;
cursor: pointer;
visibility: inherit;
}
.rm-text{
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
color: #999;
text-align: left;
text-transform: none;
word-wrap: break-word;
visibility: inherit;
}
}
}
}
.box{
position: relative;
width: 100%;
padding: 10px ;
border-bottom: 1px solid rgb(242 242 242 / 100%);
p{
padding: 0;
margin: 0;
}
.box-title{
display: inline-block;
font-family: "微软雅黑 Bold", "微软雅黑 Regular", "微软雅黑", sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 700;
line-height: 30px;
color:#666;
cursor: pointer;
}
.box-text{
font-family: MicrosoftYaHei, '微软雅黑', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 30px;
color: #999;
text-align: left;
}
.box-lls{
position: absolute;
right: 50px;
bottom: 50px;
display: flex;
font-family: '微软雅黑', sans-serif;
font-size: 12px;
font-weight: 400;
color: #999;
align-items: center;
}
}
.page{
display: flex;
padding: 20px;
justify-content: center;
}
.acitive:hover{
color: #409eff !important;
}
.tabs:hover{
color: #409eff !important;
cursor: pointer;
}
.bj{
background: #f9f9f9 !important;
border: 1px solid rgb(233 233 233 / 100%);
}
.color{
color: #fff !important;
background: #409eff;
}
::v-deep .el-card {
background-color: rgba($color: #000, $alpha: 0%) !important;
}
::v-deep .el-input__wrapper{
border-right:0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: 0 0 0 1px #409eff inset;
}
</style>

View File

@@ -0,0 +1,343 @@
<template>
<div class="TeamDetails-box rounded-xl">
<div class="flex border-box p-20px h-110px items-center">
<div class="mr-15px">
<el-avatar class="avatar" :size="60" :src="PropsData.tx" />
</div>
<div class="flex">
<div>
<div class="text-18px font-700 c-#666666 font-wryh h-28px flex items-center">
{{ PropsData.name }}
</div>
<div class="text-14px c-#999999 flex items-center w-90%" style="line-height: 28px">
{{ PropsData.tdjs }}
</div>
</div>
<div class="flex items-center">
<el-button type="primary" @click="dialogBjtd = true">
<Icon icon="fa-solid:pen" class="mr-3px"></Icon>
编辑团队
</el-button>
<el-button @click="dialogDel = true">
<Icon icon="ic:sharp-delete" class="mr-3px"></Icon>
删除团队
</el-button>
</div>
</div>
</div>
<div class="flex w-100% mt-20px items-start">
<!-- -->
<div class="flex-shrink-0 w-70% mr-10px border-box p-20px">
<div class="h-60px flex items-center">
<div
class="font-wryh font-700 c-#666666 text-14px pl-10px"
style="border-left: 6px solid #409eff"
>
数据统计
</div>
</div>
<LowTable tableId="1852234473116233730" :enhanceData="{ type: 'view' }"> </LowTable>
<div class="h-60px flex items-center">
<div
class="font-wryh font-700 c-#666666 text-14px pl-10px"
style="border-left: 6px solid #409eff"
>
团队动态
</div>
</div>
<div
v-for="(item, index) in tddtList"
:key="index"
class="flex pl-20px h-70px items-center"
style="border-bottom: 1px solid rgb(233 233 233 / 100%)"
>
<div class="mr-20px">
<el-avatar v-if="item.avatar" class="avatar" :size="35" :src="item.avatar" />
<el-avatar
v-else
class="avatar"
:size="35"
src="https://oss.yckxt.com/chatgpt/upload/1/2024-10-30/1/f0ee8a3c7c9638a54940382568c9dpng_3.png"
/>
</div>
<div style="font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif">
<div claas="h-24px flex items-center">
<span class="mr-5px"> {{ item.xm }} </span>
<template v-if="item.sjType == 0">
<span class="mr-5px c-#409EFF"> {{ item.blueText }} </span>
<span class="c-#666666"> {{ item.text }} </span>
</template>
<template v-else-if="item.sjType == 1">
<span class="mr-5px c-#666666"> {{ item.text }}</span>
<span class="c-#409EFF"> {{ item.blueText }} </span>
</template>
</div>
<div class="text-12px h-24px flex items-center c-#999999">几秒前</div>
</div>
</div>
<div class="h-100px pb-10px flex items-center justify-center ">
<el-button round class="w-120px" style="height: 35px;">查看更多</el-button>
</div>
</div>
<div class="w-25% ml-10px cylb border-box">
<el-card>
<template #header>
<div class="flex h-60px items-center justify-between pl-20px pr-20px bg-#F9F9F9">
<div class="text-16px c-#666666 font-wryh font-700"> 成员列表 </div>
<div>
<el-button-group @click="dialogDel = true">
<el-button style="width: 50px; height: 35px">
<Icon icon="formkit:left" class="c-#999999"></Icon>
</el-button>
<el-button style="width: 50px; height: 35px">
<Icon icon="formkit:right" class="c-#999999"></Icon>
</el-button>
</el-button-group>
</div>
</div>
</template>
<div
v-for="(item, index) in cylbList"
:key="index"
class="h-70px flex items-center pl-20px pr-20px"
style="border-bottom: 1px solid rgb(242 242 242 / 100%)"
>
<el-avatar class="avatar" :size="35" :src="item" />
<span class="ml-10px c-#666666 font-700 font-wryh">小明</span>
<span class="ml-10px c-#999999 font-wryh">销售总监</span>
<Icon
class="ml-auto c-#999999 cursor-pointer"
icon="mingcute:delete-line"
@click="iconClick"
></Icon>
</div>
</el-card>
</div>
</div>
</div>
<div class="dialog-bjtd">
<el-dialog v-model="dialogBjtd" title="创建团队" width="720px" :before-close="handleClose">
<FormView
formType="add"
handleType="returnData"
showType="view"
:showButton="false"
:defaultData="PropsData"
formId="1852168337334951938"
></FormView>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogBjtd = false">取消</el-button>
<el-button type="primary" @click="dialogBjtdClick"> 保存 </el-button>
</div>
</template>
</el-dialog>
</div>
<div class="dialog-czqr">
<el-dialog v-model="dialogDel" title="操作确认" width="450" :before-close="handleClose">
<div class="flex p-20px h-100px">
<div class="w-50px h-50px flex justify-center items-center">
<el-avatar
class="bg-white"
:size="27"
src="http://oss.yckxt.com/chatgpt/upload/1/2024-10-14/1/mdi--question-mark-circle-outline (1).png"
/>
</div>
<div class="mt-15px">
<div class="text-16px c-#666666 font-700 h-28px">是否确定删除数据</div>
<div class="c-#999999 h-28px">是否确定删除数据</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogDel = false">取消</el-button>
<el-button type="primary" @click="dialogDelClick"> 确定 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { FormView, LowTable } from '@/components/LowDesign/index'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'TeamDetails' })
const props = defineProps({
data: {
// 自定义的翻译文件
type: Object,
default: () => {}
}
})
const PropsData = props.data
const cylbList = ref([])
cylbList.value = PropsData.cytx.split(',')
const dialogBjtd = ref(false)
const dialogBjtdClick = () => {
dialogBjtd.value = false
ElMessage({
message: '保存成功',
type: 'success'
})
}
const handleClose = (done: () => void) => {
done()
}
const dialogDel = ref(false)
const iconClick = () => {
dialogDel.value = true
}
const dialogDelClick = () => {
dialogDel.value = false
ElMessage({
message: '删除成功',
type: 'success'
})
}
const tddtList = ref([
{
avatar:
'https://oss.yckxt.com/chatgpt/upload/1/2024-10-30/1/f0ee8a3c7c9638a54940382568c9dpng_3.png',
xm: '赵小刚',
blueText: '将 5 月日常迭代',
text: '更新至已发布状态',
fbTime: '几秒前',
sjType: 0
},
{
avatar:
'https://oss.yckxt.com/chatgpt/upload/1/2024-10-30/1/f0ee8a3c7c9638a54940382568c9dpng_3.png',
xm: '赵小刚',
blueText: '将 5 月日常迭代',
text: '更新至已发布状态',
fbTime: '几秒前',
sjType: 1
},
{
avatar: 'http://oss.yckxt.com/chatgpt/upload/1/2024-10-30/1/formkit--people.png',
xm: '赵小刚',
blueText: '将 5 月日常迭代',
text: '更新至已发布状态',
fbTime: '几秒前',
sjType: 0
},
{
avatar: '',
xm: '赵小刚',
blueText: '将 5 月日常迭代',
text: '更新至已发布状态',
fbTime: '几秒前',
sjType: 1
}
])
</script>
<style lang="scss" scoped>
.TeamDetails-box {
min-height: 500px;
.font-wryh {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
}
.border-box {
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
.avatar {
background-color: white;
}
}
.cylb {
::v-deep(.el-card) {
border: none;
border-radius: 10px;
box-shadow: none;
.el-card__header {
padding: 0;
}
.el-card__body {
padding: 0;
padding-bottom: 10px;
}
}
}
}
.dialog-bjtd,
.dialog-czqr {
::v-deep(.el-overlay) {
.el-dialog {
border-radius: 5px;
.el-dialog__header {
display: flex;
height: 50px;
padding-top: 0;
padding-bottom: 0;
background-color: rgb(245 245 245 / 100%);
border-radius: 5px 5px 0 0;
box-shadow: 0 1px 1px rgb(233 233 233 / 100%);
align-items: center;
.el-dialog__title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 700;
color: #666;
}
.el-dialog__headerbtn {
display: flex;
height: 50px;
align-items: center;
justify-content: center;
.el-icon {
color: #999;
}
}
}
.el-dialog__footer {
height: 60px;
padding: 15px 20px;
box-shadow: 0 1px 1px 0 rgb(233 233 233 / 100%) inset;
.el-button {
width: 80px;
height: 30px;
padding: 5px 15px;
font-family: MicrosoftYaHei, '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 12px;
}
}
}
}
}
.dialog-czqr {
::v-deep(.el-overlay) {
.el-dialog__body {
.el-avatar {
background-color: white;
}
}
}
}
</style>

View File

@@ -0,0 +1,394 @@
<template>
<div class="userManagement-box">
<div class="b-#F0F2F5 b-2px b-solid rounded-xl">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="userViewTopData.defaultData"
:showButton="false"
:formId="userViewTopData.formId"
></FormView>
</div>
<div class="mt-20px pb-20px b-#F0F2F5 b-2px b-solid" style="border-radius: 5px">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<template v-for="item in tabsList" :key="item.labelKey">
<el-tab-pane :label="item.label" :name="item.labelKey">
<template v-if="item.lowDesign.type == 'design' && item.lowDesign.formId">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="item.lowDesign?.defaultData"
:showButton="false"
:formId="item.lowDesign.formId"
></FormView>
</template>
<template v-if="item.lowDesign.type == 'exploit' && item.lowDesign.tableId">
<div style="padding: 5px 18px">
<LowTable
:tableId="item.lowDesign.tableId"
:enhanceData="item.lowDesign?.defaultData"
>
</LowTable>
</div>
</template>
<template v-if="item.lowDesign.type == 'designAndExploit' && item.lowDesign.formId">
<FormView
formType="view"
handleType="returnData"
showType="view"
:defaultData="item.lowDesign?.defaultData"
:showButton="false"
:formId="item.lowDesign.formId"
></FormView>
<div class="ml-28px mr-28px">
<div class="mt-22px font-700 text-14px c-#666666 left-bule-text">
<span class="pl-8px">用户标签</span>
</div>
<div class="flex">
<el-tag
v-for="tag in dynamicTags"
:key="tag"
closable
:disable-transitions="false"
@close="tagHandleClose(tag)"
>
{{ tag }}
</el-tag>
<el-button
v-if="!inputVisible"
class="button-new-tag"
@click="inputVisible = true"
>
<Icon icon="ic:twotone-plus" class="text-#409EFF"></Icon>
<span class="text-#409EFF text-14px">添加标签</span>
</el-button>
</div>
<div class="mt-38px font-700 text-14px c-#666666 left-bule-text">
<span class="pl-8px">收货地址</span>
</div>
<div>
<LowTable tableId="1846073998846337026" :enhanceData="{ type: 'view' }">
</LowTable>
</div>
</div>
</template>
</el-tab-pane>
</template>
</el-tabs>
</div>
<el-dialog
v-model="inputVisible"
title="标签设置"
width="600"
:before-close="dialogHandleClose"
destroy-on-close
>
<FormView
formType="add"
:ref="(el) => (yhxqRef = el)"
handleType="returnData"
showType="view"
:showButton="false"
formId="1845041920745218050"
></FormView>
<template #footer>
<div class="dialog-footer">
<el-button @click="inputVisible = false">取消</el-button>
<el-button type="primary" @click="dialogSuccess"> 确认 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { FormView, LowTable } from '@/components/LowDesign/index'
import type { TabsPaneContext } from 'element-plus'
defineOptions({ name: 'UserManagement' })
const props = defineProps({
data: {
// 自定义的翻译文件
type: Object,
default: () => {}
}
})
const userViewTopDefaultData = ref(props.data)
userViewTopDefaultData.value = {
...userViewTopDefaultData.value,
avatar_img:
'http://oss.yckxt.com/chatgpt/upload/1/2024-10-14/1/f0ee8a3c7c9638a54940382568c9dpng.png'
}
const userViewTopData = ref({
formId: '1845673651634458625',
defaultData: userViewTopDefaultData.value
})
let tabsList = ref([
{
labelKey: 'yhxq',
label: '用户详情',
lowDesign: {
formId: '1845757477997731842',
type: 'designAndExploit',
defaultData: {
...props.data,
cs: '220000,220100,220102',
czz: '100',
dlcs: '100',
fields_8038839: '统计信息',
fields_9105030: '用户资料',
fs: '100',
gxqm: '一个有个性的男子',
gz: '100',
id: 'id',
scht: '100',
scpj: '100',
scsp: '100',
sczt: '100',
sjhm: '18088889999',
sppj: '100',
sr: '2016-10-18',
sycjcs: '100',
thjl: '100',
xb: '1',
xhdfl: '服装、餐厨',
yhj: '100',
yqhy: '100',
zhdl: '2024-08-22 22:10:00',
zy: '学生'
}
}
},
{
labelKey: 'ddjl',
label: '订单记录',
lowDesign: {
tableId: '1844275960258019330',
type: 'exploit',
defaultData: {
type: 'view'
}
}
},
{
labelKey: 'jfjl',
label: '积分记录',
lowDesign: {
tableId: '1846095079682482177',
type: 'exploit',
defaultData: {}
}
},
{
labelKey: 'dlrz',
label: '登录日志',
lowDesign: {
tableId: '1846100518918823938',
type: 'exploit',
defaultData: {}
}
},
{
labelKey: 'czjl',
label: '操作记录',
lowDesign: {
tableId: '1846103236139950082',
type: 'exploit',
defaultData: {}
}
}
])
const activeName = ref('yhxq')
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
const dynamicTags = ref(['标签名称一', '标签名称二', '标签名称三', '标签名称四'])
const inputVisible = ref(false)
const yhxqRef = ref()
const tagHandleClose = (tag: string) => {
dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1)
}
const dialogHandleClose = (done: () => void) => {
done()
}
const dialogSuccess = async () => {
const formRef = yhxqRef.value.controlRef
const data = await formRef.handleSubmit(true)
let bq = data.$tjbq
if (bq) {
if (bq.indexOf('|') == -1) {
dynamicTags.value.push(bq)
} else {
let bqList = bq.split('|')
bqList.forEach((ele) => {
dynamicTags.value.push(ele)
})
}
}
inputVisible.value = false
}
</script>
<style lang="scss" scoped>
::v-deep(.el-tabs) {
.el-tabs__nav-wrap {
height: 53px;
background-color: #f9f9f9;
.el-tabs__item {
height: 53px;
font-weight: bold;
line-height: 53px;
color: rgb(102 102 102);
&.is-active {
color: rgb(64 158 255);
}
}
&::after {
height: 1px;
}
}
}
.left-bule-text {
margin-bottom: 20px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
border-left: 6px solid #409eff;
}
::v-deep(.el-tag) {
width: 120px;
height: 40px;
margin-right: 10px;
font-size: 14px;
color: #999;
background-color: white;
border-color: #ebeef5;
&:hover {
background-color: #f5f5f5;
}
.el-icon {
font-size: 16px;
color: #999;
&:hover {
background-color: #f5f5f5;
}
}
}
.button-new-tag {
height: 40px;
background-color: white;
border: none;
}
::v-deep(.el-overlay) {
.el-dialog {
border-radius: 5px;
.el-dialog__header {
padding: 13px 13px 13px 15px;
background-color: #f5f5f5;
border-radius: 5px 5px 0 0;
.el-dialog__title {
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: bold;
color: #666;
}
.el-dialog__headerbtn {
height: 56.8px;
}
}
.el-dialog__body {
.el-form-item {
margin-bottom: 0;
.el-checkbox-group {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
.el-checkbox {
.el-checkbox__inner {
width: 14px;
height: 14px;
}
.el-checkbox__label {
font-size: 14px;
}
}
}
}
}
.el-dialog__footer {
display: flex;
height: 60px;
padding: 10px 20px;
border-top: 1px solid #f0f0f0;
align-items: center;
justify-content: right;
.el-button {
width: 80px;
height: 30px;
font-size: 12px;
color: #999;
background-color: #f9f9f9;
}
.el-button--primary {
color: white;
background-color: #409eff;
}
}
}
}
::v-deep .el-tabs .el-tabs__nav-wrap {
height: 60px;
.el-tabs__nav {
height: 60px;
.el-tabs__item {
height: 60px;
// line-height: 60px;
font-family: '微软雅黑 Bold', '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 32px;
color: #666;
&.is-active {
color: #409eff;
}
}
}
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div>
<el-alert v-for="item in defineData" :key="item.id" type="info">
<template #title>
<el-avatar :size="28" class="mr-5px" :src="item.imgUrl" />
<span class="c-#666666 mr-10px">{{ item.name }}</span>
<span class="c-#999999 text-12px">{{ item.time }}</span>
</template>
<template #default>
<span class="c-#999999">{{ item.content }}</span>
</template>
</el-alert>
<div class="flex h-60px mb-10px bottom">
<el-input type="text" placeholder="请输入批阅内容" v-model="inputVal" />
<el-button type="primary" @click="fbBtn">发布</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { formatDate } from '@/utils/formatTime'
interface DefineData {
id: number
imgUrl: string
name: string
time: string
content: string
}
const props = defineProps<{
data?: DefineData[]
}>()
const defineData = ref(props.data)
const inputVal = ref('')
let dataLength = ref(0)
const fbBtn = () => {
if (!inputVal.value) return
dataLength.value = defineData.value?.length || 0
let endVal = defineData.value![dataLength.value - 1]
let id = endVal.id
defineData.value![dataLength.value] = {
id: id,
imgUrl:
'https://oss.yckxt.com/chatgpt/upload/1/2024-11-06/1/f0ee8a3c7c9638a54940382568c9dpng_5.png',
name: '赵小刚',
time: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
content: inputVal.value
}
inputVal.value = ''
}
</script>
<style lang="scss" scoped>
::v-deep(.el-alert) {
height: 90px;
margin-bottom: 20px;
background-color: rgb(249 249 249 / 49.8%);
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 0;
.el-alert__content {
height: 72px;
align-items: baseline;
.el-alert__title {
display: flex;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
align-items: end;
}
.el-alert__description {
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
font-size: 14px;
font-weight: 400;
}
.el-alert__close-btn {
font-size: 12px;
}
}
}
.bottom {
::v-deep(.el-input) {
border: 1px solid rgb(233 233 233 / 100%);
.el-input__wrapper {
border-radius: 0;
box-shadow: none;
}
&:hover {
border-color: #409eff;
}
}
.el-button {
width: 80px;
height: 60px;
font-family: '微软雅黑 Regular', '微软雅黑', sans-serif;
border-radius: 0;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="flex items-start">
<div class="w-30% mr-20px border-style">
<FormView
formType="edit"
handleType="returnData"
showType="view"
:defaultData="form1Data"
:showButton="false"
formId="1853701496069386242"
></FormView>
</div>
<div class="w-[calc(70%-20px)] border-style">
<FormView
formType="edit"
handleType="returnData"
showType="view"
:enhanceData="form2EData"
:defaultData="form2Data"
:showButton="false"
formId="1853994635845922818"
></FormView>
</div>
</div>
</template>
<script setup lang="ts">
import { FormView } from '@/components/LowDesign/index'
defineOptions({ name: 'WorkReportView' })
const prop = defineProps({
data: Object,
stepList: Object,
form2Data: Object
})
const form1Data = computed(() => {
return {
...prop.data,
stepList: prop.stepList
}
})
const form2EData = ref({})
</script>
<style scoped>
.border-style {
border: 1px solid rgb(233 233 233 / 100%);
border-radius: 10px;
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
}
</style>

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