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

View File

@@ -0,0 +1,347 @@
<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"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button type="danger" :size="size" @click="clearData.popup = true">
<Icon :size="14" icon="ep:delete"></Icon>
<span>清除日志</span>
</el-button>
</template>
<template #menu-before="{ row, size }">
<el-button type="danger" :size="size" link @click="openViewText(row)"> 失败原因 </el-button>
</template>
<template #contentText="{ row }">
<div>
<avue-text-ellipsis
:text="`${row.modelTitle}【${row.title}】【${row.requestMethod}】`"
:height="30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
<avue-text-ellipsis
:text="`${row.requestUri}${row.isParams ? '?' + row.requestParams : ''}`"
:height="row.isParams ? 60 : 30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
<avue-text-ellipsis
v-if="row.isData"
:text="`请求参数:${row.requestParams}`"
:height="30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
</div>
</template>
</avue-crud>
<DesignPopup
v-model="clearData.popup"
title="清除日志"
:width="400"
:is-footer="true"
:dialog-params="{}"
>
<div class="w-100% p-20px box-border">
<div class="flex items-center">
<div class="flex-basis-80px flex-shrink-0">保留时间</div>
<avue-select
v-model="clearData.clearType"
placeholder="请选择内容"
type="tree"
:dic="clearDic"
:clearable="false"
></avue-select>
</div>
<avue-date
v-show="clearData.clearType == 'custom'"
class="ml-80px mt-5px"
v-model="clearData.customValue"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
></avue-date>
</div>
<template #footer>
<el-button @click="clearData.popup = false"> </el-button>
<el-button type="primary" @click="handleClear"> </el-button>
</template>
</DesignPopup>
<DesignPopup
v-model="viewData.popup"
:title="viewData.title"
control-type="drawer"
width="80%"
:is-body-full="true"
>
<template #default>
<MonacoEditor
v-model="viewData.text"
language="java"
:editor-option="{ readOnly: true }"
></MonacoEditor>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter, formatDate } from '@/utils/formatTime'
import * as ApiLogApi from '@/api/system/apiLog'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SystemApiFailureLog' })
const message = useMessage() // 消息弹窗
const { getCurrPermi } = useCrudPermi()
const userStore = useUserStore()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 160,
labelWidth: 140,
column: {
contentText: {
label: '请求信息',
display: false
},
modelTitle: {
label: '模块名称',
overHidden: true,
hide: true
},
title: {
label: '请求名称',
hide: true
},
requestUri: {
label: '请求地址',
search: true,
hide: true
},
requestMethod: {
label: '请求方式',
hide: true
},
requestParams: {
label: '请求参数',
hide: true
},
createUserName: {
label: '请求用户',
width: 120,
search: true
},
ip: {
label: '操作IP地址',
width: 110
},
methodClass: {
label: '方法类',
hide: true
},
methodName: {
label: '方法名',
hide: true
},
time: {
label: '执行时间ms',
width: 130,
search: true,
searchLabel: '执行时间',
searchPrepend: '大于',
searchAppend: 'ms'
},
createTime: {
label: '请求时间',
type: 'datetime',
search: true,
searchType: 'datetimerange',
dataType: 'string',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 100,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
tenantId: {
label: '租户',
search: userStore.user.id === 1,
type: 'select',
dataType: 'number',
dicUrl: '/system/tenant/all',
props: { label: 'name', value: 'id' },
hide: userStore.user.id !== 1,
width: 100,
overHidden: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const clearData = ref({ popup: false, clearType: '7', customValue: '' })
const clearDic = [
{ label: '保留7天以内的数据', value: '7' },
{ label: '保留一个月以内的数据', value: '30' },
{ label: '保留三个月以内的数据', value: '90' },
{ label: '保留一年以内的数据', value: '365' },
{ label: '全部清除', value: 'all' },
{ label: '自定义', value: 'custom' }
]
const viewData = ref({ popup: false, text: '', title: '' })
const logType = 'error'
const permission = getCurrPermi(['system:login-log'])
const crudRef = ref()
useCrudHeight(crudRef)
const handleClear = async () => {
let clearTime = ''
const { clearType, customValue } = clearData.value
if (clearType == 'custom') {
if (!customValue) return message.info('请先选择保留时间')
clearTime = customValue
} else {
const date = new Date()
let time = 0
if (clearType == 'all') time = 3600 * 1000 * 24
else time = 3600 * 1000 * 24 * (Number(clearType) + 1)
date.setTime(date.getTime() - time)
clearTime = formatDate(date, 'YYYY-MM-DD')
}
clearData.value.popup = false
loading.value = true
await ApiLogApi.deleteLog(logType, clearTime).catch(() => false)
tablePage.value.currentPage = 1
await getTableData()
loading.value = false
}
const openViewText = async (row) => {
loading.value = true
const data = await ApiLogApi.getLogDetail(logType, row.id)
viewData.value.text = data.error
viewData.value.title = `失败原因(${row.requestUri}`
viewData.value.popup = true
loading.value = false
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ApiLogApi.getLogDetail(logType, tableForm.value.id)
tableForm.value.createTime = formatDate(tableForm.value.createTime, 'YYYY-MM-DD HH:mm:ss')
}
done()
}
/** 查询列表 */
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 ApiLogApi.getLogPage(logType, searchObj)
tableData.value = data.records.map((item) => {
if (
item.requestParams &&
!['{}', '[]'].includes(item.requestParams) &&
item.requestParams.length < 1000
) {
const list = item.requestParams.split('')
if (
(list[0] == '{' && list[list.length - 1] == '}') ||
(list[0] == '[' && list[list.length - 1] == ']')
) {
item.isData = true
} else item.isParams = true
}
return item
})
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()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,316 @@
<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"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button type="danger" :size="size" @click="clearData.popup = true">
<Icon :size="14" icon="ep:delete"></Icon>
<span>清除日志</span>
</el-button>
</template>
<template #contentText="{ row }">
<div>
<avue-text-ellipsis
:text="`${row.modelTitle}【${row.title}】【${row.requestMethod}】`"
:height="30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
<avue-text-ellipsis
:text="`${row.requestUri}${row.isParams ? '?' + row.requestParams : ''}`"
:height="row.isParams ? 60 : 30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
<avue-text-ellipsis
v-if="row.isData"
:text="`请求参数:${row.requestParams}`"
:height="30"
use-tooltip
placement="top"
>
<template #more>
<small>...</small>
</template>
</avue-text-ellipsis>
</div>
</template>
</avue-crud>
<DesignPopup
v-model="clearData.popup"
title="清除日志"
:width="400"
:is-footer="true"
:dialog-params="{}"
>
<div class="w-100% p-20px box-border">
<div class="flex items-center">
<div class="flex-basis-80px flex-shrink-0">保留时间</div>
<avue-select
v-model="clearData.clearType"
placeholder="请选择内容"
type="tree"
:dic="clearDic"
:clearable="false"
></avue-select>
</div>
<avue-date
v-show="clearData.clearType == 'custom'"
class="ml-80px mt-5px"
v-model="clearData.customValue"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
></avue-date>
</div>
<template #footer>
<el-button @click="clearData.popup = false"> </el-button>
<el-button type="primary" @click="handleClear"> </el-button>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter, formatDate } from '@/utils/formatTime'
import * as ApiLogApi from '@/api/system/apiLog'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'SystemApiSuccessLog' })
const message = useMessage() // 消息弹窗
const { getCurrPermi } = useCrudPermi()
const userStore = useUserStore()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
labelWidth: 140,
column: {
contentText: {
label: '请求信息',
display: false
},
modelTitle: {
label: '模块名称',
overHidden: true,
hide: true
},
title: {
label: '请求名称',
hide: true
},
requestUri: {
label: '请求地址',
search: true,
hide: true
},
requestMethod: {
label: '请求方式',
hide: true
},
requestParams: {
label: '请求参数',
hide: true
},
createUserName: {
label: '请求用户',
width: 120,
search: true
},
ip: {
label: '操作IP地址',
width: 110
},
methodClass: {
label: '方法类',
hide: true
},
methodName: {
label: '方法名',
hide: true
},
time: {
label: '执行时间ms',
width: 130,
search: true,
searchLabel: '执行时间',
searchPrepend: '大于',
searchAppend: 'ms'
},
createTime: {
label: '请求时间',
type: 'datetime',
search: true,
searchType: 'datetimerange',
dataType: 'string',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
width: 100,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
tenantId: {
label: '租户',
search: userStore.user.id === 1,
type: 'select',
dataType: 'number',
dicUrl: '/system/tenant/all',
props: { label: 'name', value: 'id' },
hide: userStore.user.id !== 1,
width: 100,
overHidden: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const clearData = ref({ popup: false, clearType: '7', customValue: '' })
const clearDic = [
{ label: '保留7天以内的数据', value: '7' },
{ label: '保留一个月以内的数据', value: '30' },
{ label: '保留三个月以内的数据', value: '90' },
{ label: '保留一年以内的数据', value: '365' },
{ label: '全部清除', value: 'all' },
{ label: '自定义', value: 'custom' }
]
const logType = 'info'
const permission = getCurrPermi(['system:login-log'])
const crudRef = ref()
useCrudHeight(crudRef)
const handleClear = async () => {
let clearTime = ''
const { clearType, customValue } = clearData.value
if (clearType == 'custom') {
if (!customValue) return message.info('请先选择保留时间')
clearTime = customValue
} else {
const date = new Date()
let time = 0
if (clearType == 'all') time = 3600 * 1000 * 24
else time = 3600 * 1000 * 24 * (Number(clearType) + 1)
date.setTime(date.getTime() - time)
clearTime = formatDate(date, 'YYYY-MM-DD')
}
clearData.value.popup = false
loading.value = true
await ApiLogApi.deleteLog(logType, clearTime).catch(() => false)
tablePage.value.currentPage = 1
await getTableData()
loading.value = false
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await ApiLogApi.getLogDetail(logType, tableForm.value.id)
tableForm.value.createTime = formatDate(tableForm.value.createTime, 'YYYY-MM-DD HH:mm:ss')
}
done()
}
/** 查询列表 */
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 ApiLogApi.getLogPage(logType, searchObj)
tableData.value = data.records.map((item) => {
if (
item.requestParams &&
!['{}', '[]'].includes(item.requestParams) &&
item.requestParams.length < 1000
) {
const list = item.requestParams.split('')
if (
(list[0] == '{' && list[list.length - 1] == '}') ||
(list[0] == '[' && list[list.length - 1] == ']')
) {
item.isData = true
} else item.isParams = true
}
return item
})
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()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,108 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-button style="margin-bottom: 20px" type="primary" @click="testPopup = true">
<Icon icon="ep:plus" class="mr-5px" /> IP 查询
</el-button>
<div style="width: 100%; height: 700px">
<!-- AutoResizer 自动调节大小 -->
<el-auto-resizer>
<template #default="{ height, width }">
<!-- Virtualized Table 虚拟化表格高性能解决表格在大数据量下的卡顿问题 -->
<el-table-v2
:columns="columns"
:data="list"
:width="width"
:height="height"
expand-column-key="id"
/>
</template>
</el-auto-resizer>
</div>
</ContentWrap>
<!-- 表单弹窗查询 -->
<DesignPopup v-model="testPopup" title="IP 查询" :is-footer="true" width="40%">
<div class="p-20px">
<avue-form
ref="textFormRef"
v-if="testPopup"
v-model="testFormData"
:option="testFormOption"
></avue-form>
</div>
<template #footer>
<el-button :loading="textLoading" type="primary" @click="testSubmitFun"> </el-button>
<el-button @click="testPopup = false"> </el-button>
</template>
</DesignPopup>
</template>
<script setup lang="tsx">
import type { Column } from 'element-plus'
import * as AreaApi from '@/api/system/area'
defineOptions({ name: 'SystemArea' })
const message = useMessage()
// 表格的 column 字段
const columns: Column[] = [
{
dataKey: 'id',
title: '编号',
width: 280,
fixed: true,
key: 'id'
},
{
dataKey: 'name',
title: '地名',
width: 200
}
]
// 表格的数据
const list = ref([])
/**
* 获得数据列表
*/
const getList = async () => {
list.value = await AreaApi.getAreaTree()
}
/** 查询操作 */
const testPopup = ref(false)
const testFormData = ref<any>({})
const testFormOption = ref({
submitBtn: false,
emptyBtn: false,
span: 24,
labelWidth: 80,
column: {
ip: { label: 'IP', rules: [{ required: true, message: 'IP不能为空', trigger: 'blur' }] },
result: { label: '地址', placeholder: '展示查询 IP 结果', readonly: true }
}
})
const textLoading = ref(false)
const textFormRef = ref()
// 提交表单
const testSubmitFun = () => {
textFormRef.value.validate(async (valid, hide) => {
if (!valid) return hide()
try {
textLoading.value = true
testFormData.value.result = await AreaApi.getAreaByIp(testFormData.value.ip)
message.success('查询成功')
} finally {
hide()
textLoading.value = false
}
})
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,343 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
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"
>
<!-- 表格 -->
<template #type="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" />
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 表单 -->
<template #parentId-form>
<el-tree-select
v-model="tableForm.parentId"
:data="deptTree"
:props="defaultProps"
@change="parentIdChange"
check-strictly
default-expand-all
placeholder="请选择上级部门"
value-key="deptId"
style="width: 366px"
/>
</template>
<template #leaderUserId-form="scope">
<UserSelect
v-model="tableForm.leaderUserId"
:column="scope.column"
:disabled="scope.disabled"
:size="scope.size"
:type="scope.type"
prop="leaderUserId"
></UserSelect>
</template>
<!-- 自定义按钮 -->
<template #menu-left="{ size }">
<el-button
plain
:size="size"
@click="defaultExpandAllShow = !defaultExpandAllShow"
type="danger"
icon="el-icon-sort"
>展开/折叠</el-button
>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import { CommonStatusEnum } from '@/utils/constants'
import { useLowStoreWithOut } from '@/store/modules/low'
defineOptions({ name: 'SystemDept' })
const crudRef = ref()
useCrudHeight(crudRef)
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const lowStore = useLowStoreWithOut()
const loading = ref(true) // 列表的加载中
const defaultExpandAllShow = ref(true) // 默认展开所有表项
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
rowKey: 'id',
rowParentKey: 'parentId',
defaultExpandAll: defaultExpandAllShow,
dialogWidth: '50%',
column: {
parentId: {
label: '上级部门',
hide: true,
minWidth: 100,
rules: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }]
},
name: {
label: '部门名称',
align: 'left',
headerAlign: 'left',
search: true,
minWidth: 100,
rules: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }]
},
fullName: {
label: '部门全称',
minWidth: 100,
display: false,
},
sort: {
label: '排序',
width: 80,
span: 12,
type: 'number',
rules: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
},
leaderUserId: {
label: '负责人',
span: 12,
minWidth: 90,
findType: 'all',
columnKey: ['mobile', 'sex', 'post', 'deptName'],
defaultSearchDept: '',
type: 'select',
dicUrl: '/system/user/simple-list',
dicFormatter: (data) => {
const dicObj = {}
data.forEach((item) => (dicObj[item.id] = item.nickname))
lowStore.setDicObj('userSelect', dicObj)
return data
},
props: {
label: 'nickname',
value: 'id'
}
},
phone: {
label: '联系电话',
width: 120,
rules: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
]
},
email: {
label: '邮箱',
minWidth: 90,
rules: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }]
},
status: {
label: '状态',
width: 80,
search: true,
type: 'select',
span: 12,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
isInternal: {
label: '是否内部单位',
width: 80,
search: true,
type: 'select',
span: 12,
rules: [{ required: true, message: '是否内部单位不能为空', trigger: 'blur' }],
dicData: [
{ label: '是', value: CommonStatusEnum.DISABLE },
{ label: '否', value: CommonStatusEnum.ENABLE }
],
// value: CommonStatusEnum.ENABLE
},
campusId: {
label: '所属园区',
width: 80,
hide: true,
type: 'select',
span: 12,
dicUrl: '/jeelowcode/dbform-data/list/1960952987557363713',
dataType: 'array',
dicMethod: 'post',
props: { label: 'campus_name', value: 'campus_id' },
dicFormatter: (data) => data.records,
rules: [{ required: true, message: '所属园区不能为空', trigger: 'blur' }],
},
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref()
const tableSearch = ref({})
const deptTree = ref() // 上级部门
const permission = getCurrPermi(['system:dept'])
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await DeptApi.getDeptPage(searchObj)
tableData.value = handleTree(data)
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
await getTree()
const id = tableForm.value.id
if (['edit', 'view'].includes(type) && id) {
tableForm.value = await DeptApi.getDept(id)
}
tableOption.column.leaderUserId.defaultSearchDept = id || ''
done()
}
// 递归查找部门树中指定ID的部门名称
const findDeptNameById = (tree, targetId) => {
for (const node of tree) {
if (node.id === targetId) {
return node.name
}
if (node.children && node.children.length > 0) {
const result = findDeptNameById(node.children, targetId)
if (result) {
return result
}
}
}
return null
}
const assembleForm = (form) => {
// 添加fullName字段
// 查找父部门名称并构建完整路径
const parentName = findDeptNameById(deptTree.value, form.parentId)
if (parentName) {
form.fullName = parentName + '-' + form.name
} else {
form.fullName = form.name
}
return form
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
// 组装表单数据添加fullName字段
const assembledForm = assembleForm(form)
let bool = await DeptApi.createDept(assembledForm).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
// 组装表单数据添加fullName字段
const assembledForm = assembleForm(form)
let bool = await DeptApi.updateDept(assembledForm).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DeptApi.deleteDept(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 获得部门树 */
const getTree = async () => {
deptTree.value = []
const data = await DeptApi.getSimpleDeptList()
let dept: Tree = { id: 0, name: '顶级部门', children: [] }
dept.children = handleTree(data)
deptTree.value.push(dept)
}
watch(
() => defaultExpandAllShow.value,
() => {
if (crudRef.value) crudRef.value?.refreshTable()
}
)
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,285 @@
<template>
<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="['system:dict:export']"
>导出</el-button
>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
</avue-crud>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as DictDataApi from '@/api/system/dict/dict.data'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemDictData' })
interface Props {
dicType: string
}
const props = defineProps<Props>()
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%',
calcHeight: 20,
column: {
dictType: {
label: '所属字典',
type: 'select',
minWidth: 90,
disabled: true,
value: props.dicType,
hide: true,
clearable: false,
filterable: true,
dicUrl: '/system/dict-type/list-all-simple',
props: { label: 'name', value: 'type' },
dicFlag: true
},
label: {
label: '标签',
search: true,
minWidth: 90,
rules: [{ required: true, message: '标签不能为空', trigger: 'blur' }]
},
value: {
label: '键值',
minWidth: 90,
rules: [{ required: true, message: '键值不能为空', trigger: 'blur' }]
},
sort: {
label: '排序',
type: 'number',
span: 12,
value: 0,
minWidth: 90,
rules: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'radio',
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
colorType: {
label: '颜色类型',
type: 'select',
span: 12,
minWidth: 90,
dicData: [
{ value: 'default', label: '默认' },
{ value: 'primary', label: '主要' },
{ value: 'success', label: '成功' },
{ value: 'info', label: '信息' },
{ value: 'warning', label: '警告' },
{ value: 'danger', label: '危险' }
]
},
cssClass: {
label: 'CSS Class'
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 150
},
createTime: {
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({
dictType: props.dicType
})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:dict'])
const crudRef = ref()
useCrudHeight(crudRef)
const init = () => {
tableData.value = []
tablePage.value.total = 0
tableOption.column.dictType.value = props.dicType
tableSearch.value.dictType = props.dicType
resetChange()
}
/** 查询列表 */
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 DictDataApi.getDictDataPage(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 DictDataApi.getDictData(tableForm.value.id)
}
if (['edit'].includes(type)) {
tableOption.column.dictType.disabled = true
} else if (['add'].includes(type)) {
tableOption.column.dictType.disabled = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await DictDataApi.createDictData(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await DictDataApi.updateDictData(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DictDataApi.deleteDictData(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 DictDataApi.exportDictData(searchObj)
download.excel(data, '字典数据.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
init()
})
defineExpose({ init })
</script>

View File

@@ -0,0 +1,263 @@
<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="['system:dict:export']"
>导出</el-button
>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button link type="primary" @click="openDicData(row)" class="ml-4px!">
<Icon :size="14" icon="ep:document"></Icon>
<span>数据</span>
</el-button>
</template>
</avue-crud>
<DesignPopup v-model="popupData.show" :title="popupData.title" width="80%" controlType="drawer">
<div class="p-20px">
<SystemDictData ref="dicDataRef" :dicType="popupData.dicType"></SystemDictData>
</div>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as DictTypeApi from '@/api/system/dict/dict.type'
import { CommonStatusEnum } from '@/utils/constants'
import SystemDictData from './data/index.vue'
defineOptions({ name: 'SystemDictType' })
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: {
name: {
label: '字典名称',
search: true,
minWidth: 120,
rules: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }]
},
type: {
label: '字典编码',
search: true,
minWidth: 120,
rules: [{ required: true, message: '字典编码不能为空', trigger: 'blur' }]
},
createTime: {
label: '创建时间',
search: true,
searchRange: 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)
}
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 120
},
status: {
label: '状态',
search: true,
type: 'radio',
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:dict'])
const popupData = ref({
show: false,
dicType: '',
title: ''
})
const crudRef = ref()
const dicDataRef = ref()
useCrudHeight(crudRef)
const openDicData = (row) => {
popupData.value.dicType = row.type
popupData.value.title = `${row.name}${row.type}`
setTimeout(() => {
if (dicDataRef.value) dicDataRef.value?.init()
popupData.value.show = true
}, 0)
}
/** 查询列表 */
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 DictTypeApi.getDictTypePage(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 DictTypeApi.getDictType(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await DictTypeApi.createDictType(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await DictTypeApi.updateDictType(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DictTypeApi.deleteDictType(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 DictTypeApi.exportDictType(searchObj)
download.excel(data, '字典类型.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,232 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
:permission="permission"
@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="['system:duty:export']"
>导出</el-button
>
</template>
<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 } from '@/utils/formatTime'
import download from '@/utils/download'
import * as DutyApi from '@/api/system/duty'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemDuty' })
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%',
column: {
id: {
label: '职务编号',
display: false,
width: 90
},
name: {
label: '职务名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '职务名称不能为空', trigger: 'blur' }]
},
sort: {
label: '职务顺序',
width: 90,
rules: [{ required: true, message: '职务顺序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
remark: {
label: '职务备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 150,
span: 24
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:duty'])
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 DutyApi.getDutyPage(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 DutyApi.getDuty(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await DutyApi.createDuty(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await DutyApi.updateDuty(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await DutyApi.deleteDuty(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 DutyApi.exportDuty(searchObj)
download.excel(data, '职务列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,239 @@
<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="['system:error-code:export']"
>导出</el-button
>
</template>
<template #type="scope">
<dict-tag
v-if="scope.row.type !== undefined"
:type="DICT_TYPE.SYSTEM_ERROR_CODE_TYPE"
:value="scope.row.type"
/>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as ErrorCodeApi from '@/api/system/errorCode'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 100,
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
type: {
label: '类型',
type: 'select',
width: 100,
search: true,
display: false,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_ERROR_CODE_TYPE)
},
applicationName: {
label: '应用名',
search: true,
minWidth: 100,
rules: [{ required: true, message: '应用名不能为空', trigger: 'blur' }]
},
code: {
label: '错误码编码',
search: true,
minWidth: 100,
rules: [{ required: true, message: '错误码编码不能为空', trigger: 'blur' }]
},
message: {
label: '错误码提示',
search: true,
minWidth: 140,
rules: [{ required: true, message: '错误码提示不能为空', trigger: 'blur' }]
},
memo: {
label: '备注',
minWidth: 140
},
createTime: {
label: '创建时间',
search: true,
searchRange: 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(['system:error-code'])
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 ErrorCodeApi.getErrorCodePage(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 ErrorCodeApi.getErrorCode(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await ErrorCodeApi.createErrorCode(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ErrorCodeApi.updateErrorCode(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ErrorCodeApi.deleteErrorCode(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 ErrorCodeApi.excelErrorCode(searchObj)
download.excel(data, '错误码列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,211 @@
<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 #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:login-log:export']"
>导出</el-button
>
</template>
<!-- 表格 -->
<template #logType="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_LOGIN_TYPE"
:value="scope.row.logType != undefined ? scope.row.logType : ''"
/>
</template>
<template #result="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_LOGIN_RESULT"
:value="scope.row.result != undefined ? scope.row.result : ''"
/>
</template>
<!-- 表单 -->
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as LoginLogApi from '@/api/system/loginLog'
defineOptions({ name: 'SystemLoginLog' })
const message = useMessage() // 消息弹窗
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
logType: {
label: '操作类型',
width: 100,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_LOGIN_TYPE),
value: ''
},
username: {
label: '用户名称'
},
userIp: {
label: '登录地址',
width: 140
},
userAgent: {
label: '浏览器',
width: 400,
type: 'textarea',
minRows: 2,
maxRows: 4,
overHidden: true
},
searchCreateTime: {
label: '登录日期',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
createTime: {
label: '登录日期',
type: 'datetime',
width: 160,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
result: {
label: '登陆结果',
width: 90,
search: true,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_LOGIN_RESULT),
value: ''
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:login-log'])
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)
}
delete searchObj.searchCreateTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await LoginLogApi.getLoginLogPage(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 = { ...tableSearch.value }
if (searchObj.searchCreateTime?.length) {
searchObj.createTime = getSearchDate(searchObj.searchCreateTime)
}
delete searchObj.searchCreateTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await LoginLogApi.exportLoginLog(searchObj)
download.excel(data, '登录日志.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,220 @@
<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 #sslEnable="scope">
<template v-if="scope.row.sslEnable && scope.row.sslEnable === true">
<el-tag type="success"></el-tag>
</template>
<template v-else>
<el-tag type="danger"></el-tag>
</template>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import * as MailAccountApi from '@/api/system/mail/account'
defineOptions({ name: 'SystemMailAccount' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: 'el-icon-view',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 140,
span: 12,
dialogWidth: '50%',
overHidden: true,
column: {
mail: {
label: '邮箱',
search: true,
minWidth: 100,
rules: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
]
},
username: {
label: '用户名',
search: true,
minWidth: 100,
rules: [{ required: true, message: '用户名不能为空', trigger: 'blur' }]
},
password: {
label: '密码',
hide: true,
rules: [{ required: true, message: '密码不能为空', trigger: 'blur' }]
},
host: {
label: 'SMTP 服务器域名',
minWidth: 100,
rules: [{ required: true, message: 'SMTP 服务器域名不能为空', trigger: 'blur' }]
},
port: {
label: 'SMTP 服务器端口',
span: 12,
type: 'number',
value: 465,
width: 90,
rules: [{ required: true, message: 'SMTP 服务器端口不能为空', trigger: 'blur' }]
},
sslEnable: {
label: '是否开启 SSL',
type: 'radio',
width: 90,
span: 12,
dicData: [
{ label: '是', value: true },
{ label: '否', value: false }
],
rules: [{ required: true, message: '是否开启 SSL不能为空', trigger: 'blur' }],
value: true
},
createTime: {
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 permission = getCurrPermi(['system:mail-account'])
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 MailAccountApi.getMailAccountPage(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 MailAccountApi.getMailAccount(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await MailAccountApi.createMailAccount(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await MailAccountApi.updateMailAccount(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await MailAccountApi.deleteMailAccount(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,260 @@
<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"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 表格 -->
<template #sendStatus="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS"
:value="scope.row.sendStatus != undefined ? scope.row.sendStatus : ''"
/>
</template>
<template #receiveStatus="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS"
:value="scope.row.receiveStatus != undefined ? scope.row.receiveStatus : ''"
/>
</template>
<!-- 表单 -->
<template #templateParams-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
{{ JSON.stringify(value) }}
</div>
</template>
<template #sendStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #receiveStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #templateContent-form="{ value }">
<div v-html="value"></div>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as MailLogApi from '@/api/system/mail/log'
defineOptions({ name: 'SystemMailLog' })
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewTitle: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 160,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
id: {
label: '编号',
width: 80
},
searchSendTime: {
label: '发送时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
sendTime: {
label: '发送时间',
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
toMail: {
label: '接收邮箱',
minWidth: 100
},
userId: {
label: '用户编号',
search: true,
minWidth: 100,
hide: true
},
templateTitle: {
label: '邮件标题',
overHidden: true,
minWidth: 100
},
templateContent: {
label: '邮件内容',
hide: true
},
templateParams: {
label: '邮箱参数',
hide: true
},
sendStatus: {
label: '发送状态',
search: true,
width: 100,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS),
value: ''
},
accountId: {
label: '邮箱账号',
search: true,
type: 'select',
hide: true,
dicUrl: '/system/mail-account/simple-list',
props: { label: 'mail', value: 'id' },
dicFlag: true
},
fromMail: {
label: '发送邮箱地址',
overHidden: true
},
templateId: {
label: '模板编号',
minWidth: 90,
search: true
},
templateCode: {
label: '模板编码',
hide: true
},
templateNickname: {
label: '模版发送人名称',
hide: true
},
sendMessageId: {
label: '发送返回的消息编号',
hide: true
},
sendException: {
label: '发送异常',
hide: true
},
createTime: {
label: '创建时间',
searchRange: true,
type: 'datetime',
hide: true,
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(['system:mail-log'])
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.searchSendTime?.length) {
searchObj.sendTime = getSearchDate(searchObj.searchSendTime)
}
delete searchObj.searchSendTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await MailLogApi.getMailLogPage(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 (['view'].includes(type) && tableForm.value.id) {
tableForm.value = await MailLogApi.getMailLog(tableForm.value.id)
}
done()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,380 @@
<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 #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
v-if="row.status === 0"
class="ml-4px!"
link
type="primary"
@click="openSendForm(row.id)"
v-hasPermi="['system:mail-template:send-mail']"
>
<Icon :size="14" icon="ep:tickets"></Icon>
<span>测试</span>
</el-button>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗测试发送 -->
<DesignPopup
v-model="testPopup"
title="发送邮箱模板测试"
:is-footer="true"
width="40%"
class="mail-template-popup"
>
<div class="p-20px">
<avue-form
ref="textFormRef"
v-if="testPopup"
v-model="testFormData"
:option="testFormOption"
></avue-form>
</div>
<template #footer>
<el-button :loading="textLoading" type="primary" @click="testSubmitFun"> </el-button>
<el-button @click="testPopup = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as MailTemplateApi from '@/api/system/mail/template'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemMailTemplate' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 140,
labelSuffix: ' ',
labelWidth: 160,
span: 24,
dialogWidth: '70%',
overHidden: true,
column: {
code: {
label: '模板编码',
search: true,
minWidth: 100,
span: 12,
rules: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }]
},
name: {
label: '模板名称',
search: true,
minWidth: 100,
span: 12,
rules: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
},
title: {
label: '模板标题',
minWidth: 100,
overHidden: true
},
content: {
label: '模板内容',
minWidth: 100,
type: 'ueditor',
rules: [
{
validator: (rule, value, callback) => {
if (!value || !value.replace(/<[\s\S]*?>|[' ']/g, '')) {
callback(new Error('模板内容不能为空'))
} else callback()
},
required: true,
trigger: 'blur'
}
]
},
accountId: {
label: '邮箱账号',
minWidth: 120,
search: true,
type: 'select',
span: 12,
dicUrl: '/system/mail-account/simple-list',
props: { label: 'mail', value: 'id' },
dicFlag: true,
rules: [{ required: true, message: '邮箱账号不能为空', trigger: 'blur' }]
},
nickname: {
label: '发送人名称',
minWidth: 100,
span: 12
},
status: {
label: '开启状态',
search: true,
type: 'select',
span: 12,
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
minWidth: 130
},
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<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['system:mail-template'])
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 MailTemplateApi.getMailTemplatePage(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 MailTemplateApi.getMailTemplate(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await MailTemplateApi.createMailTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await MailTemplateApi.updateMailTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await MailTemplateApi.deleteMailTemplate(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 发送短信按钮 */
const testPopup = ref(false)
const testFormData = ref<any>({})
const testFormOption = ref({
submitBtn: false,
emptyBtn: false,
span: 24,
labelWidth: 120,
column: {}
})
const textLoading = ref(false)
const textFormRef = ref()
// 打开测试表单
const openSendForm = async (id: number) => {
loading.value = true
const formDatas = await MailTemplateApi.getMailTemplate(id)
testFormData.value = {
content: formDatas.content,
params: formDatas.params,
templateCode: formDatas.code,
templateParams: {}
}
testFormOption.value.column = {
content: {
label: '模板内容',
type: 'ueditor',
disabled: true,
className: 'mail-template-ueditor',
customConfig: {
toolbarKeys: []
}
},
mail: {
label: '收件邮箱',
rules: [
{ required: true, message: '收件邮箱不能为空', trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
]
}
}
// 设置动态参数
if (formDatas.params?.length) {
formDatas.params.forEach((item) => {
testFormData.value.templateParams[item] = ''
testFormOption.value.column[item] = {
label: `参数 ${item}`,
value: '',
bind: `templateParams.${item}`,
rules: [{ required: true, message: `参数 ${item} 不能为空`, trigger: 'blur' }]
}
})
}
loading.value = false
testPopup.value = true
}
// 提交表单
const testSubmitFun = () => {
textFormRef.value.validate(async (valid, hide) => {
if (!valid) return hide()
try {
textLoading.value = true
const logId = await MailTemplateApi.sendMail({
mail: testFormData.value.mail,
templateCode: testFormData.value.templateCode,
templateParams: testFormData.value.templateParams
})
if (logId) {
message.success('提交发送成功!发送结果见邮件记录,编号:' + logId)
}
testPopup.value = false
} finally {
hide()
textLoading.value = false
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.mail-template-popup {
.el-form-item__label {
height: auto;
min-height: 32px;
overflow-wrap: anywhere;
}
}
.mail-template-ueditor {
.avue-ueditor {
div:nth-child(2) {
height: 200px !important;
}
}
}
</style>

View File

@@ -0,0 +1,732 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
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"
>
<!-- 表格 -->
<template #icon-form="{ column, size, disabled }">
<IconSelectInput
v-model="tableForm.icon"
:prop="column.prop"
:column="column"
:size="size"
:disabled="disabled"
/>
</template>
<template #icon="{ row }">
<Icon v-if="row.icon" :size="14" :icon="row.icon" />
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 表单 -->
<template #parentId-form>
<el-tree-select
v-model="tableForm.parentId"
:data="deptTree"
:props="defaultProps"
check-strictly
placeholder="请选择上级菜单"
value-key="deptId"
style="width: 366px"
/>
</template>
<template #nameI18n-form>
<I18nInput v-model="tableForm.nameI18n" :def-name="tableForm.name"></I18nInput>
</template>
<!-- 自定义按钮 -->
<template #menu-left>
<el-button plain @click="expandAll = !expandAll" type="danger" icon="el-icon-sort"
>展开/折叠</el-button
>
<el-button plain @click="refreshMenu">
<Icon class="mr-5px" icon="ep:refresh" />
刷新菜单缓存
</el-button>
</template>
<template #menu="{ row, size }">
<el-button
v-if="row.type === 1"
icon="el-icon-circle-plus"
:size="size"
text
type="primary"
v-hasPermi="['system:menu:create']"
@click="openForm(row)"
>新增子级</el-button
>
<el-button v-if="row.type === 2" :size="size" text type="primary" @click="openBtnView(row)"
>按钮管理</el-button
>
</template>
</avue-crud>
<DesignPopup
v-model="btnDialog"
:title="`【${currRow['name']}】按钮管理`"
width="70%"
:isBodyFull="true"
>
<template #default>
<div class="h-100% p-20px box-border">
<avue-crud
ref="btnCrudRef"
v-model="tableForm"
:table-loading="btnLoading"
:data="btnTableData"
:option="btnTableOption"
:permission="permission"
:before-open="beforeOpen"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="getBtnTableData"
@selection-change="btnSelectionChange"
>
<template #menu-left="scope">
<el-button
plain
:size="scope.size"
@click="autoCreatedBtn"
v-if="designOnlyVal.val"
v-hasPermi="['system:menu:create']"
>
<Icon class="mr-2px" icon="ep:refresh" />
<span>根据配置创建</span>
</el-button>
<el-button
plain
:size="scope.size"
:loading="selectLoading"
@click="selectLowTable"
v-else
v-hasPermi="['system:menu:create']"
>
<Icon class="mr-2px" icon="ep:refresh" />
<span>选择表单开发|报表设计创建</span>
</el-button>
<el-button
v-if="permission['delBtn']"
type="danger"
:size="scope.size"
:disabled="btnTableSelect.length == 0"
@click="btnBatchDel"
>
<Icon :size="14" icon="fluent:delete-16-regular" class="mr-3px" />批量删除
</el-button>
</template>
</avue-crud>
</div>
</template>
</DesignPopup>
</ContentWrap>
</template>
<script lang="ts" setup>
import { ElMessageBox, ElSelect, ElOption } from 'element-plus'
import { I18nInput } from '@/components/LowDesign/src/shareControl/index'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps, handleTree } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu'
import * as TableApi from '@/api/design/table'
import * as ReportApi from '@/api/design/report'
import { CommonStatusEnum } from '@/utils/constants'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'SystemMenu' })
const { wsCache } = useCache()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const expandAll = ref(false) // 默认展开所有表项
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
menuWidth: 260,
labelSuffix: ' ',
labelWidth: 100,
span: 24,
rowKey: 'id',
rowParentKey: 'parentId',
defaultExpandAll: expandAll,
virtualize: true,
dialogWidth: '50%',
column: {
parentId: {
label: '上级菜单',
hide: true,
rules: [{ required: true, message: '上级菜单不能为空', trigger: 'blur' }]
},
name: {
label: '菜单名称',
headerAlign: 'left',
align: 'left',
search: true,
minWidth: 120,
rules: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }]
},
nameI18n: { label: '国际化配置', hide: true },
type: {
label: '菜单类型',
type: 'radio',
button: true,
hide: true,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE).filter((item) => item.value !== 3),
value: 1,
control: (val) => {
return {
icon: { display: val !== 3 },
path: { display: val !== 3 },
visible: { display: val !== 3 },
alwaysShow: { display: val !== 3 },
component: { display: val === 2 },
keepAlive: { display: val === 2 },
componentName: { display: val === 2 },
permission: { display: val !== 1 }
}
}
},
sort: {
label: '排序',
span: 12,
type: 'number',
width: 60,
rules: [{ required: true, message: '排序不能为空', trigger: 'blur' }]
},
icon: {
label: '菜单图标',
width: 85,
span: 12,
display: false
},
path: {
label: '路由地址',
hide: false,
display: false,
overHidden: true,
labelTip: '如:`user`【顶级菜单下要以 / 开头】`/user`。外网地址以 `http(s)://` 开头',
labelTipPlacement: 'right-end',
rules: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }]
},
permission: {
label: '权限标识',
display: false,
minWidth: 90,
labelTip:
'Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission("system:user:list")`)',
labelTipPlacement: 'right-end'
},
component: {
label: '组件路径',
placeholder: '例如system/user/index',
overHidden: true,
display: false,
minWidth: 90,
prepend: 'src/views/'
},
componentName: {
label: '组件名称',
placeholder: '例如SystemUser',
display: false,
hide: true,
minWidth: 90
},
status: {
label: '菜单状态',
width: 85,
search: true,
type: 'radio',
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '菜单状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
visible: {
label: '显示状态',
type: 'radio',
border: true,
display: false,
hide: true,
dicData: [
{ label: '显示', value: true },
{ label: '隐藏', value: false }
],
value: true,
labelTip: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问',
labelTipPlacement: 'right-end'
},
alwaysShow: {
label: '总是显示',
type: 'radio',
border: true,
display: false,
hide: true,
dicData: [
{ label: '总是', value: true },
{ label: '不是', value: false }
],
value: true,
labelTip: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
labelTipPlacement: 'right-end'
},
keepAlive: {
label: '缓存状态',
type: 'radio',
border: true,
display: false,
hide: true,
dicData: [
{ label: '缓存', value: true },
{ label: '不缓存', value: false }
],
value: true,
labelTip: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
labelTipPlacement: 'right-end'
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref()
const tableSearch = ref({})
const deptTree = ref() // 上级部门
const btnDialog = ref(false)
const btnLoading = ref(false)
const currRow = ref({})
const btnTableData = ref([])
const btnTableSelect = ref([])
const btnTableOption = reactive({
dialogWidth: '50%',
calcHeight: 200,
selection: true,
reserveSelection: true,
column: {
name: {
label: '按钮名称',
row: true,
rules: [{ required: true, message: '按钮名称不能为空', trigger: 'blur' }],
span: 24
},
permission: {
...tableOption.column.permission,
span: 24,
display: true,
rules: [{ required: true, message: '权限标识不能为空', trigger: 'blur' }]
},
sort: { ...tableOption.column.sort, span: 24 },
status: {
label: '按钮状态',
type: 'radio',
span: 24,
width: 100,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '按钮状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
}
}
})
const permission = getCurrPermi(['system:menu'])
const crudRef = ref()
const btnCrudRef = ref()
const designOnlyVal = computed(() => {
let val = ''
let type = ''
const path = currRow.value['path'].split('?')[0] || ''
if (path.indexOf('table/view/') === 0) {
const pathList = path.split('/')
const len = pathList.length - 1
if (/\d$/.test(pathList[len])) {
val = pathList[len]
type = 'table'
}
} else if (path.indexOf('report/view/') === 0) {
const pathList = path.split('/')
const len = pathList.length - 1
if (pathList[len]) {
val = pathList[len]
type = 'report'
}
}
return { val, type }
})
useCrudHeight([crudRef, btnCrudRef])
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
btnDisplay: 1
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await MenuApi.getMenuList(searchObj)
tableData.value = handleTree(data)
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const searchChange = (params, done) => {
getTableData().finally(() => {
done()
})
}
/** 清空按钮操作 */
const resetChange = () => {
searchChange({}, () => {})
}
/** 表单打开前 */
const beforeOpen = async (done, type) => {
await getTree()
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await MenuApi.getMenu(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
if (btnDialog.value) {
form.parentId = currRow.value['id']
form.type = 3
}
let bool = await MenuApi.createMenu(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
if (btnDialog.value) getBtnTableData()
else resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await MenuApi.updateMenu(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
if (btnDialog.value) getBtnTableData()
else getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await MenuApi.deleteMenu(form.id ? [form.id] : form.ids, form.type)
message.success(t('common.delSuccess'))
// 刷新列表
if (btnDialog.value) getBtnTableData()
else getTableData()
} catch {}
}
/** 添加/修改操作 */
const openForm = (form) => {
tableForm.value.parentId = form.id
crudRef.value.rowAdd()
}
/** 刷新菜单缓存按钮操作 */
const refreshMenu = async () => {
try {
await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存')
// 清空,从而触发刷新
wsCache.delete(CACHE_KEY.USER)
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
// 刷新浏览器
location.reload()
} catch {}
}
/** 获得部门树 */
const getTree = async () => {
deptTree.value = []
const data = await MenuApi.getSimpleMenusList()
let dept: Tree = { id: 0, name: '顶级菜单', children: [] }
dept.children = handleTree(data)
deptTree.value.push(dept)
}
const openBtnView = (row) => {
currRow.value = row
btnDialog.value = true
getBtnTableData()
}
const btnSelectionChange = (data) => {
btnTableSelect.value = data
}
const btnBatchDel = () => {
const ids = btnTableSelect.value.map((item) => item['id'])
rowDel({ ids, type: '3' })
}
const getBtnTableData = async () => {
btnLoading.value = true
let searchObj = {
parentId: currRow.value['id']
}
try {
const data = await MenuApi.getMenuList(searchObj)
btnTableData.value = data
btnTableData.value = data
} finally {
btnLoading.value = false
}
}
const autoCreatedBtn = async () => {
let createData: any = {}
const type = designOnlyVal.value.type
const defaultMenu = { sort: 1, status: 0, type: 3, parentId: currRow.value['id'] }
if (type == 'table') {
createData = await createLowTable(defaultMenu, designOnlyVal.value.val)
} else if (type == 'report') {
createData = await createLowReport(defaultMenu, designOnlyVal.value.val)
}
const len = createData.apiData.length
if (len) {
await MenuApi.batchCreateMenu(createData.apiData)
getBtnTableData()
} else btnLoading.value = false
const msg = len
? `已创建 <span style="color:#409EFF">${len}</span> 条数据`
: `未发现需要创建的按钮数据`
const tip = `<div style="font-size:12px;display:${createData.tipData?.length ? 'block' : 'none'}">
发现不存在的按钮: ${createData.tipData?.join('、')}
<span style="color:#E6A23C">(注:如不需要请自行删除)</span>
</div>`
message.alert(
`<div>
<div>${msg}</div>
${tip}
</div>`,
'提示',
{ dangerouslyUseHTMLString: true }
)
}
const createLowTable = (
defaultMenu,
onlyVal,
isMsg = true
): Promise<{ apiData: object[]; tipData: string[]; type: string }> => {
return new Promise(async (resolve) => {
if (isMsg) await message.confirm('是否确定根据表单开发配置创建按钮数据?')
btnLoading.value = true
const data = await TableApi.getWebConfig(onlyVal)
const permissionKey = 'jeelowcode:dbform-data'
const defaultBtnObj = {
query: { name: '数据查询', permission: 'query' },
addBtn: { name: '数据创建', permission: 'create' },
editBtn: { name: '数据更新', permission: 'update' },
delBtn: { name: '数据删除', permission: 'delete' },
exportBtn: { name: '数据导出', permission: 'export' },
importBtn: { name: '数据导入', permission: 'import' }
}
const btnData: Array<any> = []
const basicBtn = data.dbForm.basicFunction.split(',')
for (const key in defaultBtnObj) {
const { name, permission } = defaultBtnObj[key]
const btnName = defaultMenu.postfix ? `${name}[${data.dbForm.tableDescribe}]` : name
if (key == 'query' || basicBtn.includes(key))
btnData.push({
...defaultMenu,
name: btnName,
permission: `${permissionKey}:${permission}:${onlyVal}`
})
}
data.buttonList.forEach((item) => {
if (item.buttonShow == 'Y' && item.buttonAuth == 'Y') {
const btnName = defaultMenu.postfix
? `${item.buttonName}[${data.dbForm.tableDescribe}]`
: item.buttonName
btnData.push({
...defaultMenu,
name: btnName,
permission: `${permissionKey}:${item.buttonCode}:${onlyVal}`
})
}
})
const btnName = btnData.map((item) => item.name)
const tipName: string[] = []
const nameList: string[] = btnTableData.value.map((item) => {
if (!btnName.includes(item['name'])) tipName.push(item['name'])
return item['name']
})
const apiData = btnData.filter((item) => !nameList.includes(item.name))
resolve({ apiData, tipData: tipName, type: 'table' })
})
}
const createLowReport = (
defaultMenu,
onlyVal,
isMsg = true
): Promise<{ apiData: object[]; type: string }> => {
return new Promise(async (resolve) => {
if (isMsg) await message.confirm('是否确定根据报表配置创建按钮数据?')
const permissionKey = 'jeelowcode:report-data'
const defaultData = [
{ name: '数据查询', permission: `${permissionKey}:query:${onlyVal}` },
{ name: '数据导出', permission: `${permissionKey}:export:${onlyVal}` }
]
const keys: string[] = btnTableData.value.map((item) => item['permission'])
const apiData: any = []
defaultData.forEach((item) => {
if (!keys.includes(item.permission)) {
const btnName = defaultMenu.postfix
? `${item.name}[${reportObj.value[onlyVal]}]`
: item.name
apiData.push({ ...defaultMenu, ...item, name: btnName })
}
})
resolve({ apiData, type: 'report' })
})
}
const table_ids = ref<number[]>([])
const report_ids = ref<string[]>([])
const reportObj = ref({})
const selectLoading = ref(false)
const selectLowTable = async () => {
selectLoading.value = true
table_ids.value = []
report_ids.value = []
const tableList = await TableApi.getAllDbDicData({ onlyTableName: 'Y' })
const reportRes = await ReportApi.getDbList({})
selectLoading.value = false
ElMessageBox({
title: '请选择页面使用的表单开发|报表设计',
message: () =>
h('div', { class: 'flex flex-col mt-10px mb-5px' }, [
h('div', { class: 'flex items-center' }, [
h('div', '表单开发:'),
h(
ElSelect,
{
modelValue: table_ids.value,
'onUpdate:modelValue': (val: number[]) => {
table_ids.value = val
},
class: 'my-0!',
multiple: true,
filterable: true,
style: { width: '300px', margin: '20px 0px' }
},
() =>
tableList.map((item) =>
h(ElOption, {
label: `${item.tableDescribe}[${item.tableId}]`,
value: item.tableId
})
)
)
]),
h('div', { class: 'flex items-center mt-10px' }, [
h('div', '报表设计:'),
h(
ElSelect,
{
modelValue: report_ids.value,
'onUpdate:modelValue': (val: string[]) => {
report_ids.value = val
},
class: 'my-0!',
multiple: true,
filterable: true,
style: { width: '300px', margin: '20px 0px' }
},
() =>
reportRes.records.map((item) => {
reportObj.value[item.reportCode] = item.reportName
return h(ElOption, {
label: item.reportName,
value: item.reportCode
})
})
)
])
]),
showCancelButton: true,
closeOnClickModal: false,
beforeClose: async (action, instance, done) => {
if (action == 'confirm') {
instance.confirmButtonLoading = true
const apiList: any[] = []
const promiseArr: Promise<{ apiData: object[]; type: string }>[] = []
const defaultMenu = {
sort: 1,
status: 0,
type: 3,
parentId: currRow.value['id'],
postfix: true
}
table_ids.value.forEach((tableId) => {
promiseArr.push(createLowTable(cloneDeep(defaultMenu), tableId, false))
})
report_ids.value.forEach((reportId) => {
promiseArr.push(createLowReport(cloneDeep(defaultMenu), reportId, false))
})
const resList = await Promise.all(promiseArr)
resList.forEach((res) => apiList.push(...res.apiData))
const len = apiList.length
if (len) {
await MenuApi.batchCreateMenu(apiList)
getBtnTableData()
}
instance.confirmButtonLoading = false
const msg = len
? `已创建 <span style="color:#409EFF">${len}</span> 条数据`
: `未发现需要创建的按钮数据`
message.alert(`<div>${msg}</div>`, '提示', { dangerouslyUseHTMLString: true })
}
done()
}
})
}
watch(expandAll, () => {
crudRef.value.refreshTable()
})
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,238 @@
<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 #type="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTICE_TYPE"
:value="scope.row.type ? scope.row.type : ''"
/>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
v-if="row.status !== 1"
class="ml-4px!"
link
@click="handlePush(row.id)"
type="primary"
>
<Icon :size="14" icon="ep:document"></Icon>
<span>推送</span>
</el-button>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as NoticeApi from '@/api/system/notice'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemDictType' })
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: '70%',
column: {
title: {
label: '公告标题',
search: true,
minWidth: 100,
rules: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }]
},
content: {
label: '公告内容',
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (!value || !value.replace(/<[\s\S]*?>|[' ']/g, '')) {
callback(new Error('公告内容不能为空'))
} else callback()
},
required: true,
trigger: 'blur'
}
],
hide: true,
type: 'ueditor'
},
type: {
label: '公告类型',
type: 'select',
span: 12,
width: 90,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE),
rules: [{ required: true, message: '公告类型不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
span: 12,
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
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(['system:notice'])
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 NoticeApi.getNoticePage(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 NoticeApi.getNotice(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await NoticeApi.createNotice(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await NoticeApi.updateNotice(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await NoticeApi.deleteNotice(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 推送按钮操作 */
const handlePush = async (id: number) => {
try {
// 推送的二次确认
await message.confirm('是否推送所选中通知?')
// 发起推送
await NoticeApi.pushNotice(id)
message.success(t('推送成功'))
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,231 @@
<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"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<!-- 表格 -->
<template #readStatus="scope">
<dict-tag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.readStatus != undefined ? scope.row.readStatus : ''"
/>
</template>
<template #templateType="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="scope.row.templateType != undefined ? scope.row.templateType : ''"
/>
</template>
<!-- 表单 -->
<template #templateParams-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">{{ JSON.stringify(value) }}</div>
</template>
<template #readStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #templateType-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="value != undefined ? value : ''"
/>
</div>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message'
defineOptions({ name: 'SystemNotifyMessage' })
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 160,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
id: {
label: '编号',
width: 80
},
userId: {
label: '用户编号',
search: true,
minWidth: 90
},
templateId: {
label: '模板编号',
minWidth: 90,
hide: true
},
templateCode: {
label: '模板编码',
minWidth: 90,
search: true
},
templateNickname: {
label: '发送人名称',
minWidth: 100
},
templateContent: {
label: '模版内容',
width: 200,
overHidden: true
},
templateParams: {
label: '模版参数',
width: 200,
overHidden: true
},
templateType: {
label: '模版类型',
search: true,
type: 'select',
minWidth: 100,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE),
value: ''
},
readStatus: {
label: '是否已读',
type: 'select',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING),
value: ''
},
readTime: {
label: '阅读时间',
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
searchCreateTime: {
label: '创建时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
createTime: {
label: '创建时间',
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(['system:notify-message'])
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)
}
delete searchObj.searchCreateTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await NotifyMessageApi.getNotifyMessagePage(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 (['view'].includes(type) && tableForm.value.id) {
// tableForm.value = await NotifyMessageApi.getDictType(tableForm.value.id)
}
done()
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,248 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
@selection-change="selectionChange"
@search-change="searchChange"
@search-reset="resetChange"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button @click="handleUpdateList" :size="size" :disabled="!tableSelect.length">
<Icon icon="ep:reading" class="mr-5px" /> 设置已读
</el-button>
<el-button @click="handleUpdateAll" :size="size">
<Icon icon="ep:reading" class="mr-5px" /> 全部已读
</el-button>
</template>
<!-- 表格 -->
<template #readStatus="scope">
<dict-tag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="scope.row.readStatus != undefined ? scope.row.readStatus : ''"
/>
</template>
<template #templateType="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="scope.row.templateType != undefined ? scope.row.templateType : ''"
/>
</template>
<!-- 表单 -->
<template #readStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #templateType-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="value != undefined ? value : ''"
/>
</div>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message'
defineOptions({ name: 'SystemNotifyMessage' })
const message = useMessage()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '查看',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 160,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
selection: true,
reserveSelection: true,
column: {
templateNickname: {
label: '发送人',
width: 140
},
searchCreateTime: {
label: '发送时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
createTime: {
label: '发送时间',
searchRange: true,
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
templateType: {
label: '类型',
width: 120,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)
},
templateContent: {
label: '消息内容',
type: 'textarea',
overHidden: true
},
readStatus: {
label: '是否已读',
type: 'select',
width: 90,
search: true,
dicData: getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)
},
readTime: {
label: '阅读时间',
type: 'datetime',
width: 180,
viewDisplay: true,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
}
},
selectable: (row) => {
return !row.readStatus
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableSelect = ref<any[]>([])
const crudRef = ref()
useCrudHeight(crudRef)
/** 标记一条站内信已读 */
const handleReadOne = async (id) => {
loading.value = true
await NotifyMessageApi.updateNotifyMessageRead(id)
await getTableData()
}
/** 标记全部站内信已读 **/
const handleUpdateAll = async () => {
loading.value = true
await NotifyMessageApi.updateAllNotifyMessageRead()
message.success('全部已读成功!')
crudRef.value.clearSelection()
await getTableData()
}
/** 标记一些站内信已读 **/
const handleUpdateList = async () => {
if (tableSelect.value.length === 0) {
return
}
const ids = tableSelect.value.map((item) => item.id)
await NotifyMessageApi.updateNotifyMessageRead(ids)
message.success('批量已读成功!')
crudRef.value.clearSelection()
await getTableData()
}
/** 查询列表 */
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)
}
delete searchObj.searchCreateTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await NotifyMessageApi.getMyNotifyMessagePage(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 (['view'].includes(type) && !tableForm.value.readStatus) {
handleReadOne(tableForm.value.id)
}
tableOption.column.readTime.viewDisplay = tableForm.value.readStatus
done()
}
const selectionChange = (data) => {
tableSelect.value = data
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,346 @@
<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 #type="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE"
:value="scope.row.type ? scope.row.type : ''"
/>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
v-if="row.status === 0"
class="ml-4px!"
v-hasPermi="['system:notify-template:send-notify']"
link
@click="openSendForm(row)"
type="primary"
>
<Icon :size="14" icon="ep:document"></Icon>
<span>测试</span>
</el-button>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗测试发送 -->
<DesignPopup
v-model="testPopup"
title="发送站内信模板测试"
:is-footer="true"
width="40%"
class="notify-template-popup"
>
<div class="p-20px">
<avue-form ref="textFormRef" v-if="testPopup" v-model="testFormData" :option="testFormOption">
<template #userId="{ column, disabled }">
<UserSelect
v-model="testFormData.userId"
:column="column"
:disabled="disabled"
type="edit"
prop="userId"
></UserSelect>
</template>
</avue-form>
</div>
<template #footer>
<el-button :loading="textLoading" type="primary" @click="testSubmitFun"> </el-button>
<el-button @click="testPopup = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as NotifyTemplateApi from '@/api/system/notify/template'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'NotifySmsTemplate' })
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: ' ',
labelWidth: 100,
span: 24,
dialogWidth: '55%',
overHidden: true,
searchLabelWidth: 90,
column: {
code: {
label: '模板编码',
minWidth: 90,
span: 12,
rules: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }]
},
name: {
label: '模板名称',
search: true,
span: 12,
minWidth: 90,
rules: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
},
nickname: {
label: '发送人名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '发送人名称不能为空', trigger: 'blur' }]
},
content: {
label: '模板内容',
minWidth: 100,
rules: [{ required: true, message: '模板内容不能为空', trigger: 'blur' }],
type: 'textarea',
minRows: 2,
maxRows: 4
},
type: {
label: '类型',
type: 'select',
span: 12,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE),
rules: [{ required: true, message: '类型不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'radio',
span: 12,
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
type: 'textarea',
minWidth: 130,
minRows: 2,
maxRows: 4
},
createTime: {
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 permission = getCurrPermi(['system:notify-template'])
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 NotifyTemplateApi.getNotifyTemplatePage(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 NotifyTemplateApi.getNotifyTemplate(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await NotifyTemplateApi.createNotifyTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await NotifyTemplateApi.updateNotifyTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await NotifyTemplateApi.deleteNotifyTemplate(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 发送站内信按钮 */
// 测试表单配置
const testPopup = ref(false)
const testFormData = ref<any>({})
const testFormOption = ref({
submitBtn: false,
emptyBtn: false,
span: 24,
labelWidth: 120,
column: {}
})
const textLoading = ref(false)
const textFormRef = ref()
// 打开测试表单
const openSendForm = async (row: NotifyTemplateApi.NotifyTemplateVO) => {
loading.value = true
const formDatas = await NotifyTemplateApi.getNotifyTemplate(row.id || 0)
testFormData.value = {
content: formDatas.content,
params: formDatas.params,
templateCode: formDatas.code,
templateParams: {}
}
testFormOption.value.column = {
content: { label: '模板内容', type: 'textarea', disabled: true },
userId: {
label: '接收人',
findType: 'all',
multiple: true,
columnKey: ['sex', 'post', 'deptName'],
rules: [{ required: true, message: `接收人不能为空`, trigger: 'change' }]
}
}
// 设置动态参数
if (formDatas.params?.length) {
formDatas.params.forEach((item) => {
testFormData.value.templateParams[item] = ''
testFormOption.value.column[item] = {
label: `参数 ${item} `,
value: '',
bind: `templateParams.${item}`,
rules: [{ required: true, message: `参数 ${item} 不能为空`, trigger: 'blur' }]
}
})
}
loading.value = false
testPopup.value = true
}
// 提交表单
const testSubmitFun = () => {
textFormRef.value.validate(async (valid, hide) => {
if (!valid) return hide()
try {
textLoading.value = true
await NotifyTemplateApi.sendNotify({
userIdList: testFormData.value.userId.split(','),
userType: 2,
templateCode: testFormData.value.templateCode,
templateParams: testFormData.value.templateParams
})
testPopup.value = false
} finally {
hide()
textLoading.value = false
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.notify-template-popup {
.el-form-item__label {
height: auto;
min-height: 32px;
overflow-wrap: anywhere;
}
}
</style>

View File

@@ -0,0 +1,333 @@
<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 #authorizedGrantTypes="scope">
<el-tag
:disable-transitions="true"
:key="index"
v-for="(authorizedGrantType, index) in scope.row.authorizedGrantTypes"
:index="index"
class="mr-5px"
>
{{ authorizedGrantType }}
</el-tag>
</template>
<!-- 表单 -->
<template #scopes-form="scope">
<el-select
v-model="tableForm.scopes"
filterable
multiple
allow-create
placeholder="请输入授权范围"
style="width: 100%"
>
<el-option v-for="item in scope.value" :key="item" :label="item" :value="item" />
</el-select>
</template>
<template #redirectUris-form="scope">
<el-select
v-model="tableForm.redirectUris"
filterable
multiple
allow-create
placeholder="请输入可重定向的 URI 地址"
style="width: 100%"
>
<el-option v-for="item in scope.value" :key="item" :label="item" :value="item" />
</el-select>
</template>
<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, getDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { dateFormatter } from '@/utils/formatTime'
import * as ClientApi from '@/api/system/oauth2/client'
defineOptions({ name: 'SystemOAuth2Client' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
labelWidth: 160,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '80%',
menuWidth: 160,
column: {
clientId: {
label: '客户端编号',
minWidth: 120,
span: 12,
rules: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }]
},
secret: {
label: '客户端密钥',
minWidth: 100,
span: 12,
rules: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }]
},
name: {
label: '应用名',
search: true,
minWidth: 100,
rules: [{ required: true, message: '应用名不能为空', trigger: 'blur' }]
},
logo: {
label: '应用图标',
span: 24,
minWidth: 90,
fileType: 'img',
listType: 'picture-img',
type: 'upload',
action: '/infra/file/jeelowcode/upload',
data: { updateSupport: 0 },
propsHttp: { url: 'fileUrl' }
},
description: {
label: '应用描述',
type: 'textarea',
minRows: 2,
maxRows: 4,
hide: true
},
status: {
label: '状态',
search: true,
type: 'radio',
span: 24,
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
value: CommonStatusEnum.ENABLE,
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
},
accessTokenValiditySeconds: {
label: '访问令牌的有效期',
controlsPosition: '',
type: 'number',
span: 12,
width: 90,
value: 30 * 60,
tip: '单位:秒',
rules: [{ required: true, message: '访问令牌的有效期不能为空', trigger: 'blur' }]
},
refreshTokenValiditySeconds: {
label: '刷新令牌的有效期',
controlsPosition: '',
type: 'number',
span: 12,
value: 30 * 24 * 60,
tip: '单位:秒',
width: 90,
rules: [{ required: true, message: '刷新令牌的有效期不能为空', trigger: 'blur' }]
},
authorizedGrantTypes: {
label: '授权类型',
clearable: false,
type: 'select',
multiple: true,
minWidth: 130,
span: 12,
dicData: getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE),
rules: [{ required: true, message: '授权类型不能为空', trigger: 'blur' }]
},
scopes: {
label: '授权范围',
type: 'select',
multiple: true,
span: 12,
hide: true,
control: (val) => {
let dicData = []
if (val?.length) {
dicData = val.map((item) => {
return { label: item, value: item }
})
}
return {
autoApproveScopes: { dicData }
}
}
},
autoApproveScopes: {
label: '自动授权范围',
type: 'select',
span: 12,
multiple: true,
hide: true,
dicData: []
},
redirectUris: {
label: '可重定向的 URI 地址',
type: 'select',
multiple: true,
span: 12,
hide: true,
rules: [{ required: true, message: '可重定向的 URI 地址不能为空', trigger: 'blur' }]
},
authorities: {
label: '权限',
span: 12,
hide: true
},
resourceIds: {
label: '资源',
span: 12,
hide: true
},
additionalInformation: {
label: '附加信息',
type: 'textarea',
minRows: 2,
maxRows: 4,
hide: true
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
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(['system:oauth2-client'])
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 ClientApi.getOAuth2ClientPage(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 ClientApi.getOAuth2Client(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await ClientApi.createOAuth2Client(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await ClientApi.updateOAuth2Client(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ClientApi.deleteOAuth2Client(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,192 @@
<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 #type="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" />
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
link
type="danger"
@click="handleForceLogout(row.accessToken)"
v-hasPermi="['system:oauth2-token:delete']"
>
强退
</el-button>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as OAuth2AccessTokenApi from '@/api/system/oauth2/token'
defineOptions({ name: 'SystemTokenClient' })
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: 120,
column: {
accessToken: {
label: '访问令牌',
search: true,
minWidth: 200
},
refreshToken: {
label: '刷新令牌',
minWidth: 200
},
userId: {
label: '用户编号',
search: true,
width: 90
},
expiresTime: {
label: '过期时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
createTime: {
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 permission = getCurrPermi(['system:oauth2-token'])
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 OAuth2AccessTokenApi.getAccessTokenPage(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 OAuth2AccessTokenApi.deleteAccessToken(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 强制退出操作 */
const handleForceLogout = async (accessToken: string) => {
try {
// 删除的二次确认
await message.confirm('是否要强制退出用户')
// 发起删除
await OAuth2AccessTokenApi.deleteAccessToken(accessToken)
message.success(t('common.success'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,274 @@
<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"
@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:operate-log:export']"
>导出</el-button
>
</template>
<!-- 表格 -->
<template #type="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_OPERATE_TYPE"
:value="scope.row.type != undefined ? scope.row.type : ''"
/>
</template>
<template #resultCode="scope">
<span>{{ scope.row.resultCode === 0 ? '成功' : '失败' }}</span>
</template>
<template #duration="scope">
<span>{{ scope.row.duration }} ms</span>
</template>
<!-- 表单 -->
<template #resultCode-form="scope">
<span>{{ scope.value === 0 ? '成功' : '失败' }}</span>
</template>
<template #requestUrl-form="scope">
<span>{{ tableForm.requestMethod }} {{ scope.value }}</span>
</template>
<template #duration-form="scope">
<span>{{ scope.value }} ms</span>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as OperateLogApi from '@/api/system/operatelog'
defineOptions({ name: 'SystemOperateLog' })
const message = useMessage() // 消息弹窗
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
traceId: {
label: '链路追踪',
hide: true
},
userId: {
label: '操作人编号',
hide: true
},
userIp: {
label: '操作人 IP',
hide: true
},
userAgent: {
label: '操作人 UA',
hide: true,
type: 'textarea',
minRows: 2,
maxRows: 4
},
module: {
label: '操作模块',
search: true,
minWidth: 90
},
name: {
label: '操作名',
minWidth: 90
},
content: {
label: '操作内容',
hide: true
},
exts: {
label: '操作拓展参数',
hide: true
},
requestMethod: {
label: '请求方式',
display: false,
hide: true
},
requestUrl: {
label: '请求 URL',
hide: true
},
javaMethod: {
label: 'Java方法名',
hide: true
},
javaMethodArgs: {
label: 'Java方法参数',
hide: true,
type: 'textarea',
minRows: 2,
maxRows: 4
},
type: {
label: '操作类型',
width: 90,
search: true,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_OPERATE_TYPE),
value: ''
},
userNickname: {
label: '操作人',
search: true
},
resultCode: {
label: '操作状态',
width: 90,
type: 'select'
},
searchStartTime: {
label: '操作时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
startTime: {
label: '操作时间',
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
duration: {
label: '执行时长',
minWidth: 90
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:operate-log'])
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.searchStartTime?.length) {
searchObj.startTime = getSearchDate(searchObj.searchStartTime)
}
delete searchObj.searchStartTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await OperateLogApi.getOperateLogPage(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 (['view'].includes(type) && tableForm.value.id) {
// tableForm.value = await OperateLogApi.getDictType(tableForm.value.id)
}
done()
}
/** 导出按钮操作 */
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 OperateLogApi.exportOperateLog(searchObj)
download.excel(data, '操作日志.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,232 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
:permission="permission"
@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="['system:position:export']"
>导出</el-button
>
</template>
<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 } from '@/utils/formatTime'
import download from '@/utils/download'
import * as PositionApi from '@/api/system/position'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemPosition' })
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%',
column: {
id: {
label: '职位编号',
display: false,
width: 90
},
name: {
label: '职位名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '职位名称不能为空', trigger: 'blur' }]
},
sort: {
label: '职位顺序',
width: 90,
rules: [{ required: true, message: '职位顺序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
remark: {
label: '职位备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 150,
span: 24
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:position'])
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 PositionApi.getPositionPage(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 PositionApi.getPosition(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await PositionApi.createPosition(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await PositionApi.updatePosition(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await PositionApi.deletePosition(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 PositionApi.exportPosition(searchObj)
download.excel(data, '职位列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,232 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
:permission="permission"
@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="['system:post:export']"
>导出</el-button
>
</template>
<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 } from '@/utils/formatTime'
import download from '@/utils/download'
import * as PostApi from '@/api/system/post'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemPost' })
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%',
column: {
id: {
label: '岗位编号',
display: false,
width: 90
},
name: {
label: '岗位名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }]
},
sort: {
label: '岗位顺序',
width: 90,
rules: [{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
remark: {
label: '岗位备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 150,
span: 24
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:post'])
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 PostApi.getPostPage(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 PostApi.getPost(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await PostApi.createPost(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await PostApi.updatePost(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await PostApi.deletePost(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 PostApi.exportPost(searchObj)
download.excel(data, '岗位列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,232 @@
<template>
<ContentWrap>
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:page="tablePage"
v-model:search="tableSearch"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
:permission="permission"
@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="['system:rank:export']"
>导出</el-button
>
</template>
<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 } from '@/utils/formatTime'
import download from '@/utils/download'
import * as RankApi from '@/api/system/rank'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemRank' })
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%',
column: {
id: {
label: '职级编号',
display: false,
width: 90
},
name: {
label: '职级名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '职级名称不能为空', trigger: 'blur' }]
},
sort: {
label: '职级顺序',
width: 90,
rules: [{ required: true, message: '职级顺序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
label: '创建时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
remark: {
label: '职级备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 150,
span: 24
}
}
}) //表格配置
const tableForm = ref<{ id?: number }>({})
const tableData = ref([])
const tableSearch = ref({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:rank'])
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 RankApi.getRankPage(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 RankApi.getRank(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await RankApi.createRank(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await RankApi.updateRank(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await RankApi.deleteRank(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 RankApi.exportRank(searchObj)
download.excel(data, '职级列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,168 @@
<template>
<DesignPopup v-model="dialogVisible" title="风险隐患数据上传" width="40%" :is-footer="true">
<div class="p-20px flex">
<el-upload style="flex: 1;" ref="uploadRef" v-model:file-list="fileList" :action="importUrl" :auto-upload="true"
:before-upload="beforeUpload" :disabled="formLoading" :headers="uploadHeaders" :limit="1"
:on-error="submitFormError" :on-exceed="handleExceed" :on-success="submitFormSuccess" accept=".xlsx, .xls" drag>
<Icon :size="50" icon="ep:upload-filled" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入 xlsxlsx 格式文件</span>
</div>
</template>
</el-upload>
<!-- <span v-if="errorMsg">{{ errorMsg }}</span> -->
<span v-html="errorMsg" style="font-size: 12px; color: red; max-width: 40%;"></span>
</div>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import * as RiskApi from '@/api/system/risk'
import * as TableApi from '@/api/design/table'
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'SystemRiskImportForm' })
interface Props {
dbformId: string
}
const props = defineProps<Props>()
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const uploadRef = ref()
// const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/risk/import'
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/jeelowcode/excel/viewExcel/1963446160885366786'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
fileList.value = []
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
const tableData = ref([])
const errorMsg = ref('')
/** 提交表单 */
const submitForm = async () => {
errorMsg.value = ""
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
let valid = true
tableData.value.forEach((el: { dataJson }, idx) => {
const dataJson = el.dataJson
if (dataJson.check_people != dataJson.corrective_confirm_people) {
errorMsg.value += `${idx + 1}行:检查人员、整改确认人 必须是同一人 <br>`
valid = false
}
});
if (!valid) {
message.error('请修改后重新上传')
return
}
// 导入上传数据
await TableApi.importUploadData(props.dbformId, viewExcel.value?.batchCode)
console.log("tableData.value>>>>", tableData.value);
message.alert("上传成功")
// 发送操作成功的事件
emits('success')
dialogVisible.value = false
}
// 文件上传前
const beforeUpload = () => {
// 设置header
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
return true
}
/** 文件上传成功 */
const emits = defineEmits(['success'])
interface FileResp {
fileId: string
dbFormId: string
batchCode: string
totalCou: number
}
const viewExcel = ref<FileResp>()
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
viewExcel.value = response.data
console.log('response>>>>>>', viewExcel.value);
getuploadData()
// let text = '上传成功数量:' + data.createUsernames.length + ';'
// for (let username of data.createUsernames) {
// text += '< ' + username + ' >'
// }
// text += '更新成功数量:' + data.updateUsernames.length + ';'
// for (const username of data.updateUsernames) {
// text += '< ' + username + ' >'
// }
// text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
// for (const username in data.failureUsernames) {
// text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
// }
// message.alert(text)
// 发送操作成功的事件
emits('success')
}
// 获取上传的导入数据
const getuploadData = async () => {
let searchObj = {
pageNo: 1,
pageSize: 1000,
batchCode: viewExcel.value?.batchCode
}
const data = await TableApi.getUploadImportData(props.dbformId, searchObj)
tableData.value = data.records.map((item) => {
item.dataJson = JSON.parse(item.dataJson)
return item
})
}
/** 上传错误提示 */
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,45 @@
<template>
<div class="risk-container">
<div style="display: flex; justify-content: end; margin-bottom: 10px;">
<el-button type="default" plain icon="el-icon-download" @click="importTemplate">模板下载</el-button>
<el-button type="default" plain icon="el-icon-upload" @click="handleImport">导入文件</el-button>
</div>
<LowTable ref="tableRef" :tableId="tableId"></LowTable>
<RiskImportForm ref="importFormRef" :dbformId="tableId" @success="onSuccess"></RiskImportForm>
</div>
</template>
<script setup lang="ts">
import * as RiskApi from '@/api/system/risk'
import download from '@/utils/download'
import RiskImportForm from './RiskImportForm.vue'
const tableId = ref<string>("1963446160885366786")
// 模板下载
const importTemplate = async () => {
const res = await RiskApi.importRiskTemplate()
download.excel(res, '风险隐患数据导入模版.xlsx')
}
// 导入文件
const importFormRef = ref()
const handleImport = () => {
importFormRef.value.open()
}
// 导入成功
const tableRef = ref()
const onSuccess = () => {
// 刷新table数据
tableRef.value.resetChange()
}
</script>
<style scoped lang="scss">
.risk-container {
min-height: 500px;
border: 1px solid rgb(233 233 233 / 100%);
box-shadow: 0 0 5px rgb(0 0 0 / 4.71%);
padding: 20px;
background-color: white;
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<DesignPopup v-model="dialogVisible" title="菜单权限" :is-footer="true" width="40%">
<div class="p-20px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="角色名称">
<el-tag>{{ formData.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ formData.code }}</el-tag>
</el-form-item>
<el-form-item label="菜单权限">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
全部展开/折叠:
<el-switch
v-model="menuExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
</template>
<el-tree
ref="treeRef"
:data="menuOptions"
:props="defaultProps"
empty-text="加载中请稍候"
node-key="id"
show-checkbox
/>
</el-card>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { defaultProps, handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as MenuApi from '@/api/system/menu'
import * as PermissionApi from '@/api/system/permission'
defineOptions({ name: 'SystemRoleAssignMenuForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref({
id: 0,
name: '',
code: '',
menuIds: []
})
const formRef = ref() // 表单 Ref
const menuOptions = ref<any[]>([]) // 菜单树形结构
const menuExpand = ref(false) // 展开/折叠
const treeRef = ref() // 菜单树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
/** 打开弹窗 */
const open = async (row: RoleApi.RoleVO) => {
dialogVisible.value = true
formLoading.value = true
resetForm()
// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
// 设置数据
formData.value.id = row.id
formData.value.name = row.name
formData.value.code = row.code
try {
formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
// 设置选中
formData.value.menuIds.forEach((menuId: number) => {
treeRef.value.setChecked(menuId, true, false)
})
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = {
roleId: formData.value.id,
menuIds: [
...(treeRef.value.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
...(treeRef.value.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
]
}
await PermissionApi.assignRoleMenu(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
// 重置选项
treeNodeAll.value = false
menuExpand.value = false
// 重置表单
formData.value = {
id: 0,
name: '',
code: '',
menuIds: []
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === menuExpand.value) {
continue
}
nodes[node].expanded = menuExpand.value
}
}
</script>
<style lang="scss" scoped>
.cardHeight {
width: 100%;
max-height: 400px;
overflow-y: scroll;
::v-deep(.el-card__header) {
padding: 10px 20px;
}
::v-deep(.el-card__body) {
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<DesignPopup v-model="dialogVisible" title="数据权限" width="800" :is-footer="true">
<div class="p-20px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="角色名称">
<el-tag>{{ formData.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ formData.code }}</el-tag>
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="formData.dataScope">
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<el-form-item
v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
label="权限范围"
label-width="80px"
>
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
<div class="flex items-center">
<span>全选/全不选</span>
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll()"
/>
<span class="ml-10px">全部展开/折叠</span>
<el-switch
v-model="deptExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
<span class="ml-10px">父子联动(选中父节点自动选择子节点)</span>
<el-switch
v-model="checkStrictly"
active-text=""
inactive-text=""
inline-prompt
/>
</div>
</template>
<el-tree
ref="treeRef"
:check-strictly="!checkStrictly"
:data="deptOptions"
:props="defaultProps"
default-expand-all
empty-text="加载中请稍后"
node-key="id"
show-checkbox
/>
</el-card>
</el-form-item>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps, handleTree } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PermissionApi from '@/api/system/permission'
defineOptions({ name: 'SystemRoleDataPermissionForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref({
id: 0,
name: '',
code: '',
dataScope: 1,
dataScopeDeptIds: []
})
const formRef = ref() // 表单 Ref
const deptOptions = ref<any[]>([]) // 部门树形结构
const deptExpand = ref(true) // 展开/折叠
const treeRef = ref() // 菜单树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
const checkStrictly = ref(true) // 是否严格模式,即父子不关联
/** 打开弹窗 */
const open = async (row: RoleApi.RoleVO) => {
dialogVisible.value = true
resetForm()
// 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
// 设置数据
formData.value.id = row.id
formData.value.name = row.name
formData.value.code = row.code
formData.value.dataScope = row.dataScope
await nextTick()
row.dataScopeDeptIds?.forEach((deptId: number) => {
treeRef.value.setChecked(deptId, true, false)
})
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
formLoading.value = true
try {
const data = {
roleId: formData.value.id,
dataScope: formData.value.dataScope,
dataScopeDeptIds:
formData.value.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
? []
: treeRef.value.getCheckedKeys(false)
}
await PermissionApi.assignRoleDataScope(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
// 重置选项
treeNodeAll.value = false
deptExpand.value = true
checkStrictly.value = true
// 重置表单
formData.value = {
id: 0,
name: '',
code: '',
dataScope: 1,
dataScopeDeptIds: []
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === deptExpand.value) {
continue
}
nodes[node].expanded = deptExpand.value
}
}
</script>

View File

@@ -0,0 +1,331 @@
<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="['system:role:export']"
>导出</el-button
>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-dropdown @command="menuHandle">
<div class="mt--1px cursor-pointer flex-basis-55px flex-shrink-0 py-8px px-4px">
<el-text type="primary">
{{ t('Avue.crud.moreBtn') }}
<Icon class="ml--2px" icon="iconamoon:arrow-down-2-light" />
</el-text>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:command="{ type: 'menu', row }"
v-if="checkPermi(['system:permission:assign-role-data-scope'])"
>
<div class="flex items-center">
<Icon icon="ep:menu" />
<span>菜单权限</span>
</div>
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'data', row }"
v-if="checkPermi(['system:permission:assign-role-data-scope'])"
>
<div class="flex items-center">
<Icon icon="ep:menu" />
<span>数据权限</span>
</div>
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'del', row }"
v-if="checkPermi(['system:role:delete'])"
>
<div class="flex items-center">
<Icon icon="ep:delete" />
<span>删除</span>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗菜单权限 -->
<RoleAssignMenuForm ref="assignMenuFormRef" @success="getTableData" />
<!-- 表单弹窗数据权限 -->
<RoleDataPermissionForm ref="dataPermissionFormRef" @success="getTableData" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import { checkPermi } from '@/utils/permission'
import download from '@/utils/download'
import * as RoleApi from '@/api/system/role'
import { CommonStatusEnum } from '@/utils/constants'
import RoleAssignMenuForm from './RoleAssignMenuForm.vue'
import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
defineOptions({ name: 'SystemRole' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
menuWidth: 160,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
dialogWidth: '50%',
delBtn: false,
column: {
name: {
label: '角色名称',
search: true,
minWidth: 90,
rules: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }]
},
type: {
label: '角色类型',
type: 'select',
width: 85,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_ROLE_TYPE),
rules: [{ required: true, message: '角色类型不能为空', trigger: 'blur' }],
display: false
},
code: {
label: '角色标识',
search: true,
disabled: false,
minWidth: 90,
rules: [{ required: true, message: '角色标识不能为空', trigger: 'blur' }]
},
sort: {
label: '显示顺序',
width: 85,
type: 'number',
rules: [{ required: true, message: '显示顺序不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'select',
width: 85,
span: 24,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 120,
span: 24
},
createTime: {
label: '创建时间',
search: true,
searchRange: true,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 160,
display: false,
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(['system:role'])
const crudRef = ref()
useCrudHeight(crudRef)
const exportLoading = ref(false) // 导出的加载中
const menuHandle = ({ row, type }) => {
if (type == 'menu') openAssignMenuForm(row)
else if (type == 'data') openDataPermissionForm(row)
else if (type == 'del') rowDel(row)
}
/** 查询列表 */
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 RoleApi.getRolePage(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) {
const res = await RoleApi.getRole(tableForm.value.id)
if (type == 'edit') tableOption.column.code.disabled = res.type === 1
tableForm.value = res
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await RoleApi.createRole(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await RoleApi.updateRole(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await RoleApi.deleteRole(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 数据权限操作 */
const dataPermissionFormRef = ref()
const openDataPermissionForm = async (row: RoleApi.RoleVO) => {
dataPermissionFormRef.value.open(row)
}
/** 菜单权限操作 */
const assignMenuFormRef = ref()
const openAssignMenuForm = async (row: RoleApi.RoleVO) => {
assignMenuFormRef.value.open(row)
}
/** 导出按钮操作 */
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 RoleApi.exportRole(searchObj)
download.excel(data, '角色列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss" scoped>
.cardHeight {
width: 100%;
max-height: 340px;
overflow-y: scroll;
}
</style>

View File

@@ -0,0 +1,310 @@
<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 #tags="scope">
<el-tag v-for="tag in scope.row.tags" :key="tag" :disable-transitions="true" class="mr-5px">
{{ tag }}
</el-tag>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:sensitive-word:export']"
>导出</el-button
>
<el-button :size="size" plain type="warning" @click="openTest">
<Icon class="mr-5px" icon="ep:document-checked" />
测试
</el-button>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗测试敏感词 -->
<DesignPopup v-model="testPopup" title="敏感词测试" :is-footer="true" width="40%">
<div class="p-20px">
<avue-form
ref="textFormRef"
v-if="testPopup"
v-model="testFormData"
:option="testFormOption"
></avue-form>
</div>
<template #footer>
<el-button :loading="textLoading" type="primary" @click="testSubmitFun"> </el-button>
<el-button @click="testPopup = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemSensitiveWord' })
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: {
name: {
label: '敏感词',
width: 80,
search: true,
rules: [{ required: true, message: '敏感词不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'radio',
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
value: CommonStatusEnum.ENABLE
},
description: {
label: '描述',
minWidth: 150
},
tags: {
label: '标签',
search: true,
searchType: 'input',
type: 'select',
multiple: true,
filterable: true,
allowCreate: true,
dicUrl: '/system/sensitive-word/get-tags',
dicFormatter: (data) => {
data = [...new Set(data)]
return data.map((tag) => {
return { label: tag, value: tag }
})
},
minWidth: 120,
rules: [{ required: true, message: '标签不能为空', trigger: 'blur' }],
value: ''
},
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(['system:sensitive-word'])
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 SensitiveWordApi.getSensitiveWordPage(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) => {
crudRef.value.dicInit()
if (['edit', 'view'].includes(type) && tableForm.value.id) {
tableForm.value = await SensitiveWordApi.getSensitiveWord(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await SensitiveWordApi.createSensitiveWord(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await SensitiveWordApi.updateSensitiveWord(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SensitiveWordApi.deleteSensitiveWord(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 SensitiveWordApi.exportSensitiveWord(searchObj)
download.excel(data, '敏感词列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 测试敏感词按钮操作 */
const testPopup = ref(false)
const testFormData = ref<any>({})
const testFormOption = ref({
submitBtn: false,
emptyBtn: false,
span: 24,
labelWidth: 120,
column: {
text: { label: '测试文本', type: 'textarea' },
tags: tableOption.column.tags
}
})
const textLoading = ref(false)
const textFormRef = ref()
const openTest = () => {
testFormData.value = {}
testPopup.value = true
}
// 提交表单
const testSubmitFun = () => {
textFormRef.value.validate(async (valid, hide) => {
if (!valid) return hide()
try {
textLoading.value = true
const data = await SensitiveWordApi.validateText({
text: testFormData.value.text,
tag: testFormData.value.tags
})
if (data.length === 0) message.success('不包含敏感词!')
else message.warning('包含敏感词:' + data.join(', '))
} finally {
hide()
textLoading.value = false
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,225 @@
<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 #code="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE"
:value="scope.row.code ? scope.row.code : ''"
/>
</template>
<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, getStrDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemSmsChannel' })
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: ' ',
labelWidth: 140,
span: 24,
dialogWidth: '50%',
menuWidth: 160,
overHidden:true,
column: {
id: {
label: '编号',
width: 80,
display: false
},
signature: {
label: '短信签名',
search: true,
minWidth: 100,
rules: [{ required: true, message: '短信签名不能为空', trigger: 'blur' }]
},
code: {
label: '渠道编码',
type: 'select',
span: 12,
minWidth: 100,
dicData: getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE),
rules: [{ required: true, message: '渠道编码不能为空', trigger: 'blur' }]
},
status: {
label: '启用状态',
search: true,
type: 'radio',
span: 12,
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
minWidth: 100,
overHidden: true
},
apiKey: {
label: '短信 API 的账号',
overHidden: true,
minWidth: 150,
rules: [{ required: true, message: '短信 API 的账号不能为空', trigger: 'blur' }]
},
apiSecret: {
label: '短信 API 的密钥',
minWidth: 150,
overHidden: true
},
callbackUrl: {
label: '短信发送回调 URL',
minWidth: 120
},
createTime: {
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 permission = getCurrPermi(['system:sms-channel'])
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 SmsChannelApi.getSmsChannelPage(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 SmsChannelApi.getSmsChannel(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await SmsChannelApi.createSmsChannel(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await SmsChannelApi.updateSmsChannel(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsChannelApi.deleteSmsChannel(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,341 @@
<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 #menu-left="{ size }">
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:sms-log:export']"
>导出</el-button
>
</template>
<!-- 表格 -->
<template #sendStatus="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS"
:value="scope.row.sendStatus != undefined ? scope.row.sendStatus : ''"
/>
<div>{{ formatDate(scope.row.sendTime) }}</div>
</template>
<template #receiveStatus="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS"
:value="scope.row.receiveStatus != undefined ? scope.row.receiveStatus : ''"
/>
<div>{{ formatDate(tableForm.receiveTime) }}</div>
</template>
<template #templateType="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE"
:value="scope.row.templateType != undefined ? scope.row.templateType : ''"
/>
</template>
<template #channelCode="scope">
<div>
{{ channelObj[scope.row.channelId] }}
</div>
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE"
:value="scope.row.channelCode ? scope.row.channelCode : ''"
/>
</template>
<!-- 表单 -->
<template #channelCode-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<span class="mr-5px"> {{ channelObj[tableForm.channelId] }}</span>
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #templateType-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<span class="mr-5px">{{ tableForm.templateId }} | {{ tableForm.templateCode }}</span>
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #templateParams-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<span>{{ JSON.stringify(value) }}</span>
</div>
</template>
<template #sendStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS"
:value="value != undefined ? value : ''"
/>
</div>
</template>
<template #receiveStatus-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS"
:value="value != undefined ? value : ''"
/>
{{ formatDate(tableForm.receiveTime) }}
</div>
</template>
<template #apiReceiveMsg-form="{ value }">
<div class="el-input__wrapper" style="box-shadow: none">
{{ tableForm.apiReceiveCode }} | {{ value }}
</div>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate, dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
import * as SmsLogApi from '@/api/system/sms/smsLog'
defineOptions({ name: 'SystemOperateLog' })
const message = useMessage() // 消息弹窗
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 160,
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
id: {
label: '日志编号',
width: 90
},
channelCode: {
label: '短信渠道',
type: 'select',
minWidth: 110,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE),
value: ''
},
templateId: {
label: '模板编号',
display: false,
minWidth: 90
},
templateType: {
label: '短信类型',
type: 'select',
minWidth: 100,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE),
value: ''
},
apiTemplateId: {
label: 'API 的模板编号',
hide: true
},
mobile: {
label: '手机号',
width: 120
},
templateContent: {
label: '短信内容',
width: 300,
overHidden: true
},
templateParams: {
label: '短信参数',
hide: true
},
searchCreateTime: {
label: '创建时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
createTime: {
label: '创建时间',
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
sendStatus: {
label: '发送状态',
width: 200,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS),
value: ''
},
searchSendTime: {
label: '发送时间',
search: true,
hide: true,
display: false,
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间'
},
sendTime: {
label: '发送时间',
hide: true,
type: 'datetime',
width: 180,
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
apiSendMsg: {
label: 'API 发送结果',
hide: true
},
apiSerialNo: {
label: 'API 短信编号',
hide: true
},
apiRequestId: {
label: 'API 请求编号',
display: false,
hide: true
},
receiveStatus: {
label: 'API 接收状态',
width: 110,
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS),
value: ''
},
apiReceiveMsg: {
label: 'API 接收结果',
hide: true
}
}
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const channelObj = ref<object>({}) // 短信渠道
const permission = getCurrPermi(['system:sms-log'])
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)
}
delete searchObj.searchCreateTime
if (searchObj.searchSendTime?.length) {
searchObj.sendTime = getSearchDate(searchObj.searchSendTime)
}
delete searchObj.searchSendTime
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
try {
const data = await SmsLogApi.getSmsLogPage(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 = { ...tableSearch.value }
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await SmsLogApi.exportSmsLog(searchObj)
download.excel(data, '短信日志.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
// 加载渠道列表
const channelList = await SmsChannelApi.getSimpleSmsChannelList()
channelList.forEach((item) => {
channelObj.value[item.id] = item.signature
})
})
</script>

View File

@@ -0,0 +1,434 @@
<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="['system:sms-template:export']"
>导出</el-button
>
</template>
<!-- 表格 -->
<template #type="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE"
:value="scope.row.type ? scope.row.type : ''"
/>
</template>
<template #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<template #channelCode="{ row }">
<div>
{{ channelObj[row.channelId] }}
</div>
<dict-tag
:type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE"
:value="row.channelCode ? row.channelCode : ''"
/>
</template>
<!-- 自定义操作栏 -->
<template #menu="{ row }">
<el-button
v-if="row.status === 0"
class="ml-4px!"
link
type="primary"
@click="openSendForm(row.id)"
v-hasPermi="['system:sms-template:send-sms']"
>
<Icon :size="14" icon="ep:tickets"></Icon>
<span>测试</span>
</el-button>
</template>
</avue-crud>
</ContentWrap>
<!-- 表单弹窗测试发送 -->
<DesignPopup
v-model="testPopup"
title="发送短信模板测试"
:is-footer="true"
width="40%"
class="sms-template-popup"
>
<div class="p-20px">
<avue-form
ref="textFormRef"
v-if="testPopup"
v-model="testFormData"
:option="testFormOption"
></avue-form>
</div>
<template #footer>
<el-button :loading="textLoading" type="primary" @click="testSubmitFun"> </el-button>
<el-button @click="testPopup = false"> </el-button>
</template>
</DesignPopup>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemSmsTemplate' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const channelObj = ref({})
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
labelWidth: 120,
span: 24,
dialogWidth: '50%',
column: {
channelId: {
label: '短信渠道编号',
type: 'select',
hide: true,
order: 10,
searchOrder: 1,
span: 12,
dicUrl: '/system/sms-channel/simple-list',
dicFormatter: (data) => {
data = data.map((item) => {
channelObj.value[item.id] = item.signature
return {
label: `${item.signature}${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, item.code)}`,
value: item.id
}
})
return data
},
dicFlag: true,
search: true,
searchLabel: '短信渠道',
rules: [{ required: true, message: '短信渠道编号不能为空', trigger: 'change' }]
},
code: {
label: '模板编码',
search: true,
overHidden: true,
minWidth: 100,
span: 12,
order: 8,
searchOrder: 5,
rules: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }]
},
name: {
label: '模板名称',
overHidden: true,
minWidth: 120,
span: 12,
order: 7,
rules: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
},
content: {
label: '模板内容',
overHidden: true,
type: 'textarea',
minWidth: 120,
order: 6,
rules: [{ required: true, message: '模板内容不能为空', trigger: 'blur' }]
},
type: {
label: '短信类型',
search: true,
type: 'select',
span: 12,
minWidth: 100,
order: 9,
searchOrder: 4,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE),
rules: [{ required: true, message: '短信类型不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
search: true,
type: 'radio',
span: 24,
width: 70,
order: 5,
searchOrder: 3,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
order: 3,
minWidth: 150
},
apiTemplateId: {
label: '短信 API 的模板编号',
search: true,
minWidth: 100,
order: 4,
searchOrder: 2,
searchLabelWidth: 160,
rules: [{ required: true, message: '短信 API 的模板编号不能为空', trigger: 'blur' }]
},
channelCode: {
label: '短信渠道',
display: false,
span: 12,
minWidth: 100
},
createTime: {
label: '创建时间',
searchRange: true,
search: 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<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const exportLoading = ref(false) // 导出的加载中
const permission = getCurrPermi(['system:sms-template'])
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 SmsTemplateApi.getSmsTemplatePage(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 SmsTemplateApi.getSmsTemplate(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await SmsTemplateApi.createSmsTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await SmsTemplateApi.updateSmsTemplate(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SmsTemplateApi.deleteSmsTemplate(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 SmsTemplateApi.exportSmsTemplate(searchObj)
download.excel(data, '短信模板列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 发送短信按钮 */
const testPopup = ref(false)
const testFormData = ref<any>({})
const testFormOption = ref({
submitBtn: false,
emptyBtn: false,
span: 24,
labelWidth: 120,
column: {}
})
const textLoading = ref(false)
const textFormRef = ref()
// 打开测试表单
const openSendForm = async (id: number) => {
loading.value = true
const formDatas = await SmsTemplateApi.getSmsTemplate(id)
testFormData.value = {
content: formDatas.content,
params: formDatas.params,
templateCode: formDatas.code,
templateParams: {}
}
testFormOption.value.column = {
content: { label: '模板内容', type: 'textarea', disabled: true },
mobile: {
label: '手机号',
rules: [{ required: true, message: `手机号不能为空`, trigger: 'blur' }]
}
}
// 设置动态参数
if (formDatas.params?.length) {
formDatas.params.forEach((item) => {
testFormData.value.templateParams[item] = ''
testFormOption.value.column[item] = {
label: `参数 ${item} `,
value: '',
bind: `templateParams.${item}`,
rules: [{ required: true, message: `参数 ${item} 不能为空`, trigger: 'blur' }]
}
})
}
loading.value = false
testPopup.value = true
}
// 提交表单
const testSubmitFun = () => {
textFormRef.value.validate(async (valid, hide) => {
if (!valid) return hide()
try {
textLoading.value = true
const logId = await SmsTemplateApi.sendSms({
mobile: testFormData.value.mobile,
templateCode: testFormData.value.templateCode,
templateParams: testFormData.value.templateParams
})
if (logId) {
message.success('提交发送成功!发送结果见短信日志,日志编号:' + logId)
}
testPopup.value = false
} finally {
hide()
textLoading.value = false
}
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss">
.sms-template-popup {
.el-form-item__label {
height: auto;
min-height: 32px;
overflow-wrap: anywhere;
}
}
</style>

View File

@@ -0,0 +1,217 @@
<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 #socialType="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SOCIAL_TYPE"
:value="scope.row.socialType ? scope.row.socialType : ''"
/>
</template>
<template #userType="scope">
<dict-tag
:type="DICT_TYPE.USER_TYPE"
:value="scope.row.userType ? scope.row.userType : ''"
/>
</template>
<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 } from '@/utils/formatTime'
import * as SocialClientApi from '@/api/system/social/client'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SocialClient' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelWidth: 100,
searchLabelWidth: 100,
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
column: {
name: {
label: '应用名',
search: true,
minWidth: 100,
rules: [{ required: true, message: '应用名不能为空', trigger: 'blur' }]
},
socialType: {
label: '社交平台',
type: 'radio',
search: true,
minWidth: 100,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE),
rules: [{ required: true, message: '社交平台不能为空', trigger: 'blur' }]
},
clientId: {
label: '客户端编号',
search: true,
minWidth: 120,
rules: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }]
},
clientSecret: {
label: '客户端密钥',
hide: true,
rules: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }]
},
status: {
label: '状态',
type: 'radio',
search: true,
span: 12,
width: 80,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
createTime: {
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 permission = getCurrPermi(['system:social-client'])
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 SocialClientApi.getSocialClientPage(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 SocialClientApi.getSocialClient(tableForm.value.id)
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await SocialClientApi.createSocialClient(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
let bool = await SocialClientApi.updateSocialClient(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await SocialClientApi.deleteSocialClient(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,219 @@
<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 #type="scope">
<dict-tag
:type="DICT_TYPE.SYSTEM_SOCIAL_TYPE"
:value="scope.row.type ? scope.row.type : ''"
/>
</template>
<template #avatar="scope">
<el-image
:src="scope.row.avatar"
class="h-30px w-30px"
@click="imagePreview(scope.row.avatar)"
/>
</template>
<!-- 表单 -->
<template #avatar-form>
<el-image :src="tableForm.avatar" class="h-30px w-30px" />
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import * as SocialUserApi from '@/api/system/social/user'
import { createImageViewer } from '@/components/ImageViewer'
defineOptions({ name: 'SocialUser' })
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
addBtn: false,
editBtn: false,
delBtn: false,
viewBtn: true,
viewBtnText: '详情',
viewBtnIcon: ' ',
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
searchLabelWidth: 100,
labelWidth: 160,
labelSuffix: ' ',
span: 24,
dialogWidth: '50%',
menuWidth: 120,
column: {
type: {
label: '社交平台',
type: 'select',
search: true,
minWidth: 100,
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)
},
openid: {
label: '社交 openid',
search: true,
minWidth: 100,
display: false
},
nickname: {
label: '用户昵称',
minWidth: 100,
search: true
},
avatar: {
label: '用户头像',
type: 'upload',
span: 24,
formslot: true,
minWidth: 100,
slot: true
},
token: {
label: '社交 token',
hide: true
},
rawTokenInfo: {
label: '原始 Token 数据',
type: 'textarea',
minRows: 2,
maxRows: 4,
hide: true
},
rawUserInfo: {
label: '原始 User 数据',
type: 'textarea',
minRows: 2,
maxRows: 4,
hide: true
},
code: {
label: '最后一次的认证 code',
hide: true
},
state: {
label: '最后一次的认证 state',
hide: true
},
createTime: {
label: '创建时间',
search: true,
searchRange: 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)
}
},
updateTime: {
label: '更新时间',
searchRange: true,
display: false,
type: 'datetime',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
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 permission = getCurrPermi([])
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 SocialUserApi.getSocialUserPage(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 imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>

View File

@@ -0,0 +1,320 @@
<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="['system:tenant:export']"
>导出</el-button
>
</template>
<template #menu="{ size, row }">
<el-button
v-if="row.packageId !== 0"
type="primary"
link
:size="size"
icon="el-icon-delete"
@click="rowDel(row)"
v-hasPermi="['system:tenant:delete']"
>删除</el-button
>
</template>
<template #packageId="scope">
<el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag>
<el-tag v-else type="success">
{{ scope.row.$packageId }}
</el-tag>
</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.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 download from '@/utils/download'
import * as TenantApi from '@/api/system/tenant'
import { getTenantPackageList } from '@/api/system/tenantPackage'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const loading = ref(true) // 列表的加载中
const tableOption = reactive({
span: 12,
dialogWidth: '50%',
menuWidth: 160,
column: {
name: {
label: '租户名',
search: true,
minWidth: 80,
rules: [{ required: true, message: '租户名不能为空', trigger: 'blur' }]
},
packageId: {
label: '租户套餐',
type: 'select',
dicData: [] as { name: string; id: number }[],
dicFlag: true,
props: {
label: 'name',
value: 'id'
},
width: 100,
overHidden: false,
editDisabled: false,
rules: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }]
},
contactName: {
label: '联系人',
width: 80,
search: true,
rules: [{ required: true, message: '联系人不能为空', trigger: 'blur' }]
},
contactMobile: {
label: '联系手机',
width: 115,
search: true
},
username: {
label: '用户账号',
rules: [{ required: true, message: '用户账号不能为空', trigger: 'blur' }],
hide: true,
showColumn: false,
editDisplay: false,
value: 'admin'
},
password: {
label: '用户密码',
rules: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
type: 'password',
hide: true,
showColumn: false,
editDisplay: false
},
accountCount: {
label: '账号额度',
width: 90,
rules: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
type: 'number'
},
expireTime: {
label: '过期时间',
rules: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
type: 'date',
width: 160,
valueFormat: 'x',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
}
},
website: {
label: '绑定域名',
rules: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
alone: true,
type: 'url',
dataType: 'string',
minWidth: 120
},
status: {
label: '租户状态',
search: true,
type: 'radio',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
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<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
const permission = getCurrPermi(['system:tenant'])
permission['delBtn'] = false
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 TenantApi.getTenantPage(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) => {
const packageId = tableOption.column.packageId
packageId.dicData = packageId.dicData.filter((item) => item.id !== 0)
packageId.editDisabled = false
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
tableForm.value = await TenantApi.getTenant(tableForm.value.id)
if (tableForm.value.packageId === 0) {
packageId.dicData.push({ name: '系统租户', id: 0 })
packageId.editDisabled = true
}
loading.value = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
let bool = await TenantApi.createTenant(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
delete form.password
delete form.username
let bool = await TenantApi.updateTenant(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantApi.deleteTenant(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 TenantApi.exportTenant(searchObj)
download.excel(data, '租户列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
getTenantPackageList().then((data) => {
tableOption.column.packageId.dicData = data
})
getTableData()
})
</script>

View File

@@ -0,0 +1,296 @@
<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 #status="scope">
<dict-tag
v-if="scope.row.status !== undefined"
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
<template #menuIds-form="">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
全部展开/折叠:
<el-switch
v-model="menuExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
</template>
<el-tree
ref="treeRef"
:data="menuOptions"
:props="defaultProps"
empty-text="加载中请稍候"
node-key="id"
show-checkbox
/>
</el-card>
</template>
</avue-crud>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import { defaultProps, handleTree } from '@/utils/tree'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import { CommonStatusEnum } from '@/utils/constants'
import * as MenuApi from '@/api/system/menu'
import { ElTree } from 'element-plus'
import { nextTick } from 'vue'
defineOptions({ name: 'SystemTenantPackage' })
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: {
name: {
label: '套餐名',
search: true,
minWidth: 80,
rules: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }]
},
menuIds: {
label: '套餐编号',
hide: true
},
status: {
label: '状态',
search: true,
type: 'radio',
width: 90,
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
rules: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
value: CommonStatusEnum.ENABLE
},
remark: {
label: '备注',
type: 'textarea',
minRows: 2,
maxRows: 4,
minWidth: 120
},
createTime: {
label: '创建时间',
search: true,
searchRange: true,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
display: false,
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(['system:tenant-package'])
const crudRef = ref()
useCrudHeight(crudRef)
// 菜单权限表单
const menuOptions = ref<any[]>([]) // 树形结构数据
const menuExpand = ref(false) // 展开/折叠
const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
const formData = ref({
id: null,
name: null,
remark: null,
menuIds: [],
status: CommonStatusEnum.ENABLE
})
/** 查询列表 */
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 TenantPackageApi.getTenantPackagePage(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) => {
// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
const res = await TenantPackageApi.getTenantPackage(tableForm.value.id)
tableForm.value = res
// 设置选中
formData.value = res
// 设置选中
res.menuIds.forEach((menuId: number) => {
nextTick(() => {
treeRef.value!.setChecked(menuId, true, false)
})
})
loading.value = false
}
done()
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
form.menuIds = [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
]
let bool = await TenantPackageApi.createTenantPackage(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
form.menuIds = [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
]
let bool = await TenantPackageApi.updateTenantPackage(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 删除按钮操作 */
const rowDel = async (form, index) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantPackageApi.deleteTenantPackage(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === menuExpand.value) {
continue
}
nodes[node].expanded = menuExpand.value
}
}
/** 初始化 **/
onMounted(async () => {
await getTableData()
})
</script>
<style lang="scss" scoped>
.cardHeight {
width: 100%;
max-height: 340px;
overflow-y: scroll;
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<DesignPopup v-model="dialogVisible" title="用户导入" width="400" :is-footer="true">
<div class="p-20px">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl + '?updateSupport=' + updateSupport"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".xlsx, .xls"
drag
>
<Icon :size="50" icon="ep:upload-filled" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip flex items-center justify-center">
<el-checkbox v-model="updateSupport" />
<span class="ml-5px text-14px">是否更新已经存在的用户数据</span>
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
v-loading="downLoading"
>
下载模板
</el-link>
</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>
</DesignPopup>
</template>
<script lang="ts" setup>
import * as UserApi from '@/api/system/user'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
defineOptions({ name: 'SystemUserImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const downLoading = ref(false)
const uploadRef = ref()
const importUrl =
import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
const updateSupport = ref(0) // 是否更新已经存在的用户数据
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
fileList.value = []
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emits = defineEmits(['success'])
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
// 拼接提示语
const data = response.data
let text = '上传成功数量:' + data.createUsernames.length + ';'
for (let username of data.createUsernames) {
text += '< ' + username + ' >'
}
text += '更新成功数量:' + data.updateUsernames.length + ';'
for (const username of data.updateUsernames) {
text += '< ' + username + ' >'
}
text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
for (const username in data.failureUsernames) {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
// 发送操作成功的事件
emits('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
/** 下载模板操作 */
const importTemplate = async () => {
downLoading.value = true
const res = await UserApi.importUserTemplate()
download.excel(res, '用户导入模版.xls')
downLoading.value = false
}
</script>

View File

@@ -0,0 +1,670 @@
<template>
<el-row :gutter="20">
<el-col :span="4">
<ContentWrap>
<avue-tree
ref="treeRef"
:option="treeOption"
:data="treeData"
@node-click="nodeClick"
></avue-tree>
</ContentWrap>
</el-col>
<el-col :span="20">
<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"
@refresh-change="getTableData"
@size-change="sizeChange"
@current-change="currentChange"
>
<template #menu-left="{ size }">
<el-button
type="warning"
plain
@click="handleImport"
v-hasPermi="['system:user:import']"
>
<Icon icon="ep:upload" /> 导入
</el-button>
<el-button
type="success"
plain
:size="size"
icon="el-icon-download"
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:user:export']"
>导出</el-button
>
</template>
<template #menu="{ row }">
<el-dropdown
@command="(command) => handleCommand(command, row)"
v-hasPermi="[
'system:user:delete',
'system:user:update-password',
'system:permission:assign-user-role'
]"
>
<div class="pt-3px pr-2px pb-4px pl-2px 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="handleResetPwd"
v-if="checkPermi(['system:user:update-password'])"
>
<Icon icon="ep:key" />重置密码
</el-dropdown-item>
<el-dropdown-item
command="handleDelete"
v-if="checkPermi(['system:user:delete'])"
>
<Icon icon="ep:delete" />删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #status="scope">
<el-switch
v-if="scope.row.status != undefined"
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
@change="handleStatusChange(scope.row)"
/>
</template>
<template #deptName="{ row }">
<div class="flex flex-wrap gap-2px justify-center">
<div v-for="(dept, deptIndex) in row.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="row.deptInfoList[deptIndex][type.value]">
<span class="flex-basis-42px flex-shrink-0 c-#909399"
>{{ type.label }}</span
>
<span class="flex-1">{{ row.deptInfoList[deptIndex][type.value] }}</span>
</div>
</template>
</div>
</el-popover>
</div>
</div>
</template>
<template #deptInfoList-form>
<avue-crud ref="deptCrudRef" :option="deptOption" :data="tableForm.deptInfoList">
<template #menu="{ size, index }">
<el-button text :size="size" type="danger" @click="delDeptTableRow(index)">
<Icon icon="ep:delete"></Icon>
<span>删除</span>
</el-button>
</template>
<template #deptId-form="{ index, column, disabled }">
<DeptSelect
v-model="tableForm.deptInfoList[index].deptId"
:column="column"
:disabled="disabled"
type="edit"
prop="deptId"
></DeptSelect>
</template>
</avue-crud>
</template>
</avue-crud>
</ContentWrap>
</el-col>
</el-row>
<!-- 用户导入对话框 -->
<UserImportForm ref="importFormRef" @success="getTableData" />
</template>
<script setup lang="ts">
import * as UserApi from '@/api/system/user'
import * as DeptApi from '@/api/system/dept'
import { checkPermi } from '@/utils/permission'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { handleTree } from '@/utils/tree'
import { CommonStatusEnum } from '@/utils/constants'
import { dateFormatter, getSearchDate } from '@/utils/formatTime'
import download from '@/utils/download'
import { useLowStoreWithOut } from '@/store/modules/low'
import UserImportForm from './UserImportForm.vue'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'SystemUser' })
const keyList = [
{ label: '部门', value: 'deptName' },
{ label: '角色', value: 'roleInfoList' },
{ label: '职务', value: 'dutyInfoList' },
{ label: '职位', value: 'positionInfoList' },
{ label: '岗位', value: 'postInfoList' }
]
// 树数据
const treeOption = reactive({
defaultExpandAll: true,
addBtn: false,
formOption: {
column: [{ label: '自定义项', prop: 'test' }]
},
props: { label: 'name', value: 'id', children: 'children' }
})
const treeData = ref<Tree[]>([])
const crudRef = ref()
const treeRef = ref()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { getCurrPermi } = useCrudPermi()
const lowStore = useLowStoreWithOut()
const loading = ref(true) // 列表的加载中
const tableOption = ref({
delBtn: false,
align: 'center',
headerAlign: 'center',
searchMenuSpan: 6,
searchMenuPosition: 'left',
labelSuffix: ' ',
span: 12,
dialogWidth: '80%',
menuWidth: 160,
dialogCustomClass: 'system-user-form',
column: {
username: { label: '用户账号', search: true, minWidth: 90, display: false },
nickname: { label: '用户昵称', search: true, minWidth: 100, display: false },
deptName: { label: '部门', minWidth: 100, display: false },
mobile: { label: '手机号码', search: true, width: 120, display: false },
status: {
label: '状态',
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
search: true,
display: false
},
createTime: {
label: '创建时间',
search: true,
searchRange: true,
type: 'date',
searchType: 'daterange',
valueFormat: 'YYYY-MM-DD',
width: 180,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
formatter: (row, val, value, column) => {
return dateFormatter(row, column, val)
},
display: false
}
},
group: [
{
label: '基本信息',
collapse: true,
prop: 'group_info',
column: {
username: {
label: '用户账号',
addDisplay: true,
editDisplay: false,
viewDisplay: false,
rules: [{ required: true, message: '用户账号不能为空', trigger: 'blur' }]
},
password: {
label: '用户密码',
hide: true,
addDisplay: true,
editDisplay: false,
viewDisplay: false,
rules: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
},
nickname: {
label: '用户昵称',
rules: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }]
},
mobile: { label: '手机号码' },
email: {
label: '邮箱',
rules: [
{
validator: (rule, value, callback) => {
if (
value === '' ||
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value)
) {
callback()
} else callback(new Error('请输入正确的邮箱'))
},
trigger: 'blur'
}
]
},
sex: {
label: '用户性别',
type: 'select',
dicData: getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX),
value: ''
},
rankIds: {
label: '职级',
type: 'select',
multiple: true,
cell: true,
dicUrl: '/system/rank/simple-list',
dataType: 'array',
props: { label: 'name', value: 'id' }
},
remark: { label: '备注', span: 24, type: 'textarea', minRows: 2, maxRows: 4 }
}
},
{
label: '部门信息',
collapse: true,
prop: 'group_dept',
labelWidth: 20,
display: true,
column: {
deptInfoList: {
hide: false,
span: 24,
dataType: 'array',
value: [
{ $cellEdit: true, deptId: '', roleIds: [], dutyIds: [], positionIds: [], postIds: [] }
],
rules: [
{
validator: (rule, value, callback) => {
if (value?.length) callback()
else callback(new Error('请添加用户部门信息'))
},
trigger: 'blur'
}
]
}
}
}
]
}) //表格配置
const tableForm = ref<any>({})
const tableData = ref([])
const tableSearch = ref<any>({})
const tablePage = ref({
currentPage: 1,
pageSize: 10,
total: 0,
deptId: null
})
const permission = getCurrPermi(['system:user'])
const exportLoading = ref(false) // 导出的加载中
const deptOption = ref({
addBtn: false,
editBtn: false,
addRowBtn: true,
refreshBtn: false,
columnBtn: false,
addBtnText: '新增部门',
index: true,
menuWidth: 120,
height: undefined,
calcHeight: undefined,
maxHeight: 300,
column: {
deptId: {
label: '所属部门',
findType: 'all',
multiple: false,
cell: true,
rules: [{ required: true, message: '请选择所属部门', trigger: 'change' }]
},
roleIds: {
label: '角色',
type: 'select',
multiple: true,
cell: true,
dataType: 'array',
dicUrl: '/system/role/simple-list',
props: { label: 'name', value: 'id' },
dicFormatter: (res) => {
//剔除超级管理员和租户管理员
return res.filter((item) => !['super_admin', 'tenant_admin'].includes(item.code))
}
},
dutyIds: {
label: '职务',
type: 'select',
multiple: true,
cell: true,
dataType: 'array',
dicUrl: '/system/duty/simple-list',
props: { label: 'name', value: 'id' }
},
positionIds: {
label: '职位',
type: 'select',
multiple: true,
cell: true,
dataType: 'array',
dicUrl: '/system/position/simple-list',
props: { label: 'name', value: 'id' }
},
postIds: {
label: '岗位',
type: 'select',
multiple: true,
cell: true,
dataType: 'array',
dicUrl: '/system/post/simple-list',
props: { label: 'name', value: 'id' }
}
}
})
const deptCrudRef = ref()
const nodeClick = (row: any) => {
if (tablePage.value.deptId == row.id) {
tablePage.value.deptId = null
treeRef.value.setCurrentKey(null)
} else {
tablePage.value.deptId = row.id
}
getTableData()
}
/** 查询列表 */
const getTableData = async () => {
loading.value = true
let searchObj = {
...tableSearch.value,
pageNo: tablePage.value.currentPage,
pageSize: tablePage.value.pageSize,
deptId: tablePage.value.deptId
}
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 UserApi.getUserPage(searchObj)
tableData.value = data.list.map((item) => {
item.deptInfoList = item.deptInfoList.map((dept) => {
keyList.forEach(({ value }) => {
if (value === 'deptName') return
if (dept[value]?.length) dept[value] = dept[value].map((info) => info.name).join('、')
else dept[value] = ''
})
return dept
})
return item
})
tablePage.value.total = data.total
} finally {
loading.value = false
}
}
useCrudHeight(crudRef)
/** 搜索按钮操作 */
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) => {
let bool = true
if (['edit', 'view'].includes(type) && tableForm.value.id) {
loading.value = true
const currData = await UserApi.getUser(tableForm.value.id)
if (currData.rankInfoList?.length) {
currData.rankIds = currData.rankInfoList.map((item) => item.id)
}
delete currData.rankInfoList
if (currData.deptInfoList?.length) {
const deptObj = {}
currData.deptInfoList = currData.deptInfoList.map((dept) => {
keyList.forEach(({ value }) => {
dept.deptId = dept.deptId + ''
if (value === 'deptName') deptObj[dept.deptId] = dept[value]
else {
const key = value.split('InfoList')[0] + 'Ids'
dept[key] = dept[value].map((info) => info.id)
}
delete dept[value]
})
if (type == 'edit') dept.$cellEdit = true
return dept
})
lowStore.setDicObj('deptSelect', deptObj)
} else {
currData.deptInfoList = cloneDeep(tableOption.value.group[1].column.deptInfoList?.value)
}
if (currData.supAdminFlag || currData.tenantAdminFlag) {
delete currData.deptInfoList
bool = false
}
tableForm.value = currData
loading.value = false
}
tableOption.value.group[1].display = bool
done()
}
const verifydeptTable = () => {
return new Promise((resolve) => {
deptCrudRef.value.validateCellForm().then((res) => {
if (res) {
const dom = document.querySelector('.system-user-form .el-dialog__body')
if (dom) dom.scrollTop = dom.scrollHeight
}
resolve(!res)
})
})
}
/** 新增操作 */
const rowSave = async (form, done, loading) => {
const verify = await verifydeptTable()
if (!verify) return loading()
let bool = await UserApi.createUser(form).catch(() => false)
if (bool) {
message.success(t('common.createSuccess'))
resetChange()
done()
} else loading()
}
/** 编辑操作 */
const rowUpdate = async (form, index, done, loading) => {
const verify = await verifydeptTable()
if (!verify) return loading()
delete form.password
if (form.supAdminFlag || form.tenantAdminFlag) delete form.deptInfoList
let bool = await UserApi.updateUser(form).catch(() => false)
if (bool) {
message.success(t('common.updateSuccess'))
getTableData()
done()
} else loading()
}
/** 操作分发 */
const handleCommand = (command: string, row: UserApi.UserVO) => {
switch (command) {
case 'handleDelete':
rowDel(row)
break
case 'handleResetPwd':
handleResetPwd(row)
break
case 'handleRole':
handleRole(row)
break
default:
break
}
}
/** 删除按钮操作 */
const rowDel = async (form) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await UserApi.deleteUser(form.id)
message.success(t('common.delSuccess'))
// 刷新列表
await getTableData()
} catch {}
}
/** 重置密码 */
const handleResetPwd = async (row: UserApi.UserVO) => {
try {
// 重置的二次确认
const result = await message.prompt(
'请输入"' + row.username + '"的新密码',
t('common.reminder')
)
const password = result.value
// 发起重置
await UserApi.resetUserPwd(row.id, password)
message.success('修改成功,新密码是:' + password)
} catch {}
}
/** 分配角色 */
const assignRoleFormRef = ref()
const handleRole = (row: UserApi.UserVO) => {
assignRoleFormRef.value.open(row)
}
/** 修改用户状态 */
const handleStatusChange = async (row: UserApi.UserVO) => {
try {
// 修改状态的二次确认
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
await message.confirm('确认要"' + text + '""' + row.username + '"用户吗?')
// 发起修改状态
await UserApi.updateUserStatus(row.id, row.status)
// 刷新列表
await getTableData()
} catch {
// 取消后,进行恢复按钮
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 用户导入 */
const importFormRef = ref()
const handleImport = () => {
importFormRef.value.open()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
let searchObj = { ...tableSearch.value }
if (tablePage.value.deptId) {
searchObj.deptId = tablePage.value.deptId
}
for (let key in searchObj) if (searchObj[key] === '') delete searchObj[key]
const data = await UserApi.exportUser(searchObj)
download.excel(data, '用户管理列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
const delDeptTableRow = (index) => {
tableForm.value.deptInfoList.splice(index, 1)
}
onMounted(async () => {
DeptApi.getSimpleDeptList().then((res) => {
const tree = handleTree(res)
treeData.value = tree
})
getTableData()
})
</script>
<style lang="scss" scoped>
.el-select {
width: 100%;
}
.el-dropdown {
padding: 4px 2px;
}
</style>
<style lang="scss">
.system-user-form {
.el-dialog__body {
padding-top: 0 !important;
}
.avue-crud {
.avue-crud__empty {
padding: 0;
.el-empty {
padding: 0;
}
.el-empty__description {
margin: 0;
}
}
}
}
</style>